052-进程间通信
进程间通信(Inter-Process Communication, IPC)
进程间通信有哪些方式
- 共享内存
- 管道
- 消息队列
- 信号量
- 套接字
进程间通信(IPC)的连接可以通过多种机制建立,每种机制都有其特定的用途和实现方式。以下是几种常见的 IPC 机制及其连接建立方式:
1. 管道(Pipe)
匿名管道
- 只能在父子进程之间使用
- 通过
pipe()
系统调用创建 - 创建后返回两个文件描述符:一个用于读,一个用于写
1
2
3
4
5int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}命名管道(FIFO)
- 可以在无亲缘关系的进程间使用
- 通过
mkfifo()
创建一个特殊文件 - 进程通过打开这个文件进行读写
1
mkfifo("/tmp/myfifo", 0666);
2. 消息队列
通过
msgget()
创建或获取一个消息队列标识符使用
msgsnd()
和msgrcv()
进行消息发送和接收使用
msgctl()
进行消息队列控制,如删除消息队列或修改消息队列的权限消息队列是基于消息的,消息队列是保存在内核中的链表,消息队列中每个消息都有一个类型和优先级
支持多进程并发访问
1
2key_t key = ftok("progfile", 65);
int msgid = msgget(key, 0666 | IPC_CREAT);
3. 共享内存
使用共享内存的很重要原因是性能,内核为需要通信的进程分配了一块内存,进程可以直接访问这块内存,不需要内核介入
当进程不再希望共享内存时,通过
shmdt()
将共享内存段从进程的地址空间分离,取消共享内存段与进程的地址空间的映射,只影响当前进程,其他正在使用共享内存的进程不受影响通过
shmget()
创建或获取一个共享内存段使用
shmat()
将共享内存段附加到进程的地址空间1
2
3key_t key = ftok("shmfile", 65);
int shmid = shmget(key, 1024, 0666 | IPC_CREAT);
char *str = (char*) shmat(shmid, (void*)0, 0);
4. 信号量
信号量是用于进程间同步的机制
信号量的主要操作是两个原语:
P
操作和V
操作,计数器值只能在 0 和 1 之间变化P
操作:计数器减一,如果计数器为 0,无法减一,则阻塞,直至计数器大于 0,减一成功V
操作:计数器加一,如果计数器超过 1,则无法加一,忽略本次操作
通过
semget()
创建或获取一个信号量集使用
semop()
进行信号量操作1
2key_t key = ftok("semfile", 65);
int semid = semget(key, 1, 0666 | IPC_CREAT);
5. 套接字(Sockets)
本地套接字(UNIX 域套接字)
- 通过
socket()
创建 - 使用
bind()
、listen()
、accept()
建立连接
1
2
3
4
5
6int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 5);- 通过
网络套接字
- 用于不同主机间的通信
- 通过
socket()
创建 - 使用
connect()
、bind()
、listen()
、accept()
建立连接
1
2
3
4
5
6
7int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 5);
6. 信号
通过
kill()
发送信号进程通过
signal()
或sigaction()
设置信号处理函数Linux 常规信号有 32 个,信号值范围为 1-31,信号值为 0 的信号通常用于进程间同步;POSIX 引入了 32 个信号(实时信号),信号值范围为 32-64
使用信号,一个进程可以随时通知另一个进程某个事件的发生,并且接收者不需要阻塞等待该事件,内核会帮助其切换到对应的处理函数中响应信号事件
1
2signal(SIGUSR1, handler_function);
kill(pid, SIGUSR1);
信号表
在终端,可通过kill -l
查看所有的 signal 信号。
1 | :~$ kill -l |
请给出一个管道的设计,使其支持双向通信
实现双向通信的管道可以通过创建两个单向管道来完成,一个用于从 A 到 B 的通信,另一个用于从 B 到 A 的通信。以下是实现双向管道通信的设计方案:
1. 管道结构设计
- 管道 A->B: 一个单向管道,负责从进程 A 向进程 B 发送数据。
- 管道 B->A: 另一个单向管道,负责从进程 B 向进程 A 发送数据。
2. 关键步骤
假设使用 C 语言和 POSIX 标准管道 (pipe
):
(1) 创建管道
每个管道由一对文件描述符表示:fd[0]
用于读,fd[1]
用于写。
1 | int pipeAB[2]; // A->B |
(2) 在父子进程间通信
- 父进程 A: 关闭
pipeAB[0]
和pipeBA[1]
,仅用于写入到pipeAB
和读取pipeBA
。 - 子进程 B: 关闭
pipeAB[1]
和pipeBA[0]
,仅用于读取pipeAB
和写入到pipeBA
。
1 | pid_t pid = fork(); |
3. 使用 select
或 poll
支持异步通信
为了避免阻塞,可使用 select
或 poll
对管道的读写操作进行多路复用,检查管道是否有数据可读。
4. 其他方案
除了 POSIX 管道,还可以使用以下方法实现双向通信:
- Socket: 使用
socketpair
创建全双工通信。 - 消息队列: 使用 System V 或 POSIX 消息队列。
- 共享内存: 配合信号量实现双向通信。
- FIFO(命名管道): 通过创建两个命名管道实现双向通信。
选择哪种方式取决于具体的应用场景和需求。如果是本地进程间通信,pipe
和 socketpair
是推荐的简单方案。
选择合适的 IPC 机制
- 管道适合简单的父子进程通信
- 消息队列适合需要消息排序和优先级的场景
- 共享内存适合需要高效大数据量传输的场景
- 信号量常用于进程间同步
- 套接字适合网络通信或复杂的本地通信
每种 IPC 机制都有其特定的应用场景和优缺点,选择时应根据具体需求和系统环境进行权衡。
在进程间通信过程中, 数据和控制流分别是如何传输和转移的
一种常见的通信数据的抽象是消息。消息一般包含消息头和消息体。消息头中包含消息类型、消息长度、消息优先级等控制信息,消息体中包含实际需要传输的数据。
进程间通信的连接是如何建立的
通信过程一般指通信的进程间具体的通信发起、回复、结束的过程。
发送者将要传输的数据内容拷贝到发送者消息上,然后一次设置头部的状态(设置为“准备就绪”等)。
接收者不停地轮询发送者消息的状态信息,当发现消息头部的状态变为“准备就绪”时,就表示发送者发送了一个消息。
发送者一发送完消息,就开始轮询接收者消息的状态信息,当发现接收者消息头部的状态变为“准备就绪”时,就表示接收者接收到了一个消息。
接收者在读取发送者的消息后,处理请求,并在接受者消息上准备返回结果。
发送者不停地轮询接收者消息的状态信息,当发现接收者消息头部的状态变为“返回结果”时,就表示接收者处理完了消息,并准备返回结果。
发送者读取接收者消息中的返回结果,并从消息中删除返回结果。
直接通信和间接通信
直接通信:发送者和接收者都知道彼此的标识符。比如进程号。
间接通信:发送者和接收者不知道彼此的标识符,需要一个中间实体(如消息队列、共享内存、管道等)来传递消息。
什么是超时机制, 为什么需要超时机制
超时机制是进程间通信中的一种机制,用于处理通信过程中可能出现的延迟或失败情况。允许发送者/接收者设置一个超时时间,如果在这个时间内没有收到回复,则认为通信失败。由操作系统内核结束此次 IPC 调用,返回一个超时的错误。
一个进程如何找到另一个进程提供的服务
一个进程找到另一个进程提供的服务,可以通过以下几种常见的通信机制实现,具体选择取决于操作系统环境和应用场景:
1. 使用命名管道 (FIFO)
- 适用场景: 本地进程间通信,简单高效。
- 实现步骤:
- 服务端创建一个命名管道(FIFO)。
- 客户端打开该命名管道进行读写。
- 使用文件路径标识管道。
1 | // 服务端 |
2. 使用套接字(Socket)
- 适用场景: 本地或分布式场景,通过网络通信。
- 实现步骤:
- 服务端监听一个固定的地址和端口。
- 客户端通过地址和端口连接到服务端。
1 | // 服务端 |
3. 使用共享内存
- 适用场景: 本地进程间通信,数据量大且需要高效。
- 实现步骤:
- 服务端创建一个共享内存段并设置标识符。
- 客户端通过标识符访问共享内存。
- 使用信号量或互斥锁同步访问。
1 | // 服务端 |
4. 使用系统总线 (如 D-Bus 或 ZeroMQ)
- 适用场景: 复杂系统,支持服务发现和消息路由。
- 实现步骤:
- 服务端注册服务到消息总线。
- 客户端通过总线查找服务。
5. 使用文件描述符或信号
- 适用场景: 简单标识服务状态。
- 实现方式:
- 服务端创建特定文件(如 Unix 域套接字文件)。
- 客户端通过文件路径定位服务。
6. 使用注册表或配置文件
- 适用场景: 服务动态发布或客户端动态查找。
- 实现方式:
- 服务端将自身信息(如 PID、套接字地址)写入文件或注册表。
- 客户端读取该文件查找服务。
1 | // 服务端 |
7. 服务发现协议
- 适用场景: 动态服务发现,分布式系统。
- 实现方式:
- 使用诸如
mDNS
、Consul
等服务发现工具。 - 服务端注册到发现工具,客户端通过工具查找服务。
- 使用诸如
综合建议
- 本地简单通信: 命名管道、共享内存。
- 本地或远程灵活通信: Socket。
- 动态服务发现: 服务总线或专用工具(如 Consul)。