
文章目录
- [1. 引言:为什么需要进程间通信 (IPC)](#1. 引言:为什么需要进程间通信 (IPC))
-
- [1.1 进程的独立性与协同工作的矛盾](#1.1 进程的独立性与协同工作的矛盾)
- [1.2 IPC 的核心目的:数据传输、资源共享与进程控制](#1.2 IPC 的核心目的:数据传输、资源共享与进程控制)
- [2. 核心原理:管道的本质](#2. 核心原理:管道的本质)
-
- [2.1 管道不是磁盘文件:内核中的内存级缓冲区](#2.1 管道不是磁盘文件:内核中的内存级缓冲区)
- [2.2 实现机制:`pipe()` 系统调用与文件描述符](#2.2 实现机制:
pipe()系统调用与文件描述符) - [2.3 通信基础:通过 `fork()` 继承文件描述符表](#2.3 通信基础:通过
fork()继承文件描述符表) - [2.4 生命周期管理:随进程而生,由引用计数回收](#2.4 生命周期管理:随进程而生,由引用计数回收)
- 3.基础实战:匿名管道的读写
-
- [3.1 通信模型:单向通信(单工)与读写端约定](#3.1 通信模型:单向通信(单工)与读写端约定)
- [3.2 代码演示:`pipe`、`fork`、`read`、`write` 的基本流程](#3.2 代码演示:
pipe、fork、read、write的基本流程) - [3.3 特性分析:面向字节流(读写次数不对等)](#3.3 特性分析:面向字节流(读写次数不对等))
- [4. 深入理解:管道的四大特性与四种场景](#4. 深入理解:管道的四大特性与四种场景)
-
- [4.1 特性一:自带同步与互斥机制](#4.1 特性一:自带同步与互斥机制)
-
- [4.1.1 互斥:保证写入的原子性](#4.1.1 互斥:保证写入的原子性)
- [4.1.2 同步:管道空则读阻塞,管道满则写阻塞](#4.1.2 同步:管道空则读阻塞,管道满则写阻塞)
- [4.2 四种特殊情况(边界测试)](#4.2 四种特殊情况(边界测试))
-
- [4.2.1 读端关闭,写端继续写 → `SIGPIPE` 信号](#4.2.1 读端关闭,写端继续写 →
SIGPIPE信号) - [4.2.2 写端关闭,读端继续读 → `read` 返回 0 / EOF](#4.2.2 写端关闭,读端继续读 →
read返回 0 / EOF) - [4.2.3 管道满:写端阻塞](#4.2.3 管道满:写端阻塞)
- [4.2.4 管道空:读端阻塞](#4.2.4 管道空:读端阻塞)
- [4.2.1 读端关闭,写端继续写 → `SIGPIPE` 信号](#4.2.1 读端关闭,写端继续写 →
- [5. 进阶实战:基于管道的进程池 (Master-Slave)](#5. 进阶实战:基于管道的进程池 (Master-Slave))
-
- [5.1 场景设计:Master 进程管理多个 Slave 进程](#5.1 场景设计:Master 进程管理多个 Slave 进程)
- [5.2 架构思路:池化技术与一对一管道通信](#5.2 架构思路:池化技术与一对一管道通信)
- [5.3 代码实现与修正](#5.3 代码实现与修正)
-
- [5.3.1 `Channel` 类设计](#5.3.1
Channel类设计) - [5.3.2 资源管理:使用智能指针避免对象切片](#5.3.2 资源管理:使用智能指针避免对象切片)
- [5.3.3 任务分发:轮询策略与进程控制](#5.3.3 任务分发:轮询策略与进程控制)
- [5.3.1 `Channel` 类设计](#5.3.1
- [6. 总结](#6. 总结)
- [6.1 管道的五种特性](#6.1 管道的五种特性)
- [6.2 管道的四种情况](#6.2 管道的四种情况)

1. 引言:为什么需要进程间通信 (IPC)
1.1 进程的独立性与协同工作的矛盾
我们以前学习进程,独立性很强,一个一个学习。进程之间,要如何进行协同工作呢?进程通信就是研究进程如何协同工作的问题了。
父子之间数据可以共享,父进程的数据,子进程能看到。那为什么进程间还要通信呢?因为存在写时拷贝,父子无法互相传递数据。
1.2 IPC 的核心目的:数据传输、资源共享与进程控制
- 数据传输
- 资源共享
- 通知事件
- 进程控制
2. 核心原理:管道的本质
2.1 管道不是磁盘文件:内核中的内存级缓冲区
管道的本质就是让不同的进程看到同一份资源。
管道是纯内存级的文件,不需要打开磁盘文件,也就不需要路径+文件名确定文件。
2.2 实现机制:pipe() 系统调用与文件描述符

c
int pipe(int pipefd[2]);
pipefd[0]:读端(r)pipefd[1]:写端(w)- 成功返回
0,失败返回-1
内核会创建一个管道文件,并以读和写的形式打开。
2.3 通信基础:通过 fork() 继承文件描述符表
进程间通信怎么做?父进程往文件内核缓冲区中写入数据,子进程也能读取到。这个就是管道,基于文件仅从内核级进程间通信。
拷贝
struct file的时候对我们的子进程,父子都要单独拷,文件不是单独拷的。
进程的本质是让其看到同一份资源。
细节:
fork的时候,file要进行拷贝- 管道是单向通信的,单工通信
- 全双工通信:两人互骂的时候
- 半双工通信:人类正常沟通
- 单工通信:老师上课的时候
- 管道是一个纯内存级的文件,不需要打开磁盘文件,也就不需要路径+文件名
2.4 生命周期管理:随进程而生,由引用计数回收
父子进程都退出了,管道文件有没有被释放?到最后会被系统自动回收掉。换句话说,文件的生命周期随进程。
操作系统是怎样知道他要被回收的?inode 属性里面有一个 int ref_cnt 属性来进行标记,这个文件有没有人用。管道的生命周期是随进程的,该文件你不关,会自己回收。
3.基础实战:匿名管道的读写
3.1 通信模型:单向通信(单工)与读写端约定
- 只读打开
- 只写打开
方便让子进程也能看到读和写。
为什么不关闭对应的读写段?不关闭其实也可以的,但是不建议,另一个你不用的,容易误操作。

管道是单向通信的,谁读谁写?子进程来读,父进程来写。
3.2 代码演示:pipe、fork、read、write 的基本流程
步骤:
- 创建管道文件
fork创建子进程- 关闭读写段,子进程写,父进程读
- 子 → 父 通信
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
int main()
{
// 1. 创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
perror("pipe");
return 1;
}
printf("pipefd[0]: %d, pipefd[1]: %d\n", pipefd[0], pipefd[1]); // 3, 4
// 2. 创建子进程: 子进程写
pid_t id = fork();
if (id == 0)
{
// child
close(pipefd[0]);
char *msg = "Hello, World!";
int cnt = 10;
char outbuffer[1024];
while (cnt)
{
snprintf(outbuffer, sizeof(outbuffer), "f->c# %s %d %d\n", msg, cnt--, getpid());
write(pipefd[1], outbuffer, strlen(outbuffer));
sleep(1);
}
exit(0);
}
// parent: 读
close(pipefd[1]);
char buf[1024] = {0};
while (1)
{
ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1);
if (n > 0)
{
buf[n] = 0;
printf("c->f# %s", buf);
}
else if (n == 0)
{
printf("child exit\n");
break;
}
else
{
perror("read");
break;
}
}
pid_t rid = waitpid(id, NULL, 0);
(void)rid;
return 0;
}
3.3 特性分析:面向字节流(读写次数不对等)
管道是面向字节流的。在管道的角度,它认为它写的全是字符。

写几个字符和你要读多少个字符没有正相关。文件也是字符流式的,知道是说某某流的,读写次数都不是正相关的。
4. 深入理解:管道的四大特性与四种场景
4.1 特性一:自带同步与互斥机制
管道通信,对于多进程而言,是自带互斥 与同步 机制的。
你的父进程在阻塞,问题在于你的父进程为什么在阻塞?
4.1.1 互斥:保证写入的原子性
管道通信,前提是让不同的进程看到同一份资源------文件 → 文件内核缓冲区 → 内存块。属于父子进程的共享资源。数据并发访问导致的问题。所以要对共享资源进行保护------互斥与同步。
- 同步:访问数据具有一定的顺序性
- 互斥:任何时候只允许一个人访问资源,这就是父进程卡住的原因
如何证明 :单次写入小于 4096 字节,没有人能来中断你。
4.1.2 同步:管道空则读阻塞,管道满则写阻塞
- 如果子进程写的慢,父进程就要阻塞等,等管道有数据,父进程才能读
- 如果子进程写的快,父进程不读,管道一旦被写满,子进程就必须被阻塞!不运行了,写满了,就应该要让父进程读了
管道为空,读就要阻塞,给写机会。管道为满,写就要阻塞,给读机会。
4.2 四种特殊情况(边界测试)
4.2.1 读端关闭,写端继续写 → SIGPIPE 信号
写端一直在写,读端不读关闭 fd。读端都关了,那你写是没有任何意义的。OS 不做浪费资源的事,OS 会直接杀掉写的进程。
如何证明:通过父进程回收其僵尸进程。
为什么要让子进程写?因为如果写出异常了父进程能回收异常。
退出码 13,操作系统是被异常杀掉的。
SIGPIPE :管道破裂,没有读端,写端继续写,OS 终止进程。

c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
int main()
{
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
perror("pipe");
return 1;
}
printf("pipefd[0]: %d, pipefd[1]: %d\n", pipefd[0], pipefd[1]);
pid_t id = fork();
if (id == 0)
{
// child: 写端
close(pipefd[0]);
char ch = 'A';
int size = 0;
while (1)
{
write(pipefd[1], &ch, 1);
size++;
printf("child write %c to pipe, size=%d\n", ch, size);
}
exit(0);
}
// parent: 读端(不读,直接关闭)
close(pipefd[1]); // 关闭写端
close(pipefd[0]); // 关闭读端 → 读端关闭
pid_t rid = waitpid(id, NULL, 0);
(void)rid;
return 0;
}
4.2.2 写端关闭,读端继续读 → read 返回 0 / EOF
读端在读,写端关闭。读端读完管道中剩余数据,再读就会读到 "",read 返回值为 0,表明读管道,读到文件结尾。
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
int main()
{
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
perror("pipe");
return 1;
}
printf("pipefd[0]: %d, pipefd[1]: %d\n", pipefd[0], pipefd[1]);
pid_t id = fork();
if (id == 0)
{
// child: 写端
close(pipefd[0]);
char *msg = "Hello, World!";
int cnt = 5;
char outbuffer[1024];
while (cnt--)
{
snprintf(outbuffer, sizeof(outbuffer), "f->c# %s %d %d\n", msg, cnt, getpid());
write(pipefd[1], outbuffer, strlen(outbuffer));
sleep(1);
}
printf("write endpoint quit\n");
close(pipefd[1]); // 写端关闭
exit(0);
}
// parent: 读端
close(pipefd[1]);
char buf[1024] = {0};
while (1)
{
ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1);
if (n > 0)
{
buf[n] = 0;
printf("c->f# %s\n", buf);
}
else if (n == 0)
{
printf("child exit, read return 0\n");
break;
}
else
{
perror("read");
break;
}
}
pid_t rid = waitpid(id, NULL, 0);
(void)rid;
return 0;
}
4.2.3 管道满:写端阻塞
子进程写的快,父进程不读,管道一旦被写满,子进程就必须被阻塞。
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
int main()
{
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
perror("pipe");
return 1;
}
printf("pipefd[0]: %d, pipefd[1]: %d\n", pipefd[0], pipefd[1]);
pid_t id = fork();
if (id == 0)
{
// child: 写端,疯狂写入
close(pipefd[0]);
char ch = 'A';
int size = 0;
while (1)
{
write(pipefd[1], &ch, 1);
size++;
printf("child write %c to pipe, size=%d\n", ch, size);
}
exit(0);
}
// parent: 读端(不读)
close(pipefd[1]);
char buf[1024] = {0};
while (1)
{
sleep(100); // 故意不读,让管道写满
ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1);
if (n > 0)
{
buf[n] = 0;
printf("c->f# %s\n", buf);
}
else if (n == 0)
{
printf("child exit\n");
break;
}
else
{
perror("read");
break;
}
}
pid_t rid = waitpid(id, NULL, 0);
(void)rid;
return 0;
}
4.2.4 管道空:读端阻塞
子进程写的慢,父进程就要阻塞等,等管道有数据,父进程才能读。
5. 进阶实战:基于管道的进程池 (Master-Slave)
5.1 场景设计:Master 进程管理多个 Slave 进程
完成一个进程控制一批进程的过程------Master-Slave 模式的进程池:预先将进程创建出来,用的时候直接用就好了。
所有的池化技术都是为了提高效率。
5.2 架构思路:池化技术与一对一管道通信
- 约定:统一 4 字节读写,任务码
- 如何选择一个进程呢?均匀分配,负载均衡
- 轮询
- 随机
- 权重
管道是自带同步的。
5.3 代码实现与修正
5.3.1 Channel 类设计
cpp
```cpp
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include <sys/wait.h>
#include <functional>
#include <ctime>
#include <cstdlib>
//================ 子进程要完成的任务 ===========================
void SyncDisk()
{
std::cout << getpid() << ":刷新数据到磁盘任务" << std::endl;
sleep(1);
}
void Download()
{
std::cout << getpid() << ":下载数据到系统中" << std::endl;
sleep(1);
}
void PrintLog()
{
std::cout << getpid() << ":打印日志到本地" << std::endl;
sleep(1);
}
void UpdateStatus()
{
std::cout << getpid() << ":更新一次用户的状态" << std::endl;
sleep(1);
}
typedef void (*task_t)();
task_t task[4] = {SyncDisk, Download, PrintLog, UpdateStatus};
enum
{
OK = 0,
PIPE_ERROR,
FORK_ERROR
};
using cb_t = std::function<void(int)>;
//==================== 子进程的入口函数 ============================
void DoTask(int fd)
{
while (true)
{
int task_code = 0;
ssize_t n = read(fd, &task_code, sizeof(task_code));
if (n == sizeof(task_code))
{
if (task_code >= 0 && task_code < 4)
{
task[task_code]();
sleep(1); // 任务执行完稍作休息
}
}
else if (n == 0)
{
std::cout << getpid() << ":task quit ..." << std::endl;
break;
}
else
{
perror("read");
break;
}
}
}
const int gprocessnum = 5;
//==================== 进程池 ============================
class ProcessPool
{
public:
class Channel
{
public:
Channel(int wfd, pid_t pid)
: _wfd(wfd), _sub_pid(pid)
{
_sub_name = "sub-channel-" + std::to_string(_sub_pid);
}
void PrintInfo()
{
printf("wfd=%d, who=%d, channel name=%s\n",
_wfd, _sub_pid, _sub_name.c_str());
}
std::string Name()
{
return _sub_name;
}
void Write(int index)
{
ssize_t n = write(_wfd, &index, sizeof(index));
(void)n;
}
void ClosePipe()
{
std::cout << "关闭wfd: " << _wfd << std::endl;
close(_wfd);
}
void Wait()
{
pid_t rid = waitpid(_sub_pid, nullptr, 0);
(void)rid;
std::cout << "回收子进程: " << _sub_pid << std::endl;
}
~Channel() {}
private:
int _wfd;
pid_t _sub_pid;
std::string _sub_name;
};
public:
ProcessPool()
{
srand((unsigned int)time(nullptr) ^ getpid());
}
~ProcessPool() {}
void Init(cb_t cb)
{
CreateProcessChannel(cb);
}
void Run()
{
int cnt = 10;
while (cnt--)
{
std::cout << "------------------------------------------------" << std::endl;
int itask = SelectTask();
std::cout << "itask: " << itask << std::endl;
int index = SelectChannel();
if (index < 0)
{
std::cerr << "没有可用的子进程" << std::endl;
break;
}
std::cout << "index: " << index << std::endl;
printf("发送 %d to %s\n", itask, channels[index].Name().c_str());
SendTaskToSlave(itask, index);
sleep(1);
}
}
void Quit()
{
// 先关闭所有管道写端 → 让所有子进程同时收到 EOF
for (auto &ch : channels)
{
ch.ClosePipe();
}
// 再统一回收所有子进程
for (auto &ch : channels)
{
ch.Wait();
}
}
void Debug()
{
for (auto &c : channels)
{
c.PrintInfo();
}
}
private:
int SelectTask()
{
return rand() % 4;
}
int SelectChannel()
{
if (channels.empty())
return -1;
static int index = 0;
int selected = index;
index = (index + 1) % channels.size();
return selected;
}
void SendTaskToSlave(int itask, int index)
{
if (itask < 0 || itask >= 4)
return;
if (index < 0 || index >= (int)channels.size())
return;
channels[index].Write(itask);
}
void CreateProcessChannel(cb_t cb)
{
for (int i = 0; i < gprocessnum; i++)
{
int pipefd[2] = {0};
if (pipe(pipefd) < 0)
{
std::cerr << "pipe create error" << std::endl;
exit(PIPE_ERROR);
}
pid_t id = fork();
if (id < 0)
{
std::cerr << "fork error" << std::endl;
exit(FORK_ERROR);
}
else if (id == 0)
{
// 子进程:关闭所有历史管道写端
for (auto &ch : channels)
{
ch.ClosePipe();
}
close(pipefd[1]); // 关闭当前管道的写端
cb(pipefd[0]); // 执行回调函数(DoTask)
exit(OK);
}
else
{
// 父进程:关闭当前管道的读端
close(pipefd[0]);
channels.emplace_back(pipefd[1], id);
std::cout << "创建子进程:" << id << " 成功..." << std::endl;
}
}
}
private:
std::vector<Channel> channels;
};
//==================== main ============================
int main()
{
ProcessPool pp;
pp.Init(DoTask);
// pp.Debug();
pp.Run();
pp.Quit();
return 0;
}


5.3.2 资源管理:使用智能指针避免对象切片
进程池中的 Channel 对象如果被复制,可能会导致 _wfd 被多个对象管理,关闭时出现重复关闭的问题。建议使用智能指针或禁止拷贝。
5.3.3 任务分发:轮询策略与进程控制
问题讨论:
- 如果父进程要求子进程退出,如果管道里面还有任务没有处理完,怎么办?
- 子进程不会强制退出,子进程会将管道里面的任务消耗完了才会退出。
- 为什么全部关闭,在
wait回收子进程呢?- 倒着关!最后一个子进程,
wfd只有一个,我们想要 1:1 的读写端。
- 倒着关!最后一个子进程,
- 函数回调调用完还会回来的。
DoTask是回调函数,执行完会返回到子进程的主循环。
6. 总结
6.1 管道的五种特性
- 管道是只能单向通信,也就是单工通信
- 两个不相干的进程不能用匿名管道来通信。匿名管道只能用来进行具有血缘关系的父子通信(通过继承资源来实现的)
- 管道是面向字节流的
- 管道的生命周期随进程
- 管道通信是自带同步与互斥机制的
6.2 管道的四种情况
- 子进程写的慢,父进程就要阻塞等待
- 子进程写的快,管道写满,写端阻塞
- 写端关闭,读端读完剩余数据,
read返回 0 - 读端关闭,写端收到
SIGPIPE信号,进程被终止
下一步:
管道通信是进程间通信的基础,但它只能实现单向通信。接下来我们将学习更强大的 IPC 机制------命名管道 和共享内存。
欢迎大家批评指正!!!