进程间通信---对管道的详细讲解(图文案例讲解)

🏠 大家好,我是 兔7 ,一位努力学习C++的博主~💬

🍑 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀

🚀 如有不懂,可以随时向我提问,我会全力讲解~

🔥 如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!

🔥 你们的支持是我创作的动力!

🧸 我相信现在的努力的艰辛,都是为以后的美好最好的见证!

🧸 人的心态决定姿态!

🚀 本文章CSDN首发!

目录

0. 前言

1. 管道

1.1 什么是管道

2. 匿名管道

2.1 用fork来共享管道原理

2.2 站在文件描述符角度-深度理解管道

2.3 站在内核角度-管道本质

2.4 管道特点

3. 管道读写规则

4. 命名管道


0. 前言

        此博客为博主以后复习的资料,所以大家放心学习,总结的很全面,每段代码都给大家发了出来,大家如果有疑问可以尝试去调试。

        大家一定要认真看图,图里的文字都是精华,好多的细节都在图中展示、写出来了,所以大家一定要仔细哦~

        感谢大家对我的支持,感谢大家的喜欢, 兔7 祝大家在学习的路上一路顺利,生活的路上顺心顺意~!

1. 管道

1.1 什么是管道

  • 管道是Unix中最古老的进程间通信的形式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"
  • 管道只能够进行单向通信!

        具体上面的细节这里就不过多赘述了,因为前面讲这块的时候这幅图已经讲过很多次了。


        这里要说明,管道虽然用的是文件的方案,其实OS一定不会把数据刷新到磁盘,因为要是刷新到磁盘,肯定又会由IO参与,那么效率就会降低,而且这么做也没有必要。

        所以其实像我们这里的文件是一批把数据不会写到磁盘上的文件,换句话说:文件分为磁盘类的文件和内存类的文件,它们两个不一定完全相关,也不一定非得是一一对应的,有些文件只会在内存当中存在,不会在磁盘中存在。

2. 匿名管道

#include <unistd.h>

功能:创建一无名管道

原型

int pipe(int fd[2]);

参数

fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端

返回值:成功返回0,失败返回错误代码

        这个数组(int pipe(int fd[2]);)的参数是输出型参数!

int pipe(int fd[2])
{
   fd[0] = open(read);
   fd[1] = open(write);
   ...
}

        用 0 下标和 1 下标将 open 的文件描述符保存起来,然后通过参数把两个文件描述符返回,这就叫做输出型参数,所以我们调这个函数的目的是为了得到某些返回值!

2.1 用fork来共享管道原理

2.2 站在文件描述符角度-深度理解管道

        这套管道称之为匿名管道(没有名字)!

        我们通过看到文件描述符,就可以理解,一个文件被打开了两次。

  1 #include<stdio.h>                                                                                                                  
  2 #include<unistd.h>                    
  3 #include<stdlib.h>                    
  4 #include<sys/types.h>                 
  5 #include<sys/wait.h>                  
  6                                       
  7 // child->write  father->read         
  8 int main()                            
  9 {                                     
 10   int fd[2] = {0};                    
 11   if(pipe(fd) < 0){                   
 12     perror("pipe");                   
 13     exit(-1);                         
 14   }                                   
 15                                       
 16   pid_t id = fork();                  
 17   if(id == 0){                        
 18     //child                           
 19     close(fd[0]);// 子进程要写关闭读
 20   }
 21                                                                                                                                               
 22   //father
 23   close(fd[1]);// 父进程要写读关闭写
 24   waitpid(id, NULL, 0);           
 25                                        
 26   return 0;                            
 27 }  

        这样我们就建立完成了进程间通信的管道信道。

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <string.h>
  5 #include <sys/types.h>
  6 #include <sys/wait.h>
  7 
  8 // child->write  father->read
  9 int main()
 10 {
 11   int fd[2] = {0};
 12   if(pipe(fd) < 0){
 13     perror("pipe");
 14     exit(-1);
 15   }
 16 
 17   pid_t id = fork();
 18   if(id == 0){
 19     //child
 20     close(fd[0]);// 子进程要写关闭读
 21     const char* msg = "hello father, I am child!";
 22     int count = 30;
 23     while(count){                                                                                
 24       write(fd[1], msg, strlen(msg));
 25       count--;
 26       sleep(1);
 27     }
 28   }
 29 
 30   //father
 31   close(fd[1]);// 父进程要写读关闭写
 32 
 33   char buffer[64];
 34   while(1){
 35     ssize_t s = read(fd[0], buffer, sizeof(buffer));
 36     if(s > 0){
 37       buffer[s] = '\0';
 38       printf("child send to father: %s\n",buffer);
 39     }                                                                                            
 40     else if(s == 0){
 41       printf("read file end!\n");
 42       break;
 43     }
 44     else{
 45       printf("read error!\n");
 46       break;
 47     }
 48   }
 49   waitpid(id, NULL, 0);
 50 
 51   return 0;
 52 }

        我们可以看到,有两个进程,一个父进程一个子进程,我们的父进程和子进程正在通信。子进程将 "hello father, I am child!" 发送给父进程,父进程再打印出来。

        其中有一个细节:就是当我们在子进程那里写入的时候,strlen() 是没有带上 '\0' 的,因为我们认为写入到文件里的都是一个一个字符,文件中不识别 '\0' 。

        而且现在所写的是进程间通信,而不是进程字符串通信,所以当我们读上来一个字符时,我们可以把字符当成一个个字符统一做某种处理(通过ASCLL进行计算),只不过这里是因为简化,我们将读到的一堆数据当成了字符串(因为我用 char buffer[64] 这个缓冲区接收这块数据),接收之后再将最后一个位置设为  '\0' 。

        所以虽然在 strlen() 中可以 +1 ,也就是带上 '\0' ,但是没那么做,是因为这样做是一个误导性的做法,误导是:进程间通信就只能串字符串。但这是不对的,因为传字符串只是通信的方式之一。

        所以我们这里的做法是串的时候就当作是一个一个字符,只不过我们认为在父进程读到之后将这一个个字符当作字符串,仅此而已~!

        此时虽然完成了通信,但是还是远远不够的。


父子进程通信可不可以创建全局缓冲区来完成通信呢?

        当然是不可以的!之前说过,没创建子进程前,这个全局变量属于父进程的,创建之后,因为进程具有独立性,在父子进程没有写入的时候,这个全局变量里的数据是共享的,但是写入后,就会发生写时拷贝,此时子进程写入的数据,父进程是看不到的!所以肯定是不可以的!

        在通信的时候也不是用的父子的数据区,它们两个是通过文件指针的方式指向其它文件,这个文件是由OS提供的:进程A  ->  数据 "拷贝" 给 OS  ->  OS数据 "拷贝" 给进程B 。

        所以进程A通过系统调用接口写入,数据只不过是OS中维护的文件罢了,然后B再通过系统调用接口从OS中获取数据,所以进程A和B并没有使用A或B的内部的空间,而是使用OS的文件的空间,所以不需要写时拷贝~!

2.3 站在内核角度-管道本质

        所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。

2.4 管道特点

        我们在开始的例子中有没有感到疑惑,为什么子进程是没休眠一秒钟打印一次,然后父进程就也想休眠了一秒钟一样接收一次呢?有的小伙伴可能会说:父进程不是一直在接收么(while),那么只要有了数据,就读取出来,没有数据就一直读取,直到有数据了才真正读的到。

        先说一个概念,在多执行流下(父子),看到的同一份资源,称为临界资源!那么有没有一种可能是:子进程正在写入的时候,父进程就来读取了呢?

        就是比方说,子进程先打印了一个 hello ,但是父进程不知道子进程的存在,父进程认为它就应该读取 hello ,但是子进程还没有写完,这时,父进程就读取了,那么子进程也不知道父进程存在,子进程把 hello 后面的继续写入了。如此往复,父进程读到的就会是一堆的乱码(数据不一致)。

        所以其实临界资源是要被保护起来的!

        保护的方式就是通过同步与互斥,不过这里先主要讲  互斥:任何时候只能够有一个人正在使用某种资源。(比方说这个资源我正在使用,那么我就做上一个记号证明这个我在使用)

        所以讲到这里,我们再回到开始,父进程不是一直在 while 循环么,那么是不是父进程一直在运行呢?答案是否定的。

        因为1. 管道内部已经自动提供了互斥与同步机制

        如果有数据,那么就读取出来打印,如果没有数据,read 就会识别到管道里没有数据,那么父进程就不会再读取了,而是阻塞到管道处,等待子进程进行写入,所以并不是父进程 sleep 了,而是因为子进程写的慢,所以导致父进程必须等,就好像也 sleep 了。这种一个等另一个的现象就叫做同步!

父进程不是 while(1) 么,那么父进程最后是怎么结束的呢?

        其实 2. 如果写端关闭,读端就会 read 返回 0 ,代表文件结束。所以父进程最后因为子进程结束了,而结束了。

        3. 如果打开的进程退出了,文件也就会被释放掉。这里的释放是在进程的角度上是释放的,这个文件并不一定会空间资源全被释放,而是取决于是不是和它相关的进程全退出了,如果全退出了文件才会真正释放。也就是说,进程的生命周期随进程

        这里我们的子进程可以一次写入 3、4、5 ... 个字符,父进程一次也可以读取 1、2、3 ... 个字符,也就是说,它们是按需去写入和读取,所以这其实说明 4. 管道是提供流式服务的

        计算机中还有两个概念:

  • 全双工(人在吵架的时候,两个人一直互相输出)
  • 半双工(人在正常沟通的时候,一个人输出,一个人听)

        所以这里的管道其实 5. 管道是半双工通信

        我们在上面说到的是父子见的通信,那么其实"孙子"和"爷爷"也是能够进行通信的,因为它们链接的可以是一个文件。所以还有一点是 6. 匿名管道,适合具有血缘关系的进程进行进程间通信,常用于父子

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

        那么比如说:一直让子进程写,而父进程休眠(父进程不读),那么管道既然是内存中的一段空间,如果管道满了会是怎么样呢?

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <string.h>
  5 #include <sys/types.h>
  6 #include <sys/wait.h>
  7 
  8 // child->write  father->read                                                                                                                 
  9 int main()
 10 {
 11   int fd[2] = {0};
 12   if(pipe(fd) < 0){
 13     perror("pipe");
 14     exit(-1);
 15   }
 16 
 17   pid_t id = fork();
 18   if(id == 0){
 19     //child
 20     close(fd[0]);// 子进程要写关闭读
 21     const char* msg = "hello father, I am child!";
 22     int count = 30;
 23     while(count){
 24       write(fd[1], msg, strlen(msg));
 25       //count--;
 26       printf(".");
 27       fflush(stdout);
 28       //sleep(1);
 29     }
 30   }
 31 
 32   //father
 33   close(fd[1]);// 父进程要写读关闭写
 34 
 35   char buffer[64];
 36   while(1){                                                                                                                                   
 37     sleep(1000);
 38     ssize_t s = read(fd[0], buffer, sizeof(buffer));
 39     if(s > 0){
 40       buffer[s] = '\0';
 41       printf("child send to father: %s\n",buffer);
 42     }
 43     else if(s == 0){
 44       printf("read file end!\n");
 45       break;
 46     }
 47     else{
 48       printf("read error!\n");
 49       break;
 50     }
 51   }
 52   waitpid(id, NULL, 0);
 53 
 54   return 0;
 55 }

        我们可以看到,当我们写了那么多后,子进程就不写了,换言之,当子进程把管道写满时,写端就不写了,就被挂起了。


        那么如果写端就写了一点,而读端一直读会是什么情况呢?

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <string.h>
  5 #include <sys/types.h>
  6 #include <sys/wait.h>
  7 
  8 // child->write  father->read
  9 int main()
 10 {
 11   int fd[2] = {0};
 12   if(pipe(fd) < 0){
 13     perror("pipe");
 14     exit(-1);
 15   }
 16 
 17   pid_t id = fork();
 18   if(id == 0){
 19     //child
 20     close(fd[0]);// 子进程要写关闭读
 21     const char* msg = "hello father, I am child!";
 22     int count = 5;
 23     while(count){                                                                                                                             
 24       write(fd[1], msg, strlen(msg));
 25       count--;
 26       if(count == 2)
 27       {
 28         sleep(999);
 29       }
 30       fflush(stdout);
 31       sleep(1);
 32     }
 33   }
 34 
 35   //father
 36   close(fd[1]);// 父进程要写读关闭写                                                                                                          
 37 
 38   char buffer[64];
 39   while(1){
 40     ssize_t s = read(fd[0], buffer, sizeof(buffer));
 41     if(s > 0){
 42       buffer[s] = '\0';
 43       printf("child send to father: %s\n",buffer);
 44     }
 45     else if(s == 0){
 46       printf("read file end!\n");
 47       break;
 48     }
 49     else{
 50       printf("read error!\n");
 51       break;
 52     }
 53   }
 54   waitpid(id, NULL, 0);
 55 
 56   return 0;
 57 }

        我们会发现,当他读到 3 个后就读不到任何内容了,因为子进程不写了,那么父进程再读的时候就没数据了,那么没数据能不能直接返回?肯定不可能。

        所以既然管道里没数据了,那么父进程就只能等,等管道里有数据再读。

        所以经过上面一对例子能看出:管道是自带同步与互斥机制的

        从读端不读,写端一直写,写满管道,写端就挂起。而读端一直读,写端不写了,读端把管道读完就不读了,因为管道没有数据了,所以读端就一直等。

        换言之,双方是有一个步调协同的一个过程


        如果父进程关闭了 fd[0] ,也就是关闭了读,子进程正常是写入的,那么会说什么情况呢?

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <string.h>
  5 #include <sys/types.h>
  6 #include <sys/wait.h>
  7 
  8 // child->write  father->read
  9 int main()
 10 {
 11   int fd[2] = {0};
 12   if(pipe(fd) < 0){
 13     perror("pipe");
 14     exit(-1);
 15   }
 16 
 17   pid_t id = fork();
 18   if(id == 0){
 19     //child
 20     close(fd[0]);// 子进程要写关闭读
 21     const char* msg = "hello father, I am child!";
 22     int count = 5;
 23     while(count){                                                                                                                             
 24       write(fd[1], msg, strlen(msg));
 25       count--;
 26       if(count == 2)
 27       {
 28         sleep(999);
 29       }
 30       fflush(stdout);
 31       sleep(1);
 32     }
 33   }                                                                                                                                           
 34 
 35   //father
 36   close(fd[1]);// 父进程要写读关闭写
 37 
 38   char buffer[64];
 39   while(1){
 40     ssize_t s = read(fd[0], buffer, sizeof(buffer));
 41     if(s > 0){
 42       buffer[s] = '\0';
 43       printf("child send to father: %s\n",buffer);
 44     }
 45     else if(s == 0){
 46       printf("read file end!\n");
 47       break;
 48     }
 49     else{
 50       printf("read error!\n");
 51       break;
 52     }
 53     close(fd[0]);
 54     break;
 55   }
 56   sleep(3);
 57   waitpid(id, NULL, 0);
 58   printf("child quit!\n");
 59 
 60   return 0;
 61 }

        我先解释一下上面的逻辑,就是子进程先写,然后父进程读了一个后,就退出 while() ,然后等 3 秒后再等待子进程,等子进程退出后父进程打印 "child quit!" 。

        我们可以看到,开始是读到了一串字符串,然后等了 3 秒,然后子进程就退出了。

        其实这里是因为,如果父进程关闭了读文件,那么子进程的写入就没有了任何意义,所以 OS 不会让子进程干这种没有意义和浪费空间、浪费时间的事情,OS 直接就会把子进程给 kill 掉,所以等了 3 秒后会发现子进程退出了!

        那么最后是收到什么信号被 kill 掉的呢,接下来就来测试一下:

        我们运行了两次发现发出的都是 13 号信号 SIGPIPE 。


         那么这里我有点好奇,管道是可以被写满的,那么管道是有多大呢?接下来就来验证一下。

        我们可以看到,这里看到的管道大小好像是 4096 ,那么具体是不是,我们来测试一下:

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <string.h>
  5 #include <sys/types.h>
  6 #include <sys/wait.h>
  7 
  8 // child->write  father->read
  9 int main()
 10 {
 11   int fd[2] = {0};
 12   if(pipe(fd) < 0){
 13     perror("pipe");
 14     exit(-1);
 15   }
 16 
 17   pid_t id = fork();
 18   if(id == 0){
 19     //child
 20     close(fd[0]);// 子进程要写关闭读
 21     const char* msg = "hello father, I am child!";
 22     int count = 0;
 23     char a = 'a';                                                                                                                             
 24     while(1){
 25       write(fd[1], &a, strlen(msg));
 26       count++;
 27       printf("%d\n",count);
 28     }
 29     close(fd[1]);
 30     exit(0);
 31   }
 32 
 33   //father
 34   close(fd[1]);// 父进程要写读关闭写
 35 
 36   char buffer[64];                                                                                                                            
 37   while(1){
 38     sleep(999);
 39     ssize_t s = read(fd[0], buffer, sizeof(buffer));
 40     if(s > 0){
 41       buffer[s] = '\0';
 42       printf("child send to father: %s\n",buffer);
 43     }
 44     else if(s == 0){
 45       printf("read file end!\n");
 46       break;
 47     }
 48     else{
 49       printf("read error!\n");
 50       break;
 51     }
 52     close(fd[0]);
 53     break;
 54   }
 55   sleep(3);
 56   int status = 0;
 57   waitpid(id, &status, 0);
 58   printf("child quit!, signal: %d\n", status & 0x7F);
 59 
 60   return 0;
 61 }

         我们可以看到,我的系统是只有 2608 ,但其实 2.6 之后的大小是 65536 ,可能是我的系统有点拉跨~

        讲到这里匿名管道就讲解完了,虽然匿名管道可以进行通信,可是它的缺点很严重,就是常常适用于父子,如果是两个毫无关系的进程,那么这个通信就不好用了。

3. 管道读写规则

1. 当没有数据可读时

  • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
  • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

2. 当管道满的时候

  • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
  • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

3. 如果所有管道写端对应的文件描述符被关闭,则read返回0

4. 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出

5. 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。

6. 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

4. 命名管道

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件

        毫不相干的进程如何通信呢? -> 命名管道

        虽然命名管道看起来很像文件,但是本质上这个文件也是一个内存文件,只不过在内存上只有一个简单的映像,永远都是 0 ,就只占了一个名字罢了。两个进程只需要通过这个名字在内存中打开文件就好了。

        所以命名管道和匿名管道的区别就是:匿名管道是通过创建子进程的方式让不同的进程看到同一块资源,而命名管道是通过文件名的方式让不同的进程看到同一块资源。


        接下来就用命令行式来测试:

        我们可以看到,当我们用脚本每隔 1 秒在 fifo 中写入,然后让另一个进程从 fifo 中读取时会发现,都打印到了另一个进程的屏幕上。而且我们知道,这两个进程之间没有任何关系。

        当我们先管道第二个进程后会发现:

        第一个程序挂掉了,这就是因为,第二个进程是在读,我们上面说过,如果将读的进程关掉,那么写入就没有任何意义,所以OS就会把写的进程给 kill 掉,而且是发送 13 号信号 SIGPIPE 。由因为执行这个脚本的时候是由 bash 执行的,所以被 kill 掉后则就显示为退出了!

        如果我们关闭写的,那么读的就会被终止。

        所以我们通过前面讲的可以总结一下:

  1. 如果一直读不写,那么读的就会一直等管道里有数据再读(读被挂起了)。
  2. 如果一直写不读,当将管道写满时就不写了,等管道有空间了(等其他进程来读了),再继续写(写被挂起了)。
  3. 当正常通信时,关闭读,那么写就没有意义,所以写的进程就会被 kill 掉。
  4. 当正常通信时,关闭写,那么读就会被终止。

        但是我们也看到了,我们通过命令行式的方式使得两个进程之间完成了通信!


        接下来我就要用代码来实现进程间通信了:

        我要完成的目的是在用户端 client 输入数据(从屏幕中读取去),而在客户端 server 接收客户端的数据,然后再从客户端打印出来。

        但是这里需要注意的是,我们要想进行通信,那么就必须可以看到同一份资源,所以这里我就将共同的资源放到了头文件里,然后两个程序就可以看到同一份资源了:

comm.h
  1 #pragma once
  2 #include <stdio.h>
  3 #include <unistd.h>
  4 #include <string.h>
  5 #include <sys/types.h>
  6 #include <sys/stat.h>                                                   
  7 #include <fcntl.h>
  8 
  9 #define FILE_NAME "myfifo"
server.c
  1 #include"comm.h"
  2                                                                         
  3 int main()
  4 {
  5   if(mkfifo(FILE_NAME, 0644) < 0){
  6     perror("myfifo");
  7     return 1;
  8   }
  9 
 10   int fd = open(FILE_NAME, O_RDONLY);
 11   if(fd < 0){
 12     perror("open");
 13     return 2;
 14   }
 15 
 16   char msg[128];
 17   while(1){
 18     msg[0] = 0;
 19     ssize_t s = read(fd, msg, sizeof(msg)- 1);
 20     if(s > 0){
 21       msg[s] = 0;
 22       printf("client# %s\n", msg);
 23     }
 24     else if(s == 0){
 25       printf("client quit!\n");
 26       break;
 27     }
 28     else{
 29       printf("read error!\n");
 30       break;                                                             
 31     }
 32   }
 33   close(fd);
 34 
 35   return 0;
 36 }

client.c
  1 #include"comm.h"
  2 
  3 int main()
  4 {
  5   int fd = open(FILE_NAME, O_WRONLY);
  6   if(fd < 0){
  7     perror("open");
  8     return 1;                                                           
  9   }                                                  
 10                                                  
 11   char msg[128];  
 12   while(1){                                   
 13     msg[0] = 0;                      
 14     printf("Please Enter# ");
 15     fflush(stdout);                     
 16     ssize_t s = read(0, msg, sizeof(msg));
 17     if(s > 0){
 18       msg[s] = 0;                                                  
 19       write(fd, msg, strlen(msg));                               
 20     }                                                     
 21   }                                   
 22                                                         
 23   close(fd);                                  
 24   return 0;
 25 }                             

        我们可以看到这样的效果。

        而且我们会发现,这两个进程是没有任何关系的,但是它们两个就可以进行通信。而且除了 server 创建一个管道文件,其它的操作和操作普通文件没有区别。

        那么接下来就操作:情况是不是在命名管道开头的那幅图的情况:

        首先先把 server 中读取的操作去掉:

        然后进行下方操作:

        我们可以看到,我已经写了好多消息了,但是我们发现 myfifo 的大小一直都是 0 ,换句话说就是我写的消息已经被写到管道的缓冲区里了,但是 server端 并没有读取,所以写的消息还是在内存里,但是又因为 myfifo 的大小为 0 ,就说明数据并没有刷新到磁盘,也就意味着,双方通信依旧是在内存中通信的,和匿名管道的底层原理是一样的,它们采用的都是文件通信。

        所以这下也就更能理解上面的那副图了吧。


        在上面的图,我们可以发现,虽然是可以完成通信了,但是我们输入 ls pwd 这种命令,他只是当作字符去处理了,但是现在不想这么操作,就像模拟一下 xshell 的基本运行原理,而且我们发现在服务端输出的时候还带有空行:

server.c
  1 #include"comm.h"
  2 
  3 int main()
  4 {
  5   if(mkfifo(FILE_NAME, 0644) < 0){
  6     perror("myfifo");
  7     return 1;
  8   }
  9 
 10   int fd = open(FILE_NAME, O_RDONLY);
 11   if(fd < 0){
 12     perror("open");
 13     return 2;
 14   }
 15 
 16   char msg[128];
 17   while(1){
 18     msg[0] = 0;
 19     ssize_t s = read(fd, msg, sizeof(msg)- 1);
 20     if(s > 0){
 21       msg[s] = 0;
 22       printf("client# %s\n", msg);
 23       if(fork() == 0){                                                  
 24         //child
 25         execlp(msg, msg, NULL);
 26         exit(-1);
 27       }
 28       waitpid(-1, NULL, 0);//-1表示等待任意子进程
 29     }
 30     else if(s == 0){
 31       printf("client quit!\n");
 32       break;
 33     }
 34     else{
 35       printf("read error!\n");
 36       break;
 37     }
 38   }
 39   close(fd);
 40 
 41   return 0;
 42 }
client.c
  1 #include"comm.h"                                                        
  2 
  3 int main()
  4 {
  5   int fd = open(FILE_NAME, O_WRONLY);
  6   if(fd < 0){
  7     perror("open");
  8     return 1;
  9   }
 10 
 11   char msg[128];
 12   while(1){
 13     //msg[0] = 0;
 14     printf("Please Enter# ");
 15     fflush(stdout);
 16     ssize_t s = read(0, msg, sizeof(msg));
 17     if(s > 0){
 18       msg[s-1] = 0;//注意这里为了输出的时候没空行
 19       write(fd, msg, strlen(msg));
 20     }
 21   }
 22 
 23   close(fd);
 24   return 0;
 25 }

        通过 server.c 中 23-28 行创建子进程,让子进程执行命令,并让父进程等待。

        通过 client.c 中低 18 行 msg[s-1] = 0 ,让打印出来的空行去除。空行的原因是我们在 client 端输入的时候,最后要靠回车确定输入完,所以在写入的时候,也会把 "\n" 写进去,所以将 s-1 的位置设为 "\0" 就可以将 "\n" 消除!

        最后我们看到,让我们输入 who pwd top ... ... 的时候,发现在 server 端执行了命令(当然有的命令还是不可以,因为没有带选项),所以这也就像我说的,我们的 client 端就像我们 xshell 终端,这就像进程通信版的 xshell 通信原理,只不过 xshell 是一个我们下载的客户端,它实际上是将我们输入的命令打包,通过网络发给网络上的进程,只不过现在是本地通信。但是我们依然可以通过一个进程遥控另一个进程上的任务。


        所以其实这里我们还可以让进程间进行文本分析(10+20),这也就印证了其实文本间通信不是只能给对方发字符串。

server.c
  1 #include"comm.h"
  2 
  3 int main()
  4 {
  5   if(mkfifo(FILE_NAME, 0644) < 0){
  6     perror("myfifo");
  7     return 1;
  8   }
  9 
 10   int fd = open(FILE_NAME, O_RDONLY);
 11   if(fd < 0){
 12     perror("open");
 13     return 2;
 14   }
 15 
 16   char msg[128];
 17   while(1){
 18     msg[0] = 0;
 19     ssize_t s = read(fd, msg, sizeof(msg)- 1);
 20     if(s > 0){
 21       msg[s] = 0;
 22       printf("client# %s\n", msg);
 23       char* p = msg;                                                                              
 24       const char* lable = "+-*/%";
 25       int flag = 0;
 26       while(*p){
 27         switch(*p){
 28           case '+':
 29             flag = 0;
 30             break;
 31           case '-':
 32             flag = 1;
 33             break;
 34           case '*':
 35             flag = 2;
 36             break;
 37           case '/':
 38             flag = 3;
 39             break;                                                                                
 40           case '%':
 41             flag = 4;
 42             break;
 43         }
 44         ++p;
 45       }
 46       char* data1 = strtok(msg, "+-*/%");
 47       char* data2 = strtok(NULL, "+-*/%");
 48       int x = atoi(data1);
 49       int y = atoi(data2);
 50       int z = 0;
 51       switch(flag){
 52         case 0:
 53           z = x + y;
 54           break;
 55         case 1:
 56           z = x - y;
 57           break;
 58         case 2:
 59           z = x * y;
 60           break;
 61         case 3:
 62           z = x / y;
 63           break;                                                                                  
 64         case 4:
 65           z = x % y;
 66           break;
 67       }
 68       printf("%d %c %d = %d\n",x ,lable[flag], y, z);
 69 
 70       //if(fork() == 0){
 71       //  //child
 72       //  execlp(msg, msg, NULL);
 73       //  exit(-1);
 74       //}
 75       //waitpid(-1, NULL, 0);//-1表示等待任意子进程
 76     }
 77     else if(s == 0){
 78       printf("client quit!\n");
 79       break;
 80     }
 81     else{
 82       printf("read error!\n");
 83       break;
 84     }
 85   }
 86   close(fd);
 87 
 88   return 0;
 89 }


        我们可以看到,这样就完成了这个任务,这样做的原因可能是 client 端一直在接收任务,然后 client 就给 server 端派送计算任务,这样就完成了任务分工。

        所以不要只把进程间通信理解成就是一个进程给另一个进程发字符串,就像一个聊天工具一样,其实不然,其中的 msg 可能是一个任务,是一个信息,是一个命令。

        它的核心应用场景就是一个进程完成任务的时候成本太高了,它可以将任务派发给其它进程,让其它进程帮它去完成,这样就体现了通信的价值,叫做:多进程任务协同


        接下来还有一个操作就是:同时打开两个文件,从一个文件里读,然后再写到另一个文件中,这样就实现了将一个文件里的内容拷贝到另一个文件的任务:

server.c
  1 #include"comm.h"
  2 
  3 int main()
  4 {
  5   if(mkfifo(FILE_NAME, 0644) < 0){
  6     perror("myfifo");
  7     return 1;
  8   }
  9 
 10   int fd = open(FILE_NAME, O_RDONLY);
 11   if(fd < 0){
 12     perror("open");
 13     return 2;
 14   }
 15                  
 16   int out = open("file-cp.txt", O_WRONLY|O_CREAT, 0644);
 17   if(out < 0){    
 18     perror("open");
 19     exit(4);          
 20   }               
 21                  
 22   char msg[128];      
 23   while(1){                                                                     
 24     msg[0] = 0;  
 25     ssize_t s = read(fd, msg, sizeof(msg)- 1);
 26     if(s > 0){
 27       write(out, msg, s);
 83     }
 84     else if(s == 0){
 85       printf("client quit!\n");
 86       break;
 87     }
 88     else{
 89       printf("read error!\n");
 90       break;
 91     }
 92   }
 93   close(fd);
 94   close(out);
 95   return 0;
 96 }
client.c
    1 #include"comm.h"
    2 
    3 int main()
    4 {
    5   int fd = open(FILE_NAME, O_WRONLY);
    6   if(fd < 0){
    7     perror("open");
    8     return 1;
    9   }
   10   
   11   int in = open("file.txt", O_RDONLY);
   12   if(in < 0){
   13     perror("open");
   14     exit(4);
   15   }
   16   char msg[128];
   17   while(1){
   21     dup2(in, 0);
   22     ssize_t s = read(0, msg, sizeof(msg));
   23     //ssize_t s = read(in, msg, sizeof(msg));                                                  
   24     if(s == sizeof(msg)){
   25       msg[s-1] = 0;//注意这里为了输出的时候没空行
   26       write(fd, msg, s);
   27     }
   28     else if(s < sizeof(msg)){
   29       write(fd, msg, s);
   30       printf("read end of file!\n");
   31       break;
   32     }
   33     else{
   34       break;
   35     }
   36   }
   37 
   38   close(fd);
   39   return 0;
   40 }

        我们可以看到,这样我们就完成了一次文件拷贝,是一个进程将文件的数据拷贝给了另一个进程创建的文件。

        大家会感到奇怪,其实是因为我们现在只是在一台机器上进程进程间通信,但如果将 server 看成我们的 centos 服务器,client 堪称 window xshell ,而我们现在的操作就是将本地文件上传给 centos 服务器,这么一看,是不是就不奇怪了。

        也就是开始我们讲到的 rz 命令!

        其实我们之前就用过管道,但是我们只限于用上,接下来就带大家继续看看我们之前用的管道:

        我们可以看到 | 就是管道,而且可以看到 cat  grep 都是命令,所以 | 左边的命令要运行起来,右边的命令也要运行起来,所以它们两个运行起来之后肯定是两个进程。

        那么这里的管道是命名管道还是匿名管道呢?

        那么接下来就跟着大家验证一下:

        我们就可以看到,这三个 sleep 的 ppid 是一个进程,也就是说,它们的父进程都是同一个进程,所以这三个 sleep 是兄弟关系,但是现在只是有匿名管道的一个前提,也就是是兄弟关系。但是这里还要告诉大家,如果是命名管道的话,我们在运行的时候,在目录下是有像 myfifo 这种在磁盘上对应的一个文件名,但是我们在操作 | 这个管道的时候,是并不存在的,所以这个就是匿名管道啦~!

        以上就是所有管道的内容。

         如上就是 管道 的所有知识,如果大家喜欢看此文章并且有收获,可以支持下 兔7 ,给 兔7 三连加关注,你的关注是对我最大的鼓励,也是我的创作动力~!

        再次感谢大家观看,感谢大家支持!

  • 10
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

兔7

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值