﻿# 共享内存（Shared Memory）—— 两套接口，同一种 IPC

> 共享内存是**最快的 IPC 方式**：两个（或更多）进程把同一块物理内存映射到各自的地址空间，之后数据交换就只是普通的内存读写——没有系统调用、没有数据拷贝、没有内核中转。本篇对比 Linux 上的两套共享内存接口：**System V IPC（`shmget`/`shmat`）**和 **POSIX（`shm_open`/`mmap`）**，讲清它们分别怎么用、本质区别在哪、什么时候选哪套。


> 相关：[mmap.md](/concepts/elf/mmap.md) 讲了 mmap 的完整机制（四象限、缺页惰性映射）——POSIX 共享内存底层就是 `shm_open` 拿到 fd + `mmap(MAP_SHARED)`；[memory-layout.md](/concepts/elf/memory-layout.md) 讲了共享内存映射在地址空间的哪个区域；[../process/fork-and-threads.md](/concepts/process/fork-and-threads.md) 讲了 fork 继承共享映射的语义。

## 零、一句话抓住本质

两套接口的目标完全一样：**让多个进程共享同一块物理内存**。区别只在"怎么找到这块内存"：

| 维度 | System V IPC | POSIX |
|------|-------------|-------|
| **标识方式** | 整数 `key_t`（通过 `ftok()` 生成 / `IPC_PRIVATE`） | 字符串名字（如 `"/my_shm"`） |
| **内核对象** | IPC 对象（`shmid`，一套独立命名空间） | **文件描述符**（fd），挂在 `/dev/shm` 下 |
| **生命周期** | 显式 `shmctl(IPC_RMID)` 删除，否则**永久存在直到重启** | **内核引用计数**：所有进程 unlink + unmap 后自动清理 |
| **接口风格** | 专用函数：`shmget`/`shmat`/`shmdt`/`shmctl` | 文件接口 + mmap：`shm_open`/`ftruncate`/`mmap`/`munmap`/`shm_unlink` |
| **查看/调试** | `ipcs -m` / `ipcrm -m` | `ls /dev/shm/` / `rm /dev/shm/xxx` |
| **权限模型** | `shmget` 的 `mode` 参数（类似文件权限） | 文件权限（`shm_open` 的 `mode`），就是普通文件权限模型 |
| **推荐度** | 老旧且繁琐，建议迁移 | **现代应用首选**，接口干净，与 mmap 统一 |

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<sv>> #FFCDD2
  BorderColor<<sv>> #C62828
  BackgroundColor<<posix>> #C8E6C9
  BorderColor<<posix>> #388E3C
}
rectangle "System V IPC 共享内存\n——\nftok() 生成 key\nshmget() 创建/获取 → shmid\nshmat() 映射到地址空间\nshmdt() 解除映射\nshmctl(IPC_RMID) 删除\n\n管理: ipcs -m / ipcrm -m" <<sv>> as SV
rectangle "POSIX 共享内存\n——\nshm_open(\"/name\") → fd\nftruncate() 设大小\nmmap(MAP_SHARED) 映射\nmunmap() 解除映射\nshm_unlink() 删除\n\n管理: ls /dev/shm/" <<posix>> as POSIX
SV -[hidden]right-> POSIX
note bottom of SV : 老旧接口，基于整数 key\n对象永久存在，易泄漏
note bottom of POSIX : 现代接口，基于 fd\n文件模型，引用计数自动清理
@enduml
```

---

## 一、System V IPC 共享内存

System V IPC 是 AT&T UNIX System V 引入的一套进程间通信机制，包括**共享内存**、**信号量（semaphore）**和**消息队列（message queue）**。共享内存是其中最直接的一个——就是把一段物理页映射到多个进程的地址空间。

### 1.1 核心 API

```c
#include <sys/shm.h>
// ① 创建/获取共享内存段
int shmget(key_t key, size_t size, int shmflg);
// ② 映射到进程地址空间（类似 mmap 的 MAP_SHARED）
void *shmat(int shmid, const void *shmaddr, int shmflg);
// ③ 解除映射（不从内核删除对象）
int shmdt(const void *shmaddr);
// ④ 控制操作（IPC_RMID 删除、IPC_STAT 查看、IPC_SET 改权限等）
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
```

### 1.2 完整使用流程

```c
#include <sys/shm.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <string.h>
int main() {
    key_t key = ftok("/tmp", 'A');  // 用文件+项目ID生成 key
    if (key == -1) { perror("ftok"); return 1; }
    // ① 创建共享内存段（4KB）
    int shmid = shmget(key, 4096, IPC_CREAT | 0666);
    if (shmid == -1) { perror("shmget"); return 1; }
    // ② 映射到本进程地址空间
    char *ptr = (char *)shmat(shmid, NULL, 0);
    if (ptr == (void *)-1) { perror("shmat"); return 1; }
    // ③ 读写——普通内存操作，无系统调用！
    strcpy(ptr, "hello from System V shared memory!");
    // ④ 解除映射
    shmdt(ptr);
    // ⑤ 删除共享内存对象（通常由最后一个进程做）
    // shmctl(shmid, IPC_RMID, NULL);
    return 0;
}
```

### 1.3 System V 的关键痛点

- **`key` 碰撞**：`ftok()` 用文件 inode + 项目 ID 生成 key，不同项目可能撞上同一个 key。
- **清理噩梦**：如果没人调用 `shmctl(IPC_RMID)`，共享内存段**永久存在**（即使所有进程都退出了）。用 `ipcs -m` 能看到大量"已无人使用但未删除"的段。
- **大小不可变**：`shmget` 创建时定死大小，之后不能 `ftruncate` 调整——只能删了重建。
- **独立的命名空间**：`shmid` 是一套独立于文件系统的整数 ID，调试和管理都别扭。

> 用 `ipcs -m` 和 `ipcrm -m <shmid>` 管理 System V 共享内存段。

---

## 二、POSIX 共享内存

POSIX 共享内存在设计上彻底换了一种思路：**把共享内存对象当作文件来对待**。在 `/dev/shm` 下有一个对应的文件节点，通过文件描述符来操作——这使它和 `mmap` 无缝对接。

### 2.1 核心 API

```c
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// ① 打开/创建共享内存对象（返回文件描述符）
int shm_open(const char *name, int oflag, mode_t mode);
// ② 调整大小（和普通文件一样用 ftruncate）
int ftruncate(int fd, off_t length);
// ③ 映射到地址空间（这就是 mmap 的 文件+MAP_SHARED 场景）
void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);
// ④ 解除映射
int munmap(void *addr, size_t length);
// ⑤ 删除共享内存对象名字（引用计数归零后才真正释放）
int shm_unlink(const char *name);
// ⑥ 关闭文件描述符（不影响已映射的内存）
int close(int fd);
```

### 2.2 完整使用流程

```c
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main() {
    const char *name = "/my_shared_memory";
    // ① 创建共享内存对象（O_CREAT | O_RDWR，权限 0666）
    int fd = shm_open(name, O_CREAT | O_RDWR, 0666);
    if (fd == -1) { perror("shm_open"); return 1; }
    // ② 设置大小（truncate 到 4KB）
    if (ftruncate(fd, 4096) == -1) { perror("ftruncate"); return 1; }
    // ③ 映射到本进程地址空间
    char *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                     MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) { perror("mmap"); return 1; }
    // ④ fd 已不需要，可以关了（不影响已映射的内存）
    close(fd);
    // ⑤ 读写——普通内存操作
    strcpy(ptr, "hello from POSIX shared memory!");
    // ⑥ 解除映射
    munmap(ptr, 4096);
    // ⑦ 删除名字（最后一个引用者退出后内核自动清理）
    shm_unlink(name);
    return 0;
}
```

### 2.3 POSIX 的优势

- **名字即文件路径**：`shm_open("/my_shm", ...)` 创建一个文件节点，`ls -la /dev/shm/` 直接能看到——调试特别直观。
- **引用计数自动清理**：所有进程都 `shm_unlink` + `munmap` 后，内核自动释放物理内存，不会像 System V 那样泄漏。
- **大小可变**：关闭映射后重新 `ftruncate` + `mmap` 就能调整大小。
- **与 mmap 统一**：本质就是"文件 + MAP_SHARED"这一种场景，和 `mmap.md` 里讲的四象限完全一致——**你学一套 mmap 就能同时理解文件映射和共享内存两种 IPC**。
- **fd 可以传给其他机制**：比如放进 `epoll`（虽然共享内存本身不需要事件通知，但可以配合 `eventfd` 做信号）。

---

## 三、两套接口的本质对比

```bash
               System V IPC                          POSIX
              ┌──────────┐                     ┌──────────────┐
              │   key_t  │  (整数)             │  "/name"     │  (字符串)
              │   shmid  │  (独立ID空间)        │   fd         │  (文件描述符)
              │          │                     │              │
  创建:  shmget(key,sz,flg)                shm_open()+ftruncate()
  映射:  shmat(shmid,...)                  mmap(MAP_SHARED, fd)
  解除:  shmdt(ptr)                        munmap(ptr)
  删除:  shmctl(shmid,IPC_RMID)            shm_unlink("/name")
              │          │                     │              │
              ▼          ▼                     ▼              ▼
         ┌────────────────────────────────────────────────────┐
         │              同一块物理内存（RAM）                    │
         │        被映射到不同进程的不同虚拟地址上               │
         │   进程A: ptr = 0x7f...000   进程B: ptr = 0x7f...800 │
         │              └─ 都指向同一 PFN ─┘                   │
         └────────────────────────────────────────────────────┘
```

核心理解：**两套接口只是"找到同一块物理内存的方式"不同**——System V 用整数 key（像"内存段的身份证号"），POSIX 用字符串名字（像"内存段的文件路径"）。一旦映射进地址空间（`shmat` 或 `mmap`），后续的读写就是完全一样的普通内存访问。

---

## 四、与普通文件 mmap 的区别

很多人第一次看到 POSIX 共享内存 `shm_open` 时会困惑：它和直接 `open` 一个普通文件再 `mmap(MAP_SHARED)` 有什么区别？

| 维度 | `shm_open` + mmap | `open` 普通文件 + mmap |
|------|-------------------|----------------------|
| **后备存储** | 纯内存（`tmpfs`，挂载在 `/dev/shm`） | 磁盘文件（有 page cache 层） |
| **是否落盘** | **不落盘**（重启丢失） | 脏页异步回写磁盘（持久化） |
| **性能** | 纯内存访问，无磁盘 IO | 首次读从磁盘加载，脏页被写回 |
| **物理页类型** | **匿名页**（anonymous page） | **文件页**（file-backed page） |
| **Page Cache** | 不经过 page cache | 经过 page cache |
| **用途** | 纯 IPC：进程间快速交换数据 | 持久化共享：数据库文件、配置文件 |
| **清理** | 重启/`shm_unlink` 后消失 | 文件本身在磁盘上持久存在 |

> `shm_open` 创建的共享内存对象本质是 `/dev/shm` 下 `tmpfs` 里的一个文件节点。`tmpfs` 是内存文件系统——文件内容只存在于 RAM（必要时可 swap），断电就没了。但和普通文件 mmap 不同，共享内存页是**匿名页**，不会污染 page cache，回收路径更简单。

---

## 五、使用场景与选型

### 5.1 System V 遗留场景

```bash
旧代码（1990s-2000s）：
  - 大量遗留项目 / 教科书的 "共享内存 IPC" 示例
  - Oracle 某些版本用 System V 共享内存做 SGA
  - Nginx、Apache 早期某些模块用 System V 信号量 + 共享内存
  → 新项目不推荐；但需能读得懂旧代码。
```

### 5.2 POSIX 推荐场景

```bash
需要多进程共享数据的所有场景：
  - 进程间大块数据交换（零拷贝）
  - 生产者-消费者：进程A写指针、进程B读，配合 eventfd/semaphore 通知
  - 多进程缓存：多个 worker 共享同一份只读数据
  - perf 采样环缓冲区（内核写，用户读——这不是普通文件的 shm_open，而是 perf_event_open→mmap，但同属 mmap 共享映射范畴，见 mmap.md §3.6）
  → 任何新项目需要共享内存，用 POSIX 这套。
```

### 5.3 什么时候不用共享内存？

- **只需要"通知" **：用信号/`eventfd`/管道。共享内存的优势在于"大块数据的零拷贝"，只有几个字节的状态同步别为了用而用
- **需要跨网络**：共享内存只能单机，跨机用 socket/消息队列
- **需要持久化**：用普通文件 mmap（`open` + `mmap(MAP_SHARED)`），落盘由内核保证

---

## 六、同步问题——共享内存天生不带锁

两套接口**都不提供同步机制**。共享内存本身就是一块 RAM——你的指针指向它之后，读写就是普通 `mov` 指令：

```bash
进程A:  ptr[0] = 42;          ← CPU 的一条 store 指令
进程B:  int x = ptr[0];       ← CPU 的一条 load 指令
问题：A 写到一半、B 读到了"半个值"？→ 需要用锁/原子操作保护。
```

三套配套同步方案（不是本篇重点，只点出方向）：

| 方案 | 接口 | 特点 |
|------|------|------|
| POSIX 信号量 | `sem_open`/`sem_wait`/`sem_post` | 和 POSIX shm 风格一致，都基于名字 |
| `pthread_mutex` + `PTHREAD_PROCESS_SHARED` | `pthread_mutexattr_setpshared()` | 放在共享内存里，所有进程都能锁 |
| C11 原子操作 | `atomic_load`/`atomic_store`（`<stdatomic.h>`）| 无锁场景，适合极简的生产者-消费者 |

> 如果只是单生产者+单消费者，甚至不需要锁——两个进程通过头尾指针（放到各自不重叠的区域）就能做环形缓冲区，用 `__atomic_store/load` 保证内存序。

---

## 七、观测与调试

```bash
# === POSIX 共享内存 ===
# 看所有 POSIX 共享内存对象（本质是 /dev/shm 下的文件）
ls -la /dev/shm/
#   -rw-------  1 user user 4096 Jul 14 10:00 my_shared_memory
# 看哪个进程打开了
lsof /dev/shm/my_shared_memory
# 强制删除（前提是没有进程还在用）
rm /dev/shm/my_shared_memory
# === System V IPC 共享内存 ===
# 查看所有 System V 共享内存段
ipcs -m
#   key       shmid   owner  perms  bytes  nattch  status
#   0x00000000 65538   user   644    4096   0
# 删除某个段（没有人 attach 时才能删）
ipcrm -m 65538
# 清除所有无人使用的共享内存段（危险！先确认）
# ipcrm -a
# === 看进程的映射 ===
# 两种共享内存在 /proc/$PID/maps 里的体现：
cat /proc/$$/maps
#  POSIX: 7f... rw-s ... /dev/shm/my_shared_memory  (s=shared)
#  SysV:  7f... rw-s ... [shmid=65538] (某些内核版本可能显示不同)
```

---

## 八、一句话总结

> **共享内存是最快的 IPC——数据在物理 RAM 里、被多个进程的页表 PTE 指向同一个 PFN，读写就是普通 `mov` 指令，没有内核中转。Linux 有两套接口：老旧的 System V IPC（`shmget`/`shmat`/`shmctl`，用整数 key，不手动删就永远泄漏）和现代的 POSIX（`shm_open`/`mmap`，用字符串名+fd，引用计数自动清理，本质就是把 mmap 的"文件+MAP_SHARED"场景背后的文件换成 `/dev/shm` 下的 tmpfs 节点）。新项目一律用 POSIX，但需要能读旧代码里的 System V 写法。两套接口都不带同步——它不是 bug，是让你按需选锁（POSIX semaphore / pthread mutex 共享 / 原子操作）。**

## 九、深入阅读

- **[mmap.md](/concepts/elf/mmap.md)** —— mmap 完整机制：四象限、缺页惰性映射、MAP_SHARED 的页表级共享语义
- **[memory-layout.md](/concepts/elf/memory-layout.md)** —— 共享内存映射在进程地址空间的哪个位置
- **[../process/fork-and-threads.md](/concepts/process/fork-and-threads.md)** —— fork 对共享映射的继承语义
- **[../cache/tlb.md](/concepts/cache/tlb.md)** —— 多个进程的 PTE 指向同一 PFN 的 TLB 一致性（cross-CPU shootdown）
- **[../cache/cache-organization.md](/concepts/cache/cache-organization.md)** —— 两个 CPU 核心同时写同一块共享内存时的缓存一致性（MESI 协议）

