Linux 进程间通信进阶:管道与共享内存实战
1. 进程间通信基础概念
1.1 本质与前提
进程间通信(IPC)指的是两个或多个进程进行信息传递的过程。由于进程具有独立性,一个进程的数据无法直接访问另一个进程的内存空间。父进程的全局变量对子进程不可见,或者即使可见也无法持续同步。
要让不同的进程看到同一份资源,操作系统必须提供系统调用作为桥梁。这是 IPC 的本质前提。
1.2 通信目的
- 数据传输:将一个进程的数据发送给另一个进程。
- 资源共享:多个进程共享同一块资源。
- 通知事件:发送消息通知其他进程状态变化(如进程终止)。
- 进程控制:完全控制另一个进程的执行(如调试器拦截异常)。
2. 管道通信
2.1 管道特点
管道是内核中实现的一种单向数据流通道,具有以下特点:
- 单向性:设计之初只允许单项数据通信。
- 血缘关系:通常用于具有父子关系的进程之间。
- 生命周期:管道随进程退出而关闭,文件描述符的生命周期依附于进程。
- 同步机制:内部实现了进程间的同步。
- 字节流:面向字节流,读写次数可能不匹配。
2.2 常见场景
- 写端慢读端快:以慢节奏为准,读端等待数据就绪。
- 写端快读端慢:读端一次性读取所有写入的数据。
- 写端关闭:若写端
close(wfd),读端会收到 EOF。 - 读端关闭:若读端
close(rfd),写端继续写入会导致 SIGPIPE 信号。
3. 进程池示例
在构建进程池时,需要注意资源回收。如果只退出了一个子进程而未回收所有僵尸进程,会导致资源泄漏。
修复方法是在主进程中遍历并回收所有子进程的状态。以下是创建 10 个子进程的简化逻辑:
// 伪代码逻辑示意
for (int i = 0; i < process_number; ++i) {
pid_t pid = fork();
if (pid == 0) {
// 子进程执行任务
task_function(i);
exit(0);
}
}
// 等待所有子进程结束
while (wait(NULL) > 0);
4. 命名管道(FIFO)
命名管道允许毫不相干的进程之间进行通信,它本质上是一个特殊的文件。
4.1 服务端与客户端
服务端不需要预先建立管道,只需打开已存在的 FIFO 文件。客户端同样通过文件名连接。
服务器端 (server.cc)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define FIFO_NAME "fifo"
int main() {
// 创建 FIFO(如果已存在则忽略错误)
if (mkfifo(FIFO_NAME, 0666) == -1) {
perror("mkfifo");
}
printf("Server waiting for client...\n");
int fd = open(FIFO_NAME, O_RDONLY);
if (fd == -1) {
perror("open");
exit(1);
}
printf("Client connected.\n");
char buf[1024];
ssize_t n;
while ((n = read(fd, buf, sizeof(buf) - 1)) > 0) {
buf[n] = '\0';
printf("client say# %s", buf);
fflush(stdout);
}
if (n == -1) perror("read");
close(fd);
unlink(FIFO_NAME);
return 0;
}
客户端 (client.cc)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define FIFO_NAME "fifo"
int main() {
printf("Please Enter@ ");
fflush(stdout);
char msg[1024];
if (fgets(msg, sizeof(msg), stdin) == NULL) {
perror("fgets");
exit(1);
}
// 以只写方式打开 FIFO(会阻塞直到有读者)
int fd = open(FIFO_NAME, O_WRONLY);
if (fd == -1) {
perror("open");
exit(1);
}
write(fd, msg, strlen(msg));
close(fd);
return 0;
}
Makefile
all: client server
client: client.cc
g++ -o $@ $^ -std=c++11
server: server.cc
g++ -o $@ $^ -std=c++11
.PHONY: clean
clean:
rm -f client server
4.2 运行细节
先启动服务端,再启动客户端。服务端在 open 时会阻塞,直到有客户端打开写端;反之亦然。这种机制天然保证了读写双方的同步。
4.3 应用场景
- 匿名管道:常用于竖线操作符
|连接的命令。 - 命名管道:适合无亲缘关系进程的文件拷贝或简单服务交互。
5. 共享内存
共享内存是 IPC 中速度最快的方式,因为它避免了数据的多次拷贝。
5.1 原理
共享内存将物理内存映射到多个进程的虚拟地址空间。不同进程通过各自的页表指向同一物理页,从而直接读写同一块内存区域。
5.2 关键系统调用
- shmget: 创建或获取共享内存段标识符。
- shmat: 将共享内存挂接到当前进程的地址空间。
- shmdt: 解除挂接(去关联)。
- shmctl: 控制共享内存,包括删除 (
IPC_RMID)。
5.3 键值 (Key) 管理
共享内存需要一个唯一的标识符。用户通常使用 ftok 函数结合文件路径和项目 ID 生成键值,确保不同进程能定位到同一块内存。
5.4 代码实现
这里封装了一个 Shm 类来管理共享内存的生命周期。
头文件 (Shm.hpp)
#ifndef __SHM_HPP
#define __SHM_HPP
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/shm.h>
#include <string>
const std::string proj_name = "/home";
const int proj_id = 0x6666;
const int g_size = 4096;
static std::string ToHex(int data) {
char hex[64];
snprintf(hex, sizeof(hex), "0x%x", data);
return hex;
}
typedef struct {
int count;
char buffer[26 * 2];
} buffer_t;
class Shm {
public:
Shm(int size = g_size) : _shmid(-1), _size(size), _key(0) {}
~Shm() { Detach(); Delete(); }
private:
key_t GetKey() {
_key = ftok(proj_name.c_str(), proj_id);
if (_key < 0) perror("ftok");
return _key;
}
bool CreateCoreHelper(int flags) {
key_t k = GetKey();
_shmid = shmget(k, _size, flags);
if (_shmid < 0) {
perror("shmget");
return false;
}
return true;
}
public:
bool Create() { return CreateCoreHelper(IPC_CREAT | IPC_EXCL | 0666); }
bool Get() { return CreateCoreHelper(IPC_CREAT); }
bool Delete() {
int n = shmctl(_shmid, IPC_RMID, NULL);
return n < 0 ? false : true;
}
void* Attach() { return shmat(_shmid, nullptr, 0); }
void Detach() { shmdt(_start); }
void Debug() {
std::cout << "key: " << ToHex(_key) << std::endl;
std::cout << "shmid: " << _shmid << std::endl;
}
private:
key_t _key;
int _shmid;
int _size;
void* _start;
};
#endif
写入端 (Writer.cc)
#include "Shm.hpp"
#include <iostream>
#include <string>
#include <cstring>
Shm shm;
class Init {
public:
Init() {
shm.Get();
addr = (char*)shm.Attach();
std::cout << "addr: " << ToHex((long long)addr) << std::endl;
}
~Init() { shm.Detach(); }
char* Addr() { return addr; }
public:
char* addr;
};
Init init;
int main() {
std::cout << "test Begin..." << std::endl;
buffer_t* shm_data = (buffer_t*)init.Addr();
shm_data->count = 0;
memset(shm_data->buffer, 0, 4096);
char ch = 'A';
for (int i = 0; i < 26 * 2; i += 2, ch++) {
shm_data->buffer[i] = ch;
shm_data->buffer[i + 1] = ch;
shm_data->count++;
usleep(7000000);
}
return 0;
}
读取端 (Reader.cc)
#include "Shm.hpp"
#include <iostream>
#include <string>
#include <unistd.h>
int main() {
Shm shm;
shm.Create();
sleep(100);
char* addr = (char*)shm.Attach();
buffer_t* shm_addr = (buffer_t*)addr;
int old = shm_addr->count;
while (true) {
if (old != shm_addr->count) {
std::cout << "count : " << shm_addr->count << std::endl;
std::cout << "data : " << shm_addr->buffer << std::endl;
old = shm_addr->count;
}
usleep(50000);
if (shm_addr->count >= 26) break;
}
shm.Detach();
shm.Delete();
return 0;
}
5.5 注意事项
- 大小对齐:建议设置为 4096 的整数倍,避免浪费或越界风险。
- 同步保护:共享内存本身没有锁机制。多进程同时读写可能导致数据竞争,需配合信号量使用。
- 清理机制:程序退出前务必调用
shmdt和shmctl清理资源,否则共享内存会残留至重启。
6. 总结
进程间通信的核心在于让隔离的进程看到同一份资源。管道适合简单、单向的数据流,而共享内存则是高性能大数据传输的首选。在实际开发中,注意资源的生命周期管理和并发同步问题,才能构建稳定的系统架构。

