3、共享内存的同步与泄漏:共享内存与信号量配合使用、shmdt与shmctl的调用时机、共享内存段残留(ipcs命令查看僵尸段)、ftok冲突导致的内存泄漏
共享内存,说白了就是让两个进程直接读写同一块物理内存。速度确实快,但坑也不少。我这些年排查过的线上问题里,共享内存相关的泄漏占了不小的比例。今天咱们就聊聊这块的几个关键点。
3.1 共享内存与信号量配合使用
共享内存本身没有同步机制。两个进程同时写,数据就乱了。我见过一个项目,两个进程往共享内存里写日志,结果日志内容互相穿插,根本没法看。
解决办法很简单——加信号量。信号量就像一把锁,谁拿到谁才能操作共享内存。
#include <sys/sem.h>
#include <sys/shm.h>
// 创建信号量
int sem_id = semget(IPC_PRIVATE, 1, IPC_CREAT | 0666);
// 初始化为1,表示可用
semctl(sem_id, 0, SETVAL, 1);
// 创建共享内存
int shm_id = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
void *shm_ptr = shmat(shm_id, NULL, 0);
// 写操作前加锁
struct sembuf sb = {0, -1, SEM_UNDO};
semop(sem_id, &sb, 1);
// 写入共享内存
memcpy(shm_ptr, data, size);
// 写完后解锁
sb.sem_op = 1;
semop(sem_id, &sb, 1);
这里有个细节——SEM_UNDO标志。进程意外退出时,信号量会自动归还。不然锁就永远拿不到了。嗯,这个我吃过亏。
3.2 shmdt与shmctl的调用时机
这两个函数容易搞混。我刚开始做项目时也分不清。
shmdt是把共享内存从当前进程的地址空间分离。进程退出时会自动调用,但显式调用更安全。
shmctl是真正删除共享内存段。调用shmctl(shm_id, IPC_RMID, NULL)后,所有进程都无法再attach。
调用时机很重要:
- 用完就分离:不再需要访问共享内存时,立刻调用
shmdt。别等到进程退出。 - 最后一个进程分离后删除:所有进程都调用了
shmdt,再调用shmctl删除。
// 正确的做法
void cleanup_shared_memory(int shm_id, void *shm_ptr) {
// 1. 先分离
if (shmdt(shm_ptr) == -1) {
perror("shmdt failed");
return;
}
// 2. 再删除
if (shmctl(shm_id, IPC_RMID, NULL) == -1) {
perror("shmctl failed");
}
}
3.3 共享内存段残留——僵尸段
进程崩溃了,共享内存没来得及删除。这就产生了僵尸段。用ipcs -m命令可以查看:
$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 123456 root 666 4096 0 dest
0x00000000 789012 root 666 8192 0 dest
看到nattch为0,status为dest的段,就是僵尸段。它们占着系统资源不释放。
我遇到过最严重的一次,生产环境上有几百个僵尸段,每个4KB,加起来占了几MB内存。虽然不多,但系统共享内存上限被耗尽,新进程创建共享内存时直接报错。
清理方法:
# 查看所有共享内存段
ipcs -m
# 删除指定段
ipcrm -m shmid
# 或者批量删除
ipcs -m | grep "dest" | awk '{print $2}' | xargs -I {} ipcrm -m {}
shmget时加IPC_CREAT | IPC_EXCL标志,如果段已存在就报错,然后删除重建。
3.4 ftok冲突导致的内存泄漏
ftok函数用文件路径和项目ID生成一个key。问题在于,不同文件可能生成相同的key。
key_t key1 = ftok("/tmp/file1", 'A');
key_t key2 = ftok("/tmp/file2", 'A');
// 有可能 key1 == key2
为什么会这样?ftok的实现是把文件的inode号和项目ID组合。如果两个文件在同一个文件系统,inode号可能相同(比如硬链接),或者项目ID相同,key就冲突了。
冲突的后果:两个进程以为用的是不同的共享内存,实际上操作的是同一块。数据互相覆盖,逻辑混乱。更糟的是,一个进程删除了共享内存,另一个进程还在用,直接段错误。
我曾经排查过一个诡异的问题:两个服务A和B,各自用共享内存存配置。结果A改了配置,B的配置也变了。查了半天,发现是ftok生成的key冲突了。
解决办法:
- 用
IPC_PRIVATE:不依赖key,系统自动分配。但只能用于有亲缘关系的进程。 - 用固定key:自己定义key值,比如
0x1234。但要确保不冲突。 - 检查key是否冲突:用
shmget尝试创建,如果返回EEXIST,说明key已被占用。
key_t key = ftok("/tmp/myapp.conf", 'X');
if (key == -1) {
perror("ftok failed");
exit(1);
}
int shm_id = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666);
if (shm_id == -1 && errno == EEXIST) {
// key冲突,删除旧的再创建
shm_id = shmget(key, 4096, 0);
shmctl(shm_id, IPC_RMID, NULL);
shm_id = shmget(key, 4096, IPC_CREAT | 0666);
}
- 共享内存必须配信号量,不然数据会乱
- shmdt和shmctl的调用顺序不能错
- 定期用ipcs检查僵尸段,及时清理
- ftok冲突是个隐蔽的坑,用IPC_PRIVATE或固定key更安全
嗯,这些经验都是真金白银换来的。下次咱们聊聊消息队列的泄漏问题,那个坑也不少。