我们在学习进程间通信的时候首先要知道在这一节的考点:
【面试】进程间通信有哪些方式
【面试】匿名管道和命名管道有什么区别
【面试】在进程间通信里面哪一种方式的进程间通信是最快的,为什么
一、进程间通信的介绍
1.什么是进程间通信
进程间通信就是简称间进行交流
2.为什么要进行简称间通信
我们在做一个大一点的项目的时候需要模块化,协同运行,这个时候需要做到:
(1)数据传输:一个进程要将数据出书给另外一个进程
(2)资源共享:多个进程之间要进行资源的共享
(3)通知事件:一个进程要给另外一个进程发送消息
(4)进程控制:有时候需要一个 进程控制另外一个进程
3.为什么进程间通信需要操作系统提供接口:因为进程的独立性,所以它们之间是没有联系的,唯一的联系就是都受到操作系统的控制所以需要通过操作系统提供接口来进行进程间的通信。
4.操作系统如何能让进程进行进程间通信
给多个进程提供一个公共的都能访问到的媒介。
5.操作系统因为提供进程间通信的使用场景不同所以提供的进程间通信的方式也有多种,它们各自有各自的特点。
【面试】6.进程间通信的方式
6.1进程间通信的方式种类
6.2对通信方式的讲解
6.2.1管道
在进行管道的讲解之前我们先来认识一下用户态和内核态的区别:
用户态:程序运行的功能不是操作系统内部的功能则是运行在用户态。
内核态:程序运行在操作系统内部时运行在内核态。
6.2.1.1概念:用于在进程间传输数据资源,它的本质就是内核的一个缓冲区。
6.2.1.2管道的特性
6.2.1.3分类
匿名管道
命名管道
6.2.1.3.1我们先来讲解匿名管道
a.匿名管道的概念:创建的缓冲区没有标识,因此只能用于有亲缘关系的进程间通信,创建一个管道,返回文件描述符,这时候创建一个子进程,子进程就会复制父进程的pcb,就会将pcb中关于管道的信息复制过来,所以子进程和父进程指向内核缓冲区的描述符是相同的,就可以进行进程间通信了。
b.匿名管道的特性
c.匿名管道的创建
我们通过一个实例的代码来看匿名管道的创建以及匿名管道的基本使用:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main(){
//int pipe(int pipefd[2])——>这个是创建管道的函数
//pipefd:用于获取管道的操作描述符
//pipefd[0]:用于从管道中读取数据
//piped[1]:用于向管道中写入数据
//返回值:成功——>0 失败——>-1
int pipefd[2];
int ret=pipe(pipefd[2]);//创建管道
if(ret<0){
perror("pipe error");
return -1;//失败返回-1
}
int pid=fork();//创建子进程
if(pid<0){//创建子进程失败
return -1;
}else if(pid==0){
//子进程--读取管道中的数据
char buff[1024]={0};
read(pipefd[0],buff,1023);
printf("buff:%s\n",buff);
}else{
//父进程--向管道写数据
char *ptr="hot!===sleep";
write(pipefd[1],str,strlen(ptr));
}
close(pipefd[o]);
close(pipefd[1]);
return 0;
}
程序理解的图解:
注意:子进程应在管道创建之后创建,因为如果在之前pcb中没有关于管道的信息。
d.匿名管道符的实现
//练习:
在我们的Linux中ls命令就是匿名管道符的实现
下面我们通过ls自己来实现匿名管道符
ls -l | grep Makefile
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<errno.h>
5 int main(){
6 int pid1,pid2;
7 int pipefd[2];
8 pipe(pipefd[2]);//实现管道的创建
9 if((pid1=fork()==0)){
10 //实现ls -l
11 close(pipefd[0]);
12 dup2(pipefd[1],1);//将标准输出重定向到管道写入端
13 execlp("ls","ls","-l",NULL);
14 }
15 if((pid2=fork()==0)){
16 //实现grep Makefile
17 close(pipefd[1]);
18 dup2(pipefd[0],2);//将标准输入重定向到管道读取端,那么就可以在管道中读取进去数据
19 execlp("grep","grep","Makefile",NULL);
}
21 //下面的是父进程的操作,也需要我们将管道的读写端关闭
22 close(pipefd[0]);
23 close(pipefd[1]);
24 waitpie(pid1,NULL,0);
25 waitpie(pid2,NULL,0);
26 return 0;
27 }
6.2.1.3.2命名管道
(1)概念:可见于文件系统,因此所有的进程都可以通过打开文件获取到内核管道这块缓冲区所对应的描述符,因此命名管道可以用于同一机器上面任意的进程。
(2)命名管道的基本使用
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/stat.h>
5 #include<string.h>
6 #include<fcntl.h>
7 #include<errno.h>
8 int main(){
9
10 int ret=mkfifo("./test.fifo",0664);
11 if(ret<0){
12 if(errno!=EEXIST){
13 perror("mikfifo error");
14 return -1;
15 }
16 }
17 int fd=open("./test.fifo",O_RDONLY);
18 if(fd<0){
19 perror("open error");
20 return -1;
21 }
22 printf("open fifo success");
23 while(1){
24 char buff[1024]={0};
25 read(fd,buff,1023);
26 printf("read buff:[%s]\n",buff);
27 }
28 close(fd);
29 reutrn 0;
30 }
命名管道和匿名管道的各种特性都基本相同(不包括情缘关系);
下面我们来看一下命名管道的文件打开特性:
如果管道文件只读打开,将阻塞直到这个文件被以写的方式打开;
如果管道文件只写打开,将阻塞直到这个文件被以读的方式打开;
如果管道文件以读写的方式打开将不会被阻塞。
6.2.2共享内存
1.我们先来看共享内存的原理:它是进程间通信方式最快的一种!!!,多个进程可以对一个物理内存进行操作,它用于数据共享。
原理图:
2.【面试题】为什么共享内存是进程间通信最快的一种?
答:首先我们来看一下管道通信方式的原理:它通过父进程将数据从用户空间写到内核空间,这是第一次拷贝,然后子进程再从内核空间读取数据,这是第二次数据的拷贝;再来看我们的共享内存通信方式:它是直接将虚拟地址与我们的物理地址映射起来,这样的话比起我们的管道它就少了用户空间和内核空间两次的数据拷贝,自然它就成为了最快的。
3.共享内存操作
(1)操作步骤:
(2)共享内存的实现
/*
* 创建:shmget
* 映射:shmat
* 操作:内存操作即可
* 解除:shmdt
* 删除:shmctl
* /
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/shm.h>
5 #define IPC_KEY 0X12345678
6 int main(){
7 //(1)创建共享内存
8 //int shmget(key_t key,size_t size,int shmflg);
9 //key:共享内存操作系统中标识符
10 //size:共享内存大小
11 //shmlg:IPC_CREAT IPC_EXCL
12 //返回值:表示进程共享内存的操作句柄,如果失败就返回-1
13 int shmid=shmget(IPC_KEY,32,IPC_CREAT | 0664);
14 if(shmid<0){
15 perror("shmget error");
E> 16 reurn -1;
17 }
18
19 //(2)将共享内存映射到虚拟地址空间
20 //void *shmat(int shmid,const void *shmaddr,int shmflg);
21 //shmid:共享内存的操作句柄
22 //shmaddr:用户指定共享内存在虚拟地址空间的首地址,通常置空,由操作系统设置
23 //shmflg:如果被指定为SHM_RDONLY--只读--0
24 //返回值:表示映射的首地址,失败返回-1
25 void *shm_start=shmat(shmid,NULL,0);
26 if(shm_start==(void*)-1){
27 perror("shmat error");
28 return -1;
29 }
30 while(1){
31 printf("please input:");
32 fflush(stdout);
33 scanf("%s",shm_start);
34 }
35
36 //(3)解除映射
37 //int shmdt(const void *shmaddr);
38 //shmaddr:映射首地址
39 shmdt(shm_start);
40
41 //(4)删除共享内存
42 //int shmctl(int shmid,int cmd,struct shmid_ds *buf);
43 //shmid:映射首地址
44 //cmd:对共享内存的操作
45 //buf:用于获取共享内存的状态信息
46 //如果删除共享内存并不会直接删除,而是等到映射链接数为0,并且在等待期间拒绝新的映射链>
接
47 return 0 ;
48 }
6.2.3消息队列(现在已经基本不使用了)
原理:消息队列就是内核创建的一个队列,它用来实现进程间的数据传输,消息队列传输的是一个个的数据块,因为向队列中添加的就是数据块,特别的是这些数据块都是有类型(这个类型就是可以用来区分优先级、区分进程、区分数据功能)的数据块(为了保证进程取来的数据不是自己的数据)。
6.2.4信号量
1.原理
信号量就相当于是一个计数器,它用于对资源进行计数,并没有进行什么通信。
获取一个资源,计数减1,如果没有资源,计数为0,为了获取资源一直等待;
回收一个资源,计数加1,通知(唤醒)死等的进程。
2.功能
它的功能就是等待与唤醒,用于实现进程间的同步与互斥。具体的这个同步与互斥的实现: