• Linux——匿名管道、命名管道及进程池概念和实现原理


    目录

     一.什么是匿名管道

    二.如何使用匿名管道

    (一).pipe原理

    (二).pipe使用

    三.命名管道概念及区别

    (一).什么是命名管道

    (二).与匿名管道的联系和区别

    四.命名管道的使用

    (一).系统指令

    (二).mkfifo 

    五.进程池

    (一).概念与原理

    (二).代码原理与分析

    (三).进程池管道陷阱


     一.什么是匿名管道

    匿名管道是linux中一种非常古老进程间通信方式,本质上就是一个内存级的文件。

    一般用于父子进程间通信。概念上就是父进程与子进程共同使用一个管道文件来传输数据。

    虽然父子进程都有对管道的读和写功能,但在使用时只能读或者写,因此管道是单向通信,半双工模式。

    父进程把数据写入管道,子进程从管道中读取:

    二.如何使用匿名管道

    (一).pipe原理

    linux为我们提供了系统接口pipe,用于创建管道进行通信。

     参数是长度为2的整形数组,pipefd[0]代表读端文件描述符,pipefd[1]代表写端文件描述符

    返回值是int,创建成功返回0,失败返回-1,同时记录进errno。 

    pipe的使用原理上,就是首先父进程创建一个管道文件,但同时赋予管道文件两个文件描述符。

    一个是以读方式打开即pipefd[0],另一个是以写方式打开即pipefd[1]。

    之后创建子进程,由于子进程会对父进程进行拷贝,会把父进程的fd_array同时拷贝一份,也就拥有了管道对应的两个文件描述符(读端&写端)。

    图示如下:

    (二).pipe使用

    以下面代码为例:

    父进程使用write接口将字符串给管道,子进程从管道中接收字符串并打印。

    同时,子进程的read系统接口会阻塞,直到父进程往管道中写完数据,read一次性将此时管道内数据读取完并清空管道

    当父进程关闭管道后,若管道中还有数据时read函数会一次性读取完并在下一次读取时返回0,没有数据时直接返回0。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. using namespace std;
    7. int main()
    8. {
    9. int pfd[2] = { 0 };
    10. int ret = pipe(pfd);
    11. assert(ret == 0);
    12. pid_t id = fork();
    13. assert(id >= 0);
    14. if(id == 0)
    15. {
    16. close(pfd[1]);//关闭写端
    17. char GetStr[1024] = { 0 };
    18. ssize_t i = read(pfd[0], GetStr, sizeof GetStr);//接收数据
    19. GetStr[i] = '\0';
    20. cout << GetStr << endl;
    21. exit(0);
    22. }
    23. //父进程
    24. close(pfd[0]);
    25. char str[1024] = "hello world";
    26. write(pfd[1], str, sizeof str);//发送数据
    27. return 0;
    28. }

    三.命名管道概念及区别

    (一).什么是命名管道

    顾名思义,这是有名字的管道。它以文件的形式存在于系统中,在磁盘中有对应的节点但没有为其分配数据块(block)。

    换一种说法,它是能被我们看到(看到名字)且有inode编号的文件,但是inode中记录的block数量为0,使用时只能使用内存空间。

    (二).与匿名管道的联系和区别

    和匿名管道一样,本质上都是利用内存空间进行通信。

    都是半双工通信,任意时刻只能向一端发送数据

    管道生命周期不同,匿名管道伴随进程,命名管道只要不删除一直存在。

    通信对象不同,匿名管道一般用于父子进程通信,命名管道可用于任意进程间通信

    创建管道的方式不同,匿名管道使用pipe,命名管道使用mkfifo 

    四.命名管道的使用

    (一).系统指令

    系统指令方式:$mkfifo 路径+管道名

     演示:

    (二).mkfifo 

    代码方式:int mkfifo(const char *pathname, mode_t mode)

    自制翻译:int mkfifo(文件路径+管道名,文件权限);

    头文件:

    返回值int:成功返回0,失败返回-1并在errno中记录。

    文件权限:用于规定管道文件读写权限, 实际权限=设定权限&~umask。

                      比如0666实际上就是:0666 & ~0002(umask == 0002)即0661。

    因为命名管道是一个文件,使用方式与文件的使用方式相同,但如果只打开了读端或写端,该端口就会阻塞,直到写端或读端打开为止。

    使用方式:

    1. //读端
    2. int i = mkfifo("./fifo.ipc", 0666);//创建命名管道
    3. assert(i >= 0);
    4. int fd = open("./fifo.ipc", O_RDONLY);//打开管道读端,O_RDONLY:只读方式打开
    5. . . .
    6. char buf[1024] = { '\0' };
    7. ssize_t s = read(fd, buf, sizeof buf);//读取数据
    8. . . .
    9. close(fd);//关闭读端管道
    10. //写端
    11. int fd = open("./fifo.ipc", O_WRONLY);打开管道写端,O_WRONLY:只写方式打开
    12. . . .
    13. std::string buf;
    14. std::getline(std::cin, buf);
    15. ssize_t s = write(fd, buf.c_str(), buf.size());//向管道写入数据
    16. . . .
    17. close(fd);//关闭写端管道

    五.进程池

    (一).概念与原理

    进程池可以简单理解为父进程调度多个子进程完成不同的任务。类似于人脑与四肢的关系。

    我们可以自制一个简易的进程池。

    首先利用父进程fork多个子进程,每次fork之前都先使用pipe创建与这个子进程用于联系的管道

    同时记录子进程的pid和其对应的读端文件描述符(用于向其中传输数据与回收子进程)。

    之后调度某个子进程传输相关数据。

    (二).代码原理与分析

    在这份代码中,模拟实现5个函数,父进程会随机选择任意一个子进程调用任意一个函数。

    子进程调用系统read函数接收数据并调用对应的模拟函数。

    为了提高英语能力,小编选择使用英文注释😂。

    1. //"Command.h"
    2. #pragma once
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. typedef function<void()> func;
    9. vector callCommand;
    10. void Running()
    11. {
    12. cout << "Running now" << endl;
    13. }
    14. void Writing()
    15. {
    16. cout << "Writing now" << endl;
    17. }
    18. void Eating()
    19. {
    20. cout << "Eating now" << endl;
    21. }
    22. void Sleeping()
    23. {
    24. cout << "Sleeping now" << endl;
    25. }
    26. void Testing()
    27. {
    28. cout << "Testing now" << endl;
    29. }
    30. void CommandInit()
    31. {
    32. callCommand.push_back(Running);
    33. callCommand.push_back(Writing);
    34. callCommand.push_back(Eating);
    35. callCommand.push_back(Sleeping);
    36. callCommand.push_back(Testing);
    37. }
    38. void ShowAllCommand()
    39. {
    40. //... if you want you can write one ^-^
    41. }
    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include"Command.h"
    10. using namespace std;
    11. #define PROCESS_NUM 5 //number of process is 5
    12. int main()
    13. {
    14. CommandInit(); //init for Command function
    15. vectorpid_t, int>> KvPidFd; //record children process pid & read file fd
    16. for(int i = 0; i < PROCESS_NUM; i++) //creat child process
    17. {
    18. int pipefd[2] = { 0 };
    19. int k = pipe(pipefd);
    20. assert(k == 0); //-1 : creat false
    21. pid_t id = fork();
    22. if(id == 0) //child
    23. {
    24. close(pipefd[1]);
    25. while(1)
    26. {
    27. uint32_t accept = -1; //uint32_t : unsigned int in 32bit
    28. cout << "*********" << getpid() << endl;
    29. int s = read(pipefd[0], &accept, sizeof accept);
    30. if(s == 0) break; //if s is zero, that's mean child read zero word in pipe
    31. assert(accept >= 0);
    32. cout << "I am child " << getpid() << " now accept command : ";
    33. callCommand[accept](); //invoke function
    34. }
    35. cout << "child " << getpid() << "finished work !" << endl;
    36. close(pipefd[0]);
    37. exit(0);
    38. }
    39. //only father process can go to this step
    40. //take chiid pid & it's write fd as a mapping
    41. close(pipefd[0]);
    42. KvPidFd.push_back(make_pair(id, pipefd[1]));
    43. }
    44. srand((unsigned int)time(nullptr) * getpid() * 131);
    45. uint32_t command = 0;
    46. int proc = -1;
    47. int count = 0;
    48. while(1) //father
    49. {
    50. sleep(1);
    51. //random distribute function which will be commanded
    52. command = rand() % callCommand.size();
    53. assert(command >= 0);
    54. proc = rand() % PROCESS_NUM; //random distribute process which will be used
    55. assert(proc >= 0);
    56. write(KvPidFd[proc].second, &command, sizeof(command));
    57. count++;
    58. if(count == 5) break;//stop this loop when calling child five times
    59. }
    60. //close pipe files & revoke children
    61. for(auto kv : KvPidFd)
    62. {
    63. close(kv.second);
    64. }
    65. for(auto kv : KvPidFd)
    66. {
    67. waitpid(kv.first, nullptr, 0);
    68. }
    69. return 0;
    70. }

    (三).进程池管道陷阱

     有人可能有疑惑,这里有什么陷阱呢?

    嗯。。如果父进程在关闭写端管道同时关闭相应子进程呢

    看似没有问题?

    1. //close pipe files & revoke children
    2. for(auto kv : KvPidFd)
    3. {
    4. close(kv.second);
    5. pid_t id = waitpid(kv.first, nullptr, 0);
    6. assert(id > 0);
    7. cout << "From father : child " << id << "finish work" << endl;
    8. }

    但是程序直接夯住了!

     不要着急,我们来仔细梳理一下父进程与子进程的管道关系就能得到答案。

    当父进程创建子进程1前,先创建了管道1,子进程1与父进程的fd_array都会记录管道1的fd。

    当父进程创建子进程2前,先创建了管道2,子进程2与父进程fd_array都会记录管道2的fd。但是,子进程2的fd_array中也会记录管道1的fd

    是的,后面创建的子进程不仅会记录自己管道的fd,也会记录之前创建的管道fd,准确来讲是继承之前管道的写端

    于是,当父进程关闭管道写端后,管道写端并没有被全部关闭,read函数会一直阻塞,进而根本无法执行到waitpid的步骤。

    因此,只有将父进程所有管道的写端都关闭后,首先是最后创建的子进程完成read返回0的操作,因为对它而言,有它管道的写端的只有父进程。当最后一个子进程关闭后,倒数第二个开始关闭,...直到第一个关闭。

    所以,我们这是使用关闭管道和回收子进程分开的方式进行。

    图示如下:

     

    如果你是房间里最聪明的人,那么你走错房间了——未名


    如有错误,敬请斧正 

  • 相关阅读:
    btree,hash,fulltext,Rtree索引类型区别及使用场景
    python DevOps
    从驾考科目二到自动驾驶,聊聊 GPU 为什么对自动驾驶很重要
    【递归、搜索与回溯算法】第三节.21. 合并两个有序链表和206. 反转链表和24. 两两交换链表中的节点
    原码,反码,补码 以及 位运算
    vue钩子函数以及例子
    深入解析HTTPS与HTTP
    Http请求get与post请求方式的各种相关面试总结
    实例解读丨关于GaussDB ETCD服务异常
    Git 命令记录
  • 原文地址:https://blog.csdn.net/weixin_61857742/article/details/127975539