作者说明 · 学习记录
本站作为作者的学习记录站,不保证文章内容严谨或完全正确。
本文由 Gemini 2.5 Flash 协助创作
本文部分内容由AI生成,最终版本由作者审核与修改。
版权信息

warning

本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。


在 Linux 世界中,进程是独立的执行单元,拥有自己的地址空间。但很多时候,为了完成一个复杂的任务,不同的进程需要协同工作,交换数据。这时,我们就需要进程间通信(IPC, Inter-Process Communication)。IPC 就像是进程之间的一座桥梁,让它们能够相互“交谈”,共享信息。

本文将带你深入了解 Linux 中常见的 IPC 机制,并以使用为导向,结合代码示例,让你能够快速掌握这些“通信”技术。

1. 为什么需要 IPC?

想象一个场景:你正在开发一个 Web 服务器。一个主进程负责监听网络请求,但处理这些请求非常耗时。如果主进程自己处理,服务器就会变得很慢,无法响应新的请求。一个更好的设计是,主进程每接收到一个请求,就创建一个新的子进程或将请求发送给一个工作进程池来处理。这样,主进程可以立即回去监听新的连接,而工作进程则专注于处理任务。

在这个例子中,主进程需要将请求数据传递给工作进程。这就是 IPC 发挥作用的地方。

2. 常见的 Linux IPC 机制

Linux 提供了多种 IPC 机制,每种都有其独特的优缺点和适用场景。我们可以将它们分为两大类:基于文件基于内存

2.1. 管道(Pipes)

管道可能是最简单、最古老的 IPC 形式。它就像一个单向的“水管”,一端用于写入,另一端用于读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
int pipefd[2];
pid_t pid;
char buf[20];
const char *msg = "Hello from parent!";

if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}

pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}

if (pid > 0) { // 父进程
close(pipefd[0]); // 关闭读取端
write(pipefd[1], msg, strlen(msg) + 1);
close(pipefd[1]); // 关闭写入端,如果不关,子进程读端收不到EOF信号,则会一直读导致程序阻塞
wait(NULL);
} else { // 子进程
close(pipefd[1]); // 关闭写入端,如果不关
read(pipefd[0], buf, sizeof(buf));
printf("Child received: %s\n", buf);
close(pipefd[0]); // 关闭读取端
}

return 0;
}

2.2. FIFO(命名管道)

管道只能用于有亲缘关系的进程,那如果两个毫不相关的进程想通信怎么办?答案就是 FIFO (First-In, First-Out),也叫命名管道

2.3. 信号

信号(Signal) 是一种更轻量级、更异步的进程间通信和事件通知机制。它就像一个“软中断”,用来通知进程发生了某个事件。

想象一下,你正在专注地工作,突然有人拍了你一下肩膀。你停下手中的活,转头看看发生了什么事,然后根据情况做出反应(比如,对方是同事,你可能和他聊两句;对方是领导,你可能马上站起来)。

在 Linux 中,信号就是那个“拍肩膀”的动作。当一个进程收到一个信号时,它会暂停当前执行的任务,转而去处理这个信号,处理完后再恢复执行。

2.3.1. 信号的分类

信号有很多种,每种都有其特定的用途。常见的信号及其作用如下:

信号名称 默认行为 解释
SIGHUP (1) 终止进程 当终端关闭时发送给关联的进程。
SIGINT (2) 终止进程 来自键盘中断,通常是 Ctrl+C
SIGQUIT (3) 终止并生成核心转储文件 来自键盘退出,通常是 Ctrl+\\
SIGKILL (9) 强制终止进程 无法被捕获、阻塞或忽略,强制杀死进程。
SIGTERM (15) 终止进程 友好的终止请求,可以被捕获。kill 命令默认发送此信号。
SIGCHLD 忽略 子进程终止或停止时发送给父进程。
SIGSTOP 停止进程 无法被捕获、忽略,暂停进程。
SIGCONT 继续进程 使停止的进程继续运行。

2.3.2. 信号与 IPC 的关系

理解信号,特别是信号集和阻塞的概念,对于编写健壮的多进程或多线程程序至关重要。它能让你更好地控制程序对外部事件的响应。

2.3.3. 信号的三种处理方式

当进程收到一个信号时,它可以有三种处理方式:

  1. 执行默认动作(Default):大多数信号都有一个预定义的默认行为。例如,SIGINT 的默认行为就是终止进程。

  2. 忽略信号(Ignore):有些信号可以被忽略,即进程收到信号后不做任何处理。SIGCHLD 信号的默认行为就是忽略。

  3. 捕获信号(Catch):这是最灵活的方式。进程可以为某个信号注册一个信号处理函数(Signal Handler)。当信号到来时,进程会执行这个函数来处理信号,而不是执行默认动作。

caution

注意SIGKILLSIGSTOP 这两个信号是不能被捕获、忽略或阻塞的。它们是系统管理员强制终止或停止进程的“最后手段”。

2.3.4. 如何使用信号?

2.3.4.1. 发送信号:kill() 函数

你可以使用 kill() 函数向另一个进程发送信号。

1
2
3
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
2.3.4.2. 注册信号处理函数:signal()sigaction()
1
2
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction 结构体

1
2
3
4
5
struct sigaction {
void (*sa_handler)(int); // 信号处理函数
sigset_t sa_mask; // 信号集,在信号处理函数执行期间需要阻塞的信号
int sa_flags; // 标志位
};

2.4. 信号集(Signal Set)和阻塞

当你在处理一个信号时,你可能不希望被其他信号打断。信号集(sigset_t 就是用来管理一组信号的。通过操作信号集,你可以阻塞(Block) 某些信号,让它们在进程处理完当前任务后才被传递。

2.4.1. 信号集操作函数

2.4.2. 阻塞信号:sigprocmask()

sigprocmask() 函数用来设置进程的信号阻塞掩码(Signal Mask)

1
2
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

示例:在处理关键代码段时临时阻塞 SIGINT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void my_handler(int sig) {
printf("Caught signal %d\n", sig);
}

int main() {
sigset_t block_mask, old_mask;

// 1. 设置要阻塞的信号集
sigemptyset(&block_mask);
sigaddset(&block_mask, SIGINT);

// 2. 阻塞 SIGINT 信号
sigprocmask(SIG_BLOCK, &block_mask, &old_mask);

printf("SIGINT is blocked. Press Ctrl+C...\n");
sleep(10); // 在这10秒内,Ctrl+C不会终止进程

printf("Unblocking SIGINT...\n");
// 3. 解除阻塞,恢复旧的信号掩码
sigprocmask(SIG_SETMASK, &old_mask, NULL);

printf("SIGINT is unblocked. Press Ctrl+C again.\n");

// 4. 注册一个信号处理函数
struct sigaction sa;
sa.sa_handler = my_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);

while(1) {
printf("Running...\n");
sleep(1);
}

return 0;
}

运行这段代码,你会看到在阻塞期间,Ctrl+C 无法终止进程。当解除阻塞后,Ctrl+C 才能触发信号处理函数。

2.5. System V IPC (System V IPC)

System V IPC 是 Linux 系统中一组更高级、更强大的 IPC 机制,包括消息队列、信号量和共享内存。它们都是基于内核的,需要一个唯一的键值(key)来标识。

2.5.1. 消息队列(Message Queues)

消息队列就像一个链表,允许进程向其中添加消息或从中读取消息。

2.5.2. 信号量(Semaphores)

信号量主要用于同步,控制对共享资源的访问。它本身不传递数据,而是作为一种“计数器”。

2.5.3. 共享内存(Shared Memory)

共享内存是最高效的 IPC 方式。它允许两个或多个进程共享同一块物理内存。

2.6. IPC 总结与选择

机制 适用场景 优点 缺点
管道 有亲缘关系的进程 简单,易于使用 单向,仅限于亲缘进程
FIFO 无亲缘关系的进程 可以在文件系统中命名,灵活 单向,需要同步,读写时有阻塞
消息队列 异步通信,少量数据 消息带类型,无需同步 效率较低,有大小限制
信号量 进程间同步,互斥 用于控制访问,防止竞争 不传递数据
共享内存 大量数据传输 最高效,读写速度快 必须配合其他同步机制使用

如何选择?

了解这些 IPC 机制,就如同掌握了进程之间“沟通”的多种语言。在开发时,选择合适的“语言”能让你的程序更加健壮、高效。现在,你可以尝试用这些机制来解决你遇到的实际问题了!

3. POSIX IPC:System V 的继任者

POSIX IPC(Portable Operating System Interface)是一套新的 IPC 标准,旨在解决 System V IPC 的一些局限性。它提供了一套更统一、更现代的 API,使用文件名作为标识符,而不是 System V 的键值,这使得 IPC 资源的管理更加直观。

3.1. POSIX 消息队列

与 System V 消息队列类似,但 API 更简洁。

3.2. POSIX 信号量

用于进程间的同步,功能与 System V 信号量类似,但提供了更简单的接口。

3.3. POSIX 共享内存

和 System V 共享内存一样,都是最快的 IPC 方式,但 POSIX 版本使用了文件描述符。

4. 套接字(Socket):网络通信的基石

套接字(Socket) 是一种更通用的 IPC 机制,它不仅可以在同一台机器上的进程间通信,更重要的能力是实现跨网络、不同主机上的进程通信。它是网络通信的基石。

4.1. 套接字类型

4.2. 套接字通信流程(以 TCP 为例)

服务器端:

  1. socket():创建一个套接字。

  2. bind():将套接字与一个 IP 地址和端口号绑定。

  3. listen():监听来自客户端的连接请求。

  4. accept():接受客户端连接,返回一个新的套接字用于与该客户端通信。

  5. read()/write():通过新的套接字与客户端进行数据交换。

  6. close():关闭套接字。

客户端:

  1. socket():创建一个套接字。

  2. connect():连接到服务器指定的 IP 地址和端口号。

  3. write()/read():向服务器发送数据或接收数据。

  4. close():关闭套接字。

4.3. 套接字域(Socket Domain)

除了我们熟悉的网络套接字(AF_INET),还有一种特别的 IPC 机制:UNIX 域套接字(AF_UNIX)

4.4. 总结与选择

掌握这些 IPC 机制,你就能够让你的程序在不同维度上进行“对话”,无论是同一台机器上的协作,还是跨越网络边界的协同工作。

深入浅出 Linux IPC:进程间通信的艺术

创建时间:9月 12, 2025

最后更新:9月 15, 2025

字数统计:5.1k字

预计阅读:19min