• 【Linux】进程间通信1-匿名管道1



    前言

    为什么需要进程间通信呢?

    每一个进程的数据都是存储在物理内存当中的,进程通过各自的进程虚拟地址空间进行访问,访问的时候,通过各自的页表映射关系,访问到物理内存。
    从进程的角度看,每个进程都认为自己有4G的空间,物理内存当中怎么存储,进程并不清楚,这也保证了进程的独立性。
    因为进程的独立性,所以进程之间不能进行数据交换,进程间通信就是为了让进程和进程之间交换数据的。
    进程间通信方式:管道、共享内存、消息队列、网络。

    管道符[|]

    ps aux | grep test

    我们通过上面这个命令,来理解一下管道符,管道符左侧的命令是列举当前Linux操作系统当中的进程信息,右侧命令是过滤存在test字符串的项,ps aux 的返回结果,通过管道,传输给grep进程,grep进程在ps aux的结果当中过滤存在test的字符串,这样,独立的ps aux进程就和独立的grep进程通过管道进行了通信。

    管道的本质:管道在内核当中就是一块缓冲区,供进程进行读写,交换数据。

    在这里插入图片描述

    创建匿名管道的pipe函数

    pipe函数原型:

    int pipe(int pipefd[2];

    调用该函数创建一个匿名管道

    参数:是一个数组,有两个元素。

    当我们调用pipe函数的时候,我们就在内核当中创建了一块缓冲区(管道),程序员需要往这个缓冲区中读写,于是pipe函数就给我们提供了读写两端,pipefd[0]是管道的读端,pipefd[1]是管道的写端,pipe函数通过参数告诉我们它创建的管道的读写两端是什么,所以函数参数为出参,程序员在创建管道之前,肯定是不知道所创建的管道的读写两端的。

    参数为出参,也就是pipefd的值是由pipe函数进行填充的,调用者进行使用。

    pipefd[0]是管道的读端,pipefd[1]是管道的写端,这两个元素保存的内容是文件描述符,也就是我们调用了pipe函数,该函数给我们创建了一个匿名管道,pipe函数给我们返回一个数组,该数组有两个元素,这两个元素的类型就是文件描述符,程序员通过操作这两个文件描述符,对管道进行读写。

    返回值:创建成功返回0,创建失败返回-1.

    从内核角度深入理解管道

    在这里插入图片描述

    代码验证pipe函数

    我们要通过代码验证以下三个方面的内容:

    • 验证pipe函数的出参
    • 验证fd[0]、fd[1]是文件描述符
    • 通过fd[0]、fd[1]是不是可以进行读写
     1 #include <stdio.h>
      2 #include <unistd.h>
      3 #include <string.h>
      4 int main(){
      5   int fd[2];
      6   printf("fd[0] = %d, fd[1] = %d.\n",fd[0],fd[1]);
      7   int ret = pipe(fd);
      8   if(ret == -1){
      9     perror("pipe");
     10     return 0;
     11   }
     12   printf("fd[0] = %d, fd[1] = %d.\n",fd[0],fd[1]);
     13 
     14   const char* lp = "hello world";
     15   write(fd[1],lp,strlen(lp));
     16 
     17   char buf[1024] = {0};
     18   read(fd[0],buf,sizeof(buf)-1);
     19 
     20   printf("read buf : %s\n",buf);
     21   return 0;                                                                    
     22 }          
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    执行结果:

    [jxy@VM-4-2-centos pipe_test]$ ./pipe_test 
    fd[0] = 4196224, fd[1] = 0.
    fd[0] = 3, fd[1] = 4.
    read buf : hello world
    
    • 1
    • 2
    • 3
    • 4

    我们给他加上一个睡眠,去 /proc/[pid]/fd 下面看看是不是多了两个描述符,并且该文件描述符是不是和fd[0]、fd[1]对应。

    在这里插入图片描述

    管道和子进程的先后创建顺序

    父子进程要通过匿名管道进行通信,核心点在于父子进程都要能够读写管道,也就意味着父子进程都有管道两端的文件描述符。

    那如果要实现父子进程匿名管道的通信,到底应该先创建管道还是应该先创建子进程呢?

    我们假设先创建子进程再创建匿名管道:

    子进程拷贝父进程的PCB,那子进程的文件描述符表和父进程相同,如果先创建子进程再创建管道,那子进程就得不到父进程创建的匿名管道的读写两端,最后就是这样的结果:
    在这里插入图片描述
    子进程的文件描述符表里面没有匿名管道的读写两端的文件描述符,所以父子进程此时就无法正常通过匿名管道进行通信。

    我们再假设先创建匿名管道再创建子进程:

    最后子进程拷贝父进程的PCB,得到的就是这样一个结果:
    在这里插入图片描述

    在先创建管道再创建子进程的情况下,子进程拷贝得到的文件描述符表就包含了父进程创建的读写两端的文件描述符。这种情况下,父子进程就可以通过匿名管道进行通信了。
    结论:一定要先创建匿名管道,再创建子进程,否则,子进程当中就没有匿名管道读写两端的文件描述符。

    代码实现父子进程的通信

    我们写一段代码,目标即使实现父子进程的通信,代码逻辑就是父进程先创建匿名管道,父进程再创建子进程,父进程写,子进程读,代码如下:

    #include 
      4 int main(){
      5   int fd[2];
      6   int ret = pipe(fd);
      7   //创建管道
      8   if(ret == -1){
      9     perror("pipe");
     10     return 0;
     11   }
     12 
     13   ret = fork();
     14   //创建子进程
     15   if(ret < 0){
     16     perror("fork");
     17     return 0;
     18   }else if(ret == 0){
     19     //child
     20     char buf[1024] = {0};
     21     read(fd[0],buf,sizeof(buf)-1);
     22     //子进程读
     23     printf("I am child,read buf :%s.\n",buf);
     24   }else{
     25     //father
     26     const char* lp = "I am father,hello child";                                
     27     write(fd[1],lp,strlen(lp));                
     28     //父进程写                 
     29   }           
     30   return 0;
     31 } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    执行结果:

    [jxy@VM-4-2-centos pipe_comm]$ ./pipe_comm 
    I am child,read buf :I am father,hello child.
    
    • 1
    • 2

    通过这个执行结果,不难看出来,我们的父子进程通过匿名管道进行了数据交换,进行了通信。
    针对上面的代码,我们可能会有以下疑问:父进程创建出来子进程,父子进程是抢占式执行的,如果是父进程先执行,子进程再执行,那得出上述执行结果顺理成章,父进程往匿名管道写入数据,子进程再从匿名管道读出数据,没有任何问题。但是另一种情况呢?子进程先执行,父进程再执行,子进程去匿名管道读出数据的时候,父进程还没有写入数据,但是依然得出上述执行结果了。那为什么不管怎么运行程序,输出的结果都一致呢?

    因为当管道中没有数据的时候,子进程调用的read函数从管道当中读的时候,会阻塞,知道管道当中有内容,read函数读到内容之后,read才会返回。

    我们用代码验证一下,在上面代码的基础上,让父进程休眠100秒。

    sleep(100);
    
    • 1

    查看堆栈信息如下:

    [jxy@VM-4-2-centos fd]$ ps aux | grep pipe_comm
    jxy      11266  0.0  0.0   4212   360 pts/0    S+   11:57   0:00 ./pipe_comm
    jxy      11267  0.0  0.0   4212    96 pts/0    S+   11:57   0:00 ./pipe_comm
    jxy      11524  0.0  0.0 112816   980 pts/2    S+   11:58   0:00 grep --color=autopipe_comm
    [jxy@VM-4-2-centos fd]$ pstack 11266
    #0  0x00007f9a950e99e0 in __nanosleep_nocancel () from /lib64/libc.so.6
    #1  0x00007f9a950e9894 in sleep () from /lib64/libc.so.6
    #2  0x00000000004007e1 in main ()
    [jxy@VM-4-2-centos fd]$ pstack 11267
    #0  0x00007f9a95113b40 in __read_nocancel () from /lib64/libc.so.6
    #1  0x00000000004007bc in main ()
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    管道的特性

    • 1、半双工·:数据只能从写端流向读端,单向通信
    • 2、匿名管道没有标识符,只能具有亲缘性关系的进程进行进程间通信
    • 3、管道的生命周期是跟随进程的,进程退出了,管道在内核当中就销毁了。
    • 4、管道的大小为64k

    我们写段代码验证一下管道的大小:

    代码实现思想,只写不读,直到将管道写满。

    在这里插入图片描述

    总共读入了65536个字节,也就是64k,但是程序并没有结束,我们查看一下堆栈信息:

    [jxy@VM-4-2-centos fd]$ ps aux | grep pipe_size
    jxy      31612  0.6  0.0   4216   356 pts/0    S+   14:19   0:00 ./pipe_size
    jxy      31654  0.0  0.0 112816   984 pts/2    R+   14:19   0:00 grep --color=autopipe_size
    [jxy@VM-4-2-centos fd]$ pstack 31612
    #0  0x00007f495283eba0 in __write_nocancel () from /lib64/libc.so.6
    #1  0x0000000000400656 in main ()
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    为什么程序没有结束呢?因为write往管道当中写的时候阻塞了。

    当管道写满之后,再调用write之后,write就会阻塞。

    • 5、管道提供字节流服务

    先后两次写入管道的数据之间没有间隔

    我们用代码验证一下:

    #include 
      2 #include <unistd.h>
      3 #include <string.h>
      4 int main(){
      5   int fd[2];
      6   int ret = pipe(fd);//创建管道
      7   if(ret<0){
      8     perror("pipe");
      9     return 0;
     10   }
     11 
     12   write(fd[1],"hello",5);
     13   write(fd[1],"world",5);//调用两次write
     14 
     15   char buf[1024];
     16   read(fd[0],buf,sizeof(buf)-1);//读
     17   printf("read buf : %s\n",buf);                                               
     18   return 0;
     19 }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    执行结果:

    [jxy@VM-4-2-centos pipe_stream]$ ./pipe_stream 
    read buf : helloworld
    
    
    • 1
    • 2
    • 3

    数据是被读走,而不是被拷贝走,并且读端在进行读的时候,可以按照任意大小去读。

    我们再来验证一下:

    代码1:

    #include 
      2 #include <unistd.h>
      3 #include <string.h>
      4 int main(){
      5   int fd[2];
      6   int ret = pipe(fd);
      7   if(ret<0){
      8     perror("pipe");
      9     return 0;
     10   }
     11   write(fd[1],"abc",3);                                                        
     12   char buf[1024]={0};
     13   read(fd[0],buf,sizeof(buf)-1);
     14   printf("read buf :%s.\n",buf);
     15 
     16   memset(buf,'\0',sizeof(buf));
     17 
     18   read(fd[0],buf,sizeof(buf)-1);
     19   printf("read buf :%s.\n",buf);
     20   return 0;
     21 }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    执行结果:

    在这里插入图片描述

    程序并没有结束,这是为什么呢?因为read阻塞了,我们查看一下进程的堆栈信息:

    [jxy@VM-4-2-centos fd]$ ps aux | grep pipe_stream
    jxy       6670  0.0  0.0   4216   356 pts/0    S+   15:04   0:00 ./pipe_stream
    jxy       6718  0.0  0.0 112816   988 pts/2    S+   15:05   0:00 grep --color=autopipe_stream
    [jxy@VM-4-2-centos fd]$ pstack 6670
    #0  0x00007fa1d81a7b40 in __read_nocancel () from /lib64/libc.so.6
    #1  0x0000000000400763 in main ()
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    此时管道中没有任何数据可供第二个read函数读,就造成了阻塞。

    代码2:

    1 #include <stdio.h>
      2 #include <unistd.h>
      3 #include <string.h>
      4 int main(){
      5   int fd[2];
      6   int ret = pipe(fd);
      7   if(ret<0){
      8     perror("pipe");
      9     return 0;
     10   }
     11   write(fd[1],"abcdef",6);
     12   char buf[4]={0};                                                             
     13   read(fd[0],buf,sizeof(buf)-1);
     14   printf("read buf :%s.\n",buf);
     15 
     16   memset(buf,'\0',sizeof(buf));
     17 
     18   read(fd[0],buf,sizeof(buf)-1);
     19   printf("read buf :%s.\n",buf);
     20   return 0;
     21 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    执行结果:

    [jxy@VM-4-2-centos pipe_stream]$ ./pipe_stream 
    read buf :abc.
    read buf :def.
    
    
    • 1
    • 2
    • 3
    • 4

    这两段代码就验证了我们的数据是被读走,而不是被拷贝走。

    • 6、pipe_size:4096字节

    当写入或者是读取的字节数量小于pipe_size的时候,管道保证读写的原子性。

    在这里插入图片描述

    那什么是原子性呢?

    一个操作要么不间断的全部被执行,要不一个也没有执行,非黑即白,没有中间状态。对于管道来说,就是读或者写操作在同一时刻只有一个进程在操作,保证进程在操作的时候,读写操作不会被其他进程打扰。

    • 7、阻塞属性

    读写两端的文件描述符初始的属性为阻塞属性。当write一直写,读却不去读,则写满之后write会阻塞;当read一直读,当管道内部的数据被读完之后,则read会阻塞,也就是说,当管道为空的时候,调用read,则read函数就会阻塞。

  • 相关阅读:
    新的旅程(四)
    如何在Linux系统中搭建Zookeeper集群
    前端框架的发展历程
    Vue中的事件总线(EventBus)是什么?它有什么优点和缺点?
    [附源码]SSM计算机毕业设计网上零食商城JAVA
    Linux搜索查找命令【详细整理】
    入门力扣自学笔记278 C++ (题目编号:2605)
    NCCL源码解析③:机器内拓扑分析
    【爬虫系列】Python爬虫实战--招聘网站的职位信息爬取
    STM32H5开发(6)----SPI驱动TFT-LCD屏
  • 原文地址:https://blog.csdn.net/weixin_56916549/article/details/127603177