大家好,我是程序员小青蛙,今天介绍共享内存。
在 Linux 操作系统中,进程间通信 (IPC) 是一个至关重要的概念,它允许不同的进程之间进行数据交换和同步。在众多 IPC 方式中,System V 共享内存以其极致的性能脱颖而出,被誉为最快的进程间通信方式。本文将从原理、API、实战案例到优缺点,带你全面掌握 System V 共享内存。
一、共享内存:为什么是最快的 IPC?
1.1 核心原理
共享内存的本质是:让多个进程的虚拟地址空间映射到同一块物理内存区域。
当进程 A 和进程 B 都将同一块物理内存映射到自己的虚拟地址空间后,它们对这块内存的读写操作就直接作用于物理内存,完全不需要经过内核的中转。
对比传统的管道、消息队列等 IPC 方式:
- 管道 / 消息队列:需要经历用户态缓冲区 → 内核缓冲区 → 用户态缓冲区两次数据拷贝
- 共享内存:直接操作物理内存,仅需一次拷贝(数据写入物理内存)
这就是共享内存速度远超其他 IPC 方式的根本原因。
1.2 内存映射示意图
在 Linux 进程的虚拟地址空间布局中,共享内存通常被映射到栈和堆之间的区域(大约 0x40000000 附近)。
1.3 关键特性:生命周期随内核
这是 System V IPC(共享内存、消息队列、信号量)最重要的特性之一:
- 共享内存的生命周期不随创建它的进程结束而结束
- 它会一直存在于内核中,直到:
- 手动调用
shmctl(IPC_RMID)删除 - 系统重启
- 手动调用
- 这意味着:如果程序异常退出而没有删除共享内存,会造成内核资源泄漏
二、核心数据结构与 API 详解
2.1 共享内存描述符:struct shmid_ds
内核为每个共享内存段维护一个shmid_ds结构体,记录其所有属性:
struct shmid_ds { struct ipc_perm shm_perm; /* 操作权限 */ int shm_segsz; /* 共享内存段大小(字节) */ __kernel_time_t shm_atime; /* 最后一次挂载时间 */ __kernel_time_t shm_dtime; /* 最后一次解除挂载时间 */ __kernel_time_t shm_ctime; /* 最后一次修改时间 */ __kernel_ipc_pid_t shm_cpid; /* 创建者PID */ __kernel_ipc_pid_t shm_lpid; /* 最后一次操作PID */ unsigned short shm_nattch; /* 当前挂载的进程数 */ unsigned short shm_unused; /* 兼容保留 */ void *shm_unused2; /* DIP使用 */ void *shm_unused3; /* 未使用 */ };其中shm_nattch字段非常重要,它记录了当前有多少个进程挂载了这个共享内存段。当我们调用IPC_RMID删除共享内存时,内核会先将其标记为删除状态,直到shm_nattch变为 0 时,才会真正释放物理内存。
2.2 四大核心函数
1.shmget:创建或获取共享内存
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg);- 功能:创建一个新的共享内存段,或获取一个已存在的共享内存段
- 参数:
key:共享内存的唯一标识符,通常由ftok()生成size:共享内存段的大小(字节),创建时必须指定,获取时可设为 0shmflg:标志位,用法与文件权限类似IPC_CREAT:如果不存在则创建,存在则获取IPC_EXCL:与IPC_CREAT一起使用,如果已存在则返回错误(保证创建的是新的)- 权限位:如
0666,表示所有用户都有读写权限
- 返回值:成功返回共享内存标识符
shmid,失败返回 - 1
生成 key 的常用方法:
key_t key = ftok(".", 0x6666); // 使用当前目录和项目ID生成唯一key2.shmat:将共享内存挂载到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);- 功能:将
shmid标识的共享内存段映射到调用进程的虚拟地址空间 - 参数:
shmid:由shmget返回的共享内存标识符shmaddr:指定映射的虚拟地址,建议设为 NULL,让内核自动选择合适的地址shmflg:挂载标志SHM_RDONLY:以只读方式挂载0:以读写方式挂载
- 返回值:成功返回指向共享内存首地址的指针,失败返回
(void *)-1
3.shmdt:解除共享内存挂载
int shmdt(const void *shmaddr);- 功能:将共享内存段从当前进程的虚拟地址空间中分离
- 参数:
shmaddr是shmat返回的指针 - 返回值:成功返回 0,失败返回 - 1
- 注意:这只是解除挂载,并不会删除共享内存段
4.shmctl:控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);- 功能:对共享内存段进行各种控制操作
- 参数:
shmid:共享内存标识符cmd:要执行的命令IPC_STAT:获取共享内存的状态,填充到buf中IPC_SET:设置共享内存的属性(需要足够权限)IPC_RMID:删除共享内存段(标记删除,等待所有进程解除挂载后释放)
buf:用于存储或设置共享内存属性的结构体指针
- 返回值:成功返回 0,失败返回 - 1
三、实战案例:服务端 - 客户端通信
我们将实现一个简单的 C/S 架构程序:客户端向共享内存写入 A-Z 的字母,服务端从共享内存中读取并打印。
3.1 代码结构
. ├── comm.h // 公共头文件 ├── comm.c // 公共函数实现 ├── server.c // 服务端代码 ├── client.c // 客户端代码 └── Makefile // 编译脚本3.2 公共头文件comm.h
#ifndef COMM_H #define COMM_H #include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #define PATHNAME "." #define PROJ_ID 0x6666 // 创建共享内存 int createShm(int size); // 获取已存在的共享内存 int getShm(int size); // 删除共享内存 int destroyShm(int shmid); #endif3.3 公共函数实现comm.c
#include "comm.h" // 通用的共享内存获取/创建函数 static int commShm(int size, int flags) { key_t key = ftok(PATHNAME, PROJ_ID); if (key < 0) { perror("ftok"); return -1; } int shmid = shmget(key, size, flags); if (shmid < 0) { perror("shmget"); return -1; } return shmid; } int createShm(int size) { // IPC_CREAT|IPC_EXCL:确保创建一个全新的共享内存 return commShm(size, IPC_CREAT | IPC_EXCL | 0666); } int getShm(int size) { // 只获取已存在的共享内存 return commShm(size, IPC_CREAT); } int destroyShm(int shmid) { if (shmctl(shmid, IPC_RMID, NULL) < 0) { perror("shmctl"); return -1; } return 0; }3.4 服务端代码server.c
#include "comm.h" #include <unistd.h> int main() { // 创建4096字节的共享内存 int shmid = createShm(4096); printf("服务端创建共享内存成功,shmid: %d\n", shmid); // 挂载共享内存 char *addr = shmat(shmid, NULL, 0); if (addr == (void *)-1) { perror("shmat"); return 1; } // 循环读取共享内存中的数据 int i = 0; while (i < 26) { printf("Client# %s\n", addr); sleep(1); i++; } // 解除挂载 shmdt(addr); printf("服务端解除挂载\n"); // 删除共享内存 sleep(2); destroyShm(shmid); printf("服务端删除共享内存\n"); return 0; }3.5 客户端代码client.c
#include "comm.h" #include <unistd.h> #include <string.h> int main() { // 获取服务端创建的共享内存 int shmid = getShm(4096); printf("客户端获取共享内存成功,shmid: %d\n", shmid); // 挂载共享内存 char *addr = shmat(shmid, NULL, 0); if (addr == (void *)-1) { perror("shmat"); return 1; } // 向共享内存写入A-Z int i = 0; while (i < 26) { addr[i] = 'A' + i; addr[i+1] = '\0'; sleep(1); i++; } // 解除挂载 shmdt(addr); printf("客户端解除挂载\n"); sleep(2); return 0; }3.6 编译与运行
- 编写
Makefile:
.PHONY: all clean all: server client server: server.c comm.c gcc -o $@ $^ client: client.c comm.c gcc -o $@ $^ clean: rm -f server client- 编译运行:
# 编译 make # 先启动服务端 ./server # 再打开另一个终端启动客户端 ./client3.7 运行结果
服务端会依次打印:
Client# A Client# AB Client# ABC ... Client# ABCDEFGHIJKLMNOPQRSTUVWXYZ3.8 常见问题与解决
问题:第二次运行服务端时,提示shmget: File exists原因:上一次运行的服务端异常退出,没有删除共享内存,导致共享内存仍然存在于内核中解决:
- 查看系统中的共享内存:
ipcs -m - 删除指定的共享内存:
ipcrm -m <shmid>
四、共享内存的优缺点分析
4.1 优点
- 速度最快:所有 IPC 方式中性能最高,适合大数据量传输
- 灵活性高:可以直接通过指针操作内存,支持任意数据结构
- 无数据拷贝:避免了内核态和用户态之间的多次数据拷贝
4.2 缺点
- 没有同步互斥机制:这是共享内存最大的缺点。多个进程同时读写共享内存时,会出现数据不一致的问题。必须配合信号量、互斥锁等同步机制使用
- 生命周期随内核:如果程序忘记删除共享内存,会造成内核资源泄漏
- 没有数据保护:任何挂载了共享内存的进程都可以随意修改数据,安全性较低
五、总结与扩展
System V 共享内存是 Linux 下性能最优的进程间通信方式,特别适合需要频繁、大量传输数据的场景。但它的缺点也很明显 —— 缺乏同步机制,需要开发者自己实现进程间的同步与互斥。
在实际开发中,我们通常会将共享内存 + 信号量结合使用:用共享内存传输数据,用信号量保证数据的一致性。