共享内存:进程间通信、shmget/shmat使用、同步问题
共享内存,说白了就是让两个或多个进程,能直接看到同一块物理内存。这玩意儿在进程间通信(IPC)里,是速度最快的——没有之一。为什么?因为数据不需要在内核和用户空间之间来回拷贝,大家直接读写同一块地址。
我刚开始做嵌入式Linux开发时,第一次接触共享内存,觉得这玩意儿太神奇了。两个完全独立的进程,居然能像操作自己的变量一样操作同一块数据。但后来踩了不少坑,尤其是同步问题,让我深刻体会到:共享内存是一把双刃剑。
1. 共享内存的核心原理
先说说它的本质。每个进程都有自己的虚拟地址空间,正常情况下,进程A看不到进程B的内存。但共享内存机制,会在物理内存中划出一块区域,然后让多个进程的虚拟地址都映射到这块物理内存上。
你想想看,这不就是“共享”吗?大家都能访问,谁写谁读都行。但问题也来了——如果两个进程同时写,数据就乱了。
核心要点:共享内存是IPC中最快的方式,但本身不提供同步机制。同步需要你自己用信号量或互斥锁来保证。
2. shmget:创建或获取共享内存
shmget是第一步。它的原型长这样:
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数解释:
- key:一个整数,用来标识共享内存段。不同进程用同一个key,就能找到同一块内存。
- size:共享内存的大小,单位字节。我建议你按页对齐,通常是4KB的整数倍。
- shmflg:权限标志,类似文件权限。比如
IPC_CREAT | 0666表示如果不存在就创建,权限是读写。
返回值是共享内存的ID(shmid),后续操作全靠它。
个人经验:我习惯用 ftok() 函数生成key,而不是直接写死一个数字。因为不同项目可能冲突,ftok用路径和项目ID生成唯一key,靠谱得多。
3. shmat:把共享内存挂到进程空间
有了shmid,还得把这块内存“挂”到当前进程的虚拟地址空间里。这就是shmat干的活:
void *shmat(int shmid, const void *shmaddr, int shmflg);
- shmid:shmget返回的ID。
- shmaddr:建议填NULL,让系统自动选地址。自己指定地址容易出问题,我从不这么干。
- shmflg:一般填0,表示可读写。如果填
SHM_RDONLY就是只读。
返回值是挂载后的虚拟地址,强转成你需要的指针类型就能用了。
// 示例:创建并挂载共享内存
int shmid;
int *shared_data;
// 创建共享内存,大小4KB
shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget failed");
exit(1);
}
// 挂载到进程空间
shared_data = (int *)shmat(shmid, NULL, 0);
if (shared_data == (void *)-1) {
perror("shmat failed");
exit(1);
}
// 现在可以读写 shared_data 了
*shared_data = 42;
注意:shmat返回-1表示失败,但-1强转成指针后,不能直接用 == NULL 判断。正确做法是强转成 (void *)-1 比较。
4. shmdt:分离共享内存
用完了,记得分离。shmdt把共享内存从当前进程的地址空间卸下来:
int shmdt(const void *shmaddr);
参数就是shmat返回的地址。分离后,进程不能再访问这块内存,但共享内存本身还在系统中,其他进程还能用。
5. shmctl:控制共享内存
shmctl用来做各种控制操作,比如删除共享内存、查询状态等:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
常用cmd:
IPC_RMID:删除共享内存。注意,只是标记删除,等所有进程都detach后才会真正释放。IPC_STAT:获取共享内存的状态信息。
6. 同步问题:这才是真正的坑
共享内存本身不提供任何同步机制。两个进程同时写同一块内存,数据就乱了。这就是所谓的“竞态条件”。
我曾经在一个项目中,两个进程通过共享内存传递传感器数据。一个进程写,一个进程读,本来挺简单。但后来加了第三个进程也要写,结果数据经常出现半新半旧的情况——读进程拿到的是两个写进程的混合数据。
解决方案?用信号量。POSIX信号量或者System V信号量都行。我习惯用POSIX信号量,接口更简洁:
#include <semaphore.h>
sem_t *sem;
sem = sem_open("/mysem", O_CREAT, 0666, 1); // 初始值1,互斥信号量
// 写数据前加锁
sem_wait(sem);
*shared_data = new_value;
sem_post(sem);
// 读数据前加锁
sem_wait(sem);
int val = *shared_data;
sem_post(sem);
避坑指南:我曾经犯过一个错误——信号量名字没加斜杠开头。POSIX信号量名字必须以 / 开头,否则 sem_open 会失败。排查了半天才发现。
7. 实战中的注意事项
嗯,这里再补充几个我踩过的坑:
- 共享内存大小要固定:创建时指定了大小,后面不能改。如果数据量会变,建议用固定大小的环形缓冲区。
- 记得删除:程序退出时,如果没调用
shmctl(shmid, IPC_RMID, NULL),共享内存会一直留在系统里。用ipcs -m可以查看,ipcrm -m shmid可以手动删除。 - 权限问题:不同用户运行的进程,共享内存权限要设置好。我一般用
0666,但生产环境建议更严格。 - 不要存指针:共享内存里不要存指针!因为不同进程的虚拟地址空间不同,A进程的指针在B进程里是无效的。存偏移量或者索引更安全。
8. 知识体系总览
下面这张图,把共享内存的核心流程和同步问题串起来了:
9. 完整示例:两个进程通过共享内存通信
最后,给一个完整的例子。进程A写数据,进程B读数据,用信号量同步:
// 进程A(写端)
#include <stdio.h>
#include <sys/shm.h>
#include <semaphore.h>
#include <fcntl.h>
int main() {
int shmid;
int *data;
sem_t *sem;
// 创建共享内存
shmid = shmget(1234, 4096, IPC_CREAT | 0666);
data = (int *)shmat(shmid, NULL, 0);
// 创建信号量
sem = sem_open("/mysem", O_CREAT, 0666, 1);
// 写数据
for (int i = 0; i < 10; i++) {
sem_wait(sem);
*data = i;
printf("写入: %d\n", i);
sem_post(sem);
sleep(1);
}
shmdt(data);
shmctl(shmid, IPC_RMID, NULL);
sem_close(sem);
sem_unlink("/mysem");
return 0;
}
// 进程B(读端)
#include <stdio.h>
#include <sys/shm.h>
#include <semaphore.h>
int main() {
int shmid;
int *data;
sem_t *sem;
// 获取共享内存
shmid = shmget(1234, 4096, 0666);
data = (int *)shmat(shmid, NULL, 0);
// 打开信号量
sem = sem_open("/mysem", 0);
// 读数据
for (int i = 0; i < 10; i++) {
sem_wait(sem);
printf("读取: %d\n", *data);
sem_post(sem);
sleep(1);
}
shmdt(data);
sem_close(sem);
return 0;
}
个人习惯:我通常把共享内存的创建和信号量的初始化放在同一个进程中,避免竞态条件。另一个进程只负责打开和挂载。
共享内存这东西,用好了效率极高,用不好就是各种诡异bug。记住三点:大小固定、不要存指针、一定要加同步。做到这三点,基本不会出大问题。