Linux 进程间通信(IPC) 一、进程间通信机制IPC1.1 无名管道pipe1. 核心特点无名管道是内核中的一段缓冲区只能用于有亲缘关系的进程比如父子进程、兄弟进程没有对应的磁盘文件进程结束后管道会自动消失。数据只能单向流动属于“半双工”通信即同一时间只能实现一个方向的数据传输若要实现双向通信需要创建两个无名管道。2. 核心 API 及代码示例无名管道的核心函数是pipe()用于创建管道并获取读写端的文件描述符。c// 创建无名管道int pipe(int pipefd[2]);参数说明pipefd是一个长度为2的数组其中pipefd[0]是管道的读端用于读取管道内的数据pipefd[1]是管道的写端用于向管道内写入数据数据从写端流入读端流出遵循先进先出FIFO原则。代码示例父子进程通过无名管道通信c#include stdio.h#include unistd.h#include string.h#include sys/wait.hint main() {// 定义管道文件描述符数组用于存储读端和写端int pipefd[2];// 创建无名管道失败返回-1if (pipe(pipefd) -1) {perror(pipe 创建失败);return -1;}// 创建子进程实现亲缘进程间通信pid_t pid fork();if (pid -1) {perror(fork 失败);return -1;}// 子进程负责向管道写数据if (pid 0) {// 子进程只写不读关闭读端避免资源浪费close(pipefd[0]);// 定义要发送的消息char *msg 你好父进程我是子进程;// 向管道写端写入数据参数分别为写端文件描述符、消息地址、消息长度write(pipefd[1], msg, strlen(msg));printf(子进程已向管道写入数据\n);// 写入完成后关闭写端close(pipefd[1]);return 0;}// 父进程负责从管道读数据if (pid 0) {// 父进程只读不写关闭写端close(pipefd[1]);// 定义缓冲区用于存储读取到的数据char buf[1024] {0};// 从管道读端读取数据参数分别为读端文件描述符、缓冲区地址、缓冲区大小read(pipefd[0], buf, sizeof(buf));printf(父进程从管道读取到数据%s\n, buf);// 读取完成后关闭读端close(pipefd[0]);// 等待子进程执行完毕避免产生僵尸进程wait(NULL);}return 0;}代码说明父子进程通过管道实现单向通信子进程负责写数据父进程负责读数据管道的读写端需按需关闭否则可能导致进程阻塞或资源泄漏编译时无需额外链接库直接用gcc test.c -o test即可。3. 管道的四个核心读写行为必记读阻塞管道内没有数据且写端仍然存在时调用read()函数会阻塞进程直到有其他进程向管道内写入数据。写阻塞管道缓冲区被写满且读端仍然存在时调用write()函数会阻塞进程直到有其他进程从管道内读取数据腾出缓冲区空间。管道破裂所有读端都被关闭此时再调用write()向管道写数据会触发SIGPIPE信号进程默认会被终止。读返回0所有写端都被关闭读完管道内剩余数据后再次调用read()会返回0相当于读到了文件末尾不会再阻塞。1.2 有名管道FIFO1. 核心特点有名管道又称 FIFO 文件解决了无名管道只能用于亲缘进程通信的限制可以实现同一主机内任意两个进程的通信不限亲缘关系。有名管道在文件系统中有对应的“p 类型”文件通过ls -l命令查看文件类型标识为 p但数据仍存储在内核缓冲区该文件仅作为进程访问管道的入口不实际存储数据。2. 核心 API 及代码示例有名管道的使用分为两步首先通过mkfifo()创建管道文件然后通过open()、read()、write()等文件操作函数读写数据。c// 创建有名管道文件int mkfifo(const char *pathname, mode_t mode);参数说明pathname是管道文件的路径和名称比如./my_fifomode是管道文件的权限常用0664表示所有者可读可写同组用户和其他用户可读权限设置需结合系统 umask 值实际权限为mode ~umask。代码示例两个独立进程通过有名管道通信该示例分为写进程和读进程两个独立文件运行时需先启动写进程再启动读进程读进程会阻塞等待写进程打开管道并写入数据。1写进程代码fifo_write.cc#include stdio.h#include unistd.h#include fcntl.h#include string.hint main() {// 定义管道文件的路径和名称char *fifo_path ./my_fifo;// 创建有名管道文件权限0664失败返回-1if (mkfifo(fifo_path, 0664) -1) {perror(mkfifo 失败);return -1;}printf(管道文件创建成功准备写入数据\n);// 以只写方式打开管道文件会阻塞直到有进程以读方式打开int fd open(fifo_path, O_WRONLY);if (fd -1) {perror(open 失败);return -1;}// 定义要发送的消息char *msg 你好读进程我是写进程发送的数据;// 向管道写入数据write(fd, msg, strlen(msg));printf(写进程数据写入完成\n);// 关闭文件描述符释放资源close(fd);return 0;}2读进程代码fifo_read.cc#include stdio.h#include unistd.h#include fcntl.h#include string.hint main() {// 管道文件路径需和写进程保持一致char *fifo_path ./my_fifo;// 以只读方式打开管道文件会阻塞直到有进程以写方式打开int fd open(fifo_path, O_RDONLY);if (fd -1) {perror(open 失败);return -1;}printf(读进程成功打开管道准备读取数据\n);// 定义缓冲区存储读取到的数据char buf[1024] {0};// 从管道读取数据read(fd, buf, sizeof(buf));printf(读进程读取到数据%s\n, buf);// 关闭文件描述符close(fd);return 0;}代码说明编译两个文件gcc fifo_write.c -o fifo_write、gcc fifo_read.c -o fifo_read先运行写进程再运行读进程读进程会成功读取到写进程发送的数据有名管道的读写阻塞特性和无名管道完全一致通信完成后可通过rm my_fifo删除管道文件。1.3 信号1. 核心概念信号是进程间的异步通知机制也被称为“软中断”其核心特点是异步性进程不需要一直等待信号可正常执行自身代码当信号到达时CPU 会打断进程当前的执行流程先处理信号事件处理完成后再回到原来的位置继续执行。比如我们在终端按下Ctrl C本质上就是给前台运行的进程发送SIGINT信号默认会终止该进程。2. 信号的三种处理方式忽略进程收到信号后不做任何处理直接忽略该信号。注意SIGKILL强制终止和SIGSTOP强制暂停这两个信号无法被忽略是系统预留的用于强制控制进程。默认按操作系统预设的行为处理信号大部分信号的默认行为是终止进程少数信号如SIGCHLD的默认行为是忽略。捕获自定义信号处理函数当进程收到信号时会执行我们编写的处理逻辑替代默认行为实现灵活的信号处理。3. 常用信号高频信号编号信号名触发方式默认行为2SIGINT键盘按下Ctrl C终止当前前台进程9SIGKILL终端执行kill -9 进程 PID强制终止进程不可捕获、不可忽略19SIGSTOP终端执行kill -19 进程 PID强制暂停进程不可捕获、不可忽略17SIGCHLD子进程退出或被暂停时自动发送给父进程忽略该信号4. 核心 API 及代码示例常用的信号操作函数signal()注册信号处理方式、kill()向指定进程发送信号、pause()阻塞进程等待任意信号到达。c#include stdio.h#include signal.h#include unistd.h// 自定义信号处理函数参数为收到的信号编号void sig_handler(int signum) {printf(收到信号编号%d信号名SIGINT\n, signum);printf(信号处理完成继续执行原程序\n);}int main() {// 注册 SIGINT 信号Ctrl C指定使用自定义处理函数signal(SIGINT, sig_handler);printf(程序正在运行按下 Ctrl C 触发信号...\n);// 阻塞进程等待任意信号到达pause();printf(程序继续执行即将退出\n);return 0;}代码说明编译运行后程序会打印提示信息并阻塞等待信号此时按下Ctrl C不会终止程序而是执行我们自定义的sig_handler函数处理完成后继续执行原程序直到正常退出。若注释掉signal()注册语句按下Ctrl C会触发默认行为直接终止程序。1.4 消息队列1. 核心特点消息队列是内核中维护的一条消息链表用于进程间传递带类型的数据接收方可以根据消息类型筛选读取实现消息优先级控制比如优先处理类型为10的消息再处理类型为5的消息。消息队列属于 System V IPC 机制其生命周期随内核进程退出后消息队列不会自动销毁若不手动删除会一直占用内核资源直到重启系统或手动删除。2. 核心 API 及代码示例消息队列的使用流程首先通过ftok()生成唯一的 IPC 键值然后通过msgget()创建或获取消息队列接着通过msgsnd()发送消息、msgrcv()接收消息最后通过msgctl()删除消息队列。代码示例两个进程通过消息队列通信该示例分为发送消息进程和接收消息进程两个进程通过相同的 IPC 键值关联到同一个消息队列实现数据传递。1发送消息进程msg_send.cc#include stdio.h#include sys/msg.h#include sys/ipc.h#include string.h// 定义消息结构体首成员必须是long类型的消息类型必须大于0struct msgbuf {long mtype; // 消息类型用于接收方筛选消息char mtext[1024]; // 消息正文存储实际要传递的数据};int main() {// 生成唯一的IPC键值参数为文件路径和项目ID项目ID取值1~255key_t key ftok(./, 100);if (key -1) {perror(ftok 失败);return -1;}// 创建/获取消息队列权限0664失败返回-1int msgid msgget(key, IPC_CREAT | 0664);if (msgid -1) {perror(msgget 失败);return -1;}// 定义消息结构体变量用于存储要发送的消息struct msgbuf msg;// 设置消息类型为10接收方需按该类型读取msg.mtype 10;// 填充消息正文内容strcpy(msg.mtext, 这是消息队列发送的消息);// 发送消息到消息队列参数为消息队列ID、消息结构体地址、正文长度、标志位0为阻塞if (msgsnd(msgid, msg, strlen(msg.mtext), 0) -1) {perror(msgsnd 失败);return -1;}printf(发送消息成功消息内容%s\n, msg.mtext);return 0;}2接收消息进程msg_recv.cc#include stdio.h#include sys/msg.h#include sys/ipc.h#include string.h// 消息结构体需和发送进程保持一致struct msgbuf {long mtype;char mtext[1024];};int main() {// 生成和发送进程相同的IPC键值确保关联到同一个消息队列key_t key ftok(./, 100);if (key -1) {perror(ftok 失败);return -1;}// 获取消息队列权限0664int msgid msgget(key, IPC_CREAT | 0664);if (msgid -1) {perror(msgget 失败);return -1;}// 定义消息结构体变量用于存储接收的消息struct msgbuf msg;// 接收消息类型为10的消息参数为队列ID、结构体地址、正文最大长度、消息类型、标志位if (msgrcv(msgid, msg, sizeof(msg.mtext), 10, 0) -1) {perror(msgrcv 失败);return -1;}printf(接收消息成功消息类型%ld消息内容%s\n, msg.mtype, msg.mtext);// 删除消息队列释放内核资源不再使用时必须手动删除msgctl(msgid, IPC_RMID, NULL);return 0;}代码说明编译两个文件后先运行发送进程再运行接收进程接收进程会按消息类型10读取消息发送进程可多次发送消息接收进程可循环读取使用完消息队列后必须通过msgctl(msgid, IPC_RMID, NULL)手动删除避免内核资源残留也可通过终端命令ipcrm -q 消息队列ID删除。1.5 共享内存1. 核心特点共享内存是效率最高的 IPC 方式其原理是操作系统在内核空间开辟一块物理内存多个进程将这块物理内存映射到自己的虚拟地址空间进程可以直接读写该内存区域无需经过内核数据拷贝这是其效率高于管道、消息队列的核心原因。注意共享内存本身没有同步互斥机制若多个进程同时读写共享内存会导致数据错乱因此通常需要配合信号量或互斥锁使用实现进程间的同步互斥。2. 核心 API 及代码示例共享内存的使用流程创建/获取共享内存 → 将共享内存映射到进程虚拟地址空间 → 读写共享内存数据 → 解除内存映射 → 删除共享内存。代码示例两个进程通过共享内存通信该示例分为写共享内存进程和读共享内存进程两个进程通过相同的 IPC 键值关联到同一块共享内存实现数据读写。1写共享内存进程shm_write.cc#include stdio.h#include sys/shm.h#include sys/ipc.h#include string.hint main() {// 生成唯一的IPC键值key_t key ftok(./, 200);if (key -1) {perror(ftok 失败);return -1;}// 创建共享内存大小4096字节按页对齐一页通常为4096字节权限0664int shmid shmget(key, 4096, IPC_CREAT | 0664);if (shmid -1) {perror(shmget 失败);return -1;}// 将共享内存映射到进程的虚拟地址空间参数为共享内存ID、映射地址NULL为系统自动分配、标志位0为读写char *shm_addr (char*)shmat(shmid, NULL, 0);if (shm_addr (char*)-1) {perror(shmat 失败);return -1;}// 向共享内存写入数据直接通过指针操作无需调用read/writestrcpy(shm_addr, 这是共享内存中的数据);printf(写进程数据写入共享内存成功\n);// 等待读进程读取数据按任意键继续执行后续操作getchar();// 解除共享内存与进程的映射关系shmdt(shm_addr);// 删除共享内存释放内核资源shmctl(shmid, IPC_RMID, NULL);return 0;}2读共享内存进程shm_read.cc#include stdio.h#include sys/shm.h#include sys/ipc.h#include string.hint main() {// 生成和写进程相同的IPC键值关联到同一块共享内存key_t key ftok(./, 200);if (key -1) {perror(ftok 失败);return -1;}// 获取共享内存大小需和写进程一致int shmid shmget(key, 4096, IPC_CREAT | 0664);if (shmid -1) {perror(shmget 失败);return -1;}// 映射共享内存到进程虚拟地址空间char *shm_addr (char*)shmat(shmid, NULL, 0);if (shm_addr (char*)-1) {perror(shmat 失败);return -1;}// 从共享内存读取数据直接通过指针访问printf(读进程从共享内存读取到数据%s\n, shm_addr);// 解除内存映射shmdt(shm_addr);return 0;}代码说明编译两个文件后先运行写进程再运行读进程读进程会直接读取共享内存中的数据效率极高写进程通过getchar()等待读进程读取完成避免提前删除共享内存所有进程解除映射后再通过shmctl()删除共享内存也可通过终端命令ipcrm -m 共享内存ID删除。二、高频易错点汇总必记1. 主线程调用exit()会销毁整个进程进程内所有线程都会被强制终止而调用pthread_exit()仅退出主线程其他线程仍可继续运行。2. 非分离线程执行完毕后资源不会自动释放必须通过pthread_join()回收否则会产生“僵尸线程”导致系统资源泄漏分离线程执行完毕后操作系统会自动回收其资源无需调用pthread_join()。3. 无名管道仅适用于亲缘进程通信有名管道可适用于任意进程通信两者均为半双工通信数据遵循先进先出原则。4.SIGKILL和SIGSTOP信号无法被捕获、忽略只能按系统默认行为处理常用于强制控制进程。5. System V IPC消息队列、共享内存、信号量集的生命周期随内核进程退出后不会自动销毁必须手动删除否则会一直占用内核资源。6. 共享内存本身无同步互斥机制多进程同时读写时需配合信号量或互斥锁避免数据错乱读写共享内存直接通过指针操作无需调用read()和write()函数。7. 管道的读写端需按需关闭否则可能导致进程阻塞所有读端关闭后向管道写数据会触发SIGPIPE信号导致进程终止。