操作系统中的进程间通信:共享内存(Shared Memory)详解
字数 3096 2025-12-11 08:49:55
操作系统中的进程间通信:共享内存(Shared Memory)详解
1. 描述与核心概念
共享内存是一种高效的进程间通信(IPC)机制。它允许两个或多个进程共享同一块物理内存区域,从而使得一个进程对该内存区域的写入,可以被其他进程直接读取。由于数据不需要在进程和内核之间进行多次复制,共享内存通常被认为是速度最快的IPC方式。
核心特征:
- 直接内存访问:进程像访问普通内存一样读写共享区域,无需调用系统调用(建立映射后)。
- 无格式:内存区域只是一段原始的字节序列,其数据结构的含义由通信进程自行约定。
- 需要同步:因为对共享内存的访问是并发的,所以必须搭配其他同步机制(如信号量、互斥锁)来防止数据竞争和不一致。
2. 共享内存的基本原理
操作系统内核会划分出一块内存区域,该区域可以被映射到多个进程各自的虚拟地址空间中,但所有这些虚拟地址最终都指向同一块物理内存页。这就建立了进程间共享物理内存的桥梁。
关键步骤抽象:
- 创建/获取:一个进程请求内核创建一块新的共享内存区域,或获取一个已存在区域的标识符。
- 附加(映射):进程将这块共享内存区域“附加”到自己的地址空间,即在自己的虚拟地址空间中分配一个区间,并让页表将该区间映射到共享的物理页上。
- 访问:进程通过映射到本地的虚拟地址指针,直接读写共享内存。
- 分离(解除映射):当进程不再需要时,它可以解除这个映射关系,这个操作只影响本进程的地址空间,不影响共享内存和其他进程。
- 销毁:当所有进程都分离后,某个进程可以通知内核销毁这块共享内存区域,释放物理内存。
3. 在Unix/Linux(System V IPC)中的详细实现步骤与API
(1) 创建或获取共享内存段:shmget()
int shmget(key_t key, size_t size, int shmflg);
key:一个唯一的键值,用于标识共享内存段。可以使用IPC_PRIVATE让内核生成新键值,或使用ftok()函数基于路径名生成。size:要创建或获取的共享内存段的大小(字节)。如果是获取已存在的段,此参数通常被忽略,但必须小于等于段的大小。shmflg:标志位,组合以下值:IPC_CREAT:如果键值对应的段不存在,则创建它。IPC_EXCL:与IPC_CREAT一起使用,如果段已存在,则调用失败。这确保了创建者身份。- 权限位:如
0666,控制该共享内存段的读写权限(同文件权限)。
- 返回值:成功时返回共享内存段的标识符(
shmid),这是一个内核用于管理该段的句柄,后续操作都基于此shmid。失败返回-1。
(2) 将共享内存附加到进程地址空间:shmat()
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid:由shmget()返回的标识符。shmaddr:建议的附加地址。通常设为NULL,让内核自动选择一个合适的、未使用的虚拟地址。shmflg:SHM_RDONLY:以只读方式附加。默认(0)为可读写。SHM_RND:当shmaddr非空时,表示地址是近似值,内核会选择最接近的页面边界地址。
- 返回值:成功时返回附加后的起始虚拟地址指针,进程通过此指针访问共享内存。失败返回
(void*) -1。
(3) 从进程地址空间分离共享内存:shmdt()
int shmdt(const void *shmaddr);
shmaddr:之前由shmat()返回的地址指针。- 调用后,该进程的虚拟地址
shmaddr将不再有效,对它的访问会引发段错误。但这块共享内存本身依然存在,其他附加了它的进程仍可正常使用。
(4) 控制共享内存段(获取信息、设置属性、删除):shmctl()
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:共享内存标识符。cmd:控制命令,最重要的是:IPC_RMID:标记删除。内核不会立即删除该段,而是将其标记为“待销毁”。只有当所有附加到它的进程都调用了shmdt()分离后,内核才会真正销毁它并回收物理内存。这是一个“延迟销毁”机制。IPC_STAT:获取该共享内存段的状态信息(如创建者、权限、大小、附加进程数等),存入buf指向的结构体。IPC_SET:设置该段的某些属性(如权限)。
buf:指向struct shmid_ds结构体的指针,用于传入或传出信息。
4. 一个典型的使用流程示例(生产者-消费者模型)
假设有两个进程:生产者(Producer) 和消费者(Consumer)。
步骤1:创建/获取共享内存和信号量(通常用信号量同步)
- 双方进程通过同一个
key调用shmget(..., IPC_CREAT, ...)。先运行的进程会创建它,后运行的进程会获取到同一个shmid。 - 同时,它们也需要创建一个或一组信号量(例如,一个用于互斥,两个用于计数),用于协调对共享缓冲区的访问。
步骤2:附加共享内存
- 双方进程各自调用
shmat(shmid, NULL, 0),获得一个指向同一块物理内存的本地指针shared_mem。
步骤3:定义共享数据结构(进程间需预先约定)
- 例如,在C语言中,双方可以约定共享内存的前
sizeof(struct shared_data)字节是一个结构体:struct shared_data { int data[10]; // 环形缓冲区 int in; // 生产者放入位置 int out; // 消费者取出位置 }; - 在生产者进程中:
struct shared_data *shm = (struct shared_data*) shared_mem; - 在消费者进程中做同样的类型转换。这样,双方就可以通过
shm->data,shm->in,shm->out来访问共享变量。
步骤4:通过同步机制访问共享数据
- 生产者在写入
shm->data[shm->in]前,需要等待“空槽”信号量,然后获取互斥锁,写入数据,更新shm->in,释放互斥锁,并增加“满槽”信号量。 - 消费者反之,等待“满槽”信号量,获取互斥锁,读取数据,更新
shm->out,释放互斥锁,并增加“空槽”信号量。
步骤5:清理
- 通信结束后,双方都调用
shmdt(shm)分离内存。 - 最后一个结束的进程(通常是创建者)调用
shmctl(shmid, IPC_RMID, NULL)来标记删除共享内存段。当所有进程都分离后,内核会将其彻底销毁。
5. 共享内存的优缺点
优点:
- 速度极快:数据只需一次拷贝到共享内存,之后所有进程直接内存访问,无需内核干预(除了同步操作)。
- 通信量大:适合传输大量数据。
缺点:
- 同步复杂:必须由程序员显式使用其他IPC机制(信号量、锁等)来同步访问,否则极易产生竞态条件。
- 安全性:一个进程的错误(如越界写)会直接破坏其他进程的数据,甚至导致崩溃。
- 内核持久性:共享内存段(System V风格)会一直存在内核中,直到被显式删除或系统重启,如果程序异常结束未清理,可能造成“内存泄漏”,需用
ipcrm命令手动清理。
6. 其他实现
- POSIX共享内存:使用类似文件操作的API(
shm_open,mmap,shm_unlink),更符合现代Unix编程风格,与内存映射文件结合更紧密。 - 内存映射文件:通过
mmap()将同一个文件映射到多个进程的地址空间,也是一种共享内存,且有文件作为持久化后端。
总结:共享内存是性能最高的IPC方式,其本质是让多个进程的页表项指向相同的物理页帧。其使用关键在于正确的创建/映射流程和必不可少的同步机制。理解共享内存,对于深入理解进程地址空间的隔离性与共享性、虚拟内存管理以及高性能并发程序设计都至关重要。