第 48 章 System V 共享内存 (System V Shared Memory)
核心结论
-
共享内存本质:多个进程把同一物理页映射到自己的虚拟地址空间——「无内核介入」最快的 IPC;写后其他进程立即可见。
-
shmget 创建/打开:
shmget(key, size, shmflg)创建/打开指定字节的 shm 段(按页对齐);返回 shmid。 -
shmat / shmdt 映射/解除:
shmat(id, NULL, 0)把 shm attach 到进程虚拟地址空间返回指针(推荐 NULL 让内核选地址);shmdt(addr)解除(进程退出自动 detach)。 -
共享内存坐标 = 指针 vs 偏移:因为 shm 在不同进程可能 attach 到不同地址——存储「指针」用 偏移(相对 baseaddr),不要存绝对指针;链表/树用 offset 串起来。
-
shmctl 控制:IPC_RMID 标记待删(nattch=0 才真删);IPC_STAT/SET;SHM_LOCK/UNLOCK 锁页(2.6.10+ 非特权可锁 own 段)。
-
典型用法:和 SysV 信号量(binary sema)配合——一个 sem 控制「共享内存互斥访问」,一个 sem 控制「读/写轮流」——见
svshm_xfr_writer/reader.c。 -
Limits:SHMMNI (段数)、SHMMAX (单段最大)、SHMALL (系统总页);/proc/sys/kernel/shm* 调整;SHM_HUGETLB 用 huge pages。
|
本章主旨
SysV 共享内存是「最快的 IPC」——一次 shmat 后,进程对 shm 内存的读写直接命中物理页,不经内核。本章覆盖 (1) shmget/shmat/shmdt/shmctl 的具体语义;(2) shmid_ds 字段;(3) 偏移而非指针存储(关键点!);(4) 与 sem 配合的经典模式( |
一、核心概念
本章围绕 6 个核心概念展开:从 shm 速度本质、shmget/shmat/shmdt、shmid_ds、shmctl、坐标存储、Limits。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
shm 本质(最快 IPC) |
多进程把同一物理页映射到自己虚拟地址;写后其他进程立即可见;不需内核介入;user→user 而非 user→kernel→user |
§48.0;shm 不是「自动同步」——必须用 sem/file lock;shm 段用 tmpfs 实现(§48.5) |
shmget + shmat + shmdt |
|
§48.2-3;shmat NULL 让内核选地址(最常见);SHM_RDONLY 只读 attach;shmat 后像普通 C 指针 |
shmid_ds 关联数据 |
|
§48.8;shm_perm.mode 含 SHM_DEST(待删)和 SHM_LOCKED(已锁)两个 read-only flag |
shmctl IPC_RMID 延迟删除 |
|
§48.7;server 创建段后立即 |
坐标用偏移(关键) |
shm 在不同进程 attach 到不同地址——结构内的「指针」用 offset(相对 baseaddr); |
§48.6;树/链表在 shm 里都用 offset 串起来;用绝对指针 = 数据损坏 |
shm + sem 协调模式 |
|
§48.4; |
二、详细笔记
48.1-48.2 shmget 创建/打开
What:shmget(key, size, shmflg) 创建/打开共享内存段。
Why:最快的 IPC——写后立即可见。
How:
// 摘自《The Linux Programming Interface》 第 48 章
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg); /* 返 shmid 或 -1 */
/* 创建 1K 段 */
int shmid = shmget(IPC_PRIVATE, 1024, 0660);
/* 打开已存在 */
int shmid = shmget(key, 0, 0); /* size 忽略但须 ≤ 实际大小 */
shmflg flags:
. 9 个低 bit = mode。
. IPC_CREAT/IPC_EXCL 同其他 SysV IPC。
. SHM_HUGETLB(Linux 2.6+):特权 (CAP_IPC_LOCK) 才能用——用 huge pages 减少 TLB miss。
. SHM_NORESERVE(Linux 2.6.15+):不预留 swap(同 MAP_NORESERVE)。
size 按页对齐——实际段大小 = ceil(size / page_size) * page_size。
When:server 启动用 shmget(IPC_PRIVATE, …) 或 ftok 后 shmget(key, IPC_CREAT, …)。
48.3 shmat + shmdt 映射/解除
What:把 shm 段 attach 到当前进程的虚拟地址空间;返回的指针可像普通 C 指针使用。
Why:attach 后程序可读写;detach 后虚拟地址释放。
How——推荐用法(NULL 让内核选地址):
// 摘自《The Linux Programming Interface》 第 48 章
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
/* NULL shmaddr = 内核选地址(最安全) */
struct shmseg *shmp = (struct shmseg *)shmat(shmid, NULL, 0);
if (shmp == (void *)-1) errExit("shmat");
/* 只读 attach */
shmp = (struct shmseg *)shmat(shmid, NULL, SHM_RDONLY);
/* 解除 */
shmdt(shmp);
shmflg flags:
. SHM_RDONLY:只读 attach。
. SHM_RND:shmaddr 非空时,向下舍入到 SHMLBA 倍数(避免缓存一致性问题)。
. SHM_REMAP(Linux 扩展):替换 shmaddr 处的旧 mapping。
shmaddr 建议*:永远传 NULL——非空是高级用法(在不同进程固定地址、缓存控制)。
attach 权限:shmat 需要 r/w 权限(除非 SHM_RDONLY)。
exec + shm:
. fork() 继承 shm(子共享 attach)。
. exec() 自动 detach 所有 shm。
. 进程终止自动 detach。
When:所有 shm 程序都按此模式。
48.4 svshm_xfr——shm + sem 经典实例
What:svshm_xfr_writer.c + svshm_xfr_reader.c —— 1-reader-1-writer 块传输协议。
Why:理解 shm 与 sem 如何配合的实际模板。
How——3 文件:
// 摘自《The Linux Programming Interface》 第 48 章 Listing 48-1
/* svshm_xfr.h */
struct shmseg {
int cnt; /* buf 字节数 */
char buf[BUF_SIZE]; /* 数据 */
};
#define SHM_KEY 0x1234
#define SEM_KEY 0x5678
#define WRITE_SEM 0
#define READ_SEM 1
// 摘自《The Linux Programming Interface》 第 48 章 Listing 48-2
/* writer:先 init 两个 sem(自己 init sem),create shm,循环 reserve WRITE_SEM */
/* 写数据 → release READ_SEM;EOF = cnt=0 */
int main(void) {
int semid = semget(SEM_KEY, 2, IPC_CREAT | OBJ_PERMS);
initSemAvailable(semid, WRITE_SEM);
initSemInUse(semid, READ_SEM);
int shmid = shmget(SHM_KEY, sizeof(struct shmseg), IPC_CREAT | OBJ_PERMS);
struct shmseg *shmp = shmat(shmid, NULL, 0);
for (;;) {
reserveSem(semid, WRITE_SEM);
shmp->cnt = read(STDIN_FILENO, shmp->buf, BUF_SIZE);
releaseSem(semid, READ_SEM);
if (shmp->cnt == 0) break;
}
/* 清理:等 reader 完成后删 */
reserveSem(semid, WRITE_SEM);
semctl(semid, 0, IPC_RMID, NULL);
shmdt(shmp);
shmctl(shmid, IPC_RMID, NULL);
}
// 摘自《The Linux Programming Interface》 第 48 章 Listing 48-3
/* reader:循环 reserve READ_SEM;写 stdout;release WRITE_SEM;EOF 退出 */
shmp = shmat(shmid, NULL, SHM_RDONLY);
for (;;) {
reserveSem(semid, READ_SEM);
if (shmp->cnt == 0) break;
write(STDOUT_FILENO, shmp->buf, shmp->cnt);
releaseSem(semid, WRITE_SEM);
}
shmdt(shmp);
releaseSem(semid, WRITE_SEM); /* 通知 writer 可清理 */
When:写「shm 块传输」应用的标准模板;可扩到 N-reader-N-writer(用 4 个 sem)。
48.5 共享内存虚拟地址位置
What:shm attach 的虚拟地址在 task_unmapped_base 之上(x86-32 = 0x40000000);mmap 也在同一区域。
Why:理解 /proc/PID/maps 的输出——shm 段 vs mmap vs shared library。
How——/proc/PID/maps 行:
b7bed000-b7f0d000 rw-s 00000000 00:09 9666565 /SYSV00000000 (deleted)
b7f0d000-b7f26000 rw-s 00000000 00:09 9633796 /SYSV00000000 (deleted)
解读:
. 起始-结束地址
. 权限 rw-s(read+write+shared)
. 文件偏移(shm 总是 0)
. dev:inode(shm 是 tmpfs 文件的 inode)
. 名称:/SYSV<key>(IPC_PRIVATE 时是 0)
. (deleted):tmpfs 文件被 unlink 但 mapping 还在——是 Linux 共享内存的实现方式
When:调试时 /proc/PID/maps 是最直接的 shm 调试工具。
48.6 共享内存中存指针——必须用偏移
What:shm 在不同进程 attach 到不同地址——存「绝对指针」会让读端拿到错的值。
Why:*最常见 shm bug*之一。
How——target = p 的正确做法:
// 摘自《The Linux Programming Interface》 第 48 章
char *baseaddr = (char *)shmat(...); /* 自己 attach 的地址 */
/* 设:p 指向的字段应存 target 的「相对 baseaddr 的偏移」 */
*((long *)p) = (long)(target - baseaddr);
/* 用:把偏移加回 baseaddr 得到 target */
target = baseaddr + *((long *)p);
```
或者用「数组 index」——把 shm 当成 fixed-size struct 数组,index 就是天然 offset。
*When*:在 shm 里建链表/树/图结构时永远用 offset。
=== 48.7 shmctl 控制
*What*:`shmctl(shmid, cmd, buf)` 提供控制操作。
*Why*:删除、统计、锁页、改权限。
*How*——常见 cmd:
[source,c]
shmctl(shmid, IPC_RMID, NULL); /* 标记待删(nattch=0 时真删) / shmctl(shmid, IPC_STAT, &ds); / 取 shmid_ds / shmctl(shmid, IPC_SET, &ds); / 改 uid/gid/mode / shmctl(shmid, SHM_LOCK, NULL); / 锁页(特权 / own segment + RLIMIT_MEMLOCK) / shmctl(shmid, SHM_UNLOCK, NULL); / 解锁 */
**IPC_RMID 立即删 vs 标记删**: . shm 不像 msg/sem 立即删;标记为「待删」等 nattch=0。 . 应用模式:server 创建 + attach 后立即 `IPC_RMID`——「unlink 立即删」语义;最末用户 detach 时实际删除。 . Linux 还允许「已 IPC_RMID 但 nattch>0」的 attach(其他 UNIX 多禁止)——可移植代码不要依赖。 *SHM_LOCK 锁页*: . 把 shm 锁在物理 RAM——不被 swap。 . 2.6.10 之前需要 CAP_IPC_LOCK。 . 2.6.10+ 非特权可锁 own 段(需 RLIMIT_MEMLOCK)。 *When*:server 启动 shm 后立即 `IPC_RMID`;高频实时处理时考虑 `SHM_LOCK`。 === 48.8 shmid_ds 关联数据 *What*:每 shmid 对应 `struct shmid_ds` 9 字段(§48.8)。 *Why*:决定 IPC_STAT/SET 行为、决定何时真删。 *How*——关键字段: [source,c]
struct shmid_ds { struct ipc_perm shm_perm; /* 权限 / size_t shm_segsz; / 段大小(创建时请求) / time_t shm_atime; / 最后 shmat / time_t shm_dtime; / 最后 shmdt / time_t shm_ctime; / 最后变化 / pid_t shm_cpid; / 创建者 PID / pid_t shm_lpid; / 最后 shmat/shmdt PID / shmatt_t shm_nattch; / 当前 attach 数 */ };
`shm_perm.mode` 含两个 read-only flag: . `SHM_DEST`:段被标记待删。 . `SHM_LOCKED`:段已被锁页。 *When*:监控用 `IPC_STAT` 读;`nattch==0` 才是真删。 === 48.9 共享内存限制 *What*:kernel 限制 SysV shm 数量与大小。 *Why*:防资源耗尽。 *How*——Limits(来自 §48.9 Table 48-2): [cols="1,3,2", options="header"] |=== | 限制 | 含义 | Linux 调整 | SHMMNI | 最大段数 | ≤ 32768 (IPCMNI);`/proc/sys/kernel/shmmni` | SHMMAX | 单段最大字节 | 实际由 RAM + swap 决定;`/proc/sys/kernel/shmmax` | SHMALL | 系统级总页数 | 实际由 RAM + swap 决定;`/proc/sys/kernel/shmall` | SHMMIN | 最小段大小 | 固定 1(实际 = page size) |=== *Linux 没有 SHMSEG*(per-process 限制)。 *SHM_HUGETLB 2.6+*:用 huge page 减少 TLB 压力。 *When*:装大内存应用时调 SHMMAX + SHMALL;做实时/科学计算时考虑 SHM_HUGETLB。 == 三、关键图表 [NOTE] .非可视化条目(shm 关键 syscall 与 flags) ==== [cols="1,3", options="header"] |=== | 项 | 描述 | `shmget(key, size, shmflg)` | 创建/打开;按页对齐 | `shmat(id, NULL, 0)` | attach 到虚拟地址;返指针 | `shmat(id, NULL, SHM_RDONLY)` | 只读 attach | `shmdt(ptr)` | 解除 attach | `shmctl(id, IPC_RMID, NULL)` | 标记待删(nattch=0 时真删) | `shmctl(id, IPC_STAT/SET, &ds)` | 读/写 shmid_ds | `shmctl(id, SHM_LOCK/UNLOCK, NULL)` | 锁页 / 解锁 | `SHM_HUGETLB` | Linux:用 huge pages | `SHM_NORESERVE` | Linux:不预留 swap | `SHM_RDONLY / SHM_RND / SHM_REMAP` | 只读 / 舍入 / 替换 | `fork()` | 继承 shm attach | `exec()` | detach 所有 shm | `_exit()` | 自动 detach | `struct shmid_ds` | 9 字段:perm/segsz/atime/dtime/ctime/cpid/lpid/nattch | `shm_nattch` | 当前 attach 进程数;0 = 真删 | `shm_perm.mode` | 含 SHM_DEST + SHM_LOCKED 标志 | `proc PID maps` | 看 shm 映射:`rw-s ... /SYSV<key> (deleted)` | Limits: SHMMNI/SHMMAX/SHMALL | 调优点 |=== ==== == 四、思维导图 [source,mermaid]
mindmap root第 48 章 SysV 共享内存 最快 IPC user user 而非 user kernel user 共享物理页 无自动同步 shmget 按页对齐 IPC_PRIVATE 或 ftok SHM_HUGETLB shmat shmdt NULL 让内核选地址 SHM_RDONLY 只读 exec 时自动 detach 偏移而非指针 不同进程 attach 不同地址 target baseaddr 数组 index svshm_xfr 模式 writer reserve WRITE SEM read 写 shm release READ SEM reader 反之 cnt 0 EOF shmid_ds nattch 0 才真删 SHM_DEST 标志 SHM_LOCKED IPC_RMID 立即删 server create 后立即删 nattch 0 时真删 limits SHMMNI 段数 SHMMAX 单段 SHMALL 总页
五、重点与易错点
-
shm 是最快的 IPC 但要自己同步——必须有 sem 或 file lock 配合;不要以为「写后立即可见」就够了。
-
存指针用偏移——
target = baseaddr + *p;绝对指针在不同进程 attach 地址不同时会错位(最常见 shm bug)。 -
shmget 按页对齐——实际段大小 = ceil(size / page_size) * page_size;多分配一些无妨。
-
shmat(NULL, …) 推荐——非 NULL 是高级用法(避免 cache aliasing 等),多数情况 NULL 就够。
-
shmat 允许多次 attach——同一进程可多次 attach;权限可不同(read-only + read-write 混用)。
-
shmctl(IPC_RMID) 标记待删——nattch=0 时真删;不是立即删。
-
「server 创建后立即 IPC_RMID」是推荐模式——最末 detach 时实际删除;不需要最末用户记 IPC_RMID。
-
fork 继承 shm——父子共享 attach;exec 自动 detach。
-
SHM_LOCK 锁页——2.6.10+ 非特权可锁 own 段(用 RLIMIT_MEMLOCK);实时应用考虑。
-
Linux 允许「已 IPC_RMID 但 nattch>0」时 attach——可移植代码不要依赖(其他 UNIX 多禁止)。
-
SHM_HUGETLB——2.6+;用 huge pages 减少 TLB miss;需 CAP_IPC_LOCK。
-
Limits 调优——
/proc/sys/kernel/shmmax/shmall/shmmni调大后装大内存应用。 -
tmpfs 实现——shm 段本质是 tmpfs 文件 unlink 后的引用;/proc/PID/maps 显示
/SYSV<key> (deleted)。 -
0 长度 mtext——writer 用
cnt=0表示 EOF(reader 检测 cnt==0 退出)。 -
跨章衔接:第 24 章 fork;第 27 章 exec;第 44 章 管道;第 47 章 SysV sem(与 shm 配合);第 49 章 mmap;第 54 章 POSIX shm。