共享内存:进程间最快的数据通道
各位同学,今天我们来聊聊System V共享内存。说实话,这是我在嵌入式项目中用得最多的IPC机制。为什么?因为它快。不是一般的快,是那种让你感觉不到数据在传输的快。
共享内存的原理其实很简单:让两个或多个进程共享同一块物理内存区域。数据写进去,对方立刻就能看到,不需要内核在中间当传话筒。我当年第一次用共享内存做数据采集系统时,看到延迟从毫秒级降到微秒级,着实被震撼到了。
核心函数一览
System V共享内存有四个核心函数,我习惯把它们分成两组:
- 创建/获取:
shmget()— 分配或获取一块共享内存 - 挂接/分离:
shmat()和shmdt()— 把共享内存映射到进程地址空间 - 控制/销毁:
shmctl()— 设置属性、获取信息、删除共享内存
说白了,shmget是申请房间,shmat是拿到房间钥匙走进去,shmdt是走出来,shmctl是退房或者查房。
shmget() — 创建或获取共享内存
这个函数的原型长这样:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数说明:
key:一个整数标识符,不同进程通过相同的key找到同一块共享内存。可以用IPC_PRIVATE创建私有共享内存(只能用于父子进程)。size:共享内存的大小,单位字节。系统会按页对齐,但你别指望能多用。shmflg:权限标志,类似文件权限。常用组合:IPC_CREAT | 0666。
返回值:成功返回共享内存标识符(一个整数),失败返回-1。
我的经验:key的生成我推荐用ftok()函数,它根据文件路径和项目ID生成一个唯一的key。我在项目中吃过亏,直接用固定数字当key,结果和其他程序冲突了。后来统一用ftok,再也没出过问题。
shmat() — 把共享内存挂接到进程
拿到共享内存的ID后,你得把它映射到自己的地址空间才能用:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid:shmget返回的ID。shmaddr:建议的挂接地址。传NULL让系统自己选,我建议你永远传NULL。shmflg:标志位。常用0(可读写)或SHM_RDONLY(只读)。
返回值:成功返回映射后的指针,失败返回(void *)-1。
注意:返回的指针不要自己指定地址!我曾经见过有人试图指定固定地址,结果在不同系统上直接段错误。让内核帮你选,它比你更懂内存布局。
shmdt() — 分离共享内存
用完了,记得分离:
int shmdt(const void *shmaddr);
参数就是shmat返回的那个指针。分离后,进程不能再访问这块内存。但注意,这只是断开映射,共享内存本身还在,其他进程仍然能用。
shmctl() — 控制共享内存
这个函数功能很丰富:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
常用命令:
IPC_RMID:标记删除共享内存。注意是标记,真正删除要等到所有进程都分离后。IPC_STAT:获取共享内存的状态信息。IPC_SET:设置共享内存的属性。
避坑指南:我曾经在项目中犯过一个错误——进程崩溃退出前没调用shmdt,导致共享内存一直挂着。虽然系统会在进程退出时自动分离,但如果你用IPC_RMID删除时还有进程挂着,删除操作会失败。所以,一定要在进程退出前做好清理工作。
完整示例:两个进程通过共享内存通信
下面是一个完整的例子。写端进程创建共享内存并写入数据,读端进程读取并打印。
写端(writer.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define SHM_SIZE 1024
int main() {
key_t key = ftok("/tmp", 'A');
if (key == -1) {
perror("ftok");
exit(1);
}
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}
char *data = (char *)shmat(shmid, NULL, 0);
if (data == (void *)-1) {
perror("shmat");
exit(1);
}
const char *msg = "Hello from writer!";
strcpy(data, msg);
printf("Writer: 已写入数据: %s\n", msg);
// 等待读端读取
sleep(2);
shmdt(data);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
读端(reader.c):
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_SIZE 1024
int main() {
key_t key = ftok("/tmp", 'A');
if (key == -1) {
perror("ftok");
exit(1);
}
int shmid = shmget(key, SHM_SIZE, 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}
char *data = (char *)shmat(shmid, NULL, 0);
if (data == (void *)-1) {
perror("shmat");
exit(1);
}
printf("Reader: 读取到数据: %s\n", data);
shmdt(data);
return 0;
}
共享内存的同步问题
嗯,这里要重点说一下。共享内存本身不提供任何同步机制。多个进程同时读写同一块内存,数据竞争是必然的。我见过最典型的bug就是:写端还没写完,读端就开始读,结果读到一半的数据。
解决方案通常有两种:
- 信号量:用System V信号量或POSIX信号量做互斥。
- 读写锁:适合读多写少的场景。
我个人习惯用POSIX信号量配合共享内存,接口更简洁,跨平台性也更好。
知识体系总览
下面这张图帮你理清共享内存的完整流程:
共享内存 vs 其他IPC
我整理了一个对比表,方便你选型:
| 特性 | 共享内存 | 消息队列 | 管道 | 信号量 |
|---|---|---|---|---|
| 速度 | 最快(无内核拷贝) | 中等 | 慢 | 快(仅传递信号) |
| 数据量 | 大(可达几GB) | 有限(通常几KB) | 有限 | 无(仅计数) |
| 同步机制 | 需要额外实现 | 自带阻塞 | 自带阻塞 | 自带 |
| 适用场景 | 大数据量、高频通信 | 消息传递、解耦 | 父子进程简单通信 | 资源互斥、同步 |
几个实用技巧
- 查看系统中的共享内存:用命令
ipcs -m,可以列出所有共享内存段。用ipcrm -m shmid删除。 - 共享内存的生命周期:它不随进程退出而销毁。即使所有进程都分离了,共享内存仍然存在,直到你显式调用
shmctl(IPC_RMID)或者系统重启。 - 大小限制:可以用
cat /proc/sys/kernel/shmmax查看系统允许的最大共享内存大小。我遇到过嵌入式设备默认只有32MB,需要调大的情况。
我的习惯:在调试阶段,我经常用 ipcs -m 检查是否有残留的共享内存。如果程序异常退出,共享内存可能没被删除,下次运行时会因为key冲突而出错。这时候手动 ipcrm -m 一下就好。
总结
共享内存是Linux下最高效的IPC方式,没有之一。它的核心思想就是让多个进程直接访问同一块物理内存,省去了内核拷贝的开销。但高效也意味着责任——你必须自己处理好同步和生命周期管理。
嗯,最后提醒一句:如果你在项目中用共享内存,记得在程序退出时做好清理。我曾经因为忘记删除共享内存,导致系统资源泄漏,最后不得不重启设备。从那以后,我每次写共享内存代码,都会在atexit()里注册清理函数。