• linux——进程间通信——管道


     ✅<1>主页::我的代码爱吃辣
    📃<2>知识讲解:Linux——进程间通信——管道通信
    ☂️<3>开发环境:Centos7
    💬<4>前言:进程间通信(InterProcess Communication,IPC)是指在不同进程之间传播或交换信息。

    目录

    一.什么是进程间通信

    二.进程间通信目的

     三.进程间通信发展

    四.什么是管道

    五.匿名管道

     六.父子进程管道通信

    1.匿名管道的场景与特点

    2. 用fork来共享管道原理

     七.基于匿名管道实现进程池


    一.什么是进程间通信

    进程间通信(InterProcess Communication,IPC)是指在不同进程之间传播或交换信息。

    二.进程间通信目的

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

     三.进程间通信发展

    管道

    • 匿名管道pipe
    • 命名管道

    System V IPC

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

    POSIX IPC

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

    四.什么是管道

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

    五.匿名管道

    头文件:#include
    功能:创建一无名管道。
    原型:int pipe(int fd[2]);
    参数:fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端。
    返回值:成功返回0,失败返回错误代码。

     测试代码:

    pipe.cc:

    1. #include
    2. #include
    3. #include
    4. #include
    5. int main(void)
    6. {
    7. int fds[2]; // f[0]管道读端,f[1]管道写端
    8. char buf[100];
    9. int len;
    10. // 创建管道
    11. if (pipe(fds) == -1)
    12. perror("make pipe"), exit(1);
    13. // read from stdin
    14. while (fgets(buf, 100, stdin))
    15. {
    16. len = strlen(buf);
    17. // 写入管道
    18. if (write(fds[1], buf, len) != len)
    19. {
    20. perror("write to pipe");
    21. break;
    22. }
    23. memset(buf, 0, sizeof(buf));
    24. // 从管道中读取
    25. if ((len = read(fds[0], buf, 100)) == -1)
    26. {
    27. perror("read from pipe");
    28. break;
    29. }
    30. // 写入显示器
    31. if (write(1, buf, len) != len)
    32. {
    33. perror("write to stdout");
    34. break;
    35. }
    36. }
    37. }

     makefile:

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

    测试结果:

     六.父子进程管道通信

    我们知道fork之后,子进程会继承父进程的代码,数据会发生写时拷贝,那么父进程的文件描述符会不会被继承呢?会的。那么父进程创建的管道,其中的两个文件描述符一个指向管道的读端,一个指向管道的写端,也会被子进程继承。

    1.匿名管道的场景与特点

    管道的特点:

    1. 管道只具有单向通信的功能。
    2. 管道的本质是文件,因为fd的生命周期是随进程的,所以管道的生命周期也是随进程的。
    3. 管道通信,通常用来进行具有“血缘”关系的进程,进行进程间的通信。常用父子进程通信,
    4. 在管道的通信中,写入的次数,和读取的次数,不是严格匹配的,读写次数没有强相关,是面向字节流。
    5. 具有一定的协同能力,如果写端没有写入,读端会被阻塞——自带同步机制。

    特殊场景:

    1. 如果我们read读取完毕了所有的管道数据,如果对方不发,我就只能等待。
    2. 如果我们writer端将管道写满了,我们还能写吗?不能
    3. 如果我关闭了写端,读取完毕管道数据,在读,就会read返回0,表明读到了文件结尾
    4. 写端一直写,读端关闭,会发生什么呢?没有意义。OS不会维护无意义,低效率,或者浪费资源的事情。OS会杀死一直在写入的进程!OS会通过信号来终止进程,(13)SIGPIPE。

    测试代码:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. int main()
    10. {
    11. // 让不同的进程看到同一份资源!!!!
    12. // 任何一种任何一种进程间通信中,一定要 先 保证不同的进程之间看到同一份资源
    13. int pipefd[2] = {0}; // pipefd[0] 读端, pipe[1]写端
    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. std::cout << "pipefd[0]: " << pipefd[0] << std::endl; // 读端
    22. std::cout << "pipefd[1]: " << pipefd[1] << std::endl; // 写端
    23. // 2. 创建子进程
    24. pid_t id = fork();
    25. assert(id != -1);
    26. if (id == 0) // 子进程
    27. {
    28. // 3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入
    29. close(pipefd[0]);
    30. // 4. 开始通信 -- 结合某种场景
    31. const std::string namestr = "hello,我是子进程";
    32. int cnt = 1;
    33. char buffer[1024];
    34. while (true)
    35. {
    36. snprintf(buffer, sizeof buffer, "%s, 计数器: %d, 我的PID: %d", namestr.c_str(), cnt++, getpid());
    37. write(pipefd[1], buffer, strlen(buffer));
    38. sleep(1);
    39. }
    40. // 退出时关闭打开的文件描述符
    41. close(pipefd[1]);
    42. exit(0);
    43. }
    44. // 父进程
    45. // 3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入
    46. close(pipefd[1]);
    47. // 4. 开始通信 -- 结合某种场景
    48. char buffer[1024];
    49. int cnt = 0;
    50. while (true)
    51. {
    52. // sleep(10);
    53. // sleep(1);
    54. int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
    55. if (n > 0)
    56. {
    57. buffer[n] = '\0';
    58. std::cout << "我是父进程, child give me message: " << buffer << std::endl;
    59. }
    60. else if (n == 0)
    61. {
    62. std::cout << "我是父进程, 读到了文件结尾" << std::endl;
    63. break;
    64. }
    65. else
    66. {
    67. std::cout << "我是父进程, 读异常了" << std::endl;
    68. break;
    69. }
    70. sleep(1);
    71. if (cnt++ > 5)
    72. break;
    73. }
    74. // 父进程读端关闭,子进程会收到13号信号
    75. close(pipefd[0]);
    76. // 回收子进程的僵尸状态
    77. int status = 0;
    78. waitpid(id, &status, 0);
    79. std::cout << "子进程pid:" << id << ",收到的信号sig: " << (status & 0x7F) << std::endl;
    80. sleep(1);
    81. return 0;
    82. }

    测试结果:

    2. 用fork来共享管道原理

     

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

     七.基于匿名管道实现进程池

    当没有数据可读时,read调用阻塞,即进程暂停执行,一直等到有数据来到为止。

    如果我们使用,父进程来控制写端,子进程进行读取,发送数据让子进程执行特定的任务,我们就可以实现对子进程的控制。

    代码:

    CtrlProc.cc:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include "Task.hpp"
    8. using namespace std;
    9. #define NUM_PROC 5
    10. struct child_pip
    11. {
    12. child_pip(int fd, pid_t pid)
    13. : _fd(fd), _pid(pid)
    14. {
    15. }
    16. ~child_pip()
    17. {
    18. }
    19. int _fd;
    20. pid_t _pid;
    21. };
    22. void WaitCommand()
    23. {
    24. Task task;
    25. int command;
    26. while (1)
    27. {
    28. size_t n = read(0, &command, sizeof(int));
    29. if (n == 4) // 读取成功
    30. {
    31. task.funcs[command]();
    32. }
    33. else if (n == 0) // 读取失败
    34. {
    35. break;
    36. }
    37. else
    38. {
    39. break;
    40. }
    41. }
    42. }
    43. void creatproc(vector &child_pip_v)
    44. {
    45. for (int i = 0; i < NUM_PROC; i++)
    46. {
    47. // 1.创建管道
    48. int pipfd[2];
    49. pipe(pipfd);
    50. // 2.创建子进程
    51. pid_t pid = fork();
    52. if (pid < 0)
    53. perror("fork");
    54. // 我们想让子进程从管道读,父进程向管道写
    55. if (pid == 0) // 子进程
    56. {
    57. // 3.关闭不必要的文件描述符
    58. close(pipfd[1]);
    59. // 3.1重定向,将来子进程指向0号文件描述符读取
    60. dup2(pipfd[0], 0);
    61. WaitCommand();
    62. exit(0);
    63. }
    64. // 父进程
    65. // 3.关闭不必要的文件描述符
    66. cout << "子进程pid:" << pid << endl;
    67. close(pipfd[0]);
    68. // 建立好子进程与管道的映射,父进程的写端口,和子进程pid
    69. child_pip_v.push_back(child_pip(pipfd[1], pid));
    70. }
    71. }
    72. void ctrlproc(vector &child_pip_v)
    73. {
    74. while (1)
    75. {
    76. int command = 0;
    77. cin >> command;
    78. if (command == -1)
    79. break;
    80. int i = rand() % NUM_PROC;
    81. write(child_pip_v[i]._fd, &command, sizeof(int));
    82. }
    83. }
    84. void waitproc(vector &child_pip_v)
    85. {
    86. int status = 0;
    87. for (int i = 0; i < child_pip_v.size(); i++)
    88. {
    89. close(child_pip_v[i]._fd);
    90. }
    91. // sleep(5);
    92. for (int i = 0; i < child_pip_v.size(); i++)
    93. {
    94. waitpid(child_pip_v[i]._pid, &status, 0);
    95. cout << "子进程:" << child_pip_v[i]._pid << "退出" << endl;
    96. }
    97. }
    98. int main()
    99. {
    100. vector child_pip_v;
    101. creatproc(child_pip_v);
    102. ctrlproc(child_pip_v);
    103. waitproc(child_pip_v);
    104. return 0;
    105. }

     Task.cc:

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. typedef void (*fun_t)();
    6. void beatxyy()
    7. {
    8. cout << "子进程:" << getpid() << ",执行数据库" << endl;
    9. }
    10. void beatxyf()
    11. {
    12. cout << "子进程:" << getpid() << ",写入日志" << endl;
    13. }
    14. void beatwy()
    15. {
    16. cout << "子进程:" << getpid() << ",读取网卡" << endl;
    17. }
    18. void beatwj()
    19. {
    20. cout << "子进程:" << getpid() << ",刷新缓冲区" << endl;
    21. }
    22. void beatxjy()
    23. {
    24. cout << "子进程:" << getpid() << ",数据比对" << endl;
    25. }
    26. struct Task
    27. {
    28. Task()
    29. {
    30. funcs.push_back(beatxyy);
    31. funcs.push_back(beatxyf);
    32. funcs.push_back(beatwy);
    33. funcs.push_back(beatwj);
    34. funcs.push_back(beatxjy);
    35. }
    36. vector<fun_t> funcs;
    37. };

    makefile:

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

    测试结果:

     注意:

    为什么这里的waitproc我们要分开成两个循环,如果一个循环,文件描述符会无法关闭完,子进程也就无法退出。

    我们关闭了第一个文件描述符,第一个管道由于继承问题,第一个管道还会有后面的子进程也会指向。最终导致我们只能有最后一个子进程先退出了,其他子进程进程陆续退出,此时进程等待也已经结束了,除了最后一个子进程。其他子进程的僵尸状态都没有被回收。

  • 相关阅读:
    工单系统相关概念
    笔试选择题-图
    【SpringCloud负载均衡】【源码+图解】【一】LoadBalancer的HelloWorld体验
    实验室安全巡检管理系统—全面安全检查
    动态规划-完全平方数
    按键精灵调用大漠插件源码例子
    电视盒子什么牌子好?花费30天测评盘点超值电视盒子推荐
    c++ - 第10节 - list类
    SystemVerilog Assertions应用指南 Chapter 1.17使用参数的SVA检验器
    【Ant Design Pro】使用ant design pro做为你的开发模板(完结篇)上线部署项目
  • 原文地址:https://blog.csdn.net/qq_63943454/article/details/133467895