目录
进程间通信分类:
- 管道
- 匿名管道pipe
- 命名管道
- 进程值
- System V IPC
- 消息队列
- 共享内存
- 信息量
在 Linux/Unix 系统中,进程作为独立的执行单元,彼此间的内存空间相互隔离,但实际开发中(比如进程池、多任务协作场景),进程间的信息交互是绕不开的核心需求 ------ 这就是进程间通信(IPC,Inter-Process Communication)的价值所在。
从最基础的管道到高性能的共享内存,Linux 提供了多套 IPC 机制,而其中管道(Pipe)和 System V IPC(消息队列、共享内存、信号量)是最经典、最常用的两类。本文将从 "进程间通信的核心前提(让不同进程看到同一份资源)" 出发,结合大量可运行的 C/C++ 代码示例和实操演示,拆解匿名管道、命名管道、共享内存等核心 IPC 方式的底层原理、使用方法、特性限制,帮你彻底搞懂 Linux 进程间通信的底层逻辑。
通信的前提:让不同的进程看到同一份的资源
- 同一份资源:某种形式的内存空间
- 提供资源的人:只是操作系统
管道
什么是管道
管道是Unix中最古老的进程间通信的形式
我们把从一个进程链接到另一个进程的一个数据流称为一个"管道"
下图结果是 2,可能的情况包括:
- 你通过两个终端登录了这个节点
- 你和一个其他用户同时登录了这个节点
- 系统服务创建了一个登录会话

匿名管道
管道不需要路径,不需要名字
- 父进程创建管道
- 父进程fork出子进程
- 关闭对应的读端写端(管道只能进行单向通信)
匿名管道 = 临时的单向数据通道,适用于父子进程简单通信。
Shell 的 | 就是匿名管道,比如 cat file.txt | grep "hello"。
缺点:不能跨非亲缘进程,不能双向通信(需用 FIFO 或 socket)。
验证接口
c
int pipe(int pipefd[2]); // 创建管道,返回两个文件描述符
Shell 中的匿名管道(|)
Shell 的 | 符号就是匿名管道的典型应用:
bash
ls -l | grep ".txt" # ls 的输出通过管道传给 grep
ls -l 的 stdout(写端)连接到管道的写端。
grep 的 stdin(读端)连接到管道的读端。
匿名管道的限制
单向通信:如果需要双向通信,必须创建两个管道。
仅限亲缘进程:无法用于无关进程(需用命名管道 FIFO 或 socket)。
无持久性:进程退出后,管道自动销毁。
缓冲区有限:默认 64KB,写满会阻塞。
演示管道(pipe)的创建
cpp
#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
int fds[2] = {0};
int n = pipe(fds); //fds:输出型参数
if(n == 0)
{
std::cout << "fds[0] = " << fds[0] << std::endl;
std::cout << "fds[1] = " << fds[1] << std::endl;
}
return 0;
}
运行结果:

(1)理解管道的基础机制
管道是 Linux/Unix 中 进程间通信(IPC) 的一种方式。
它创建了一个 单向数据通道,一端写入数据,另一端读取数据。
(2)观察文件描述符的分配
系统默认会分配当前可用的最小文件描述符(通常是 3 和 4,因为 0、1、2 已被标准输入、输出、错误占用)。
cpp
#include <iostream>
#include <cstdlib>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
//管道通信的示例程序
//Fater process -> read
//Child process -> write
int main() {
//1. 创建管道
int fds[2] = {0};
int n = pipe(fds); //fds:输出型参数
if(n != 0)
{
std::cerr << "pipe error" << std::endl;
return -1;
}
//2. 创建子进程
pid_t pid = fork();
if(pid < 0)
{
std::cerr << "fork error" << std::endl;
return 2;
}
else if(pid == 0)
{
//子进程
//3. 关闭不需要的fd,关闭read
::close(fds[0]); //:: 表示系统调用的函数,区分库函数
int cnt = 0;
while(true)
{
std::string message = "hello, bit, hello ";
message += std::to_string(getpid());
message += ", ";
message += std::to_string(cnt);
//fds[1]
::write(fds[1], message.c_str(), message.size());
cnt++;
sleep(1);
}
exit(0);
}
else
{
//父进程
//3. 关闭不需要的fd,关闭write
::close(fds[1]);
char buffer[1024];
while(true)
{
ssize_t n = ::read(fds[0], buffer, 1023);
if(n > 0)
{
buffer[n] = 0;
std::cout << "father read from pipe: " << buffer << std::endl;
}
}
pid_t rid = waitpid(pid, NULL, 0);
std::cout << "father wait child success: " << rid << std::endl;
}
return 0;
}

IPC本质:先让不同的进程,看到同一份资源
可能导致数据不一致问题->保护共享资源 -- 临界资源
现象:
- 管道为空 && 管道正常,read会阻塞
- 管道为满 && 管道正常,write会阻塞
- 管道写端关闭 && 读端继续,读端读到0,表示读到文件结尾
- 管道写端正常 && 读端关闭,OS会直接杀掉写入进程

匿名管道特性:
- 面向字节流
- 用来进行具有血缘关系的进程,进行IPC,常用于父子
- 文件的生命周期,随进程,管道也是
- 单向数据通信
- 管道自带同步互斥等保护机制
⭐完整例子请跳转至_->例子
cpp
// 1. 头文件说明:Linux 多进程+管道编程核心头文件
#include <iostream> // 标准输入输出(打印提示、调试信息)
#include <string> // 字符串处理(Channel 名称拼接)
#include <vector> // 动态数组(管理多个 Channel 对象)
#include <cstdlib> // 通用工具:stoi(字符串转整数)、exit(进程退出)
#include <unistd.h> // Linux 系统调用:pipe/fork/close/dup2/sleep
#include <sys/types.h> // 系统类型定义:pid_t(进程ID类型)
#include <functional> // 函数对象封装(std::function)
using namespace std;
// 2. 类型别名:定义"无参数、无返回值"的工作函数类型(子进程业务逻辑的统一接口)
// work_t 可以接收任意符合"void()"签名的函数/lambda/函数对象
using work_t = std::function<void()>;
// 3. 枚举错误码:规范程序退出状态,替代魔法数字(提高可读性/可维护性)
enum
{
OK = 0, // 执行成功
UsageError = 1, // 命令行参数错误
PipeError, // 管道创建失败
ForkError // 进程创建失败
};
// 4. Channel 类:封装"管道写端 + 对应子进程PID",管理单个通信信道
// 作用:绑定"主进程写端fd"和"子进程PID",方便主进程管理每个子进程的通信入口
class Channel
{
public:
// 构造函数:初始化管道写端和子进程PID,生成唯一信道名称
// 参数:_wfd - 管道写端文件描述符;who - 对应子进程的PID
Channel(int wfd, pid_t who):_wfd(wfd),_who(who)
{
// 拼接信道名称:Channel-写端fd-子进程PID(方便调试时区分不同信道)
_name = "Channel-" + std::to_string(wfd) + "-" + std::to_string(who);
}
// 成员函数:获取信道名称(用于调试打印)
std::string Name(){
return _name;
}
// 析构函数:空实现(当前无需自定义释放资源,fd由主进程手动管理)
~Channel()
{}
private:
int _wfd; // 核心:管道的写端文件描述符(主进程通过此fd向子进程写数据)
std::string _name; // 信道名称(仅标识/调试用,无实际通信作用)
pid_t _who; // 对应子进程的PID(主进程可通过PID回收/管理子进程)
};
// 5. Usage 函数:命令行参数错误时,打印用法提示
// 参数:proc - 程序名(argv[0])
void Usage(string proc){
cout << "Usage: " << proc << " proc_num" << endl; // 提示格式:./程序名 子进程数量
}
// 6. Worker 函数:子进程的业务逻辑(示例)
// 后续可替换为任意符合 work_t 签名的函数(比如处理任务、计算等)
void Worker()
{
// 子进程被重定向后,标准输入(0)就是管道读端,可通过read(0, ...)读取主进程数据
// 此处sleep(10)模拟子进程"干活"的耗时操作
sleep(10);
}
// 7. InitProcessPool 函数:初始化进程池(核心函数)
// 参数:
// processnum - 要创建的子进程数量
// channels - 输出型参数(主进程传入空vector,函数内填充Channel对象)
// work - 回调函数(子进程要执行的业务逻辑)
// 返回值:错误码(OK/UsageError/PipeError/ForkError)
int InitProcessPool(const int &processnum, std::vector<Channel>& channels, work_t work){
// 循环创建 processnum 个子进程 + 对应匿名管道
for(int i = 0; i < processnum; ++i){
// 7.1 定义管道文件描述符数组:pipefd[0] = 读端,pipefd[1] = 写端(Linux管道固定规则)
int pipefd[2] = {0};
// 7.2 创建匿名管道:成功返回0,失败返回-1
int n = pipe(pipefd);
if(n < 0){
perror("pipe error"); // 打印管道创建失败原因(Linux 错误提示)
return PipeError; // 返回管道错误码
}
// 7.3 创建子进程:fork() 复制当前进程,返回值分三种情况
pid_t id = fork();
if(id < 0){ // 情况1:fork失败
perror("fork error");
return ForkError; // 返回进程创建错误码
}
else if(id == 0){ // 情况2:子进程分支(fork返回0给子进程)
// 子进程只需要读管道,关闭写端(避免资源泄漏,防止误写)
::close(pipefd[1]);
// 核心:将子进程的"标准输入(文件描述符0)"重定向到管道读端
// 作用:后续子进程可通过 read(0, ...) 读取主进程数据(无需关心管道fd,更通用)
dup2(pipefd[0], 0);
// 执行子进程的业务逻辑(回调函数,解耦框架与业务)
work();
// 子进程执行完业务后退出,避免继续执行父进程的循环代码
::exit(0);
}
else{ // 情况3:父进程分支(fork返回子进程PID给父进程)
// 父进程只需要写管道,关闭读端(避免资源泄漏)
::close(pipefd[0]);
// 7.4 封装Channel对象,存入vector(主进程管理所有通信信道)
// emplace_back:直接在vector中构造对象(比push_back更高效,避免拷贝)
channels.emplace_back(pipefd[1], id);
// 等价写法(可读性更高,新手友好):
// Channel ch(pipefd[1], id);
// channels.push_back(ch);
}
}
// 所有子进程创建成功,返回成功码
return OK;
}
// 8. DebugPrint 函数:调试用,打印所有通信信道的名称
// 参数:channels - 主进程管理的所有Channel对象
void DebugPrint(std::vector<Channel>& channels){
for(auto &ch : channels){ // 范围for循环遍历所有Channel
cout << ch.Name() << endl; // 打印每个信道的名称(如Channel-5-12345)
}
}
// ------------------- 主函数(Master 进程核心逻辑) ------------------- //
int main(int argc, char* argv[]){
// 步骤1:检查命令行参数(必须传入"子进程数量",否则提示用法)
// argc - 参数个数(./程序名 10 → argc=2;仅./程序名 → argc=1)
if (argc != 2){
Usage(argv[0]); // 打印用法提示
return UsageError; // 返回参数错误码
}
// 步骤2:将命令行传入的"子进程数量"字符串转为整数
int num = std::stoi(argv[1]);
// 步骤3:创建空vector,用于存储所有子进程的通信信道
std::vector<Channel> channels;
// 步骤4:初始化进程池(创建指定数量的子进程+管道)
// 传入:子进程数量、空channels(输出型)、子进程业务函数Worker
InitProcessPool(num, channels, Worker);
// 步骤5:调试打印所有信道名称(验证进程池创建成功)
DebugPrint(channels);
// 可选:主进程后续逻辑(比如通过channels中的写端fd给子进程发任务)
// sleep(100); // 模拟主进程"下发任务"的耗时操作
return 0; // 主进程正常退出
}
- 进程池初始化成功:按参数创建 10 个子进程,无 fork/pipe 失败;
- 通信信道建立完成:10 个 Channel 封装了父子进程的管道写端 + 子进程 PID,通信链路就绪;
- 子进程状态正常:子进程处于睡眠等待任务状态,业务逻辑可扩展;

命名管道
管道通信的场景 -> 进程池
这段代码是 Linux 环境下 "主从进程(Master-Worker)" 通信框架的基础实现:
- Master 进程(主进程):根据命令行传入的参数,创建指定数量的 Worker 子进程;
- 匿名管道:为每个子进程创建一个匿名管道,实现 Master → Worker 的单向通信(Master 持管道写端,Worker 持管道读端);
- Channel 类:封装 "管道写端文件描述符" 和 "对应子进程 PID",方便 Master 统一管理所有通信信道;
- 核心依赖 Linux 系统调用(pipe/fork/close)实现进程创建和管道通信,是多进程编程的典型入门案例。
- 例子
bash
mkfifo fifo


我们把这种管道就叫做命名管道
- 命名管道的原理,理解(匿名打通)
为什么叫做命名管道? 因为是真正存在的文件!
路径+文件名 ---> 具有唯一性
代码演示:
形成两个可执行程序
公共资源:一般要让指定的一个进程先创建
创建 && 使用
获取 && 使用

创建管道本质也是新建文件
mode创建文件难度的权限
cpp
#pragma once
#include <iostream>
#include "Comm.hpp"
class Init
{
public:
Init()
{
umask(0);
int n = ::mkfifo(gpipeFile.c_str(), gmode);
if (n < 0)
{
std::cerr << "mkfifo error" << std::endl;
return;
}
std::cout << "mkfifo success" << std::endl;
}
~Init()
{
int n = ::unlink(gpipeFile.c_str());
//unlink;删除一个文件的名字和inode之间的联系
if (n < 0)
{
std::cerr << "unlink error" << std::endl;
return;
}
std::cout << "unlink success" << std::endl;
}
};
Init inti;
class Server
{
public:
Server():_fd(gdefultfd)
{
}
bool OpenPiprForRead()
{
_fd = OpenPipe(gForRead);
if(_fd < 0) return false;
return true;
}
//std::string *: 输出型参数
//const std::string &: 输入型参数
//std::string *&: 输入输出型参数
int recvPipe(std::string *out)
{
char buffer[gsize] = {0};
ssize_t n = ::read(_fd, buffer, sizeof(buffer)-1);
if(n > 0)
{
buffer[n] = 0;
*out = buffer;
}
return n;
}
void ClosePipe()
{
}
~Server()
{
ClosePipeHelper(_fd);
}
private:
int _fd;
};
详细代码跳转至👉命名管道 FIFO
共享内存
共享内存存在任何时刻,可以在OS内部同事存在很多个
- IPC_CREAT: 在单独使用时,如果shm不存在,创建。如果存在,获取并返回
- IPC_EXCL:单独使用无意义
IPC_CREAT | IPC_EXCL: 如果shm不存在,就创建它。如果存在,出错返回 ------ 只要成功,一定是新的共享内存!
Linux是如何在应用层面,保证不同进程看到同一份资源的?
路径+项目ID
Server.cc示例代码
cpp
#include <iostream>
#include "Comm.hpp"
int main()
{
key_t k = ::ftok(gpath.c_str(), gprojId);
//1. 创建key
if(k < 0){
std::cerr << "ftok failed" << std::endl;
return 1;
}
std::cout << "key: " << ToHex(k) << std::endl;
//2. 创建共享内存 && 获取
int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL);
if(shmid < 0){
std::cerr << "shmget failed" << std::endl;
return 2;
}
std::cout << "shmid: " << shmid << std::endl;
return 0;
}
在这里第一次运行成功了,第二次再运行 ./server 会失败
因为共享内存的生命周期是随内核的,所以有一下解决方案:
- 用户让OS释放
- OS重启

共享内存的管理指令:
ipcs -m : 查看
ipcrm -m [shmid] 删除

- shmid vs key
- shmid:只给用户用的一个表示shm的标识符
- key: 只作为内核中,区分shm唯一性的标识符,不作为用户管理shm的id值
- shmat : 将内核中的共享内存段挂接到当前进程的虚拟地址空间

- shmdt 去除关联

建立共享资源
cpp
#include <iostream>
#include <unistd.h>
#include "Comm.hpp"
int main()
{
key_t k = ::ftok(gpath.c_str(), gprojId);
//1. 创建key
if(k < 0){
std::cerr << "ftok failed" << std::endl;
return 1;
}
std::cout << "key: " << ToHex(k) << std::endl;
//2. 创建共享内存 && 获取
//注意:为什么共享内存也有权限!
int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL | gmode);
if(shmid < 0){
std::cerr << "shmget failed" << std::endl;
return 2;
}
std::cout << "shmid: " << shmid << std::endl;
sleep(5);
//3. 共享内存挂机到自己的地址空间
void *ret = shmat(shmid, nullptr, 0);
std::cout << "attach done: " << (long long)ret << std::endl;
sleep(5);
::shmdt(ret);
std::cout << "detach done" << std::endl;
sleep(5);
//在这里进行通信
//n. 删除共享内存
shmctl(shmid, IPC_RMID, nullptr);
std::cout << "delete shm done" << std::endl;
sleep(5);
return 0;
}
/**
* 监控脚本:
* 1.查IP资源
* while :; do ipcs -m; sleep 1; done
*/
共享内存的特点:
- 通信速度最快
- 让两个进程再各自的用户空间共享内存块,但是,没有加任何保护机制!
- 共享内存,保护机制,需要由用户自己完成保护 -- 信号量 -- 命名管道
共享内存被保护起来叫做临界资源
访问公共资源的代码叫做临界区
完整的代码👉共享内存示例/学习用 demo
system V消息队列
内核提供的一种进程通信的方式
消息队列的本质:一个进程向另一个进程发送有类型的数据块的方法!
消息队列的管理指令:
- ipcs -q : 查看
- ipcrm -q msqid : 删除
- smop: 信号量的操作
system V信号量

保护的常见方式:互斥和同步
->(任何时候,只允许一个执行流访问资源,叫做互斥)
->(多个执行流,访问临界资源的时候,具有一定的顺序性,叫做同步)

- 信号量 != 信号
信号量是一个计数器
特性方面:IPC资源必须删除,否则不会自动清楚,除非重启,所以system V IPC 资源的生命周期随内核
信号量,信号灯:对资源进行预定的计数器
二元信号量:0,1
信号量支持同时对多个信号量进行PV操作
- 互斥访问,把资源当作整体使用,临界区是串行执行
- 不同进程,访问共享资源,具有一定的并发性

System V 是如何实现IPC呢?和管道为什么不同?
- 应用角度,看IPC属性

推测:在 OS方面,IPC是同类资源
cpp
void ShmMeta(){
structshmid_dsbuffer;//系统提供的数据类型
int n =::shmctl(_shmid,IPC_STAT,&buffer);
if(n <0) return;
std::cout << buffer.shm_atime << std::endl;
std::cout << buffer.shm_cpid << std::endl;
std::cout<buffer.shm_ctime < std::endl;
std::cout < buffer.shm_nattch< std::endl;
std::cout << buffer.shm_perm.__key << std::endl;
}
- 内核角度,看IPC结构
IPC资源一定是全局的资源!被所有的进程看到!
消息队列,共享内存,信号量,key区分唯一性
进程间通信的本质,始终是 "让不同进程访问同一份系统级资源"------ 管道依托文件系统实现单向 / 跨进程的数据传输,System V IPC 则通过内核级的全局资源(共享内存、消息队列)实现更高性能的交互,而信号量则为这些共享资源提供了关键的同步互斥保护。
本文从基础概念到代码实操,覆盖了管道(匿名 / 命名)、共享内存、System V 消息队列和信号量的核心知识点,你可以通过文中的示例代码动手编译运行,结合ipcs/ipcrm等指令观察 IPC 资源的生命周期,加深对 "IPC 资源随内核""临界资源需要保护" 等核心原则的理解。
当然,Linux 的 IPC 体系远不止这些(比如 Socket、信号也是重要的 IPC 方式),掌握本文的基础后,你可以进一步学习不同 IPC 方式的性能对比、场景选型(比如共享内存快但无保护,管道简单但效率低),以及实际项目中进程池 + IPC 的综合应用,让这些底层知识真正落地到工程实践中。