Linux进程间通信(IPC)详解:信号、管道、共享内存与信号量
1. 信号(Signal)
信号是Linux系统中用于通知进程某个事件已发生的异步通信机制。信号可以由内核、其他进程或进程自身发送。
1.1 常见信号
- SIGINT:终端中断信号,通常由Ctrl+C产生,默认行为是终止进程。
- SIGKILL:强制终止信号,不能被捕获、忽略或阻塞。
- SIGTERM:终止信号,请求进程正常退出。
- SIGSTOP:停止信号,暂停进程执行。
- SIGCONT:继续信号,恢复被暂停的进程。
1.2 信号处理函数
进程可以通过以下方式处理信号:
- SIG_IGN:忽略该信号。
- SIG_DFL:恢复信号的默认行为。
- 自定义处理函数:使用
signal()或sigaction()注册自定义信号处理函数。
#include <signal.h> #include <stdio.h> void signal_handler(int sig) { printf("Received signal: %d\n", sig); } int main() { // 注册SIGINT信号处理函数 signal(SIGINT, signal_handler); while(1) { // 主循环 } return 0; }2. 管道(Pipe)
管道是Linux进程间通信的一种基本方式,数据在管道中单向流动。
2.1 管道特性
- 管道是单向的,数据只能从左到右流动
- 管道会创建子进程来执行命令
- 管道中的命令是并行执行的
- 管道可以串联形成管道链
2.2 有名管道(Named Pipe/FIFO)
有名管道通过mkfifo命令或mkfifo()系统调用创建。
特性:
- 有可见的文件名(存在于文件系统中)
- 可以被任意进程(需要有权限)访问
- 数据传输遵循先进先出原则
- 本质上是内存中的特殊文件,不占用磁盘空间
- 关闭所有引用它的进程后,管道文件依然存在(需要手动删除)
测试示例:
# 创建有名管道 mkfifo mypipe 读端:持续监听管道 cat < mypipe 或 while true; do cat < mypipe; done 写端:多次写入内容 echo "Hello" > mypipe echo "World" > mypipe#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { // 创建有名管道 mkfifo("mypipe", 0666); // 写端进程 int fd = open("mypipe", O_WRONLY); char buffer[] = "Hello from named pipe"; write(fd, buffer, strlen(buffer)); close(fd); return 0; }2.3 无名管道(Anonymous Pipe)
无名管道通过pipe()系统调用创建。
特性:
- 无名称,仅通过文件描述符访问
- 仅支持亲缘进程:父子、兄弟进程
- 创建方式:
pipe()系统调用 - 生命周期:随进程结束而销毁
创建示例:
#include <stdio.h> #include <unistd.h> #include <string.h> int main() { int pipefd[2]; char buffer[100]; // 创建管道 if (pipe(pipefd) == -1) { perror("pipe"); return 1; } pid_t pid = fork(); if (pid == 0) { // 子进程:写端 close(pipefd[0]); // 关闭读端 char *msg = "Hello from child"; write(pipefd[1], msg, strlen(msg)); close(pipefd[1]); } else { // 父进程:读端 close(pipefd[1]); // 关闭写端 read(pipefd[0], buffer, sizeof(buffer)); printf("Parent received: %s\n", buffer); close(pipefd[0]); } return 0; }2.4 有名管道 vs 无名管道
| 特性 | 有名管道 | 无名管道 |
|---|---|---|
| 名称 | 有名(存在文件系统中) | 不在文件系统中 |
| 进程关系 | 任意进程(有权限) | 父子、兄弟进程 |
| 生命周期 | 随文件系统的销毁而销毁 | 随进程的销毁而销毁 |
| 创建方式 | mkfifo | pipe() |
2.5 管道注意事项
- 避免管道破裂:读端已关闭的情况下,写端还在输出数据,会触发SIGPIPE信号,终止写端进程
- 数据无边界:管道是字节流数据,若按照信息的格式一条一条发送,需要自定义信息格式(长度+数据),避免多读少读
- 缓冲区阻塞问题:读空、写满会阻塞进程,需要使用
fcntl()设置非阻塞属性进行处理 - 资源泄露问题:进程退出前要关闭管道描述符,否则可能导致缓冲区中的数据丢失
2.6 通信方式
- 单工:单向性,确定通信方式后固定,不能相互传输数据
- 全双工:双向性,同一时刻,双方可以相互传输数据
- 半双工:双向性,同一时刻,只能一方给另一方传输数据
管道的通信方式为半双工。
3. 共享内存(Shared Memory)
共享内存允许多个进程访问同一块内存区域,是最高效的IPC方式。
3.1 共享内存函数
#include <sys/ipc.h> #include <sys/shm.h> // 创建或获取共享内存 int shmget(key_t key, size_t size, int shmflg); // 将共享内存附加到进程地址空间 void *shmat(int shmid, const void *shmaddr, int shmflg); // 分离共享内存 int shmdt(const void *shmaddr); // 控制共享内存 int shmctl(int shmid, int cmd, struct shmid_ds *buf);3.2 shmget函数详解
int shmget(key_t key, size_t size, int shmflg);
- 返回值:-1表示共享内存创建失败;正数表示该段的标识符
- key:共享内存段的唯一标识
- size:创建的共享内存空间的大小
- 创建新共享空间时,该参数填正整数
- 获取已有共享空间时,该参数可以填0
- shmflg:标志位,用来控制函数行为
- 创建标志:
IPC_CREAT:如果key不存在,则创建IPC_EXCL:与IPC_CREAT配合使用,如果key已经创建则报错并设置错误原因给errno
- 权限控制:需要指定权限位(0666、0777、0600)
- 创建标志:
3.3 shmat函数详解
void *shmat(int shmid, const void *shmaddr, int shmflg);
作用:将一个已存在的共享内存段映射到进程的地址空间中
- shmid:共享内存段的标识符,通常是
shmget函数的返回值 - shmaddr:指定共享内存段要附加到进程地址空间的哪个地址
NULL:让系统内核自动选择一块合适的、未使用的地址来附加共享内存段- 非NULL(填具体地址):系统会将共享内存段附加到指定位置,如果指定位置不能使用,则报错,附加失败
- shmflg:控制附加操作控制位
- 0:默认权限,根据
shmget创建时的权限进行读写 SHM_RDONLY:以只读的方式附加共享内存段,如果该进程尝试写入,则引发段错误SHM_EXEC:允许在共享内存段上执行代码
- 0:默认权限,根据
3.4 共享内存示例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ipc.h> #include <sys/shm.h> #include <unistd.h> #define SHM_SIZE 1024 int main() { key_t key = ftok(".", 'R'); int shmid; char *data; // 创建共享内存 if ((shmid = shmget(key, SHM_SIZE, 0644 | IPC_CREAT)) == -1) { perror("shmget"); exit(1); } // 附加到进程地址空间 data = shmat(shmid, NULL, 0); if (data == (char *)(-1)) { perror("shmat"); exit(1); } pid_t pid = fork(); if (pid == 0) { // 子进程:写入数据 printf("Child writing to shared memory...\n"); strcpy(data, "Hello from child process"); printf("Child wrote: %s\n", data); } else { // 父进程:读取数据 sleep(1); // 等待子进程写入 printf("Parent reading from shared memory...\n"); printf("Parent read: %s\n", data); // 分离共享内存 shmdt(data); // 删除共享内存 shmctl(shmid, IPC_RMID, NULL); } return 0; }练习:设置一个进程为读取共享内存上的数值,持续读取;另外一个进程可以持续写入,直到输入"end"后两个进程都结束。共享内存的工作方式为全双工。
4. 信号量(Semaphore)
信号量是操作系统用于进程间同步与互斥的关键机制,解决多个进程对共享资源的竞争问题,避免数据冲突或不一致。
4.1 信号量基本概念
- 本质:一个受保护的整数变量,其值反映了共享资源的可用数量
- 原子操作:对信号量的修改(加减)是不可被中断的操作,避免多进程并发修改导致数据不一致
- 阻塞机制:当信号量的值为0时,试图获取该信号量的进程会被阻塞,直到有其他进程释放资源
4.2 信号量类型
- 二值信号量:值只能是0和1,用于实现互斥(类似锁,一次只允许一个进程运行)
- 计数信号量:值可以是任意非负数,用于控制对多个相同资源的并发访问。假设信号量值为3,最大可支持3个进程同时运行
4.3 P、V原语
信号量的行为由两个基本操作定义(称为"原语",确保原子性):
- P操作(获取资源):将信号量减一
- 若结果≥0,进程可继续执行
- 若结果<0,进程被阻塞,放入信号量的等待队列
- V操作(释放资源):将信号量加一
- 不会阻塞,永远释放资源
- 若结果≤0,从等待队列中唤醒一个进程,使其继续执行
4.4 相关术语
- 临界资源:同一时刻,只允许一个进程访问的资源
- 临界区:访问临界资源的代码段
4.5 信号量函数
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> // 创建或获取信号量集 int semget(key_t key, int nsems, int semflg); // 控制信号量集 int semctl(int semid, int semnum, int cmd, ...); // 执行信号量操作 int semop(int semid, struct sembuf *sops, size_t nsops);semget函数详解
int semget(key_t key, int nsems, int semflg);
- key:用于信号量集的唯一性,确保不同进程能通过相同的key访问同一信号量集
- nsems:信号量的数量(要创建的信号量集中包含的信号量的个数)
- 若是创建新信号量集,则nsems必须大于0
- 若是获取已存在的信号量集,则nsems可设置为0
- semflg(标志位):控制信号量的创建/获取行为
IPC_CREAT:若对应的信号量集不存在,则创建新集;若已存在,则返回其IDIPC_EXCL:与IPC_CREAT组合使用,若信号量集已存在,则创建失败(确保获得新资源)- 权限:例如0600
semctl函数详解
int semctl(int semid, int semnum, int cmd, ...);
- semid:信号量集的标识符(由
semget()返回),指定要操作的信号量集 - semnum:信号量在集中的索引(从0开始),指定要操作的单个信号量
- 若对整个信号量集进行操作(如
IPC_RMID),则此参数忽略
- 若对整个信号量集进行操作(如
- cmd:要执行的控制命令,决定
semctl的行为SETVAL:设置semnum指定的信号量的初始值(需配合第四个参数传递)GETVAL:获取semnum指定的信号量的当前值(返回值为信号量的值)IPC_RMID:删除整个信号量集(所有进程都无法访问)IPC_STAT:获取信号量集的状态信息,存入semid_ds结构体(创建时间,权限)IPC_SET:设置信号量集的属性,需要通过semid_
5. 消息队列(Message Queue)
消息队列是Linux系统中另一种进程间通信(IPC)机制,它允许进程通过发送和接收消息来交换数据。消息队列由内核维护,具有以下特点:
5.1 消息队列基本概念
- 内核维护:消息队列由操作系统内核创建和管理
- 消息结构:每条消息通常包含类型字段和实际数据
- 先进先出:消息按照发送顺序排队,但可以根据类型选择性接收
- 异步通信:发送者和接收者不需要同时存在
5.2 消息队列数据结构
消息队列中的消息通常采用以下结构:
struct mess { long type; // 消息类型,用于标识消息类别(如1,2,3...) char data[1024]; // 消息数据,可根据需要自定义 };5.3 消息队列相关函数
#include <sys/msg.h> // 创建或获取消息队列 int msgget(key_t key, int msgflg); // 发送消息到消息队列 int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); // 从消息队列接收消息 ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); // 控制消息队列 int msgctl(int msqid, int cmd, struct msqid_ds *buf);5.4 消息队列示例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ipc.h> #include <sys/msg.h> struct message { long msg_type; char msg_text[100]; }; int main() { key_t key; int msgid; struct message msg; // 生成key key = ftok("progfile", 65); // 创建消息队列 msgid = msgget(key, 0666 | IPC_CREAT); // 发送消息 msg.msg_type = 1; strcpy(msg.msg_text, "Hello from message queue"); msgsnd(msgid, &msg, sizeof(msg), 0); printf("Message sent: %s\n", msg.msg_text); // 接收消息 msgrcv(msgid, &msg, sizeof(msg), 1, 0); printf("Message received: %s\n", msg.msg_text); // 删除消息队列 msgctl(msgid, IPC_RMID, NULL); return 0; }6. 线程示例:pthread基本使用
以下是一个使用pthread库创建线程的完整示例,演示了线程的创建、执行和回收:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> void* fun(void* arg) { for(int i = 0; i < 5; i++) { printf("fun run\n"); sleep(1); } pthread_exit("fun over\n"); } int main() { pthread_t id; pthread_create(&id, NULL, fun, NULL); for(int i = 0; i < 2; i++) { printf("main run\n"); sleep(1); } char* s = NULL; pthread_join(id, (void**)&s); // 阻塞等待线程结束 printf("s=%s\n", s); exit(0); }6.1 代码解析
- pthread_create:创建新线程,参数包括线程ID、属性、线程函数和参数
- pthread_exit:线程退出,可以返回一个值给主线程
- pthread_join:主线程等待子线程结束,并获取线程的返回值
- 编译命令:
gcc test.c -o test -lpthread - 运行命令:
./test
6.2 运行结果分析
程序运行时会交替输出"main run"和"fun run",因为主线程和子线程并发执行:
main run fun run main run fun run fun run fun run fun run s=fun over主线程执行2次循环后,通过pthread_join等待子线程结束,最后打印子线程返回的字符串。
7. IPC方式总结
Linux系统提供了多种进程间通信(IPC)方式,每种方式都有其适用场景:
| IPC方式 | 特点 | 适用场景 |
|---|---|---|
| 管道 | 半双工,亲缘进程间通信 | 父子进程间简单数据传递 |
| 有名管道 | 有文件名,任意进程可访问 | 无亲缘关系进程间通信 |
| 共享内存 | 最高效,直接内存访问 | 需要高速数据交换的场景 |
| 信号量 | 进程同步与互斥 | 控制对共享资源的访问 |
| 消息队列 | 异步通信,支持消息类型 | 需要可靠消息传递的系统 |
| 套接字 | 网络通信,跨主机 | 网络应用程序 |
7.1 关键概念回顾
- P,V操作:信号量的基本原子操作,用于进程同步
- 临界资源:同一时刻,只允许一个进程访问的资源
- 临界区:访问临界资源的代码段
- 全双工/半双工:通信方向性,共享内存为全双工,管道为半双工