进程间通信
进程间通信的作用
由于进程独立性的存在,两个进程想要直接交换数据是非常的困难的,所以需要进程间通信来解决进程与进程之间交换数据的问题
目前最大的进程间通信方式:网络
管道
匿名管道
命令感受匿名管道
这一个 “丨” 就是管道
作用:将 ps aux 命令的输出结果通过管道输入 grep 并作为 grep 的输入参数
从内核角度去解释匿名管道
管道就是内核当中的一块缓冲区(一块内存),进程A和进程B可以通过这个缓冲区进行交换数据
代码创建匿名管道
int pipe(int pipefd[2]);
参数:
pipefd:类型为整型数组,有两个元素,pipefd[0],pipefd[1]
pipefd[0],pipefd[1]:当中保存的是一个文件描述符
pipefd[0]:对应的文件描述符可以从管道当中进行读,不能写
pipefd[1]:对应的文件描述符可以往管道当中写,但不能读
pipefd[0],pipefd[1]当中的值是pipe函数进行赋值的,直白的说,当我们调用pipe函数的时候,只需要
给pipe函数传递一个拥有两个元素的整型数据的数组,pipe函数在创建完毕管道后,会给pipefd[0],pipefd[1]进行赋值
返回值:
-1:创建失败
0:创建成功
闪烁有两种情况:
1、软链接指向的源文件被删除
2、软链接指向的是一块内存,而不是一个具体的文件
从PCB角度去分析匿名管道
1、匿名管道只适用于具有亲缘关系的进程,进行进程间通信
2、先创建管道,再创建子进程,父子进程才可以进行进程间通信
3、如果想要两个子进程使用匿名管道进行进程间通信,需要先创建管道,再创建子进程
4.管道的数据只能从写端流向读端,这是一种半双工的通信方式
全双工通信:数据可以从A端流向B端,也可以从B端流向A端
5、通过fd[0]从管道当中读取数据的时候,是将数据读走了(并不是拷贝了一份)
6、从管道当中去读数据的时候,可以指定读取任意大小的数据,如果管道当中没有数据,默认情况下,进行读则会阻塞
7、多次写入的数据之间是没有明显的分界的,上一条数据的末尾连接下一条数据的开头位置
8、匿名管道的生命周期跟随进程
匿名管道的非阻塞读写特性
非阻塞:
fcntl函数:设置/获取文件描述符的属性
int fcntl(int fd, int cmd, ...);
cmd 决定了 fcntl 函数究竟做什么事情
F_GETFL:获取文件描述符的属性,可变参数列表就可以不用传递任何值
F_SETFL:设置文件描述符的属性,需要制定设置文件描述符的属性,采用按位或的方式
非阻塞属性:O_NONBLOCK
返回值:
如果是获取(F_GETFL),返回文件描述符的属性
创建管道,获取管道读写两端文件描述符的属性
1 #include <stdio.h>
2 #include <fcntl.h>
3 #include <unistd.h>
4
5 int main()
6 {
7 int fd[2];
8 int ret = pipe(fd);
9 if(ret < 0)
10 {
11 perror("pipe fail\n");
12 return 0;
13 }
14
15 //获取读端的文件描述符属性
16 int flag = fcntl(fd[0],F_GETFL);
17 printf("flag fd[0]: %d\n",flag);
18 //获取写端的文件描述符属性
19 flag = fcntl(fd[1],F_GETFL);
20 printf("flag fd[1]: %d\n",flag);
21
22 return 0;
23 }
给读写两端的文件描述符设置非阻塞属性
首先是读端
1 #include <stdio.h>
2 #include <fcntl.h>
3 #include <unistd.h>
4
5 int main()
6 {
7 int fd[2];
8 int ret = pipe(fd);
9 if(ret < 0)
10 {
11 perror("pipe fail\n");
12 return 0;
13 }
14
15 int flag = fcntl(fd[0],F_GETFL);
16 printf("flag fd[0]: %d\n",flag);
17
18 fcntl(fd[0], F_SETFL, flag | O_NONBLOCK);
19
20 flag = fcntl(fd[0],F_GETFL);
21 printf("flag fd[0]: %d\n",flag);
22
23 return 0;
24 }
随后是写端
1 #include <stdio.h>
2 #include <fcntl.h>
3 #include <unistd.h>
4
5 int main()
6 {
7 int fd[2];
8 int ret = pipe(fd);
9 if(ret < 0)
10 {
11 perror("pipe fail\n");
12 return 0;
13 }
14
15 int flag = fcntl(fd[1],F_GETFL);
16 printf("flag fd[0]: %d\n",flag);
17
18 fcntl(fd[1], F_SETFL, flag | O_NONBLOCK);
19
20 flag = fcntl(fd[1],F_GETFL);
21 printf("flag fd[0]: %d\n",flag);
22
23 return 0;
24 }
测试非阻塞
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <fcntl.h>
4
5 /* 1、创建匿名管道,之后创建子进程,让子进程进行进程间通信
6 * 2、因为父子进程当中的文件描述符表都是拥有fd[0],fd[1],规定父进程读,子进程写
7 * 3、再测试非阻塞属性*/
8
9 void SetNonBlock(int fd)//提供一个函数给对应的fd[x]加上非阻塞属性
10 {
11 int flag = fcntl(fd, F_GETFL);
12 fcntl(fd, F_SETFL, flag | O_NONBLOCK);
13 }
14
15 int main()
16 {
17 int fd[2];
18 int ret = pipe(fd);
19 if(ret < 0)
20 {
21 perror("pipe fail");
22 return 0;
23 }
24
25 ret = fork();
26 if(ret < 0)
27 {
28 perror("fork fail");
29 return 0;
30 }
31 else if(ret == 0)
32 {
33 //child
34 close(fd[0]);//关闭读端,只留下写端
35 SetNonBlock(fd[1]);
36 }
37 else
38 {
39 //father
40 close(fd[1]);//关闭写端,只留下读端
41 SetNonBlock(fd[0]);
42 }
43 return 0;
44 }
读端进行读(非阻塞),写端不写(不操作)
1、写端不关闭
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <fcntl.h>
4
5 void SetNonBlock(int fd)
6 {
7 int flag = fcntl(fd, F_GETFL);
8 fcntl(fd, F_SETFL, flag | O_NONBLOCK);
9 }
10
11 int main()
12 {
13 int fd[2];
14 int ret = pipe(fd);
15
16 if(ret < 0)
17 {
18 perror("pipe fail");
19 return 0;
20 }
21
22 ret = fork();
23
24 if(ret < 0)
25 {
26 perror("fork fail");
27 return 0;
28 }
29
30 else if(ret == 0)
31 {
32 //child
33 close(fd[0]);//关闭读端,只留下写端
34 SetNonBlock(fd[1]);
35
36 //写端不关闭
37 while(1)
38 {
39 sleep(1);
40 }
41 }
42
43 else
44 {
45 //father
46 close(fd[1]);//关闭写端,只留下读端
47 SetNonBlock(fd[0]);
48
49 char buf[1024] = {0};
50 int read_size = read(fd[0], buf, sizeof(buf) - 1);
51
52 while(1)
53 {
54 printf("read_size : %d\nbuf : %s\n", read_size, buf);
55 }
56 }
57
58 return 0;
59 }
但是此时无法确定读端调用 read,read 函数返回-1,是因为管道当中没有内容还是由于调用函数出错表示的,因此需要更改
44 else
45 {
46 //father
47 close(fd[1]);//关闭写端,只留下读端
48 SetNonBlock(fd[0]);
49
50 while(1)
51 {
52 char buf[1024] = {0};
53 int read_size = read(fd[0], buf, sizeof(buf) - 1);
54
55 if(read_size < 0)
56 {
57 if(errno == EAGAIN)
58 {
59 printf("管道为空\n");
60 printf("read_size : %d\nbuf : %s\n", read_size, buf);
61 }
62 }
63 }
64 }
此时需要额外包一个头文件
#include <errno.h>
再运行一次
如果错误码为 EAGAIN ,应该认为是正常情况
2、写端关闭
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <fcntl.h>
4 #include <errno.h>
5
6 void SetNonBlock(int fd)
7 {
8 int flag = fcntl(fd, F_GETFL);
9 fcntl(fd, F_SETFL, flag | O_NONBLOCK);
10 }
11
12 int main()
13 {
14 int fd[2];
15 int ret = pipe(fd);
16
17 if(ret < 0)
18 {
19 perror("pipe fail");
20 return 0;
21 }
22
23 ret = fork();
24
25 if(ret < 0)
26 {
27 perror("fork fail");
28 return 0;
29 }
30
31 else if(ret == 0)
32 {
33 //child
34 close(fd[0]);//关闭读端,只留下写端
35 close(fd[1]);//写端关闭
36
37 while(1)
38 {
39 sleep(1);
40 }
41 }
42
43 else
44 {
45 //father
46 close(fd[1]);//关闭写端,只留下读端
47 SetNonBlock(fd[0]);
48
49 char buf[1024] = {0};
50 int read_size = read(fd[0], buf, sizeof(buf) - 1);
51 printf("read_size : %d, buf = %s\n", read_size, buf);
52 }
53
54 return 0;
55 }
调用 read 返回 -1
写端非阻塞进行写
1、读端关闭
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <fcntl.h>
4 #include <errno.h>
5
6 void SetNonBlock(int fd)
7 {
8 int flag = fcntl(fd, F_GETFL);
9 fcntl(fd, F_SETFL, flag | O_NONBLOCK);
10 }
11
12 int main()
13 {
14 int fd[2];
15 int ret = pipe(fd);
16
17 if(ret < 0)
18 {
19 perror("pipe fail");
20 return 0;
21 }
22
23 ret = fork();
24
25 if(ret < 0)
26 {
27 perror("fork fail");
28 return 0;
29 }
30
31 else if(ret == 0)
32 {
33 //child
34 close(fd[0]);//关闭读端,只留下写端
35
36 int count = 0;
37 while(1)
38 {
39 write(fd[1], "a", 1);
40 printf("count : %d\n", count++);
41 }
42 }
43
44 else
45 {
46 //father
47 close(fd[1]);//关闭写端,只留下读端
48 close(fd[0]);//关闭读端
49
50 while(1)
51 {
52 sleep(1);
53 }
54 }
55 return 0;
56 }
因为此时读端已经被关闭了,而写端在进行写入,就好比一个水管不停的往里边输水,但是把出水口堵住,最终水管会破裂,也就导致了现在的僵尸进程
此时加上非阻塞
31 else if(ret == 0)
32 {
33 //child
34 close(fd[0]);//关闭读端,只留下写端
35 SetNonBlock(fd[1]);
36
37 int count = 0;
38 while(1)
39 {
40 write(fd[1], "a", 1);
41 printf("count : %d\n", count++);
42 }
43 }
可以看到还是一样的情况,都是僵尸进程
即当前在通过 fd[1] 往管道当中去写的时候,会导致管道破裂,调用写的进程会被终止(信号终止)
2、读端不关闭
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <fcntl.h>
4 #include <errno.h>
5
6 void SetNonBlock(int fd)
7 {
8 int flag = fcntl(fd, F_GETFL);
9 fcntl(fd, F_SETFL, flag | O_NONBLOCK);
10 }
11
12 int main()
13 {
14 int fd[2];
15 int ret = pipe(fd);
16
17 if(ret < 0)
18 {
19 perror("pipe fail");
20 return 0;
21 }
22
23 ret = fork();
24
25 if(ret < 0)
26 {
27 perror("fork fail");
28 return 0;
29 }
30
31 else if(ret == 0)
32 {
33 //child
34 close(fd[0]);//关闭读端,只留下写端
35 SetNonBlock(fd[1]);
36
37 int count = 0;
38 while(1)
39 {
40 write(fd[1], "a", 1);
41 printf("count : %d\n", count++);
42 }
43 }
44
45 else
46 {
47 //father
48 close(fd[1]);//关闭写端,只留下读端
49 // close(fd[0]);不关闭读端
50
51 while(1)
52 {
53 sleep(1);
54 }
55 }
56 return 0;
57 }
可以看到现在就不断的往里边写了
但是数值还是有点问题,于是修改一下
31 else if(ret == 0)
32 {
33 //child
34 close(fd[0]);//关闭读端,只留下写端
35 SetNonBlock(fd[1]);
36
37 int count = 0;
38 while(1)
39 {
40 int write_size = write(fd[1], "a", 1);
41
42 if(write_size < 0)
43 {
44 printf("write_size: %d\n",write_size);
45 if(errno == EAGAIN)
46 {
47 printf("管道已满\n");
48 break;
49 }
50 }
51 printf("count : %d\n", count++);
52 }
命名管道
原理
也是在内核当中开辟了一块缓冲区,这块缓冲区是有标识符,可以被任何进程通过标识符找到
创建命名管道及使用命名管道进行进程间通信
命令创建:mkfifo
p 代表的是管道文件
写:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <fcntl.h>
4
5 int main()
6 {
7 int fd = open("./fifo_test", O_RDWR);
8 if(fd < 0)
9 {
10 perror("open fail\n");
11 return 0;
12 }
13
14 while(1)
15 {
16 write(fd, "oulaoula", 8);
17 sleep(1);
18 }
19 close(fd);
20 return 0;
21 }
读:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <fcntl.h>
4
5 int main()
6 {
7 int fd = open("./fifo_test", O_RDWR);
8 if(fd < 0)
9 {
10 perror("open fail\n");
11 return 0;
12 }
13
14 while(1)
15 {
16 char buf[1024] = {0};
17 read(fd, buf, sizeof(buf) - 1);
18 printf("buf : %s\n", buf);
19 }
20
21 close(fd);
22 return 0;
23 }
此时这两个进程便实现了进程间通信
命名管道的生命周期也跟随进程
小知识:fifo,为first in first out的缩写,即先进先出
因为命名管道有标识符,所以命名管道支持不同进程之间的进程间通信
其他特性同匿名管道一样
共享内存
原理
1、首先在物理内存中创建了一块内存
2、不同的进程通过页表映射,将同一块物理内存映射到自己的虚拟地址空间
3、不同的进程操作进程虚拟地址,通过页表的映射,就相当于操作同一块内存,从而完成了数据交换
共享内存的接口
创建共享内存
int shmget(key_t key, size_t shmflg);
key:共享内存的标识符,用来标识一块共享内存,在操作系统中,共享内存的标识是不能重复的,可以直接给一个32位的16进制数字
size:共享内存的大小
shmflg:
IPC_CREAT:如果key标识的共享内存不存在,则创建
IPC_EXCL | IPC_CREAT:如果key标识的共享内存存在,则新创建一个后报错
权限:按位或 8进制数字
例:0664 创建用户可读可写,组内用户可读可写,其他用户可读
返回值:
-1:创建失败了
>0:成功,返回的是共享内存的操作句柄,后续是通过操作句柄来操作共享内存的
查看共享内存的命令:ipcs -m
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/shm.h>
4
5 #define key 0x12121212
6
7 int main()
8 {
9 int shmid = shmget(key, 1024, IPC_CREAT | 0664);
10 if(shmid < 0)
11 {
12 perror("shmget fail");
13 return 0;
14 }
15
16
17 return 0;
18 }
key:标识符
shmid:操作句柄
owner:创建者
perms:权限
bytes:共享内存大小
nattch:附加进程数量
ststus:状态
共享内存的生命周期跟随操作系统内核
附加共享内存到进程
void *shmat(int shmind, const void *shmaddr, int shmflg);
shmid:共享内存操作句柄,即shmget的返回值
shmaddr:
将共享内存附加到shmaddr,一般情况下都不会自己去指定映射到共享区中的哪一个虚拟地址,而是传递NULL值,让操作系统去选择
shmflg:标志将共享内存附加到进程后,进程对共享内存的读写属性
0:读写
SHM_RDONLY:只读
返回值:
附加成功:返回值为附加到共享区当中的虚拟地址
附加失败:NULL
分离共享内存
int shmdt(const void *shmaddr);
shmaddr:刚刚附加的时候,返回的共享区的地址
共享内存操作函数
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:共享内存操作句柄
cmd:
IPC_STAT:获取共享内存参数
IPC_SET :设置共享内存属性
IPC_RMID:删除共享内存
struct shmid_ds:共享内存属性对应的结构体
共享内存的代码
写:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/shm.h>
4 #include <string.h>
5
6 #define key 0x77777777
7
8 int main()
9 {
10 int shmid = shmget(key, 1024, IPC_CREAT | 0664);
11 if(shmid < 0)
12 {
13 perror("shmget fail\n");
14 return 0;
15 }
16
17 //附加到当前的进程
18 void* addr = shmat(shmid, NULL, 0);
19 if(addr == NULL)
20 {
21 perror("shmat fail\n");
22 return 0;
23 }
24 //写
25 strncpy((char*)addr, "i am write", 10);
26
27 shmdt(addr);
28
29 return 0;
30 }
读:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/shm.h>
4
5 #define key 0x77777777
6
7 int main()
8 {
9 int shmid = shmget(key, 1024, IPC_CREAT | 0664);
10 if(shmid < 0)
11 {
12 perror("shmget fail\n");
13 return 0;
14 }
15
16 //附加到当前的进程
17 void* addr = shmat(shmid, NULL, 0);
18 if(addr == NULL)
19 {
20 perror("shmat fail\n");
21 return 0;
22 }
23
24 while(1)
25 {
26 printf("%s\n", (char*)addr);
27 sleep(1);
28 }
29
30 shmdt(addr);
31
32 return 0;
33 }
共享内存读取的时候采用的是拷贝,而不是类似于管道一样的读走
更改一下
23 int count = 0;
24 //写
25 while(1)
26 {
27 //strncpy((char*)addr, "i am write", 10);
28 sprintf((char*)addr, "%s-%d", "i am write", count++);
29 sleep(1);
30 }
即共享内存在写的时候采用的是覆盖写的方式
使用 ipcrm -m [共享内存操作句柄] 可以删除共享内存
如果删除了一个被进程附加的共享内存
当前共享内存的标识符会改变成为0x00000000,且共享内存的状态会变成dest(destory)
可以通过 ipcs -m 这个命令查看到当前被删除共享内存的信息,说明在操作系统内核,描述该共享内存的结构体没有被释放,但是共享内存所使用的空间
已经被释放了,所以附加的进程如果再次操作共享内存,则有崩溃的风险
消息队列&信号量
队列的特性:先进先出
消息队列本质上也是在内核当中维护的一个双向链表,但满足了先进先出的特性小,所以被称之为队列
消息队列当中的消息:消息只的是带有类型的数据,类型和类型之间是有优先级的
接口
int msgget(key_t key, int msgflg);
key: 消息队列的标识符
返回值:成功返回消息队列的操作句柄
int msgsnd(int msqid, const void *msgp, size_t msgsz, int masgflg);
msqid:消息队列的操作句柄
msgp:要发送到消息队列的消息
msgsz:指定发送的数据大小,只计算自己发送数据的大小
msgflg:
IPC_NOWAIT:非阻塞发送方式
0:阻塞发送
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msqid:消息队列的操作句柄
msgp:将接收的数据放到哪里
msgsz:最大的接收能力
msgtyp:
> 0:表示获取队列当中距离队首最近同类型的元素
==0:直接拿队首的元素
< 0:
1、需要将小于0的msgtype的值取绝对值
2、过滤从队首到[msgtype]区间的消息
3、从区间中获取和[msgtype]一样的消息
4、再去当中获取类型最小的数据
msgflg:
阻塞接收:0
非阻塞接收:IPC_NOWAIT