【Linux】进程间通信——管道

忘是亡心i 2024-04-27 07:34 155阅读 0赞

文章目录

  • 一、进程间通信
    • 进程间通信的目的
    • 进程间通信的发展和分类
  • 二、匿名管道
    • 管道介绍
    • 管道的原理
    • 管道的实现
    • 管道的特点
    • 管道的四种场景
    • mini进程池的实现
  • 三、命名管道
    • 命名管道模拟客户端和服务端

一、进程间通信

进程间通信(Interprocess Communication) 就是两个进程之间进行通信。进程是具有独立性(虚拟地址空间 + 页表保证进程运行的独立性),所以进程间通信成本会比较高!进程间通信的前提条件是先让不同的进程看到同一份资源(内存空间),该资源不能隶属于任何一个进程,应该属于操作系统,被进行通信的进程所共享。

进程间通信的目的

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

进程间通信的发展和分类

进程间通信的发展和分类如下:

  • Linux 原生能提供的管道,管道主要包括匿名管道 pipe 和命名管道。
  • SystemV 进程间通信,System V IPC 主要包括 System V 消息队列、System V 共享内存和 System V 信号量。System V 只能本地通信。
  • POSIX 进程间通信,POSIX IPC 主要包括消息队列、共享内存、信号量、互斥量、条件变量和读写锁。POSIX 进程通信既能进行本地通信,又能进行网络远程通信,具有高扩展和高可用性。

二、匿名管道

管道介绍

日常生活中,有非常多的管道,如:天然气管道、石油管道和自来水管道等。管道是 Unix 中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为管道。管道传输的都是资源,并且只能单向通信。

在这里插入图片描述

管道的原理

每个进程都有对应的文件描述符表,文件描述符表中有相应的数组,数组中存放了标准输入0,标准输出1,标准错误2,而每个进程描述符都会存放相应struct file的地址,在进程间通信的时候系统会提供一个内存文件,这个内存文件不会在磁盘刷新,这个文件被称为匿名文件,当我们以读和写方式打开一个文件,然后我们fork创建一个子进程,子进程也具有task_struct,并且子进程会继承父进程的文件描述符表(但是不会复制父进程打开的文件对象),而文件描述符表中存放文件的地址都是相同的,所以子进程的文件描述符表也指向父进程的文件,正是因为这样,在父进程以读和写打开一份文件,而子进程也同样读和写打开和父进程打开的一样的一份文件,这就让两个进程看到了同一份资源。但是这种管道只能实现单向通信,比如我们关闭父进程的写端,关闭子进程的读端让子进程去写这两个进程就实现单向通信了。管道只能单向通信的原因是文件只有一个缓冲区,一个写入位置一个读取位置所以只能单向通信,要是想双向通信那就打开两个管道!而上面所讲的管道就是匿名管道

在这里插入图片描述


管道的实现

? Makefile

  1. mypipe:mypipe.cc
  2. g++ -o $@ $^ -std=c++11
  3. .PHONY:clean
  4. clean:
  5. rm -rf mypipe

? 代码实现

  1. #include <iostream>
  2. #include <cassert>
  3. #include <string>
  4. #include <unistd.h>
  5. #include <cstring>
  6. #include <sys/types.h>
  7. #include <sys/wait.h>
  8. using namespace std;
  9. int main()
  10. {
  11. // 让不同的进程看到同一份资源
  12. int pipefd[2] = {
  13. 0};
  14. // 1.创建管道
  15. int n = pipe(pipefd);
  16. if(n < 0)
  17. {
  18. std::cout << "pipe error, " << errno << ": " << strerror(errno) << std::endl;
  19. return 1;
  20. }
  21. // printf("pipefd[0]:%d\n",pipefd[0]);
  22. // printf("pipefd[1]:%d\n",pipefd[1]);
  23. // 创建子进程
  24. pid_t id = fork();
  25. assert(id != -1);
  26. if(id == 0) //子进程 —— 往管道中写入数据
  27. {
  28. close(pipefd[0]);
  29. //开始通信
  30. const string namestr = "hello,我是子进程";
  31. int cnt = 1;
  32. char buffer[1024];
  33. while(true)
  34. {
  35. snprintf(buffer, sizeof buffer, "%s, 计数器: %d, 我的PID: %d", namestr.c_str(), cnt++, getpid());
  36. write(pipefd[1], buffer, strlen(buffer));
  37. sleep(1);
  38. }
  39. close(pipefd[1]);
  40. exit(0);
  41. }
  42. //父进程 —— 从管道中读取数据
  43. close(pipefd[1]);
  44. char buffer[1024];
  45. //int cnt = 0;
  46. while(true)
  47. {
  48. int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
  49. if(n > 0)
  50. {
  51. buffer[n] = '\0';
  52. cout << "我是父进程,子进程给我的message是:" << buffer << endl;
  53. }
  54. else if(n == 0)
  55. {
  56. cout << "我是父进程,读到了文件的结尾" << endl;
  57. break;
  58. }
  59. else
  60. {
  61. cout << "我是父进程,读取异常了" << endl;
  62. break;
  63. }
  64. }
  65. close(pipefd[0]);
  66. return 0;
  67. }

运行结果如图:
在这里插入图片描述
在这里插入图片描述

这里确实完成了进程间的单向通信,我们可以清晰地看到有两个进程,并且子进程将自己的数据给了父进程。


管道的特点

  • 单向通信
  • 管道的本质是文件,因为fd的生命周期随进程,管道的生命周期也是随进程的。
  • 管道通信,通常是用来进行 “血缘关系” 的进程,进行进程间通信,常用于父子进程间通信——pipe打开管道,并不清楚管道的名字,所以是匿名管道。
  • 在管道通信中,写入的次数,和读取的次数,不是严格匹配的 读写次数的多少没有强相关 — 表现 ——字节流。
  • 具有一定的协同能力,让reader和writer能够按照一定的步骤进行通信 — 自带同步机制。
  • 管道是基于文件的,文件的生命周期是随进程的,那么管道的生命周期也是随进程的。
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是单向通信的,就是半双工通信的一种特殊情况,数据只能向一个方向流动。需要双方通信时,需要建立起两个管道。半双工通信就是要么在收数据,要么在发数据,不能同时在收数据和发数据(比如两个人在交流时,一个人在说,另一个人在听);而全双工通信是同时进行收数据和发数据(比如两个人吵架的时候,相互问候对方,一个人既在问候对方又在听对方的问候)。
  • 当要写入的数据量不大于 PIPE_BUF 时,Linux 将保证写入的原子性。
  • 当要写入的数据量大于 PIPE_BUF 时,Linux 将不再保证写入的原子性。
  • 指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。

管道的四种场景

  • 如果我们read读取完毕了所有的管道数据,如果对方不发,我就只能等待

这里我们让父进程的读取速度不变,让子进程写的慢一些。

在这里插入图片描述
在这里插入图片描述

这里我们可以看到,光标卡在这儿不动了,这是因为子进程每隔5秒向管道写入一次数据,因此,如果管道中没有数据,读端在读,此时默认会直接阻塞当前正在读取的进程

  • 如果我们writer端将管道写满了,我们将不能继续往管道中写入数据。

在这里插入图片描述
在这里插入图片描述

管道是固定大小的缓冲区,当管道被写满,就不能再写了。此时写端会阻塞。因此管道具有一定的协同能力,能让reader和writer按照一定的步骤进行通信

  • 如果关闭了写端,读取完毕管道数据,在读,就会read返回0,表明读到了文件结尾。

在这里插入图片描述

在这里插入图片描述

  • 写端一直写,读端关闭,操作系统不会维护无意义,低效率,或者浪费资源的事情。因此OS会杀死一直在写入的进程!

在这里插入图片描述
在这里插入图片描述

这里我们看到确实如此,OS不会维护无意义,低效率,或者浪费资源的事情,因此操作系统通过13号信号来杀死了子进程。


mini进程池的实现

  1. // Task.hpp的实现
  2. #pragma once
  3. #include <iostream>
  4. #include <vector>
  5. #include <unistd.h>
  6. using namespace std;
  7. //定义函数指针
  8. typedef void (*func_t) ();
  9. void PrintLog()
  10. {
  11. std::cout << "pid: "<< getpid() << ", 打印日志任务,正在被执行..." << std::endl;
  12. }
  13. void InsertMySQL()
  14. {
  15. std::cout << "执行数据库任务,正在被执行..." << std::endl;
  16. }
  17. void NetRequest()
  18. {
  19. std::cout << "执行网络请求任务,正在被执行..." << std::endl;
  20. }
  21. //这里我们规定,每一个command都必须是四字节
  22. #define COMMAND_LOG 0
  23. #define COMMAND_MYSQL 1
  24. #define COMMAND_REQEUST 2
  25. class Task
  26. {
  27. public:
  28. Task()
  29. {
  30. funcs.push_back(PrintLog);
  31. funcs.push_back(InsertMySQL);
  32. funcs.push_back(NetRequest);
  33. }
  34. void Execute(int command)
  35. {
  36. if(command >=0 && command < funcs.size())
  37. funcs[command]();
  38. }
  39. ~Task()
  40. {
  41. };
  42. public:
  43. vector<func_t> funcs;
  44. };
  45. // ctrlProcess的实现
  46. #include <iostream>
  47. #include <vector>
  48. #include <cassert>
  49. #include <unistd.h>
  50. #include <sys/types.h>
  51. #include <sys/wait.h>
  52. using namespace std;
  53. #include "Task.hpp"
  54. const int gnum = 3;
  55. Task t;
  56. class Endpoint
  57. {
  58. private:
  59. static int number;
  60. public:
  61. Endpoint(pid_t id, int write_fd)
  62. :_child_id(id)
  63. ,_write_fd(write_fd)
  64. {
  65. char namebuffer[64];
  66. snprintf(namebuffer, sizeof namebuffer, "process-%d[%d:%d]", number++, _child_id, _write_fd);
  67. processname = namebuffer;
  68. }
  69. string name() const
  70. {
  71. return processname;
  72. }
  73. ~Endpoint()
  74. {
  75. };
  76. public:
  77. pid_t _child_id;
  78. int _write_fd;
  79. string processname;
  80. };
  81. int Endpoint::number = 0;
  82. //子进程要执行的方法
  83. void WaitCommand()
  84. {
  85. while(true)
  86. {
  87. int command = 0;
  88. int n = read(0, &command, sizeof(int));
  89. if(n == sizeof(int))
  90. {
  91. t.Execute(command);
  92. }
  93. else if(n == 0)
  94. {
  95. cout << "父进程让我退出,我就退出了: " << getpid() << endl;
  96. break;
  97. }
  98. else
  99. {
  100. break;
  101. }
  102. }
  103. }
  104. void createProcesses(vector<Endpoint>* end_points)
  105. {
  106. vector<int> fds;
  107. for(int i = 0; i < gnum; i++)
  108. {
  109. // 1.1 创建管道
  110. int pipefd[2] = {
  111. 0};
  112. int n = pipe(pipefd);
  113. assert(n == 0);
  114. (void)n;
  115. // 1.2 创建进程
  116. pid_t id = fork();
  117. assert(id != -1);
  118. //子进程 —— 从管道中读取数据
  119. if(id == 0)
  120. {
  121. for(auto& fd : fds) close(fd);
  122. close(pipefd[1]);
  123. dup2(pipefd[0], 0); // 子进程读取指令的时候,从标准输入读取 == 输入重定向
  124. WaitCommand(); // 子进程等待获取命令
  125. close(pipefd[0]);
  126. exit(0);
  127. }
  128. //1.3 父进程——关闭不需要的fd
  129. close(pipefd[0]);
  130. // 1.4 将新的子进程和他的管道写端构建对象
  131. end_points->push_back(Endpoint(id, pipefd[1]));
  132. fds.push_back(pipefd[1]);
  133. }
  134. }
  135. int ShowBoard()
  136. {
  137. std::cout << "------------------------------------------" << std::endl;
  138. std::cout << "| 0. 执行日志任务 1. 执行数据库任务 |" << std::endl;
  139. std::cout << "| 2. 执行请求任务 3. 退出 |" << std::endl;
  140. std::cout << "------------------------------------------" << std::endl;
  141. std::cout << "请选择$ ";
  142. int command = 0;
  143. std::cin >> command;
  144. return command;
  145. }
  146. void ctrlProcess(const vector<Endpoint>& end_points)
  147. {
  148. int num = 0;
  149. int cnt = 0;
  150. while(true)
  151. {
  152. // 1. 选择任务
  153. int command = ShowBoard();
  154. if(command == 3) break;
  155. if(command < 0 || command > 3) continue;
  156. // 2.选择进程
  157. int index = cnt++;
  158. cnt %= end_points.size();
  159. cout << "选择了进程: " << end_points[index].name() << " | 处理任务: " << command << endl;
  160. // 3. 下发任务
  161. write(end_points[index]._write_fd, &command, sizeof(command));
  162. sleep(1);
  163. }
  164. }
  165. void waitProcess(const vector<Endpoint>& end_points)
  166. {
  167. for(int end = 0; end < end_points.size(); end++)
  168. {
  169. cout << "父进程让子进程退出:" << end_points[end]._child_id << endl;
  170. close(end_points[end]._write_fd);
  171. waitpid(end_points[end]._child_id, nullptr, 0);
  172. cout << "父进程回收了子进程:" << end_points[end]._child_id << endl;
  173. }
  174. sleep(5);
  175. }
  176. int main()
  177. {
  178. // 1. 构建控制结构,父进程写入,子进程读取。
  179. vector<Endpoint> end_points;
  180. createProcesses(&end_points);
  181. // 2. 进程控制
  182. ctrlProcess(end_points);
  183. // 3. 处理所有的退出问题
  184. waitProcess(end_points);
  185. return 0;
  186. }

随机派发任务:

在这里插入图片描述
用户派发指定任务

在这里插入图片描述


三、命名管道

匿名管道有一个 缺陷 就是:只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想要让两个毫不相干的进程进行通信,可以使用FIFO文件来实现,他就是 命名管道

命名管道的创建:

  1. mkfifo named_pipe

在这里插入图片描述

往管道里面写入数据 / 从管道中读取数据

在这里插入图片描述

原理如下:

两个进程打开同一个文件,站在内核的角度,第二个文件不需要再被创建struct file对象,因为OS会识别到打开的文件被打开了。在内核中,此时就看到了同一份资源,有着操作方法和缓冲区,不需要把数据刷新到磁盘上去,所以无论是匿名还是命名管道,本质上都是管道。

在这里插入图片描述

匿名管道:通过继承的方式看到同一份资源。
命名管道:通过让不同的进程打开指定名称(路径+文件名,具备唯一性)的同一个文件看到同一份资源,所以命名管道是通过文件名来标定唯一性的。而匿名管道是通过继承的方式来标定的。


命名管道模拟客户端和服务端

创建一个管道文件,让读写端进程分别按照自己的需求打开文件,然后进行通信。

makefile

  1. .PHONY:all
  2. all:server client
  3. server:server.cc
  4. g++ -o $@ $^ -std=c++11
  5. client:client.cc
  6. g++ -o $@ $^ -std=c++11
  7. .PHONY:clean
  8. clean:
  9. rm -f client server

comm.hpp

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #define NUM 1024
  5. const std::string fifoname = "./fifo";
  6. uint32_t mode = 0666;

server.cc

在这里插入图片描述

client.cc

在这里插入图片描述

运行结果:

在这里插入图片描述

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

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

发表评论

表情:
评论列表 (有 0 条评论,155人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Linux进程通信管道

    管道用于进程间的通信,进程间通信的 公共资源叫做临界资源,访问临界资源的代码叫做临界区 。 管道文件以p开头,管道是一种最基本的IPC机制 管道又有匿名管道和命名管道之分