warning
本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
在 Linux 世界中,进程是独立的执行单元,拥有自己的地址空间。但很多时候,为了完成一个复杂的任务,不同的进程需要协同工作,交换数据。这时,我们就需要进程间通信(IPC, Inter-Process Communication)。IPC 就像是进程之间的一座桥梁,让它们能够相互“交谈”,共享信息。
本文将带你深入了解 Linux 中常见的 IPC 机制,并以使用为导向,结合代码示例,让你能够快速掌握这些“通信”技术。
想象一个场景:你正在开发一个 Web 服务器。一个主进程负责监听网络请求,但处理这些请求非常耗时。如果主进程自己处理,服务器就会变得很慢,无法响应新的请求。一个更好的设计是,主进程每接收到一个请求,就创建一个新的子进程或将请求发送给一个工作进程池来处理。这样,主进程可以立即回去监听新的连接,而工作进程则专注于处理任务。
在这个例子中,主进程需要将请求数据传递给工作进程。这就是 IPC 发挥作用的地方。
Linux 提供了多种 IPC 机制,每种都有其独特的优缺点和适用场景。我们可以将它们分为两大类:基于文件和基于内存。
管道可能是最简单、最古老的 IPC 形式。它就像一个单向的“水管”,一端用于写入,另一端用于读取。
特点:
单向通信:数据只能从一端流向另一端。
父子进程通信:管道通常用于有亲缘关系的进程之间,比如父进程和子进程。
半双工:虽然是单向,但如果创建两个管道,就可以实现双向通信。
使用:
pipe()
函数:这是创建管道的核心函数。1 |
|
pipefd
是一个包含两个文件描述符的数组,pipefd[0]
用于读取,pipefd[1]
用于写入。
代码示例:一个简单的父子进程通信。
1 |
|
管道只能用于有亲缘关系的进程,那如果两个毫不相关的进程想通信怎么办?答案就是 FIFO (First-In, First-Out),也叫命名管道。
特点:
文件系统路径:它在文件系统中有一个路径名,不同于匿名管道。
非亲缘进程通信:任意两个进程都可以通过这个路径名打开并通信。
单向:和管道一样,FIFO 也是单向的,需要两个 FIFO 来实现双向通信。
使用:
mkfifo()
函数或 mkfifo
命令:用来创建 FIFO。1 |
|
open()
, read()
, write()
函数:像操作普通文件一样来操作 FIFO。代码示例:
writer.c
:1 |
|
reader.c
:1 |
|
你可以先运行 writer.c
,再运行 reader.c
。
信号(Signal) 是一种更轻量级、更异步的进程间通信和事件通知机制。它就像一个“软中断”,用来通知进程发生了某个事件。
想象一下,你正在专注地工作,突然有人拍了你一下肩膀。你停下手中的活,转头看看发生了什么事,然后根据情况做出反应(比如,对方是同事,你可能和他聊两句;对方是领导,你可能马上站起来)。
在 Linux 中,信号就是那个“拍肩膀”的动作。当一个进程收到一个信号时,它会暂停当前执行的任务,转而去处理这个信号,处理完后再恢复执行。
发送者:可以是内核(比如你按下 Ctrl+C
,内核会发送 SIGINT
信号给前台进程)、也可以是其他进程(使用 kill()
函数)。
接收者:任何一个进程都可以接收信号。
信号有很多种,每种都有其特定的用途。常见的信号及其作用如下:
信号名称 | 默认行为 | 解释 |
---|---|---|
SIGHUP (1) |
终止进程 | 当终端关闭时发送给关联的进程。 |
SIGINT (2) |
终止进程 | 来自键盘中断,通常是 Ctrl+C 。 |
SIGQUIT (3) |
终止并生成核心转储文件 | 来自键盘退出,通常是 Ctrl+\\ 。 |
SIGKILL (9) |
强制终止进程 | 无法被捕获、阻塞或忽略,强制杀死进程。 |
SIGTERM (15) |
终止进程 | 友好的终止请求,可以被捕获。kill 命令默认发送此信号。 |
SIGCHLD |
忽略 | 子进程终止或停止时发送给父进程。 |
SIGSTOP |
停止进程 | 无法被捕获、忽略,暂停进程。 |
SIGCONT |
继续进程 | 使停止的进程继续运行。 |
异步通知:信号是典型的异步 IPC 机制,它不像管道或共享内存那样传递数据,而是传递事件信息。
轻量级:相比于其他 IPC,信号的开销非常小。
同步:信号也可以用于同步目的,例如 SIGCHLD
信号常用于父进程等待子进程结束。
理解信号,特别是信号集和阻塞的概念,对于编写健壮的多进程或多线程程序至关重要。它能让你更好地控制程序对外部事件的响应。
当进程收到一个信号时,它可以有三种处理方式:
执行默认动作(Default):大多数信号都有一个预定义的默认行为。例如,SIGINT
的默认行为就是终止进程。
忽略信号(Ignore):有些信号可以被忽略,即进程收到信号后不做任何处理。SIGCHLD
信号的默认行为就是忽略。
捕获信号(Catch):这是最灵活的方式。进程可以为某个信号注册一个信号处理函数(Signal Handler)。当信号到来时,进程会执行这个函数来处理信号,而不是执行默认动作。
caution
注意:SIGKILL
和 SIGSTOP
这两个信号是不能被捕获、忽略或阻塞的。它们是系统管理员强制终止或停止进程的“最后手段”。
kill()
函数你可以使用 kill()
函数向另一个进程发送信号。
1 |
|
pid
:目标进程的 ID。
sig
:要发送的信号编号。
signal()
和 sigaction()
signal()
函数:这是最简单的注册方式,但它在不同系统上的行为可能不一致,不推荐在新代码中使用。
sigaction()
函数:这是 POSIX 标准推荐的方式,更强大,更可靠。
1 |
|
signum
:要捕获的信号编号。
act
:指向 struct sigaction
结构体,该结构体定义了新的信号处理行为。
oldact
:可选,用于保存旧的信号处理行为。
struct sigaction
结构体:
1 |
|
当你在处理一个信号时,你可能不希望被其他信号打断。信号集(sigset_t
) 就是用来管理一组信号的。通过操作信号集,你可以阻塞(Block) 某些信号,让它们在进程处理完当前任务后才被传递。
sigemptyset()
:初始化一个空的信号集。
sigaddset()
:向信号集中添加一个信号。
sigdelset()
:从信号集中删除一个信号。
sigismember()
:检查一个信号是否在信号集中。
sigprocmask()
sigprocmask()
函数用来设置进程的信号阻塞掩码(Signal Mask)。
1 |
|
how
:指定如何修改信号阻塞掩码,有以下几种:
SIG_BLOCK
:将 set
中的信号添加到阻塞掩码中。
SIG_UNBLOCK
:将 set
中的信号从阻塞掩码中移除。
SIG_SETMASK
:将阻塞掩码设置为 set
。
set
:包含要阻塞或解除阻塞的信号集。
oldset
:可选,用于保存旧的阻塞掩码。
示例:在处理关键代码段时临时阻塞 SIGINT
。
1 |
|
运行这段代码,你会看到在阻塞期间,Ctrl+C
无法终止进程。当解除阻塞后,Ctrl+C
才能触发信号处理函数。
System V IPC 是 Linux 系统中一组更高级、更强大的 IPC 机制,包括消息队列、信号量和共享内存。它们都是基于内核的,需要一个唯一的键值(key)来标识。
消息队列就像一个链表,允许进程向其中添加消息或从中读取消息。
特点:
异步通信:发送进程可以发送消息后立即返回,不需要等待接收进程。
带类型:消息可以带有类型,接收进程可以只接收特定类型的消息。
存储在内核中:即使发送进程结束,消息依然保留在队列中,直到被读取。
使用:
ftok()
:将文件路径和整数转换为一个唯一的 IPC 键值。
msgget()
:创建或获取一个消息队列。
msgsnd()
:发送消息。
msgrcv()
:接收消息。
msgctl()
:控制消息队列,如删除。
信号量主要用于同步,控制对共享资源的访问。它本身不传递数据,而是作为一种“计数器”。
特点:
互斥和同步:常用于实现互斥锁,确保同一时间只有一个进程访问共享资源。
原子操作:信号量的操作(P/V操作)是原子的,不会被中断。
使用:
semget()
:创建或获取一组信号量。
semop()
:对信号量进行操作,如加/减计数。
semctl()
:控制信号量。
共享内存是最高效的 IPC 方式。它允许两个或多个进程共享同一块物理内存。
特点:
最高效:一旦映射到进程的地址空间,读写操作就像访问普通内存一样,不需要内核的参与。
需要同步:由于多个进程同时访问,需要用信号量等机制来同步访问,防止数据竞争。
使用:
shmget()
:创建或获取一个共享内存段。
shmat()
:将共享内存段附加到进程的地址空间。
shmdt()
:将共享内存段从进程的地址空间分离。
shmctl()
:控制共享内存,如删除。
机制 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
管道 | 有亲缘关系的进程 | 简单,易于使用 | 单向,仅限于亲缘进程 |
FIFO | 无亲缘关系的进程 | 可以在文件系统中命名,灵活 | 单向,需要同步,读写时有阻塞 |
消息队列 | 异步通信,少量数据 | 消息带类型,无需同步 | 效率较低,有大小限制 |
信号量 | 进程间同步,互斥 | 用于控制访问,防止竞争 | 不传递数据 |
共享内存 | 大量数据传输 | 最高效,读写速度快 | 必须配合其他同步机制使用 |
如何选择?
如果是父子进程之间少量数据的通信,管道是最佳选择。
如果是两个不相关的进程,且数据量不大,消息队列是一个不错的方案。
如果需要传输大量数据,且对性能要求极高,共享内存是首选,但必须结合信号量或其他同步机制。
如果你只想解决资源访问的同步问题,信号量是专门为此设计的。
了解这些 IPC 机制,就如同掌握了进程之间“沟通”的多种语言。在开发时,选择合适的“语言”能让你的程序更加健壮、高效。现在,你可以尝试用这些机制来解决你遇到的实际问题了!
POSIX IPC(Portable Operating System Interface)是一套新的 IPC 标准,旨在解决 System V IPC 的一些局限性。它提供了一套更统一、更现代的 API,使用文件名作为标识符,而不是 System V 的键值,这使得 IPC 资源的管理更加直观。
与 System V 消息队列类似,但 API 更简洁。
特点:
基于文件:通过 /dev/mqueue
目录下的文件名来标识。
优先级:支持消息优先级,高优先级的消息会被优先处理。
非阻塞模式:可以设置消息队列为非阻塞模式,防止 mq_send
或 mq_receive
阻塞进程。
使用:
mq_open()
:创建或打开一个消息队列。
mq_send()
:发送消息。
mq_receive()
:接收消息。
mq_close()
:关闭消息队列。
mq_unlink()
:删除消息队列。
用于进程间的同步,功能与 System V 信号量类似,但提供了更简单的接口。
特点:
有名信号量:通过一个文件名标识,可以用于非亲缘进程。
无名信号量:常用于线程间的同步,存放在共享内存中,只能用于有亲缘关系的进程。
使用:
sem_open()
:创建或打开有名信号量。
sem_wait()
:原子性地减少信号量计数器,如果为0则阻塞。
sem_post()
:原子性地增加信号量计数器。
sem_close()
:关闭信号量。
sem_unlink()
:删除信号量。
和 System V 共享内存一样,都是最快的 IPC 方式,但 POSIX 版本使用了文件描述符。
特点:
基于文件:通过 shm_open
创建或打开一个共享内存对象,返回一个文件描述符。
内存映射:通过 mmap()
将文件描述符对应的内存映射到进程的地址空间。
使用:
shm_open()
:创建或打开共享内存对象。
ftruncate()
:调整共享内存对象的大小。
mmap()
:将共享内存映射到进程地址空间。
munmap()
:解除映射。
shm_unlink()
:删除共享内存对象。
套接字(Socket) 是一种更通用的 IPC 机制,它不仅可以在同一台机器上的进程间通信,更重要的能力是实现跨网络、不同主机上的进程通信。它是网络通信的基石。
流式套接字(SOCK_STREAM):
特点:提供可靠的、面向连接的、全双工的数据流。
协议:通常使用 TCP 协议。
适用场景:需要保证数据完整性和顺序的通信,如文件传输、HTTP 请求等。
数据报套接字(SOCK_DGRAM):
特点:提供不可靠的、无连接的数据报服务。
协议:通常使用 UDP 协议。
适用场景:对实时性要求高、少量数据、不严格要求可靠性的通信,如在线游戏、DNS 查询等。
原始套接字(SOCK_RAW):
服务器端:
socket()
:创建一个套接字。
bind()
:将套接字与一个 IP 地址和端口号绑定。
listen()
:监听来自客户端的连接请求。
accept()
:接受客户端连接,返回一个新的套接字用于与该客户端通信。
read()
/write()
:通过新的套接字与客户端进行数据交换。
close()
:关闭套接字。
客户端:
socket()
:创建一个套接字。
connect()
:连接到服务器指定的 IP 地址和端口号。
write()
/read()
:向服务器发送数据或接收数据。
close()
:关闭套接字。
除了我们熟悉的网络套接字(AF_INET
),还有一种特别的 IPC 机制:UNIX 域套接字(AF_UNIX)。
UNIX 域套接字:
特点:只在同一台机器上的进程间通信。它使用文件系统路径作为地址,而非 IP 地址和端口。
优点:相比于网络套接字,它不涉及网络协议栈,因此通信效率更高,且安全性更好。
POSIX IPC 是 System V IPC 的现代版本,如果你正在编写新的应用程序,推荐优先使用 POSIX IPC,因为它提供了更简洁、更标准化的接口。
套接字 是最通用的通信方式。如果你需要跨网络通信,套接字是唯一的选择。
如果你的通信只限于同一台机器上,但需要像网络通信一样客户端-服务器模式,且追求更高的效率,那么 UNIX 域套接字 是一个非常好的替代方案。
掌握这些 IPC 机制,你就能够让你的程序在不同维度上进行“对话”,无论是同一台机器上的协作,还是跨越网络边界的协同工作。
创建时间:9月 12, 2025
最后更新:9月 15, 2025
字数统计:5.1k字
预计阅读:19min