进程间通信的方式有什么
进程间通信 (IPC) 是指两个进程之间进行信息交流的机制,它不仅可以发生在同一主机的两个进程之间,也可以发生在不同主机的两个进程之间。UNIX 系统提供了多种进程间通信方式,主要包括管道 (pipe)、信号量 (semaphore)、共享内存 (shared memory)、消息队列 (message queue) 和套接字 (socket)。
管道
管道是最早出现且使用频繁的进程间通信机制之一。它允许一个进程的输出作为另一个进程的输入,实现两个进程之间的数据流传输。管道可以是无名管道(pipe)或命名管道(named pipe)。
实现原理:
-
无名管道 (pipe):
popen()和pclose():popen()函数通过创建一个双向管道,然后调用fork()产生子进程,并在子进程中通过/bin/sh -c来执行参数command的命令。这样,调用进程可以读取被调用进程的输出或向其输入数据。open_mode参数决定数据流向,r用于读取被调用进程的输出,w用于向被调用进程写入数据。pclose()函数用于关闭管道,它会阻塞直到被调用进程退出。早期popen()实现的管道是单向的,但现代一些实现(如 FreeBSD 2.2.6 引入)支持双向管道 (r+模式)。pipe()系统调用:pipe()系统调用创建一个无名管道,并返回两个文件描述符file_descriptor[0]和file_descriptor[1]。写入file_descriptor[1]的数据可以从file_descriptor[0]读取,两者遵循 FIFO (First-In, First-Out) 原则。这种底层管道通常在fork()调用之后使用,实现父子进程之间的通信:父进程可以向管道一端写入数据,子进程从另一端读取,反之亦然。父子进程通过共享由pipe()创建的文件描述符来实现通信。
-
命名管道 (named pipe, FIFO):
- 定义: 命名管道是一种特殊的文件,可以在文件系统中存在,因此它允许不相关的进程之间进行通信。
- 实现原理: 用户可以使用
mkfifo <pipe-name>命令创建命名管道。一旦创建,命名管道就表现得像一个普通文件,不同进程可以通过打开、读取和写入这个文件来交换数据。当一个进程打开命名管道进行读取时,如果管道中没有数据,它会阻塞直到有数据写入。同样,写入进程也可能在管道满时阻塞。操作系统维护管道的内部缓冲区来存储数据。命名管道的API与普通文件操作类似,但本文未详细给出其函数原型。
信号量
信号量是由 Dijkstra 提出的,用于解决并发程序中对共享资源(临界资源)的访问控制问题。它通过简单的 P (wait/decrement) 和 V (signal/increment) 操作来协调多个进程或线程对共享资源的访问,防止数据错误。最简单的信号量是一个只能取 0 或 1 的变量(二值信号量)。
实现原理:
- P 操作 (wait): 当进程要进入临界区时,首先调用 P(s)。如果信号量
s的值大于 0,则将其减 1,并允许进程进入临界区。如果s的值等于 0,则表示临界区已被占用,进程会被挂起,直到s的值变为大于 0。 - V 操作 (signal): 当进程完成对临界区的访问后,调用 V(s)。如果此时有进程因为执行 P(s) 而被挂起,则恢复该进程的运行。否则,将信号量
s的值加 1。 - UNIX API: UNIX 提供了
semctl(信号量控制)、semget(获取信号量集标识符) 和semop(信号量操作) 等函数来管理和操作信号量。这些函数通常操作的是一个信号量集,而不是单个信号量。通过这些原语操作,操作系统可以确保在任何给定时刻只有一个进程能够访问共享资源(对于二值信号量)。
共享内存
共享内存是一种允许不同进程将同一块物理内存地址连接到它们各自的虚拟内存空间中的机制。一旦连接,这些进程就可以直接读写这块内存区域,从而实现高速的进程间通信。
实现原理:
- 内存映射: 操作系统提供机制(如
shmget)来创建一块特殊的内存区域作为共享内存。然后,通过shmat函数,不同的进程可以将这块共享内存映射到它们自己的地址空间中。 - 直接访问: 一旦内存被映射,进程就可以像访问自己的私有内存一样访问这块共享内存。任何一个进程对共享内存的写入,其他进程都能立即看到。
- 缺乏同步: 共享内存本身不提供同步机制。这意味着,如果多个进程同时尝试读写共享内存,可能会导致竞态条件和数据不一致。因此,在使用共享内存时,程序员通常需要结合其他 IPC 机制(如信号量或互斥锁)来维护对共享内存的读写同步。
- UNIX API: UNIX 提供了
shmget(创建或获取共享内存段)、shmat(将共享内存连接到进程地址空间)、shmctl(控制共享内存) 和shmdt(将共享内存与进程地址空间分离) 等函数。
消息队列
消息队列是一种允许进程通过发送和接收具有特定格式的消息来进行通信的机制。它类似于命名管道,但提供了更灵活的消息处理能力。
实现原理:
- 消息存储: 消息队列由操作系统维护,它是一个链表结构,用于存储进程发送的消息。
- 消息类型: 消息队列中的每条消息通常都带有一个类型字段。这使得接收进程可以选择性地接收特定类型的消息,而不是严格按照 FIFO 顺序接收所有消息。
- 发送与接收: 进程通过
msgsnd函数将消息发送到队列中,通过msgrcv函数从队列中接收消息。 - UNIX API: UNIX 提供了
msgget(创建或获取消息队列)、msgsnd(发送消息)、msgrcv(接收消息) 和msgctl(控制消息队列) 等函数。
套接字
套接字是一种用于在网络中进行进程间通信的机制,它不仅可以用于不同主机上的进程通信,也可以用于同一主机上的进程通信。它是网络通信的基础。
实现原理:
- 网络协议栈: 对于跨主机的通信,套接字通过建立传输层连接(如 TCP 或 UDP)来进行数据传输。
- TCP (可靠传输):
- 服务端: 调用
socket()创建套接字 ->bind()绑定 IP/Port ->listen()监听端口 ->accept()等待客户端连接。 - 客户端: 调用
socket()创建套接字 ->connect()发起 TCP 握手与服务端建立连接。 - 通信: 连接建立后,客户端和服务端都可以使用
send()和recv()进行数据的发送和接收。TCP 是全双工的,支持双向数据流。 - 结束: 通信完成后,调用
close()断开连接。
- 服务端: 调用
- UDP (无连接传输): UDP 套接字不建立连接,直接发送数据报。
- TCP (可靠传输):
- UNIX Domain Socket (本地套接字):
- 定义: 对于同一主机上的两个进程通信,可以使用 UNIX Domain Socket。它不需要经过网络协议栈,直接在操作系统内部进行数据传输。
- 实现原理: UNIX Domain Socket 使用与普通套接字几乎相同的 API,但套接字类型设置为
AF_UNIX(或AF_LOCAL)。它会在文件系统上创建一个.sock文件,不同进程通过读写这个.sock文件来实现通信,而不是通过网络接口。这种方式比通过 LoopBack 地址 (127.0.0.1) 进行 TCP/IP 通信更高效,因为它避免了网络协议栈的开销。