💻CS 操作系统
进程间通信
难度:⭐⭐ | 高频指数:🔥🔥
面试回答
常见问法
- Linux 进程间通信有哪些方式?
- 共享内存和管道有什么区别?
- 什么场景用什么 IPC 方式?
- 共享内存怎么保证同步?
- socket 和其他 IPC 方式有什么区别?
回答
Linux 进程间通信(IPC)主要有以下几种方式:
| IPC 方式 | 特点 | 适用场景 |
|---|---|---|
| 管道(pipe) | 半双工、父子进程间 | 简单的父子进程数据传递 |
| 命名管道(FIFO) | 半双工、无亲缘关系进程可用 | 不相关进程间简单通信 |
| 消息队列 | 有格式的消息、可按类型读取 | 需要消息分类的场景 |
| 共享内存 | 最快、直接读写同一块内存 | 大量数据高速交换 |
| 信号量 | 用于同步、不传数据 | 控制共享资源访问 |
| 信号(signal) | 异步通知 | 通知进程某事件发生 |
| socket | 可跨网络、全双工 | 网络通信、跨机器通信 |
最常考的对比:共享内存 vs 管道
- 共享内存:速度最快(不需要内核中转),但需要自己处理同步(信号量/互斥锁)
- 管道:使用简单(read/write),内核帮你处理同步,但每次通信都要经过内核拷贝
追问
1. 管道的实现原理?
管道本质是内核中的一块缓冲区(通常 64KB)。写端写入数据到缓冲区,读端从缓冲区读取。如果缓冲区满,写端阻塞;如果缓冲区空,读端阻塞。
int fd[2];
pipe(fd); // fd[0] 读端,fd[1] 写端
pid_t pid = fork();
if (pid == 0) {
close(fd[1]); // 子进程关闭写端
read(fd[0], buf, sizeof(buf));
close(fd[0]);
} else {
close(fd[0]); // 父进程关闭读端
write(fd[1], "hello", 5);
close(fd[1]);
}
2. 共享内存怎么用?
// 创建共享内存
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
// 映射到进程地址空间
void* addr = shmat(shmid, NULL, 0);
// 直接读写
strcpy((char*)addr, "hello from process A");
// 解除映射
shmdt(addr);
// 删除共享内存
shmctl(shmid, IPC_RMID, NULL);
3. 共享内存为什么需要额外同步?
因为两个进程同时读写同一块内存,没有内核帮你排队。必须用信号量或互斥锁(放在共享内存中的 pthread_mutex)来保证互斥访问。
原理展开
1. 管道(Pipe)
特点:
- 半双工(数据单向流动)
- 只能用于有亲缘关系的进程(fork 后继承文件描述符)
- 基于文件描述符操作(read/write)
- 内核缓冲区大小通常 64KB(Linux)
限制:
- 不能广播(一对一)
- 数据无边界(字节流)
- 容量有限,满了就阻塞
2. 命名管道(FIFO)
// 创建命名管道
mkfifo("/tmp/myfifo", 0666);
// 进程 A:写
int fd = open("/tmp/myfifo", O_WRONLY);
write(fd, "data", 4);
// 进程 B:读
int fd = open("/tmp/myfifo", O_RDONLY);
read(fd, buf, sizeof(buf));
和匿名管道的区别:
- 有文件系统路径名,无亲缘关系的进程也能用
- 其他特性(半双工、字节流、内核缓冲)相同
3. 消息队列
// 创建/获取消息队列
int msqid = msgget(key, IPC_CREAT | 0666);
// 发送消息
struct msgbuf {
long mtype; // 消息类型
char mtext[256];
};
struct msgbuf msg = {1, "hello"};
msgsnd(msqid, &msg, strlen(msg.mtext), 0);
// 接收消息(可按类型过滤)
msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0);
优点:
- 消息有类型,可以选择性接收
- 有边界(一条消息是一个整体)
- 不需要读写双方同时存在
缺点:
- 每条消息有大小限制
- 性能不如共享内存
- System V 接口较老,POSIX 消息队列更现代
4. 共享内存
为什么最快:
管道/消息队列:进程A → 用户态拷贝到内核 → 内核拷贝到进程B(两次拷贝)
共享内存: 进程A 直接写 → 进程B 直接读(零拷贝)
POSIX 共享内存(更现代的接口):
// 创建
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);
// 映射
void* addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
// 使用...
// 清理
munmap(addr, 4096);
shm_unlink("/my_shm");
5. 信号量
信号量不传输数据,只用于同步/互斥控制。
// POSIX 命名信号量
sem_t* sem = sem_open("/my_sem", O_CREAT, 0666, 1);
sem_wait(sem); // P 操作(-1,可能阻塞)
// 临界区
sem_post(sem); // V 操作(+1,唤醒等待者)
sem_close(sem);
sem_unlink("/my_sem");
信号量 vs 互斥锁:
- 互斥锁:只能 0/1,谁加锁谁解锁
- 信号量:可以是任意非负整数,可以由不同进程/线程操作
6. Socket
Socket 是最通用的 IPC 方式,既能本机通信也能跨网络。
// Unix Domain Socket(本机进程间通信)
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/my.sock");
bind(fd, (struct sockaddr*)&addr, sizeof(addr));
listen(fd, 5);
Unix Domain Socket vs TCP Socket:
- 不经过网络协议栈,性能更好
- 支持传递文件描述符
- 只能本机使用
7. 各种 IPC 方式对比总结
| 方式 | 速度 | 复杂度 | 是否需要同步 | 跨机器 | 数据边界 |
|---|---|---|---|---|---|
| 管道 | 中 | 低 | 内核保证 | 否 | 无(字节流) |
| 共享内存 | 最快 | 高 | 需要自己做 | 否 | 无 |
| 消息队列 | 中 | 中 | 内核保证 | 否 | 有 |
| 信号量 | - | 中 | 本身就是同步工具 | 否 | - |
| Socket | 较慢 | 高 | 内核保证 | 是 | 取决于类型 |
易错点
- 说”管道是双向的”——匿名管道是半双工,要双向通信需要两个管道。
- 忘记共享内存需要额外同步——这是面试必追问的点。
- 混淆 System V IPC 和 POSIX IPC——面试时说清楚用的是哪套接口。
- 说”信号量是用来传数据的”——信号量只做同步控制,不传输数据。
- 忘记 Unix Domain Socket——它是本机 IPC 中性能很好的选择,比 TCP socket 快。
- 不知道各种方式的适用场景——面试官经常问”什么时候用什么”。
记忆技巧
- IPC 七兄弟:管(管道)、名(命名管道)、消(消息队列)、共(共享内存)、信(信号量)、号(信号)、套(socket)
- 速度排序:共享内存 > 管道/消息队列 > socket
- 共享内存一句话:最快但最危险,必须自己加锁
- 管道一句话:简单但只能父子、单向、字节流
- socket 一句话:最通用,能跨网络,但开销最大
面试速答版
Linux IPC 主要有管道、命名管道、消息队列、共享内存、信号量、信号和 socket。共享内存最快,因为不需要内核中转数据(零拷贝),但需要自己用信号量或互斥锁做同步。管道简单但只能父子进程间单向通信。消息队列有消息边界和类型过滤。Socket 最通用,能跨网络,本机通信可以用 Unix Domain Socket 避免网络协议栈开销。选型原则:大量数据高速交换用共享内存,简单父子通信用管道,需要跨机器用 socket。
Related · 操作系统