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

news/2024/11/29 1:34:41/

目录

 一.什么是匿名管道

二.如何使用匿名管道

(一).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。

#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<string>
#include<assert.h>
using namespace std;
int main()
{int pfd[2] = { 0 };int ret = pipe(pfd);assert(ret == 0);pid_t id = fork();assert(id >= 0);if(id == 0){close(pfd[1]);//关闭写端char GetStr[1024] = { 0 };ssize_t i = read(pfd[0], GetStr, sizeof GetStr);//接收数据GetStr[i] = '\0';cout << GetStr << endl;exit(0);}//父进程close(pfd[0]);char str[1024] = "hello world";write(pfd[1], str, sizeof str);//发送数据return 0;
}

三.命名管道概念及区别

(一).什么是命名管道

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

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

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

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

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

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

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

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

四.命名管道的使用

(一).系统指令

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

 演示:

(二).mkfifo 

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

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

头文件:<sys/types.h>、<sys/stat.h>

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

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

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

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

使用方式:

//读端
int i = mkfifo("./fifo.ipc", 0666);//创建命名管道
assert(i >= 0);
int fd = open("./fifo.ipc", O_RDONLY);//打开管道读端,O_RDONLY:只读方式打开
. . .
char buf[1024] = { '\0' };
ssize_t s  = read(fd, buf, sizeof buf);//读取数据
. . .
close(fd);//关闭读端管道//写端
int fd = open("./fifo.ipc", O_WRONLY);打开管道写端,O_WRONLY:只写方式打开
. . .
std::string buf;
std::getline(std::cin, buf);
ssize_t s  = write(fd, buf.c_str(), buf.size());//向管道写入数据
. . .
close(fd);//关闭写端管道

五.进程池

(一).概念与原理

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

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

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

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

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

(二).代码原理与分析

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

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

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

//"Command.h"
#pragma once
#include<iostream>
#include<cstdio>
#include<vector>
#include<functional>
using namespace std;
typedef function<void()> func;
vector<func> callCommand;
void Running()
{cout << "Running now" << endl;
}
void Writing()
{cout << "Writing now" << endl;
}
void Eating()
{cout << "Eating now" << endl;
}
void Sleeping()
{cout << "Sleeping now" << endl;
}
void Testing()
{cout << "Testing now" << endl;
}void CommandInit()
{callCommand.push_back(Running);callCommand.push_back(Writing);callCommand.push_back(Eating);callCommand.push_back(Sleeping);callCommand.push_back(Testing);
}
void ShowAllCommand()
{//... if you want you can write one ^-^
}
#include<cstdlib>
#include<iostream>
#include<cstdio>
#include<vector>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/fcntl.h>
#include<assert.h>
#include"Command.h"
using namespace std;
#define PROCESS_NUM 5 //number of process is 5int main()
{CommandInit(); //init for Command functionvector<pair<pid_t, int>> KvPidFd; //record children process pid & read file fdfor(int i = 0; i < PROCESS_NUM; i++) //creat child process{int pipefd[2] = { 0 };int k = pipe(pipefd);assert(k == 0); //-1 : creat falsepid_t id = fork();if(id == 0) //child{close(pipefd[1]);while(1){uint32_t accept = -1;  //uint32_t : unsigned int in 32bitcout << "*********" << getpid() << endl; int s = read(pipefd[0], &accept, sizeof accept);if(s == 0) break; //if s is zero, that's mean child read zero word in pipeassert(accept >= 0);cout << "I am child " << getpid() << " now accept command : ";callCommand[accept]();  //invoke function}cout << "child " << getpid() << "finished work !" << endl;close(pipefd[0]);exit(0);}//only father process can go to this step//take chiid pid & it's write fd as a mappingclose(pipefd[0]);KvPidFd.push_back(make_pair(id, pipefd[1]));}srand((unsigned int)time(nullptr) * getpid() * 131);uint32_t command = 0;int proc = -1;int count = 0;while(1) //father{sleep(1); //random distribute function which will be commanded command = rand() % callCommand.size(); assert(command >= 0);proc = rand() % PROCESS_NUM; //random distribute process which will be used assert(proc >= 0);write(KvPidFd[proc].second, &command, sizeof(command));count++;if(count == 5) break;//stop this loop when calling child five times}//close pipe files & revoke childrenfor(auto kv : KvPidFd){close(kv.second);}for(auto kv : KvPidFd){waitpid(kv.first, nullptr, 0);}return 0;
}

(三).进程池管道陷阱

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

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

看似没有问题?

//close pipe files & revoke childrenfor(auto kv : KvPidFd){close(kv.second);pid_t id = waitpid(kv.first, nullptr, 0);assert(id > 0);cout << "From father : child " << id << "finish work" << endl;}

但是程序直接夯住了!

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

当父进程创建子进程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的操作,因为对它而言,有它管道的写端的只有父进程。当最后一个子进程关闭后,倒数第二个开始关闭,...直到第一个关闭。

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

图示如下:

 

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


如有错误,敬请斧正 


http://www.ppmy.cn/news/1618.html

相关文章

【Python百日进阶-数据分析】Day124 - Plotly Figure参数:饼图(二)

文章目录metacustomdata 自定义数据domain 域automarginmarker 标记textfont 文字字体textinfo 文本信息direction 方向holehoverlabel 悬停标签insidetextfont 内部文字字体insidetextorientation 内部文本方向outsidetextfont 外部文本字体rotation 旋转scalegroupsort 排序u…

华为机试真题 C++ 实现【连接器问题】【2022.11 Q4新题】

目录 题目 思路 考点 Code 题目 有一组区间[a0,b0],[a1,b1],…(a,b表示起点,终点),区间有可能重叠、相邻,重叠或相邻则可以合并为更大的区间; 给定一组连接器[x1,x2,x3,…](x表示连接器的最大可连接长度,即x>=gap),可用于将分离的区间连接起来,但两个…

GIT分布式版本控制系统 | 命令讲解入门

Git概述 Git是一个开源的分布式版本控制系统&#xff0c;可以有效、高速地处理从很小到非常大的项目版本管理。 也是Linus Torvalds为了帮助管理Linux内核开发而开发的一个开放源码的版本控制软件&#xff1b;分布式相比于集中式的最大区别在于开发者可以提交到本地&#xff0c…

【免费赠送源码】Springboot喵喵宠物医院管理系统ti5f6计算机毕业设计-课程设计-期末作业-毕设程序代做

【免费赠送源码】Springboot喵喵宠物医院管理系统ti5f6计算机毕业设计-课程设计-期末作业-毕设程序代做 【免费赠送源码】Springboot喵喵宠物医院管理系统ti5f6计算机毕业设计-课程设计-期末作业-毕设程序代做本源码技术栈&#xff1a; 项目架构&#xff1a;B/S架构 开发语言…

[go学习笔记.第十八章.数据结构] 1.基本介绍,稀疏数组,队列(数组实现),链表

一.基本介绍 1.数据结构(算法)的介绍 (1).数据结构是一门研究算法的学科&#xff0c;自从有了编程语言也就有了数据结构,学好数据结构可以编写出更加漂亮&#xff0c;更加有效率的代码 (2).要学习好数据结构就要多多考虑如何将生活中遇到的问题用程序去实现解决 (3).程序&…

Linux UART编程 驱动蓝牙芯片

在熟悉了UART概念后&#xff0c;我们要学以致用&#xff0c;在Linux用起来来驱动起来蓝牙芯片&#xff01; 我们直接借用man来看下&#xff0c;命令如下&#xff1a; man termios 1.头文件引用 #include <termios.h> #include <unistd.h> 2.串口打开关闭 open…

数据库基础 - 数据类型、关键字、cmd中操作数据库的命令

cmd中操作数据库的命令 mysql -hlocalhost -用户名 -密码 show database&#xff1b;查询数据库中的小数据库 show 数据库名&#xff1b;查询某一个小数据库 show 表名&#xff1b;查询表的结构 exit 退出数据类型 数值类型 int &#xff1a;整形 double&#xff1a;双精度&…

XXL-Job海量数据处理-分片任务实战

文章目录一、需求1. 场景2. 分析3. 案例二、什么是分⽚任务2.1. 分⽚路由策略2.2. 海量数据处理2.3. 分片数量2.4. 分片值颁发2.5. 案例三、解决思路3.1. 数据拆分3.2. 分片数量3.3. 分⽚⽅式3.4. 路由策略3.5. 程序实战一、需求 1. 场景 有⼀个任务需要处理100W条数据&#…