• 【Linux】进程间是这样通信的--管道篇


    @TOC

    目录

    进程间通信的介绍

    进程间通信的概念

    进程间通信的目的

    进程间通信的本质

    进程间通信的分类

    管道

    什么是管道

    匿名管道

    pipe函数

    匿名管道使用步骤

    管道读写规则

    管道的特点

    1、管道内部自带同步与互斥机制

    2、管道的生命周期随进程

    3、管道提供的是流式服务

    4、管道是半双工通信的

    管道的四种特殊情况

    管道的大小

    方法一:使用man手册

    方法二:使用ulimit命令

    方法三:自行测试

    命名管道

    命名管道的原理

    使用命令创建命名管道

    创建一个命名管道

    命名管道的打开规则

    用命名管道实现serve&client通信

    用命名管道实现派发计算任务

    用命名管道实现文件拷贝

    命令行当中的管道


    进程间通信的介绍

    进程间通信的概念

    进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。

    进程间通信的目的

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

    进程间通信的本质

    进程间通信的本质就是,让不同的进程看到同一份资源。

    由于各个运行进程之间具有独立性,这个独立性主要体现在数据层面,而代码逻辑层面可以私有也可以公有(例如父子进程),因此各个进程之间要实现通信是非常困难的。

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

    因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件内核缓冲等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。

    进程间通信的分类

    管道

    • 匿名管道
    • 命名管道

    System V IPC

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

    POSIX IPC

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

    管道

    什么是管道

    管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的数据流称为一个1“管道”。

    例如,统计我们当前使用云服务器上的登录用户个数。

    其中,who命令和wc命令都是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据打到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理。

    注明: who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc -l用于统计当前的行数。

    匿名管道

    匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。

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

    注:

    • 这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
    • 管道虽然用的是文件的方案,但操作系统一定不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率,而且也没有必要。也就是说,这种文件是一批不会把数据写到磁盘当中的文件,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在。

    pipe函数

    pipe函数用于创建匿名管道,pip函数的函数原型如下:

    int pipe(int pipefd[2]);
    

    pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:

    数组元素含义
    pipefd[0]管道读端的文件描述符
    pipefd[1]管道写端的文件描述符

    pipe函数调用成功时返回0,调用失败时返回-1。

    源码如下:

    1. struct pipe_buffer {
    2. struct page *page;
    3. unsigned int offset, len;
    4. const struct pipe_buf_operations *ops;
    5. unsigned int flags;
    6. unsigned long private;
    7. };

    其中我们的管道文件拥有一个缓冲区,这个缓冲区有一个专门的struct pipe_buf_operations结构体用来处理它的输入输出方法,以及flags用来标识当前缓冲区的装态

    匿名管道使用步骤

    在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:

    1、父进程调用pipe函数创建管道。

    2、父进程创建子进程。

    3、父进程关闭写端,子进程关闭读端。

    注:

    1. 管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
    2. 从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取。

    我们可以站在文件描述符的角度再来看看这三个步骤:

    1、父进程调用pipe函数创建管道

    2、父进程创建子进程

    3、父进程关闭写端,子进程关闭读端

    例如,在以下代码当中,父进程向匿名管道当中写入10行数据,子进程从匿名管道当中将数据读出。

    1. #include <iostream>
    2. #include <vector>
    3. #include <cstdio>
    4. #include <cstring>
    5. #include <ctime>
    6. #include <cstdlib>
    7. #include <sys/wait.h>
    8. #include <sys/types.h>
    9. #include <unistd.h>
    10. #include "Log.hpp"
    11. using namespace std;
    12. #define NUM 1024
    13. //匿名管道
    14. int main( )
    15. {
    16. //创建管道
    17. int pipefd[2] = {0};
    18. if(pipe(pipefd) != 0)
    19. {
    20. cerr << "pipe error" << endl;
    21. return 1;
    22. }
    23. //2.创建子进程
    24. pid_t id = fork();
    25. if(id < 0)
    26. {
    27. cerr << "fork error" << endl;
    28. return 2;
    29. }
    30. else if(id == 0)
    31. {
    32. //3.子进程管道
    33. // 子进程来进行读取,子进程就应该关掉写端
    34. close(pipefd[1]);
    35. char buffer[NUM];
    36. while(1)
    37. {
    38. Log("等待写入", Debug) << endl;;
    39. memset(buffer, 0, sizeof(buffer));
    40. ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
    41. if(s > 0)
    42. {
    43. //读取成功
    44. buffer[s] = '\0';
    45. cout << "子进程收到消息, 内容是: "<< buffer<< endl;
    46. }
    47. else if(s == 0)
    48. {
    49. cout << "父进程写完了,子进程可以退出了"<< endl;
    50. break;
    51. }
    52. else
    53. {
    54. cerr <<"err while child read pipe "<< endl;
    55. }
    56. }
    57. close(pipefd[0]);
    58. exit(0);
    59. }
    60. else
    61. {
    62. //4.父进程管道
    63. //父进程来进行写入, 就应该关掉读端
    64. close(pipefd[0]);
    65. const char* msg = "我是父进程,这次发出的信息编号是";
    66. int cnt = 0;
    67. while(cnt < 10)
    68. {
    69. char sendBuffer[1024];
    70. sprintf(sendBuffer, "%s : %d", msg, cnt);//格式化控制字符串
    71. write(pipefd[1], sendBuffer, strlen(sendBuffer));
    72. cnt++;
    73. cout << "cnt: "<< cnt << endl;
    74. sleep(1);
    75. }
    76. close(pipefd[1]);
    77. cout << "父进程输出完毕"<< endl;
    78. }
    79. //父进程等待子进程结束
    80. pid_t res = waitpid(id, nullptr, 0);
    81. if(res > 0)
    82. {
    83. cout << "等待子进程成功" << endl;
    84. }
    85. cout << "子进程退出" <<endl;
    86. return 0;
    87. }

    这里我们可以查看一下代码,并没有sleep的停止语句,但是为什么在输出的时候,父进程输出一次,子进程输出一次,父进程休眠的时候,子进程看起来啥事没有做。实际上,子进程是在等待父进程对管道的写入。

    管道读写规则

    pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:

    int pipe2(int pipefd[2], int flags);

    pipe2函数的第二个参数用于设置选项。

    1、当没有数据可读时:

    O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来为止。
    O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
    2、当管道满的时候:

    O_NONBLOCK disable:write调用阻塞,直到有进程读走数据。
    O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN。
    3、如果所有管道写端对应的文件描述符被关闭,则read返回0。
    4、如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
    5、当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。
    6、当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。

    管道的特点
    1、管道内部自带同步与互斥机制

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

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

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

    同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
    互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。
    实际上,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。对于管道的场景来说,互斥就是两个进程不可以同时对管道进行操作,它们会相互排斥,必须等一个进程操作完毕,另一个才能操作,而同步也是指这两个不能同时对管道进行操作,但这两个进程必须要按照某种次序来对管道进行操作。

    也就是说,互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,而同步的任务之间则有明确的顺序关系。
     

    2、管道的生命周期随进程

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

    3、管道提供的是流式服务

    对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:

    • 流式服务: 数据没有明确的分割,不分一定的报文段。
    • 数据报服务: 数据有明确的分割,拿数据按报文段拿。
    4、管道是半双工通信的

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

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

    管道的四种特殊情况

    在使用管道时,可能出现以下四种特殊情况:

            写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
    读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
    写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
    读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。
    其中前面两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。

            第三种情况也很好理解,读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。

            第四种情况也不难理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。
    我们可以通过以下代码看看情况四中,子进程退出时究竟是收到了什么信号。

    1. #include <iostream>
    2. #include <string.h>
    3. #include <stdlib.h>
    4. #include <sys/types.h>
    5. #include <sys/wait.h>
    6. #include <unistd.h>
    7. int main()
    8. {
    9. int fd[2] = {0};
    10. if(pipe(fd) < 0)
    11. {
    12. perror("pipe");
    13. return 1;
    14. }
    15. pid_t id = fork();//创建进程
    16. if(id == 0)
    17. {
    18. //child
    19. close(fd[0]); // 子进程关闭读段
    20. //子进程向管道写入数据
    21. const char* msg = "我是子进程";
    22. int count = 10;
    23. while(count--)
    24. {
    25. write(fd[1], msg, strlen(msg));
    26. sleep (1);
    27. }
    28. close(fd[1]);
    29. exit(0);
    30. }
    31. //father
    32. close(fd[1]);//父进程关闭写端
    33. close(fd[0]);//父进程直接关闭读端(导致子进程直接被操作系统杀掉)
    34. int status = 0;
    35. waitpid(id, &status, 0);
    36. printf("子进程获得的信号:%d\n",status & 0x7F);
    37. return 0;
    38. }

    通过kill -l命令可以查看13对应的具体信号。

    由此可知,当发生情况四时,操作系统向子进程发送的是SIGPIPE信号将子进程终止的。

    管道的大小

    管道的容量是有限的,如果管道已满,那么写端将阻塞或失败,那么管道的最大容量是多少呢?

    方法一:使用man手册

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

    然后我们可以使用uname -r命令,查看自己使用的Linux版本。

    根据man手册,我使用的是Linux 2.6.11之后的版本,因此管道的最大容量是65536字节。

    方法二:使用ulimit命令

    其次,我们还可以使用ulimit -a命令,查看当前资源限制的设定。

    根据显示,管道的最大容量是 512 × 8 = 4096 512\times8=4096512×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){
    14. //child
    15. close(fd[0]); //子进程关闭读端
    16. char c = 'a';
    17. int count = 0;
    18. //子进程一直进行写入,一次写入一个字节
    19. while (1){
    20. write(fd[1], &c, 1);
    21. count++;
    22. printf("%d\n", count); //打印当前写入的字节数
    23. }
    24. close(fd[1]);
    25. exit(0);
    26. }
    27. //father
    28. close(fd[1]); //父进程关闭写端
    29. //父进程不进行读取
    30. waitpid(id, NULL, 0);
    31. close(fd[0]);
    32. return 0;
    33. }

    可以看到,在读端进程不进行读取的情况下,写端进程最多写65536字节的数据就被操作系统挂起了,也就是说,我当前Linux版本中管道的最大容量是65536字节。

    命名管道

    命名管道的原理


    匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
    如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。

    注:

    1. 普通文件是很难做到通信的,即便做到通信也无法解决一些安全问题。
    2. 命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。

    使用命令创建命名管道

    我们可以使用mkfifo命令创建一个命名管道。

    可以看到,创建出来的文件的类型是p,代表该文件是命名管道文件。

    使用这个命名管道文件,就能实现两个进程之间的通信了。我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。
    现象就是当进程A启动后,进程B会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。
     

    创建一个命名管道

    在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:

    int mkfifo(const char *pathname, mode_t mode);

    mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。

    • 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
    • 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)

    mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。

    例如,将mode设置为0666,则命名管道文件创建出来的权限如下:

    但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。

    若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。

    umask(0); //将文件默认掩码设置为0

     mkfifo函数的返回值。

    • 命名管道创建成功,返回0。
    • 命名管道创建失败,返回-1。

    创建命名管道示例:

    使用以下代码即可在当前路径下,创建出一个名为myfifo的命名管道。

    1. #include <stdio.h>
    2. #include <sys/types.h>
    3. #include <sys/stat.h>
    4. #define FILE_NAME "myfifo"
    5. int main()
    6. {
    7. umask(0); //将文件默认掩码设置为0
    8. if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件
    9. perror("mkfifo");
    10. return 1;
    11. }
    12. //create success...
    13. return 0;
    14. }

    运行代码后,命名管道myfifo就在当前路径下被创建了。

    命名管道的打开规则

    1、如果当前打开操作是为读而打开FIFO时。

    • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
    • O_NONBLOCK enable:立刻返回成功。

    2、如果当前打开操作是为写而打开FIFO时。

    • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
    • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。
       

    用命名管道实现serve&client通信

    实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。


    服务端代码如下:

    1. #include "comm.hpp"
    2. int main()
    3. {
    4. umask(0);
    5. //1.创建管道文件
    6. if(mkfifo(ipcPath.c_str(), MODE) < 0)
    7. {
    8. perror("mkfifo");
    9. exit(1);
    10. }
    11. Log("创建管道文件成功", Debug) << " step 1"<< endl;
    12. //2.正常的文件操作
    13. int fd = open(ipcPath.c_str(), O_RDONLY);
    14. if(fd < 0 )
    15. {
    16. perror("open");
    17. exit(2);
    18. }
    19. Log("打开管道文件成功", Debug) << " step 2"<< endl;
    20. //3.编写正常的通信代码
    21. char buffer[SIZE];
    22. while(true)
    23. {
    24. memset (buffer, '\0', sizeof(buffer));
    25. ssize_t s = read (fd, buffer,sizeof(buffer) - 1);
    26. if(s > 0)
    27. {
    28. cout <<"[" << getpid() << "] "<< "client say> " << buffer << endl;
    29. }
    30. else if(s == 0)
    31. {
    32. //end of file
    33. cerr <<"[" << getpid() << "] " << "read end of file, clien quit, server quit too!" << endl;
    34. break;
    35. }
    36. else
    37. {
    38. //read error
    39. perror("read");
    40. break;
    41. }
    42. }
    43. //4.关闭文件
    44. close(fd);
    45. Log("关闭管道文件成功", Debug) << " step 3"<< endl;
    46. unlink(ipcPath.c_str());//通信完毕
    47. Log("删除管道文件成功", Debug) << " step 4"<< endl;
    48. return 0;
    49. }

    而对于客户端来说,因为服务端运行起来后命名管道文件就已经被创建了,所以客户端只需以写的方式打开该命名管道文件,之后客户端就可以将通信信息写入到命名管道文件当中,进而实现和服务端的通信。

    客户端的代码如下:

    1. #include "comm.hpp"
    2. int main()
    3. {
    4. //1.获取管道文件
    5. int fd = open(ipcPath.c_str(), O_WRONLY);
    6. if(fd < 0)
    7. {
    8. perror("open");
    9. exit(1);
    10. }
    11. //2.ipc过程
    12. string buffer;
    13. while(true)
    14. {
    15. cout << "Please Enter Message Line :>";
    16. std::getline(std::cin, buffer);
    17. write(fd, buffer.c_str(), buffer.size());
    18. }
    19. //3. 关闭
    20. close(fd);
    21. return 0;
    22. }

    公用头文件:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include "Log.hpp"
    6. using namespace std;
    7. #define MODE 0664
    8. #define SIZE 128
    9. string ipcPath = "./fifo.ipc";
    10. #endif

    日志步骤记录

    1. #ifndef _LOG_H_
    2. #define _LOG_H_
    3. #include <iostream>
    4. #include <ctime>
    5. #include <string>
    6. using namespace std;
    7. #define Notice 0
    8. #define Warning 1
    9. #define Error 2
    10. #define Debug 3
    11. const std::string msg[] = { "Notice", "Warning", "Error", "Debug"};
    12. std::ostream &Log(std::string message, int level)
    13. {
    14. std::cout <<" | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
    15. return std::cout;
    16. }
    17. #endif

    代码编写完毕后,先将服务端进程运行起来,之后我们就能在客户端看到这个已经被创建的命名管道文件。

    接着再将客户端也运行起来,此时我们从客户端写入的信息被客户端写入到命名管道当中,服务端再从命名管道当中将信息读取出来打印在服务端的显示器上,该现象说明服务端是能够通过命名管道获取到客户端发来的信息的,换句话说,此时这两个进程之间是能够通信的。

    服务端和客户端之间的退出关系

    当客户端退出后,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中是直接退出了)。

    当服务端退出后,客户端写入管道的数据就不会被读取了,也就没有意义了,那么当客户端下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE),此时客户端就被操作系统强制杀掉了。

    通信是在内存当中进行的

    若是我们只让客户端向管道写入数据,而服务端不从管道读取数据,那么这个管道文件的大小会不会发生变化呢?

    1. #include "comm.hpp"
    2. //server.c
    3. int main()
    4. {
    5. umask(0); //将文件默认掩码设置为0
    6. if (mkfifo(ipcPath.c_str(), 0666) < 0){ //使用mkfifo创建命名管道文件
    7. perror("mkfifo");
    8. return 1;
    9. }
    10. int fd = open(ipcPath.c_str(), O_RDONLY); //以读的方式打开命名管道文件
    11. if (fd < 0){
    12. perror("open");
    13. return 2;
    14. }
    15. while (1){
    16. //服务端不读取管道信息
    17. }
    18. close(fd); //通信完毕,关闭命名管道文件
    19. return 0;
    20. }

    可以看到,尽管服务端不读取管道当中的数据,但是管道当中的数据并没有被刷新到磁盘,使用ll命令看到命名管道文件的大小依旧为0,也就说明了双方进程之间的通信依旧是在内存当中进行的,和匿名管道通信是一样的。

    用命名管道实现派发计算任务

    需要注意的是两个进程之间的通信,并不是简单的发送字符串而已,服务端是会对客户端发送过来的信息进行某些处理的。

    这里我们以客户端向服务端派发计算任务为例,客户端通过管道向服务端发送双操作数的计算请求,服务端接收到客户端的信息后需要计算出相应的结果。

    这里我们无需更改客户端的代码,只需改变服务端处理通信信息的逻辑即可。

    用命名管道实现文件拷贝

    这里我们再用命名管道实现一下文件的拷贝

    需要拷贝的文件是file.txt,该文件当中的内容如下:

    我们要做的就是,让客户端将file.txt文件通过管道发送给服务端,在服务端创建一个file-bat.txt文件,并将从管道获取到的数据写入file-bat.txt文件当中,至此便实现了file.txt文件的拷贝。

    其中服务端需要做的就是,创建命名管道并以读的方式打开该命名管道,再创建一个名为file-bat.txt的文件,之后需要做的就是将从管道当中读取到的数据写入到file-bat.txt文件当中即可。

    服务端的代码如下:

    1. #include "comm.hpp"
    2. //server.c
    3. int main()
    4. {
    5. umask(0); //将文件默认掩码设置为0
    6. if (mkfifo(ipcPath.c_str(), 0666) < 0){ //使用mkfifo创建命名管道文件
    7. perror("mkfifo");
    8. return 1;
    9. }
    10. int fd = open(ipcPath.c_str(), O_RDONLY); //以读的方式打开命名管道文件
    11. if (fd < 0){
    12. perror("open");
    13. return 2;
    14. }
    15. //创建文件file-bat.txt,并以写的方式打开该文件
    16. int fdout = open("file-bat.txt", O_CREAT | O_WRONLY, 0666);
    17. if (fdout < 0){
    18. perror("open");
    19. return 3;
    20. }
    21. char msg[128];
    22. while (1){
    23. msg[0] = '\0'; //每次读之前将msg清空
    24. //从命名管道当中读取信息
    25. ssize_t s = read(fd, msg, sizeof(msg)-1);
    26. if (s > 0){
    27. write(fdout, msg, s); //将读取到的信息写入到file-bat.txt文件当中
    28. }
    29. else if (s == 0){
    30. cout <<"client quit!" << endl;
    31. break;
    32. }
    33. else{
    34. cout << "read error!" << endl;
    35. break;
    36. }
    37. }
    38. close(fd); //通信完毕,关闭命名管道文件
    39. close(fdout); //数据写入完毕,关闭file-bat.txt文件
    40. return 0;
    41. }

    而客户端需要做的就是,以写的方式打开这个已经存在的命名管道文件,再以读的方式打开file.txt文件,之后需要做的就是将file.txt文件当中的数据读取出来并写入管道当中即可。

    客户端的代码如下:

    1. #include "comm.hpp"
    2. int main()
    3. {
    4. int fd = open(ipcPath.c_str(), O_WRONLY);//以写的方法打开命令管道文件
    5. if(fd < 0)
    6. {
    7. perror("open");
    8. return 1;
    9. }
    10. int fdin = open("file.txt", O_RDONLY);
    11. if(fdin < 0)
    12. {
    13. perror("open");
    14. return 2;
    15. }
    16. char msg[128];
    17. while(1)
    18. {
    19. //file.txt文档中读取数据
    20. ssize_t s = read(fdin, msg, sizeof (msg));
    21. if(s > 0)
    22. {
    23. write(fd, msg, s);//将读取到的数据写入到命名管道中
    24. }
    25. else if(s == 0)
    26. {
    27. cout << "read end of file!" <<endl;
    28. break;
    29. }
    30. else
    31. {
    32. cout << "read error"<< endl;
    33. break;
    34. }
    35. }
    36. close( fd);
    37. close(fdin);
    38. return 0;
    39. }

    编写完代码后,先运行服务端,再运行客户端,一瞬间这两个进程就相继运行结束了。

    查看这个目录,就会发现一个file.txt文件的拷贝。

    内容和file.txt一样

    使用管道实现文件的拷贝有什么意义?

    因为这里是使用管道在本地进行的文件拷贝,所以看似没什么意义,但我们若是将这里的管道想象成“网络”,将客户端想象成“Windows Xshell”,再将服务端想象成“centos服务器”。那我们此时实现的就是文件上传的功能,若是将方向反过来,那么实现的就是文件下载的功能。

    命名管道和匿名管道的区别

    • 匿名管道由pipe函数创建并打开。
    • 命名管道由mkfifo函数创建,由open函数打开。
    • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。

    命令行当中的管道

    现有file.txt文件,文件当中的内容如下:

    我们可以利用管道(“|”)同时使用cat命令和grep命令,进而实现文本过滤。

    这里的 | 就是命令行当中的管道

    由于匿名管道只能用于有亲缘关系的进程之间的通信,而命名管道可以用于两个毫不相关的进程之间的通信,因此我们可以先看看命令行当中用管道(“|”)连接起来的各个进程之间是否具有亲缘关系。

    下面通过管道(“|”)连接了三个进程,通过ps命令查看这三个进程可以发现,这三个进程的PPID是相同的,也就是说它们是由同一个父进程创建的子进程。


    而它们的父进程实际上就是命令行解释器,这里为bash

    也就是说,由管道(“|”)连接起来的各个进程是有亲缘关系的,它们之间互为兄弟进程。

    现在我们已经知道了,若是两个进程之间采用的是命名管道,那么在磁盘上必须有一个对应的命名管道文件名,而实际上我们在使用命令的时候并不存在类似的命名管道文件名,因此命令行上的管道实际上是匿名管道。

  • 相关阅读:
    hive创建分区表
    基于SpringBoot的校园志愿者管理系统
    自定义表单工具好用的优点是什么?
    JWT 技术
    想了解软件测试生命周期知识吗?
    Tkinter制作股票数据抓取小程序,有点秀!
    如何通过Photoshop将视频转换成GIF图片
    Altium Designer_PCB板装配图的PDF文件输出
    一个 不用充钱 也能让你变强的 VSCode 插件
    python基础语法(六)
  • 原文地址:https://blog.csdn.net/m0_62812354/article/details/134429726