上一篇文章我们详细介绍了System V 共享内存,这篇文章我们就来认识一下System V IPC的另外两种消息队列和信号量,本篇文章暂只对原理进行介绍
System V 消息队列是 Unix System V 引入的一种经典的进程间通信机制。它允许不相关的进程(甚至在不同机器上的进程,如果配合其他机制)通过在内核中维护的一个队列结构来交换数据块(称为消息)。
long mtype): 一个大于 0 的长整型值。它不是队列的标识符,而是用于在接收时选择特定消息的关键字段。发送者可以自由定义其含义(例如,表示消息来源、优先级、消息种类等)。
char mtext[]): 实际传输的数据内容。长度可变(最大长度由系统限制决定,可通过 msgmax 参数配置)。
msgctl(..., IPC_RMID, ...))或系统重启,队列及其中的消息就会一直存在于内核中。 这点与管道不同。
msgbnb)和总消息数(msgmni)都有系统级上限。
key_t key):
ftok() 函数根据一个路径名(必须存在且可访问)和一个项目标识符(一个字符)生成一个唯一的键值。
IPC_PRIVATE (值为 0) 用于创建一个新的、键值唯一的队列(通常用于父子进程间)。
rw-rw-rw-),控制哪些进程可以访问队列(发送、接收、控制)。这些在创建队列 (msgget) 时设置。
一、描述:单个消息队列的结构体(struct msqid_ds)
操作系统为每个消息队列创建 元数据结构体(如Linux中的 msqid_ds),存储队列的完整状态信息(见):
struct msqid_ds {
struct ipc_perm msg_perm; // 权限控制(所有者、读写模式等)
time_t msg_stime; // 最后发送消息的时间戳
time_t msg_rtime; // 最后接收消息的时间戳
time_t msg_ctime; // 队列最后修改时间
unsigned long msg_cbytes; // 当前队列总字节数
unsigned long msg_qnum; // 当前消息数量
unsigned long msg_qbytes; // 队列最大容量(字节)
pid_t msg_lspid; // 最后发送消息的进程PID
pid_t msg_lrpid; // 最后接收消息的进程PID
struct msg *msg_first; // 指向队列首消息(内核链表)
struct msg *msg_last; // 指向队列尾消息
};关键成员解析:
msg_perm
struct ipc_permstruct ipc_perm {
key_t key; // 队列键值
uid_t uid; // 所有者用户ID
gid_t gid; // 所有者组ID
mode_t mode; // 读写权限(如0666)
};消息链表指针

msg_first 和 msg_last 分别指向队列中第一条和最后一条消息,构成消息链表(FIFO基础)。
每个消息节点由内核结构体 msg_msg 描述:
struct msg_msg {
long m_type; // 消息类型
size_t m_ts; // 消息长度
struct msg_msg *next; // 指向下一条消息
char mtext[]; // 消息数据(柔性数组)
};二、组织:全局消息队列的管理
操作系统通过 链表或数组 组织所有消息队列的 msqid_ds 结构体:
msqid_ds 结构体通过内部指针(如 ipc_ids.entries)链接成链表。ipc_ids 结构管理,实现高效遍历。key_t key)标识队列,msgget() 系统调用根据键值在链表中查找或创建队列。组织示意图:
全局管理链表
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ msqid_ds 1 │ ──→ │ msqid_ds 2 │ ──→ │ msqid_ds 3 │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ msg_perm │ │ msg_perm │ │ msg_perm │
│ msg_first → ├────→│ msg_first → ├────→│ msg_first → ├─→ 消息链表
│ ... │ │ ... │ │ ... │
└─────────────┘ └─────────────┘ └─────────────┘三、运作机制:队列操作与内核协作
1. 消息存储
msgsnd()): msg_msg 节点,拷贝用户数据到 mtext。msg_last)。msgrcv()): msg_first)或按类型匹配节点,拷贝数据到用户空间。2. 阻塞控制
msgsnd()/msgrcv() 的进程可能被挂起: msqid_ds.suspend_thread)。3. 资源回收
msgctl(msqid, IPC_RMID) 释放队列内存。四、设计思想:先描述,再组织
msqid_ds)描述其属性。创建或获取队列 (msgget):
msgget。
key 对应的队列不存在,且 msgflg 参数包含了 IPC_CREAT,则内核创建一个新队列。
msqid),后续操作(发送、接收、控制)都使用这个标识符来指定操作哪个队列。
发送消息 (msgsnd):
进程准备好一个包含 mtype 和 mtext 的消息结构体。
// 定义消息结构
struct msgbuf {
long mtype; // 消息类型
char mtext[100]; // 消息内容
};调用 msgsnd,指定目标队列的 msqid、消息结构体指针、消息正文长度(不包括 mtype 的长度)、以及可选的阻塞标志 msgflg。
内核动作:
msgbnb 和 msgmni 限制)。
msgflg 设置了 IPC_NOWAIT: 立即返回错误 (EAGAIN)。
msgflg 未设置 IPC_NOWAIT(阻塞模式): 发送进程被挂起(睡眠),直到队列中有足够空间或队列被删除。
接收消息 (msgrcv):
msgrcv,指定源队列的 msqid、用于存放接收到的消息的缓冲区指针、缓冲区大小、期望接收的消息类型 (msgtyp)、以及可选的标志 msgflg。
msgtyp 在队列中查找符合条件的消息:
msgtyp == 0: 读取队列中的第一条消息(FIFO)。
msgtyp > 0: 读取队列中第一条 mtype 等于 msgtyp 的消息。用于选择特定类型的消息。
msgtyp < 0: 读取队列中 mtype 小于等于 msgtyp 绝对值的消息中,mtype 值最小的第一条消息。可用于实现某种形式的优先级接收(较小绝对值的 mtype 被视为较高优先级)。
mtext 部分)。
msgflg 设置了 MSG_NOERROR,则消息正文被截断以适应缓冲区(超出部分丢失)。
MSG_NOERROR,则返回错误 (E2BIG)。
msgflg 设置了 IPC_NOWAIT: 立即返回错误 (ENOMSG)。
IPC_NOWAIT(阻塞模式): 接收进程被挂起(睡眠),直到队列中出现符合条件的消息或队列被删除。
控制队列 (msgctl):
IPC_RMID: 立即删除整个消息队列及其中的所有消息。所有阻塞在该队列上的 msgsnd 和 msgrcv 调用将立即失败返回 (EIDRM)。
IPC_SET: 修改队列的属性(主要是所有者、组、权限)。
IPC_STAT: 获取队列的状态信息(填充到一个 struct msqid_ds 结构中),包括权限、大小、时间戳、当前消息数和字节数等。
关键特点与优缺点:
mtype 提供了一种简单的消息分类和选择机制。
msgtyp < 0 的接收方式可以实现基于 mtype 值的优先级选择。
msgsnd 通常很快完成(除非阻塞),发送者不需要等待接收者。
key, msqid, msgctl 等,略显繁琐。
ipcrm 命令或 msgctl(IPC_RMID) 手动清理)。
mq_open, mq_send, mq_receive),其 API 更符合文件操作习惯(使用路径名标识),支持通知机制,但 System V 消息队列依然广泛使用。
简单应用场景示例:
一个日志服务系统:
mtype 可表示日志级别,如 1=DEBUG, 2=INFO, 3=ERROR)发送到同一个消息队列 (msgsnd)。
msgrcv)。
msgtyp=0 按顺序处理所有日志。
msgtyp=3 优先只处理 ERROR 日志。
msgtyp=-2 读取优先级高于 INFO(即 DEBUG 和 INFO)的日志。
常用命令:
ipcs -q: 查看系统中所有的 System V 消息队列及其状态(IPC ID, Key, 拥有者, 权限, 当前消息数, 当前字节数)。
ipcrm -q msqid: 删除指定的消息队列(msqid 从 ipcs -q 获取)。
核心概念体系:
count++)。你写的代码 = 访问临界资源的代码(临界区) + 不访问临界资源的代码(非临界区)Mutex),自旋锁 (Spinlock)。概念关系图:
+----------------------+
| 共享资源 (Shared) |
+----------+-----------+
|
| (其中需要互斥访问的)
v
+------------------+ +-----v-----+ +------------------+
| 非临界区代码 | | 临界资源 |<----| 临界区代码 |
| (Non-Critical) | | (Critical)| | (Critical Section)|
+------------------+ | Resource | +---------+---------+
+-----------+ |
| 访问
v
+----------+-----------+
| 执行流 |
| (Process/Thread) |
+----------+-----------+
|
| 需要控制
v
+----------------------+ +--------------+--------------+
| 互斥 (Mutex) | | 同步 (Sync) |
| (保证独占访问) | | (保证执行顺序/协作) |
+----------------------+ +----------------------------+总结与串联:
理解要点:

本质:信号量是一种受保护的整型变量,用于协调多个执行流(进程/线程)对共享资源的访问。其核心包含两部分:
P()(wait/获取资源)和 V()(signal/释放资源),由操作系统保证其执行不可中断。核心作用:
类型:
📖 操作系统通过内核数据结构(如Linux的
semid_ds)封装信号量属性,确保操作的原子性和安全性。
场景:电影院有100个座位(共享资源),观众(进程)需购票入场。
信号量初始化:S.value = 100(初始空闲座位数)。
P()操作(购票):
void P(semaphore S) {
S.value--; // 尝试占用一个座位
if (S.value < 0) { // 无空闲座位
block(S.wait_queue); // 观众排队等待(阻塞)
}
}当第101位观众购票时,S.value=-1,该观众被加入等待队列(阻塞机制)。
V()操作(离场):
void V(semaphore S) {
S.value++; // 释放一个座位
if (S.value <= 0) { // 有观众在等待
wakeup(S.wait_queue); // 唤醒队列中第一位观众
}
}每离场一人,唤醒一名等待观众(唤醒机制)。
互斥场景(二元信号量):
sem_mutex = 1。
P(sem_mutex)(占用闸机,sem_mutex=0)。
V(sem_mutex)(释放闸机,sem_mutex=1)。
关键点:
wait):申请资源(可能阻塞)。signal):释放资源(可能唤醒等待者)。当多个进程并发访问共享资源(如打印机、内存变量)时:
count++,实际可能只增加1次(因汇编指令被打断)。💡 信号量通过对临界区代码的封装(P/V操作),解决上述问题。
信号量是进程间通信(IPC)的同步机制,而非数据传输工具:
empty:生产者写入前执行 P(empty)。full:消费者读取后执行 V(full)。🌰 两个项目组共享打印机:
S=1(互斥锁),确保任一时刻仅一组使用打印机。核心:PV操作的原子性
S.value--)。S.value < 0),进程加入等待队列并阻塞。S.value++)。S.value ≤ 0),唤醒队首进程。原子性实现
操作系统通过 屏蔽中断 或 硬件原子指令(如CAS)保证PV操作不可分割:
// 伪代码:PV操作的原子性保障
void P(semaphore S) {
disable_interrupts(); // 屏蔽中断(单CPU)
S.value--;
if (S.value < 0) {
enqueue(S.wait_queue, current_process);
enable_interrupts();
block(); // 主动让出CPU
} else {
enable_interrupts();
}
}解决互斥与同步
mutex=1,临界区前后分别执行 P(mutex)/V(mutex)。sync=0,在需等待的操作前执行 P(sync),在条件满足后执行 V(sync)(前驱关系)。1. 描述:定义信号量结构体
操作系统为每个信号量集合创建元数据 semid_ds:
struct semid_ds {
struct ipc_perm sem_perm; // 权限控制(所有者/权限位)
time_t sem_otime; // 最后一次操作时间
time_t sem_ctime; // 最后一次修改时间
struct sem *sem_base; // 指向信号量数组
struct sem_queue *sem_pending; // 等待队列头指针
};其中单个信号量 struct sem 包含:
struct sem {
int semval; // 当前计数值
int sempid; // 最后一次操作的进程PID
};2. 组织:全局统一管理
semid_ds 通过链表或数组组织。ipc_ids 全局数组,通过首个成员 struct ipc_perm 统一寻址。semget(key, ...) 根据 key 查找或创建信号量集合,返回唯一标识符 semid。semid 定位到对应的 semid_ds。sem_array)统一管理。
semid)访问。
semget() 分配内核对象,初始化 count 和 wait_list。
semop())进入内核。
count → 操作等待队列。
wait_list,调度其他进程。
wait_list 取出一个进程唤醒。
semctl(IPC_RMID) 释放内核资源。
3. 生命周期管理
操作 | 系统调用 | 内核行为 |
|---|---|---|
创建 | semget() | 分配 semid_ds,初始化 sem_base 数组 |
操作 | semop() | 原子执行PV操作,更新 semval 并管理阻塞队列 |
删除 | semctl(..., IPC_RMID) | 释放 semid_ds 结构体 |
命令行工具:
ipcs -s:查看所有信号量集合的 semid_ds 信息。ipcrm -s semid:删除指定信号量。一、信号量核心系统调用
Linux 通过 System V IPC 提供三类信号量操作接口(需包含头文件 <sys/sem.h>):
1. semget - 创建/获取信号量集
int semget(key_t key, int nsems, int semflg);参数解析:
key:唯一标识符(ftok()生成或IPC_PRIVATE)nsems:信号量数量(集合大小)semflg:标志位(IPC_CREAT | 0666 创建可读写信号量)返回值:信号量标识符(semid)或 -1(失败)
典型场景:
key_t key = ftok("/tmp", 'A');
int semid = semget(key, 1, IPC_CREAT | 0666); // 创建含1个信号量的集合2. semop - 原子操作信号量
int semop(int semid, struct sembuf *sops, size_t nsops);关键结构体:
struct sembuf {
unsigned short sem_num; // 信号量索引
short sem_op; // 操作值(P:-1, V:+1)
short sem_flg; // 标志(IPC_NOWAIT/SEM_UNDO)
};操作类型:
sem_op | 行为 | 等效原语 |
|---|---|---|
负数 | 申请资源(P操作) | wait() |
正数 | 释放资源(V操作) | signal() |
0 | 等待信号量归零 | 同步检查 |
示例(生产者-消费者模型):
struct sembuf op_wait = {0, -1, 0}; // P操作
struct sembuf op_signal = {0, +1, 0}; // V操作
semop(semid, &op_wait, 1); // 申请资源3. semctl - 控制信号量
int semctl(int semid, int semnum, int cmd, ...);核心命令:
命令 | 功能 | 参数要求 |
|---|---|---|
IPC_RMID | 立即删除信号量集 | 忽略semnum |
SETVAL | 设置单个信号量初始值 | 需union semun的val |
GETVAL | 获取信号量当前值 | - |
参数联合体(需自定义):
union semun {
int val; // SETVAL用
struct semid_ds *buf; // IPC_STAT用
unsigned short *array; // SETALL用
};ftok()生成key,并通过xxxget()(如shmget/msgget/semget)创建或获取对象标识符(ID)。IPC_RMID)或系统重启,否则对象会持续存在。xxxget(key, ...)xxxctl(id, cmd, ...)(如删除IPC_RMID、修改权限)shmat/msgsnd/semop)。struct ipc_perm结构体实现(包含所有者、权限位等)。ipcs查看对象信息,ipcrm删除对象(如ipcrm -m [shmid])。1. 功能与设计目的
机制 | 核心功能 | 典型场景 |
|---|---|---|
共享内存 | 多个进程直接访问同一块物理内存,无需数据拷贝,速度最快。 | 高频数据交换(如视频处理、数据库缓存)。 |
消息队列 | 进程间通过带类型标记的消息通信,消息按链表存储,支持优先级和选择性接收。 | 异步通信、结构化数据传输(如任务调度)。 |
信号量 | 本质是计数器,用于同步/互斥(如控制共享资源访问权限)。 | 保护临界区(如防止共享内存数据竞争)。 |
2. 数据交互方式
机制 | 数据传递方式 |
|---|---|
共享内存 | 零拷贝:进程直接读写内存,但需自行处理同步(如配合信号量)。 |
消息队列 | 内核复制:发送方复制数据到内核队列,接收方从内核复制数据,有额外开销。 |
信号量 | 无数据传输:仅通过计数器状态协调进程行为(如P/V操作)。 |
3. 技术实现差异
特性 | 共享内存 | 消息队列 | 信号量 |
|---|---|---|---|
核心结构 | struct shmid_ds(含物理页映射) | struct msqid_ds(含消息链表头) | struct semid_ds(含信号量集合) |
操作函数 | shmat/shmdt(附加/分离内存) | msgsnd/msgrcv(发送/接收) | semop(P/V操作) |
同步需求 | 必须外部同步(如信号量) | 内置消息阻塞机制 | 自身实现同步 |
容量限制 | 受物理内存大小限制 | 队列长度和消息大小有限制 | 信号量数量有限制 |
4. 性能对比
一、核心设计:统一描述与多态管理
1. 描述结构:struct ipc_perm
所有 IPC 资源的内核描述结构体(如 shmid_ds、msqid_ds、semid_ds)首个成员均为 struct ipc_perm。该结构存储资源的通用属性:
key_t __key(创建时指定的键值)uid/gid(所有者)、cuid/cgid(创建者)、mode(读写权限位)__seq(避免 ID 重用冲突)struct ipc_perm {
key_t __key; // IPC 键值
uid_t uid; // 所有者有效 UID
gid_t gid; // 所有者有效 GID
uid_t cuid; // 创建者有效 UID
gid_t cgid; // 创建者有效 GID
unsigned short mode; // 权限位(如 0666)
unsigned short __seq; // 序列号
};2. 多态性实现
ipc_perm 位于结构体首部,其地址与整个资源描述结构体(如 shmid_ds)地址相同。(struct shmid_ds *)perm_ptr)访问资源专属属性(如共享内存的页映射表)。例如:共享内存结构体实际为:
struct shmid_ds {
struct ipc_perm shm_perm; // 首成员
size_t shm_segsz; // 共享内存大小
// 其他专属属性...
};二、组织架构:全局资源表与 ID 分配
1. 资源表管理(ipc_ids)
内核为每类 IPC 资源(共享内存/消息队列/信号量)维护一个全局表 ipc_ids,其核心成员包括:
entries:指向资源指针数组的柔性数组(实际存储 struct kern_ipc_perm*)。in_use:当前资源数量。max_idx:最大索引号(动态扩容)。seq:全局序列计数器(避免 ID 重用)。2. ID 分配机制
shmget(key, ...)): key 查找或新建资源描述结构体(如 shmid_ds)。ipc_ids.entries 中分配空闲槽位,存入资源指针(kern_ipc_perm*)。槽位索引 × SEQ_MULTIPLIER + ipc_ids.seq,确保全局唯一。shmctl(IPC_RMID)): entries 对应槽位。ipc_ids.in_use 计数。新旧内核差异:
struct ipc_id_ary 管理槽位,大小固定。三、权限控制与安全机制
shmat()/msgsnd() 等操作前,内核通过 ipc_perm.mode 检查其 UID/GID 是否具备权限(如 SHM_R 读权限)。IPC_RMID)或重启系统才能释放。四、内核数据结构全景
+---------------+
| ipc_ids | (全局资源表,如 shm_ids)
|---------------| +------------+ +---------------+
| entries: ptr |---->| slot[0] | ----> | shmid_ds | (共享内存实例)
| in_use: 5 | | slot[1] | |---------------|
| max_idx: 128 | | ... | | ipc_perm | --> key, uid, mode...
| seq: 0xab | | slot[N] | | shm_segsz | --> 专属属性
+---------------+ +------------+ +---------------+
↑
| (同结构存储 msqid_ds/semid_ds)五、设计优势与约束
优势
get/ctl/op 接口管理(如 semget/semctl/semop)。ipc_perm 统一安全策略。约束
msgmni)、信号量集大小(semmsl)等受内核参数限制。ipcrm 命令)。六、用户层工具
ipcs:查看所有 IPC 资源状态(ID、键值、权限)。ipcrm:删除指定资源(如 ipcrm -m [shmid])。内核为每类 IPC 资源(共享内存/消息队列/信号量)维护一个全局表 ipc_ids,其核心成员包括:
entries:指向资源指针数组的柔性数组(实际存储 struct kern_ipc_perm*)。in_use:当前资源数量。max_idx:最大索引号(动态扩容)。seq:全局序列计数器(避免 ID 重用)。其中entries指针指向struct ipc_id_ary(柔性数组),详细如下:
一、柔性数组的技术实现
1. 数据结构定义
柔性数组在代码中体现为 struct ipc_id_ary(旧内核)或 IDR树(新内核),其核心结构如下:
// 旧内核静态方案(Linux 2.6.18前)
struct ipc_id_ary {
int size; // 数组容量
struct kern_ipc_perm *p[0]; // 柔性指针数组
};
// 新内核动态方案(Linux 2.6.18+)
struct ipc_ids {
struct idr ipcs_idr; // 基数树(IDR)动态管理指针
unsigned short seq; // 序列号防ID重用
};p[0]:
本质是长度可变的指针数组,每个元素指向一个 struct kern_ipc_perm(资源描述符基类)。
内存分配时通过 kmalloc(sizeof(struct ipc_id_ary) + size * sizeof(void*)) 动态扩展 。2. 资源存储方式
资源类型 | 实际内核结构体 | 与kern_ipc_perm的关系 |
|---|---|---|
共享内存 | struct shmid_kernel | 首成员为kern_ipc_perm |
消息队列 | struct msg_queue | 首成员为kern_ipc_perm |
信号量 | struct sem_array | 首成员为kern_ipc_perm |
访问逻辑:
// 通过柔性数组定位资源
struct kern_ipc_perm *base = entries->p[id]; // 获取基类指针
// 多态转换:根据资源类型强转为具体结构体
if (resource_type == SHM) {
struct shmid_kernel *shm = (struct shmid_kernel*)base; // 地址一致
access(shm->shm_segsz); // 访问共享内存专属属性
}因
kern_ipc_perm位于子结构体首部,其地址与子结构体地址相同,通过强制类型转换即可访问完整资源 。
这就是C语言实现的多态行为!!
二、设计原理与核心优势
1. 动态扩容机制
size 字段预设上限。当资源数量超过 size 时,采用 回绕策略(id % size)复用槽位,但易导致ID冲突 。id = 槽位索引 × SEQ_MULTIPLIER + ipc_ids.seq
(SEQ_MULTIPLIER 通常为 2^16,seq 防重用)。2. 高效检索与安全控制
操作 | 实现方式 | 复杂度 |
|---|---|---|
资源创建 | 在IDR树中分配新槽位,生成唯一ID | O(log n) |
资源查找 | 通过 id 计算槽位索引,检索IDR树 | O(1) |
权限校验 | 检查 kern_ipc_perm 中的 mode 和 uid/gid 字段 | O(1) |
防ID冲突 | seq 计数器每次分配后递增,确保旧ID无法被新资源复用 | — |
3. 多态统一管理
kern_ipc_perm 包含所有资源的共性属性(Key、UID、权限等),实现描述符标准化 。shm_segsz)存储在子结构体中,通过首地址一致性访问 。三、内核版本演进对比
特性 | 旧内核(ipc_id_ary) | 新内核(IDR树) | 优势 |
|---|---|---|---|
容量管理 | 静态数组,需预设 size | 动态扩容,无上限 | 避免资源耗尽 |
ID分配策略 | 线性递增,回绕复用(id % size) | 全局唯一ID(索引+序列号) | 彻底解决ID冲突 |
内存开销 | 每个槽位固定占用指针空间(即使为空) | 仅存储非空指针,稀疏场景省内存 | 节省50%+内存 |
查找效率 | 数组下标直接访问(O(1)) | IDR树哈希检索(O(1)) | 维持高效性 |
代码维护 | 需手动处理回绕逻辑 | 内核通用IDR API管理 | 降低复杂度 |
注:Linux 4.0+ 已全面采用IDR树方案 。
四、典型工作流程示例
以创建共享内存为例:
struct shmid_kernel,其首部 shm_perm(kern_ipc_perm类型)初始化权限和Key。ipc_id_ary.p[id],id 为数组下标。idr_alloc() 在IDR树中分配槽位,生成唯一ID。0x10023)返回给用户进程,作为 shmget 返回值。shmat(id) 时: id 从柔性数组/IDR树中检索到 kern_ipc_perm*。shmid_kernel*,完成内存映射 。注意:柔性数组中存储的是所有申请的IPC资源(包括共享内存,消息队列,信号量,不单单只存储其中某一个),而柔性数组下标就是我们用户层拿到的ID。这其实和文件描述符表有异曲同工之妙。
五、设计思想总结
seq 序列号虽增加ID长度,但彻底杜绝了ID重用导致的安全风险 。一张图总结:
