
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[一. System V 消息队列:结构化的跨进程通信](#一. System V 消息队列:结构化的跨进程通信)
[1.1 核心原理与特性](#1.1 核心原理与特性)
[1.2 核心 API 详解](#1.2 核心 API 详解)
[1.2.1 数据结构(内核管理结构体)](#1.2.1 数据结构(内核管理结构体))
[1.2.2 核心 API 使用](#1.2.2 核心 API 使用)
[1.3 实战案例:消息队列实现 C/S 通信](#1.3 实战案例:消息队列实现 C/S 通信)
[1.3.1 公共头文件(comm.hpp)](#1.3.1 公共头文件(comm.hpp))
[1.3.2 服务端(server.cc)](#1.3.2 服务端(server.cc))
[1.3.3 客户端(client.cc)](#1.3.3 客户端(client.cc))
[1.3.4 编译与运行](#1.3.4 编译与运行)
[1.4 消息队列避坑指南](#1.4 消息队列避坑指南)
[二. System V 信号量:同步与互斥的核心工具](#二. System V 信号量:同步与互斥的核心工具)
[2.1 核心概念铺垫](#2.1 核心概念铺垫)
[2.2 核心原理与特性](#2.2 核心原理与特性)
[2.2.1 底层实现逻辑](#2.2.1 底层实现逻辑)
[2.2.2 核心特性](#2.2.2 核心特性)
[2.3 核心 API 详解](#2.3 核心 API 详解)
[2.3.1 semget:创建 / 获取信号量集](#2.3.1 semget:创建 / 获取信号量集)
[2.3.2 semop:执行 P/V 操作(核心 API)](#2.3.2 semop:执行 P/V 操作(核心 API))
[2.3.3 semctl:控制信号量集](#2.3.3 semctl:控制信号量集)
[2.3.4 union semun 结构体(需手动定义)](#2.3.4 union semun 结构体(需手动定义))
[2.4 实战案例:信号量保护共享内存](#2.4 实战案例:信号量保护共享内存)
[2.4.1 公共头文件(sem_comm.h)](#2.4.1 公共头文件(sem_comm.h))
[2.4.2 服务端(server.cc)](#2.4.2 服务端(server.cc))
[2.4.3 客户端(client.cc)](#2.4.3 客户端(client.cc))
[2.4.4 编译与运行](#2.4.4 编译与运行)
[2.5 信号量避坑指南](#2.5 信号量避坑指南)
[三. 内核如何管理 System V IPC 资源](#三. 内核如何管理 System V IPC 资源)
[3.1 核心管理结构](#3.1 核心管理结构)
[3.1.1 struct ipc_ids(全局管理结构体)](#3.1.1 struct ipc_ids(全局管理结构体))
[3.1.2 struct kern_ipc_perm(权限控制结构体)](#3.1.2 struct kern_ipc_perm(权限控制结构体))
[3.2 内核管理流程图(重点)](#3.2 内核管理流程图(重点))
[3.4 结论和总结](#3.4 结论和总结)
前言
在 Linux 进程间通信(IPC)体系中,System V IPC 家族除了高效的共享内存,还包含消息队列和信号量两大核心组件。消息队列负责结构化数据的传输,信号量专注于进程间的同步与互斥,二者与共享内存配合使用,可以构建出稳定、安全的跨进程通信方案。本文将从原理、API 使用、实战案例到内核管理机制,全面拆解这两种 IPC 技术,带你深入理解 System V IPC 的完整生态。
一. System V 消息队列:结构化的跨进程通信
消息队列 是一种 "基于消息的 IPC 机制" ,核心是内核 维护的一个链表结构的消息队列,进程可向队列中添加带类型的消息,也可按类型读取消息,实现结构化、异步的数据传输。
1.1 核心原理与特性
消息队列的本质是内核中的链表 ,每个消息队列由唯一的 key标识,每个消息包含三部分:
- 消息类型(正整数,用于接收方筛选消息);
- 消息长度(消息正文的字节数);
- 消息正文(实际传输的数据)。
进程通过 msgsnd 向队列发送消息 (链表尾插入 ),通过msgrcv 从队列读取消息 (按类型从链表头或指定位置提取),内核自动管理消息的存储和同步。

1.2 核心 API 详解
System V 消息队列通过ftok、msgget、msgsnd、msgrcv、msgctl五个 API 实现完整操作,与共享内存的 API 设计逻辑一致,降低学习成本。
1.2.1 数据结构(内核管理结构体)
内核通过 struct msg_queue管理消息队列属性(和我们查指令查出来的不同,这个是内核里的),核心字段如下:
cpp
struct msg_queue
{
struct kern_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; // 最后一次属性修改时间
unsigned long q_cbytes; // 队列中消息总字节数
unsigned long q_qnum; // 队列中消息总数
unsigned long q_qbytes; // 队列最大允许字节数(默认16384)
pid_t q_lspid; // 最后一次发送消息的进程PID
pid_t q_lrpid; // 最后一次接收消息的进程PID
struct list_head q_messages; // 消息链表头
struct list_head q_receivers; // 等待接收消息的进程链表
struct list_head q_senders; // 等待发送消息的进程链表
};
1.2.2 核心 API 使用
(1)ftok:生成唯一 Key(与共享内存通用)
cpp
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
- 作用 :将**"文件路径 + 项目 ID"** 转换为唯一 key,作为消息队列的全局标识;
- 注意:路径必须是存在的文件,proj_id为非 0 整数(如0x6666)。
(2)msgget:创建 / 获取消息队列
cpp
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数:
- **key:**ftok 生成的 Key;
- msgflg: 权限标志,常用组合:
- **IPC_CREAT:**不存在则创建,存在则获取(一般用于获取消息队列);
- **IPC_CREAT | IPC_EXCL:**不存在则创建,存在则报错(一般用于创建消息队列);
- 权限位(如
0666):控制进程对队列的访问权限; - 返回值:成功返回消息队列 ID(msgid),失败返回 - 1。
(3)msgsnd:发送消息
cpp
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
- **msqid:**msgget返回的消息队列 ID;
- msgp: 指向消息结构 体的指针 (需自定义,首字段必须是long mtype消息类型);
- msgsz: 消息正文长度(不含消息类型字段);
- **msgflg:**发送标志(0阻塞,IPC_NOWAIT非阻塞);
- 返回值:成功返回 0,失败返回 - 1。

(5)msgctl:控制消息队列(核心功能:删除)
cpp
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
- cmd: 控制命令(核心为 IPC_RMID,删除消息队列);
- buf: 存储队列属性的结构体指针(IPC_RMID 时可设为 NULL);
- 返回值:成功返回 0,失败返回 - 1。

1.3 实战案例:消息队列实现 C/S 通信
通过**"服务端 + 客户端"**演示消息队列的使用,服务端接收客户端消息并回复,客户端发送消息并接收回复。
1.3.1 公共头文件(comm.hpp)
cpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <sys/msg.h>
// 错误处理宏
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
// key_t ftok(const char *pathname, int proj_id);
const int projid = 0x66; // int proj_id
const std::string pathname = "."; // const char *pathname
const int gmode = 0666; // 权限位
struct my_msgbuf
{
long mtype; // 消息类型(服务端→客户端:100,客户端→服务端:200)
char mtext[1024]; // 消息正文
};
#define CREATER "creater"
#define USER "user"
// 消息队列Msg结构体
class Msg
{
public:
Msg(const std::string &pathname, int projid, const std::string &usertype)
: _msgid(-1), _usertype(usertype)
{
_key = ftok(pathname.c_str(), projid);
if (_key == -1)
{
ERR_EXIT("ftok");
}
printf("msg key:0x%x\n", _key);
// 创建/获取信号量
if (usertype == USER)
{
Get();
}
else if (usertype == CREATER)
{
Creat();
}
}
// 发送消息
void SendMsg(long type, const char *text)
{
struct my_msgbuf msg;
msg.mtype =type;
strncpy(msg.mtext, text, 1023);
int n = msgsnd(_msgid, &msg, strlen(msg.mtext), 0);
if (n < 0)
{
ERR_EXIT("msgsnd");
}
}
// 接收消息
void RecvMsg(long type, char *text)
{
struct my_msgbuf msg;
msg.mtype = type;
ssize_t n = msgrcv(_msgid, &msg, 1024, type, 0);
if (n < 0)
{
ERR_EXIT("msgrcv");
}
// 结尾手动加\0
msg.mtext[n] = 0;
strncpy(text, msg.mtext, 1023);
}
~Msg()
{
if (_usertype == CREATER)
{
Destroy();
printf("服务端:消息队列已删除\n");
}
else
{
printf("客户端:退出通信\n");
}
}
private:
// 接口私有化:将所有接口在构造函数中调用,避免接口面向用户
// 创建全新消息队列
void Creat()
{
CreatHelper(IPC_CREAT | IPC_EXCL | gmode);
printf("服务端: 创建消息队列成功, msgid=%d\n", _msgid);
}
// 获取已存在的消息队列
void Get()
{
CreatHelper(IPC_CREAT);
printf("客户端: 获取消息队列成功, msgid=%d\n", _msgid);
}
// 内部通用函数:创建/获取消息队列
void CreatHelper(int flg)
{
_msgid = msgget(_key, flg);
if (_msgid == -1)
{
ERR_EXIT("msgget");
}
}
// 删除消息队列
void Destroy()
{
int n = msgctl(_msgid, IPC_RMID, nullptr);
if (n < 0)
{
ERR_EXIT("msgctl");
}
}
private:
key_t _key;
int _msgid;
std::string _usertype;
};
1.3.2 服务端(server.cc)
cpp
#include "comm.hpp"
int main()
{
// 1. 创建消息队列(通信发起者)
Msg msg(pathname, projid, CREATER);
// 2. 循环接收客户端消息(类型200)并回复消息给客户端(类型100)
char buffer[1024];
while (1)
{
memset(buffer, 0, sizeof(buffer));
// 接收客户端消息
msg.RecvMsg(200, buffer);
printf("服务端收到:%s\n", buffer);
// 回复客户端
if (strcmp(buffer, "quit") == 0)
{
msg.SendMsg(100, "客户端退出,服务端即将关闭");
break;
}
msg.SendMsg(100, "已收到你的消息");
sleep(1);
}
// 3. 删除消息队列
// 进程结束自动调用析构函数
return 0;
}
1.3.3 客户端(client.cc)
cpp
#include "comm.hpp"
int main()
{
// 1. 获取消息队列
Msg msg(pathname, projid, USER);
// 2. 循环发送消息(类型200)并接收回复(类型100)
char buffer[1024];
while (true)
{
memset(buffer, 0, sizeof(buffer));
printf("客户端请输入:");
fflush(stdout);
fgets(buffer, 1024, stdin);
buffer[strlen(buffer) - 1] = '\0'; // 去除换行符
// 发送消息
msg.SendMsg(200, buffer);
if (strcmp(buffer, "quit") == 0)
{
break;
}
// 接收回复
memset(buffer, 0, sizeof(buffer));
msg.RecvMsg(100, buffer);
printf("服务端回复:%s\n", buffer);
}
return 0;
}
1.3.4 编译与运行
Makefile:
cpp
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server

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描述:
cpp
struct sem_array
{
struct kern_ipc_perm sem_perm; //IPC权限信息(所有者、权限掩码、key等,参考ipc.h)
int sem_id; //信号量集合在内核的标识符semid
time_t sem_otime; //最后一次执行semop()操作的时间戳
time_t sem_ctime; //最后一次修改信号量集合属性的时间(semctl/创建/删除)
struct sem *sem_base; //指向信号量数组首元素的指针,数组内是单个struct sem
struct sem_queue *sem_pending;//阻塞等待该信号量集合的进程等待队列头(挂起的阻塞操作)
struct sem_queue **sem_pending_last; //等待队列尾指针,用于快速尾插新阻塞进程
struct sem_undo *undo; //关联该信号量集的sem_undo链表,实现进程退出自动撤销信号量操作
unsigned long sem_nsems; //当前信号量集合内包含的信号量个数
};
cpp
struct sem
{
int semval; // 信号量计数器值
pid_t sempid; // 最后一次操作该信号量的进程PID
unsigned short semncnt; // 等待计数器>0的进程数
unsigned short semzcnt; // 等待计数器=0的进程数
};
2.2.2 核心特性


为什么要有信号量?

2.3 核心 API 详解
System V 信号量的 API 与消息队列、共享内存风格一致,核心包括semget、semop、semctl。
2.3.1 semget:创建 / 获取信号量集
cpp
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
参数:
- nsems:信号量集 中的信号量个数(创建时必须指定,获取时可设为 0);
- 其他参数与上面的msgget 一致;
- 返回值:成功返回信号量集 ID(semid),失败返回 - 1。

2.3.2 semop:执行 P/V 操作(核心 API)
cpp
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
参数:
- sops: 指向 struct sembuf 数组的指针,每个元素描述一个信号量的操作;
- nsops: sops数组的长度(操作的信号量个数);
- struct sembuf结构体:
cpp
struct sembuf
{
unsigned short sem_num; // 信号量集中的信号量下标(从0开始)
short sem_op; // 操作类型(-1=P操作,+1=V操作)
short sem_flg; // 操作标志(0=阻塞,IPC_NOWAIT=非阻塞)
};

2.3.3 semctl:控制信号量集
cpp
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
参数:
- semnum: 信号量集中的信号量下标(操作单个信号量时指定);
- **cmd:**控制命令(核心命令如下);
- 可变参数: 根据cmd不同,可传入union semun结构体 (用于设置信号量初始值);
核心命令说明:
| 命令 | 描述 |
|---|---|
| IPC_RMID | 删除信号量集(忽略其他参数) |
| SETVAL | 设置单个信号量的初始值(需传入 union semun) |
| GETVAL | 获取单个信号量的当前值 |
| SETALL | 设置信号量集中所有信号量的初始值 |
| GETALL | 获取信号量集中所有信号量的当前值 |


2.3.4 union semun 结构体(需手动定义)
cpp
union semun
{
int val; // 用于SETVAL/GETVAL
struct semid_ds *buf; // 用于IPC_STAT/IPC_SET
unsigned short *array; // 用于SETALL/GETALL
struct seminfo *__buf; // 用于IPC_INFO
};
2.4 实战案例:信号量保护共享内存
结合共享内存和信号量,实现**"多进程安全写共享内存"**------ 通过信号量的互斥机制,确保同一时刻仅一个进程写入共享内存,避免数据混乱。
2.4.1 公共头文件(sem_comm.h)
cpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/sem.h>
// 错误处理宏
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
// key_t ftok(const char *pathname, int proj_id);
const int projid = 0x66; // int proj_id
const std::string pathname = "."; // const char *pathname
const int gmode = 0666; // 权限位
#define SEM_NUM 1 // 信号量集中的信号量个数
// 定义semun联合体,用于semctl系统调用中的可变参数
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
#define CREATER "creater"
#define USER "user"
// 信号量sem结构体
class Sem
{
public:
Sem(const std::string &pathname, int projid, const std::string &usertype)
: _semid(-1), _usertype(usertype)
{
_key = ftok(pathname.c_str(), projid);
if (_key == -1)
{
ERR_EXIT("ftok");
}
printf("sem key:0x%x\n", _key);
// 创建/获取信号量
if (usertype == USER)
{
Get();
}
else if (usertype == CREATER)
{
Creat();
// void InitSem(int semid, int semnum, int val)
// semnum:信号量集中的信号量下标(操作单个信号量时指定)
// val:初始化信号量值为 1(互斥场景初始值设为 1(二元信号量))
InitSem(_semid, 0, 1);
}
}
// P操作
void P()
{
struct sembuf sb;
//初始化struct sembuf sb
sb.sem_num = 0; //下标为0的信号量
sb.sem_op = -1; // P操作:计数器-1,此时联合体semun中的val减为0,其他进程无法再进入
sb.sem_flg = 0; // 阻塞模式
int n = semop(_semid, &sb, 1);
if(n == -1)
{
ERR_EXIT("semop P");
}
}
// V操作
void V()
{
struct sembuf sb;
//初始化struct sembuf sb
sb.sem_num = 0; //下标为0的信号量
sb.sem_op = 1; // V操作:计数器+1,此时联合体semun中的val恢复成1,使其他进程可以进入
sb.sem_flg = 0; // 阻塞模式
int n = semop(_semid, &sb, 1);
if(n == -1)
{
ERR_EXIT("semop V");
}
}
~Sem()
{
if (_usertype == CREATER)
{
Destroy();
}
}
private:
// 接口私有化:将所有接口在构造函数中调用,避免接口面向用户
// 创建信号量集
void Creat()
{
CreatHelper(IPC_CREAT | IPC_EXCL | gmode);
}
// 获取信号量集
void Get()
{
CreatHelper(IPC_CREAT);
}
// 信号量集通用函数
void CreatHelper(int flg)
{
_semid = semget(_key, 1, flg);
if (_semid == -1)
{
ERR_EXIT("semget");
}
}
// 初始化信号量
void InitSem(int semid, int semnum, int val)
{
// 创建联合体semun对象,对val进行初始化
union semun un;
un.val = val;
int n = semctl(semid, semnum, SETVAL, un);
if (n < 0)
{
ERR_EXIT("semctl SETVAL");
}
}
// 删除信号量集
void Destroy()
{
int n = semctl(_semid, 0, IPC_RMID);
if (n < 0)
{
ERR_EXIT("semctl IPC_RMID");
}
}
private:
int _semid;
key_t _key;
std::string _usertype; // 判断是使用者还是创建者,用于区分调用不同的函数
};
//复用共享内存Shm结构体
class Shm
{
public:
Shm(const std::string &pathname, int projid, const std::string &usertype)
: _shmid(-1), _size(4096), _start_mem(nullptr), _usertype(usertype)
{
// 获取唯一 Key
_key = ftok(pathname.c_str(), projid);
if (_key == -1)
{
ERR_EXIT("ftok");
}
printf("key:0x%x\n", _key);
// 创建/获取共享内存
if (usertype == USER)
{
Get();
}
else if (usertype == CREATER)
{
Creat();
}
// 进程连接共享内存
Attach();
}
void *GetVirtualAddr()
{
printf("VirtualAddr:%p\n", _start_mem);
return _start_mem;
}
// 获取指定共享内存的相关属性
void Attr()
{
struct shmid_ds ds; // 描述共享内存的数据结构
int n = shmctl(_shmid, IPC_STAT, &ds); // ds:输出型参数
printf("shsm_segsz: %ld\n", ds.shm_segsz);
printf("key: 0x%x\n", ds.shm_perm.__key);
printf("uid: %d\n", ds.shm_perm.uid);
}
// 析构函数
~Shm()
{
Detach();
if (_usertype == CREATER)
{
Destroy();
}
}
private:
// 接口私有化:将所有接口在构造函数中调用,避免接口面向用户
// 创建共享内存
void Creat()
{
CreatHelper(IPC_CREAT | IPC_EXCL | gmode);
}
// 获取共享内存
void Get()
{
CreatHelper(IPC_CREAT);
}
void CreatHelper(int flg)
{
_shmid = shmget(_key, _size, flg);
if (_shmid == -1)
{
ERR_EXIT("shmget");
}
printf("shmid:%d\n", _shmid);
}
// 共享内存映射挂载
void Attach()
{
_start_mem = shmat(_shmid, nullptr, 0);
if (_start_mem == (void *)(-1))
{
ERR_EXIT("shmat");
}
printf("attach success\n");
}
// 去关联
void Detach()
{
int n = shmdt(_start_mem);
if (n == 0)
{
printf("detach success\n");
}
else
{
ERR_EXIT("shmdt");
}
}
// 删除共享内存
void Destroy()
{
if (_shmid == -1)
return;
int n = shmctl(_shmid, IPC_RMID, nullptr);
if (n < 0)
{
ERR_EXIT("shmctl");
}
else
{
printf("shmctl delete shm:%d success\n", _shmid);
}
}
private:
int _shmid;
int _size;
void *_start_mem; // 共享内存在地址空间的起始地址
key_t _key;
std::string _usertype; // 判断是使用者还是创建者,用于区分调用不同的函数
};
2.4.2 服务端(server.cc)
cpp
#include "sem_comm.hpp"
int main()
{
// 1. 创建信号量集(1个信号量)并初始化(初始值1,互斥)
Sem sem(pathname, projid, CREATER);
// 2. 创建共享内存
Shm shm(pathname, projid, CREATER);
char *shmaddr = (char *)shm.GetVirtualAddr();
// 3. 循环写入共享内存(P/V操作保护临界区)
for (int i = 0; i < 4; i++)
{
sem.P(); // 申请资源(进入临界区)
snprintf(shmaddr, 1024, "进程[%d]写入数据:%d", getpid(), i);
printf("进程[%d]写入:%s\n", getpid(), shmaddr);
sleep(3);
sem.V(); // 释放资源(退出临界区)
}
sleep(2);
// 4. 清理资源
// 进程结束自动调用sem和shm的析构函数
return 0;
}
2.4.3 客户端(client.cc)
cpp
#include "sem_comm.hpp"
int main()
{
// 1. 获取信号量集
Sem sem(pathname, projid, USER);
// 2. 获取共享内存
Shm shm(pathname, projid, USER);
char *shmaddr = (char *)shm.GetVirtualAddr();
// 3. 循环读取共享内存(P/V操作保护临界区)
for (int i = 0; i < 4; i++)
{
sem.P(); // 申请资源(进入临界区)
printf("进程[%d]读取:%s\n", getpid(), shmaddr);
// sleep(3);
sem.V(); // 释放资源(退出临界区)
}
// 4. 清理资源
//进程结束自动调用sem和shm的析构函数
return 0;
}
2.4.4 编译与运行
Makefile:
bash
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server

- 关键:信号量初始值为 1,确保同一时刻仅一个进程进入临界区(读写共享内存),避免数据混乱。
2.5 信号量避坑指南
- 信号量初始值设置 :互斥场景 初始值设为 1(二元信号量 ),同步场景 按资源数量设置(如 2 个资源初始值设为 2:多元信号量);
- P/V 操作成对出现 :进入临界区前执行 P 操作,退出后执行 V 操作,避免死锁;
- 信号量集删除时机 :需等待所有进程 完成操作后再删除,否则正在操作的进程会报错;
- 避免信号量滥用 :信号量仅用于同步互斥 ,不用于数据传输,不要试图通过信号量传递信息。
三. 内核如何管理 System V IPC 资源
System V IPC(共享内存、消息队列、信号量)的内核管理逻辑一致 ,核心通过 struct ipc_ids 和 struct kern_ipc_perm 实现全局管理,这是理解 System V IPC 本质的关键。

3.1 核心管理结构
我们这里仅展示两个具有共性的管理结构体(共享内存,消息队列,信号量都有),剩下这些结构之间的关联的具体流程看后面的部分!
3.1.1 struct ipc_ids(全局管理结构体)
内核 维护三个全局 ipc_ids 结构体 ,分别管理共享内存 、消息队列 、信号量(有三个静态全局变量):
cpp
struct ipc_ids
{
int in_use; // 当前使用的IPC资源个数
int max_id; // 最大的IPC资源ID
unsigned short seq; // 序列号(用于生成唯一ID)
unsigned short seq_max; // 序列号最大值
struct mutex mutex; // 保护该结构体的互斥锁
struct ipc_id_ary nullentry; // 空条目
struct ipc_id_ary *entries; // 指向IPC资源数组的指针
};
- 作用:记录系统中所有该类型 IPC 资源的元数据,实现资源的创建、查找、删除。
3.1.2 struct kern_ipc_perm(权限控制结构体)
所有 System V IPC 资源都包含 struct kern_ipc_perm 字段,用于权限控制和唯一标识:
cpp
struct kern_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)
unsigned long seq; // 序列号
void *security; // 安全相关指针
};
- 核心 :key 字段是 IPC 资源的全局唯一标识 ,mode 字段控制进程对资源的访问权限。
3.2 内核管理流程图(重点)




3.4 结论和总结
- System V IPC 资源的生命周期随内核 ,本质是内核维护 的结构体和数据结构;
- key 是资源的全局唯一标识 ,msgid 是进程访问资源的句柄;
- 权限控制 通过 kern_ipc_perm.mode 实现,与文件权限规则一致。
System V 消息队列、信号量 核心要点总结:
- 消息队列:用于结构化、异步跨进程通信,支持按类型筛选消息,适合数据传输场景;
- 信号量:用于同步与互斥,通过 P/V 操作保护临界资源,常与共享内存配合使用;
- 内核管理 :System V IPC 通过ipc_ids 和kern_ipc_perm 实现全局管理 ,生命周期随内核 ,需手动删除;
- 选型建议 :
- 需结构化数据传输→消息队列;
- 需同步互斥→信号量;
- 需高效大数据传输→共享内存 + 信号量。
结束语
至此,我们已经完整走完了 System V IPC 的三大组件:消息队列、共享内存和信号量。消息队列提供结构化的数据传递,共享内存以绕开内核的机制实现最高效的通信,而信号量则通过 P/V 操作解决了共享内存的同步问题。同时,我们也深入内核,看到了
ipc_ids、kern_ipc_perm等管理结构,理解了操作系统"先描述,再组织"的管理思路。System V IPC 是 Linux 传统 IPC 的核心,掌握这三种技术,足以应对绝大多数跨进程通信场景,也为后续学习网络编程和多线程同步打下坚实基础。