资讯动态

Linux进程间通信(IPC)完全指南:消息队列、共享内存与信号量实战

发布时间:2026/9/9 6:19:22 来源:尧图企业网站定制
如果你写过一阵子 Linux 下的服务端程序一定遇到过这样的场景两个进程各干各的但需要把一份几十 KB 的中间结果互相递过去。第 1 部分里我们聊过管道、命名管道和信号它们应付简单通知、单向字节流够用但一旦消息量上来、需要双向通信、或者数据格式是结构化报文这几个基础件就会显得力不从心。这篇 Linux 进程间通信IPC指南第 2 部分就把剩下的主力机制逐个拆开消息队列、共享内存、信号量还有 Unix Domain Socket。读完你不仅知道每个 API 怎么调还能明白什么时候该用哪个、真出问题了从哪下手查。1. 先复盘第 1 部分的边界管道和信号到底输在哪里1.1 管道与信号解决不了的三类场景第 1 部分详细写过管道和信号。简单回顾一下匿名管道适用于父子进程之间的单向字节流FIFO 把能力扩展到了任意两个进程之间但本质上仍然只能单向信号的作用更像是通知告诉进程发生了什么比如 SIGTERM 让它优雅退出、SIGCHLD 让它回收子进程能携带的信息量非常有限。管道和信号作为 IPC 的基础件问题不在于能不能用而在于边界。我自己归纳了三个典型场景一出现就该考虑换更重的机制场景 A消息有边界。管道是纯字节流写端写完 1024 字节读端可能分两次读到 512 加 512也可能一次读走 2048。如果业务消息有固定结构读端必须自己维护缓冲、自己切分协议非常容易出错。消息队列自带一条消息的概念天然解决这个问题。场景 B读写双方没有血缘关系且生命周期差得很远。管道和 FIFO 要求两边同时打开任何一端进程退出连接就断了。生产者临时挂了消费者收到的可能就是 EOF没有办法排队、重连更没办法等发布者恢复。场景 C要高吞吐地搬运大块数据。管道数据要经过内核缓冲区每次 read/write 都涉及用户态到内核态的切换。数据量小的时候无所谓几十 KB 甚至上 MB 连续传复制开销会非常明显。1.2 通信与同步IPC 的两个正交维度还有一件事经常被混淆值得单独拎出来说IPC 其实包含通信和同步两个维度。通信解决的是数据怎么从 A 到 B。管道、消息队列、共享内存、socket 都属于这个范畴。同步解决的是多个进程里的多个执行流能不能在正确的时机去访问这些数据。互斥锁、信号量、原子操作属于这个范畴。很多人把消息队列和共享内存放在一起比吞吐量比完就迷惑既然共享内存最快为什么还要用消息队列因为共享内存虽然解决了通信但它把同步问题一并丢给了你。谁负责保证读者不会读到写了一半的数据信号量、原子操作、内存屏障这些都得自己配。而消息队列、管道、socket 这类内核替我们做同步的机制数据完整性和顺序性由内核保证代价就是中间多一层拷贝性能上不去。所以正确的理解方式不是哪个更快用哪个而是我愿不愿意为数据正确性写同步代码来换取那一份吞吐量。这个判断贯穿第 2 部分后面所有内容。2. 消息队列把数传从流变成信2.1 System V 消息队列的三件套ftok、msgget、msgsnd/msgrcv消息队列解决了管道最让人头疼的消息边界问题。一次 msgsnd 对应一条完整消息一次 msgrcv 取走一条完整消息读写两端不需要自己维护拆包逻辑。在 Linux 上System V 消息队列是最有历史感的实现。先看一个最简的发送端#include sys/ipc.h #include sys/msg.h #include stdio.h #include string.h #include unistd.h struct msgbuf { long mtype; char mtext[128]; }; int main() { key_t key ftok(/tmp/msg_demo, 65); if (key -1) { perror(ftok); return 1; } int msqid msgget(key, IPC_CREAT | 0666); if (msqid -1) { perror(msgget); return 1; } struct msgbuf msg {0}; msg.mtype 1; snprintf(msg.mtext, sizeof(msg.mtext), hello from pid %d, getpid()); if (msgsnd(msqid, msg, sizeof(msg.mtext), 0) -1) { perror(msgsnd); } return 0; }流程是 ftok 生成 keymsgget 拿队列 idmsgsnd 发消息。这里的 ftok 很多人第一次用会踩坑它用路径名加整数的组合生成 key路径对应的文件必须真实存在否则返回 -1。所以 demo 里我固定用 /tmp 下已经存在的文件路径。struct msgbuf 的第一个字段必须是 long 类型的 mtype这是 System V 的硬性规定。msgsnd 的第三个参数是消息正文长度不包含 mtype。内核不会替你做数据序列化mtext 你想放什么字节都行放结构体、放 JSON 字符串、放二进制协议都可以只要长度控制在限制内。2.2 消息类型与阻塞语义msgrcv 的第四个参数没那么简单接收端对应这样写struct msgbuf msg; ssize_t n msgrcv(msqid, msg, sizeof(msg.mtext), 1, 0); if (n -1) { perror(msgrcv); return 1; } printf(mtype%ld, text%s, len%zd\n, msg.mtype, msg.mtext, n);msgrcv 的第四个参数 msgtyp 是消息队列最好用、也最容易被忽略的特性msgtyp 等于 0取队列里第一条消息谁先到先取谁。msgtyp 大于 0取第一个 mtype 等于这个值的消息。msgtyp 小于 0取 mtype 小于等于这个值绝对值的最小消息。这实际上实现了一种简单的优先级调度。第五个参数常用的两个 flag 是 IPC_NOWAIT 和 MSG_NOERROR。IPC_NOWAIT 表示队列里没有符合条件的消息时不阻塞直接返回errno 置为 ENOMSG。MSG_NOERROR 表示如果队列里的消息比我给的缓冲区还长截断后返回给我而不是报 E2BIG。阻塞语义是消息队列和共享内存使用体验上最大的区别之一。发送方在队列满时会阻塞接收方在队列空时会阻塞也就是说内核直接帮你做了流控和数据同步。你的数据不会因为消费者没来得及取就丢也不会出现共享内存里那种读到半截的脏数据。系统对 System V 消息队列有几个默认限制/proc/sys/kernel/msgmax 控制单条消息最大字节数常见默认 8192/proc/sys/kernel/msgmnb 控制一个队列的总字节数常见默认 16384。如果你测试稍大数据msgsnd 返回 EAGAIN 或资源不足十有八九是触到这两个天花板。2.3 POSIX 消息队列与遗留 IPC 对象的清理问题Linux 上还有一套 POSIX 标准的消息队列接口和 System V 最大的区别是消息自带优先级而且可以注册异步通知。#include mqueue.h #include fcntl.h #include sys/stat.h struct mq_attr attr {0}; attr.mq_maxmsg 10; attr.mq_msgsize 128; mqd_t mq mq_open(/my_ipc_queue, O_CREAT | O_RDWR, 0644, attr); if (mq (mqd_t)-1) { perror(mq_open); return 1; } char buf[128] hello posix mq; unsigned int prio 1; mq_send(mq, buf, strlen(buf), prio); mq_receive(mq, buf, sizeof(buf), prio); mq_close(mq); mq_unlink(/my_ipc_queue);编译时在旧版 glibc 上需要链接-lrt新版本已经合并进 libc但习惯性加上不影响。POSIX 消息队列的名字在文件系统里可以对应到 /dev/mqueue 目录前提是挂载了 mqueue 文件系统。mq_notify 可以用来在消息到达时通知进程支持信号和线程两种通知方式比 System V 只能干等要灵活。说到清理这是我见过最多人踩的坑。System V IPC 对象的生命周期和任何进程都没有关系它属于内核。进程退出后消息队列不会自动消失必须显式调用 msgctl(msqid, IPC_RMID, NULL)或者在命令行用 ipcrm -q msqid 清理。开发机上用 ipcs -q 一看全是不知道哪个星期留下的僵尸队列这种情况在用了共享内存和信号量之后会更严重。我的习惯是所有 demo 程序都在退出路径里做完整清理包括对 SIGINT、SIGTERM 做收尾处理。否则一次 Ctrl-C 留下的内核对象就是下一次运行报Resource temporarily unavailable的元凶。3. 共享内存零拷贝背后的代价是没有规矩3.1 从虚拟内存角度理解共享内存为什么快换个生活化的类比管道、消息队列、socket 都像两个人通过中间人传纸条中间人每次要跑一趟纸条还要经过他的手。共享内存是两个人站在同一块白板前你写一笔他直接看到中间不需要任何人搬运。技术的本质是页表映射。每个进程有自己独立的虚拟地址空间正常情况下同一物理页只会映射到自己的空间里A 进程写的物理页和 B 进程写的物理页互不相干。但通过 shmget、mmap 这些接口可以让两个进程的页表项指向同一个物理页。物理页只有一份谁写都是写在这一份上另一方读到的自然是同一份内核全程不参与数据拷贝。所以共享内存被公认为 Linux 上吞吐量最高的 IPC 方式。数据在物理内存里只存在一份没有用户态和内核态之间的数据复制理论上 CPU cache 里的数据还能直接在多个核之间共享。实测里以几十 KB 数据块为粒度做压测共享内存加信号量的组合比管道高一到两个数量级并不夸张。3.2 shmget 与 mmap两种打开方式的取舍共享内存在 Linux 上主要有两套 API。System V 风格key_t key ftok(/tmp/shm_demo, 65); int shmid shmget(key, 4096, IPC_CREAT | 0666); char *buf shmat(shmid, NULL, 0); // 用 buf 读写字面意义上的共享内存 shmdt(buf); shmctl(shmid, IPC_RMID, NULL);POSIX 风格int fd shm_open(/my_shm, O_CREAT | O_RDWR, 0644); ftruncate(fd, 4096); char *buf mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); // 用 buf munmap(buf, 4096); close(fd); shm_unlink(/my_shm);两套方式在能力上等价差别在编码体验。System V 的 shmget 返回一个整数 id需要 shmat 再挂到地址空间整个过程有点绕。POSIX 的 shm_open 返回文件描述符再配合通用的 mmap思路和操作普通文件一致更容易理解也更容易在 Go、Rust 里通过 FFI 封装。POSIX 共享内存对象默认落在 /dev/shm本质是一个 tmpfs。这意味着一个附加好处直接用 mmap 映射一个普通文件也能实现进程间共享而且内容可以落盘持久化。比如多个进程要维护同一份配置缓存映射同一个文件一个进程更新了其他进程立即就能看到。我的取舍原则很简单老项目如果已经大量使用 sysvipc 风格的代码没必要推翻重来新项目一律用 POSIX 方式。父子进程之间做简单共享时还可以直接 mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0)配合 fork 使用省掉 shm_open 的步骤。3.3 共享内存示例与三个高频翻车点一个最经典的父子进程示例#include sys/ipc.h #include sys/shm.h #include stdio.h #include string.h #include unistd.h #include sys/wait.h #define SHM_SIZE 4096 int main() { key_t key ftok(/tmp/shm_demo, 65); int shmid shmget(key, SHM_SIZE, IPC_CREAT | 0666); if (shmid -1) { perror(shmget); return 1; } pid_t pid fork(); if (pid 0) { char *buf shmat(shmid, NULL, 0); if (buf (char *)-1) { perror(shmat); return 1; } strcpy(buf, hello from child); shmdt(buf); return 0; } wait(NULL); char *buf shmat(shmid, NULL, 0); if (buf (char *)-1) { perror(shmat); return 1; } printf(parent read: %s\n, buf); shmdt(buf); shmctl(shmid, IPC_RMID, NULL); return 0; }注意这个示例用了 wait(NULL) 来保证父进程等到子进程写完再读这在教学里可以但生产中不行。多个执行流并发访问同一块共享内存时你真正需要的是第 4 节的信号量。这里先铺垫三个高频翻车点第一个shmat 失败时返回的是(void *)-1不是 NULL。不检查这个返回值直接解引用必然段错误。写代码的时候别只判断 NULL。第二个shmctl(shmid, IPC_RMID, NULL) 的含义是标记删除不是立即销毁。已经 attach 这块内存的进程可以继续正常读写直到它们自己 shmdt。新进程从这一刻起无法再 attach。这个语义在编排进程退出顺序时非常容易误解我见过一个服务因为这个 bug重启后老进程还在写一块已经被标记删除的内存数据错乱查了半天。第三个mmap 映射的长度超过 ftruncate 设定的大小时访问越界部分会触发 SIGBUS不是普通的段错误也没有机会让你捕获并优雅处理。所以凡是 ftruncate 改了大小的地方长度校验一定要跟上。4. 信号量给共享内存装上 V 型路口4.1 信号量与互斥锁看着像其实不是一回事共享内存本身不提供任何同步能力它只是一块内存。多个进程同时往里写谁后写谁就覆盖前一个的结果读者可能看到的是半个新数据加半个旧数据。这个问题必须靠同步原语解决信号量就是为此设计的。信号量本质上是一个计数器最重要的两个原子操作是 wait 和 post。wait 把计数器减一如果结果小于零就阻塞post 把计数器加一如果有进程在等就唤醒一个。很多人以为信号量就是加强版互斥锁这个理解不准确。互斥锁有所有权概念哪个线程锁的就应该哪个线程解锁。信号量没有任何一个进程都可以对同一个信号量做 post。这一点在生产者消费者模型里是决定性的。生产者负责减少空槽位信号量、增加满槽位信号量消费者反过来操作两者操作的是不同的计数器没有谁必须持有锁的限制。另一个差别是计数能力。二进制信号量的值只有 0 和 1行为和互斥锁接近。但计数信号量可以初始化为任意非负整数用来描述停车场还剩多少个车位。每来一辆车剩余数量减一走一辆加一。这正好对应共享内存里环形缓冲区还剩多少个空位的场景。4.2 System V 与 POSIX 信号量的核心调用对比System V 信号量的核心是 semget、semctl、semop。#include sys/ipc.h #include sys/sem.h int semid semget(key, 1, IPC_CREAT | 0666); // 初始化第 0 个信号量为 1 semctl(semid, 0, SETVAL, 1); struct sembuf op; op.sem_num 0; op.sem_op -1; // P 操作申请资源 op.sem_flg SEM_UNDO; semop(semid, op, 1);sem_op 为负数表示申请资源为正数表示释放资源为 0 表示等待信号量变成 0。SEM_UNDO 标志很实用如果进程在持有信号量的时候崩溃了内核会自动帮它做撤销操作避免全员死锁。但注意这个撤销机制只存在于 System VPOSIX 信号量没有。POSIX 信号量的接口直观得多sem_t *sem sem_open(/my_sem, O_CREAT | O_EXCL, 0644, 1); sem_wait(sem); // P sem_post(sem); // V sem_close(sem); sem_unlink(/my_sem);sem_open 的最后一个参数是初值。如果不想用名字可以创建无名信号量但要注意无名信号量如果要在多个进程之间共享必须放在共享内存区域里且 init 时 pshared 参数传 1。放在普通堆栈上子进程永远看不到父进程对它的修改。4.3 一个完整的生产者消费者两个信号量调度一块共享缓存把共享内存和信号量组合起来才是生产中真正会用的姿势。下面这个例子父进程当生产者子进程当消费者中间只有一块共享内存作为单槽缓冲区。#include semaphore.h #include fcntl.h #include sys/mman.h #include sys/stat.h #include sys/types.h #include sys/wait.h #include unistd.h #include stdio.h #include stdlib.h #include string.h #define SHM_NAME /pc_shm struct shared { int value; }; int main() { sem_t *empty sem_open(/sem_empty, O_CREAT | O_EXCL, 0644, 1); sem_t *full sem_open(/sem_full, O_CREAT | O_EXCL, 0644, 0); int fd shm_open(SHM_NAME, O_CREAT | O_RDWR, 0644); ftruncate(fd, sizeof(struct shared)); struct shared *sh mmap(NULL, sizeof(struct shared), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); close(fd); pid_t pid fork(); if (pid 0) { for (int i 1; i 5; i) { sem_wait(empty); sh-value i; printf(producer put %d\n, i); sem_post(full); } return 0; } for (int i 1; i 5; i) { sem_wait(full); printf(consumer got %d\n, sh-value); sem_post(empty); } wait(NULL); munmap(sh, sizeof(struct shared)); shm_unlink(SHM_NAME); sem_close(empty); sem_close(full); sem_unlink(/sem_empty); sem_unlink(/sem_full); return 0; }empty 信号量初值是 1表示一开始有一个空槽位full 信号量初值是 0表示还没有可消费的数据。生产者必须先 wait(empty) 拿到空位写入再 post(full) 告诉消费者有货了。消费者反过来。两个信号量把一个生产者的写动作和一个消费者的读动作严格隔离不会出现消费者读到旧值或者生产者覆盖还没被读走的数据。如果要把单槽扩成环形缓冲思路完全一样empty 初值设成缓冲区大小 N生产者和消费者各自维护 in、out 索引剩下的逻辑原封不动。编译命令是gcc -o sem_demo sem_demo.c -pthread这里有两个细节值得说。第一个sem_open 加 O_EXCL 是为了保证每一次运行都拿到一个干净的信号量但如果上一次运行在 sem_unlink 之前被 Ctrl-C 杀掉第二次运行就会因为同名信号量已存在而失败。所以 terminate 信号处理器里的收尾工作不是可选项。第二个sem_timedwait 在排查问题的时候非常好用给阻塞操作加一个超时僵死进程至少能打印日志退出而不是永远挂在那里。5. Unix Domain Socket面向本机的网络通信5.1 为什么 IPC 榜单里必须有 socket管道和共享内存各有各的缺点管道不能双向共享内存要自己管同步。Unix Domain Socket 正好站在中间它复用完整的 socket API却完全不经过网络协议栈。数据在同一个内核 socket 缓冲区里排队没有 IP 层、没有路由、没有网卡中断性能远高于 TCP loopback。选择 Unix Domain Socket 最大的原因其实是可迁移性。今天两边进程在同一台机器上用 AF_UNIX数据走内核 socket buffer延迟低明天业务量大了要拆到两台机器把地址族从 AF_UNIX 换成 AF_INET把路径名换成 IP 加端口逻辑代码几乎不用动。如果你的两个进程是 fork 出来的父子关系用 socketpair(AF_UNIX, SOCK_STREAM, 0, sv) 可以一步拿到一对已经连接好的 socket 描述符不需要 bind、listen、accept是父子进程间双向通信最高效的写法。5.2 SOCK_STREAM 与 SOCK_DGRAM本地版 TCP 与可靠的本地版 UDPUnix Domain Socket 支持两种类型。SOCK_STREAM 是流式行为像 TCP连接导向、可靠、按序到达但没有消息边界。适合做一般的请求响应数据交换。缺点是和管道一样应用层要自己处理粘包拆包。SOCK_DGRAM 是数据报式每次 sendto 对应一条完整消息recvfrom 一次取一条完整消息。这里有件很多人不知道的事本地数据报 socket 和 UDP 不一样它不会丢包也不会乱序。因为数据只在本机内核缓冲区里排队不到网线上走一圈没有网络会丢包的理由。所以需要消息边界又要可靠性的本地通信AF_UNIX 的 SOCK_DGRAM 是很合适的选择。Unix Domain Socket 还有一个其他 IPC 机制完全做不到的高级能力通过辅助数据在进程之间传递文件描述符。守护进程可以把打开好的 fd 传给业务进程让业务进程直接读写这个文件。这类能力用管道和共享内存实现起来异常困难。5.3 bind、listen、accept一个随手能跑的本地服务示例服务端#include sys/socket.h #include sys/un.h #include unistd.h #include stdio.h #include string.h int main() { int sfd socket(AF_UNIX, SOCK_STREAM, 0); struct sockaddr_un addr; memset(addr, 0, sizeof(addr)); addr.sun_family AF_UNIX; strcpy(addr.sun_path, /tmp/ipc2.sock); unlink(/tmp/ipc2.sock); if (bind(sfd, (struct sockaddr *)addr, sizeof(addr)) -1) { perror(bind); return 1; } listen(sfd, 5); int cfd accept(sfd, NULL, NULL); char buf[128]; ssize_t n read(cfd, buf, sizeof(buf) - 1); buf[n] \0; printf(server received: %s\n, buf); write(cfd, pong, 5); close(cfd); close(sfd); unlink(/tmp/ipc2.sock); return 0; }客户端#include sys/socket.h #include sys/un.h #include unistd.h #include stdio.h #include string.h int main() { int cfd socket(AF_UNIX, SOCK_STREAM, 0); struct sockaddr_un addr; memset(addr, 0, sizeof(addr)); addr.sun_family AF_UNIX; strcpy(addr.sun_path, /tmp/ipc2.sock); if (connect(cfd, (struct sockaddr *)addr, sizeof(addr)) -1) { perror(connect); return 1; } write(cfd, ping, 5); char buf[16] {0}; read(cfd, buf, sizeof(buf) - 1); printf(client received: %s\n, buf); close(cfd); return 0; }这里有几个必须知道的点。第一bind 之前必须先 unlink 旧的 socket 文件否则会报 EADDRINUSE。但 unlink 之后到 bind 完成之前有一个短窗口如果这时候有别的进程没有按这个约定可能连到错误的 socket 文件上。所以生产环境更建议用固定路径由服务端自己负责或者用 systemd socket unit 管理生命周期。第二sun_path 最大长度限制是 108 字节路径太长 bind 会失败。第三还有一个文件系统里看不到的抽象命名空间。把 sun_path 的第一个字节设成 \0后面接任意应用名内核会创建一个不落盘的 socket。抽象 socket 在进程退出后自动清理连 unlink 都不用做非常适合临时服务。struct sockaddr_un addr; memset(addr, 0, sizeof(addr)); addr.sun_family AF_UNIX; addr.sun_path[0] \0; strcpy(addr.sun_path 1, my-service);客户端连不上最典型的原因就是 connection refused。这种报错通常意味着目标路径不存在或者服务端根本没有 listen。看到ipc connection error, connection refused这类错误第一反应应该是去查服务端进程是否存活、socket 路径是否一致而不是翻业务逻辑。6. IPC 选型速查与实践中的那些坑6.1 六种机制横向对比把这几种机制放在一张表里选型时一眼就能看清楚机制数据粒度同步方式双向跨主机典型场景管道/FIFO字节流内核阻塞否否父子进程简单数据搬运System V 消息队列带类型消息内核阻塞否否多消费者按类型取消息POSIX 消息队列带优先级消息内核阻塞加通知否否异步解耦、短消息共享内存任意字节自备信号量或原子操作是否最大吞吐的批量数据交换Unix Domain Socket 流字节流内核阻塞是否通用本地通信Unix Domain Socket 报文消息内核阻塞是否需要有消息边界、又要可靠的通信TCP Socket字节流内核阻塞是是跨机器分布式部署6.2 拿到一个 IPC 报错我的排查顺序每次有人把 IPC 报错截图发我我基本按以下顺序查先跑ipcs -m -q -s看内核里残留了什么对象。第一件事永远是确认这个对象到底存不存在、权限够不够。如果是消息队列或共享内存返回资源不足十有八九是之前的进程没清理ipcs 一看就明白了。ipcrm -m shmid、ipcrm -q msqid、ipcrm -s semid直接清掉。再看 Unix Domain Socket 的监听状态用ss -x或lsof -U查本地 socket。如果你的程序报 connection refused但服务端进程还活着很大概率是 socket 路径对不上。文件系统里那个 .sock 文件到底是谁创建的、还有没有人在监听用ls -l看一下所有权和启动时间常常能直接发现问题。如果怀疑内核参数不足查 /proc/sys/kernel/msgmax、msgmnb、sem、shmmax 这几项。注意不要一上来就调大先确认是参数不够还是对象泄漏。我见过好几次因为没有 sem_unlink信号量越攒越多调大参数只是把死锁问题推迟了。最后还有一招万能的strace -e traceipc ./your_program把 System V IPC 相关的系统调用全部打出来。管理类系统调用里面参数都是数字打出来一比就能看出哪个对象创建失败了、errno 是什么。6.3 项目里最常踩的坑与我的固定组合拳这些年在实际项目里反复吃亏我总结出几句固定的心得。高吞吐场景我固定用共享内存加 POSIX 信号量加原子版本号的组合不用 System V 消息队列。数据结构里带一个 atomic 的序号字段写端写完数据先更新序号读端每次先看序号变了没有再用信号量做真正的互斥控制。这样吞吐和正确性都能兼顾。中等数据量、进程之间接口清晰、并且业务未来可能扩到多机的场景我直接上 AF_UNIX SOCK_STREAM。协议格式从第一天就用简单的 length-prefix 框架前四个字节是长度后面是负载。这样以后换成 TCP 只是改地址族的问题拆包逻辑一行都不用动。大量短消息、需要按优先级处理、而且业务本来就是异步解耦形态的我用 POSIX 消息队列加 mq_notify。mq_notify 的好处是进程不需要一直阻塞在 mq_receive 上可以干其他活消息到了再被唤醒。最后一条也是我想强调的一条清理逻辑和业务逻辑同样重要。共享内存、信号量、消息队列都是内核对象进程死了它不一定会跟着死。每个程序都要在退出路径上做 shm_unlink、sem_unlink、mq_unlink、close并且把 SIGINT、SIGTERM 都纳入收尾考虑。我踩过最大的坑就是在两台开发机上同时跑带共享内存的 demo忘了处理其中一个进程的清理结果另一台机器上跑出来的业务数据全是上一台进程留下的残影。把这几条记住至少能帮你省下两个通宵排障的时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价