Linux进程间通信(IPC)详解:信号、管道、共享内存与信号量
1. 信号Signal信号是Linux系统中用于通知进程某个事件已发生的异步通信机制。信号可以由内核、其他进程或进程自身发送。1.1 常见信号SIGINT终端中断信号通常由CtrlC产生默认行为是终止进程。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 无名管道特性有名管道无名管道名称有名存在文件系统中不在文件系统中进程关系任意进程有权限父子、兄弟进程生命周期随文件系统的销毁而销毁随进程的销毁而销毁创建方式mkfifopipe()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创建的共享内存空间的大小创建新共享空间时该参数填正整数获取已有共享空间时该参数可以填0shmflg标志位用来控制函数行为创建标志IPC_CREAT如果key不存在则创建IPC_EXCL与IPC_CREAT配合使用如果key已经创建则报错并设置错误原因给errno权限控制需要指定权限位0666、0777、06003.3 shmat函数详解void *shmat(int shmid, const void *shmaddr, int shmflg);作用将一个已存在的共享内存段映射到进程的地址空间中shmid共享内存段的标识符通常是shmget函数的返回值shmaddr指定共享内存段要附加到进程地址空间的哪个地址NULL让系统内核自动选择一块合适的、未使用的地址来附加共享内存段非NULL填具体地址系统会将共享内存段附加到指定位置如果指定位置不能使用则报错附加失败shmflg控制附加操作控制位0默认权限根据shmget创建时的权限进行读写SHM_RDONLY以只读的方式附加共享内存段如果该进程尝试写入则引发段错误SHM_EXEC允许在共享内存段上执行代码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可设置为0semflg标志位控制信号量的创建/获取行为IPC_CREAT若对应的信号量集不存在则创建新集若已存在则返回其IDIPC_EXCL与IPC_CREAT组合使用若信号量集已存在则创建失败确保获得新资源权限例如0600semctl函数详解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运行命令./test6.2 运行结果分析程序运行时会交替输出main run和fun run因为主线程和子线程并发执行main run fun run main run fun run fun run fun run fun run sfun over主线程执行2次循环后通过pthread_join等待子线程结束最后打印子线程返回的字符串。7. IPC方式总结Linux系统提供了多种进程间通信IPC方式每种方式都有其适用场景IPC方式特点适用场景管道半双工亲缘进程间通信父子进程间简单数据传递有名管道有文件名任意进程可访问无亲缘关系进程间通信共享内存最高效直接内存访问需要高速数据交换的场景信号量进程同步与互斥控制对共享资源的访问消息队列异步通信支持消息类型需要可靠消息传递的系统套接字网络通信跨主机网络应用程序7.1 关键概念回顾P,V操作信号量的基本原子操作用于进程同步临界资源同一时刻只允许一个进程访问的资源临界区访问临界资源的代码段全双工/半双工通信方向性共享内存为全双工管道为半双工