1.shm 共享内存
共享内存(Shared Memory,简称 SHM)是进程间通信(IPC)的一种机制,它允许多个进程访问同一块物理内存区域,从而实现高效的数据共享。不同于管道或消息队列,共享内存不涉及内核的复制开销,而是直接映射到用户空间地址,因此性能最高,但需要进程自行处理同步和互斥问题。
本文将详细解释 System V 风格的共享内存(SHM),这是 Unix/Linux 系统中最常见的实现方式。POSIX 也有共享内存 API(如 shm_open 和 mmap),但 System V SHM 更传统且广泛使用。我们将从概念、API、示例、优缺点等方面展开。
1. 共享内存的基本概念
- 原理:操作系统为共享内存分配一块连续的物理内存页,并将其映射到多个进程的虚拟地址空间中。进程通过系统调用获取这段内存的句柄(ID),然后附加(attach)到自己的地址空间进行读写。
- 关键元素:
- 键值(Key):一个整数(通常用 ftok 生成),用于标识共享内存段。多个进程使用相同键值才能访问同一段内存。
- 大小(Size):共享内存段的字节大小,必须是页面大小的整数倍(通常 4KB)。
- 权限(Permissions):类似文件权限,包括读(R)、写(W)、创建(C)等标志。
- ID:成功创建后返回的唯一标识符(int shmid),用于后续操作。
- 生命周期:共享内存段在创建后存在于内核,直到显式删除或系统重启。进程附加后,可以随时分离,但段本身不会自动销毁。
共享内存适用于高性能场景,如多进程数据库、实时系统,但需配合信号量(semaphore)或锁机制避免竞态条件。
2. 核心 API 函数
System V SHM 主要由四个函数组成:shmget(创建/获取)、shmat(附加)、shmdt(分离)、shmctl(控制)。这些函数定义在 <sys/shm.h> 头文件中。
2.1 shmget - 创建或获取共享内存段
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
2.2 shmat - 附加共享内存到进程地址空间
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 参数:
- key:键值。常用 IPC_PRIVATE(0)表示私有段,或用 ftok 生成(如 ftok("/tmp", 'a'))。
- size:段大小(字节)。若 key 已存在,则忽略此参数。
- shmflg:标志位,包括:
- IPC_CREAT:如果键不存在,则创建新段。
- IPC_EXCL:与 IPC_CREAT 结合使用,若已存在则失败。
- 权限位:如 0666(读写权限)。
- 返回值:成功返回 shmid(≥0),失败返回 -1(errno 如 ENOSPC 表示空间不足)。
- 示例:创建 1024 字节段:int shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666);
- 参数:
- shmid:由 shmget 返回的 ID。
- shmaddr:附加地址。NULL 表示让内核自动选择。
- shmflg:标志,如 SHM_RDONLY(只读附加)。
- 返回值:成功返回附加后的指针(void*),失败返回 (void*)-1。
- 注意:附加后,指针指向共享内存,可像普通数组一样读写。多个进程附加同一段时,修改会立即可见。
2.3 shmdt - 从进程地址空间分离共享内存
#include <sys/shm.h>
int shmdt(const void *shmaddr);
- 参数:shmaddr:由 shmat 返回的指针。
- 返回值:成功返回 0,失败返回 -1。
- 注意:分离不删除段本身,仅断开当前进程的映射。所有进程分离后,段仍存在直到 shmctl 删除。
2.4 shmctl - 控制共享内存段
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 参数:
- shmid:共享内存 ID。
- cmd:命令,如:
- IPC_STAT:获取段信息到 buf。
- IPC_SET:设置段权限/所有者。
- IPC_RMID:标记段删除(最后分离时真正销毁)。
- buf:struct shmid_ds 结构体,包含段统计信息(如大小、附加进程数)。
- 返回值:成功 0,失败 -1。
- 示例:删除段:shmctl(shmid, IPC_RMID, NULL);
3. 完整示例:父子进程共享内存通信
以下是一个 C 语言示例:父进程创建共享内存,写入数据;子进程附加、读取、修改后分离。编译运行:gcc example.c -o example && ./example。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/wait.h>
int main() {
int shmid;
char *shm_ptr;
pid_t pid;
// 创建共享内存:1024 字节
shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}
// 父进程附加并写入数据
shm_ptr = shmat(shmid, NULL, 0);
if (shm_ptr == (char*)-1) {
perror("shmat");
exit(1);
}
sprintf(shm_ptr, "Hello from parent!");
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
exit(1);
}
if (pid == 0) { // 子进程
// 子进程附加(自动获取同一段)
char *child_ptr = shmat(shmid, NULL, 0);
if (child_ptr == (char*)-1) {
perror("shmat child");
exit(1);
}
printf("Child reads: %s\n", child_ptr);
sprintf(child_ptr, "Modified by child!");
shmdt(child_ptr); // 子进程分离
exit(0);
} else { // 父进程
wait(NULL); // 等待子进程
printf("Parent reads after child: %s\n", shm_ptr);
shmdt(shm_ptr); // 父进程分离
shmctl(shmid, IPC_RMID, NULL); // 删除段
}
return 0;
}
运行输出:
text
Child reads: Hello from parent!
Parent reads after child: Modified by child!
- 解释:父进程创建段并写入,子进程读取并修改,父进程最后读取修改结果。使用 IPC_PRIVATE 确保私有段。
. 高级主题
- 键值生成:用 ftok(<sys/ipc.h>)基于路径和 proj_id 生成唯一键:key_t key = ftok("/tmp/shmfile", 65);。路径需存在。
- 权限与安全:权限位类似 open 系统调用。段所有者默认为创建进程 UID。跨用户访问需设置 IPC_SET。
- 限制与查询:用 shmctl 的 IPC_STAT 查看段信息。系统限制(如最大段数)在 /proc/sys/kernel/shm* 可调。
- 同步问题:SHM 无内置锁,需结合信号量(semget)或互斥锁(pthread_mutex)。示例:用信号量保护读写。
- 错误处理:常见 errno:
- EINVAL:无效参数(如 size=0)。
- EACCES:权限不足。
- ENOENT:键不存在且无 IPC_CREAT。
-
Unix/Linux 文件权限的数字表示详解
在 Unix/Linux 系统(如 Linux、macOS)中,文件和目录的权限常用八进制数字表示(通常是 3 位数字,如 644)。这个数字表示**所有者(owner)、组(group)和其他人(others)各自的权限组合。每个组的权限由读(r)、写(w)、执行(x)**三项相加计算:
- 读(r) = 4
- 写(w) = 2
- 执行(x) = 1
- 无权限 = 0
常见文件读写权限的数字表示
以下是文件(非目录)常见的读写权限组合。注意:文件默认无执行权限,除非是脚本/可执行文件。
| 权限描述(符号表示) | 数字表示 | 解释(所有者/组/其他) | 典型用途 |
|---|---|---|---|
| rw-r--r-- | 644 | rw- / r-- / r-- | 普通文件:所有者读写,其他只读(默认文件权限) |
| rw-rw-r-- | 664 | rw- / rw- / r-- | 组内协作文件:所有者和组读写,其他只读 |
| rw-rw-rw- | 666 | rw- / rw- / rw- | 所有人读写(共享内存 SHM 常用) |
| rw------- | 600 | rw- / --- / --- | 私有文件:仅所有者读写 |
| r--r--r-- | 444 | r-- / r-- / r-- | 只读文件:所有人只读,无写 |
mmap 进程间通信详解
mmap(memory map)是 POSIX 标准的一种系统调用,用于将文件或其他对象映射到进程的虚拟地址空间中,从而实现高效的进程间通信(IPC)。它常用于共享内存场景,与 System V SHM 类似,但更灵活、可基于文件持久化数据。mmap 不限于文件,还支持匿名映射(anonymous mapping),适用于纯内存共享。
相比 SHM,mmap 更现代、跨平台(Linux/Unix/macOS),且易于与文件 I/O 结合。但它也需手动处理同步(如使用 msync 同步到磁盘)。本文聚焦进程间通信用法,从概念、API、示例、优缺点等方面详解。
1. mmap 的基本概念
- 原理:mmap 将文件(或匿名内存)映射到进程的地址空间,形成共享视图。多个进程映射同一文件时,修改会立即可见(若使用 MAP_SHARED 标志)。内核通过页表管理物理页的共享,避免数据复制。
- 关键元素:
- 文件描述符(fd):通过 open 获取文件句柄。若匿名映射,fd = -1。
- 映射大小(length):映射的字节数,通常为文件大小(用 fstat 获取)。
- 标志(flags):
- MAP_SHARED:修改反映到所有进程和底层文件(IPC 核心)。
- MAP_PRIVATE:私有复制(写时复制,COW),不共享。
- MAP_ANONYMOUS:匿名映射,无底层文件,纯内存共享。
- 偏移(offset):文件偏移,从何处开始映射(通常 0)。
- 保护(prot):如 PROT_READ | PROT_WRITE(读写)。
- 生命周期:映射在 munmap 或进程退出时解除。文件需手动关闭,但映射独立。
- 适用场景:多进程共享大文件(如数据库页)、实时数据交换。需配合锁(如 flock 或信号量)避免竞态。
匿名 mmap 是 SHM 的现代替代:无需键值,直接 fork 后子进程继承映射。
2. 核心 API 函数
mmap 系列定义在 <sys/mman.h> 头文件中。主要函数:open(可选)、mmap(映射)、munmap(解除)、msync(同步)、close(关闭文件)。
2.1 open - 打开文件(文件映射时用)
c
#include <fcntl.h>
#include <sys/stat.h>
int open(const char *pathname, int flags, mode_t mode);
- 参数:pathname 文件路径;flags 如 O_RDWR | O_CREAT;mode 如 0666。
- 返回值:文件描述符(fd ≥0),失败 -1。
- 注意:匿名映射无需此步。
2.2 mmap - 映射内存
c
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
- 参数:
- addr:建议地址(NULL 让内核选择)。
- length:映射长度(字节)。
- prot:保护,如 PROT_READ | PROT_WRITE。
- flags:如 MAP_SHARED | MAP_ANONYMOUS。
- fd:文件 fd(匿名时 -1)。
- offset:偏移(页对齐)。
- 返回值:成功返回映射指针(void*),失败 (void*)-1(errno 如 ENOMEM)。
- 示例:匿名 1024 字节映射:void *ptr = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
2.3 munmap - 解除映射
c
#include <sys/mman.h>
int munmap(void *addr, size_t length);
- 参数:addr 和 length 与 mmap 对应。
- 返回值:成功 0,失败 -1。
- 注意:进程退出自动 munmap,但显式调用更安全。
2.4 msync - 同步映射到文件(可选)
c
#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);
- 参数:flags 如 MS_SYNC(阻塞同步)或 MS_ASYNC(异步)。
- 用途:确保修改写入磁盘,防止数据丢失。
3. 完整示例:父子进程使用 mmap 通信
以下 C 示例:父进程创建匿名 mmap 区域,写入数据;fork 子进程,子进程读取、修改;父进程最后读取。编译:gcc example.c -o example && ./example。
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <string.h>
int main() {
size_t size = 1024;
void *shm_ptr;
// 创建匿名共享映射:1024 字节
shm_ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (shm_ptr == MAP_FAILED) {
perror("mmap");
exit(1);
}
// 父进程写入数据
strcpy((char *)shm_ptr, "Hello from parent!");
// 创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(1);
}
if (pid == 0) { // 子进程
// 子进程直接访问同一映射(继承自父进程)
printf("Child reads: %s\n", (char *)shm_ptr);
strcpy((char *)shm_ptr, "Modified by child!");
munmap(shm_ptr, size); // 子进程解除
exit(0);
} else { // 父进程
wait(NULL); // 等待子进程
printf("Parent reads after child: %s\n", (char *)shm_ptr);
munmap(shm_ptr, size); // 父进程解除
}
return 0;
}
运行输出:
text
Child reads: Hello from parent!
Parent reads after child: Modified by child!
- 解释:fork 后,子进程继承父进程的 mmap 视图,实现零复制共享。匿名标志避免文件 I/O。
4. 文件映射示例(持久化)
若需持久化,使用文件:
c
int fd = open("shared.dat", O_RDWR | O_CREAT | O_TRUNC, 0666);
ftruncate(fd, 1024); // 设置文件大小
void *ptr = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
// ... 使用后
msync(ptr, 1024, MS_SYNC);
munmap(ptr, 1024);
close(fd);
多个进程打开同一文件,即可共享。
5. 高级主题
- 匿名 vs 文件映射:匿名(MAP_ANONYMOUS)适合临时共享;文件映射支持持久化和大文件(> 虚拟地址限)。
- 同步:用 pthread_mutex(初始化在共享内存)或文件锁(fcntl)。示例:mutex 在 mmap 首部。
- 页对齐:length/offset 需页对齐(getpagesize() 获取,通常 4KB)。
- 错误处理:常见 errno:EACCES(权限)、ENOMEM(内存不足)、EINVAL(无效参数)。
- 限制:虚拟地址空间限(ulimit -v);大映射需考虑 TLB 缓存。
6. 优缺点
| 方面 | 优点 | 缺点 |
|---|---|---|
| 性能 | 零复制、懒加载(按需页入),高效 | 需手动同步,易页错误(SIGSEGV) |
| 使用 | POSIX 标准,易与文件结合 | 地址不固定(ASLR),调试难 |
| 适用 | 跨进程/线程、大文件共享 | 不支持跨机器,文件版需磁盘 I/O |
| 兼容 | Linux/Unix/macOS/Windows(模拟) | 老系统可能不支持匿名共享 |
7. 与 SHM 的比较
| 方面 | mmap | System V SHM |
|---|---|---|
| 创建 | mmap(匿名或文件) | shmget(键值) |
| 持久化 | 支持文件映射 | 纯内存,无持久 |
| API 复杂 | 简单,集成文件 I/O | 需额外 shmat/shmdt |
| 移植性 | 高(POSIX) | Unix 特定 |
| 同步 | 手动(msync/锁) | 手动(信号量) |
| 推荐 | 现代应用、文件共享 | 传统高性能 IPC |
mmap 是 SHM 的升级版,尤其适合文件-backed 场景。如果需要线程内通信(pthread)、POSIX SHM(shm_open)示例,或特定同步代码,欢迎补充细节!参考:man mmap。
mmap 创建的文件路径及与 SHM 的区别详解
针对你的问题,我将分步解释 mmap 创建的“文件”路径(其实是底层文件描述符),以及为什么 System V SHM 不是基于文件的通信机制。最后用表格总结二者区别。基于 POSIX/Linux 标准,这些概念在用户空间实现中通用。
1. mmap 创建的文件在哪个路径下?
- 文件映射(File-backed mmap):
- mmap 本身不“创建”文件,而是将现有文件映射到内存。如果你想通过 mmap 实现共享,首先需用 open() 创建或打开一个文件作为底层存储。
- 路径:文件路径完全由 open() 的 pathname 参数决定,通常是用户指定的任意路径。例如:
- /tmp/shared.dat:临时文件,常用于测试。
- /var/run/myapp/shm_file:持久化目录。
- 当前工作目录:如 ./data.bin。
- 示例代码:
c
int fd = open("/tmp/mmap_shared.dat", O_RDWR | O_CREAT | O_TRUNC, 0666); // 路径:/tmp/mmap_shared.dat ftruncate(fd, 1024); // 设置文件大小 void *ptr = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);- 这里,文件实际创建在 /tmp/ 目录下(需有写权限)。进程退出后,文件仍存在于磁盘,除非手动 unlink(fd) 删除。
- 注意:文件大小需预设(ftruncate),否则映射失败。修改映射内存会同步到文件(用 msync 强制)。
- 匿名映射(Anonymous mmap):
- 使用 MAP_ANONYMOUS 标志时,无底层文件(fd = -1),纯内存区域,由内核分配虚拟页。
- 路径:无路径!它不依赖文件系统,直接在进程地址空间中创建共享内存。fork 后的子进程继承视图,实现 IPC。
- 示例:
c
void *ptr = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); // 无文件路径 - 适用:临时共享,无需持久化。
- 查询路径:用 ls /tmp/ 或 lsof -p <PID> 查看打开的文件描述符。匿名无文件可见。
2. SHM 不也是基于文件通信的吗?
- 不是!System V SHM(shmget 等)是纯内核内存管理,不涉及文件系统:
- SHM 段是内核 IPC 资源,通过**键值(key)**标识(如 ftok 生成),存储在内核的共享内存表中(用 ipcs -m 查看)。
- 无物理文件:数据仅在 RAM 中,进程附加(shmat)后直接访问。进程退出或 shmctl(IPC_RMID) 后,段被释放(可能延迟直到最后附加进程分离)。
- “基于文件”的误解:有时用 ftok("/path/to/file", proj_id) 生成键值,但文件仅用于生成唯一 ID(路径需存在,但文件内容无关)。实际 SHM 不读写该文件。
- 示例:key_t key = ftok("/tmp/keyfile", 65); int shmid = shmget(key, 1024, IPC_CREAT | 0666); —— /tmp/keyfile 只用于键生成,无通信作用。
3. mmap 与 SHM 的区别
二者都用于进程间共享内存,但 mmap 更灵活(文件/匿名),SHM 更传统(纯 IPC)。以下表格对比:
| 方面 | mmap (文件映射) | mmap (匿名) | System V SHM |
|---|---|---|---|
| 底层存储 | 基于磁盘文件(持久化) | 纯内存(无文件) | 纯内核内存(无文件) |
| 路径/标识 | open() 的 pathname(如 /tmp/xx) | 无路径(fd=-1) | 键值(key,如 ftok 生成) |
| 创建方式 | open + ftruncate + mmap | mmap(MAP_ANONYMOUS) | shmget(key, size, flags) |
| 持久化 | 是(文件存盘,msync 同步) | 否(进程退出丢失) | 否(内核段,系统重启丢失) |
| IPC 实现 | 多个进程 open 同一文件 + MAP_SHARED | fork 继承,或多进程 mmap 同一匿名 | 多进程用同一 key + shmat |
| 大小限制 | 文件系统限(大文件 OK) | 虚拟内存限 | 系统 SHMMAX/SHMALL(/proc/sys/kernel/shm*) |
| 清理 | unlink(fd) 删除文件;munmap | munmap(自动) | shmctl(IPC_RMID);shmdt |
| 优点 | 持久、易备份、跨重启 | 简单、高效、无文件开销 | 传统、键值命名、易多进程访问 |
| 缺点 | 磁盘 I/O 慢、需文件权限 | 无持久、地址不固定 | 无持久、需手动删段(易泄漏) |
| 适用 | 数据库页、配置文件共享 | 临时多进程数据交换 | 高性能 IPC(如消息队列后端) |
- 总结区别:mmap 是“内存映射”通用工具,可模拟文件 I/O 或纯 SHM;SHM 是专为 IPC 设计的内核抽象,无文件介入。匿名 mmap 几乎等价于 SHM,但更 POSIX 兼容(无键值)。在现代代码中,优先 mmap(尤其是匿名),除非遗留系统需 SHM。
如果需要代码示例(如 mmap 文件版 vs SHM)、特定平台(如 ARM)细节,或调试工具(如 ipcs 输出),随时补充!参考:man mmap、man shm_overview。
1万+

被折叠的 条评论
为什么被折叠?



