【Linux】历程间通信1——管道概念,匿名管道

打印 上一主题 下一主题

主题 972|帖子 972|积分 2916

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
1.历程间通信介绍

        历程是盘算机体系分配资源的最小单位(严酷说来是线程)。每个历程都有自己的一部分独立的体系资源,相互是隔离的。为了能使不同的历程相互访问资源并举行协调工作,才有了历程间通信。
      历程间通信,简称为 IPC(Interprocess communication),顾名思义,就是历程与历程之间互通信交流,OS保证了各历程之间相互独立,但这不意味着历程与历程之间就必须完全隔离开,在不少的环境下,历程之间必要相互配合共同完成某项使命,这就要求各历程之间可以或许相互交流。

1.1、历程间通信的概念

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

1.2.历程间通信的目的

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

  • 数据传输:一个历程必要将它的数据发送给另一个历程
  • 资源共享:多个历程之间共享同样的资源
  • 通知事故:一个历程必要向另一个或一组历程发送消息,通知它(它们)发生了某种事故(如历程克制时要通知父历程)
  • 历程控制:有些历程渴望完全控制另一个历程的执行(如Debug历程),此时控制历程渴望可以或许拦截另一个历程的所有陷入和异常,并可以或许及时知道它的状态改变
总得来说,实现历程间通信就是为了历程之间可以或许协同完成某项使命
1.3..历程间通信的本质

   历程间通信的本质是让 不同的历程看到同一份资源(内存 , 文件,内核缓冲等)
  资源由谁(OS的哪些模块)提供 , 就有了不同的历程间通信方式!
这里的模块可以是: (文件–管道) , (OS内核IPC提供- SystemV IPC) , (网络–套接字)
                                
历程运行的时候是具有独立性的!(数据层面) , 因此历程之间要实现通信是非常困难的。
各个历程之间若想实现通信,一定要借助第三方资源,这些历程就可以通过向这个第三方资源写入或是读取数据,进而实现历程之间的通信,这个第三方资源现实上就是操作体系提供的一段内存区域。
通信的本质就是”数据的拷贝“

1.4.历程间通信分类

   管道
  

  • 匿名管道
  • 定名管道
    System V IPC
  

  • System V 消息队列
  • System V 共享内存
  • System V 信号量
    POSIX IPC
  

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁
            历程间通信早先有许多不同的的相关协议,随着不断的实践发展,目前主要有两个主流的通信规则,一个是System V,另一个是POSIX
  

  • POSIX:让通信过程可以跨主机,System V 标准如今比较少用了,但其通信速率极快的共享内存照旧值得深入学习的
  • System V:聚焦在本地通信,POSIX 是 Unix 体系的一个设计标准,许多类 Unix 体系也在支持兼容这个标准,如 Linux , POSIX 标准具有跨平台性,就连 Windows 也对其举行了支持,后续学习 同步与互斥 时,所使用的信号量等都是出自 POSIX 标准,这是历程间通信的学习重点,POSIX 标准支持网络中通信,比如 套接字(socket) 就在此标准中
          由于System V由于订定的比较早,不支持跨主机间的通信,在今天属于比较陈旧的标准了,因此我们会将更多精力放在POSIX上,不过POSIX通信并不是这篇文章的内容,因此不会提及,而System V我们关注比较重要的共享内存的概念,通信规则并非只局限于POSIX和System V,我们先介绍比较简朴易继承的管道通信
  管道可以说是十分古老且简朴了,适合深入学习,探究历程间通信时的原理及执行流程 
2.管道通信

   1.什么是管道?
  

  • 管道是Unix中最古老的历程间通信的形式。
  • 我们把从一个历程毗连到另一个历程的一个数据流称为一个"管道"
  • 管道是一种最基本的历程间通信机制。 把一个历程毗连到另一个历程的一个数据流称为一个“管道”,通常是用作把一个历程的输出通过管道毗连到另一个历程的输入
  管道的实质是一个内核缓冲区,历程以先进先出的方式从缓冲区存取数据,管道一端的历程顺序的将数据写入缓冲区,另一端的历程则顺序的读出数据。 该缓冲区可以看做是一个循环队列,读和写的位置都是自动增长的,不能随意改变,一个数据只能被读一次,读出来以后在缓冲区就不复存在了。 

   2.管道, 其实是 一个打开的文件 . 但是这个文件很特殊, 向这个文件中写入数据现实上并不会真正写入磁盘中.
  Linux 中一切皆文件,以是管道本质上就是一个文件  
在介绍Linux体系的文件形貌符时, 简朴介绍了Linux体系中 形貌已打开文件的结构体files_struct, 其中存储着 指向打开文件的数组fd_array, 此数组的范例是 struct files*.
而这个 files结构体中, 直接或间接形貌了文件的所有属性, 以及 此文件的缓冲区相关信息:

缓冲区信息中, 包含着形貌文件的inode结构体, 而inode结构体中其实形貌着一个团结体:

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

通过文件的inode, 体系可以辨别出打开的文件是管道文件.
而**向管道文件中写入数据现实上并不会写入到磁盘上, 而是只写入到文件的缓冲区中** , 因为管道文件主要是用来源程间通信的, 如果先写入磁盘另一个历程再读取, 整个过程就太慢了
这种不现实存储数据的举动特点, 其实也符合生活中管道的特点, 管道不能用来存储资源, 只能用来传输资源
并且, 除了管道不现实存储资源以外, 管道另有一个特点:管道是单向传输的
这是管道的特点, Linux的管道也是遵循这个特点的, 也就是说, 两个历程间使用管道通信时, 其中一个历程若以只写方式打开管道, 那么另一个历程就只能以只读方式打开文件.

   3.管道通信主要是借助文件体系来实现的,怎么明白呢?
          我们假设现在体系上的历程A和历程B要相互通信,A不能直接去B内里读数据,因为历程具有独立性,那该怎么办呢?这就必要找一块空间C,空间C用来存放通信两边通信的数据,现在历程A要给B发送数据,那么A和B要向体系声明建立毗连,申请一块空间C,然后A往空间C里发送数据,B从空间C里读取数据,这样A就实现了和B的通信这块空间C就像一根管道一样,毗连着A与B,整个管道通信的基本原理就是如此,当然这只解释了管道名称的由来,并没有解释管道通信是借助文件体系来实现的
        我们要理清楚怎样在Linux体系中让两个历程读取到同一块内存空间,如果看过IO篇的同学应该会想到,那就是通过文件,历程从磁盘中或除自身以外的其他可读写的内存区域中读取或写入数据主要是通过文件体系来办理的只要体系在内存中创建一个文件,A历程打开这个文件,B历程也打开这个文件,那么A与B就通过这个文件毗连起来举行通信了,这就是管道通信是借助文件体系来实现的原因
   我们举个例子
          在shell中执行命令,经常会将上一个命令的输出作为下一个命令的输入,由多个命令配合完成一件事变。而这就是通过管道来实现的。|这个竖线就是管道符号
  1. ls -l | grep string   //grep是抓取指令
复制代码


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

  3.匿名管道

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

  • 一种是父子历程或兄弟历程之间的通信
  • 另一种则是没有亲属关系的历程间的通信
匿名管道的创建, 不会指定打开文件的文件名、文件路径等, 即不会有目的的打开文件
只是在内存中打开一个文件, 用于历程间的通信
而由于匿名管道是非明白目的的文件, 也就意味着两个完全不相关的历程是无法一起访问这个管道的, 因为完全不相关的历程无法找到这个管道文件.
这也就意味着, 匿名管道其实只能用于具有血缘关系的历程间通信.
   父子历程之间是共享代码和数据的,但这个数据共享只能用来读,一旦一方试图使数据发生变化会触发写时拷贝,父历程与子历程的数据就存放到了不同的地址,这时父子两边该怎样通知对方数据发生了变化呢?
          这就是匿名管道通信要研究的东西
          历程间通信的本质就是,让不同的历程看到同一份资源,使用匿名管道实现父子历程间通信的原理就是,让两个父子历程先看到同一份被打开的文件资源,然后父子历程就可以对该文件举行读写操作,进而实现父子历程间通信

        子历程拷贝父历程的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.历程是怎样知道管道被打开了什么端口的?或者说 历程是怎样知道管道被打开了几次的?
  其着实file结构体中, 存在一个计数变量 f_count:

不过, 这个变量现实上照旧一个结构体, 用于计数
很好,到这里原理结束
3.2.pipe函数

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


  1. #include <unistd.h>
  2. int pipe(int fd[2]);
  3. //功能:创建一个无名管道
复制代码


  • fd:文件形貌符数组,其中fd[0]表示读端, fd[1]表示写端
  • 返回值:成功返回0,失败返回错误代码
    注意:该函数的参数是输出型参数,在传参fd时要先创建fd,也就是事先声明 int fd[2];
  且, pipe(), 如果 创建管道成功, 则返回0, 否则返回-1, 并设置errno
pipe体系调用的作用是, 打开一个管道文件. 其参数是一个 输出型参数
在pipe体系调用 执行成功之后, 参数数组内会存储两个元素 :

  • pipe[0], 存储以 只读方式 打开管道时获得的fd
  • pipe[1], 存储以 只写方式 打开管道时获得的fd
之后就可以根据需求, 选择父子历程的端口关闭
3.3.创建匿名管道

我们将按照原理一步一步来解说
  1. int fd[2];//这个语句就是用来记录进程分别以读写端打开管道文件的fd
  2. int main() {
  3.     int fd[2];
  4.     int check = pipe(fd);
  5.    
  6.     if (check != 0) {
  7.         printf("create pipe error\n");
  8.         return 0;
  9.     }
  10. }
复制代码

父历程在创建子历程时,子历程会拷贝一份父历程的历程地址空间,同样的,子历程也会拷贝父历程的文件形貌符表
  1. int main() {
  2.     int fd[2];
  3.     int check = pipe(fd);
  4.    
  5.     if (check != 0) {
  6.         printf("create pipe error\n");
  7.         return 0;
  8.     }
  9.     pid_t id = fork();
  10.     if (id > 0) { /*执行父进程代码*/ };
  11.     if (id = 0) { /*执行子进程代码*/ };
  12.     return 0;
  13. }
复制代码

接下来,我们明白父子历程谁是读端,谁是写端,就可以举行通信了,这里我们让父历程写数据给子历程,那么父历程就要关闭自己的读端,子历程就要关闭自己的写端
   fd[0]是读端,fd[1]是写端
  巧记:按照读音的顺序,读写,01,正好对应。另有1像一支笔,以是是写端,0像伸开的嘴,以是是读端
  1. int main() {
  2.     int fd[2];
  3.     int check = pipe(fd);
  4.    
  5.     if (check != 0) {
  6.         printf("create pipe error\n");
  7.         return 0;
  8.     }
  9.     pid_t id = fork();
  10.     if (id > 0) {
  11.        close(fd[0]);
  12.        /*关闭父进程的读端,接着执行父进程代码*/
  13.     };
  14.     if (id = 0) {
  15.        close(fd[1]);
  16.        /*关闭子进程的写端,接着执行子进程代码*/
  17.     };
  18.     return 0;
  19. }
复制代码
现在读写两边都确定了,那写方怎样给读方发数据,读方又怎样读取写方的数据呢?
既然管道通信是借助文件体系实现的,那么是不是......没错,就是使用read和write函数,接下来通过一个demo来示例这个通信过程
  1. #include<stdio.h>
  2. #include<string.h>
  3. #include<unistd.h>
  4. #include<stdlib.h>
  5. #include<sys/stat.h>
  6. #include<sys/types.h>
  7. #include<fcntl.h>
  8. #include<sys/wait.h>
  9. int main()
  10. {
  11.     int fd[2];
  12.     int check = pipe(fd);//声明创建管道
  13.     if (check != 0) { printf("create pipe error"); return 0; }
  14.     char test_buff[64] = "this is a communication test";
  15.     pid_t id = fork();
  16.     if (id > 0) {
  17.         close(fd[0]);
  18.         write(fd[1], test_buff, sizeof(test_buff));
  19.         wait();
  20.     }
  21.     if (id == 0) {
  22.         close(fd[1]);
  23.         memset(test_buff, 0, sizeof(test_buff));
  24.         read(fd[0], test_buff, sizeof(test_buff));
  25.         printf("测试结果:%s\n", test_buff);
  26.     }
  27.    
  28.     return 0;
  29. }
复制代码

我们成功实现了父子历程之间的通信,接下来我们修改部分代码,然后刨析一下通信的过程
如下是修改后的代码以及运行结果
  1. #include<iostream>
  2. #include <string.h>
  3. #include <unistd.h>
  4. #include <stdlib.h>
  5. #include <sys/stat.h>
  6. #include <sys/types.h>
  7. #include <fcntl.h>
  8. #include<sys/wait.h>
  9. using namespace std;
  10. int main()
  11. {
  12.     int fd[2];
  13.     int check = pipe(fd);
  14.    
  15.     if (check != 0)   {  std::cout << "create pipe error" <<endl; return 0; }
  16.    
  17.     char test_buff[64] = "this is a communication test";
  18.     pid_t id = fork();
  19.     if (id > 0)
  20.     {
  21.         close(fd[0]);
  22.         write(fd[1], test_buff, sizeof(test_buff));
  23.         std::cout << "我是父进程,我的pid是"<<getpid() << endl;
  24.     }
  25.     if (id == 0)
  26.     {
  27.         close(fd[1]);
  28.         while (true)
  29.         {
  30.             sleep(1);
  31.             memset(test_buff, 0, sizeof(test_buff));
  32.             read(fd[0], test_buff, sizeof(test_buff)-1);
  33.             std::cout << "我是子进程,我的pid是"<<getpid() << endl;
  34.             std::cout << test_buff << endl;
  35.         }
  36.     }
  37.    
  38.     wait(nullptr);
  39.     return 0;
  40. }
复制代码

   1.观察运行结果可以发现,子历程循环两次后就卡在了那里不动了,这是什么原因呢?
          这是因为read函数是一个壅闭式函数,在上述步伐中,父历程往管道中写入一次数据后,就进入了wait壅闭,等待回收子历程,子历程则是循环从管道中读取数据,等到把管道中的数据读完的时候,而父历程又没有关闭它的写端,此时子历程的read函数就会进入读堵塞状态,等待父历程继续向管道中写入数据,父历程已经进入了历程壅闭等待,天然不会再向管道中写入数据,因此就进入了卡死状态
        2.   仔细的小同伴可能会有疑惑,父历程只向管道中写入一次数据,子历程读取一次就应该将数据读取完了呀,子历程循环一次就该进入堵塞,而运行结果表现子历程的循环举行了两次呢?
  这是因为,我们使用write函数举行写入时,写入的巨细是sizeof(test_buff)也就是64个字节,而我们用read函数读取数据的时候,读取的是sizeof(test_buff)-1,也就是63个字节,此时管道中还剩一个字节,管道并不为空,因此read函数还可以读取一次,以是循环就举行了两次
   3.可能你会想为什么要读取sizeof(test_buff)-1个字节呢?一次读完不好吗?
  这是因为在C语言中有些函数会自动给字符数组的末端添加/0,而有的函数又不会自动添加,如果一次读完,遇到了末端自动添加/0的函数,就会将末端的数据给覆盖掉,导致数据丢失,因此在不能分辨某个字符处置惩罚函数是否会自动在字符末端添加/0的时候,为了安全,我们统一把字符数组的最后一位给留出来,也就是数据只读取字符数组巨细-1个
上述的环境是父历程进入壅闭等待时,并没有关闭写端,导致子历程的read函数误认为父历程还会向管道中写入数据,于是就进入壅闭状态一直等待。
   4.如果父历程写完了,并且关闭了自己的写端呢?
  如果管道中另有数据,那么子历程的read函数会继续读取数据,如果管道中没有数据了,那么read函数就会返回0,不会进入壅闭等待状态,因此在循环读取的场景下,一定要注意接收read函数的返回值,否则会进入死循环的状态的
   5.如果父历程往管道中写入的数据很快,而子历程读取的速率比较慢的话,会出现什么环境呢?
  我们前面说过管道通信文件是借助文件体系实现的,但是管道通信文件跟一样平常的文件还不太一样,管道通信文件不像平凡文件一样可以存放到磁盘中,管道通信文件不存放到磁盘上,和磁盘没有关系且没有inode,是操作体系暂时分配的一块固定巨细的内存,我们也称其为管道缓冲区,以是当写的速率太快,读的速率太慢,管道缓冲区被写满的时候,此时写方就会进入写入壅闭状态,直到缓冲区足够再次容纳写入的数据时,才会再次允许写入

   6.另有一种场景,如果写方还在继续向管道缓冲区写入数据时,而读方却关闭了读端,那么此时体系就会克制并杀死写端,因为读方都已经关闭读端了,再写也没故意义了
  这样子就引出了四大特殊环境,我们下面再说
   7.说了这么多,貌似并没有解释为什么会叫匿名管道?
  父子历程之间举行通信时,暂时创建的这个管道文件并没有对应的文件名和inode,只是体系分配的一块内存空间,可以以文件的形式被父子历程打开或关闭,这一切工作都在不知不觉中由OS全部完成了,以是称为匿名管道,等定名管道文件看完,也可以回头对比着明白
   8.现在我们回过头来明白命令ps ajx | grep pid
  管道符|用于将一个命令的输出作为另一个命令的输入。在这个命令中,ps ajx命令的输出将作为grep pid命令的输入。 当这个命令在shell中执行时,shell会创建一个匿名管道。ps ajx命令形成的历程作为管道的写端,将其输出写入管道;而grep pid命令形成的历程作为管道的读端,从管道中读取输入。 因此,ps ajx和grep pid都作为shell的子历程,通过匿名管道举行通信。ps ajx将其输出写入管道,而grep pid从管道中读取数据,实现了两个命令之间的通信
4.管道的4中特殊环境 

我们看段代码,先确定几个事项,父子历程读写题目,这里我以父历程为写端,子历程为读端(相反也行)。
  1. #include<stdio.h>
  2. #include<string.h>
  3. #include<stdlib.h>
  4. #include<unistd.h>
  5. #include<sys/stat.h>
  6. #include<sys/wait.h>
  7. #include<sys/types.h>
  8. void writer(int wfd)//写端调用
  9. {
  10.     const char* str = "hello father, I am child";
  11.     char buffer[128];
  12.     int cnt = 0;
  13.     pid_t pid = getpid();
  14.     while(1)
  15.     {
  16.         snprintf(buffer, sizeof(buffer), "messge:%s, pid: %d, count: %d\n", str, pid, cnt);//向buffer内写入str
  17.         write(wfd, buffer, sizeof(buffer));//通过系统调用对管道文件进行写入
  18.         cnt++;
  19.         sleep(1);
  20.     }
  21. }
  22. void reader(int rfd)//读端调用
  23. {
  24.     char buffer[1024];
  25.     while(1)
  26.     {
  27.         ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);//系统文件与C语言没关系所以不算 '\0'
  28.         (void)n;//返回值用不到,避免警告,制造的假应用场景
  29.         printf("father get a message: %s", buffer);
  30.     }
  31. }
  32. int main()
  33. {
  34.     // 创建管道
  35.     int pipefd[2];
  36.     int n = pipe(pipefd);
  37.     if(n < 0) return 1;
  38.     printf("pipefd[0]: %d, pipefd[1]: %d\n", pipefd[0]/*reader*/, pipefd[1]/*writer*/);
  39.     // fork子进程
  40.     pid_t id = fork();
  41.     if(id == 0)
  42.     {
  43.         // child w端
  44.         close(pipefd[0]);
  45.         writer(pipefd[1]);
  46.         exit(0);
  47.     }
  48.     //father r端
  49.     close(pipefd[1]);
  50.    
  51.     reader(pipefd[0]);//通过系统调用 对管道文件读取
  52.     wait(NULL);
  53.     return 0;
  54. }
复制代码

我们通过对上面这个代码的不断修改来解说读写端的四种环境
 ①写端历程不写,读端历程一直读

照旧上述匿名管道测试代码,子历程一直在写,父历程一直在读子历程写的数据,现在我们让子历程等待五秒之后再对管道文件举行写入:

        那么题目就来了,在子历程休眠的这五秒期间,父历程在干吗?
        现实上,在子历程休眠的这5秒,父历程在等待子历程休眠结束,直到子历程再次写入数据时,父历程才会读取
  以是我们的 结论 就是:管道内部没有数据的时候,并且其中的写端不关闭自己的文件形貌符时,读端就要举行壅闭等待,直到管道文件有数据
   

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

第二种环境,当写端一直在对管道文件举行写入,而读端却不再对管道文件(一直执行sleep)举行读取,我们修改写端接口如下:
  1. void writer(int wfd)
  2. {
  3.     const char* str = "hello father, I am child";
  4.     char buffer[128];
  5.     int cnt = 0;
  6.     pid_t pid = getpid();
  7.     while(1)
  8.     {
  9.         // snprintf(buffer, sizeof(buffer), "messge:%s, pid: %d, count: %d\n", str, pid, cnt);//向buffer内写入str
  10.         // write(wfd, buffer, sizeof(buffer));//通过系统调用对管道文件进行写入
  11.         char* ch = "X";
  12.         write(wfd, ch, 1);
  13.         cnt++;
  14.         printf("cnt: %d\n", cnt);
  15.     }
  16. }
复制代码

 如果我们编译运行步伐我们会发现,写端对管道文件一直写入一个字符,但是到了第65536个字符时却卡在这里了。
  其实这个时候 写端在壅闭,这是因为我们写入的对象,也就是 管道文件 被写满了从计数器我们可以看出一个管道文件的巨细为 65536 个字节(ubuntu20.04)!也就是 64KB 巨细! 注意管道文件的巨细依据平台的不同也各不相同
  以是我们得到的 结论 是:当管道内部被写满,且读端不关闭自己的文件形貌符,写端写满之后,就要举行壅闭等待
   

  • ②读端历程不读,写端历程一直写,那么当管道被写满后,对应的写端历程会被壅闭挂起,直到管道当中的数据被读端历程读取后,写端历程才会被唤醒。
               前面的①②两种环境就可以或许很好的说明,管道是自带同步与互斥机制的读端历程和写端历程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。
           读端历程读取数据的条件是管道内里有数据,写端历程写入数据的条件是管道当中另有空间,若是条件不满足,则相应的历程就会被挂起,直到条件满足后才会被再次唤醒。
   
       怎样明白壅闭挂起 ?唤醒?       
            历程先立即克制执行,然后将PCB的状态改为壅闭状态,并将PCB插入相应的壅闭队列。
        当被壅闭历程所期待的事变发生,将壅闭历程从壅闭队列中移出,将其PCB的状态改为停当状态(R),然后将PCB插入到停当队列中.       
   
  ③写端历程将数据写完后将写端关闭

当写端对管道文件缓冲区举行了有限次的写入,并且把写端的文件形貌符关闭,而读端我们保持正常读取内容,读端多的仅仅把读端的返回值打印出来。


我们发现当10读取执行完成之后,就一直在执行读取操作,而我们读取使用的 read 接口的返回值却从0变为了1。我们接着用监督窗口来监督一下:

当写端写了10个数据之后将文件形貌符关闭,那么读端历程就会变为僵尸状态。由此我们可以得出,read接口返回值的含义 是,当写端克制写入并关闭了文件形貌符,read的返回值为0,正常读取的返回值 >0
以是我们可以这样修改读端的代码:
  1. void reader(int rfd)
  2. {
  3.     char buffer[1024];
  4.     while (1)
  5.     {
  6.         ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
  7.         if (n > 0)
  8.             printf("father get a message: %s, ret: %ld\n", buffer, n);
  9.         else if (n == 0)
  10.         {
  11.             printf("read pipe done, read file done!\n");
  12.             break;
  13.         }
  14.         else
  15.             break;
  16.     }
  17. }
复制代码
 

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

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

我们把环境三最后的代码变换一下,读端读取改为有次数限制,并且读取一定次数之后关闭读的文件形貌符,而写端无穷制对管道文件写入,那么我们会看到什么现象呢?


而我们发现似乎也没什么不对啊?读取完之后不就直接退出了吗?
你应该仔细想想,我们仅仅是关闭了读的文件形貌符,但是没有关闭写的文件形貌符啊。
  这就是最后一个 结论当读端不再举行读取操作,并且关闭自己的文件形貌符fd,而写端仍旧在写。那么OS就会通过信号(SIGPIPE)的方式直接克制写端的历程
   

  • ④读端历程将读端关闭,而写端历程还在一直向管道写入数据,没有历程读取,那么写入的数据就没故意义,那么操作体系会将写端历程杀掉。
  • 既然管道当中的数据已经没有历程会读取了,那么写端历程的写入将没故意义,因此操作体系直接将写端历程杀掉。而此时子历程代码都还没跑完就被克制了,属于异常退出,那么子历程必然收到了某种信号。 
  • 管道是单向通信,如果读端不读数据且把文件形貌符关闭,那么写端做的就没故意义了。写端相称于废弃的动作,浪费资源,以是OS直接将子历程干掉。为什么?
  • OS不做不做任何浪费空间或者低效的事变,只要发现OS一定要把这个事变修正了。 
    怎样证实读端是被13号信号杀死的?
          我们采用的是父历程读子历程写的方式,也就是说将来子历程被杀死而父历程则可以通过wait的方式来获取子历程退出时的异常!
  1. int status = 0;
  2. pid_t rid = waitpid(id, &status, 0);
  3. if(rid == id)
  4. {
  5.     printf("exit code : %d, exit signal : %d\n", WEXITSTATUS(status), status & 0x7F);
  6. }
复制代码

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

   5.验证管道的巨细

管道的容量是有限的,如果管道已满,那么写端将壅闭或失败,必要了解一下管道的巨细             
①方法一:使用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命令得到的管道容量不同,测试验证
  • 代码概述: 读历程一直不读,写历程一直写,直到管道被写满 
  1. #include <unistd.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <sys/wait.h>
  5. int main()
  6. {
  7.         int fd[2] = { 0 };
  8.         if (pipe(fd) < 0){ //使用pipe创建匿名管道
  9.                 perror("pipe");
  10.                 return 1;
  11.         }
  12.         pid_t id = fork(); //使用fork创建子进程
  13.         if (id == 0){ //child
  14.                 close(fd[0]); //子进程关闭读端
  15.                 char c = '.';
  16.                 int count = 0;
  17.                 while (1){
  18.                         write(fd[1], &c, 1);
  19.                         count++;
  20.                         printf("%d\n", count); //打印当前写入的字节数
  21.                 }
  22.                 close(fd[1]);
  23.                 exit(0);
  24.         }
  25.         //father
  26.         close(fd[1]); //父进程关闭写端
  27.    
  28.     //父进程不读取数据
  29.         waitpid(id, NULL, 0);
  30.         close(fd[0]);
  31.         return 0;
  32. }
复制代码


  • 在读端历程不举行读取的环境下,写端历程最多写65536字节的数据就被操作体系挂起了,也就是说,我当前Linux版本中管道的最大容量是65536字节 

6.管道的特点


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

我们将一次只允许一个历程使用的资源,称为临界资源。管道在同一时候只允许一个历程对其举行写入或是读取操作,因此管道也就是一种临界资源。
临界资源是必要被掩护的,若是我们不对管道这种临界资源举行任何掩护机制,那么就可能出现同一时候有多个历程对同一管道举行操作的环境,进而导致同时读写、交叉读写以及读取到的数据不一致等题目。
   为了克制这些题目,内核会对管道操作举行同步与互斥
  

  • 同步: 两个或两个以上的历程在运行过程中协同步调,按预定的先后次序运行。比如,A使命的运行依赖于B使命产生的数据。
  • 互斥: 一个公共资源同一时候只能被一个历程使用,多个历程不能同时使用公共资源。
  现实上,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。
   对于管道的场景来说,
  

  • 互斥就是两个历程不可以同时对管道举行操作,它们会相互排挤,必须等一个历程操作完毕,另一个才能操作
  • 而同步也是指这两个不能同时对管道举行操作,但这两个历程必须要按照某种次序来对管道举行操作。
          也就是说,互斥具有唯一性和排它性,但互斥并不限制使命的运行顺序,而同步的使命之间则有明白的顺序关系。
        子历程往管道内里写入,子历程去读取的时候,有数据就拿上来,没数据就不在读取而是壅闭式的等待管道数据写入,并非父历程sleep了,而是因为子历程写的慢,父历程必须等,而引起似乎父历程sleep了,这种—个等另一个的现象叫做同步。
                                      
②管道的生命周期随历程。

管道本质上是通过文件举行通信的,也就是说管道依赖于文件体系,那么当所有打开该文件的历程都退出后,该文件也就会被开释掉,以是说管道的生命周期随历程。
③管道提供的是流式服务。

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

  • 流式服务: 数据没有明白的分割,一次拿多少数据都行。
  • 数据报服务: 数据有明白的分割,拿数据按报文段拿。
④管道是半双工通信的。

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

  • 单工通信:单工模式的数据传输是单向的。通信两边中,一方固定为发送端,另一方固定为接收端。
  • 半双工通信:半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
  • 全双工通信:全双工通信允许数据在两个方向上同时传输,它的能力相称于两个单工通信方式的结合。全双工可以同时(瞬时)举行信号的双向传输。
管道是半双工的,数据只能向一个方向流动,必要两边通信时,必要建立起两个管道。

7.管道的读写规则 



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

雁过留声

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表