文章目录:
1. 进程间通信的作用
由于进程独立性的存在,两个进程想要直接交换数据是非常困难的,所以应用进程间通信来解决进程与进程之间交换数据的问题
2. 匿名管道
2.1 通过命令感受管道
- ps: 命令
- aux: 命令行参数
- |: 管道
- grep: 可执行程序
2.2 从内核角度解释管道
管道就是内核当中的一块缓冲区(一块内存),进程A和进程B可以通过这个缓冲区进行数据交换,匿名管道是不具备标识符的
2.3 代码创建管道
int pipe(int pipefd[2]);
- 功能:创建一个匿名管道
- 参数(为输出型参数):
(1)pipefd:类型为整型数组,有两个元素,pipefd[0],pipefd[1]
(2)pipefd[0],pipefd[1]:当中保存的是一个文件描述符
(3)pipefd[0]:对应的文件描述符可以从管道当中进行读,但不能写
(4)pipefd[1]:对应的文件描述符可以往管道中进行写,但不能读
(5)pipefd[0],pipefd[1]当中的值是pipe函数进行赋值的,当我们调用pipe函数的时候,只需要给pipe函数传递一个拥有两个元素的整型数组,pipe函数在创建完毕管道之后,会给pipefd[0],pipefd[1]进行赋值
- 返回值:
-1:创建失败
0:创建成功
验证如下:
我们先写一个test.c文件,在此文件中调用pipe函数,并打印出fd[0],fd[1]的值,之后让程序死循环不要退出,方便后面查看现象
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 int fd[2];
7 int ret = pipe(fd);
8 if(ret < 0)
9 {
10 perror("pipe");
11 return 0;
12 }
13 printf("fd[0]:%d",fd[0]);
14 printf("fd[1]:%d",fd[1]);
15
16
17 while(1)
18 {
19 sleep(1);
20 }
21 return 0;
22 }
运行程序我们可以看到fd[0]对应的文件描述符是3,文件权限为只读,fd[1]对应的文件描述符是4,文件权限为致谢,符合我们的预期
[拓展]——输出型参数
参数赋值在被调用函数内部,使用在调用函数处
如下图所示:
2.4 从PCB角度去分析管道
进程之间是如何通过匿名管道进行通信的呢?
答:通过fork来实现管道共享
父子进程共享管道代码验证如下:
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 int fd[2];
7 int ret = pipe(fd);
8 if(ret < 0)
9 {
10 perror("pipe");
11 return 0;
12 }
13 printf("fd[0]:%d\n",fd[0]);
14 printf("fd[1]:%d\n",fd[1]);
15 pid_t pid = fork();
16 if(pid < 0)
17 {
18 perror("fork\n");
19 }
20
21 while(1)
22 {
23 sleep(1);
24 }
25 return 0;
26 }
父子进程共享管道图解如下:
- (1)匿名管道只适用于具有亲缘关系的进程,进行进程间的通信
- (2) 先创建管道,再创建子进程,父子进程才可以进行进程间通信
如果两个子进程想要使用匿名管道进行进程通信可不可以?
答:理论上是可以的,因为两个子进程都拷贝了父进程中的文件描述符,可以找到内核中的那一块缓冲区,但具体操作上面,一定要遵守先创建管道,再创建子进程
2.5 匿名管道配合父子进程使用及其特性验证
2.5.1 如果关闭子进程的fd[0],fd[1]会不会影响父进程中的fd[0],fd[1]?
答:因为进程的独立性,在一个进程中操作并不会影响另一个进程,所以在父子进程任意一个进程关闭文件描述符,不会影响另一个进程
代码验证如下:
我们预期关闭子进程当中的fd[0],fd[1],子进程中的fd[0],fd[1]没有了,但父进程中的fd[0],fd[1]还在
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 int fd[2];
7 int ret = pipe(fd);
8 if(ret < 0)
9 {
10 perror("pipe");
11 return 0;
12 }
13 printf("fd[0]:%d\n",fd[0]);
14 printf("fd[1]:%d\n",fd[1]);
15 pid_t pid = fork();
16 if(pid < 0)
17 {
18 perror("fork\n");
19 }
20 else if(pid == 0)
21 {
22 //child
23 printf("i am child,fd[0]:%d,fd[1]:%d\n",fd[0],fd[1]);
24 close(fd[0]);
25 close(fd[1]);
26 }
27 else
28 {
29 //father
30 }
31
32 while(1)
33 {
34 sleep(1);
35 }
36 return 0;
37 }
测试结果如下图所示,子进程的fd[0],fd[1]被关闭了,父进程没受影响
2.5.2 实现父子进程间进程通信
假设让父进程写,子进程读,预期,子进程会读到父进程写端内容
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 int fd[2];
7 int ret = pipe(fd);
8 if(ret < 0)
9 {
10 perror("pipe");
11 return 0;
12 }
13 printf("fd[0]:%d\n",fd[0]);
14 printf("fd[1]:%d\n",fd[1]);
15 pid_t pid = fork();
16 if(pid < 0)
17 {
18 perror("fork\n");
19 }
20 else if(pid == 0)
21 {
22 //child
23 //子进程读
24 char buf[1024] = {0};
25 read(fd[0],buf,sizeof(buf)-1);
26 printf("i am child,buf:%s\n",buf);
27 }
28 else
29 {
30 //father
31 //父进程写
32 char buf[] = "i am father write!";
33 write(fd[1],buf,18);
34
35 }
36
37 while(1)
38 {
39 sleep(1);
40 }
41 return 0;
42 }
测试结果如下,符合我们的预期:
管道的数据只能从写端流向读端,这是一种半双工的通信方式
全双工通信(网络):数据可以从A端流往B端,也可以从B端流向A端
2.5.3 fd[0]是将管道中的数据读走函数拷贝了一份
通过fd[0]从管道中读取数据的时候,是将数据读走了,并不是拷贝了一份
代码验证如下:
我们要想验证fd[0]是将管道中的数据读走了函数拷贝了一份,只需再进行一次读取,若读到数据,则证明是拷贝了一份数据,若没读到数据,则证明fd[0]将数据读走了
如上图所示,当我们将程序跑起来,我们看到的运行结果和之前子进程只读一次的结果一样,此时要验证如上结论,我们就要通过pstack命令查看子进程现在到底在做什么,如果子进程还是进行读,那么说明管道当中的数据已经被读走了,子进程阻塞在read处
如上图所示的结果验证了我们的结论,管道当中的数据已经被读走了,子进程阻塞在read处,所以fd[0]是将管道中的数据读走
2.5.4 字节流服务
- (1) 从管道当中去读取数据的时候,可以指定读取任意大小的数据。如果管道当中没有数据,默认情况下,进行读(read),则会阻塞
- (2) 多次写入的数据之间是没有明显的分界的,上一条数据的末尾链接下一条数据的开头
如上两个内容可以称为字节流服务
代码验证如下:
让父进程write两次,子进程每次read1一个字节
2.5.5 匿名管道的生命周期跟随进程
2.6 默认情况(不对管道创建出来的文件描述符做任何操作)下的读写属性
2.6.1 如果读端不读,写端一直写,会产生什么情况
- 写端写满之后就会阻塞
验证如下:
此时我们通过pstack命令可以看到写端进行阻塞
[拓展]–>管道的大小为65536
2.6.2 如果写端不写,读端一直读,会产生什么情况
- 读端将管道中的数据读完之后,再次进行读的时候,就会进行阻塞
(在2.5.3中已验证过,此处不再验证)
2.7 [拓展]–>原子性
原子性:非黑即白,要么完成了,要么还没开始
pipe_buf:程序员在操作管道进行读写的时候,保证读写原子性
的数据最大的大小
我们在ulimit -a中看到的pipe size就是保证原子数最大的大小
8 * 512 = 4096字节