Linux IPC 进阶:System V 消息队列与信号量(含内核管理深度解析)
🔥草莓熊Lotso:个人主页
❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:
文章目录
前言:
在 Linux 进程间通信(IPC)体系中,System V IPC 家族除了高效的共享内存,还包含消息队列和信号量两大核心组件。消息队列解决了 “数据结构化传输” 的需求,信号量则专注于 “同步与互斥”,二者与共享内存配合,可构建稳定、安全的跨进程通信方案。本文将从原理、API、实战到内核管理,全面拆解这两种 IPC 技术,带你吃透 System V IPC 的完整生态。
声明:本文中消息队列和信号量的代码示例是临时找的,后续博主在继续学习后会更新新的博客或者直接在此博客中进行修改重塑。本篇博客中目前的重点部分是内核管理IPC资源
一. System V 消息队列:结构化的跨进程通信
消息队列是一种 “基于消息的 IPC 机制”,核心是内核维护的一个链表结构的消息队列,进程可向队列中添加带类型的消息,也可按类型读取消息,实现结构化、异步的数据传输。
1.1 核心原理与特性
1.1.1 底层实现逻辑
消息队列的本质是内核中的链表,每个消息队列由唯一的key标识,每个消息包含三部分:
- 消息类型(正整数,用于接收方筛选消息);
- 消息长度(消息正文的字节数);
- 消息正文(实际传输的数据)。
进程通过msgsnd向队列发送消息(链表尾插入),通过msgrcv从队列读取消息(按类型从链表头或指定位置提取),内核自动管理消息的存储和同步。
1.2 核心 API 详解
System V 消息队列通过ftok、msgget、msgsnd、msgrcv、msgctl五个 API 实现完整操作,与共享内存的 API 设计逻辑一致,降低学习成本。

1.2.1 数据结构(内核管理结构体)
内核通过struct msg_queue管理消息队列属性(和我们查指令查出来的不同,这个是内核里的),核心字段如下:
structmsg_queue{structkern_ipc_perm q_perm;// 权限控制结构体(含key、uid、gid等)int q_id;// 消息队列ID time_t q_stime;// 最后一次msgsnd时间 time_t q_rtime;// 最后一次msgrcv时间 time_t q_ctime;// 最后一次属性修改时间unsignedlong q_cbytes;// 队列中消息总字节数unsignedlong q_qnum;// 队列中消息总数unsignedlong q_qbytes;// 队列最大允许字节数(默认16384) pid_t q_lspid;// 最后一次发送消息的进程PID pid_t q_lrpid;// 最后一次接收消息的进程PIDstructlist_head q_messages;// 消息链表头structlist_head q_receivers;// 等待接收消息的进程链表structlist_head q_senders;// 等待发送消息的进程链表};1.2.2 核心 API 使用
(1)ftok:生成唯一 Key(与共享内存通用)
#include<sys/ipc.h> key_t ftok(constchar*pathname,int proj_id);- 作用:将 “文件路径 + 项目 ID” 转换为唯一
key,作为消息队列的全局标识; - 注意:路径必须是存在的文件,
proj_id为非 0 整数(如0x6666)。
(2)msgget:创建 / 获取消息队列
#include<sys/msg.h>intmsgget(key_t key,int msgflg);- 参数:
key:ftok生成的 Key;msgflg:权限标志,常用组合:IPC_CREAT:不存在则创建,存在则获取;IPC_CREAT | IPC_EXCL:不存在则创建,存在则报错;
- 权限位(如
0666):控制进程对队列的访问权限;
- 返回值:成功返回消息队列 ID(msgid),失败返回 - 1。
(3)msgsnd:发送消息
#include<sys/msg.h>intmsgsnd(int msqid,constvoid*msgp, size_t msgsz,int msgflg);- 参数:
msqid:msgget返回的消息队列 ID;msgp:指向消息结构体的指针(需自定义,首字段必须是long mtype消息类型);msgsz:消息正文长度(不含消息类型字段);msgflg:发送标志(0阻塞,IPC_NOWAIT非阻塞);
- 返回值:成功返回 0,失败返回 - 1。

(5)msgctl:控制消息队列(核心功能:删除)
#include<sys/msg.h>intmsgctl(int msqid,int cmd,structmsqid_ds*buf);- 参数:
cmd:控制命令(核心为IPC_RMID,删除消息队列);buf:存储队列属性的结构体指针(IPC_RMID时可设为 NULL);
- 返回值:成功返回 0,失败返回 - 1。
1.3 实战案例:消息队列实现 C/S 通信
通过 “服务端 + 客户端” 演示消息队列的使用,服务端接收客户端消息并回复,客户端发送消息并接收回复。
1.3.1 公共头文件(comm.h)
#ifndef_COMM_H_#define_COMM_H_#include<stdio.h>#include<sys/ipc.h>#include<sys/msg.h>#include<string.h>#definePATHNAME"."#definePROJ_ID0x6666#defineMSG_SIZE1024// 消息结构体(首字段必须是long mtype)typedefstructmsgbuf{long mtype;// 消息类型(服务端→客户端:100,客户端→服务端:200)char mtext[MSG_SIZE];// 消息正文} msg_t;// 创建消息队列intcreateMsgQueue();// 获取消息队列intgetMsgQueue();// 发送消息intsendMsg(int msgid,long type,constchar*text);// 接收消息intrecvMsg(int msgid,long type,char*text);// 删除消息队列intdestroyMsgQueue(int msgid);#endif1.3.2 公共实现(comm.c)
#include"comm.h"// 内部通用函数:创建/获取消息队列staticintcommMsgQueue(int flags){ key_t key =ftok(PATHNAME, PROJ_ID);if(key <0){perror("ftok error");return-1;}int msgid =msgget(key, flags);if(msgid <0){perror("msgget error");return-2;}return msgid;}// 创建全新消息队列intcreateMsgQueue(){returncommMsgQueue(IPC_CREAT | IPC_EXCL |0666);}// 获取已存在的消息队列intgetMsgQueue(){returncommMsgQueue(IPC_CREAT);}// 发送消息intsendMsg(int msgid,long type,constchar*text){ msg_t msg; msg.mtype = type;strncpy(msg.mtext, text, MSG_SIZE -1);if(msgsnd(msgid,&msg,strlen(msg.mtext),0)<0){perror("msgsnd error");return-1;}return0;}// 接收消息intrecvMsg(int msgid,long type,char*text){ msg_t msg; ssize_t n =msgrcv(msgid,&msg, MSG_SIZE, type,0);if(n <0){perror("msgrcv error");return-1;} msg.mtext[n]='\0';strcpy(text, msg.mtext);return0;}// 删除消息队列intdestroyMsgQueue(int msgid){if(msgctl(msgid, IPC_RMID,NULL)<0){perror("msgctl error");return-1;}return0;}1.3.3 服务端(server.c)
#include"comm.h"#include<unistd.h>intmain(){// 1. 创建消息队列(通信发起者)int msgid =createMsgQueue();if(msgid <0){return1;}printf("服务端:创建消息队列成功,msgid=%d\n", msgid);// 2. 循环接收客户端消息(类型200)并回复(类型100)char buf[MSG_SIZE];while(1){memset(buf,0,sizeof(buf));// 接收客户端消息if(recvMsg(msgid,200, buf)<0){break;}printf("服务端收到:%s\n", buf);// 回复客户端if(strcmp(buf,"quit")==0){sendMsg(msgid,100,"客户端退出,服务端即将关闭");break;}sendMsg(msgid,100,"已收到你的消息");sleep(1);}// 3. 删除消息队列destroyMsgQueue(msgid);printf("服务端:消息队列已删除\n");return0;}1.3.4 客户端(client.c)
#include"comm.h"#include<unistd.h>intmain(){// 1. 获取消息队列int msgid =getMsgQueue();if(msgid <0){return1;}printf("客户端:获取消息队列成功,msgid=%d\n", msgid);// 2. 循环发送消息(类型200)并接收回复(类型100)char buf[MSG_SIZE];while(1){memset(buf,0,sizeof(buf));printf("客户端请输入:");fflush(stdout);fgets(buf, MSG_SIZE,stdin); buf[strlen(buf)-1]='\0';// 去除换行符// 发送消息sendMsg(msgid,200, buf);if(strcmp(buf,"quit")==0){break;}// 接收回复memset(buf,0,sizeof(buf));recvMsg(msgid,100, buf);printf("服务端回复:%s\n", buf);}printf("客户端:退出通信\n");return0;}1.3.5 编译与运行
# 编译 gcc server.c comm.c -o server gcc client.c comm.c -o client # 终端1:启动服务端 ./server # 输出:服务端:创建消息队列成功,msgid=123456# 终端2:启动客户端 ./client # 输出:客户端:获取消息队列成功,msgid=123456# 客户端请输入:hello# 服务端回复:已收到你的消息# 客户端请输入:quit# 服务端:收到:quit# 服务端:消息队列已删除1.4 消息队列避坑指南
- 消息结构体必须以 long mtype 开头:这是内核规定的格式,否则
msgsnd/msgrcv会报错; - 消息类型必须为正整数:
mtype不能为 0 或负数,否则发送失败; - 消息长度不含 mtype 字段:
msgsnd的msgsz参数是消息正文长度,不是整个结构体长度; - 必须手动删除队列:消息队列不会随进程退出而释放,需用
msgctl(IPC_RMID)删除,残留队列可通过ipcs -q查看、ipcrm -q msgid删除。
二. System V 信号量:同步与互斥的核心工具
信号量并非用于数据传输,而是用于保护临界资源,实现进程间的同步与互斥。它本质是一个 “内核维护的计数器”,通过 P/V 操作(申请 / 释放资源)控制进程对临界资源的访问。
2.1 核心概念铺垫
在学习信号量前,需明确三个关键概念:
- 临界资源:多个进程共享的资源(如共享内存、文件),一次仅允许一个进程访问;
- 临界区:进程中访问临界资源的代码段;
- 同步与互斥:
- 互斥:任意时刻仅允许一个进程进入临界区(如多个进程写共享内存);
- 同步:多个进程访问临界资源时需遵循特定顺序(如 “生产者先写,消费者再读”)。
信号量的核心是 “通过计数器控制资源访问权限”,计数器值代表 “可用资源数量”:
P 操作(申请资源):计数器 - 1,若计数器 < 0,进程阻塞;V 操作(释放资源):计数器 + 1,若计数器≤0,唤醒一个阻塞进程。

2.2 核心原理与特性
2.2.1 底层实现逻辑
System V 信号量是一个 “信号量集”(可包含多个信号量),内核通过struct sem_array管理信号量集属性,每个信号量通过struct sem描述:
structsem{int semval;// 信号量计数器值 pid_t sempid;// 最后一次操作该信号量的进程PIDunsignedshort semncnt;// 等待计数器>0的进程数unsignedshort semzcnt;// 等待计数器=0的进程数};2.2.2 核心特性
- 本质是计数器:不存储数据,仅用于权限控制;
- 生命周期随内核:信号量集创建后,需手动删除,否则残留于内核;
- 原子操作:P/V 操作是原子的,避免并发冲突;
- 支持多资源控制:一个信号量集可包含多个信号量,分别控制不同临界资源。



2.3 核心 API 详解
System V 信号量的 API 与消息队列、共享内存风格一致,核心包括semget、semop、semctl。

2.3.1 semget:创建 / 获取信号量集
#include<sys/sem.h>intsemget(key_t key,int nsems,int semflg);- 参数:
nsems:信号量集中的信号量个数(创建时必须指定,获取时可设为 0);- 其他参数与
msgget一致;
- 返回值:成功返回信号量集 ID(semid),失败返回 - 1。
2.3.2 semop:执行 P/V 操作(核心 API)
#include<sys/sem.h>intsemop(int semid,structsembuf*sops, size_t nsops);- 参数:
- sops:指向struct sembuf数组的指针,每个元素描述一个信号量的操作;
- nsops:sops数组的长度(操作的信号量个数);
struct sembuf结构体:
structsembuf{unsignedshort sem_num;// 信号量集中的信号量下标(从0开始)short sem_op;// 操作类型(-1=P操作,+1=V操作)short sem_flg;// 操作标志(0=阻塞,IPC_NOWAIT=非阻塞)};2.3.3 semctl:控制信号量集
#include<sys/sem.h>intsemctl(int semid,int semnum,int cmd,...);- 参数:
semnum:信号量集中的信号量下标(操作单个信号量时指定);cmd:控制命令(核心命令如下);- 可变参数:根据
cmd不同,可传入union semun结构体(用于设置信号量初始值);
核心命令说明:
| 命令 | 描述 |
|---|---|
| IPC_RMID | 删除信号量集(忽略其他参数) |
| SETVAL | 设置单个信号量的初始值(需传入 union semun) |
| GETVAL | 获取单个信号量的当前值 |
| SETALL | 设置信号量集中所有信号量的初始值 |
| GETALL | 获取信号量集中所有信号量的当前值 |
2.3.4 union semun 结构体(需手动定义)
union semun {int val;// 用于SETVAL/GETVALstructsemid_ds*buf;// 用于IPC_STAT/IPC_SETunsignedshort*array;// 用于SETALL/GETALLstructseminfo*__buf;// 用于IPC_INFO};2.4 实战案例:信号量保护共享内存
结合共享内存和信号量,实现 “多进程安全写共享内存”—— 通过信号量的互斥机制,确保同一时刻仅一个进程写入共享内存,避免数据混乱。
2.4.1 公共头文件(sem_comm.h)
#ifndef_SEM_COMM_H_#define_SEM_COMM_H_#include<stdio.h>#include<sys/ipc.h>#include<sys/sem.h>#include<sys/shm.h>#include<string.h>#definePATHNAME"."#definePROJ_ID0x6666#defineSHM_SIZE1024#defineSEM_NUM1// 信号量集中的信号量个数// 定义semun联合体union semun {int val;structsemid_ds*buf;unsignedshort*array;structseminfo*__buf;};// 创建信号量集intcreateSemSet(int nsems);// 获取信号量集intgetSemSet(int nsems);// 初始化信号量值intinitSem(int semid,int semnum,int val);// P操作(申请资源)intP(int semid,int semnum);// V操作(释放资源)intV(int semid,int semnum);// 删除信号量集intdestroySemSet(int semid);// 共享内存相关函数(复用之前的逻辑)intcreateShm(int size);intgetShm(int size);void*attachShm(int shmid);intdetachShm(void*addr);intdestroyShm(int shmid);#endif2.4.2 公共实现(sem_comm.c)
#include"sem_comm.h"// 信号量集通用函数staticintcommSemSet(int nsems,int flags){ key_t key =ftok(PATHNAME, PROJ_ID);if(key <0){perror("ftok error");return-1;}int semid =semget(key, nsems, flags);if(semid <0){perror("semget error");return-2;}return semid;}// 创建信号量集intcreateSemSet(int nsems){returncommSemSet(nsems, IPC_CREAT | IPC_EXCL |0666);}// 获取信号量集intgetSemSet(int nsems){returncommSemSet(nsems, IPC_CREAT);}// 初始化信号量intinitSem(int semid,int semnum,int val){union semun un; un.val = val;if(semctl(semid, semnum, SETVAL, un)<0){perror("semctl SETVAL error");return-1;}return0;}// P操作intP(int semid,int semnum){structsembuf sb; sb.sem_num = semnum; sb.sem_op =-1;// P操作:计数器-1 sb.sem_flg =0;// 阻塞模式if(semop(semid,&sb,1)<0){perror("semop P error");return-1;}return0;}// V操作intV(int semid,int semnum){structsembuf sb; sb.sem_num = semnum; sb.sem_op =1;// V操作:计数器+1 sb.sem_flg =0;if(semop(semid,&sb,1)<0){perror("semop V error");return-1;}return0;}// 删除信号量集intdestroySemSet(int semid){if(semctl(semid,0, IPC_RMID)<0){perror("semctl IPC_RMID error");return-1;}return0;}// 共享内存函数实现(复用)intcreateShm(int size){ key_t key =ftok(PATHNAME, PROJ_ID);returnshmget(key, size, IPC_CREAT | IPC_EXCL |0666);}intgetShm(int size){ key_t key =ftok(PATHNAME, PROJ_ID);returnshmget(key, size, IPC_CREAT);}void*attachShm(int shmid){returnshmat(shmid,NULL,0);}intdetachShm(void*addr){returnshmdt(addr);}intdestroyShm(int shmid){returnshmctl(shmid, IPC_RMID,NULL);}2.4.3 写进程(writer.c)
#include"sem_comm.h"#include<unistd.h>intmain(){// 1. 创建信号量集(1个信号量)并初始化(初始值1,互斥)int semid =createSemSet(SEM_NUM);initSem(semid,0,1);// 2. 创建共享内存int shmid =createShm(SHM_SIZE);char*shmaddr =(char*)attachShm(shmid);// 3. 循环写入共享内存(P/V操作保护临界区)for(int i =0; i <5; i++){P(semid,0);// 申请资源(进入临界区)snprintf(shmaddr, SHM_SIZE,"进程[%d]写入数据:%d",getpid(), i);printf("进程[%d]写入:%s\n",getpid(), shmaddr);V(semid,0);// 释放资源(退出临界区)sleep(1);}// 4. 清理资源detachShm(shmaddr);sleep(10);// 等待读进程读取destroyShm(shmid);destroySemSet(semid);return0;}2.4.4 读进程(reader.c)
#include"sem_comm.h"#include<unistd.h>intmain(){// 1. 获取信号量集int semid =getSemSet(SEM_NUM);// 2. 获取共享内存int shmid =getShm(SHM_SIZE);char*shmaddr =(char*)attachShm(shmid);// 3. 循环读取共享内存(P/V操作保护临界区)for(int i =0; i <5; i++){P(semid,0);// 申请资源(进入临界区)printf("进程[%d]读取:%s\n",getpid(), shmaddr);V(semid,0);// 释放资源(退出临界区)sleep(1);}// 4. 清理资源detachShm(shmaddr);return0;}2.4.5 运行效果
# 终端1:启动写进程 ./writer # 输出:进程[1234]写入:进程[1234]写入数据:0# 进程[1234]写入:进程[1234]写入数据:1# 终端2:启动读进程 ./reader # 输出:进程[5678]读取:进程[1234]写入数据:0# 进程[5678]读取:进程[1234]写入数据:1- 关键:信号量初始值为 1,确保同一时刻仅一个进程进入临界区(读写共享内存),避免数据混乱。
2.5 信号量避坑指南
- 信号量初始值设置:互斥场景初始值设为 1(二元信号量),同步场景按资源数量设置(如 2 个资源初始值设为 2);
- P/V 操作成对出现:进入临界区前执行 P 操作,退出后执行 V 操作,避免死锁;
- 信号量集删除时机:需等待所有进程完成操作后再删除,否则正在操作的进程会报错;
- 避免信号量滥用:信号量仅用于同步互斥,不用于数据传输,不要试图通过信号量传递信息。
三. 内核如何管理 System V IPC 资源(PPT 深度解析)
System V IPC(共享内存、消息队列、信号量)的内核管理逻辑一致,核心通过struct ipc_ids和struct kern_ipc_perm实现全局管理,这是理解 System V IPC 本质的关键。
前置理解和引入图示(从我们之前查命令看到过的数据结构入手引出内核的管理结构)

3.1 核心管理结构
我们这里仅展示两个具有共性的管理结构体(共享内存,消息队列,信号量都有),剩下的具体流程看后面的部分!
3.1.1 struct ipc_ids(全局管理结构体)
内核维护三个全局ipc_ids结构体,分别管理共享内存、消息队列、信号量(有三个静态全局变量):
structipc_ids{int in_use;// 当前使用的IPC资源个数int max_id;// 最大的IPC资源IDunsignedshort seq;// 序列号(用于生成唯一ID)unsignedshort seq_max;// 序列号最大值structmutex mutex;// 保护该结构体的互斥锁structipc_id_ary nullentry;// 空条目structipc_id_ary*entries;// 指向IPC资源数组的指针};- 作用:记录系统中所有该类型 IPC 资源的元数据,实现资源的创建、查找、删除。
3.1.2 struct kern_ipc_perm(权限控制结构体)
所有 System V IPC 资源都包含struct kern_ipc_perm字段,用于权限控制和唯一标识:
structkern_ipc_perm{ spinlock_t lock;// 自旋锁int deleted;// 资源是否被标记删除 key_t key;// 资源的唯一Key uid_t uid;// 创建者用户ID gid_t gid;// 创建者组ID uid_t cuid;// 最后修改者用户ID gid_t cgid;// 最后修改者组ID mode_t mode;// 访问权限(如0666)unsignedlong seq;// 序列号void*security;// 安全相关指针};- 核心:
key字段是 IPC 资源的全局唯一标识,mode字段控制进程对资源的访问权限。
3.2 内核管理流程图(重点模块,图中信息很多还包含一个面试题,大家可以仔细看看)

3.3 重点补充:共享内存是怎么做到把开辟出来的内存块映射到我们当前的进程地址空间的呢?(结合图中的图示和文字来理解,也是个重要的模块,顺便引出mmap)


图中提到的mmap,博主后续也可能会单独更新一篇博客来详细的讲解一下这个知识点,这里仅仅是理解
3.4 关键结论和总结
- System V IPC 资源的生命周期随内核,本质是内核维护的结构体和数据结构;
key是资源的全局唯一标识,msgid是进程访问资源的句柄;- 权限控制通过
kern_ipc_perm.mode实现,与文件权限规则一致。
本文全面覆盖了 System V 消息队列、信号量的原理、API、实战和内核管理,核心要点总结:
- 消息队列:用于结构化、异步跨进程通信,支持按类型筛选消息,适合数据传输场景;
- 信号量:用于同步与互斥,通过 P/V 操作保护临界资源,常与共享内存配合使用;
- 内核管理:System V IPC 通过
ipc_ids和kern_ipc_perm实现全局管理,生命周期随内核,需手动删除; - 选型建议:
- 需结构化数据传输→消息队列;
- 需同步互斥→信号量;
- 需高效大数据传输→共享内存 + 信号量。
结尾:
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 结语:System V IPC 是 Linux 传统 IPC 的核心,虽然 POSIX IPC 在接口上更简洁,但 System V IPC 的设计思想(如内核管理、权限控制)仍值得深入学习。掌握这三种技术,能应对绝大多数跨进程通信场景。创作不易,觉得有帮助的话,欢迎点赞、收藏、关注三连~ 后续会持续更新 Linux 内核源码解析、高级 IPC 应用等内容,带你从底层吃透 Linux 开发。
✨把这些内容吃透超牛的!放松下吧✨ʕ˘ᴥ˘ʔづきらど