1. 引言
在 Linux 多进程编程中,进程间通信(Inter-Process Communication,IPC)是核心话题。本文将系统梳理信号(Signal)以及 System-V IPC 的三大件:消息队列、共享内存、信号量,涵盖核心 API、参数含义、注意事项与生活化比喻,帮助读者快速建立知识框架。
2. 信号(Signal)
信号是 Linux 中一种异步通知机制,用于告知进程发生了某个事件。这里重点介绍两个最常用的信号发送函数:alarm 和 kill。
2.1 alarm:定时闹钟
c
unsigned int alarm(unsigned int seconds);
- 参数 :
seconds秒数 - 返回值:上一次 alarm 剩余秒数;没有则返回 0
- 作用 :隔
seconds秒之后,给当前进程发送SIGALRM闹钟信号 - 注意点 :
- 调用 alarm 会覆盖上一次闹钟
seconds=0代表取消闹钟- 闹钟信号只会发一次
- 比喻:手机定时闹钟,到点给你发提醒,响一次就结束
2.2 kill:发送信号
c
int kill(pid_t pid, int sig);
- 参数 :
pid:目标进程 IDsig:要发送的信号编号
- 返回值:成功返回 0;失败返回 -1
- 作用:向 pid 对应的进程发送指定信号
- 注意点 :
- 不是只能杀死进程,可以发任意信号
- 权限不够会失败
pid>0发给指定进程
- 比喻:给别的程序发一条通知消息,可以是终止、暂停等不同指令
3. IPC 对象总览
System-V IPC 包含三类对象:消息队列、共享内存、信号量。
每一个
key(由ftok生成),可以同时拥有:1 个消息队列 + 1 块共享内存 + 1 组信号量数组。
查看与删除命令:
bash
ipcs -q/m/s # 查看消息队列/共享内存/信号量
ipcrm -Q/M/S key # 通过key删除对象
ipcrm -q/m/s id # 通过id号删除对象
3.1 ftok:生成 IPC 的 key 值
c
key_t ftok(const char *pathname, int proj_id);
- 参数 :
pathname:存在的文件路径proj_id:项目 ID,只使用低 8 位
- 返回值:成功返回 key_t 类型 key;失败返回 -1
- 作用:根据文件路径 + 项目 ID,生成一个唯一 key,作为 IPC 对象名字
- 注意点 :
- path 文件不能删除
- 不同文件 + 相同 proj_id 得到不同 key
- 比喻:根据文件和编号生成一个房间门牌号,进程之间靠这个门牌号找到同一个 IPC 对象
4. 消息队列
消息队列是内核维护的一个消息链表,进程可以向其中投递或读取消息。
流程 :ftok 生成 key → msgget 创建消息队列 → msgsnd 发消息 → msgrcv 收消息 → msgctl 删除队列
4.1 msgget:创建/获取消息队列
c
int msgget(key_t key, int msgflg);
- 参数 :
key:ftok 得到的 key 值msgflg:IPC_CREAT不存在就创建;IPC_EXCL存在就报错;还要或上权限 0666
- 返回值:成功返回消息队列 ID;失败 -1
- 作用:根据 key 创建 / 打开消息队列,拿到操作 id
- 注意点 :key 填
IPC_PRIVATE会创建私有队列,只能父子进程使用 - 比喻:拿到信箱的编号,后续收发信件都用这个编号
4.2 msgsnd:发送消息
c
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 参数 :
msqid:消息队列 idmsgp:自定义消息结构体首地址,第一个成员必须是 long mtype(消息类型)msgsz:消息正文字节大小,不包含 mtypemsgflg:一般写 0,队列满时阻塞;IPC_NOWAIT队列满直接返回不阻塞
- 返回值:成功 0;失败 -1
- 作用:往消息队列投递一条消息
- 注意点:消息结构体格式强制要求;msgsz 只算数据部分,不要把 long 类型算进去
- 比喻:把信件投入信箱
4.3 msgrcv:接收消息
c
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
- 参数 :
msqid:队列 idmsgp:存放消息的缓冲区msgsz:缓冲区最大字节数(正文)msgtyp:要接收哪一类消息;0 读取队列第一条;正数读取对应类型消息msgflg:0 没有消息就阻塞等待;IPC_NOWAIT无消息直接返回
- 返回值:成功返回收到的正文字节数;失败 -1
- 作用:从消息队列取出消息,可以按消息类型筛选
- 注意点:可以定向接收某一类消息;消息读完队列就删掉这条消息
- 比喻:从信箱取出信件,可以指定只拿某一类信件
4.4 msgctl:消息队列控制
c
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- 参数 :
msqid:队列 idcmd:IPC_STAT:获取队列状态信息IPC_SET:修改权限IPC_RMID:删除消息队列
buf:状态结构体指针;删除时传 NULL
- 返回值:成功 0;失败 -1
- 作用:获取状态、修改权限、销毁消息队列
- 注意点 :程序退出消息队列不会自动销毁,必须手动
IPC_RMID或者ipcrm命令删除,否则会残留在系统 - 比喻:查看信箱信息、修改信箱权限、拆掉整个信箱
练习:send.c 终端读字符串发送消息队列;recv.c 读取消息打印输出。
5. 共享内存
共享内存是 IPC 里面速度最高 的方式,直接映射物理内存到进程虚拟地址,不需要拷贝数据;但是没有自带同步机制,一般配合信号量做互斥。
操作步骤:
- ftok 生成 key
- shmget 创建 / 打开共享内存,拿到 shmid
- shmat 将共享内存映射到当前进程虚拟地址空间,得到内存指针
- 直接读写这块内存
- shmdt 解除映射(只是断开本进程,不销毁共享内存)
- shmctl (IPC_RMID) 销毁共享内存对象
5.1 shmget:创建共享内存
c
int shmget(key_t key, size_t size, int shmflg);
- 参数 :
key:ftok 生成 keysize:共享内存字节大小,一般是 4096 整数倍(页大小)shmflg:IPC_CREAT|0666;IPC_EXCL存在报错
- 返回值:成功返回共享内存 id;失败 -1
- 作用:创建一块共享内存,申请物理内存页
- 注意点:size 建议按页对齐;创建之后 size 不能修改
- 比喻:申请一块公共黑板,多个进程都可以来这块黑板写字
5.2 shmat:映射共享内存
c
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 参数 :
shmid:共享内存 idshmaddr:填NULL,让操作系统自动选虚拟地址(推荐)shmflg:0 可读可写;SHM_RDONLY只读映射
- 返回值 :成功返回映射后的虚拟地址指针;失败
(void*)-1 - 作用:把共享内存挂载到自己进程地址空间,拿到指针直接读写
- 注意点:返回值是 void*,需要强转成自己需要的类型;出错返回 -1,判断不能直接 ==NULL
- 比喻:把公共黑板搬到自己家窗户边,伸手就能读写黑板内容
5.3 shmdt:解除映射
c
int shmdt(const void *shmaddr);
- 参数 :
shmaddr:shmat 返回的映射首地址 - 返回值:成功 0;失败 -1
- 作用:断开当前进程和共享内存的映射
- 注意点 :shmdt 不会销毁共享内存对象,只是当前进程不能再访问;别的进程还能用
- 比喻:把自家窗户关上,看不到黑板了,黑板本身还在
5.4 shmctl:共享内存控制
c
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 参数 :
shmid:共享内存 idcmd:IPC_STAT获取状态IPC_SET修改权限IPC_RMID标记销毁共享内存
buf:状态结构体,删除时传 NULL
- 返回值:成功 0;失败 -1
- 作用:获取状态、修改权限、销毁共享内存
- 注意点 :
IPC_RMID只是标记删除;所有进程都 shmdt 解除映射之后,内存才真正释放 - 比喻:标记黑板要拆掉;等所有人都离开黑板之后,黑板才真正拆除
6. 信号量(System-V 有名信号量数组)
信号量本质是计数器,用来做进程互斥、同步;不是用来传数据,是用来做锁。
流程 :ftok 生成 key → semget 创建信号量数组 → semctl 初始化信号量初值 → semop P/V 操作(申请 / 释放锁)→ semctl IPC_RMID 删除信号量数组
6.1 semget:创建信号量数组
c
int semget(key_t key, int nsems, int semflg);
- 参数 :
key:ftok 的 keynsems:信号量数组里面有多少个信号量semflg:IPC_CREAT|0666;IPC_EXCL存在报错
- 返回值:成功返回信号量集合 id;失败 -1
- 作用:创建一组(多个)信号量
- 注意点:nsems 是数组元素个数;比如只需要一把锁填 1
- 比喻:申请一组门禁计数器,可以有多把锁
6.2 semctl:信号量控制
c
int semctl(int semid, int semnum, int cmd, ...);
- 参数 :
semid:信号量集合 idsemnum:操作数组里面第几个信号量,下标从 0 开始cmd:SETVAL:设置信号量初值(初始化锁)IPC_RMID:删除整个信号量数组,此时 semnum 忽略
- 返回值:成功返回 0;失败 -1
- 作用:初始化信号量、删除信号量集合
- 注意点 :创建完信号量默认值是随机,必须手动 SETVAL 初始化,这是高频踩坑点
- 比喻:给门禁锁设置初始可用数量;拆掉整套门禁
6.3 semop:P/V 操作
c
int semop(int semid, struct sembuf *sops, size_t nsops);
- 参数 :
semid:信号量集合 idsops:struct sembuf 操作数组,一次可以做多组 P/Vnsops:sops 数组有多少个元素
- 返回值:成功 0;失败 -1
- 作用:P 操作(申请锁,sem_op=-1);V 操作(释放锁,sem_op=+1)
c
struct sembuf
{
unsigned short sem_num; // 操作第几号信号量
short sem_op; // 操作类型:-1 为 P 操作,+1 为 V 操作
short sem_flg; // 一般填 0
};
- 比喻:P 操作是进门拿钥匙(锁的数量减一),V 操作是出门还钥匙(锁的数量加一)
7. 总结
| 机制 | 核心 API | 特点 | 典型用途 |
|---|---|---|---|
| 信号 | alarm / kill |
异步通知,非数据传输 | 定时、进程控制 |
| 消息队列 | msgget / msgsnd / msgrcv / msgctl |
按类型收发,有格式要求 | 结构化消息传递 |
| 共享内存 | shmget / shmat / shmdt / shmctl |
速度最快,无同步机制 | 大数据量高频读写 |
| 信号量 | semget / semctl / semop |
计数器,做锁 | 互斥与同步 |
理解这些 IPC 机制,是掌握 Linux 多进程协作的关键。建议结合代码练习,重点体会共享内存与信号量的配合使用。