进程间通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程资源共享:多个进程之间共享同样的资源。通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事(如进程终止时要通知父进程)进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。
一、管道
作用:实现资源数据的传输
特性:半双工通信(同一时间内不能同时发送又接受,一条管道只能一个进程写,一个进程读,一个进程写完后,另一个进程开始读),可以选择方向的单向通信
本质:管道的本质是内核中的一块缓冲区(内存,由系统进行管理)
1.匿名管道
匿名管道:管道没有标识符,不能被其他进程所找到,适用于具有亲缘关系的进程间通信(例如褥子进程)
实现原理:父子进程对文件进行读写操作,父子进程在文件内核缓冲区中写入或读出数据,从而实现通信。
接口:
int pipe(int pipefd[2]);
功能:创建一个管道,通过参数返回管道的两个操作句柄(一个是写入端的描述符,一个是输出端的描述符,可以通过这两个描述符往管道写入或者读取数据。)
参数:
pipefd[2]---具有两个整形元素的数组
pipefd[0]---向管道读取数据
pipefd[1]---向管道写入数据
返回值:
成功返回0,失败返回-1
站在文件描述符角度-深度理解管道
我们知道在Linux下,一切皆为文件,所以也可以将管道当做文件来处理
代码实现:
父进程写入数据,子进程读出
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<fcntl.h>
6 #include<sys/wait.h>
7
8 int main()
9 {
10 int pipefd[2];
11 //父进程创建管道
12 int ret=pipe(pipefd);
13 //创建失败,返回-1
14 if(ret<0)
15 {
16 perror("pipe error");
17 return -1;
18 }
19 char*data="I am father";
20 //父进程向管道的写端写入数据
21 write(pipefd[1],data,strlen(data));
22 //创建子进程
23 pid_t pid1=fork();
24 if(pid1==0)
25 {
26 char buf[1024]={0};
27 //子进程从管道的读端读取strlen(data)个字节的数据到buf中
28 read(pipefd[0],buf,strlen(data));
29 printf("子进程接收到父进程的数据:%s\n",buf);
30 exit(0);
31 }
32 //等待子进程退出
33 wait(NULL);
34 return 0;
35 }
匿名管道特点:
管道读写原则:
1.管道中如果没有数据,则read从管道中读取数据会被阻塞,直到有数据了,才会读取数据返回
2.如果管道中数据满了,则write继续向管道中写入数据则会被阻塞,直到管道中有空间才可以继续写入
3.管道的所有读端被关闭,则再向管道中写入数据,会导致程序崩溃退出(所有读端被关闭,已经没有进程再去从管道读端读取数据,此时在进行写入已经没有意义)
4.管道的所有写端被关闭,则read从管道中读取完所有数据后,将不会被阻塞,直接返回0
2.命名管道
本质:内核中的一块缓冲区,但是有名字,能够被其他进程找到,因此适用于同一主机上的任意进程间通信(多个进程用同一个标识符访问同一个管道)
标识符:是一个可见于文件系统的管道文件
接口:int mkfifo(char*pathname,mode_t mode);
pathname: 管道文件名称
node: 管道文件的访问权限
返回值:成功返回0,失败返回-1
原理:
一个进程创建了一个命名管道的名字,多个进程通过相同的管道名字,打开同一个管道,访问同一块内核缓冲区
注意:管道文件的创建,并不涉及缓冲区的创建,管道文件时标识符,是一直存在的,但这个管道有没有被其他进程用来通信是不一定的,如果管道一直没有进程通信,一直维护缓冲区就很不划算,所以,只有当进行进程通信的时候,才会开辟缓冲区
代码实现:
read_fifo.c
1 #include<stdio.h>
2 #include<string.h>
3 #include<stdlib.h>
4 #include<fcntl.h>
5 #include<unistd.h>
6 #include<sys/stat.h>
7 #include<errno.h>
8
9 //命名管道
10 int main()
11 {
12 umask(0);
13 //mkfifo函数创建命名管道
14 int ret=mkfifo("./test.fifo",0664);
15
16 if(ret<0&&errno!=EEXIST)
17 {
18 perror("mkfifo error");
19 return -1;
20 }
21 //以只读的方式打开管道文件./test.fifo
22 int fd=open("./test.fifo",O_RDONLY);
23 if(fd<0)
24 {
25 perror("open error");
26 return -1;
27 }
28
29 while(1)
30 {
31 char buf[1024]={0};
32 //将管道文件./test.fifo中的数据读入到buf中
33 int ret=read(fd,buf,1023);
34 if(ret<0)
35 {
36 //如果读入数据失败,则关闭文件并返回
37 perror("read error");
38 close(fd);
39 return -1;
40 }
41 //read函数返回实际读出的字节数
42 //如果管道文件中没有数据,则说明read函数读出字节数为0,也就是返回值ret等于0
43 else if(ret==0)
44 {
45 printf("所有写端已经关闭\n");
46 close(fd);
47 return -1;
48 }
49 //读取到数据,打印数据
50 printf("%s\n",buf);
51 }
52 close(fd);
53 return 0;
54 }
write_fifo.c
1 #include<stdio.h>
2 #include<string.h>
3 #include<stdlib.h>
4 #include<fcntl.h>
5 #include<unistd.h>
6 #include<sys/stat.h>
7 #include<errno.h>
8
9 //命名管道
10 int main()
11 {
12 umask(0);
13 //mkfifo函数创建命名管道
14 int ret=mkfifo("./test.fifo",0664);
15
16 if(ret<0&&errno!=EEXIST)
17 {
18 perror("mkfifo error");
19 return -1;
20 }
21 //以只写的方式打开管道文件./test.fifo
22 int fd=open("./test.fifo",O_WRONLY);
23 if(fd<0)
24 {
25 perror("open error");
26 return -1;
27 }
28
29 while(1)
30 {
31 char buf[1024]={0};
32 scanf("%s",buf);
33 //将buf中的数据写入文件./test.fifo中
34 int ret=write(fd,buf,strlen(buf));
35 if(ret<0)
36 {
37 //如果写入数据失败,则关闭文件并返回
38 perror("write error");
39 close(fd);
40 return -1;
41 }
42 }
43 close(fd);
44 return 0;
45 }
此处,输出没有空格原因是用scanf输入的字符串,遇到空格结束
命名管道特性:
具备上述匿名管道的特性
如果以只读的方式打开管道文件,则会阻塞,直到管道被任意进程以写的方式打开
如果以只写的方式打开管道文件,则会阻塞,直到管道被任意进程以读端方式打开
因为一个管道如果不具备同时读写,就没必要开辟缓冲区
二、共享内存
作用:用于多个进程间的数据共享
原理:开辟出一块物理内存,然后多个进程将这块物理内存映射到自己的虚拟地址空间中,这些进程通过虚拟地址直接访问这块物理内存中的数据
特性:共享内存是最快的进程间通信方式(共享内存的操作相对于其他进程间通信方式,少了俩次数据拷贝过程
接口:
创建共享内存
int shmget(key_t key,size_t size,int shmflg);
key:共享内存的标识符
size:要创建共享内存的大小,最好是页面大小的整数倍
shmflg:IPC_CREAT|IPC_EXCL|0664
IPC|CREAT----如果共享内存不存在,则创建打开,如果已经存在则直接打开
IPC|EXCL-----与IPC|CREAT搭配使用,共享内存不存在则创建打开,若存在则报错返回
mode_flags---共享内存访问权限
返回值:成功返回一个操作句柄(非负数),失败返回-1
映射共享内存
void*shmat(int shmid,void*addr,int shmflag);
shmid:shmget打开共享内存时返回的操作句柄
addr:映射首地址,通常设置为NULL,让操作系统自动分配
shmflag:默认为0,表示可读可写,
返回值:成功返回映射的首地址,失败返回-1
解除映射
int shmdt(const void*shmaddr);
shmaddr:shmat函数的返回值,映射的首地址
删除共享内存
int shmctl(int shmid,int cmd,stuct shmid_ds*buf);
shmid:是shmget()函数返回的操作句柄
cmd:是对共享内存进行的操作
IPC_RMID:标记一个共享内存段要被删除
buf:用于设置或者获取共享内存信息,当cmd是IPC_RMID时被忽略
代码实现:
shmread.c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/shm.h>
6
7 #define SHM_KEY 0x12345678
8 int main()
9 {
10 //创建一块共享内存,打开成功返回操作句柄,失败返回-1
11 int shmid=shmget(SHM_KEY,4096,IPC_CREAT|0664);
12 if(shmid<0)
13 {
14 perror("shmget error");
15 return -1;
16 }
17
18 //映射共享内存到当前进程的虚拟地址空间
19 void *start=shmat(shmid,NULL,0); //0表示可读可写
20 if(start==(void*)-1)
21 {
22 perror("shmat error");
23 return -1;
24 }
25 while(1)
26 {
27 //从共享内存中读出数据,但不会删除共享内存中的数据,共享内存不会随着进程的结束而被删除
28 printf("%s\n",start);
29 sleep(1);
30 }
31 //解除映射关系
32 shmdt(start);
33 //删除共享内存
34 shmctl(shmid,IPC_RMID,NULL);
35 return 0;
36 }
shmwrite.c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/shm.h>
6
7 #define SHM_KEY 0x12345678
8 int main()
9 {
10 //创建一块共享内存,打开成功返回操作句柄,失败返回-1
11 int shmid=shmget(SHM_KEY,4096,IPC_CREAT|0664);
12 if(shmid<0)
13 {
14 perror("shmget error");
15 return -1;
16 }
17
18 //映射共享内存到当前进程的虚拟地址空间
19 void *start=shmat(shmid,NULL,0); //0表示可读可写
20 if(start==(void*)-1)
21 {
22 perror("shmat error");
23 return -1;
24 }
25 int count=0;
26 while(1)
27 {
28 //从共享内存中读出数据,但不会删除共享内存中的数据,共享内存不会随着进程的结束而被删除
29 sprintf(start,"I am process A +%d\n",count++);
30 sleep(1);
31 }
32 //解除映射关系
33 shmdt(start);
34 //删除共享内存
35 shmctl(shmid,IPC_RMID,NULL);
36
从运行结果我们可以看出,当停止进程shmwrite后,此时共享内存中的数据不会被修改,所以shmread进程一直打印相同的内容,当我们将进程shmread也退出去之后,共享内存也不会被删除,只是映射连接数为0,所以共享内存不会随着进程的退出而退出
特性:
1.最快的通信方式
2.生命周期随内核(不会随着进程的退出而被释放)
三、消息队列
功能:实现进程间的数据传输(适用于频繁的交换数据)
本质:内核中的一个优先级队列
实现原理:A进程往消息队列写入数据后就可以正常返回,B进程需要时再去读取就可以了,效率比较高。
特性:
生命周期随内核
自带互斥与同步
传输是一种数据块的传输
四、信号量
作用:用于实现进程的同步与互斥
本质:计数器+pcb等待队列
实现原理:
1.信号量实现互斥
进程互斥:多个进程在运行的过程中,都需要某一个资源时,他们便产生的竞争关系,当一个进程获取到资源时,其他进程只能等待,只有当这个进程访问资源结束释放资源后,其他进程才可以访问
初始化信号量为1,表示资源只有1个
P操作:先读计数器值进行-1,如果-1之后的值大于等于0,说明此时有资源,可以访问;如果小于0,说明没有资源访问,被阻塞
V操作:先将计数器的值进行+1,如果+1之后的值小于等于0,说明此时有进程在等待,此时释放资源,唤醒一个正在等待的进程;若+1之后值大于0,则说明此时无人在等待队列,直接释放资源即可
举例:
进程A在访问临界资源之前,首先进行P操作,因为此时计数器值为1,进行-1之后大于等于0,说明此时临界资源无人占用,A进程可以访问
当A进程正在访问临界资源时,B进程此时也想访问,则在访问之前,进行P操作,此时计数器的值为0,进行-1之后值小于0,说明此时有人占用临界资源,则B进程无法访问,被阻塞到队列中等待
当A进程访问完临界资源后,进行V操作,此时计数器的值为-1,进行+1之后值小于等于0,说明此时有进程在等待队列中等待,此时,A进程释放资源,同时唤醒队列中的第一个进程
2.信号量实现同步
进程同步:多个进程中发生的一些事件存在某种时序关系,必须协同动作共同完成一个任务。
当俩个进程A,B同时运行,进程A需要进程B运行到某一步的结果,才能进行自己的下一步运行,这个时候就需要等待B进程与自己进行通信,发送一个消息或者信号,才能继续往下执行,这种进程间的协作称作为进程同步
我们知道,因为进程并发,所以进程的执行顺序可能不一样,而进程同步就是要让各并发进程按要求有序地推进
具体例子: