Linux应用开发里进程间通信IPC是绕不开的一块硬骨头。不管是写嵌入式后台服务、搞中间件还是单纯为了面试和笔试你都得把这几种通信方式吃透。这篇笔记我把管道、消息队列、共享内存、信号量、信号这些常见手段从头到尾捋了一遍每个都有原理说明、C语言实操示例和踩坑记录适合正在啃《Unix环境高级编程》或者准备Linux系统编程面试的读者。照着我这套思路学下来至少不会在面试被问到“进程间通信方式有哪些”时卡壳。1. 为什么需要进程间通信以及怎么挑IPC1.1 进程隔离带来的通信需求首先要明确一个前提Linux下的每个进程都有独立的虚拟地址空间。什么意思就是进程A里的变量进程B根本看不见就算两个进程跑的是同一份代码它们在内存里的数据也是各玩各的。这个隔离机制保障了稳定性——一个进程崩溃不会直接拖垮另一个——但也带来了一个实际问题如果两个进程需要协作完成同一个任务怎么交换数据、怎么互相通知状态比如说一个典型的生产者消费者模型。生产者进程负责采集数据消费者进程负责处理数据两个进程之间需要有个“交接区”生产者把数据放进去消费者从里面拿。这个“交接区”不可能用普通全局变量实现因为进程地址空间是隔离的。这时候就得靠IPC机制。归纳下来IPC要解决的是四类问题数据交换把一段数据从A传给B、事件通知告诉对方“我干完了”或“该你了”、资源共享多个进程同时访问同一份资源时的互斥、同步控制协调各进程的执行节奏。管道、共享内存、消息队列、信号量、信号、Socket这几种方式本质上就是针对这四类需求的组合拳。1.2 IPC方案选型的总体思路很多初学者上来就问“到底哪个IPC最好”这问题没法一概而论得看场景。我自己做选型时一般看四个维度数据量大小、实时性要求、进程间是否有血缘关系、是否需要跨机器通信。拿数据量来说如果你只需要传递几个字节的状态通知那用信号或者管道就够了搞个共享内存反而大材小用还引入一堆同步问题。如果数据量达到几十KB、上百KB甚至更大那管道和消息队列就有些吃力了——它们都有内核缓冲区大小限制消息队列还有单条消息上限。这时候共享内存是首选因为它直接映射同一块物理内存数据拷贝次数最少。再看进程关系。父子进程之间通信匿名管道最简单fork之后天然共享文件描述符。没有血缘关系的进程之间就得用命名管道FIFO、消息队列、共享内存或者Unix domain socket。这里我列一个常用的选型对照表通信方式数据量是否需同步进程关系跨主机典型场景匿名管道小~中无需父子进程否shell管道、父进程收集子进程输出命名管道FIFO小~中无需任意进程否日志收集、客户端向守护进程送数据消息队列小~中无需任意进程否请求/响应模式的报文通信共享内存大必须配信号量任意进程否音视频帧、大数据缓存信号量无数据本身用于同步任意进程否多进程互斥访问共享资源信号极小无需任意进程否进程终止、状态通知套用我常用的类比管道是传纸条两个人通过一个管子递纸条顺序保证但一次传递的内容有限消息队列是邮局你可以给邮件贴上类型标签收件时按标签取共享内存是一块公共黑板谁都能上去写字但正因为谁都能写才需要“红绿灯”也就是信号量来控制先后顺序信号则是敲门或拍肩膀只能告诉对方“有事”具体什么事还得双方提前约定。2. 管道最基础也最容易忽略细节的IPC2.1 匿名管道父子进程之间的串联通道管道是Linux里最古老、最简单的一种IPC。实现原理很直观内核维护一个环形缓冲区一端写、一端读。你往写端丢数据数据在内核缓冲区里排队读端按顺序取出来。这个模型就像一根水管数据只能从一端流到另一端是单向的。匿名管道用pipe()系统调用创建调用成功后返回两个文件描述符pipefd[0]是读端pipefd[1]是写端。单独一个进程拿着这两个fd没太大意义因为自己写自己读等于脱裤子放屁。它的正确玩法是先pipe()再fork()父子进程会共享这对文件描述符然后各自关掉不需要的一端形成一条单向的数据流通路。一个基础示例#include stdio.h #include unistd.h #include string.h #include sys/wait.h int main(void) { int pipefd[2]; pid_t pid; char buf[64]; if (pipe(pipefd) -1) { perror(pipe); return 1; } pid fork(); if (pid -1) { perror(fork); return 1; } if (pid 0) { /* 子进程关闭读端只写 */ close(pipefd[0]); const char *msg hello from child; write(pipefd[1], msg, strlen(msg) 1); close(pipefd[1]); return 0; } else { /* 父进程关闭写端只读 */ close(pipefd[1]); ssize_t n read(pipefd[0], buf, sizeof(buf)); if (n 0) printf(parent received: %s\n, buf); close(pipefd[0]); wait(NULL); } return 0; }这里有个致命细节子进程一定要close(pipefd[0])父进程一定要close(pipefd[1])。为什么因为管道的读端read()只有在所有写端都关闭的情况下才会返回0EOF。如果子进程没关自己的写端而父进程又不需要它读那读端就会一直等下去。同理父进程如果不关写端子进程那边读不到EOF也容易出问题。很多第一次写管道的人都会在阻塞上卡很久十有八九就是fd没关干净。管道缓冲区默认容量在Linux上一般是65536字节64KB具体可以用fpathconf(pipefd[0], _PC_PIPE_BUF)查询。写入的数据量少于PIPE_BUF4096字节时write操作是原子的超过这个值多进程并发写就可能出现数据交错。这个特性在考虑“能否多个写者往同一管道里写”时很关键。2.2 命名管道没有血缘关系也能通信匿名管道有个硬伤只能用于父子或共享fd的进程之间。如果两个毫不相干的进程想通过管道通信就得用命名管道FIFO。它在文件系统里有一个真实路径名进程通过这个路径打开它操作方式跟文件很像。创建FIFO有两种方式。命令行里直接mkfifo /tmp/myfifo代码里则用mkfifo()函数。创建之后某个进程以只读方式open另一个进程以只写方式open内核就把它们串起来了。这里有个体验感很强的问题如果只打开读端或只打开写端open操作会默认阻塞直到另一端也打开为止。换句话说你开一个终端执行cat /tmp/myfifo它会卡在那里直到另一个终端往里写东西。利用这个特性可以在shell脚本里做简单的等待同步。一个典型的FIFO服务端示例#include stdio.h #include sys/stat.h #include fcntl.h #include unistd.h #include string.h #define FIFO_PATH /tmp/ipc_demo_fifo int main(void) { char buf[128]; /* 如果文件已存在mkfifo会失败忽略即可 */ mkfifo(FIFO_PATH, 0644); /* 以只读方式打开会阻塞直到有写端打开 */ int fd open(FIFO_PATH, O_RDONLY); if (fd -1) { perror(open); return 1; } ssize_t n read(fd, buf, sizeof(buf) - 1); if (n 0) { buf[n] \0; printf(fifo received: %s\n, buf); } close(fd); return 0; }客户端的写法就是open(FIFO_PATH, O_WRONLY); write(fd, ...)。对比一下可以看出FIFO把IPC机制文件化了好处是接口统一可以用read/write那套与文件一致的API还能配合shell重定向和管道组合使用。2.3 管道的边界和常见坑管道虽然简单坑真不少。第一个坑是上面说的SIGPIPE。当进程往一个没有任何读端的管道写数据时内核会给写进程发SIGPIPE信号而这个信号的默认行为是终止进程。这在多进程协作里非常危险某个读端进程崩了写端进程接着写直接就被信号打死了。处理办法很暴力也很实用在程序初始化时signal(SIGPIPE, SIG_IGN)忽略掉让write返回EPIPE错误码你自己决定怎么处理。第二个坑是管道没有消息边界本质上是个字节流。如果你和对方约定每行是一条完整消息那么上次读到的内容包括换行符在内的所有字节都得处理因为read()可能读到半条消息。很多从消息队列转过来的人特别不适应这一点总觉得数据会“粘包”实际不是粘包是管道根本不保证消息完整性。第三个坑是死锁。父进程写管道时如果管道缓冲区满了write会阻塞等待读端消费。如果此时父进程还同时等着子进程完成某个操作而子进程又在等父进程写完才能继续程序就活活卡死了。解决办法就是别搞双向依赖或者用全双工通信时开两条管道、分别处理各自方向的数据。3. 共享内存速度最快的IPC搭配信号量才是完整方案3.1 共享内存为什么快共享内存是所有IPC里速度最快的这一点没有悬念。它的大致原理是内核创建一块物理内存区域多个进程分别把这同一块物理内存映射到自己的虚拟地址空间。进程A往这块内存写数据进程B在同一块地址上读实际上读的就是进程A写的内容全程不需要系统调用拷贝也不需要内核在用户态和内核态之间搬运数据。你可以把它理解成几个人共同租用了一个房间每个人手里都有钥匙进了房间就能看到桌面上放着的资料。对比之下管道和消息队列都像是“通过管理员转交”进程A把数据交给内核内核再交给进程B中间多了一道手开销自然高一些。共享内存常见的实现方式有两种System V IPC风格的shmget/shmat/shmat/shmdt/shmctl以及POSIX风格的shm_open/mmap。System V那套是老牌接口Linux上支持得非常稳定POSIX那套接口更现代配合mmap使用逻辑上也更容易理解。我实际项目里两种都用过学习阶段建议先把System V的接口搞明白因为网上的资料和老项目的代码大多还都是这一套。3.2 用共享内存实现一个最简单的数据交换下面这段示例展示的是最基本的共享内存使用流程创建、附加、写、读、分离、删除。#include stdio.h #include sys/shm.h #include string.h #include unistd.h #define SHM_KEY 0x1234 int main(void) { /* 创建共享内存1KB权限0644 */ int shmid shmget(SHM_KEY, 1024, IPC_CREAT | 0644); if (shmid -1) { perror(shmget); return 1; } /* 把共享内存附加到当前进程地址空间 */ char *addr shmat(shmid, NULL, 0); if (addr (void *)-1) { perror(shmat); return 1; } /* 父子进程示例父进程写然后fork子进程读 */ const char *msg shared memory message; memcpy(addr, msg, strlen(msg) 1); pid_t pid fork(); if (pid 0) { /* 子进程直接读因为这块内存在fork时被继承了 */ printf(child reads: %s\n, addr); /* 分离但不删除 */ shmdt(addr); return 0; } else { wait(NULL); shmdt(addr); /* 删除共享内存对象 */ shmctl(shmid, IPC_RMID, NULL); return 0; } }注意这里的写法只适合演示实际生产代码里进程A和进程B一般是通过ftok()或固定key来关联同一块内存。ftok()用文件路径和一个整数项目ID生成key比如ftok(/tmp/shmfile, A)。这个函数有个坑如果它依赖的文件被删除再重建inode变了生成的key就变了原先的共享内存就找不到了。所以要么用固定key要么确保ftok用的文件长期稳定存在。3.3 共享内存必须配信号量共享内存虽然快但内核不提供任何同步机制。也就是说它不像管道和消息队列那样自带互斥和缓冲控制。两个进程同时往共享内存里写数据就会互相覆盖产生不可预知的错乱结果。这就好比几个人共用一块白板如果没人约束先后顺序你刚写完一行字下一秒就被别人擦掉了。解决办法是引入信号量做互斥和同步。System V信号量的接口是semget/semop/semctlsemget创建或获取一个信号量集合semop对信号量做PV操作semctl可以用来初始化信号量的值或者在最后删除它。最简单的使用流程如下#include sys/sem.h union semun { int val; struct semid_ds *buf; unsigned short *array; }; /* 创建信号量初值设为1实现互斥锁 */ int semid semget(SHM_KEY, 1, IPC_CREAT | 0644); union semun su; su.val 1; semctl(semid, 0, SETVAL, su); /* P操作申请资源若值为0则阻塞 */ struct sembuf op {0, -1, SEM_UNDO}; semop(semid, op, 1); /* ... 访问共享内存 ... */ /* V操作释放资源 */ struct sembuf op2 {0, 1, SEM_UNDO}; semop(semid, op2, 1);特别提醒SEM_UNDO这个标志它在信号量操作上加了一层“进程退出时自动归还”的保险。如果一个进程在持有信号量的时候崩溃了内核会自动把信号量值回滚到操作前的状态避免其他进程永远等下去。这个机制在写守护进程、重点监控程序时尤其重要不加SEM_UNDO的话进程异常退出容易让信号量卡在0值上整个系统后续的进程全被堵死。3.4 查看与清理共享内存资源共享内存有个很头疼的特点进程崩溃或者忘记调用shmctl(IPC_RMID)时内核里的共享内存不会被自动回收。这会导致系统IPC资源被慢慢耗尽。排查和清理时linux提供的ipcs命令是主力工具。用ipcs -m可以列出当前系统里的所有共享内存段。输出里有shmid、key、大小、关联进程数等信息。发现不需要的共享内存直接用ipcrm -m shmid删除。同理ipcs -s看信号量ipcrm -s semid删除ipcs -q看消息队列ipcrm -q msqid删除。我自己的习惯是在写调试程序时先用ipcrm -a把系统里所有IPC对象清一遍自己开发机上这么干没问题生产环境别乱来避免上一版程序的残留干扰测试结果。4. 消息队列带结构的数据报文4.1 消息队列与管道、共享内存的差别消息队列可以理解为“带类型标签的管道”。它也是内核维护的一个数据链表但每条消息都有两个部分消息类型一个正整数mtype和消息数据。发送方用msgsnd投递消息接收方用msgrcv按类型接收整个过程不需要像管道那样严格区分读写方向多个进程可以往同一个队列里投递消息也可以按类型各取所需。对比管道消息队列最大的优势是有边界每次收发都是以消息为单位的不会出现半条消息或者粘包问题队列中的消息会被内核持久保存直到被取走发送方不会因为接收方没准备好而阻塞当然队列满了还是会阻塞。对比共享内存消息队列不需要你额外做同步内核帮你处理了互斥和缓冲。缺点是性能不如共享内存。毕竟每条消息都要从用户态拷贝到内核态再从内核态拷贝到用户态。但这个开销对于大部分业务场景来说完全可接受尤其是消息量不大、但需要按类型分发的场景消息队列的编程模型比共享内存舒服太多了。4.2 消息队列的实操要点消息队列的接口很集中msgget创建或获取队列msgsnd发送msgrcv接收msgctl做控制。使用前要定义一个消息结构体注意第一个成员必须是long mtype后面是数据部分。比如struct msg_buf { long mtype; int data; };发送消息时msgsnd最后一个参数是消息大小但这里的大小只算data部分不含mtype。接收时msgrcv的第二个参数同样只关心结构体指针最后一个参数如果传0表示接收队列里第一条消息传正数表示接收指定mtype的消息传负数则接收类型小于等于该绝对值的最小类型消息。这个第三条规则挺绕的实际用的时候按顺序记住就行。队列的大小限制也要留意。Linux下默认一条消息最大8192字节队列默认总字节数16384。这个限制可以用msgctl配合IPC_SET调整但调整需要权限而且生产环境下往往不是改参就能解决——消息设计得过大本身就是个需要优化的信号。ftok同一个坑在消息队列上也会出现依赖文件路径的稳定性。用固定key的时候注意别和其他应用撞车用ftok的时候文件路径别用临时目录里的文件。4.3 消息队列的适用场景我实际工作中用到消息队列最多的地方是那种“多客户端请求、单服务器处理”的模式。比如后台有多个采集进程它们各自往同一个消息队列发送带不同mtype的请求服务进程统一接收。服务进程处理完后可以按请求进程约定的mtype回复。整个架构里任务分发的逻辑清晰每个进程之间无需建立Socket连接部署也简单。但要注意Linux传统System V消息队列由于接口比较古老现代新项目里越来越多人倾向于使用POSIX消息队列或者直接上Redis这类外部组件。System V消息队列在没有外部依赖的嵌入式环境和服务端老项目里仍然大量存在面试也常常考所以原理必须懂代码也得能写。学的时候别只背函数名最好自己动手把发送端、接收端都完整跑一遍。5. 信号简单粗暴的通知机制5.1 信号的本质及其局限信号是另一种进程间通信方式但它不传数据只做事件通知。你可以把信号理解成内核往进程身上拍了一下“注意有事发生”。进程是否理会这个信号取决于它有没有注册对应该信号的处理函数。如果没有注册就按默认行为处理比如SIGINT默认终止进程、SIGCHLD默认忽略、SIGKILL无法被捕获和阻塞。常用的几个信号SIGINTCtrlC触发、SIGTERMkill默认发的信号可被捕获用于优雅退出、SIGHUP终端挂断守护进程常监听它做配置重载、SIGUSR1/SIGUSR2留给用户自定义使用常作为进程间简单通知。由于信号不携带附加数据它适合做“事件触发”不适合做“数据交换”。比如主进程收到SIGTERM立刻做清理工作然后退出工作进程收到SIGUSR1重新读取配置文件。这种场景用信号非常轻量但如果想传递一组参数信号就无能为力了得配合共享内存或消息队列。5.2 信号处理函数与可重入问题注册信号处理函数有两个接口signal()和sigaction()。signal用起来简单但语义在不同Unix系统上有差异而且它在处理函数执行期间可能会自动把该信号恢复为默认行为导致丢信号。sigaction则提供了更精细的控制能设置SA_RESTART、指定信号集等是更可靠的选择。#include stdio.h #include signal.h #include unistd.h static volatile sig_atomic_t flag 0; void handler(int sig) { /* 信号处理函数里只做标志置位不做复杂操作 */ flag 1; } int main(void) { struct sigaction sa; sa.sa_handler handler; sigemptyset(sa.sa_mask); sa.sa_flags SA_RESTART; sigaction(SIGUSR1, sa, NULL); while (1) { pause(); /* 等待信号 */ if (flag) { printf(got signal, do something...\n); flag 0; } } return 0; }这里要特别强调一个关键点信号处理函数内部不要调用printf、malloc这类非异步信号安全的函数。因为信号随时可能打断主流程如果在处理函数里调用这些函数而主流程又正好在调用它们的中途就可能产生不可重入的问题导致数据错乱甚至死锁。正确做法是在处理函数里只设置一个sig_atomic_t类型的标志位主循环看到标志位后再去做实际操作。上面示例里的volatile sig_atomic_t flag就是这么用的volatile告诉编译器每次都要从内存读取防止缓存优化。5.3 用SIGUSR1/SIGUSR2实现进程间通知最简单的例子父进程创建子进程后给子进程发SIGUSR1。子进程在处理函数里置位标志主循环感知到后执行某个任务父进程再次发SIGUSR2子进程执行另一个任务。整个过程不需要父子进程之间建立管道或者共享内存就能完成基本的任务调度。需要注意的是虽然用kill(pid, sig)可以直接给另一个进程发信号但信号达到的时机是异步的不能保证到达时对方处于什么状态。因此在多线程程序里使用信号要格外小心信号的默认投递目标是进程内的任意某个线程要精确控制投递给哪个线程还得用pthread_kill复杂度会上一个台阶。我的建议是进程间简单通知用信号没问题一旦逻辑复杂到要区分多线程优先考虑用条件变量或者eventfd代替信号可维护性会好很多。6. 实操过程与核心环节实现6.1 实操环境准备我做这套演示的环境是Ubuntu 22.04内核版本6.xgcc 11.4。其实这些代码在任何一个主流Linux发行版上都能编译运行CentOS、Debian、Deepin都行不必纠结具体版本。编译工具除了gcc最好准备strace和ipcs方便观察系统调用和IPC资源变化。如果机器上没有先装一下sudo apt install gcc strace iproute2 2/dev/null # 或者 sudo yum install gcc strace iproute另外强烈建议在终端里多开几个窗口用来观察不同进程的行为。共享内存那部分尤其如此你会直观看到数据在一个进程写入后另一个进程立刻就能读。6.2 完整示例双进程通过共享内存加信号量协同工作这个示例模拟一个真实场景进程A生成一批随机数写入共享内存进程B读取共享内存并求和然后打印结果。两个进程通过System V信号量保证互斥防止A在写入时B去读、导致读到半截数据。共享内存区规划如下开头4字节存数据条数后面是int数组最多存256个随机数。这样一个简单的数据布局既能看清进程间数据交换又能看出信号量在互斥中的必要性。/* shm_sem_demo.c */ #include stdio.h #include stdlib.h #include string.h #include time.h #include sys/shm.h #include sys/sem.h #include sys/stat.h #include unistd.h #include sys/wait.h #define SHM_KEY 0x2001 #define SEM_KEY 0x2002 #define MAX_ITEMS 256 struct share_data { int count; int items[MAX_ITEMS]; }; union semun { int val; struct semid_ds *buf; unsigned short *array; }; static int sem_p(int semid) { struct sembuf op {0, -1, SEM_UNDO}; return semop(semid, op, 1); } static int sem_v(int semid) { struct sembuf op {0, 1, SEM_UNDO}; return semop(semid, op, 1); } int main(void) { int shmid shmget(SHM_KEY, sizeof(struct share_data), IPC_CREAT | 0644); if (shmid -1) { perror(shmget); return 1; } struct share_data *shm shmat(shmid, NULL, 0); if (shm (void *)-1) { perror(shmat); return 1; } int semid semget(SEM_KEY, 1, IPC_CREAT | 0644); if (semid -1) { perror(semget); return 1; } union semun su; su.val 1; semctl(semid, 0, SETVAL, su); /* 初始值1作为互斥锁 */ pid_t pid fork(); if (pid 0) { /* 子进程消费者读共享内存并求和 */ int sum 0; sem_p(semid); for (int i 0; i shm-count; i) sum shm-items[i]; sem_v(semid); printf([child] read %d items, sum %d\n, shm-count, sum); shmdt(shm); return 0; } else if (pid 0) { /* 父进程生产者写入随机数 */ srand((unsigned)time(NULL)); sem_p(semid); shm-count 10; for (int i 0; i shm-count; i) shm-items[i] rand() % 100; sem_v(semid); printf([parent] wrote %d items\n, shm-count); wait(NULL); shmdt(shm); shmctl(shmid, IPC_RMID, NULL); semctl(semid, 0, IPC_RMID); return 0; } return 0; }编译和运行gcc -o shm_sem_demo shm_sem_demo.c ./shm_sem_demo运行结果大致如下[parent] wrote 10 items [child] read 10 items, sum 428这个示例里没有做复杂的同步因为进程执行的先后顺序基本是父进程先写、子进程后读。如果去掉信号量父进程写了一半子进程就去读很可能读到不完整的数据。你把sem_p和sem_v注释掉多跑几次会有机会看到子进程sum的值变得不稳定甚至读到count为0的情况。这个简单的演示能让你直观感受“互斥”到底在防什么。6.3 调试IPCC程序的实用技巧第一个调试技巧是用strace跟踪系统调用。管道、消息队列、共享内存这些IPC都涉及系统调用用strace可以清楚地看到每次调用的参数和返回值strace -f -e traceipc,pipe,write,read ./shm_sem_demo-f参数跟踪fork出来的子进程-e traceipc会把semop、semget、shmat等调用全部打印出来。一旦程序卡住你会很容易看出它阻塞在哪一步。第二个技巧是用gdb调试多进程。默认情况下gdb只跟随父进程子进程继续运行。如果想调试子进程需要在启动程序前设置set follow-fork-mode child这样fork之后gdb就会自动切换到子进程。如果父子进程都要调试可以搭配set detach-on-fork off让gdb同时控制两个进程但这需要多点耐心。第三个技巧是观察系统IPC资源。运行程序前后分别执行ipcs -a能看出共享内存、信号量、消息队列的创建和释放是否正常。如果程序退出后还有残留的shmid或semid用ipcrm -m shmid和ipcrm -s semid手动清理。这个习惯在做实验时尤其重要不然随着测试的次数增加系统里堆满孤儿IPC对象排查问题时会非常混乱。7. 常见问题速查与避坑指南这里整理一份问题速查表覆盖我实践中最常遇到的情况供读者对照排查现象可能原因解决方式read(pipefd[0])一直阻塞写端没有全部关闭检查是否关闭了所有多余的pipefd[1]副本写管道时进程被莫名杀死读端关闭后写入触发SIGPIPE初始化时signal(SIGPIPE, SIG_IGN)两个管道进程通信数据混乱单次写入超过PIPE_BUF4096字节把数据拆小或换消息队列/共享内存共享内存数据读到一半缺少互斥机制引入信号量保证读写互斥信号量值变成0后所有进程等待持有信号量的进程异常退出所有semop操作加上SEM_UNDO标志程序退出后共享内存还在没调用shmctl(IPC_RMID)用ipcs -m查找ipcrm -m删除消息队列发送失败errnoEINVAL消息大小超出队列或系统限制检查单条消息是否超过MSGSZ用msgctl查询ftok后获取的queue不是预期那个路径文件被删重建导致inode变化改用固定key或确保路径文件不变信号处理函数里printf导致死锁调用了非异步信号安全函数处理函数只置位标志主循环再执行操作针对Linux面试里高频出现的IPC问题我整理了几个简洁答案可以背诵进程间通信用了哪些方式管道匿名管道和命名管道、消息队列、共享内存、信号量、信号、Socket。共享内存为什么最快因为多个进程共享同一块物理内存数据不需要通过内核在进程之间拷贝性能瓶颈基本只在内存访问本身。管道容量是多大默认65536字节单次写入小于PIPE_BUF4096字节保证原子性更大则不保证。信号和信号量什么区别信号是异步事件通知机制传送的是事件本身信号量是同步原语用于控制多进程对共享资源的访问不传业务数据。还有一个容易被追问的点如果让你设计一个高性能的本地IPC你会选什么我的思路是优先考虑数据量。如果是高频小消息可以选Unix domain socket配上SOCK_DGRAM模式自带消息边界。如果是大数据块那就共享内存加信号量或者用mmap映射文件来做。除非整个项目已经有现成的消息队列基础否则我不会刻意去用System V消息队列因为它在清理和权限管理上确实比较麻烦。8. 关于IPC的一些经验之谈最后聊点实在的。在我的实际开发里真正用得最多的IPC其实不是面试必背的System V全家桶而是Socket和eventfd。原因很简单Socket能跨主机、能走网络协议栈、编程模型统一eventfd则在事件通知上比信号更可控配合epoll使用非常顺手。共享内存虽然快但一旦进程异常退出、信号量没释放整个系统的恢复成本很高所以在做高可靠性服务时我会谨慎使用。如果你正在学习阶段我的建议是不要跳过System V消息队列和共享内存这套老接口。它们是Linux IPC知识的底座理解了它们的设计思路你再去看POSIX版本或者kernel内部的机制都会顺很多。而如果你是为了赶项目、快速落地那就务实一点优先考虑简单的管道和Socket不要为了追求技术上的“高级”而把系统搞得过于复杂。另外一个小技巧写IPC相关代码前先画一张简单的数据流图把进程角色、数据方向、同步点标清楚。很多所谓的“IPC bug”其实不是接口用错了而是整个通信模型就没理顺。模型对了代码就成功了一半。