2. 共享内存基础:System V共享内存与POSIX共享内存对比、shmget/shmat/shmdt接口详解
共享内存,说白了就是让两个或多个进程直接读写同一块物理内存。这是IPC(进程间通信)里速度最快的方式——没有中间商赚差价,数据不用在内核和用户态之间来回拷贝。
我刚开始做多进程开发时,总觉得消息队列和管道就够用了。直到有一次做一个实时行情处理系统,数据量一上来,消息队列的拷贝开销直接让CPU飙到80%。嗯,从那以后,我对共享内存就「真香」了。
2.1 System V 共享内存 vs POSIX 共享内存
Linux下有两套共享内存的API:一套是古老的System V,一套是较新的POSIX。我个人习惯,新项目用POSIX,维护老代码时经常碰到System V。
| 对比维度 | System V 共享内存 | POSIX 共享内存 |
|---|---|---|
| 接口风格 | shmget / shmat / shmdt | shm_open / mmap / munmap |
| 标识方式 | 整数 key(ftok生成) | 文件名(/dev/shm下的文件) |
| 生命周期 | 内核持久化,直到显式删除 | 文件持久化,进程退出后仍存在 |
| 大小限制 | SHMMAX 系统参数控制 | 受文件系统大小限制 |
| 可移植性 | 所有Unix都支持 | Linux/macOS支持,Windows有限 |
核心区别一句话:System V用整数key找共享内存,POSIX用文件名找。POSIX的接口更接近文件操作,我个人觉得更自然一些。
2.2 shmget —— 创建或获取共享内存
shmget是System V共享内存的入口函数。你想想看,进程之间要共享一块内存,总得先有个「门牌号」吧?shmget就是干这个的。
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
// 返回值:成功返回共享内存ID,失败返回-1
参数详解:
- key:共享内存的标识符。通常用ftok()生成,也可以传IPC_PRIVATE(创建私有共享内存)。
- size:共享内存的大小(字节)。创建时必须指定,获取时传0即可。
- shmflg:权限标志。IPC_CREAT表示创建,IPC_EXCL表示排他创建,后面跟八进制权限(如0666)。
我的习惯:创建共享内存时,我一般用 IPC_CREAT | 0666。0666表示所有用户可读写,省得后面权限出问题。生产环境记得收紧权限。
常见用法:
// 创建一块1024字节的共享内存
key_t key = ftok("/tmp/shmfile", 'A');
int shmid = shmget(key, 1024, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget failed");
exit(1);
}
我曾经踩过的坑:ftok的路径必须存在且可访问,否则返回的key是-1。有一次我路径写错了,排查了半天才发现是ftok失败了。建议用绝对路径,比如/tmp/xxx。
2.3 shmat —— 把共享内存挂到进程地址空间
shmget只是拿到了一个ID,就像你拿到了一个仓库的钥匙。但你还得把仓库「映射」到你的院子里才能用。shmat就是干这个的。
void *shmat(int shmid, const void *shmaddr, int shmflg);
// 返回值:成功返回映射地址,失败返回(void*)-1
参数详解:
- shmid:shmget返回的共享内存ID。
- shmaddr:建议的映射地址。传NULL让系统自动选,我建议永远传NULL。
- shmflg:标志位。SHM_RDONLY表示只读映射,0表示读写映射。
// 把共享内存挂到进程地址空间
char *data = (char *)shmat(shmid, NULL, 0);
if (data == (void *)-1) {
perror("shmat failed");
exit(1);
}
// 现在可以直接读写data指向的内存了
strcpy(data, "Hello from process A!");
注意:shmat返回的地址是虚拟地址,不同进程里这个地址可能不一样。但没关系,它们指向的是同一块物理内存。这就是共享内存的魔力。
2.4 shmdt —— 分离共享内存
用完了共享内存,记得「分离」。shmdt不会删除共享内存,只是让当前进程不再能访问它。
int shmdt(const void *shmaddr);
// 返回值:成功返回0,失败返回-1
// 分离共享内存
if (shmdt(data) == -1) {
perror("shmdt failed");
exit(1);
}
我的建议:进程退出前一定要调用shmdt。虽然进程退出时系统会自动分离,但显式调用是个好习惯。我曾经见过一个程序,子进程异常退出没分离,导致共享内存泄漏——虽然内存还在,但引用计数乱了。
2.5 完整示例:两个进程通过共享内存通信
下面是一个完整的例子。进程A写入数据,进程B读取数据。你想想看,这比管道和消息队列简单多了吧?
进程A(写入者):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/ipc.h>
int main() {
key_t key = ftok("/tmp/shmfile", 'A');
int shmid = shmget(key, 1024, IPC_CREAT | 0666);
char *data = (char *)shmat(shmid, NULL, 0);
strcpy(data, "Hello from process A!");
printf("Process A wrote: %s\n", data);
shmdt(data);
return 0;
}
进程B(读取者):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/ipc.h>
int main() {
key_t key = ftok("/tmp/shmfile", 'A');
int shmid = shmget(key, 1024, 0666); // 注意:这里没有IPC_CREAT
char *data = (char *)shmat(shmid, NULL, 0);
printf("Process B read: %s\n", data);
shmdt(data);
return 0;
}
我曾经踩过的坑:进程B读取时,如果进程A还没写入,进程B读到的是未初始化的数据(全是0或者垃圾值)。所以共享内存通常需要配合信号量或互斥锁来同步。这个我们后面章节会详细讲。
2.6 删除共享内存
共享内存不会因为进程退出而自动删除。你得手动删,不然它一直占着内存。
// 用shmctl删除共享内存
shmctl(shmid, IPC_RMID, NULL);
也可以用命令行工具:
ipcs -m # 查看所有共享内存
ipcrm -m shmid # 删除指定共享内存
我的习惯:开发阶段我经常用 ipcrm -a 清掉所有共享内存。但生产环境千万别这么干!我建议在程序里用atexit注册一个清理函数,确保退出时删除共享内存。
2.7 小结
嗯,到这里你应该对System V共享内存有了基本了解。说白了就是三步走:shmget创建、shmat映射、shmdt分离。最后别忘了shmctl删除。
POSIX共享内存的接口我们后面章节再细讲。但核心思想是一样的——让多个进程直接读写同一块物理内存,实现最高效的IPC。
下一章我们会讲共享内存的同步问题。你想想看,多个进程同时写一块内存,不出乱子才怪。到时候我会分享我在项目中用信号量做同步的实战经验。