马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
命名管道与共享内存:Linux 进程间通信的两种经典方式
Linux 下进程间通信(IPC)手段很多,管道和共享内存是最基础的两类。管道分匿名与命名两种:匿名管道只能给有血缘关系的进程用,命名管道(FIFO)靠文件路径把毫不相干的两个进程拉进同一份资源。共享内存则绕开内核拷贝,直接给两个进程映射同一块物理内存,是最快的一种 IPC。这篇把这两条线讲清楚,顺带提一下 System V 的消息队列和信号量。
一、命名管道(FIFO)
1.1 为什么需要命名管道
匿名管道的局限是只能用在有共同祖先的进程之间(典型就是父子进程)。想让两个毫无关系的进程交换数据,就得有个双方都能看见的"抓手"——这就是命名管道的由来。命名管道是一种特殊类型的文件,有文件路径,不相关进程只要打开同一个路径就能通信。
1.2 原理
两个进程打开同一个文件时,各自都会创建一个 struct file、拿到自己的文件描述符和文件指针。以读、写方式分别打开的两个进程各有一份 struct file,正好避免读写冲突。匿名管道靠父子继承让双方"看到同一个被打开的文件";命名管道则靠唯一的文件路径——给 client 和 server 同一个路径,它们就打开了同一个管道文件。
1.3 创建和删除
命令行创建:
man mkfifo # 查看创建命名管道的用法
mkfifo myfifo # 创建名为 myfifo 的管道文件(ls 时首属性是 p)
程序里创建,用 C 接口:
int mkfifo(const char *pathname, mode_t mode);
// 参数一:路径 + 文件名;参数二:创建时的权限
// 返回值:成功返回 0,失败返回 -1
删除同样两种途径:
man 2 unlink # 查看删除管道文件的用法
unlink myfile # 删除指定管道文件
int unlink(const char *pathname); // 程序里删除文件
1.4 匿名管道 vs 命名管道
- 匿名管道由
pipe() 创建并打开;命名管道由 mkfifo() 创建,用 open() 打开。
- 两者创建与打开方式不同,但完成之后语义完全一样——都是先进先出、都按字节流读写。
- 命名管道还要额外处理:以只读打开的进程会阻塞直到有写端;以只写打开的会阻塞直到有读端。这是"打开即等待"的特性,写服务端代码时容易踩。
1.5 一个 server/client 示例
先写 Makefile,一次编出两个可执行文件:
.PHONY: all
all: client server
client: client.cc
g++ -o $@ $^ -std=c++11
server: server.cc
g++ -o $@ $^ -std=c++11
.PHONY: clean
clean:
rm -rf client server
注意点:Makefile 默认只生成第一个目标对应的程序,所以必须显式定义一个只含依赖、不含命令的 all 目标,把 client 和 server 都挂在它下面,一次全编出来。接下来服务端与客户端共用一份头文件,约定管道路径、读写方式和一次通信的字节数:
// namedPipe.hpp —— 命名管道公共定义
// 把管道文件路径(公共路径)放在当前目录下,文件名叫 myfifo
const std::string comm_path = "./myfifo";
#define DefaultFd -1
#define Creater 1 // 管道的创建者
#define User 2 // 管道的使用者
#define Read O_RDONLY
#define Write O_WRONLY
#define BaseSize 4096 // 一次通信的基本大小
类里封装了打开管道的动作——构造函数接收路径和角色,创建者才会去 mkfifo(path, 0666),使用者直接 open();打开成功拿到 fd,失败返回 false:
bool OpenNamedPipe(int mode)
{
_fd = open(_fifo_path.c_str(), mode);
if (_fd < 0) return false;
return true;
}
NamePiped(const std::string &path, int who)
: _fifo_path(path), _id(who), _fd(DefaultFd)
{
if (_id == Creater) {
int res = mkfifo(_fifo_path.c_str(), 0666);
if (res != 0) perror("mkfifo");
}
}
二、共享内存
2.1 原理
管道 transferring 数据要走内核:写进程把数据从用户态拷进内核缓冲区,读进程再从内核拷回用户态,两次拷贝跑不掉。共享内存的思路是直接让多个进程把同一段物理内存映射到各自虚拟地址空间,读写变成一次内存访问,没有内核中转,是 IPC 里效率最高的一档。代价也明确:它没有同步机制,得自己配信号量或用文件锁保证互斥,否则就是写了半截被读走。
2.2 相关接口(System V)
int shmget(key_t key, size_t size, int shmflg); // 创建或获取共享内存段
void *shmat(int shmid, const void *shmaddr, int shmflg); // 挂接到本进程地址空间
int shmdt(const void *shmaddr); // 脱离(解除映射)
int shmctl(int shmid, int cmd, struct shmid_ds *buf); // 控制(IPC_RMID 删除)
常用组合:shmget 拿到 id → shmat 得到指针直接读写 → 用完 shmdt 脱离 → shmctl(..., IPC_RMID, ...) 删除段。键值一般用 ftok() 由路径和项目编号生成。
2.3 和命名管道怎么选
- 传结构化数据、量不大、要简单 → 命名管道,代码几十行就够,天然有序。
- 大块数据高频读写(图像、视频帧、大模型 tensor)→ 共享内存,配合信号量做同步。
- 现代项目更常用 POSIX 版的共享内存
shm_open + mmap + ftruncate,接口比 System V 一套更直观,也能直接用文件工具查看。
三、另外两个 System V 成员(了解即可)
- 消息队列:内核维护的格式化消息链表,按类型收发,带边界(一条消息整体读走),适合传带类型的小报文。
- 信号量:计数器,用来做进程间互斥与同步,本身不传数据,专门管"谁先谁后"。理解方式是把它当成一个带等待队列的计数器:P 操作减 1(不够就阻塞),V 操作加 1 并唤醒等待者。
四、总结
- 匿名管道靠继承、命名管道靠文件路径,两者语义相同,差别只在创建与打开方式。
- 共享内存绕过内核拷贝最快,但必须自己解决同步。
- 实际选型看三件事:数据量、是否需要保序、要不要带类型。
把命名管道和共享内存这两条吃透,剩下的消息队列、信号量、socket 都是同一个 IPC 体系里的变体,接口换一换思路就通了。
qidao123.com企服评测-应用市场 编辑提示 |