文章目录

序章:在孤岛上的呼唤
在计算机的世界里,每一个进程就像一座孤岛,独立运行,彼此隔绝。然而,有时这些孤岛之间需要交换讯息,仿佛远古的烽火台,在寂静中传递着意图与数据。而在这诸多桥梁之中,匿名管道(Anonymous
Pipe),恰似烟雨朦胧中的一条小舟,轻盈却有效,将数据静静地送往彼岸。
本文将带你穿越这条隐秘的通道,探寻其创建、使用与局限,感受进程之间无声却有力的对话。
🏙️正文
1、进程间通信相关概念
在正式学习 匿名管道
之前,需要简单了解一下通信的相关概念
1.1、目的
进程间通信主要有以下四个目的:
- 数据传输 :不同进程间进行数据传输,比如此时我写的博客数据正在源源不断的上传至
CSDN
服务器中 - 资源共享 :多个进程之间需要共享资源,假设每个用户都是独立的进程,那么整个 C 站就是一个被共享的资源,用户之前可以共享其技术资源
- 事件通知:一个进程向其他进程发送消息,通知处理相关事宜,比如 子进程终止时,需要通知父进程,回收其资源
- 进程控制:有些进程需要起到 管理者 的作用,于是需要与被管理进程之间构建通信关系,进程任务下达及进程控制,并对进程状态进行实时监视
其实进程间通信的最终目的就是 打破各个独立进程之前的壁垒,进行任务协同
- 进程间具有独立性,这是原则
- 让进程间可以更好的协同工作,这是目的
因此进程间通信的本质
就是 让不同
的进程看到同一份 “资源”
其中的 资源
由OS
直接或间接提供
无论后续的哪种进程间通信的解决方案,都要解决以下两个问题:
- 想办法让不同的进程看到同一份资源
- 让其中一方写入,另一方读取,完成通信;至于通信的目的及后续工作,需要结合具体场景分析
进程间通信的发展可以简单概况为以下三个时期:
管道时期(
古老的通信方式)System V
标准时期(本地化进程间通信)POSIX
标准时期(网络中进程间通信)
管道可以说是十分古老且简单了,后来新出的System V
标准丰富了进程间通信的方式,但奈何无法满足网络中的进程间通信需求,于是诞生了更好的 POSIX
标准
管道适合深入学习,探究进程间通信时的原理及执行流程
System V
标准如今比较少用了,但其通信速度极快的共享内存还是值得深入学习的
POSIX
是Unix
系统的一个设计标准,很多类Unix
系统也在支持兼容这个标准,如Linux
, POSIX
标准具有跨平台性,就连 Windows 也对其进行了支持,后续学习 同步与互斥
时,所使用的信号量等都是出自 POSIX 标准,这是进程间通信的学习重点
POSIX
标准支持网络中通信,比如 套接字(socket)
就在此标准中
1.3、分类
根据不同发展时期的标准,可以将进程间通信的解决方案划分为以下几种:
管道:
- 匿名管道
- 命名管道
System V
标准:
System V
消息队列System V
共享内存System V
信号量
POSIX
标准:
POSIX
消息队列POSIX
共享内存POSIX
信号量POSIX
互斥量(互斥锁)POSIX
条件变量POSIX
读写锁
2、什么是管道?
管道是 Unix 系统 IPC (
进程间通信)中最古老
的方式,其历史最早可追溯至 1964年10月11日
在命令行中输入|
即可使用管道
创建两个睡眠时间较长的 后台进程
sleep 10000 | sleep 20000 &
可以看出,两个 sleep
进程的 PPID
一致,同时 PID
连续,因此这两个进程是兄弟关系
管道分为 匿名管道
和 命名管道
,两者绝大部分原理、特点都一致,本文主要介绍 匿名管道
,同时适用于 命名管道
的知识点统一称为 管道
Linux 中一切皆文件,所以管道本质上就是一个文件
3、管道的工作原理
管道的工作原理其实很简单:打开一个文件,让两个进程分别享有读端与写端 fd
,对文件进行操作即可
命名管道和匿名管道基本原理都差不多,但命名管道更强大,能实现两个毫不相干的进程间通信
具体在OS
中的体现:在文件的结构体 files_struct
中,存在一个特殊的成员 struct file *fd_array[]
,这是一个指针数组,其中存储的是指向不同文件的指针
//Linux内核源码(部分)
struct files_struct {
/*
* read mostly part
*/
atomic_t count;
struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
spinlock_t file_lock ____cacheline_aligned_in_smp;
int next_fd;
unsigned long close_on_exec_init[1];
unsigned long open_fds_init[1];
struct file __rcu * fd_array[NR_OPEN_DEFAULT]; //文件指针数组
};
此时父进程可以打开匿名管道文件,fork
子进程后,子进程继承原有的文件系统
关系,与父进程共享同一份文件资源,然后父子进程分别关闭 读端与写端
,实现匿名管道的单向关系,即可正常进行通信
具体流程:
-
父进程创建
匿名管道
,同时以读、写的方式打开匿名管道,此时会分配两个fd
-
fork
创建子进程,子进程拥有自己的进程系统信息,同时会继承原父进程中的文件系统信息,此时子进程和父进程可以看到同一份资源:匿名管道 pipe -
因为子进程继承了原有关系,因此此时父子进程对于 pipe 都有读写权限,需要确定数据流向,关闭不必要的 fd,比如父进程写、子进程读,或者父进程读、子进程写都可以
注意: -
fork
创建子进程后,子进程会继承原父进程中的文件系统信息,这也就是父子进程都会同时向屏幕打印信息的原理,因为此时它们操作的是同一个文件! -
父进程需要以读写的方式打开匿名管道 pipe,这样子进程在继承时,才不会发生权限丢失
-
创建出的匿名管道文件
pipe
虽然属于文件系统,但它是一个特殊文件,一个由OS
提供的纯纯的内存文件,不需要将数据冲刷至磁盘中,只需要承担进程间通信任务即可 -
管道是一种
半双工、单流向
的通信方式,因为pipe
只有一个缓冲区
,所以这种方式才被叫做管道通信
4、匿名管道的创建与使用
4.1、pipe 函数
匿名管道是通过pipe
函数创建的,其函数原型如下所示
#include <unistd.h>
int pipe(int pipefd[2]);
关于 pipefd
数组
关于返回值:创建匿名管道成功,返回 0,失败返回 -1,并设置错误码
实际在使用此函数时,需要先创建好大小为 2 的pipefd
数组,然后将其传入函数,成功创建匿名管道后,pipefd 数组中存储的就是 匿名管道的读端和写端 fd
4.2、实例代码演示
下面通过一个简单的程序,演示 匿名管道函数 pipe
的使用
使用匿名管道步骤
- 创建匿名管道
- 创建子进程
- 关闭不需要的 fd
- 开始通信
代码示例如下:
#include <iostream>
#include <cassert>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctime>
using namespace std;
int main()
{
// 1. 创建匿名管道
int pipefd[2];
int ret = pipe(pipefd);
assert(ret == 0);
(void)ret;
// 2. 创建子进程
pid_t id = fork();
if (id == 0)
{
// 子进程内
close(pipefd[1]); // 3. 子进程关闭写端
// 4. 开始通信
char buff[64];
while (true)
{
int n = read(pipefd[0], buff, sizeof(buff) - 1);
if (n <= 0)
{
cout << "子进程没有读取到信息,通信结束!" << endl;
break;
}
buff[n] = '\0';
if (n >= 5 && n < 64)
{
cout << "子进程成功读取到信息: " << buff << endl;
}
else
{
cout << "子进程读取数据量为:" << n << " 消息过短,通信结束!" << endl;
break;
}
}
close(pipefd[0]);
exit(0);
}
else
{
// 父进程内
close(pipefd[0]); // 3. 父进程关闭读端
char buff[64];
// 4. 开始通信
srand((size_t)time(NULL));
while (true)
{
int n = rand() % 26;
for (int i = 0; i < n; i++)
buff[i] = (rand() % 26) + 'A';
buff[n] = '\0';
cout << "=============================" << endl;
cout << "父进程想对子进程说: " << buff << endl;
write(pipefd[1], buff, strlen(buff));
if (n < 5)
break;
sleep(1);
}
close(pipefd[1]);
// 等待子进程退出
int status = 0;
waitpid(id, &status, 0);
if (status & 0x7F)
{
printf("子进程异常退出,core dump: %d 退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));
}
else
{
printf("子进程正常退出,退出码: %d\n", (status >> 8) & 0xFF);
}
return 0;
}
}
站在 文件描述符
的角度理解上述代码:
站在 内核(管道本质)
的角度理解上述代码:
所以,看待 管道
,就如同看待 文件
一样!管道
的使用和 文件
一致,迎合 Linux一切皆文件思想
4.3、管道读写规则
管道是一种 半双工、单向流
的通信方式,因此在成功创建匿名管道后,需要两个待通信的进程都能获得同一个 pipefd
数组
这就是匿名管道比较特殊的地方了:匿名管道只支持具有血缘关系的进程通信,如 父子进程、兄弟进程
等,因为只有 继承
了,才能共享到同一个 pipefd
数组
当通信双方都获得 pipefd
数组后,需要根据情况关闭不需要的 fd,
确保 单流向
的原则
注:命名管道可以支持不具有血缘关系进程间通信
关于匿名管道还有一个函数:
pipe2
(了解),比 pipe 函数多一个参数2flags
,可以使匿名管道在发生特殊情况时,作出不同的动作,当 flags 为 0 时,pipe2
等价于pipe
管道的读写规则:
PIPE_BUF
为管道大小,Linux 中为 4096
字节
- 当要写入的数据量不大于
PIPE_BUF
时,Linux 将保证写入的原子性 - 当要写入的数据量大于
PIPE_BUF
时,Linux 将不再保证写入的原子性
原子性:不存在中间状态,确保数据的安全性
5、管道的特点
管道 主要有以下几个特点:
- 单向通信,管道是半双工的一种特殊情况
- 管道就像单行道,只允许数据
单向流通
,即通知,如果想要实现两个进程间相互进行通信,需要创建两条管道,管道1:父进程写,子进程读;管道2:子进程写,父进程读
- 管道的本质是文件,因为
fd
的生命周期随进程而终止,所以管道的生命周期也是随着进程而结束的
- 当进程终止运行时,管道资源会被 OS 回收
- 匿名管道常用来进行具有 “血缘” 关系的进程,进行进程间通信(常用于父子进程间通信)
- pipe 打开管道,并不清楚管道的名字等信息,这种管道称为
匿名管道
,因此匿名管道
只能用于有血缘关系的进程IPC
,因为 需要通过fork
继承匿名管道信息
- 在管道中,
写入
与读取
的次数并不是严格匹配的,此时读写次数没有强相关关系,管道是面向字节流
读写的
- 面向字节流读写又称为
流式服务
:数据没有明确的分割,不分一定的报文段; - 与之相对应的是
数据报服务
:数据有明确的分割,拿数据按报文段拿,不论写端写入了多少数据,只要写端停止写入,读端都可以将数据读取
5.具有一定的协同能力,让读端
和写端
能够按照一定的步骤进行通信(自带同步机制)
可以简单总结为:
管道是半双工通信
管道生命随进程而终止
匿名管道只支持具有血缘关系的进程间通信,而命名管道无所谓
管道提供的是流式数据传输服务 管道自带
同步与互斥
机制
6、管道的四种特殊场景
管道还存在四种特殊场景:管道为空
、管道为满
、写端关闭
、读端关闭
,四种场景对应四种不同的特殊情况,都可以通过代码进行演示
注意: 当前大部分场景中,子进程为读端,父进程为写端
6.1、场景一
父进程不写,此时管道为空,子进程尝试读取
伪代码段
// 父进程不写(空),子进程读
//子进程(尝试读取)
int cnt = 1;
while (true)
{
char ch;
read(pipefd[0], &ch, 1);
cout << "已读取 " << cnt++ << " 字节的数据" << endl;
}
//父进程(不写)
while (true) {}
结果:因为管道为空,因此子进程无法读取,即 读端阻塞
只有当写端写入数据后,读端才能正常读取
6.2、场景二
父进程不断写入,直到管道写满,子进程不读取
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cassert>
using namespace std;
int main() {
int pipefd[2];
int ret = pipe(pipefd);
assert(ret == 0);
pid_t pid = fork();
if (pid == 0) {
// 子进程:不读,只是阻塞在死循环中
close(pipefd[1]); // 不需要写端
while (true) {
// 子进程什么都不做,不读取数据
}
} else {
// 父进程:不断写入数据,直到管道写满被阻塞
close(pipefd[0]); // 不需要读端
int cnt = 1;
char ch = 'x';
while (true) {
ssize_t w = write(pipefd[1], &ch, 1); // 每次写1字节
if (w == -1) {
perror("write");
break;
}
cout << "已写入 " << cnt++ << " 字节的数据" << endl;
usleep(1000); // 微小延迟可视化阻塞前行为
}
close(pipefd[1]);
waitpid(pid, nullptr, 0);
}
return 0;
}
进程开始疯狂写入
在写入65535字节后,进程开始阻塞
结果:
-
在一段时间后,管道被写满,写端无法写入数据,进入
阻塞
状态 -
只有当读端尝试将管道中的数据读走一部分后,写端才能继续写入
形象化理解
- 管道为空:垃圾桶为空时,你不会去倒垃圾(
读端阻塞
),因为没有垃圾,需要等有垃圾了(写入数据
)才去倒 - 管道为满:垃圾桶中的垃圾装满时,无法再继续扔垃圾(
写端阻塞
),需要等把垃圾倒了(读取数据
),才能继续扔垃圾
6.3、场景三
在通信的过程中,关闭写端,只保留读端
代码示例如下:
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <cassert>
using namespace std;
int main() {
int pipefd[2];
int ret = pipe(pipefd);
assert(ret == 0);
pid_t pid = fork();
if (pid == 0) {
// 子进程:只读
close(pipefd[1]); // 关闭写端
char buff[64];
while (true) {
int n = read(pipefd[0], buff, sizeof(buff) - 1); // 留一个字节放 '\0'
if (n > 0) {
buff[n] = '\0';
cout << "成功读取到信息: " << buff << endl;
} else if (n == 0) {
// 写端关闭,read返回0
cout << "写端已关闭,读取数据量为: " << n << " 字节" << endl;
} else {
perror("read error");
break;
}
sleep(1);
}
close(pipefd[0]); // 关闭读端
exit(0);
} else {
// 父进程:只写
close(pipefd[0]); // 关闭读端
char buff[] = "Hello pipe!";
write(pipefd[1], buff, strlen(buff)); // 写入数据
cout << "父进程已写入数据: " << buff << endl;
close(pipefd[1]); // 关闭写端
cout << "父进程已关闭写端" << endl;
waitpid(pid, nullptr, 0); // 等待子进程结束
}
return 0;
}
结果:关闭写端后,读端会将匿名管道中的数据读取完后,再读,会读到 0,表示已读到文件末尾
也就是说,写端的关闭不会影响读端的读取,反之同理。
6.4、场景四
在通信过程中,关闭读端,只保留写端
注:这里将角色变换一下,方便父进程捕捉到子进程的退出信号
切换:父进程 -> 读端,子进程 -> 写端
代码示例如下:
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <cassert>
using namespace std;
int main() {
int pipefd[2];
int ret = pipe(pipefd);
assert(ret == 0);
pid_t pid = fork();
if (pid == 0) {
// 子进程:不断写入
close(pipefd[0]); // 子进程不读
while (true) {
char buff[] = "Hello pipe!";
ssize_t w = write(pipefd[1], buff, strlen(buff));
if (w == -1) {
perror("子进程写入失败");
break;
} else {
cout << "子进程写入成功: " << buff << endl;
}
sleep(1);
}
close(pipefd[1]); // 清理写端
exit(0);
} else {
// 父进程:读取5次后关闭读端
close(pipefd[1]); // 父进程不写
char buff[64];
int cnt = 1;
while (true) {
int n = read(pipefd[0], buff, sizeof(buff) - 1);
if (n > 0) {
buff[n] = '\0';
cout << "父进程成功读取到信息: " << buff << endl;
} else if (n == 0) {
cout << "写端已关闭,读取数据量为: " << n << " 字节" << endl;
} else {
perror("父进程读取失败");
}
if (cnt++ == 5) {
cout << "父进程已读取5次,关闭读端" << endl;
break;
}
sleep(1);
}
close(pipefd[0]); // 关闭读端
// 等待子进程退出
int status = 0;
waitpid(pid, &status, 0);
if (status & 0x7F) {
printf("子进程异常退出,core dump: %d 退出信号:%d\n",
(status >> 7) & 1, status & 0x7F);
} else {
printf("子进程正常退出,退出码: %d\n", (status >> 8) & 0xFF);
}
}
return 0;
}
结果:OS
不允许任何浪费资源的行为存在,如果关闭了读端,那么证明写端写了也没有,即没有存在的意义,于是 OS
会发出 13
号信号,终止写端进程
通过指令查看信号表
kill -l
13号信号就是用来专门终止管道进程的
以上就是管道的四种特殊场景,不仅适用于匿名管道,同时也适用于命名管道
7、匿名管道的大小
既然管道能被写满,那么管道的大小究竟是多少?
一、通过 man 手册查询相关信息
man 7 pipe
接着输入/pipe capacity
即可搜索出管道的大小
文档解释:在Linux 2.6.11
之前,管道大小为一个系统页的大小(比如在 i386 平台中,管道大小为 4096 字节,即 4kb),从 Linux 2.6.11 开始,管道大小的容量统一为65536
字节,即 64kb
因为在 Linux 2.6.11 版本中,对管道进行更新,采取了新的解决方案
可以通过指令查看当前系统的内核版本号
uname -a
二、通过指令查看当前系统资源的限制情况
ulimit -a
当前系统中,限制单条管道大小为512 * 8 = 4096
字节
可以前往 /usr/src/kernels/
内核版本信息/include/linux/pipe_fs_i.h
这个文件中,查看当前系统的 管道条目数
,比如我当前的系统中,管道条目数为 16
,因此管道的大小上限为 4096 * 16 = 65536 字节
此时可以猜测:新的管道解决方案中,为所有的管道分配了一块定额空间,可用的 16 条管道中,可以根据自己的需要,获取大小,极大提高了效率
三、通过程序验证
这个前面就已经验证过了,不断往管道中写数据,直到管道被写满
每次写入 1 字节的数据,可以看到最终写了 65536 字节的数据
总之,从Linux 2.6.11
版本开始,管道大小上限为 64kb
8、匿名管道实操-进程控制
匿名管道作为 IPC
的其中一种解决方案,那么肯定有它的实战价值
场景:父进程创建了一批子进程,并通过多条匿名管道与它们链接,父进程选择某个子进程,并通过匿名管道与子进程通信,并下达指定的任务让其执行
8.1、逻辑设计
首先创建一批子进程及匿名管道 -> 子进程(读端)阻塞,等待写端写入数据 -> 选择相应的进程,并对其写入任务编号(数据)-> 子进程拿到数据后,执行相应任务
8.2、具体功能实现
下面来看看具体功能实现(部分细节可能未展示,详细实现可以看源码)
1. 创建一批进程及管道
创建一批进程及管道
- 首先需要先创建一个包含进程信息的类,最主要的就是子进程的写端
fd
,这样父进程才能通过此fd
进行数据写入 - 循环创建管道、子进程,进行相应的管道链接操作,然后子进程进入
任务等待
状态,父进程将创建好的子进程信息注册 - 假设子进程获取了任务代号,那么应该根据任务代号,去执行相应的任务,否则
阻塞等待
注意: 因为是创建子进程,所以存在关系重复继承的情况,此时应该统计当前子进程的写端 fd,在创建下一个进程时,关闭无关的 fd
具体体现为:每次都把 写端 fd
存储起来,在确定关系前 “清理” 干净
#define NAME_SIZE 64
// 封装一个包含各种必备信息的类
class ProcInfo
{
public:
ProcInfo(pid_t id = pid_t(), int fd = int())
: _childID(id), _wfd(fd), _num(++_count)
{
char buff[NAME_SIZE];
snprintf(buff, sizeof buff, "Process %d [%d:%d]", _num, _childID, _wfd);
_name = string(buff);
}
~ProcInfo()
{
_childID = _wfd = 0;
}
pid_t _childID; // pid
int _wfd; // 写端 fd
string _name; // 进程名
int _num; //编号
static int _count; // 计数
};
int ProcInfo::_count = 0; // 静态成员初始化
void CreateProcessAndPipe(vector<ProcInfo> &PP, int ppNum = 3)
{
vector<int> fds; //存储继承中不需要的写端 fd
for(int i = 0; i < ppNum; i++)
{
//首先创建管道
int pipefd[2];
int ret = pipe(pipefd);
assert(ret != -1);
(void)ret;
//然后创建子进程
int id = fork();
assert(id != -1);
(void)id;
if(id == 0)
{
//子进程内
//需要先关闭之前子进程遗留的写端fd
for(auto e : fds)
close(e);
close(pipefd[1]); //子进程关闭写端
waitCommand(pipefd[0]); //子进程等待命令
close(pipefd[0]);
exit(0);
}
//父进程内
close(pipefd[0]); //父进程关闭读端
PP.push_back(ProcInfo(id, pipefd[1]));
fds.push_back(pipefd[1]);
}
}
2、任务类创建及任务等待
子进程在创建完成后,需要进入一个 等待阶段 -> 读端阻塞,同时当子进程读取到相应的 指令 时,需要执行相应任务,这里将封装成了一个类,并通过对象调用函数
ctrlProc.cc 中
void waitCommand(int rfd)
{
while(true)
{
//读端尝试读取信息
int command = 0;
int n = read(rfd, &command, sizeof(command));
if(n != 0)
{
TaskPools().Execute(command);
}
else
{
cout << "当前子进程读取任务失败,已退出!" << endl;
break;
}
}
}
Task.hpp 中
#pragma once
#include <iostream>
#include <vector>
#include <unistd.h>
using namespace std;
void PrintLOG()
{
cout << "PID: " << getpid() << " 正在执行打印日志的任务…" << endl;
}
void InsertSQL()
{
cout << "PID: " << getpid() << " 正在执行数据库插入的任务…" << endl;
}
void NetRequst()
{
cout << "PID: " << getpid() << " 正在执行网络请求的任务…" << endl;
}
typedef void(*func_t)();
//任务池
class TaskPools
{
public:
TaskPools()
{
//装载任务
_vft.push_back(PrintLOG);
_vft.push_back(InsertSQL);
_vft.push_back(NetRequst);
}
~TaskPools()
{}
void Execute(int num)
{
//根据编号,执行任务
if(num < 0 || num > _vft.size())
cout << "没有这个任务" << endl;
else
_vft[num]();
}
private:
vector<func_t> _vft; //可用任务表
};
3、子进程控制
当所有子进程都完成注册后(统计至 PP 中),可以让用户输入下标选择程序、输入任务编号选择任务、或者输入程序退出
注意: 因为当前子进程编号从1
开始,所以在进行下标访问时,需要-1
避免越界
void showTask()
{
cout << "**************************" << endl;
cout << "* 0.日志打印 1.数据插入 *" << endl;
cout << "* 2.网络请求 3.退出程序 *" << endl;
cout << "**************************" << endl;
}
void CtrlProcess(vector<ProcInfo> &PP)
{
while (true)
{
// 展示当前可用的进程
int index = 0;
do
{
cout << "当前可选择进程:";
for (int i = 1; i <= PP.size(); i++)
cout << i << " ";
cout << endl;
cout << "请选择进程: ";
cin >> index;
} while (index < 1 || index > PP.size());
int taskNum = 0;
do
{
showTask(); // 展示可选任务
cout << "请选择任务: ";
cin >> taskNum;
} while (taskNum < 0 || taskNum > 3);
// 分配任务
if(taskNum == 3)
break;
cout << "已选择: " << PP[index - 1]._num << " 号进程 | " << PP[index - 1]._name << endl;
write(PP[index - 1]._wfd, &taskNum, sizeof(taskNum));
sleep(1); //执行完任务后,睡一会
}
}
8.4、注意事项
总体来说,在使用这个小程序时,以下关键点还是值得多注意的
- 注册子进程信息时,存储的是
写端 fd
,目的是为了通过此fd
向对应的子进程写数据,即使用不同的匿名管道 - 创建管道后,需要关闭父、子进程中不必要的
fd
- 需要特别注意父进程写端 fd 被多次继承的问题,避免因写端没有关干净,而导致读端持续阻塞
- 关闭读端对应的写端后,读端会读到
0
,可以借助此特性结束子进程的运行 - 在选择进程 / 任务 时,要做好越界检查
- 等待子进程退出时,需要先关闭写端,子进程才会退出,然后才能正常等待
8.5、完整源码
整个程序的完成源码如下所示:
ctrlProc.cc
#include <iostream>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp" //任务所需头文件
using namespace std;
#define NAME_SIZE 64
// 封装一个包含各种必备信息的类
class ProcInfo
{
public:
ProcInfo(pid_t id = pid_t(), int fd = int())
: _childID(id), _wfd(fd), _num(++_count)
{
char buff[NAME_SIZE];
snprintf(buff, sizeof buff, "Process %d [%d:%d]", _num, _childID, _wfd);
_name = string(buff);
}
~ProcInfo()
{
_childID = _wfd = 0;
}
pid_t _childID; // pid
int _wfd; // 写端 fd
string _name; // 进程名
int _num; // 编号
static int _count; // 计数
};
int ProcInfo::_count = 0; // 静态成员初始化
void waitCommand(int rfd)
{
while (true)
{
// 读端尝试读取信息
int command = 0;
int n = read(rfd, &command, sizeof(command));
if (n != 0)
{
TaskPools().Execute(command);
}
else
{
cout << "当前子进程读取任务失败,已退出!" << endl;
break;
}
}
}
void CreateProcessAndPipe(vector<ProcInfo> &PP, int ppNum = 3)
{
vector<int> fds; // 存储继承中不需要的写端 fd
for (int i = 0; i < ppNum; i++)
{
// 首先创建管道
int pipefd[2];
int ret = pipe(pipefd);
assert(ret != -1);
(void)ret;
// 然后创建子进程
int id = fork();
assert(id != -1);
(void)id;
if (id == 0)
{
// 子进程内
// 需要先关闭之前子进程遗留的写端fd
for (auto e : fds)
close(e);
close(pipefd[1]); // 子进程关闭写端
waitCommand(pipefd[0]); // 子进程等待命令
close(pipefd[0]);
exit(0);
}
// 父进程内
close(pipefd[0]); // 父进程关闭读端
PP.push_back(ProcInfo(id, pipefd[1]));
fds.push_back(pipefd[1]);
}
}
void showTask()
{
cout << "**************************" << endl;
cout << "* 0.日志打印 1.数据插入 *" << endl;
cout << "* 2.网络请求 3.退出程序 *" << endl;
cout << "**************************" << endl;
}
void CtrlProcess(vector<ProcInfo> &PP)
{
while (true)
{
// 展示当前可用的进程
int index = 0;
do
{
cout << "当前可选择进程:";
for (int i = 1; i <= PP.size(); i++)
cout << i << " ";
cout << endl;
cout << "请选择进程: ";
cin >> index;
} while (index < 1 || index > PP.size());
int taskNum = 0;
do
{
showTask(); // 展示可选任务
cout << "请选择任务: ";
cin >> taskNum;
} while (taskNum < 0 || taskNum > 3);
// 分配任务
if (taskNum == 3)
break;
cout << "已选择: " << PP[index - 1]._num << " 号进程 | " << PP[index - 1]._name << endl;
write(PP[index - 1]._wfd, &taskNum, sizeof(taskNum));
sleep(1); // 执行完任务后,睡一会
}
}
void WaitProcess(vector<ProcInfo> &PP)
{
// 遍历回收就好了
for (auto e : PP)
{
close(e._wfd); //关闭写端,读端读取到 0 自动结束阻塞
int status = 0;
waitpid(e._childID, &status, 0);
// 通过 status 判断子进程运行情况
if ((status & 0x7F))
{
printf("子进程异常退出,core dump: %d 退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));
}
else
{
printf("子进程 %s 正常退出,退出码:%d\n", e._name.c_str(), (status >> 8) & 0xFF);
}
}
cout << "所有子进程都已回收" << endl;
}
int main()
{
// 1、创建一批进程及匿名管道
vector<ProcInfo> PP;
CreateProcessAndPipe(PP);
// 2、进程控制
CtrlProcess(PP);
// 3、进程回收
WaitProcess(PP);
return 0;
}
Task.hpp
#pragma once
#include <iostream>
#include <vector>
#include <unistd.h>
using namespace std;
void PrintLOG()
{
cout << "PID: " << getpid() << " 正在执行打印日志的任务…" << endl;
}
void InsertSQL()
{
cout << "PID: " << getpid() << " 正在执行数据库插入的任务…" << endl;
}
void NetRequst()
{
cout << "PID: " << getpid() << " 正在执行网络请求的任务…" << endl;
}
typedef void(*func_t)();
//任务池
class TaskPools
{
public:
TaskPools()
{
_vft.push_back(PrintLOG);
_vft.push_back(InsertSQL);
_vft.push_back(NetRequst);
}
~TaskPools()
{}
void Execute(int num)
{
//根据编号,执行任务
if(num < 0 || num > _vft.size())
cout << "没有这个任务" << endl;
else
_vft[num]();
}
private:
vector<func_t> _vft; //可用任务表
};
小结
本篇关于匿名管道
和进程间通信
的讲解就暂告段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持斧正!!!