开篇介绍:
hello 大家,那么我们前面几篇博客一直在讲解实现IPC的一些方法,但是我们却没有去探究过内核是如何组织管理IPC资源的,所以,在本篇博客中,我就将带领着大家一起去探索内核是如何组织管理IPC资源。
OK,话不多说,我们开始。
一、IPC 管理的 "通用基石":内核级 "园区管理规则"
内核为了避免三种 IPC 资源(消息队列、信号量、共享内存)各自为政,设计了三个通用结构体,相当于园区的 "基础管理条例"------ 所有设施都要遵守,确保统一识别、统一权限、统一计数。
1. 核心结构体 1:struct kern_ipc_perm------IPC 资源的 "身份证 + 权限证"
这是所有 IPC 资源的 "基类",任何 IPC 资源都必须包含这个结构体,就像所有园区设施都要有 "房产证 + 准入证"。
内核简化源码(2.6.18):
// 简化自linux/ipc.h,剔除平台相关字段
struct kern_ipc_perm {
key_t key; // IPC资源的"门牌号",用户进程通过key查找资源
uid_t uid; // 资源所有者的用户ID
gid_t gid; // 资源所有者的组ID
uid_t cuid; // 创建者的用户ID
gid_t cgid; // 创建者的组ID
mode_t mode; // 访问权限(和文件权限一致,如0666=所有人可读写)
unsigned short seq; // 序列号,用于生成唯一的ipc_id
spinlock_t lock; // 自旋锁,保护结构体字段的原子修改
};
通俗解析:
key:相当于设施的 "门牌号"(比如 "3 号楼 101"),用户进程通过ftok生成的key,就能找到对应的 IPC 资源;uid/gid/cuid/cgid:记录 "设施创建者" 和 "当前所有者"(比如小区里的商铺,创建者是开发商,所有者是租户);mode:相当于 "门禁规则",比如0600表示只有所有者能读写,0666表示所有人都能读写;spinlock_t lock:一个 "高速小锁"------ 内核修改这个结构体时(比如改权限、改所有者),其他内核路径不能打断,保证操作原子性(比如同时有两个进程申请修改权限,不会出现 "权限一半改了一半没改" 的情况)。
关键作用:
所有 IPC 资源的 "身份识别" 和 "权限检查" 都依赖这个结构体,内核在处理任何 IPC 操作(创建、访问、删除)时,第一步就是通过key找到kern_ipc_perm,再检查调用进程是否有操作权限(比如普通用户不能修改 root 创建的 IPC 资源)。
2. 核心结构体 2:struct ipc_id_ary------IPC 资源的 "分类货架"
一个园区里有很多设施,不可能杂乱无章堆放,ipc_id_ary就是 "分类货架"------ 专门用来批量存放同一类 IPC 资源(比如所有消息队列放一个货架,所有信号量放另一个货架)。
内核简化源码(2.6.18):
// 简化自ipc/util.h,柔性数组是核心设计
struct ipc_id_ary {
int size; // 货架的"格子总数",最多能放多少个IPC资源
struct kern_ipc_perm *p[0]; // 柔性数组,每个格子存一个IPC资源的kern_ipc_perm指针
};
通俗解析:
- 柔性数组
p[0]是关键:它本身不占内存,内核会根据size动态分配空间(比如size=100,就分配 100 个kern_ipc_perm*指针的空间),关于柔性数组的知识,大家可以自行回忆一下。 - 每个 "格子" 里的
kern_ipc_perm*,就像货架上的 "商品标签"------ 通过标签能找到对应的完整 IPC 资源(比如消息队列的msg_queue、信号量的sem_array)。
示例:
如果 "消息队列货架" 的size=50,说明这个货架最多能放 50 个消息队列,每个格子里的p[i]指向某个消息队列的kern_ipc_perm结构体(进而找到整个msg_queue)。
3. 核心结构体 3:struct ipc_ids------IPC 资源的 "园区管理处"
这是 IPC 资源的 "总控中心",内核为三种 IPC 资源各维护一个ipc_ids实例(msg_ids、sem_ids、shm_ids),相当于园区里的 "消息队列管理处""信号量管理处""共享内存管理处",各自负责对应货架的管理。
内核简化源码(2.6.18):
// 简化自ipc/util.h,核心是管理货架和计数
struct ipc_ids {
int in_use; // 当前货架上已使用的"格子数"(已创建的IPC资源数)
int max_id; // 已分配的最大资源ID(比如1、2、3...,用于标识资源)
struct mutex mutex; // 互斥锁,保护货架操作的互斥性
struct ipc_id_ary *entries; // 指向对应的"分类货架"(ipc_id_ary)
int seq; // 序列号,用于生成唯一ID,避免ID重复
};
// 内核全局变量:三种IPC资源各自的管理处
struct ipc_ids msg_ids; // 消息队列管理处
struct ipc_ids sem_ids; // 信号量管理处
struct ipc_ids shm_ids; // 共享内存管理处
通俗解析:
in_use:比如消息队列货架上已用了 10 个格子,in_use=10;max_id:比如已创建的消息队列 ID 最大是 10,下一个新资源的 ID 就是 11;mutex:"管理处的叫号机"------ 多个进程同时来创建 / 查找 IPC 资源时,必须排队(互斥),避免两个进程同时修改货架(比如同时往一个格子里放资源);entries:指向对应的货架,比如msg_ids.entries就是消息队列的 "分类货架"。
内核初始化简化代码:
内核启动时会初始化这三个管理处,以消息队列为例:
// 内核启动时调用的初始化函数(简化版)
void ipc_init(void) {
// 初始化消息队列管理处
mutex_init(&msg_ids.mutex); // 初始化互斥锁
msg_ids.in_use = 0; // 初始无资源
msg_ids.max_id = 0; // 初始ID为0
msg_ids.entries = NULL; // 初始无货架,后续动态创建
msg_ids.seq = 0; // 序列号初始为0
// 信号量、共享内存管理处初始化逻辑类似
mutex_init(&sem_ids.mutex);
sem_ids.in_use = 0;
// ... 其他字段初始化
}
二、消息队列:"带等候区的智能快递柜"
消息队列的核心是 "存消息 + 让进程排队等消息",内核用struct msg_queue描述每个消息队列,相当于 "一个带等候区的智能快递柜"------ 既能存消息,又能让 "寄件人"(发消息进程)和 "收件人"(收消息进程)排队。
1. 核心结构体:struct msg_queue(消息队列的 "快递柜详情")
内核简化源码(2.6.18):
// 简化自linux/msg.h,剔除复杂链表操作和宏定义
struct msg_queue {
struct kern_ipc_perm q_perm; // 继承"身份证+权限证",必含字段
time_t q_stime; // 最后一次发消息(msgsnd)的时间
time_t q_rtime; // 最后一次收消息(msgrcv)的时间
time_t q_ctime; // 最后一次修改队列的时间(如改权限)
unsigned long q_cbytes; // 当前队列中所有消息的总字节数
unsigned long q_qnum; // 当前队列中的消息个数
unsigned long q_qbytes; // 队列最大容量(字节数,用户创建时指定)
pid_t q_lspid; // 最后一次发消息的进程PID
pid_t q_lrpid; // 最后一次收消息的进程PID
// 消息链表:存放所有消息,每个节点是struct msg_msg
struct list_head q_messages; // 消息链表头(内核链表结构)
// 等候队列:收消息的进程排队
struct list_head q_receivers; // 收消息等候队列头
// 等候队列:发消息的进程排队(队列满时)
struct list_head q_senders; // 发消息等候队列头
};
// 单个消息的结构体(快递本身)
struct msg_msg {
struct list_head m_list; // 链表节点,挂到q_messages上
long m_type; // 消息类型(用户自定义,如1、2、3)
size_t m_ts; // 消息长度(不含消息类型字段)
// 消息数据(柔性数组,存放实际消息内容)
char m_text[0]; // 消息正文
};
通俗解析:
q_messages:"快递存放架",所有消息(msg_msg)通过m_list挂在这个链表上,内核收 / 发消息就是操作这个链表;q_receivers:"收件人等候区"------ 如果队列空了,进程调用msgrcv收消息时,会被放进这个队列,直到有消息进来;q_senders:"寄件人等候区"------ 如果队列满了(达到q_qbytes),进程调用msgsnd发消息时,会被放进这个队列,直到队列有空闲空间;q_lspid/q_lrpid:记录 "最后操作人",方便调试(比如查看哪个进程最后发了消息)。
2. 消息队列的 "生命周期":从创建到删除(代码 + 流程)
我们结合用户态调用 和内核态处理,用代码示例拆解完整流程,让你看清 "用户操作" 和 "内核管理" 的关联。
(1)创建消息队列:msgget→内核sys_msgget
用户态调用msgget创建消息队列,最终会触发内核的sys_msgget函数(简化源码如下):
用户态代码示例:
// 用户态创建消息队列
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
int main() {
key_t key = ftok("/tmp", 0x77); // 生成key(门牌号)
if (key == -1) { perror("ftok"); return 1; }
// 创建消息队列:IPC_CREAT=创建,IPC_EXCL=不存在才创建,0666=权限
int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
if (msgid == -1) { perror("msgget"); return 1; }
printf("创建消息队列成功,msgid=%d\n", msgid);
return 0;
}
内核态sys_msgget简化源码:
// 内核处理msgget的系统调用(简化版)
asmlinkage long sys_msgget(key_t key, int msgflg) {
struct ipc_ids *ids = &msg_ids; // 指向消息队列管理处
struct kern_ipc_perm *perm;
struct msg_queue *mq;
int id, ret;
// 1. 加锁:保证操作互斥(避免多个进程同时创建)
mutex_lock(&ids->mutex);
// 2. 查找是否已有key对应的消息队列
perm = ipc_findkey(ids, key); // 内核内部函数:根据key查货架
if (perm != NULL) {
// 找到已有队列:检查权限(msgflg中的权限和q_perm.mode匹配)
if ((msgflg & IPC_CREAT) && (msgflg & IPC_EXCL)) {
// 要求"不存在才创建",但已存在,返回错误
ret = -EEXIST;
goto out_unlock;
}
// 权限检查通过,返回对应的msgid
id = ipc_getid(ids, perm); // 由perm生成msgid
ret = id;
goto out_unlock;
}
// 3. 没找到,创建新消息队列
if (!(msgflg & IPC_CREAT)) {
// 没找到且没要求创建,返回错误
ret = -ENOENT;
goto out_unlock;
}
// 4. 分配内核内存:创建msg_queue结构体
mq = kmalloc(sizeof(struct msg_queue), GFP_KERNEL);
if (!mq) { ret = -ENOMEM; goto out_unlock; }
// 5. 初始化msg_queue字段
memset(mq, 0, sizeof(struct msg_queue));
// 初始化权限结构体
mq->q_perm.key = key;
mq->q_perm.uid = current->uid; // current是当前进程,取当前用户ID
mq->q_perm.gid = current->gid;
mq->q_perm.cuid = current->uid;
mq->q_perm.cgid = current->gid;
mq->q_perm.mode = msgflg & 0777; // 取用户指定的权限(0666)
spin_lock_init(&mq->q_perm.lock); // 初始化自旋锁
mq->q_perm.seq = ids->seq++;
// 初始化链表头(等候区和消息链表)
INIT_LIST_HEAD(&mq->q_messages);
INIT_LIST_HEAD(&mq->q_receivers);
INIT_LIST_HEAD(&mq->q_senders);
// 初始化队列容量(默认8192字节,可通过msgflg修改)
mq->q_qbytes = msgflg & MSGMAX ? (msgflg & MSGMAX) : 8192;
mq->q_stime = mq->q_rtime = mq->q_ctime = CURRENT_TIME;
mq->q_lspid = mq->q_lrpid = current->pid;
// 6. 把新队列加入"货架"(ipc_id_ary)
ret = ipc_addid(ids, &mq->q_perm); // 内核函数:把perm加入货架,更新in_use
if (ret < 0) {
kfree(mq); // 加入失败,释放内存
goto out_unlock;
}
// 7. 生成msgid并返回
ret = ipc_getid(ids, &mq->q_perm);
out_unlock:
mutex_unlock(&ids->mutex); // 解锁
return ret;
}
流程类比:
用户(进程)去 "消息队列管理处"(msg_ids)申请创建快递柜:
- 管理处先 "叫号"(加
mutex锁),避免插队; - 查货架(
ipc_findkey),看有没有 "门牌号"(key)匹配的快递柜; - 没有的话,新建一个快递柜(
kmalloc分配msg_queue),填好身份证(q_perm)、设置容量(q_qbytes)、搭好消息架(q_messages)和等候区(q_receivers/q_senders); - 把快递柜放进货架(
ipc_addid),更新已用数量(in_use++); - 给快递柜编个号(
msgid),返回给用户。
(2)发消息:msgsnd→内核sys_msgsnd
用户态调用msgsnd发消息,内核把消息放进q_messages链表,队列满了就让进程排队。
用户态代码示例:
// 往消息队列发消息
struct msgbuf {
long mtype; // 消息类型
char mtext[100]; // 消息内容
};
int main() {
key_t key = ftok("/tmp", 0x77);
int msgid = msgget(key, 0666); // 获取已创建的消息队列
struct msgbuf buf;
buf.mtype = 1; // 消息类型设为1
snprintf(buf.mtext, sizeof(buf.mtext), "这是一条测试消息");
// 发消息:msgid=队列ID,&buf=消息地址,长度=内容长度,0=阻塞模式
int ret = msgsnd(msgid, &buf, strlen(buf.mtext), 0);
if (ret == -1) { perror("msgsnd"); return 1; }
printf("消息发送成功\n");
return 0;
}
内核态sys_msgsnd核心逻辑(简化):
asmlinkage long sys_msgsnd(int msqid, const void __user *msgp, size_t msgsz, int msgflg) {
struct msg_queue *mq;
struct msg_msg *msg;
int ret;
// 1. 根据msgid找到对应的msg_queue(内核内部函数)
mq = msg_lock(msqid);
if (IS_ERR(mq)) return PTR_ERR(mq);
// 2. 检查队列是否满了(当前总字节数+新消息长度 > 队列容量)
if (mq->q_cbytes + msgsz > mq->q_qbytes) {
if (msgflg & IPC_NOWAIT) {
// 非阻塞模式,直接返回错误
ret = -EAGAIN;
goto out_unlock;
}
// 阻塞模式:把当前进程加入发件人等候区(q_senders)
ipc_add_wait_queue(&mq->q_senders, ¤t->wait_entry);
set_current_state(TASK_INTERRUPTIBLE); // 进程设为等待状态
msg_unlock(mq);
schedule(); // 放弃CPU,切换到其他进程
// 被唤醒后重新检查队列是否有空间(此处省略重复逻辑)
}
// 3. 分配内核内存,存储消息(从用户态拷贝消息到内核态)
msg = kmalloc(sizeof(struct msg_msg) + msgsz, GFP_KERNEL);
if (!msg) { ret = -ENOMEM; goto out_unlock; }
// 4. 初始化消息结构体,拷贝消息类型和内容
msg->m_type = ((struct msgbuf __user *)msgp)->mtype; // 从用户态取类型
msg->m_ts = msgsz;
copy_from_user(msg->m_text, ((struct msgbuf __user *)msgp)->mtext, msgsz);
// 5. 把消息加入队列的消息链表(q_messages)
list_add_tail(&msg->m_list, &mq->q_messages);
// 6. 更新队列统计信息
mq->q_cbytes += msgsz;
mq->q_qnum++;
mq->q_stime = CURRENT_TIME;
mq->q_lspid = current->pid;
// 7. 如果有进程在等消息(q_receivers非空),唤醒第一个进程
if (!list_empty(&mq->q_receivers)) {
wake_up_interruptible(&mq->q_receivers);
}
ret = 0;
out_unlock:
msg_unlock(mq);
return ret;
}
流程类比:
用户(进程)去快递柜寄快递:
- 先找到自己的快递柜(
msg_lock通过msgid找mq); - 如果快递柜满了,要么 "扭头就走"(非阻塞
IPC_NOWAIT),要么 "在等候区排队"(加入q_senders,放弃 CPU); - 有空间的话,把快递(
msg_msg)打包好,放进快递架(q_messages); - 更新快递柜统计(总字节数
q_cbytes、消息数q_qnum),如果有收件人在等,就喊第一个人来取。
(3)收消息:msgrcv→内核sys_msgrcv(核心逻辑)
用户态调用msgrcv收消息,内核从q_messages链表取消息,队列空了就让进程排队;取到消息后,从链表中删除。
用户态代码示例:
// 从消息队列收消息
int main() {
key_t key = ftok("/tmp", 0x77);
int msgid = msgget(key, 0666);
struct msgbuf buf;
// 收消息:msgid=队列ID,&buf=接收缓冲区,100=最大长度,1=接收类型1的消息,0=阻塞
ssize_t ret = msgrcv(msgid, &buf, sizeof(buf.mtext), 1, 0);
if (ret == -1) { perror("msgrcv"); return 1; }
buf.mtext[ret] = '\0'; // 手动加字符串结束符
printf("收到消息:%s(类型=%ld)\n", buf.mtext, buf.mtype);
return 0;
}
内核态sys_msgrcv核心逻辑(简化):
asmlinkage long sys_msgrcv(int msqid, void __user *msgp, size_t msgsz, long msgtyp, int msgflg) {
struct msg_queue *mq;
struct msg_msg *msg;
int ret;
// 1. 找到消息队列并加锁
mq = msg_lock(msqid);
if (IS_ERR(mq)) return PTR_ERR(mq);
retry:
// 2. 查找对应类型的消息(msgtyp=1,找m_type=1的消息)
msg = msg_find(mq, msgtyp, msgflg); // 内核内部函数:遍历q_messages找消息
if (!msg) {
if (msgflg & IPC_NOWAIT) {
ret = -EAGAIN;
goto out_unlock;
}
// 队列空,把进程加入收件人等候区
ipc_add_wait_queue(&mq->q_receivers, ¤t->wait_entry);
set_current_state(TASK_INTERRUPTIBLE);
msg_unlock(mq);
schedule();
goto retry; // 被唤醒后重新查找消息
}
// 3. 检查消息长度是否超过用户缓冲区
if (msg->m_ts > msgsz) {
ret = -E2BIG;
goto out_unlock;
}
// 4. 把消息从内核态拷贝到用户态缓冲区
((struct msgbuf __user *)msgp)->mtype = msg->m_type;
copy_to_user(((struct msgbuf __user *)msgp)->mtext, msg->m_text, msg->m_ts);
// 5. 从消息链表中删除该消息,更新队列统计
list_del(&msg->m_list);
mq->q_cbytes -= msg->m_ts;
mq->q_qnum--;
mq->q_rtime = CURRENT_TIME;
mq->q_lrpid = current->pid;
// 6. 释放消息的内核内存
kfree(msg);
// 7. 如果有进程在等发消息(q_senders非空),唤醒第一个进程
if (!list_empty(&mq->q_senders)) {
wake_up_interruptible(&mq->q_senders);
}
ret = msg->m_ts; // 返回消息长度
out_unlock:
msg_unlock(mq);
return ret;
}
(4)删除消息队列:msgctl(IPC_RMID)→内核处理
用户态调用msgctl(msgid, IPC_RMID, NULL)删除消息队列,内核会:
- 把
msg_queue从货架(ipc_id_ary)中移除; - 释放
q_messages链表中所有消息的内存; - 唤醒
q_receivers和q_senders中所有排队的进程(返回错误); - 释放
msg_queue本身的内核内存。
三、信号量:"带排队系统的银行窗口"
信号量的核心是 "资源计数 + 进程排队",内核用struct sem_array(信号量集合)和struct sem(单个信号量)管理,相当于 "一个有多个窗口的银行"------ 每个窗口(struct sem)对应一个资源,semval是 "窗口剩余服务名额",进程申请资源时名额减 1,释放时名额加 1,没名额就排队。
1. 核心结构体(内核简化源码)
(1)信号量集合:struct sem_array(银行整体)
// 简化自linux/sem.h
struct sem_array {
struct kern_ipc_perm sem_perm; // 身份证+权限证
time_t sem_ctime; // 最后一次修改时间
unsigned long sem_nsems; // 集合中信号量的个数(用户创建时指定)
struct sem *sem_base; // 指向第一个信号量(struct sem)的指针
struct list_head sem_pending; // 等待处理的信号量操作队列
struct list_head sem_pending_last; // 操作队列尾
struct sem_undo *undo; // 进程退出时需撤销的操作链表
};
// 单个信号量:struct sem(银行窗口)
struct sem {
int semval; // 信号量值(剩余资源数)
int sempid; // 最后一次操作该信号量的进程PID
};
// 信号量操作等候队列:struct sem_queue(银行等候区)
struct sem_queue {
struct list_head list; // 链表节点,挂到sem_pending
struct task_struct *sleeper; // 等待的进程
struct sem_array *sma; // 对应的信号量集合
int sem_num; // 操作的信号量序号(集合中的第n个)
int sem_op; // 操作类型(-1=P操作,1=V操作)
struct sem_undo *undo; // 撤销操作记录
};
// 撤销操作记录:struct sem_undo(业务撤销单)
struct sem_undo {
struct list_head list; // 挂到sem_array的undo链表
struct task_struct *task; // 对应的进程
struct sem_array *sma; // 对应的信号量集合
int *semadj; // 每个信号量的调整值(P操作-1,V操作+1)
};
通俗解析:
sem_array.sem_base:银行的 "窗口数组",比如sem_nsems=3,就有 3 个窗口(sem_base[0]、sem_base[1]、sem_base[2]);sem.semval:窗口的 "剩余服务名额",比如semval=2,表示当前有 2 个资源可用;sem_pending:银行的 "业务等候区",进程申请资源时名额不够,就把操作请求(sem_queue)放进这里;sem_undo:"业务撤销单"------ 进程执行 P 操作后崩溃,内核会根据semadj自动恢复semval(比如 P 操作减 1,崩溃后自动加 1),避免资源泄漏。
2. 信号量的 "生命周期":创建→P/V 操作→删除
(1)创建信号量集合:semget→内核sys_semget
用户态创建信号量集合(比如创建 3 个信号量,初始值都是 1):
// 用户态创建信号量集合
int main() {
key_t key = ftok("/tmp", 0x78);
// 创建信号量集合:IPC_CREAT=创建,0666=权限,10=集合中信号量个数
int semid = semget(key, 3, IPC_CREAT | 0666);
if (semid == -1) { perror("semget"); return 1; }
// 初始化第一个信号量为1(二元信号量)
union semun un;
un.val = 1;
semctl(semid, 0, SETVAL, un); // 序号0的信号量初始值=1
printf("创建信号量集合成功,semid=%d\n", semid);
return 0;
}
内核sys_semget核心逻辑和sys_msgget类似:
- 加锁→查找 key 对应的
sem_array; - 没找到则创建
sem_array,初始化sem_perm(权限、key); - 分配
sem_base数组(比如 3 个信号量,分配 3 个struct sem的空间); - 把
sem_array加入sem_ids的货架,返回semid。
(2)P/V 操作:semop→内核sys_semop
用户态执行 P 操作(申请资源)或 V 操作(释放资源),通过semop函数:
// 用户态执行P/V操作
int main() {
key_t key = ftok("/tmp", 0x78);
int semid = semget(key, 3, 0666);
struct sembuf sb;
sb.sem_num = 0; // 操作序号0的信号量
sb.sem_op = -1; // -1=P操作(申请资源),1=V操作(释放资源)
sb.sem_flg = SEM_UNDO; // 进程退出时自动撤销操作
// 执行P操作
int ret = semop(semid, &sb, 1); // 1=操作数组长度
if (ret == -1) { perror("semop"); return 1; }
printf("P操作成功,信号量值变为0\n");
// 执行V操作
sb.sem_op = 1;
ret = semop(semid, &sb, 1);
printf("V操作成功,信号量值变为1\n");
return 0;
}
内核sys_semop核心逻辑(简化):
asmlinkage long sys_semop(int semid, struct sembuf __user *sops, size_t nsops) {
struct sem_array *sma;
struct sem_queue *q;
int ret, i;
// 1. 找到信号量集合并加锁
sma = sem_lock(semid);
if (IS_ERR(sma)) return PTR_ERR(sma);
// 2. 遍历所有操作(nsops=操作个数)
for (i = 0; i < nsops; i++) {
struct sembuf sop;
struct sem *sem;
int sem_num, sem_op;
// 从用户态拷贝操作参数
copy_from_user(&sop, &sops[i], sizeof(struct sembuf));
sem_num = sop.sem_num; // 操作的信号量序号
sem_op = sop.sem_op; // 操作类型(-1=P,1=V)
// 3. 找到对应的单个信号量
sem = &sma->sem_base[sem_num];
// 4. 执行P操作(sem_op=-1)
if (sem_op < 0) {
int abs_op = -sem_op;
// 检查资源是否足够
if (sem->semval >= abs_op) {
// 资源足够,直接减semval
sem->semval -= abs_op;
sem->sempid = current->pid;
} else {
// 资源不足,创建等候队列节点
q = kmalloc(sizeof(struct sem_queue), GFP_KERNEL);
if (!q) { ret = -ENOMEM; goto out_unlock; }
// 初始化等候节点
q->sleeper = current;
q->sma = sma;
q->sem_num = sem_num;
q->sem_op = sem_op;
// 记录撤销操作(SEM_UNDO标志)
if (sop.sem_flg & SEM_UNDO) {
q->undo = sem_undo_alloc(current, sma);
q->undo->semadj[sem_num] -= abs_op;
}
// 把节点加入等候队列
list_add_tail(&q->list, &sma->sem_pending);
// 进程设为等待状态,放弃CPU
set_current_state(TASK_INTERRUPTIBLE);
sem_unlock(sma);
schedule();
// 被唤醒后重新检查资源(省略重复逻辑)
}
}
// 5. 执行V操作(sem_op=1)
else if (sem_op > 0) {
sem->semval += sem_op; // 资源数加1
sem->sempid = current->pid;
// 唤醒等候队列中的第一个进程
if (!list_empty(&sma->sem_pending)) {
q = list_first_entry(&sma->sem_pending, struct sem_queue, list);
wake_up_process(q->sleeper);
list_del(&q->list);
kfree(q);
}
}
}
ret = 0;
out_unlock:
sem_unlock(sma);
return ret;
}
流程类比:
用户(进程)去银行办业务:
- 找到银行(
sem_array),找到要办理的窗口(sem_base[sem_num]); - 办 P 业务(申请资源):如果窗口有剩余名额(
semval≥abs_op),直接扣名额;没有就填 "等候单"(sem_queue),在等候区排队; - 办 V 业务(释放资源):给窗口加名额(
semval+=sem_op),如果有人排队,就喊第一个人来办业务; - 办业务时填 "撤销单"(
sem_undo),万一中途走了(进程崩溃),银行会自动把名额恢复。
四、共享内存:"可多人共享的'内存文件'"
共享内存是三种 IPC 中最快的,因为它直接让进程映射内核内存到自己的地址空间,无需数据拷贝。内核用struct shmid_kernel管理,底层基于tmpfs(内存文件系统),相当于 "一个能被多个进程同时打开的共享文件"。
1. 核心结构体:struct shmid_kernel(共享内存的 "文件详情")
内核简化源码(2.6.18):
// 简化自linux/shm.h
struct shmid_kernel {
struct kern_ipc_perm shm_perm; // 身份证+权限证
struct file *shm_file; // 指向tmpfs中的文件(实际存储载体)
unsigned long shm_segsz; // 共享内存的大小(字节数)
pid_t shm_cprid; // 创建进程的PID
pid_t shm_lprid; // 最后操作进程的PID
struct task_struct *shm_creator; // 创建进程的task_struct指针
unsigned short shm_nattch; // 当前连接到该共享内存的进程数
unsigned short shm_unused; // 保留字段
time_t shm_atim; // 最后一次访问时间
time_t shm_dtim; // 最后一次断开连接的时间
time_t shm_ctim; // 最后一次修改时间
void *shm_mlock_user; // 内存锁定相关(简化忽略)
};
通俗解析:
shm_file:共享内存的 "实际存储文件"------ 内核在tmpfs(内存文件系统)中创建一个文件,共享内存的数据就存在这个文件里;shm_nattch:"当前使用人数"------ 比如 3 个进程同时映射了这个共享内存,shm_nattch=3;shm_segsz:"文件大小"------ 共享内存的容量,用户创建时指定(比如 1MB)。
2. 共享内存的 "生命周期":创建→映射→断开→删除
(1)创建共享内存:shmget→内核sys_shmget
用户态创建共享内存(大小 1MB):
int main() {
key_t key = ftok("/tmp", 0x79);
// 创建共享内存:1024*1024=1MB,IPC_CREAT=创建,0666=权限
int shmid = shmget(key, 1024*1024, IPC_CREAT | 0666);
if (shmid == -1) { perror("shmget"); return 1; }
printf("创建共享内存成功,shmid=%d\n", shmid);
return 0;
}
内核sys_shmget核心逻辑:
- 加锁→查找
shm_ids货架中是否有key对应的shmid_kernel; - 没有则创建
shmid_kernel,初始化shm_perm(权限、key); - 在
tmpfs中创建文件(shm_file = filp_open("/dev/shm/xxx", O_CREAT | O_RDWR, 0666)); - 设置
shm_segsz(共享内存大小)、shm_cprid(创建进程 PID)、shm_nattch=0(初始无进程连接); - 把
shmid_kernel加入shm_ids货架,返回shmid。
(2)映射共享内存:shmat→内核sys_shmat
用户态把共享内存映射到自己的地址空间,之后就能像操作普通内存一样读写:
int main() {
key_t key = ftok("/tmp", 0x79);
int shmid = shmget(key, 1024*1024, 0666);
// 映射共享内存:NULL=内核自动分配地址,0=默认权限
char *shmaddr = (char *)shmat(shmid, NULL, 0);
if (shmaddr == (void *)-1) { perror("shmat"); return 1; }
// 读写共享内存(像操作普通数组一样)
snprintf(shmaddr, 100, "共享内存中的数据");
printf("写入共享内存:%s\n", shmaddr);
return 0;
}
内核sys_shmat核心逻辑:
- 根据
shmid找到shmid_kernel; - 调用
do_mmap(内存映射函数),把shm_file对应的内存区域映射到当前进程的地址空间; shm_nattch++(连接数加 1);- 返回映射后的用户态地址(
shmaddr)。
(3)断开映射:shmdt→内核sys_shmdt
用户态断开共享内存映射:
shmdt(shmaddr); // 传入映射后的地址
内核逻辑:
- 解除进程地址空间与
shm_file的映射; shm_nattch--(连接数减 1);- 如果
shm_nattch=0且用户已调用shmctl(IPC_RMID),则删除shm_file和shmid_kernel。
(4)删除共享内存:shmctl(IPC_RMID)→内核处理
用户态删除共享内存:
shmctl(shmid, IPC_RMID, NULL);
内核逻辑:
- 标记
shmid_kernel为 "待删除"; - 如果当前
shm_nattch>0(还有进程在使用),则等所有进程shmdt后(shm_nattch=0)再删除; - 否则直接删除
shm_file(释放tmpfs内存)和shmid_kernel,从shm_ids货架中移除。
五、内核管理 IPC 资源的 "全景图"(总结 + 核心逻辑)
Linux 内核 2.6.18 管理 IPC 资源的本质,是 **"通用框架统一管理,专属结构定制功能"**,全程围绕 "安全、有序、高效" 三个目标:
1. 核心设计逻辑
| 层级 | 核心组件 | 作用 | 类比 |
|---|---|---|---|
| 全局管理层 | ipc_ids(3 个实例) |
每种 IPC 资源的 "管理处",负责计数、锁保护 | 园区分类管理处 |
| 容器层 | ipc_id_ary |
每种 IPC 资源的 "货架",批量存放资源 | 分类货架 |
| 身份权限层 | kern_ipc_perm |
所有 IPC 资源的 "身份证 + 权限证" | 房产证 + 准入证 |
| 功能实现层 | msg_queue/sem_array/shmid_kernel |
每种 IPC 资源的专属结构,实现核心功能 | 快递柜 / 银行 / 共享文件 |
2. 所有 IPC 资源的通用流程
无论是消息队列、信号量还是共享内存,内核处理的通用流程都是:
- 加锁 :通过
ipc_ids的mutex保证操作互斥; - 查找 / 创建 :根据
key在ipc_id_ary货架中查找资源,没找到则创建; - 权限检查 :通过
kern_ipc_perm.mode检查进程是否有操作权限; - 执行操作:根据资源类型执行专属操作(发消息 / 申请资源 / 映射内存);
- 解锁 :释放
mutex,允许其他进程操作; - 资源回收:删除资源时,释放所有关联内存和依赖(比如消息链表、等候队列)。
3. 内核源码设计的 "巧思"
- 柔性数组 :
ipc_id_ary.p[0]、msg_msg.m_text[0],动态分配内存,避免浪费; - 链表管理 :用内核链表(
struct list_head)管理消息、等候进程,插入 / 删除高效; - 锁机制 :自旋锁(
spinlock)保护结构体字段,互斥锁(mutex)保护货架操作,兼顾效率和互斥; - 撤销机制 :信号量的
sem_undo、共享内存的shm_nattch,避免进程崩溃导致资源泄漏。
4. 通俗总结
内核就像一个 "智能园区管理员":
- 所有 IPC 资源都是园区里的设施,管理员给它们统一编号、登记身份、设置权限;
- 消息队列是 "带等候区的快递柜",负责 "存消息、让进程排队等消息";
- 信号量是 "带排队系统的银行",负责 "管资源、让进程按规则用资源";
- 共享内存是 "可多人共享的内存文件",负责 "快传输、让进程直接用内存";
- 管理员(内核)全程用 "锁" 保证没人插队,用 "撤销机制" 避免设施损坏,用 "分类管理" 保证有序。
通过这种设计,三种 IPC 资源既能各自发挥优势(消息队列有序、信号量控资源、共享内存高速),又能被内核统一管理,确保整个系统的稳定性和高效性。
kern_ipc_perm、ipc_id_ary、ipc_ids,msg_queue/sem_array/shmid_kernel之间的关系:
一、先明确核心角色:四个层次的 "分工定位"
我们先给每个结构体一个 "生活化身份",方便理解:
- ipc_ids:小区的 "三大管理处"(消息队列管理处、信号量管理处、共享内存管理处),每个管理处负责一类 IPC 资源的全局管控;
- ipc_id_ary:每个管理处里的 "分类货架",专门用来批量存放同一类 IPC 资源的 "身份标签";
- kern_ipc_perm:每个 IPC 资源的 "身份证 + 门禁卡",记录资源的核心身份和权限信息;
- msg_queue/sem_array/shmid_kernel:三种具体的 IPC 资源(消息队列、信号量集合、共享内存),相当于 "快递柜、银行窗口、共享文件柜",每个资源都必须带着自己的 "身份证"(kern_ipc_perm)。
二、逐层拆解关系:从 "总控" 到 "具体资源"
- 最上层:ipc_ids(管理处)------ 管 "货架" 和全局统计
每个ipc_ids是某一类 IPC 资源的 "总负责人",内核为三种 IPC 资源各配了一个ipc_ids实例:
msg_ids:消息队列管理处;sem_ids:信号量管理处;shm_ids:共享内存管理处。
它的核心任务:
- 管理一个 "专属货架"(
ipc_id_ary类型的entries字段),这个货架只放本类资源的 "身份证"; - 统计本类资源的总量(
in_use:已用货架格子数)、最大编号(max_id:资源的序号); - 用互斥锁(
mutex)保证多个进程同时操作时 "不插队"(比如同时创建消息队列)。
类比 :消息队列管理处就像 "小区快递柜管理处",里面有一个 "快递柜货架"(entries),记录着小区里所有快递柜的总量(in_use)、最大编号(max_id),还负责给来办理业务的人 "叫号"(mutex)。
- 中间层:ipc_id_ary(货架)------ 放 "身份证" 的容器
ipc_id_ary是ipc_ids管理的 "货架",每个货架有固定数量的 "格子"(size字段),每个格子里放的是 "身份证指针"(p[0]柔性数组,存放kern_ipc_perm*)。
它的核心任务:
- 批量存放同一类 IPC 资源的 "身份证"(
kern_ipc_perm),方便管理处快速查找; - 用
size记录货架的总容量(最多能放多少个资源的身份证)。
和 ipc_ids 的关系 :ipc_ids的entries字段直接指向这个货架,比如msg_ids.entries就是消息队列管理处的 "快递柜身份证货架"。
类比 :快递柜管理处的货架(ipc_id_ary)有 50 个格子(size=50),每个格子里放着一个快递柜的 "身份证复印件"(kern_ipc_perm*),管理处通过货架就能快速找到所有快递柜的身份信息。
- 基础层:kern_ipc_perm(身份证)------ 所有资源的 "通用身份"
kern_ipc_perm是所有 IPC 资源的 "基础身份标识",任何一个 IPC 资源(msg_queue/sem_array/shmid_kernel)都必须包含它(作为自己的一个字段)。
它的核心任务:
- 记录资源的 "门牌号"(
key)、所有者(uid/gid)、权限(mode)等通用信息; - 作为资源的 "唯一标识",让管理处能通过它找到对应的具体资源。
和 ipc_id_ary 的关系 :ipc_id_ary的p[0]柔性数组里存的就是指向kern_ipc_perm的指针。比如货架第 3 个格子的p[2],指向某个消息队列的q_perm(msg_queue里的kern_ipc_perm字段)。
类比 :每个快递柜(msg_queue)都有一张身份证(q_perm,类型是kern_ipc_perm),上面写着门牌号(key)、业主(uid)、门禁规则(mode)。货架上的格子里放的就是这张身份证的 "指针"(相当于 "取件码",通过它能找到对应的快递柜)。
- 具体资源:msg_queue/sem_array/shmid_kernel------ 带身份证的 "实际设施"
这三个是三种 IPC 资源的 "具体形态",它们是真正被进程使用的资源,每个都包含一个kern_ipc_perm字段(即自己的 "身份证")。
- msg_queue :消息队列,相当于 "带等候区的快递柜",它的
q_perm字段是自己的身份证; - sem_array :信号量集合,相当于 "带排队系统的银行窗口群",它的
sem_perm字段是自己的身份证; - shmid_kernel :共享内存,相当于 "可多人共享的文件柜",它的
shm_perm字段是自己的身份证。
和前面三层的关系:
- 它们通过自己的
kern_ipc_perm字段(如q_perm)被关联到ipc_id_ary货架(格子里的p[i]指向q_perm); - 最终由对应的
ipc_ids管理处(如msg_ids)统一管控(统计数量、分配编号等)。
类比 :"快递柜"(msg_queue)是具体设施,它自带身份证(q_perm);这张身份证被放在 "快递柜货架"(ipc_id_ary)的某个格子里;整个货架由 "快递柜管理处"(msg_ids)负责管理。
三、用 "完整流程" 串起所有关系
以 "创建一个消息队列" 为例,看这些结构体如何协作:
- 用户调用
msgget:告诉内核 "我要创建一个门牌号为key的消息队列"; - msg_ids(管理处)工作 :
- 加锁(
mutex),防止其他人插队; - 检查自己的货架(
entries,即ipc_id_ary),看有没有key对应的身份证(kern_ipc_perm);
- 加锁(
- 没找到?创建新消息队列 :
- 分配一个
msg_queue结构体(快递柜),初始化它的q_perm(身份证):填写key、所有者uid、权限mode等; - 把
q_perm的指针放进货架(ipc_id_ary)的空格子里(p[i] = &msg_queue->q_perm); - 更新管理处的统计:
in_use(已用格子数)加 1,max_id(最大编号)更新;
- 分配一个
- 返回结果 :给用户返回消息队列的
msgid(由管理处根据q_perm生成的编号)。
整个过程中:msg_ids(管理处)管控ipc_id_ary(货架),货架存放kern_ipc_perm*(身份证指针),而kern_ipc_perm是msg_queue(具体资源)的一部分 ------ 四层结构环环相扣。
四、一句话总结关系
ipc_ids是 "管理处",管着ipc_id_ary(货架);ipc_id_ary是 "货架",放着kern_ipc_perm*(身份证指针);kern_ipc_perm是 "身份证",属于msg_queue/sem_array/shmid_kernel(具体资源);- 三种具体资源通过自己的 "身份证" 被管理处和货架统一管控。
就像 "校长(ipc_ids)管年级组(ipc_id_ary),年级组管学生档案(kern_ipc_perm),档案属于每个学生(具体资源)"------ 层次分明,各司其职,共同构成了 Linux 内核的 IPC 资源管理体系。

其实大家做个简单了解就行,这一部分内容并不算是太重要,毕竟现代技术日新月异,IPC的设计早已更新迭代,所以大家不必太过深究,我们会用IPC,知道一定的原理即可。
结语:于内核细节见真章,在技术长河中成长
亲爱的读者,当你看到这里,想必已经和我一起走完了 Linux 内核 2.6.18 中 IPC 资源管理的探索之旅。从 "通用基石" 的kern_ipc_perm、ipc_id_ary、ipc_ids,到消息队列 "快递柜"、信号量 "银行窗口"、共享内存 "共享文件" 的专属设计,再到内核源码中柔性数组、锁机制、撤销机制的巧思,我们一点点揭开了内核管理 IPC 资源的神秘面纱。
或许你会觉得,这些内容 "太底层""太陈旧"------ 毕竟 Linux 内核版本早已更新到 6.x,IPC 的实现也在不断演进。但我想说,技术的迭代从不是对基础的否定,而是对本质的传承与升华。我们今天剖析的 2.6.18 版本 IPC 管理逻辑,就像计算机学科的 "经典力学",它是理解更复杂、更现代 IPC 机制的 "脚手架"。
一、从 "知其然" 到 "知其所以然":理解内核的价值
你有没有想过,当你在用户态调用msgget创建消息队列时,内核是如何 "秒懂" 你的需求,还能保证不与其他进程的资源冲突?当你用semop实现进程同步时,内核又是如何精准控制 "资源名额",让进程们有条不紊地协作?这些问题的答案,就藏在我们今天拆解的结构体和流程里。
理解内核如何组织管理 IPC 资源,不是为了让你立刻去写内核代码,而是为了:
- 更自信地使用 IPC :当你知道消息队列的 "等候区" 机制,就明白为什么
msgsnd有时会阻塞;当你清楚信号量的 "撤销单" 设计,就理解SEM_UNDO标志为何能避免资源泄漏;当你了解共享内存的 "内存文件" 本质,就懂得shmdt后数据为何还能存在。 - 更高效地排查问题 :遇到 "IPC 资源创建失败""信号量死锁""共享内存段残留" 等问题时,你能从内核管理的角度推测原因 ------ 是
key冲突了?是sem_undo没处理?还是shm_nattch没清零? - 更深刻地理解 "系统":内核对 IPC 资源的 "分类管理、统一规则、专属优化",是整个计算机系统 "模块化 + 标准化" 设计思想的缩影。这种思想,在分布式系统、云原生架构中同样适用 ------ 本质都是 "资源的高效组织与有序访问"。
二、技术长河中的 "变" 与 "不变"
Linux 内核版本在变,IPC 的实现细节在变,但 **"管理资源以支撑上层应用" 的核心目标从未改变 **;编程语言在迭代,开发框架在更新,但 "理解底层才能更好驾驭上层" 的逻辑从未改变。
你看,我们今天讨论的柔性数组p[0],是 C 语言为动态内存管理提供的优雅方案,它的设计思想在现代编程语言的 "切片""动态数组" 中仍能看到影子;内核的 "锁机制",是并发编程中 "互斥与同步" 的经典实践,这也是你学习 Go、Rust 等语言并发模型时绕不开的话题;而 IPC 资源的 "身份 - 权限 - 计数" 管理,更是所有 "资源型系统"(从数据库到云平台)的基础逻辑。
所以,不必因为 "内容陈旧" 而轻视这些知识。它们是技术长河中沉淀下来的 "鹅卵石",看似朴素,却能在你构建自己的技术大厦时,成为最稳固的基石。
三、给技术路上的你:保持好奇,持续探索
如果你是一名 Linux 开发者,希望这篇博客能让你在使用msgget、semop、shmget时,多一份 "知其所以然" 的笃定;如果你是一名计算机学生,希望这些内核细节能让你对 "操作系统" 这门课多一份具象的感知,明白课本上的 "进程通信""同步互斥" 不是空洞的概念;如果你是一名技术爱好者,希望这次探索能点燃你对 "系统底层" 的好奇心 ------ 要知道,每一个看似简单的用户态调用背后,都有如此精妙的内核逻辑在支撑。
技术之路从来不是一蹴而就的。它需要我们像今天这样,偶尔停下脚步,钻进 "底层" 的世界里看看;也需要我们带着这份对细节的理解,回到 "上层" 的开发中,去创造更高效、更稳定的应用。
最后,送你一句话:"于细微处见真章,于底层处见功夫"。愿你在未来的技术旅程中,既能仰望架构的星空,也能俯拾内核的碎片,在 "变" 与 "不变" 中找到自己的节奏,持续成长,成为你想成为的技术人。
感谢你的阅读,我们下一次技术探索之旅,再见!