在Linux下写多进程程序,时间一长一定会遇到一个尴尬:每个进程的地址空间是隔离的,A进程里面改一个变量,B进程完全看不见。要解决这件事,就得靠进程间通信IPC。而System V IPC,就是Linux上最常见的那套老牌进程间通信方案。它诞生年代久远,但直到今天,消息队列、共享内存、信号量这“三件套”依然是无数中间件、数据库、嵌入式服务的内核级基础设施。
这篇文章我会从实际使用的角度,把System V IPC这套东西拆开来讲:先用一个真实场景说清楚它到底解决什么问题,然后分别过一遍共享内存、消息队列、信号量的核心API、能跑的最小代码,再补充ipcs、ipcrm、内核参数这些日常运维一定会碰到的知识。最后是我踩过的坑和选型建议。适合正在学Linux系统编程的初学者,也适合需要排查线上IPC问题的运维和C/C++开发,读完之后你至少能自己写出一个能运行的demo,也能知道出问题的时候该往哪查。
1. 从一次实际需求聊起:进程间通信到底卡在哪
1.1 隔离地址空间带来的通信难题
先回到最底层的问题。Linux下的每个进程都有自己的虚拟地址空间,内核通过页表把虚拟地址映射到物理内存。这样的好处是进程之间天然隔离,一个进程崩溃不会把别人的内存踩坏,安全性有保障。但坏处也显而易见:你没法在进程A里写一个数据结构,然后让进程B直接读。两个进程看起来都操作“同一个”指针,实际上指向的是完全不同的物理页面。
我举一个特别常见的例子:一个采集程序不断从传感器读取数据,一个分析程序负责处理。这两个程序是独立进程,数据必须从采集端稳定地送到分析端。管道能传数据,但管道是按字节流来的,要自己切分消息边界;socket也能传,但包格式、连接管理都要自己处理;文件就更不用说了,磁盘IO的延迟和磨损都不合适。这时候System V IPC就有了用武之地:它提供一套内核级的对象,让进程之间可以共享内存、传递有结构的消息、做互斥同步。你不需要自己设计协议边界,内核已经帮你约定好了规则。
1.2 System V IPC 的三大件定位
System V IPC这个概念来自Unix System V,Linux把它全盘继承了下来。它一共提供三种对象,分别是信号量、共享内存、消息队列。很多初学者会把它们混在一起背,其实三个东西解决的问题完全不同。
共享内存是效率最高的数据交换方式,它把同一块物理内存映射到多个进程的虚拟地址空间里,写入即可见,没有内核缓冲区拷贝,适合大流量、低延迟的数据交换。但它本身不提供任何同步机制,多个进程同时读写会产生数据竞争。消息队列则是内核维护的一个个带类型的链表,发送方把消息扔进队列,接收方按类型取走,天然带边界,像快递柜一样一格一格分得清楚,适合进程间传递结构化短消息。信号量不负责传数据,它是一个计数器,只做一件事:保证临界区的互斥访问,防止多个进程同时改同一份数据。
所以正确姿势是:共享内存负责搬数据,信号量负责保证“搬的时候别打架”,消息队列负责传递控制指令和事件通知。我的建议是先把三者当成一组工具看待,而不是一个个孤立知识点。后面我分别拆开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 绕不开的基础设施:key、ID 和 ipc_perm
2.1 用 ftok 把两个整数变成一把“钥匙”
System V IPC对象是内核级的,不依赖创建它的进程存续,所以不同进程想访问同一个对象,必须有一个共同的名字。这个名字就是key_t。key_t本质上就是一个整数,但为了让多个进程能约定出同一个整数值,标准做法是用ftok函数生成:
c复制#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
ftok的逻辑是:取pathname对应文件的st_dev(设备号)和st_ino(inode号),再结合proj_id的低8位,通过一系列位运算混合成一个key_t。它要求pathname必须指向一个真实存在、且调用者有访问权限的文件。
这里有个大坑,我曾线上踩过:有人用/tmp下的临时文件作为pathname,结果系统重启后临时文件被清理,程序重新创建同一个路径名的文件,inode已经变了,ftok算出的key跟之前完全不同。于是新启动的进程访问不到老进程创建的IPC对象。正确做法是选一个基本不变的文件作为pathname,比如程序的配置目录、日志目录,或者干脆用项目部署路径下的固定文件。
另外proj_id只需要低8位有效,也就是0到255。如果传一个超过255的值,高8位会被丢弃,多个项目之间可能撞key。我习惯直接用项目代号,例如0x01、0x02这种一眼能认出来的值。如果真的需要完全避免key冲突,也可以用IPC_PRIVATE作为key创建对象,后面我会讲它的使用限制。
2.2 为什么说 IPC 操作永远是“先拿 ID,再干活”
ftok生成的是key_t,但实际操作IPC对象时用的不是key,而是一个整数ID。这个ID是由内核在创建对象时分配的唯一编号,类似于文件描述符。从key到ID靠的是xxxget系列函数:
- shmget:根据key得到共享内存ID
- msgget:根据key得到消息队列ID
- semget:根据key得到信号量集ID
这几个get函数都有一个flag参数,最常用的组合是IPC_CREAT | 0666,意思是:如果key对应的对象不存在就创建,权限是八进制的0666,即所有用户可读写。如果加了IPC_CREAT | IPC_EXCL,那再遇到已存在的对象就会直接报EEXIST错误,这有点像open的O_CREAT|O_EXCL,常用来保证“只能由第一个进程创建”。
我把key和ID的区别比喻成Path和fd的关系:key是你要找的文件路径,ID是打开之后内核给你的句柄。打开之后你对句柄操作,不再关心路径。这种设计的好处是,内核可以在get时做权限检查和对象查找,之后的所有操作都基于ID索引,效率高。
还有一套配套的权限结构体ipc_perm,定义在sys/ipc.h里:
c复制struct ipc_perm {
uid_t uid; /* 属主有效用户ID */
gid_t gid; /* 属主有效组ID */
uid_t cuid; /* 创建者用户ID */
gid_t cgid; /* 创建者组ID */
mode_t mode; /* 权限位,如0666 */
};
注意:IPC对象创建时记录的uid是创建进程的有效用户ID。如果进程以root创建了共享内存,之后以普通用户身份运行的进程去shmat,即使mode是0666,也可能因为实际权限检查规则不通过而失败。这个问题我见过不止一次,后面排查专节再细说。
3. 共享内存:效率最高的 IPC,坑也最多
3.1 核心 API 与一段能跑的最小示例
共享内存的使用流程概括起来四步:创建或获取ID、映射到自己的地址空间、读写、解除映射。对应的四个函数是shmget、shmat、shmdt、shmctl。
c复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_KEY 0x1024
#define SHM_SIZE 1024
/* 写端:创建共享内存并写入一行字符串 */
int main(void) {
int shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
if (shmid < 0) { perror("shmget"); exit(1); }
char *addr = shmat(shmid, NULL, 0);
if (addr == (void *)-1) { perror("shmat"); exit(1); }
strcpy(addr, "hello from shm writer");
printf("writer: %s\n", addr);
/* 解除映射,不删除共享内存,留给读端 */
shmdt(addr);
return 0;
}
我建议初次接触时,把上面的代码编译运行一遍,再用ipcs看一下。
读端代码则简单得多:用相同的key调用shmget获取同一个ID,shmat之后直接printf里面的内容。注意读端shmget可以不传IPC_CREAT,只用0666,这样如果写端还没创建,shmget会直接报ENOENT,能及早暴露问题。
c复制/* 读端:获取写端创建的共享内存并读取 */
int shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
if (shmid < 0) { perror("shmget"); exit(1); }
char *addr = shmat(shmid, NULL, 0);
if (addr == (void *)-1) { perror("shmat"); exit(1); }
printf("reader: %s\n", addr);
shmdt(addr);
/* 最后一个使用者在确认无人再需要后,删除对象 */
shmctl(shmid, IPC_RMID, NULL);
有几个细节值得强调。第一,shmat返回的不是普通NULL而是(void*)-1表示失败,判断时别写反。第二,shmat的第二个参数传NULL,表示让内核帮忙选一个合适的虚拟地址,这是最安全的做法,不要自己指定固定地址,除非你对进程地址空间布局非常清楚。第三,shmdt之后原来那块地址就不能再访问了,继续读写会触发段错误,这个和free之后再用悬垂指针一样危险。
最后是生命周期问题。共享内存对象一旦创建,不会因为进程退出而消失。只有两种途径销毁:显式调用shmctl(shmid, IPC_RMID, NULL),或者系统重启。我把这个特性称为“双刃剑”,因为它既能让数据在进程间无缝传递,也会因为忘记删除而变成僵尸资源。
3.2 共享内存为什么必须配同步机制
共享内存本身没有任何同步机制,这是设计使然,不是缺陷。它就像一块白板,两个进程都能往上写字,但如果一个进程正在写一半,另一个进程就开始读,读到的就是残缺内容。
我用一个简单场景解释:写端往共享内存里放一个结构体,包含一个整型长度字段和一个数据数组。它先写长度,再写数据。如果读端在“长度已写、数据未写完”的时间窗口去读,就会拿到错误长度,或者读到属于上一次的旧数据。这类问题不一定会稳定复现,但一旦出现,排查成本极高,因为它是时序相关bug。
解决办法是给共享内存加上锁或信号量。经典组合是“共享内存 + 信号量”:写端先做P操作把信号量减一,进入临界区,写完数据后做V操作加一;读端则先P操作再读取,读完V操作。这样任何时刻只有一个进程在访问那块区域。如果只是单写多读场景,还可以用读写锁;如果数据是定长的、写入是原子的,甚至可以不加锁,但“不加锁”必须有严格依据,而不是拍脑袋。
另一个被忽略的坑是shm_nattch。用ipcs -m能看到每块共享内存当前有多少个进程attach在上面。shmat一次nattch就加一,shmdt减一。如果进程异常崩溃,内核会自动做detach,所以正常情况下nattch会归零。但如果你调用了IPC_RMID,而当时还有进程attach在上面,内核不会立刻销毁这块内存,而是等最后一个进程detach之后才真正释放。这个机制保护了正在使用内存的进程,但也会造成一种假象:内存明明删了,ipcs里还能看到。你别慌,等所有attach进程退出后它就会自然消失。
4. 消息队列:天然带边界的异步通道
4.1 核心 API 与一段能跑的最小示例
消息队列适合传递短小、有明确边界的消息。每个消息由一个长整型mtype和一段正文mtext组成。发送方用msgsnd,接收方用msgrcv。
c复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MSG_KEY 0x2048
struct msg_buf {
long mtype;
char mtext[128];
};
/* 发送端 */
int main(void) {
int msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
if (msqid < 0) { perror("msgget"); exit(1); }
struct msg_buf msg;
memset(&msg, 0, sizeof(msg));
msg.mtype = 1;
strcpy(msg.mtext, "hello from msg sender");
if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) < 0) {
perror("msgsnd");
exit(1);
}
printf("sender: sent message\n");
return 0;
}
接收端的关键是msgrcv的第三个参数和第四个参数。第三个参数是接收缓冲区可用大小,必须大于等于消息正文长度,否则默认会报E2BIG。第四个参数msgtyp决定了取哪条消息,这个参数很有讲究。
c复制/* 接收端 */
int msqid = msgget(MSG_KEY, 0666);
if (msqid < 0) { perror("msgget"); exit(1); }
struct msg_buf rcv;
memset(&rcv, 0, sizeof(rcv));
if (msgrcv(msqid, &rcv, sizeof(rcv.mtext), 0, 0) < 0) {
perror("msgrcv");
exit(1);
}
printf("receiver: %s\n", rcv.mtext);
msgctl(msqid, IPC_RMID, NULL);
msgtyp参数有三个语义区间,我整理在下面:
| msgtyp值 | 行为 |
|---|---|
| 0 | 取队列中第一条消息,即先进先出 |
| 正数 | 取mtype等于该值的消息,若有多个同类型则先进先出 |
| 负数 | 取mtype小于等于绝对值的最小mtype消息 |
这个设计非常适合做“多接收方按类型分流”:比如mtype为1是控制指令,2是数据帧,接收方可以只取自己关心的类型,内核帮你完成筛选。第三个参数还有最后一个标志位MSG_NOERROR,加上它之后,如果队列里消息长度超过缓冲区大小,不会报错,而是截断后接收。我的建议是,除非你清楚数据长度边界,否则不要用这个标志,留着报错就等于多了一道防护。
4.2 msgsz、msgtyp 和那几类经典错误
消息队列的边界是内核强制的。发送时msgsnd的第二个参数实际传入的是“正文大小”,不包含mtype。内核会把mtype和mtext拼在一起存储,所以消息的真实长度等于sizeof(long) + msgsz。接收时msgrcv的第三个参数必须大于或等于发送时填入的正文大小,否则要么截断要么报E2BIG。
这里有一个我在实际项目中踩过的坑:不同进程编译时结构体padding不同,虽然mtype在结构体开头,但两个进程如果对struct msg_buf的定义不一致,比如一个进程的mtext是char[128],另一个是char[256],互相收发就会出问题。我建议消息结构体固定为“long mtype + 定长char数组”,不要塞可变长度字段和容易受对齐影响的结构体。
消息队列还有一个容量限制,由内核参数msgmnb控制,默认一般是一万多字节,指的是一个队列里所有消息正文的总字节上限。如果发送方速度远快于接收方,msgsnd会阻塞,这个阻塞行为是可控的:第四个参数flags传IPC_NOWAIT就会立即返回EAGAIN,让你有机会做自己的调度。我在生产者消费者模型里通常不会让发送方无限阻塞,而是配合IPC_NOWAIT计数,超过阈值就丢弃或者落盘,防止进程被消息背压拖死。
还有一点值得知道:消息队列没有类似“共享内存nattch”这样的引用计数,创建它的进程退出后队列依然存在,消息也依然存在。如果你只是删除了接收端进程,忘了IPC_RMID,消息会一直堆积。我见过一台机器上几十个废弃队列占满msgmni,新进程msgget直接报ENOSPC的情况,后面资源管理部分会专门讲清理思路。
5. 信号量:用 PV 操作给临界区上锁
5.1 核心 API 与一段能跑的最小示例
System V信号量以“信号量集”为单位,一个集合里可以包含多个信号量。最常用的API是semget、semop、semctl。其中semop负责P/V操作,semctl负责初始化值、查询状态、删除集合。
c复制#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define SEM_KEY 0x3072
/* 注意:较新版本的 glibc 不帮你定义 semun,需要自己写 */
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
int main(void) {
int semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);
if (semid < 0) { perror("semget"); exit(1); }
/* 初始化信号量值为1,代表一个可用资源 */
union semun su;
su.val = 1;
if (semctl(semid, 0, SETVAL, su) < 0) { perror("semctl"); exit(1); }
/* P 操作:申请资源 */
struct sembuf op;
op.sem_num = 0;
op.sem_op = -1;
op.sem_flg = SEM_UNDO;
if (semop(semid, &op, 1) < 0) { perror("semop P"); exit(1); }
/* 临界区代码,模拟保护 */
printf("in critical section\n");
/* V 操作:释放资源 */
op.sem_op = 1;
if (semop(semid, &op, 1) < 0) { perror("semop V"); exit(1); }
semctl(semid, 0, IPC_RMID, NULL);
return 0;
}
semop的语义说穿了就三句话:sem_op为负数,申请资源,信号量当前值必须大于等于|sem_op|,否则阻塞等待;sem_op为正数,释放资源,信号量值增加;sem_op为0,等待信号量值变为0,这个用途比较少见,多用于同步屏障。
注意一个关键点:信号量的值不能小于0。SV信号量的计数逻辑是“值>=请求量就减,否则等待”,而不是像某些语言信号量可以进入负数表示等待队列长度。所以在初始化时,要把信号量值理解成“当前可用资源数”。
5.2 SEM_UNDO 是保命符,不是所有问题的解药
semop中的sem_flg可以传SEM_UNDO,这是System V信号量最人性化的设计之一。含义是:当进程异常退出时,内核自动撤销这次操作对信号量值的影响。我举个例子,进程P做了P操作把信号量从1变成0,然后它在临界区里崩溃了。如果没有SEM_UNDO,信号量永远是0,其他所有进程都会永久阻塞,造成死锁。加上SEM_UNDO后,内核检测到进程退出,会把信号量恢复为1,其他进程得以继续。
听起来SEM_UNDO是万能保险,但它有两个缺点。第一,它只回滚量,不回滚业务状态。假设共享内存里正在写一个链表,写着写着进程崩溃,SEM_UNDO虽然把锁解开了,但链表已经处于半写状态,后面进程拿到的是一份脏数据。所以真正的健壮做法是:共享内存里记录完整写入标志或事务序列号,拿到锁之后先校验数据的完整性。第二,滥用SEM_UNDO会掩盖逻辑错误。有些死锁本来就是程序逻辑bug,加上SEM_UNDO之后,进程退出后锁被自动解开,你以为问题解决了,其实只是眼不见心不烦。
我个人习惯是:涉及保护共享数据结构的信号量,一定加SEM_UNDO,因为线上环境什么意外都可能发生,宁可在恢复后做数据校验,也不能让整个集群全部卡死。而用于资源计数的信号量,比如限流器,要不要加取决于你对进程退出后进行资源回收的要求,通常也要加。
关于semctl,它承担了信号量的“管理”功能:SETVAL设置某个信号量的初值,GETVAL读取当前值,IPC_RMID删除整个信号量集。注意删除时传的是semid,而不是具体的信号量编号。还有那个union semun,在旧版glibc里是系统定义的,新版本里你需要自己声明,很多初学的人在这里编译报错后一脸懵,见到就补上定义即可。
6. 资源管理:ipcs、ipcrm 与内核参数调优
6.1 先学会看状态,再谈清理
System V IPC对象一旦创建就会一直存在,线上环境必须养成看状态的习惯。查看三个对象的命令分别是ipcs -m、ipcs -q、ipcs -s,也可以不加参数一次全看。我自己最常用的是ipcs -a和ipcs -p,其中-a把所有信息展示出来,-p能看到每个对象最近一次操作的进程PID,排查谁没有清理资源时非常有用。
举一个典型的输出片段:
bash复制------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00001024 0 root 666 1024 1
看到nattch为1,就说明有一个进程还在attach这块共享内存。如果这个数字一直不降,说明某个进程没有shmdt,属于资源泄漏,要从进程那边排查。
清理命令是ipcrm,用法很简单:
bash复制ipcrm -m 共享内存ID # 按shmid删除
ipcrm -q 消息队列ID # 按msqid删除
ipcrm -s 信号量集ID # 按semid删除
也可以按key删除:ipcrm -M key, ipcrm -Q key, ipcrm -S key。但我更喜欢按ID删,因为同一个key在删除后如果被重新创建,ID会变,用key可能有歧义。每次操作之前先ipcs确认一遍ID,避免误删正在使用的对象。
这里提醒一点:非root用户只能删除自己创建的IPC对象,删除别人的对象即使权限位是0666也可能被拒绝。遇到Permission Denied时先确认进程的有效uid和对象创建者uid。
6.2 内核参数与常见调优思路
System V IPC的所有资源上限都由内核参数控制,路径在/proc/sys/kernel/下。稍大一点的系统都值得看一眼,按项目需求调优。
共享内存相关的主要是:
bash复制kernel.shmmax # 单个共享内存段的最大字节数
kernel.shmall # 系统共享内存页总数上限
kernel.shmmni # 共享内存段数量上限
shmmax的逻辑默认值很高,一般不用动。真正容易撞的是shmmni,即共享内存段的总数上限。如果你的服务按请求动态创建共享内存又没删干净,段数量会一路涨到上限,新shmget直接失败。消息队列的关键参数是msgmax、msgmnb、msgmni,很多老系统的msgmax默认只有8KB左右,这在今天看来非常小,消息稍微大一点就到上限。信号量参数是一行四个数字,含义分别是SEMMSL(每个信号量集最多信号量数)、SEMMNS(系统内信号量总数)、SEMOPM(每次semop最多同时操作数)、SEMMNI(信号量集总数上限)。默认值一般是250 32000 100 128,足够应付大多数场景。
调参有两种方式。临时生效直接写/proc/sys/kernel目录,适合线上快速验证;永久生效写/etc/sysctl.conf:
bash复制# 示例:提高消息队列上限
kernel.msgmax = 65536
kernel.msgmnb = 131072
kernel.msgmni = 2048
改完sysctl -p生效。我的建议是,参数要跟着业务特征走,不要盲目调大。比如消息队列mnb调得很大,消息积压就会吃掉大量内核内存;信号量mni调大,也意味着内核要维护更多对象。我一般先压测,看ipcs的数量和内存占用数据,再决定是否调整。
7. 实践中必须知道的坑与选型建议
7.1 高频踩坑清单
做System V IPC开发这么久,很多坑其实是重复出现的,我整理成一张速查表,方便排查时对照。
| 现象 | 大概率原因 | 解决办法 |
|---|---|---|
| shmget / msgget / semget 返回ENOENT | key对应的对象不存在,或没有加IPC_CREAT | 确认是否先创建,确认key一致 |
| 能创建但访问失败 | ftok的path文件被删过,inode变了 | 统一用固定路径,路径文件不要清除 |
| 消息队列msgrcv报E2BIG | 缓冲区大小小于消息正文 | 增大缓冲区,或加MSG_NOERROR |
| msgsnd阻塞不返回 | 队列容量已满,发送端没带IPC_NOWAIT | 视场景决定用阻塞或非阻塞 |
| 共享内存数据读到乱码 | 写读之间没有同步信号量 | 加信号量做互斥,或用原子操作 |
| ipcs里看到大量废弃对象 | 进程退出前没有调IPC_RMID | 在进程收尾逻辑统一清理 |
| semop被信号打断返回EINTR | 信号打断了阻塞中的semop | 判断errno为EINTR后重试 |
| 非root删不掉对象 | 对象属主不是当前用户 | 用root执行,或用IPC_SET改属主 |
除了表里的内容,还有两条我认为更重要。第一条:别在共享内存里放指针。共享内存映射到不同进程的虚拟地址可能完全不同,你在写端存了一个指向堆内存的指针,读端去解引用大概率段错误。正确做法是存偏移量,或者只放定长数组。第二条:消息队列消息的mtype不能为0,内核把它当非法值,msgsnd会直接报EINVAL。这个规定很反直觉,我第一次写的时候就栽过。
7.2 System V 与 POSIX IPC 怎么选
Linux还有一套更新一点的POSIX IPC,API风格和System V不同:共享内存用shm_open加mmap,消息队列用mq_open加mq_send/mq_receive,信号量用sem_open加sem_wait/sem_post。我经常被人问到底用哪套好。
POSIX IPC的API更符合现代人习惯,名字不带get/ctl这类模糊动词,支持用名字字符串标识对象,还支持通过文件描述符配合poll/select监听,集成度更好。但System V IPC在Linux上的实现非常成熟,很多数据库和中间件都基于它,兼容性极好,跨Unix系统时行为差异也小。我自己的选型逻辑是这样的:如果是新项目、代码全由自己团队维护,用POSIX IPC开发效率更高;如果是给存量系统打补丁、或者要嵌入到老中间件里,沿用System V更稳妥。对于纯粹的进程间大数据交换,现代高性能方案更倾向直接用mmap匿名映射加原子变量,System V共享内存正在被慢慢替代,但它的知识体系依然是理解Linux进程间通信的基石。
最后再聊一点经验层面的东西:System V IPC这套东西难不在API,而在于资源生命周期管理。它属于“内核不会自动帮你收尸”的类型,所以每个创建了对象的进程,都要把清理逻辑写进收尾代码里。我记得早期做采集系统的时候,因为忘了删消息队列,导致同一台机器上部署多个实例时互相读到旧数据,排查了两天才定位到是msgmni资源耗尽和key冲突叠加的结果。从那之后我在所有用到IPC的项目里都立了一条规矩:启动时清理同名旧对象,退出时删除自己创建的对象,必要的时候用一个有独立key的“管理进程”统一负责创建和销毁。这套流程虽然多几行代码,但能把线上七八成IPC问题提前消灭掉。
如果你要开始写自己的第一个System V IPC程序,我建议就从共享内存加信号量这个组合入手,先跑通一次完整的数据写入和读取,再用ipcs跟踪对象的状态变化。把get、attach、op、ctl这几个环节都亲手敲一遍,比背十遍API列表都有用。等这套流程形成肌肉记忆,再去看源码和内核实现,理解的深度会完全不同。
