进程间通信方式汇总
在操作系统中,进程间通信(Inter-Process Communication, IPC)是指不同进程之间交换数据和信息的机制。以下是几种常见的进程间通信方式:管道(Pipes)、命名管道(Named Pipes)、消息队列(Message Queues)、共享内存(Shared Memory)、信号量(Semaphores)、套接字(Sockets)等。
每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程间通信的实现通常依赖于内核提供的机制。
1. 管道(Pipes)
管道(Pipe)是一种基于内核缓冲区的、字节流式、半双工的进程间通信机制,通常用于具有亲缘关系进程之间的数据传输,其核心思想是“文件 + 读写重定向”。
shell管道是最成功的工程应用,例如:
ps auxf | grep mysql命令行中的管道符号“|”表示将前一个命令的输出作为后一个命令的输入,可以看出管道是半双工的,数据传输是单向的。
匿名管道通过pipe()系统调用创建,返回一对文件描述符,分别是管道的读取端描述符和管道的写入端描述符。匿名管道中的数据传输是先进先出的(FIFO),即先写入管道的数据会先被读取,其缓冲区大小有限,通常只有几十KB,并且没有消息边界的概念,数据以字节流的形式传输。
2. 命名管道(Named Pipes)
命名管道(FIFO)是一种在文件系统中有名字的管道,本质仍是内核管道缓冲区,通过路径名让无亲缘关系的进程获得同一管道对象,实现基于字节流的半双工进程间通信。与匿名管道不同的是:
- 命名管道存在于文件系统中,可以通过路径名进行访问。
- 生命周期独立于创建它的进程,直到显式删除。
- 可以用于无亲缘关系的进程间通信。
shell中创建命名管道的命令是mkfifo,例如:
mkfifo mypipeFIFO在shell中的应用示例: 终端1:
cat mypipe终端2:
echo "Hello, Named Pipe!" > mypipe在终端1会看到输出“Hello, Named Pipe!”。
管道进行进程间通信的局限性:
- 半双工通信:数据只能单向流动。
- 字节流传输:没有消息边界,数据以字节流形式传输。
- 缓冲区有限:通常只有几十KB,可能导致阻塞。
- 内核拷贝开销:数据在内核空间和用户空间之间拷贝,影响性能。
3. 消息队列(Message Queues)
消息队列是保存在内核中的消息链表,是一种基于内核缓冲区的、消息式、双向通信的进程间通信机制。消息队列允许进程以消息为单位进行数据传输,具有以下特点:
- 消息边界:每条消息是独立的,接收进程可以按消息读取。
- 双向通信:进程可以发送和接收消息。
- 异步通信:发送进程不需要等待接收进程接收消息。
消息队列通过msgget()系统调用创建,返回一个消息队列标识符。进程可以使用msgsnd()发送消息,使用msgrcv()接收消息。每条消息包含一个类型字段,接收进程可以根据类型选择性地接收消息。
sender ── msgsnd() ──▶ [ 内核消息队列 ] ── msgrcv() ──▶ receiver消息队列的进程间通信示例:
- 发送进程:
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
struct msgbuf {
long mtype;
char mtext[64];
};
int main() {
key_t key = ftok(".", 'm'); // 生成 key
int qid = msgget(key, IPC_CREAT | 0666);
struct msgbuf msg;
msg.mtype = 1; // 消息类型
strcpy(msg.mtext, "hello message queue");
msgsnd(qid, &msg, sizeof(msg.mtext), 0);
printf("Sender: message sent\n");
return 0;
}- 接收进程:
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
struct msgbuf {
long mtype;
char mtext[64];
};
int main() {
key_t key = ftok(".", 'm');
if (key == -1) {
perror("ftok");
exit(1);
}
// ✅ 确保队列存在
int qid = msgget(key, IPC_CREAT | 0666);
if (qid == -1) {
perror("msgget");
exit(1);
}
struct msgbuf msg;
printf("Receiver: waiting for message...\n");
if (msgrcv(qid, &msg, sizeof(msg.mtext), 1, 0) == -1) {
perror("msgrcv");
exit(1);
}
printf("Receiver got: %s\n", msg.mtext);
return 0;
}消息队列的局限性:
- 内核资源限制:消息队列数量和大小受限,可能导致资源耗尽。
- 性能开销:数据在内核空间和用户空间之间拷贝,影响性能。
4. 共享内存(Shared Memory)
在现代操作系统中,通常采用虚拟内存管理机制,每个进程都有独立的虚拟地址空间,不同进程的虚拟内存映射到不同的物理内存中。共享内存是一种让多个进程直接映射同一块物理内存的 IPC 机制,避免内核拷贝,性能最高,但必须由用户自己保证同步与一致性。
共享内存通过shmget()系统调用创建,返回一个共享内存标识符。进程可以使用shmat()将共享内存映射到自己的地址空间,使用shmdt()解除映射。多个进程可以通过映射同一块共享内存来实现数据交换。
共享内存的简单示例:
- 创建和写入共享内存的进程:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct shm_data {
int ready;
char buf[64];
};
int main() {
key_t key = ftok(".", 's');
int shmid = shmget(key, sizeof(struct shm_data), IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}
struct shm_data *data = shmat(shmid, NULL, 0);
if (data == (void *)-1) {
perror("shmat");
exit(1);
}
data->ready = 0;
strcpy(data->buf, "hello shared memory");
data->ready = 1;
printf("Writer: data written\n");
shmdt(data);
return 0;
}- 读取共享内存的进程:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>union semun arg;
int main() {
key_t key = ftok(".", 's');
int shmid = shmget(key, sizeof(struct shm_data), 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}
struct shm_data *data = shmat(shmid, NULL, 0);
if (data == (void *)-1) {
perror("shmat");
exit(1);
}
// 简单自旋等待(危险但直观)
while (data->ready == 0) {
usleep(1000);
}
printf("Reader got: %s\n", data->buf);
shmdt(data);
return 0;
}上面的方法简单实现了基于共享内存的进程间通信,但存在竞态条件,多个进程同时修改同一个共享内存,很有可能就冲突了,内核不提供同步与一致性保障,必须由用户显式配合信号量、互斥锁或 futex 使用。
5. 信号量(Semaphores)
为了防止多进程竞争共享资源,需要保护机制使得共享资源在同一时刻只能被一个进程访问,**信号量(Semaphore)**是一种用于进程间同步和互斥的机制。信号量可以看作是一个计数器,主要用于实现进程间的互斥与同步,协调多个进程对共享资源的访问顺序,而不是用于缓存进程间通信的数据。
信号量表示资源的数量,控制信号量的方式有两种原子操作:
- P操作(wait/decrement):将信号量的值减1,相减后如果信号量<0,表明资源已被占用,进程需阻塞等待;相减后如果信号量>=0,表明资源可用,进程继续执行。
- V操作(signal/increment):将信号量的值加1,相加后如果信号量<=0,表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量>0,表明没有阻塞进程。
P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的。
如果进程A和进程B互斥访问共享内存,将信号量初始化为1:
- 进程A在访问共享内存前先执行P操作,由于信号量初始值为1,在进程A执行P操作后信号量变为0,表示共享资源可用,进程A可以继续访问共享内存。
- 若此时进程B也访问共享内存,执行了P操作,信号量变为-1,表示资源已被占用,进程B将被阻塞。
- 直到进程A访问完共享内存,执行V操作使得信号量恢复为0,接着就会唤醒阻塞中的线程B,使得进程B可以继续访问共享内存。
- 最后完成共享内存的访问后,执行V操作,信号量恢复到初始值1。
从上面过程可以看出,信号量初始化为1,代表了互斥信号量,可以保证共享内存在任何时刻只能被一个进程访问,保护了共享内存。
还有一些进程是相互依赖的,进程A先生产了数据,进程B才能消费数据,这种场景下可以使用同步信号量来实现进程间的同步。
要用信号量来实现多进程同步的方式,初始化信号量为0:
- 如果进程B比进程A先执行,那么执行P操作时,信号量变为-1,进程B将被阻塞,等待进程A生产数据。
- 当进程A生产完数据后,执行V操作,信号量变为0,唤醒阻塞中的进程B,使得进程B可以继续执行,消费数据。
- 最后,进程B被唤醒,进程A已经生产了数据,进程B执行消费数据的操作。
信号量初始化为0,代表了同步信号量,可以保证进程A在进程B只前执行。
信号量通过semget()系统调用创建,返回一个信号量标识符。进程可以使用semop()执行P和V操作。信号量还可以通过semctl()进行控制和管理。
信号量控制多进程访问共享资源的示例:
定义头文件shm_data.h:
#ifndef SHM_DATA_H
#define SHM_DATA_H
struct shm_data {
int counter;
};
#endif写信号量初始化代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include "shm_data.h"
union semun {
int val;
};
/* P 操作 */
void sem_p(int semid) {
struct sembuf op = {0, -1, SEM_UNDO};
semop(semid, &op, 1);
}
/* V 操作 */
void sem_v(int semid) {
struct sembuf op = {0, +1, SEM_UNDO};
semop(semid, &op, 1);
}
int main() {
key_t key = ftok(".", 'x');
/* 1. 创建共享内存 */
int shmid = shmget(key, sizeof(struct shm_data),
IPC_CREAT | 0666);
if (shmid < 0) {
perror("shmget");
exit(1);
}
struct shm_data *data =
(struct shm_data *)shmat(shmid, NULL, 0);
/* 2. 创建信号量 */
int semid = semget(key, 1, IPC_CREAT | 0666);
if (semid < 0) {
perror("semget");
exit(1);
}
/* 3. 初始化信号量(只需一次) */
union semun arg;
arg.val = 1;
semctl(semid, 0, SETVAL, arg);
/* 4. 写共享内存 */
sem_p(semid);
data->counter += 1;
printf("Writer: counter = %d\n", data->counter);
sem_v(semid);
shmdt(data);
return 0;
}读信号量初始化代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include "shm_data.h"
/* P 操作 */
void sem_p(int semid) {
struct sembuf op = {0, -1, SEM_UNDO};
semop(semid, &op, 1);
}
/* V 操作 */
void sem_v(int semid) {
struct sembuf op = {0, +1, SEM_UNDO};
semop(semid, &op, 1);
}
int main() {
key_t key = ftok(".", 'x');
/* 1. 获取共享内存 */
int shmid = shmget(key, sizeof(struct shm_data), 0666);
if (shmid < 0) {
perror("shmget");
exit(1);
}
struct shm_data *data =
(struct shm_data *)shmat(shmid, NULL, 0);
/* 2. 获取信号量 */
int semid = semget(key, 1, 0666);
if (semid < 0) {
perror("semget");
exit(1);
}
/* 3. 读共享内存 */
sem_p(semid);
printf("Reader: counter = %d\n", data->counter);
sem_v(semid);
shmdt(data);
return 0;
}6. 信号(Signals)
上面介绍的进程间通信都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用信号的方式来通知进程。在Linux操作系统中,为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过kill -l查看所有信号:
kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX当在shell终端执行程序时,可以通过键盘输入某些组合键给进程发送信号,例如:
Ctrl+C发送SIGINT信号,通常用于终止进程。Ctrl+Z发送SIGTSTP信号,通常用于暂停进程。
如果程序在后台运行,可以使用kill命令发送信号给进程,例如:
kill -9 <pid>表示发送SIGKILL信号强制终止进程。
信号是内核向进程发送的一种异步通知机制,处理方式有三种:
- 默认处理:每个信号都有一个默认的处理方式,例如终止进程、忽略信号等。
- 忽略信号:进程可以选择忽略某些信号。
- 自定义处理函数:进程可以注册一个信号处理函数,当接收到信号时执行该函数。可以使用
signal()或sigaction()函数注册信号处理函数。
6. 套接字(Socket)
前面提到的所有进程间通信方式都适用于同一台主机上的进程通信,而套接字(Socket)是一种支持不同主机之间进程通信的机制,可以实现跨网络的进程间通信。套接字提供了一种通用的接口,支持多种协议(如TCP、UDP等),可以用于本地进程间通信(UNIX域套接字)和远程进程间通信(网络套接字)。
创建socket的系统调用:
int socket(int domain, int type, int protocol);domain:指定通信域,如AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX/AF_LOCAL(本地通信)等。type:指定套接字类型,如SOCK_STREAM表示字节流(面向连接的TCP)、SOCK_DGRAM表示数据报(无连接的UDP)、SOCK_RAW表示原始套接字等。protocol:通常设为0,表示使用默认协议。
根据创建的socket类型不同,通信方式也不同:
- 面向连接的字节流通信(如TCP):
socket域和类型指定为AF_INET和SOCK_STREAM,需要先建立连接,然后进行数据传输,适用于可靠的数据传输场景。 - 无连接的数据报通信(如UDP):
socket域和类型指定为AF_INET和SOCK_DGRAM,不需要建立连接,直接发送和接收数据报,适用于对实时性要求高但允许丢包的场景。 - 本地进程间通信(UNIX域套接字):
socket域指定为AF_UNIX/AF_LOCAL,适用于同一台主机上的进程间通信,性能较高。也分为面向连接和无连接两种类型。
参考资料
https://xiaolincoding.com/os/4_process/process_commu.html
ChatGPT