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

avatar
作者
筋斗云
阅读量:0

1.进程间通信介绍(IPC)

  1. 进程是计算机系统分配资源的最小单位(严格说来是线程)。
  2. 每个进程都有自己的一部分独立的系统资源,彼此是隔离的,也就是说进程具有独立性

为了能使不同的进程互相访问资源并进行协调工作,才有了进程间通信。

      进程间通信简称为 IPC(Interprocess communication),顾名思义,就是进程与进程之间进行数据的交流,OS保证了各进程之间相互独立,但这不意味着进程与进程之间就必须完全隔离开,在不少的情况下,进程之间需要相互配合共同完成某项任务,这就要求各进程之间能够互相交流。

1.1、进程间通信的概念

        每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)

       各个进程之间若想实现通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域。

所以

  1. 我们两个要通信的进程,本质是要访问操作系统。
  2. 进程代表用户,“资源”从创建,使用,到释放,都是在调用系统调用接口。
  3. 有很多进程在通信,就会创建很多内存区域来通信,操作系统就会对这些内存区域来先描述后组织——操作系统有独立的通信模块,这个通信模块隶属于文件系统
  4. 进程间通信是有标准的

1.2.进程间通信的目的

进程间通信的目的和原因,有如下几个点

  1. 数据传输:一个进程需要将它的数据发送给另一个进程
  2. 资源共享:多个进程之间共享同样的资源
  3. 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
  4. 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

总得来说,实现进程间通信就是为了进程之间能够协同完成某项任务

1.3..进程间通信的本质

进程间通信的原理是让 不同的进程看到同一份资源(内存 , 文件,内核缓冲等)

看到同一份资源还不够,通信的根本目的是要传输数据 

因此通信的本质就是”数据的拷贝“

1.4.进程间通信分类

资源由谁(OS的哪些模块)提供 , 就有了不同的进程间通信方式!

这里的模块可以是: (文件–管道) , (OS内核IPC提供- SystemV IPC) , (网络–套接字)                        

管道(基于文件的方案)

  • 匿名管道(重点)
  • 命名管道(重点)

System V IPC

  • System V 消息队列
  • System V 共享内存(重点)
  • System V 信号量

POSIX IPC(不讲)

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

        进程间通信起初有很多不同的的相关协议,随着不断的实践发展,目前主要有两个主流的通信规则,一个是System V,另一个是POSIX

  • System V:让通信过程可以跨主机,System V 标准如今比较少用了,但其通信速度极快的共享内存还是值得深入学习的
  • POSIX:聚焦在本地通信,POSIX 是 Unix 系统的一个设计标准,很多类 Unix 系统也在支持兼容这个标准,如 Linux , POSIX 标准具有跨平台性,就连 Windows 也对其进行了支持,后续学习 同步与互斥 时,所使用的信号量等都是出自 POSIX 标准,这是进程间通信的学习重点,POSIX 标准支持网络中通信,比如 套接字(socket) 就在此标准中

        由于System V由于制定的比较早,不支持跨主机间的通信,在今天属于比较陈旧的标准了,因此我们会将更多精力放在POSIX上,不过POSIX通信并不是这篇文章的内容,因此不会提及而System V我们关注比较重要的共享内存的概念,通信规则并非只局限于POSIX和System V,我们先介绍比较简单易接受的管道通信

管道可以说是十分古老且简单了,适合深入学习,探究进程间通信时的原理及执行流程 

2.管道通信

2.1.什么是管道?

  • 管道是Unix中最古老的进程间通信的形式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"
  • 管道是一种最基本的进程间通信机制。 把一个进程连接到另一个进程的一个数据流称为一个“管道”,通常是用作把一个进程的输出通过管道连接到另一个进程的输入

管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据,管道一端的进程顺序的将数据写入缓冲区,另一端的进程则顺序的读出数据。

        该缓冲区可以看做是一个循环队列,读和写的位置都是自动增长的,不能随意改变,一个数据只能被读一次,读出来以后在缓冲区就不复存在了。 

2.2.管道, 其实是一个打开的内存级文件 .

  1. 描述已打开文件的结构体files_struct, 其中存储着 指向打开文件的数组fd_array, 此数组的类型是 struct files*.
  2.  此外,操作系统会为每个打开的文件配置一个缓冲区
  3. 无论是对文件进行读和写,都得先把文件加载到内存中才行
  4. 操作系统可以创建一个在磁盘中不存在的内存文件

  Linux 中一切皆文件,所以管道本质上就是一个文件 ,但是这个文件很特殊, 向这个文件中写入数据实际上并不会真正写入磁盘中.

        在介绍Linux系统的文件描述符时, 简单介绍了Linux系统中 描述已打开文件的结构体files_struct, 其中存储着 指向打开文件的数组fd_array, 此数组的类型是 struct files*.

而这个files_struct中, 直接或间接描述了文件的所有属性, 以及 此文件的缓冲区相关信息:

缓冲区信息中, 包含着描述文件的inode结构体, 而inode结构体中其实描述着一个联合体:

 这个处于inode结构体中的联合体, 其实就是为了标识这个文件的类型, 其中pipe 就表示此文件的类型是管道文件.

通过文件的inode, 系统可以辨别出打开的文件是管道文件.

        向管道文件写入数据实际上并不会写到磁盘上,而只是写到文件的缓冲区中, 因为管道文件主要是用来进程间通信的, 如果先写入磁盘另一个进程再读取, 整个过程就太慢了

这种不实际存储数据的行为特点, 其实也符合生活中管道的特点,——管道不能用来存储资源, 只能用来传输资源

并且, 除了管道不实际存储资源以外, 管道还有一个特点:管道是单向传输的

这是管道的特点, Linux的管道也是遵循这个特点的, 也就是说, 两个进程间使用管道通信时, 其中一个进程若以只写方式打开管道, 那么另一个进程就只能以只读方式打开文件.

2.3.管道通信主要是借助文件系统来实现的

管道通信主要是借助文件系统来实现的,怎么理解呢? 

        我们假设现在系统上的进程A和进程B要互相通信,A不能直接去B里面读数据,因为进程具有独立性,那该怎么办呢?这就需要找一块空间C,空间C用来存放通信双方通信的数据,现在进程A要给B发送数据,那么A和B要向系统声明建立连接,申请一块空间C,然后A往空间C里发送数据,B从空间C里读取数据,这样A就实现了和B的通信这块空间C就像一根管道一样,连接着A与B,整个管道通信的基本原理就是如此,当然这只解释了管道名称的由来,并没有解释管道通信是借助文件系统来实现的

        我们要理清楚如何在Linux系统中让两个进程读取到同一块内存空间,如果看过我的基础I/O篇的同学应该会想到,那就是通过文件,进程从磁盘中或除自身以外的其他可读写的内存区域中读取或写入数据主要是通过文件系统来解决的只要系统在内存中创建一个文件,A进程打开这个文件,B进程也打开这个文件,那么A与B就通过这个文件连接起来进行通信了,这就是管道通信是借助文件系统来实现的原因

我们举个例子

        在shell中执行命令,经常会将上一个命令的输出作为下一个命令的输入,由多个命令配合完成一件事情。而这就是通过管道来实现的。|这个竖线就是管道符号

ls -l | grep string   //grep是抓取指令 
  • ls命令(其实也是一个进程)会把当前目录中的文件都列出来
  • 但它不会直接输出,而是把要输出到屏幕上的数据通过管道输出到grep这个进程中,作为grep这个进程的输入
  • 然后这个进程对输入的信息进行筛选(grep的作用),把存在string的信息的字符串(以行为单位)打印在屏幕上。

3.匿名管道

经过上述的说明,我们已经明白了管道通信就是用来实现进程与进程之间的通信,但是进程与进程之间的通信也分为两种

  1. 一种是父子进程或兄弟进程之间的通信(匿名管道)
  2. 另一种则是没有亲属关系的进程间的通信(命名管道)

        匿名管道的创建, 不会指定打开文件的文件名、文件路径等, 即不会有目标的打开文件,只是在内存中打开一个文件, 用于进程间的通信

        而由于匿名管道是非明确目标的文件, 也就意味着两个完全不相关的进程是无法一起访问这个管道的, 因为完全不相关的进程无法找到这个管道文件.

这也就意味着,匿名管道其实只能用于具有血缘关系的进程间通信.

        父子进程之间是共享代码和数据的,但这个数据共享只能用来读,一旦一方试图使数据发生变化会触发写时拷贝,父进程与子进程的数据就存放到了不同的地址,这时父子双方该如何通知对方数据发生了变化呢?

        这就是匿名管道通信要研究的东西

        进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行读写操作,进而实现父子进程间通信

        子进程拷贝父进程的fd_array,父子进程看到同一份文件 , 这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝(文件并不存在磁盘,只在内存中存在)

        管道虽然用的是文件的方案,但操作系统一定不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率。也就是说,这种文件是一批不会把数据写到磁盘当中的文件,换句话说,磁盘文件和内存文件不一定是一 一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在

3.1.创建匿名管道的原理

  • 创建一个管道也就是系统在内存中创建一个文件,进程A与进程B通过这个文件互相读取数据,这就涉及到另一个问题,是否进程A和进程B可以双向通信,即都可向管道文件中读写数据,若可以,则进程A与B又该如何分辨自己该读取哪部分数据呢?

        可能你会说等A写完,B赶紧读,然后B再写,A再读,这样会有潜在的隐患,因为A写的时候你要阻止B写入,如果这个时候B有很重要的数据不能及时写入,就造成数据丢失

        因此我们规定管道通信都是单向通信,创建一个管道时,只能由一方负责写,一方负责读,这是在创建管道时就要决定好的,如果要实现双方都可以读写,那就创建两个管道,创建两个管道无非是创建两个文件罢了,开销并不大

  • 管道的通信是单向的,也就是A进程在管道通信时,既可以做写方,又可以做读方,系统如何区分此时A是写还是读呢?

        解决办法就是让父进程以读和写两种形式分别打开管道文件,也就是我们需要一个数组,这个数组只有两个元素,用来记录以读的形式打开管道文件的fd以及以写的形式打开管道文件的fd再创建子进程,然后根据相关情况,选择关闭其中一个

我们从进程视角来看 这个过程

子进程继承父进程的所有数据,代码 

子进程发生写时拷贝 

也就是说, 匿名管道的创建应该是由父进程创建, 然后创建子进程继承父进程的管道, 然后再关闭管道的写入端或读取端

这样就创建了一个管道通信

1.一个进程实现对同一个文件的读和写的过程

2.为什么父子进程要分别以只读和只写方式打开两次文件, 然后再创建子进程呢?

为什么不是父进程以一个方式打开, 子进程再以另一个方式打开呢?

因为子进程会以继承父进程的方式打开同一个文件, 即子进程打开文件的方式与父进程是相同的

如果父子进程通过想要通过管道实现进程通信, 子进程就需要先关闭已打开的文件, 再以某种方式打开同一个文件,这样比较麻烦, 如果在创建子进程之前, 父进程就已经以两种方式打开同一个文件, 那么再子进程创建之后, 只需要父进程关闭一个端口, 子进程关闭另一个端口就可以了

3.必须父进程关闭读取端, 子进程关闭写入端吗?

并不是的, 父子进程关闭哪个端口, 其实是 根据需求 关闭的.

如果子进程要向父进程传输数据, 那么关闭读取端的就应该是子进程

4.进程是如何知道管道被打开了什么端口的?或者说会不会发生父进程把这个管道文件关掉了,子进程还在读的情况

其实在file结构体中, 存在一个计数变量 f_count:

不过, 这个变量实际上还是一个结构体, 用于计数

如果父子进程都打开了这个文件,那么这个文件的引用计数就是2,如果父进程关闭了,引用计数就会减一,系统就会知道,做出相应措施

很好,到这里原理结束

3.2.系统调用接口——pipe函数

Linux操作系统提供了一个接口来进行匿名管道的创建与使用 

  • 功能:创建一个无名管道 
  • fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
  • 返回值:如果创建管道成功, 则返回0, 否则返回-1, 并设置errno

 注意:该函数的参数是输出型参数,在传参pipefd时要先创建一个数组,例如事先声明 int fd[2];

pipe系统调用的作用是, 创建一个管道文件. 其参数是一个 输出型参数

在pipe系统调用 执行成功之后, 参数数组内会存储两个元素

  1. pipe[0], 存储以 只读方式 打开管道时获得的fd
  2. pipe[1], 存储以 只写方式 打开管道时获得的fd

之后就可以根据需求, 选择父子进程的端口关闭

3.3.创建匿名管道

我们先复习一下makefile

我们将按照原理一步一步来讲解

#include<unistd.h> #include<stdio.h>  int main() {        int fd[2]={0};//这个语句就是用来记录进程分别以读写端打开管道文件的fd     int check = pipe(fd);//创建管道          if (check != 0) {         printf("create pipe error\n");         return 0;     }   }

我们在这里可以验证一下fd数组里面的数字是不是3和4

#include<iostream> #include<unistd.h>  using namespace std;  int main() {     int fd[2]={0};     int n=pipe(fd);      if(n<0)     {         cout<<"creat pipe failed"<<endl;         return 1;     }     cout<<"fd[0]:"<<fd[0]<<endl;     cout<<"fd[1]:"<<fd[1]<<endl; }

怎么样

父进程在创建子进程时,子进程会拷贝一份父进程的进程地址空间,同样的,子进程也会拷贝父进程的文件描述符表

#include<unistd.h> #include<stdio.h>  int main() {        int fd[2]={0};//第一个是读,第二个是写     int check = pipe(fd);          if (check != 0) {         printf("create pipe error\n");         return 0;     }       pid_t id = fork();       if (id > 0) { /*执行父进程代码*/ }     if (id == 0) { /*执行子进程代码*/ }       return 0;   }

接下来,我们明确父子进程谁是读端,谁是写端,就可以进行通信了,这里我们让父进程写数据给子进程,那么父进程就要关闭自己的读端,子进程就要关闭自己的写端

        fd[0]是读端,fd[1]是写端

巧记:按照读音的顺序,读写,01,正好对应。还有1像一支笔,所以是写端,0像张开的嘴,所以是读端

#include<unistd.h> #include<stdio.h>  int main() {        int fd[2]={0};     int check = pipe(fd);          if (check != 0) {         printf("create pipe error\n");         return 0;     }       pid_t id = fork();       if (id > 0) { //父进程        close(fd[1]);        /*关闭父进程的写端,接着执行父进程代码*/         //……        close(fd[0]);//最好还是关掉     }         if (id == 0) { //子进程        close(fd[0]);        /*关闭子进程的读端,接着执行子进程代码*/          //……        close(fd[1]);//最好还是关掉     }       return 0;   }

这个过程大家可能又疑惑了,子进程关闭了fd[0],父进程关闭了fd[1],那么这个会互相影响吗?

答案是肯定不会,因为进程具有独立性,一旦父子双方任意一方修改,就会触发写时拷贝!! 

现在读写双方都确定了,那写方如何给读方发数据,读方又如何读取写方的数据呢?

既然管道通信是借助文件系统实现的,那么是不是......没错,就是使用read和write函数,

接下来通过4种情况来示例这个通信过程

3.3.1.写端进程不写,读端进程一直读

我们让子进程每隔1秒写1次,父进程一直读

#include<iostream> #include<unistd.h> #include<cstdlib> #include<cstdio> #include <sys/types.h> #include <sys/wait.h> #include<cstring>  #define NUM 1024 using namespace std;  void Writer(int wfd) {     string s="hello,I am child";     pid_t self=getpid();     int number=0;      char buffer[NUM];     while(true)     {         buffer[0]=0;//字符串清空,只是为了提醒大家,我把这个数组当字符串了         snprintf(buffer,sizeof(buffer),"%s - %d - %d",s.c_str(),self,number);//把上面的东西都转化成字符串写入buffer数组         //发送消息给父进程         write(wfd,buffer,strlen(buffer));//不需要加1         sleep(1);//子进程休眠,父进程没有休眠啊         number++;     } }  void Reader(int rfd) {   char buffer[NUM];     while(true)     {         buffer[0]=0;//字符串清空,只是为了提醒大家,我把这个数组当字符串了         ssize_t n=read(rfd,buffer,sizeof(buffer));         if(n>0)//成功         {             buffer[n]=0;//0=='\0'             cout<<"father get a message["<<getpid()<<"]#"<<buffer<<endl;         }     }  } int main() {     int fd[2]={0};     int n=pipe(fd);      if(n<0)     {         cout<<"creat pipe failed"<<endl;         return 1;     }      pid_t id=fork();     if(id<0)     {         return 1;     }     else if(id==0)//child     {         close(fd[0]);//关闭读端                  Writer(fd[1]);                  close(fd[1]);         exit(0);     }   //father         close(fd[1]);//关闭写端                  Reader(fd[0]);                  pid_t rid =waitpid(id,nullptr,0);//会是         if(rid<0)         return 3;          close(fd[0]);      }

我们成功实现了父子进程之间的简单通信,我们可是没有给父进程可是没有休眠的,为什么父进程打印打的这么慢???这就是管道的特点,没有数据读,就等待

那么问题就来了,在每轮写完后数据之后,子进程休眠的这1秒期间,父进程在干吗?

        实际上,在子进程休眠的这5秒,父进程在等待子进程休眠结束,直到子进程再次写入数据时,父进程才会读取

  所以我们的 结论 就是:管道内部没有数据的时候,并且其中的写端不关闭自己的文件描述符时,读端就要进行阻塞等待,直到管道文件有数据

  • ①写端进程不写,读端进程一直读那么此时会因为管道里面没有数据可读,对应的读端进程会被阻塞挂起,直到管道里面有数据后,读端进程才会被唤醒。

3.3.2.读端进程不读,写端进程一直写

我们对上面代码进行修改,子进程一直写,父进程每隔5秒读1次

#include<iostream> #include<unistd.h> #include<cstdlib> #include<cstdio> #include <sys/types.h> #include <sys/wait.h> #include<cstring>  #define NUM 1024 using namespace std;  void Writer(int wfd) {     string s="hello,I am child";     pid_t self=getpid();     int number=0;      char buffer[NUM];     while(true)     {         buffer[0]=0;//字符串清空,只是为了提醒大家,我把这个数组当字符串了         snprintf(buffer,sizeof(buffer),"%s - %d - %d",s.c_str(),self,number);//把上面的东西都转化成字符串写入buffer数组         //发送消息给父进程         write(wfd,buffer,strlen(buffer));//不需要加1         number++;     } }  void Reader(int rfd) {   char buffer[NUM];     while(true)     {         buffer[0]=0;//字符串清空,只是为了提醒大家,我把这个数组当字符串了         ssize_t n=read(rfd,buffer,sizeof(buffer));         if(n>0)//成功         {             buffer[n]=0;//0=='\0'             cout<<"father get a message["<<getpid()<<"]#"<<buffer<<endl;         }         sleep(5);//父进程每隔5秒读一次,子进程一直写     }  } int main() {     int fd[2]={0};     int n=pipe(fd);      if(n<0)     {         cout<<"creat pipe failed"<<endl;         return 1;     }      pid_t id=fork();     if(id<0)     {         return 1;     }     else if(id==0)//child     {         close(fd[0]);//关闭读端                  Writer(fd[1]);                  close(fd[1]);         exit(0);     }   //father         close(fd[1]);//关闭写端                  Reader(fd[0]);                  pid_t rid =waitpid(id,nullptr,0);//会是         if(rid<0)         return 3;          close(fd[0]);      }

我们先执行一下,一瞬间就出现了下面这个 ,并且阻塞在这里了

进程运行到第5秒的时候就出现了下面这个

在之后就是下图这个情况了

 来分析一下我们编译运行程序会发现,写端对管道文件一直写入字符,但是到了一定的字符时却卡在这里了。

  其实这个时候 写端在阻塞,这是因为我们写入的对象,也就是 管道文件 被写满了 注意管道文件的大小依据平台的不同也各不相同

  所以我们得到的 结论 是:当管道内部被写满,且读端不关闭自己的文件描述符,写端写满管道之后,就要进行阻塞等待

  • ②读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被阻塞挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。

此外,我们发现怎么一次打印出这么多东西?

这就是读取的机制,一次性全读,管道有多少,读多少!!!! 

        前面的①②两种情况就能够很好的说明,管道是自带同步与互斥机制的读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。

        读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。

如何理解阻塞挂起 ?唤醒?       

         进程先立即停止执行,然后将PCB的状态改为阻塞状态,并将PCB插入相应的阻塞队列。
        当被阻塞进程所期待的事情发生,将阻塞进程从阻塞队列中移出,将其PCB的状态改为就绪状态(R),然后将PCB插入到就绪队列中.       

3.3.3.写端进程将数据写完后将写端关闭

子进程每隔1秒写1次,写了5秒后就不写了,父进程一直读

#include<iostream> #include<unistd.h> #include<cstdlib> #include<cstdio> #include <sys/types.h> #include <sys/wait.h> #include<cstring>  #define NUM 1024 using namespace std;  void Writer(int wfd) {     string s="hello,I am child";     pid_t self=getpid();     int number=0;      char buffer[NUM];     while(true)     {         sleep(1);//每隔1秒写1次         char c='c';         write(wfd,&c,1);         number++;         cout<<number<<endl;          if(number>=5)         break;     } }  void Reader(int rfd) {   char buffer[NUM];     while(true)     {         buffer[0]=0;//字符串清空,只是为了提醒大家,我把这个数组当字符串了         ssize_t n=read(rfd,buffer,sizeof(buffer));         if(n>0)//成功         {             buffer[n]=0;//0=='\0'             cout<<"father get a message["<<getpid()<<"]#"<<buffer<<endl;         }         cout<<"n:"<<n<<endl;     }  } int main() {     int fd[2]={0};     int n=pipe(fd);      if(n<0)     {         cout<<"creat pipe failed"<<endl;         return 1;     }      pid_t id=fork();     if(id<0)     {         return 1;     }     else if(id==0)//child     {         close(fd[0]);//关闭读端                  Writer(fd[1]);                  close(fd[1]);         exit(0);     }   //father         close(fd[1]);//关闭写端                  Reader(fd[0]);                  pid_t rid =waitpid(id,nullptr,0);//会是         if(rid<0)         return 3;          close(fd[0]);      }

这里每隔1秒写一次 ,还是很正常的

后面5秒过后,下面这个东西刷屏了

我们接着用监视窗口来监视一下:

前5秒是这个

5秒之后是这个

子进程退了,父进程没读到数据,为什么不阻塞,read返回值为什么变0了? 

当写端写了5次之后直接退了,那么读端进程就会变为僵尸状态。

我们可查man手册,read成功了就返回读到的字节数,0表示读到了文件结尾

所以我们读到了管道的文件结尾,不会阻塞

所以我们就能得出 结论

对于读端而言当写端不再写入,并且关闭了pipe,那么读端将会把管道内的内容读完,最后就会读到返回值为0,表示读取结束,类似于读到了文件的结尾

  • ③写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
  • 读端进程已经将管道当中的所有数据都读取出来了(读端就会read返回值0,代表文件结束),而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。     

这个时候就应该把代码修改一下了

#include<iostream> #include<unistd.h> #include<cstdlib> #include<cstdio> #include <sys/types.h> #include <sys/wait.h> #include<cstring>  #define NUM 1024 using namespace std;  void Writer(int wfd) {     string s="hello,I am child";     pid_t self=getpid();     int number=0;      char buffer[NUM];     while(true)     {         sleep(1);//每隔1秒写1次         char c='c';         write(wfd,&c,1);         number++;         cout<<number<<endl;          if(number>=5)         break;     } }  void Reader(int rfd) {   char buffer[NUM];     while(true)     {         buffer[0]=0;//字符串清空,只是为了提醒大家,我把这个数组当字符串了         ssize_t n=read(rfd,buffer,sizeof(buffer));         if(n>0)//成功读取         {             buffer[n]=0;//0=='\0'             cout<<"father get a message["<<getpid()<<"]#"<<buffer<<endl;         }         else if(n==0)//读到文件尾          {             cout<<"father read file done"<<endl;             break;          }         else//出错了             break;     }  } int main() {     int fd[2]={0};     int n=pipe(fd);      if(n<0)     {         cout<<"creat pipe failed"<<endl;         return 1;     }      pid_t id=fork();     if(id<0)     {         return 1;     }     else if(id==0)//child     {         close(fd[0]);//关闭读端                  Writer(fd[1]);                  close(fd[1]);         exit(0);     }   //father         close(fd[1]);//关闭写端                  Reader(fd[0]);                  pid_t rid =waitpid(id,nullptr,0);//会是         if(rid<0)         return 3;          close(fd[0]);      }

完美

3.3.4.读端进程将读端关闭,而写端进程还在一直向管道写入数据

我们让父进程先读他个5秒,然后不读了,子进程一直写

#include<iostream> #include<unistd.h> #include<cstdlib> #include<cstdio> #include <sys/types.h> #include <sys/wait.h> #include<cstring>  #define NUM 1024 using namespace std;  void Writer(int wfd) {          int number=0;      char buffer[NUM];     while(true)     {         sleep(1);//每隔1秒写1次         char c='c';         write(wfd,&c,1);         number++;         cout<<number<<endl;     } }  void Reader(int rfd) {   char buffer[NUM];   int cnt=0;     while(true)     {         buffer[0]=0;//字符串清空,只是为了提醒大家,我把这个数组当字符串了         ssize_t n=read(rfd,buffer,sizeof(buffer));         if(n>0)//成功         {             buffer[n]=0;//0=='\0'             cout<<"father get a message["<<getpid()<<"]#"<<buffer<<endl;         }         else if(n==0)//读到文件尾          {             cout<<"father read file done"<<endl;             break;          }         else//出错了             break;                          cnt++;             sleep(1);             if(cnt>5) break;//读5次就跑啦!!!!      }  } int main() {     int fd[2]={0};     int n=pipe(fd);      if(n<0)     {         cout<<"creat pipe failed"<<endl;         return 1;     }      pid_t id=fork();     if(id<0)     {         return 1;     }     else if(id==0)//child     {         close(fd[0]);//关闭读端                  Writer(fd[1]);                  close(fd[1]);         exit(0);     }   //father         close(fd[1]);//关闭写端                  Reader(fd[0]);         close(fd[0]);//父进程不读啦         cout<<"father close read fd"<<fd[0]<<endl;         sleep(5);//为了观察子进程的僵尸状态                  pid_t rid =waitpid(id,nullptr,0);//会是         if(rid<0)         return 3;      }

我们看看监控情况

而我们发现似乎也没什么不对啊?读取完之后不就直接退出了吗?

你应该仔细想想,我们仅仅是关闭了读的文件描述符,但是没有关闭写的文件描述符啊。

  这就是最后一个 结论当读端不再进行读取操作,并且关闭自己的文件描述符fd,而写端依旧在写。那么OS就会通过信号(SIGPIPE)的方式直接终止写端的进程

  • ④读端进程将读端关闭,而写端进程还在一直向管道写入数据,没有进程读取,那么写入的数据就没有意义,那么操作系统会将写端进程杀掉。
  • 既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。 
  • 管道是单向通信,如果读端不读数据且把文件描述符关闭,那么写端做的就没有意义了。写端相当于废弃的动作,浪费资源,所以OS直接将子进程干掉。为什么?
  • OS不做不做任何浪费空间或者低效的事情,只要发现OS一定要把这个事情修正了。 

如何证明读端是被13号信号杀死的?

        我们采用的是父进程读子进程写的方式,也就是说将来子进程被杀死而父进程则可以通过wait的方式来获取子进程退出时的异常!

int status = 0; pid_t rid = waitpid(id, &status, 0);  if(rid == id) {     printf("exit code : %d, exit signal : %d\n", WEXITSTATUS(status), status & 0x7F); }

7)使用命令查看信号 kill - l

 4.验证管道的大小

管道的容量是有限的,如果管道已满,那么写端将阻塞或失败,需要了解一下管道的大小             

①方法一:使用man手册

根据man手册,在2.6.11之前的Linux版本中,管道的最大容量与系统页面大小相同,从Linux 2.6.11往后,管道的最大容量是65536字节。

查看Linux系统版本

这里使用的是Linux 2.6.11之后的版本,因此管道的最大容量是65536字节。  

②方法二:使用ulimit命令

可以使用ulimit -a 命令,查看当前资源限制的设定, 管道的最大容量是 512 × 8 = 4096 字节

③写代码验证管道容量

  • 根据man手册得到的管道容量与使用ulimit命令得到的管道容量不同,测试验证
  • 代码概述: 读进程一直不读,写进程一直写,直到管道被写满 
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h>   int main() { 	int fd[2] = { 0 };   	if (pipe(fd) < 0){ //使用pipe创建匿名管道 		perror("pipe"); 		return 1; 	}   	pid_t id = fork(); //使用fork创建子进程 	if (id == 0){ //child  		close(fd[0]); //子进程关闭读端   		char c = '.';//一个字符一个字节 		int count = 0; 		while (1){ 			write(fd[1], &c, 1); 			count++; 			printf("%d\n", count); //打印当前写入的字节数 		}   		close(fd[1]); 		exit(0); 	}     	//father 	close(fd[1]); //父进程关闭写端          //父进程不读取数据   	waitpid(id, NULL, 0); 	close(fd[0]); 	return 0; }
  • 在读端进程不进行读取的情况下,写端进程最多写65536字节的数据就被操作系统挂起了,也就是说,我当前Linux版本中管道的最大容量是65536字节 

5.管道的特点

①管道内部自带同步与互斥机制。

我们将一次只允许一个进程使用的资源,称为临界资源。管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。

临界资源是需要被保护的,若是我们不对管道这种临界资源进行任何保护机制,那么就可能出现同一时刻有多个进程对同一管道进行操作的情况,进而导致同时读写、交叉读写以及读取到的数据不一致等问题。

为了避免这些问题,内核会对管道操作进行同步与互斥

  1. 同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
  2. 互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。

实际上,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。

对于管道的场景来说,

  • 互斥就是两个进程不可以同时对管道进行操作,它们会相互排斥,必须等一个进程操作完毕,另一个才能操作
  • 而同步也是指这两个不能同时对管道进行操作,但这两个进程必须要按照某种次序来对管道进行操作。

        也就是说,互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,而同步的任务之间则有明确的顺序关系。
        子进程往管道里面写入,子进程去读取的时候,有数据就拿上来,没数据就不在读取而是阻塞式的等待管道数据写入,并非父进程sleep了,而是因为子进程写的慢,父进程必须等,而引起好像父进程sleep了,这种—个等另一个的现象叫做同步。
                                      

②管道的生命周期随进程。

管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。

我们不需要去关闭管道,系统会自己回收

③管道提供的是流式服务。

我们一般所谓的流式概念就是,给你提供一个通信的信道,你的写端就直接写,读端直接读,但是具体写多少,读多少完全有上层决定。底层就只是提供一个数据通信的信道就完了,它不关心数据本身的一些细节格式,这叫做面向字节流。

  1. 流式服务: 数据没有明确的分割,一次拿多少数据都行。
  2. 数据报服务: 数据有明确的分割,拿数据按报文段拿。

④管道是半双工通信的。

在数据通信中,数据在线路上的传送方式可以分为以下三种:

  1. 单工通信:单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
  2. 半双工通信:半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
  3. 全双工通信:全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。

管道是半双工的,数据只能向一个方向流动,需要双方通信时,需要建立起两个管道。

6.几个小问题

说了这么多,貌似并没有解释为什么会叫匿名管道?

        父子进程之间进行通信时,临时创建的这个管道文件并没有对应的文件名和inode,只是系统分配的一块内存空间,可以以文件的形式被父子进程打开或关闭,这一切工作都在不知不觉中由OS全部完成了,所以称为匿名管道,等命名管道文件看完,也可以回头对比着理解

现在我们回过头来理解命令ps ajx | grep pid

管道符|用于将一个命令的输出作为另一个命令的输入。在这个命令中,ps ajx命令的输出将作为grep pid命令的输入。 当这个命令在shell中执行时,shell会创建一个匿名管道。ps ajx命令形成的进程作为管道的写端,将其输出写入管道;而grep pid命令形成的进程作为管道的读端,从管道中读取输入。

         因此,ps ajx和grep pid都作为shell的子进程,通过匿名管道进行通信。ps ajx将其输出写入管道,而grep pid从管道中读取数据,实现了两个命令之间的通信

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!