💻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 · 操作系统