
🔥keyipatience:个人主页
🎬作者简介:C/C++后端开发学习者
⭐️patience is key in life

System背景:
System V 是一套经典的 Unix IPC 标准,除了共享内存,还包含消息队列、信号量。Linux 内核专门实现了对应的模块来支持这些机制,它们的接口设计和使用方式有很强的相似性。
System V 共享内存的原理示意图

1.工作流程:内核开辟物理内存->进程建立虚拟地址映射->进程直接读写共享内存
2.共享内存需要内核级的 "管理结构" 操作系统中可能同时存在多个共享内存,不同进程组使用不同的共享内存通信。为了区分、管理这些共享内存,内核必须为每一块共享内存创建一个内核结构体对象 (比如 struct shmid_kernel)
共享内存的生命周期:(后面把完整的代码写了,再仔细说
shmget 接口
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数:
| 参数 | 含义 | 说明 |
|---|---|---|
key |
共享内存的键值 | 用来唯一标识一块共享内存,不同进程通过相同的 key 就能找到同一块共享内存 (用户态传入内核的,才能让多个进程提前约定同一个标识。) |
size |
共享内存大小 | 单位是字节,内核实际会按页大小向上对齐(比如 4KB) |
shmflg |
标志位 | 控制创建 / 打开行为 + 权限位,比如 IPC_CREAT、IPC_EXCL |
返回值
- 成功:返回一个非负整数 ,即共享内存标识符
shmid(后续shmat/shmdt/shmctl都要用它) - 失败:返回
-1,并设置errno
核心标志位详解
1. IPC_CREAT
- 作用:创建共享内存
- 逻辑:如果
key对应的共享内存不存在 ,就创建它;如果已经存在 ,就直接打开并返回它的shmid。
2. IPC_CREAT | IPC_EXCL
- 作用:强制创建全新的共享内存
注意:IPC_EXCL 不能单独使用,必须和 IPC_CREAT 一起用,否则无意义
权限位
shmflg 还需要带上权限位
- 强制创建全新共享内存:shmget(
IPC_CREAT | IPC_EXCL | 0666) - 打开已存在的共享内存:shmget(IPC_CREAT)
ftok 函数
把一个已存在的文件路径 和一个项目 ID,转换成一个唯一的 key_t 类型的 key 值
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
参数:
pathname:必须是一个存在的文件路径 (比如当前目录下的一个普通文件),内核会根据文件的inode号来计算key。proj_id:项目 ID,低 8 位有效(通常填一个非 0 的整数,比如0x66),用来区分同一个文件生成的不同key。- 原理:
ftok会把文件的inode号和proj_id拼接起来,生成一个唯一的key。只要文件路径和proj_id不变,生成的key就不变。
shmctl
控制接口,用来管理共享内存的
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
| 参数 | 含义 |
|---|---|
shmid |
共享内存标识符,是 shmget 的返回值 |
cmd |
要执行的控制命令(核心参数) IPC_RMID:标记共享内存为删除 IPC_STAT:获取共享内存状态 IPC_STAT:获取共享内存状态 |
buf |
指向 struct shmid_ds 结构体的指针,用于获取 / 设置共享内存属性(现在不管写成nullptr即可) |
返回值
- 成功:返回
0(部分命令会返回非负值,如IPC_INFO) - 失败:返回
-1,并设置errno
shmat
共享内存的挂载接口,作用是把内核里的共享内存映射到进程的虚拟地址空间,
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
| 参数 | 含义 |
|---|---|
shmid |
shmget 返回的共享内存标识符 |
shmaddr |
指定映射到进程的哪个虚拟地址,一般填 NULL,让内核自动选择合适的地址 |
shmflg |
挂载标志,控制映射行为(现在填0即可) |
返回值
- 成功:返回共享内存映射到进程虚拟地址空间的起始地址 (
void*类型) - 失败:返回
(void*)-1,并设置errno。if (p == (void *)-1) { perror("shmat 失败");
查看 System V 共享内存段(ipcs)
ipcs -m
关键字段:
- shmid:共享内存段 ID
- owner:所有者
- bytes:大小
- nattch:挂载(attach)的进程数

ipcrm 删除共享内存
ipcrm -m 123456(shmid)
shmdt
解除当前进程虚拟地址 和 共享内存物理页 的映射关系
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
shmaddr为shmat 返回的共享内存映射起始地址。
返回值
- 成功:返回
0 - 失败:返回
-1,并设置errno
进程先调用 shmdt再shmctl
shmctl IPC_RMID:是给内核共享内存打删除标记,等所有进程都先shmdt 解绑后,内核才彻底释放内存。
演示代码:
Shm.hpp:
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include<sys/stat.h>
using namespace std;
const int projid = 0x66;
const int gmode = 0666;
const string pathname = ".";
#define gdefault -1
#define gsize 4096
#define CREATER "creater"
#define USER "user"
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
class Shm
{
private:
void Get()
{
CreateHelp(IPC_CREAT);
}
void Create()
{
CreateHelp(IPC_CREAT | IPC_EXCL | gmode);
}
void CreateHelp(int flg)
{
umask(0);
// 1.先fork()确定key
_key = ftok(pathname.c_str(), projid);
if (_key < 0)
{
ERR_EXIT("ftok\n");
}
printf("key:0x%x\n", _key);
// 2.创建共享内存,创建的一定是一个全新的共享内存
_shmid = shmget(_key, _size, flg);
if (_shmid < 0)
{
ERR_EXIT("shmget\n");
}
printf("shmid:%d\n", _shmid);
}
void Attach() // 建立物理和虚拟映射
{
_start_men = shmat(_shmid, nullptr, 0);
if (_start_men == (void *)-1)
{
ERR_EXIT("shmat\n");
}
printf("attach success\n");
}
void Detach()
{
int n = shmdt(_start_men);
if (n == 0)
{
printf("detach success\n");
}
}
void Destroy()
{
Detach();
if (_usertype == CREATER)
{
if (_shmid == gdefault)
{
return;
}
int n = shmctl(_shmid, IPC_RMID, nullptr);
if (n ==0)
{
printf("shmctl delete shm: %d success\n", _shmid);
}
else
{
ERR_EXIT("shmctl\n");
}
}
}
public:
Shm(const string &pathname, const int projid, const string &usertype) : _shmid(gdefault), _size(gsize), _start_men(nullptr), _usertype(usertype)
{
if (_usertype == CREATER)
Create();
else if (_usertype == USER)
Get();
else
{
}
Attach();
}
void *VirtualAddr()
{
printf("VirtualAddr:%p\n", _start_men);
return _start_men;
}
int Size()
{
return _size;
}
~Shm()
{
//server和client都会调析构即Destroy但是只有server才会shmctl
Destroy();
}
private:
int _flg;
int _size;
int _shmid;
void *_start_men;
string _usertype;
key_t _key;
};
client.hpp
#include "Shm.hpp"
int main()
{
Shm shm(pathname, projid, USER);
char *mem = (char *)shm.VirtualAddr();
// 可以直接把这段空间当做malloc的空间一样直接使用
int index = 0;
// 初始置0:正在写,因为创建新的共享内存会默认把整块内存全部清为0
mem[0] = 0;
// 从mem+1开始存数据,跳过flag
for (char c = 'A'; c <= 'E'; c++, index++)
{
sleep(1);
mem[index + 1] = c;
}
// 全部写完,标记置1
mem[0] = 1;
// sleep(10);
return 0;
}
server.hpp
#include"Shm.hpp"
int main()
{
Shm shm(pathname,projid,CREATER);
char *mem=(char*)shm.VirtualAddr();
while(mem[0]!=1)
{
sleep(1);
printf("%s\n",mem+1);
}
//跳出循环 → main正常return → 局部shm对象析构执行shmdt清理
printf("客户端写入完成,服务端准备退出\n");
return 0;
}
运行时要先启动server(服务端)把共享内存创建出来,再运行client
若现在server端先退出(要return 0或者exit 终止执行了析构函数后的退出),client端后退出
会看到什么呢?

解释:
-
初始状态 :Server 和 Client 都调用
shmat挂载了共享内存 右边ipcs -m显示:plaintext
key 0x660135b2, shmid 22, nattch 2nattch=2表示当前有 2 个进程挂载了这块共享内存。 -
Server 调用
shmctl(IPC_RMID)此时右边的状态变成:plaintext
key 0x00000000, shmid 9, status destkey变成0x00000000:内核标记这块共享内存为「待删除」,新进程无法再通过shmget打开它。status显示dest:表示已被标记删除。- 但
nattch仍然是1:说明 Client 进程还在挂载使用,所以物理内存和内核对象还没被释放。
-
Client 退出 / 调用
shmdt后nattch会变为0,此时内核才会真正释放这块共享内存,ipcs -m中再也看不到shmid=9的条目。
关键结论
- 调用
IPC_RMID只是 "标记删除",不是立即删除 - 共享内存会一直存在,直到所有挂载它的进程都解除挂载(
shmdt或进程退出),内核才会真正释放它。 - 标记删除后,新进程无法再通过
shmget打开,但已挂载的进程可以继续正常读写,直到自己解除挂载。
共享内存的优缺点
优点
- 映射完成后,一方写入,另一方立刻能看到修改;
- 无系统调用开销,进程间通信速度第一。
最大缺点
- 无内置同步机制内核不提供读写互斥、读写完成通知。会出现并发问题:
- client 写一半,server 就读取残缺数据;
- 多进程同时修改同一块内存,数据错乱(数据不一致)。
- 无自带数据保护没有锁、等待、唤醒逻辑,单纯一块裸内存,必须程序员手动实现同步。
共享内存的大小
举个例子我现在要申请4097
用户逻辑 size(shmget 第二个参数)
你代码里真正要用的缓冲区长度,。代码读写不能超过这个 size,越界访问会触发段错误
(就只给4097)。
内核物理段大小(shmid_ds.shm_segsz)
内核实际分配的、页对齐后的总字节数,ipcs -m 里bytes列就是这个值,永远是 4096 整数倍。
(会给4096*2)
即内核申请了4096*2但用户只能用4097。
管理共享内存的内核结构

可以用shmctl+IPC_STAT和创建的结构体来访问
struct shmid_ds buf;//自己定义的
// 获取shmid对应共享内存内核信息
shmctl(shmid, IPC_STAT, &buf);
printf("段大小:%ld\n", buf.shm_segsz);
printf("权限:%ho\n", buf.shm_perm.mode);
printf("当前挂载进程数:%ld\n", buf.shm_nattch);
printf("创建进程PID:%d\n", buf.shm_cpid);
System信号量

1.信号量是什么
信号量本质是资源计数器,用于记录临界资源剩余可用数量。
2. 电影院类比
放映厅 = 整体临界资源;座位 = 资源细分单元;买票操作 = 信号量 P 申请操作;退票 = 信号量 V 释放操作。
- 访问资源前必须 "买票(P 操作,sem--)",本质是资源预订机制;
- 无剩余座位(
sem ≤ 0)时,进程阻塞挂起等待; - 看完了释放座位(V 操作,sem++)后,唤醒等待的进程
- 操作系统内核保证 P/V 是原子操作,不会被中断
3.信号量解决共享内存并发问题
把一个放映厅当作一个共享内存
- 资源分区隔离将共享内存划分为独立区块(每一个座位就是一个子资源),每个进程只占用专属区块,互不读写对方区域,天然规避冲突。
- 信号量管控访问量用信号量限制同时进入临界区的进程总数,保证:
- 不会多个进程抢占同一内存位置;
- 不会涌入超出资源承载上限的进程。
4.二元信号量和多元信号量

二元信号量(互斥锁)
- 定义:取值仅为
0或1的信号量,初值固定设为 1。 - 作用:实现互斥访问,同一时刻只允许一个进程进入临界区,等价于互斥锁 mutex。
- 适用场景:资源必须整体独占使用
多元(计数)信号量
- 定义:初值大于 1,对应多份独立可拆分资源(如电影院多个座位、共享内存多区块)。
- 适用场景:资源可拆分、多个进程能同时占用不同子资源,互不冲突。
信号量和通信之间关系
1.要想访问信号量,多进程必须先共享同一个信号量
普通局部变量、全局变量仅属于单个进程,其他进程无法访问;System V 信号量专门解决该问题:创建内核层面持久化的信号量集,所有进程通过同一个 key 获取、访问同一份信号量,实现跨进程共享计数。
2.信号量属于 IPC 通信
传统认知里 IPC 是 "传递数据",但广义 IPC 定义:进程间能互相通知、同步、互斥,都属于进程通信。
- 信号量不传输业务数据;
- 通过 P/V 操作改变共享计数器,完成进程间通知、同步、互斥,因此归类为 System V IPC 的一种。
System V 信号量全套系统调用
前置头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
semget ------ 创建 / 获取信号量集
int semget(key_t key, int nsems, int semflg);
参数解读
key:ftok 生成的 IPC 键;nsems:信号量个数semflg:创建标记IPC_CREAT:不存在则新建,存在则直接获取;IPC_CREAT | IPC_EXCL:不存在才创建,已存在则报错,用于保证新建。
返回值
成功返回semid(信号量集标识符,后续所有操作的句柄);失败返回 - 1。
eg:
// 创建1个信号量,权限666,不存在则新建
int semid = semget(key, 1, IPC_CREAT | 0666);
查看指令ipcs -s

semop ------ 执行原子 P/V 操作(核心)
int semop(int semid, struct sembuf *sops, size_t nsops);
参数解读
semid:semget 返回的信号量集 ID;sops:操作结构体数组指针,一次可批量操作多个信号量;nsops:本次要执行几条操作(数组长度)。
结构体 struct sembuf
struct sembuf {
unsigned short sem_num; // 要操作集合里第几个信号量(下标从0开始)
short sem_op; // 操作值:-1=P申请,+1=V释放
short sem_flg; // 操作标记,常用0阻塞等待
};
eg:
struct sembuf p_op = {0, -1, 0};
semop(semid, &p_op, 1);
//sem_num=0:操作集合第 0 个信号量;sem_op=-1=P;0 代表阻塞等待
semctl ------ 信号量集控制(初始化 / 删除 / 查询)
int semctl(int semid, int semnum, int cmd, ...);
参数解读
semid:信号量集 ID;semnum:集合中目标信号量下标;cmd:控制指令
常用cmd
SETVAL:初始化信号量初值
设置单个信号量的初始资源计数,需要配套共用体union semun传参
union semun {
int val; // SETVAL时填入信号量初始值
struct semid_ds *buf; // IPC_STAT/IPC_SET用
unsigned short *array; // GETALL/SETALL批量读写
struct seminfo *__buf; // Linux专属信息查询
};
信号量创建后默认初值为 0,必须用 SETVAL 手动初始化,否则无法正常 P/V。
eg:
union semun {
int val;
};
union semun su;
su.val = 1; // 二元信号量初值1,计数信号量填资源总数
// 给集合第0个信号量赋值
semctl(semid, 0, SETVAL, su);
IPC_RMID:标记待删除
先标记为待删除,直到所有持有 semid 的进程全部终止,内核才会真正释放信号量内存、彻底删除。
eg:
// 销毁整个信号量集合
semctl(semid, 0, IPC_RMID, NULL);