# 图解System V共享内存:从shmget到内存映射的完整指南
在进程间通信(IPC)的众多方式中,共享内存是效率最高的一种。它允许两个或多个进程直接访问同一块内存区域,数据无需在内核和用户空间之间复制。本文将从原理到实践,完整解析System V共享内存的工作机制和使用方法。
## 一、共享内存的核心原理
共享内存的本质是让不同进程的虚拟地址空间映射到同一块物理内存。当多个进程的页表中都有指向同一物理页框的页表项时,它们就能直接读写这块内存。
与管道、消息队列相比,共享内存的优势在于:
- **零拷贝**:数据直接在内存中传递,无需内核缓冲
- **高吞吐量**:适合大数据量传输
- **低延迟**:一次内存访问即可完成通信
但代价是需要额外的同步机制(如信号量)来协调访问。
## 二、System V共享内存的四个核心步骤
使用System V共享内存通常遵循以下流程:
### 2.1 创建或获取共享内存段
```c
#include
#include
int shmget(key_t key, size_t size, int shmflg);
```
- `key`:IPC键值,可通过`ftok`生成或使用`IPC_PRIVATE`
- `size`:共享内存大小(字节),系统按页对齐(通常4KB)
- `shmflg`:权限标志,如`0666 | IPC_CREAT`
### 2.2 将共享内存附加到进程地址空间
```c
void *shmat(int shmid, const void *shmaddr, int shmflg);
```
- `shmid`:`shmget`返回的标识符
- `shmaddr`:指定附加地址,通常设为NULL让系统选择
- `shmflg`:`SHM_RDONLY`表示只读,0表示读写
- 返回值:附加后的内存起始地址
### 2.3 使用共享内存(读写操作)
附加成功后,进程可以像操作普通内存一样读写这块区域。
### 2.4 分离共享内存
```c
int shmdt(const void *shmaddr);
```
- `shmaddr`:`shmat`返回的地址
- 分离后进程不能再访问该内存,但内存段本身仍然存在
### 2.5 控制共享内存(可选)
```c
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
```
常用命令:
- `IPC_RMID`:删除共享内存段
- `IPC_STAT`:获取状态信息
- `IPC_SET`:设置属性
## 三、实战:两个进程通过共享内存交换数据
### 3.1 写进程(生产者)
```c
#include
#include
#include
#include
#include
#include
#include
#define SHM_SIZE 1024 // 共享内存大小
#define SHM_KEY 0x1234 // 约定的键值
int main() {
int shmid;
char *data;
// 创建共享内存
shmid = shmget(SHM_KEY, SHM_SIZE, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget");
exit(1);
}
<"l4.s6k3.org.cn"><"c6.s6k3.org.cn"><"y3.s6k3.org.cn">
// 附加共享内存
data = shmat(shmid, NULL, 0);
if (data == (char *)-1) {
perror("shmat");
exit(1);
}
// 写入数据
const char *message = "Hello from writer process!";
strcpy(data, message);
printf("写入: %s\n", message);
// 等待读取进程读取(简单同步,实际应用需用信号量)
sleep(2);
// 分离
if (shmdt(data) == -1) {
perror("shmdt");
exit(1);
}
return 0;
}
```
### 3.2 读进程(消费者)
```c
#include
#include
#include
#include
#include
#include
#define SHM_SIZE 1024
#define SHM_KEY 0x1234
int main() {
int shmid;
char *data;
// 获取共享内存
shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}
// 附加共享内存
data = shmat(shmid, NULL, 0);
if (data == (char *)-1) {
perror("shmat");
exit(1);
}
// 读取数据
printf("读取: %s\n", data);
// 分离
if (shmdt(data) == -1) {
perror("shmdt");
exit(1);
}
// 删除共享内存(通常由最后一个使用者执行)
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
exit(1);
}
return 0;
}
```
编译运行:
```bash
gcc writer.c -o writer
gcc reader.c -o reader
./writer & # 后台运行写入进程
./reader # 运行读取进程
```
## 四、共享内存的内部数据结构
### 4.1 内核中的shmid_ds结构
每个共享内存段在内核中对应一个`struct shmid_ds`:
```c
struct shmid_ds {
struct ipc_perm shm_perm; // 权限信息
size_t shm_segsz; // 段大小
time_t shm_atime; // 最后附加时间
time_t shm_dtime; // 最后分离时间
time_t shm_ctime; // 最后修改时间
pid_t shm_cpid; // 创建者PID
pid_t shm_lpid; // 最后操作者PID
shmatt_t shm_nattch; // 当前附加进程数
// ...
};
```
可以通过`ipcs -m`查看这些信息:
```bash
ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00001234 123456 user 666 1024 0
```
### 4.2 页表与物理内存映射
当进程调用`shmat`时,内核执行以下操作:
1. 检查权限和大小
2. 在进程的虚拟地址空间中分配一段区域
3. 建立页表项,指向共享内存的物理页框
4. 增加共享内存段的附加计数
多个进程的页表项指向相同的物理页框,实现内存共享。
## 五、共享内存的同步问题
共享内存本身不提供同步机制,多个进程同时读写会导致数据竞争。通常需要配合信号量使用。
### 5.1 结合信号量的完整示例
```c
#include
#include
#include
#include
#include
#include
#include
#include
#define SHM_SIZE 1024
#define SHM_KEY 0x1234
#define SEM_KEY 0x5678
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
<"e7.s6k3.org.cn"><"u9.s6k3.org.cn"><"b2.s6k3.org.cn">
// P操作
void sem_p(int semid) {
struct sembuf sb = {0, -1, 0};
semop(semid, &sb, 1);
}
// V操作
void sem_v(int semid) {
struct sembuf sb = {0, 1, 0};
semop(semid, &sb, 1);
}
int main() {
int shmid, semid;
char *data;
pid_t pid;
// 创建共享内存
shmid = shmget(SHM_KEY, SHM_SIZE, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget");
exit(1);
}
// 创建信号量(用于互斥)
semid = semget(SEM_KEY, 1, 0666 | IPC_CREAT);
if (semid == -1) {
perror("semget");
exit(1);
}
// 初始化信号量为1
union semun su;
su.val = 1;
if (semctl(semid, 0, SETVAL, su) == -1) {
perror("semctl");
exit(1);
}
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
exit(1);
}
if (pid == 0) { // 子进程 - 读取
data = shmat(shmid, NULL, 0);
if (data == (char *)-1) {
perror("shmat child");
exit(1);
}
sleep(1); // 等待父进程写入
sem_p(semid);
printf("子进程读取: %s\n", data);
sem_v(semid);
shmdt(data);
} else { // 父进程 - 写入
data = shmat(shmid, NULL, 0);
if (data == (char *)-1) {
perror("shmat parent");
exit(1);
}
sem_p(semid);
strcpy(data, "Hello with synchronization!");
printf("父进程写入完成\n");
sem_v(semid);
shmdt(data);
wait(NULL);
// 清理资源
shmctl(shmid, IPC_RMID, NULL);
semctl(semid, 0, IPC_RMID);
}
return 0;
}
```
## 六、常用命令与调试技巧
### 6.1 查看共享内存状态
```bash
# 查看所有IPC对象
ipcs -a
# 只查看共享内存
ipcs -m
# 查看详细信息
ipcs -m -i
```
### 6.2 删除共享内存
```bash
# 命令行删除
ipcrm -m
# 程序中删除
shmctl(shmid, IPC_RMID, NULL);
```
### 6.3 限制与优化
查看系统限制:
```bash
# 共享内存最大大小
cat /proc/sys/kernel/shmmax
# 系统最大共享内存段数
cat /proc/sys/kernel/shmmni
# 单个进程最大附加数
cat /proc/sys/kernel/shmall
```
## 七、共享内存 vs mmap
| 特性 | System V共享内存 | mmap匿名映射 |
|------|-----------------|--------------|
| 适用范围 | 任意进程 | 父子进程 |
| 持久性 | 内核持久 | 随进程 |
| 接口 | 较复杂 | 较简单 |
| 大小调整 | 创建时固定 | 可调整 |
| 同步 | 需外部同步 | 需外部同步 |
## 八、总结
System V共享内存是Linux下最高效的进程间通信方式。通过将同一块物理内存映射到多个进程的地址空间,实现了真正的零拷贝数据交换。掌握从`shmget`创建、`shmat`附加、内存读写到`shmdt`分离的完整流程,是编写高性能多进程应用的基础。
使用时需注意两点:一是必须配合信号量等机制保证同步,二是要及时清理不再使用的共享内存段,避免资源泄漏。结合`ipcs`命令的监控,可以轻松管理共享内存资源。