共享内存:进程间通信的“高速公路”
共享内存,说白了就是让两个或多个进程直接访问同一块物理内存。这是Linux下效率最高的IPC方式——没有之一。为什么?因为数据不需要在内核和用户空间之间来回拷贝,进程A写进去,进程B立马就能看到。
我在做视频流处理项目时,就吃过这个甜头。当时用管道传数据,CPU占用率居高不下,换成共享内存后,性能直接翻倍。嗯,今天我们就来聊聊这个“高速公路”怎么修、怎么用、怎么维护。
1. System V 共享内存:经典三件套
System V 共享内存是Unix老祖宗传下来的API,核心就四个函数:shmget、shmat、shmdt、shmctl。我习惯把它们叫做“创建-挂接-分离-控制”四步曲。
1.1 shmget:申请共享内存
这个函数负责创建或获取一块共享内存。原型长这样:
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数说明:
- key:IPC键值,相当于共享内存的身份证号。可以用
IPC_PRIVATE(私有)或ftok()生成。 - size:共享内存大小(字节),必须是页大小的整数倍?其实不是,内核会向上对齐到页边界。
- shmflg:权限标志,比如
IPC_CREAT | 0666。
ftok()生成key,别用IPC_PRIVATE。后者只能用于父子进程,跨进程通信时你会后悔的。
1.2 shmat:挂接到进程地址空间
拿到共享内存ID后,得把它映射到进程的虚拟地址空间才能用:
void *shmat(int shmid, const void *shmaddr, int shmflg);
第二个参数通常传NULL,让内核帮你选地址。第三个参数可以传SHM_RDONLY表示只读挂接。
返回值就是共享内存的起始地址。嗯,这里要注意:返回的是void *,你需要自己强转成合适的结构体指针。
1.3 shmdt:分离共享内存
用完了记得分离:
int shmdt(const void *shmaddr);
分离不是删除!只是让当前进程不再访问这块内存。其他进程还能继续用。
1.4 shmctl:控制操作
这个函数功能很强大,可以查询状态、设置权限、删除共享内存:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
常用的cmd:
IPC_STAT:获取共享内存的状态信息IPC_SET:设置共享内存的权限IPC_RMID:标记删除共享内存
IPC_RMID只是标记删除,不是立即释放。只有当所有进程都分离后,内核才会真正回收。如果你在某个进程还在挂接时就调了shmctl(shmid, IPC_RMID, NULL),这块内存会变成“僵尸”——新进程无法再挂接,但老进程还能正常读写。
2. 共享内存的同步问题
共享内存本身不提供任何同步机制。你想想看,两个进程同时写同一块内存,数据不就乱套了吗?
我在做多进程日志系统时,就遇到过这种问题。两个worker进程同时往共享内存写日志,结果日志内容互相穿插,根本没法看。
解决方案通常有两种:
2.1 信号量 + 共享内存
这是最经典的组合拳。用信号量保护共享内存的访问:
// 伪代码
struct shared_data {
int counter;
char buffer[1024];
};
// 进程A
sem_wait(&sem); // P操作
shm_ptr->counter++;
strcpy(shm_ptr->buffer, "hello");
sem_post(&sem); // V操作
// 进程B
sem_wait(&sem);
printf("%d: %s\n", shm_ptr->counter, shm_ptr->buffer);
sem_post(&sem);
2.2 原子操作
对于简单的计数器场景,可以用C11的原子操作:
#include <stdatomic.h>
struct shared_data {
atomic_int counter;
char buffer[1024];
};
// 原子递增
atomic_fetch_add(&shm_ptr->counter, 1);
3. System V vs POSIX 共享内存
Linux下有两套共享内存API:System V(老派)和POSIX(新派)。我刚开始学的时候也纠结过该用哪个,后来两个都用过,说说我的感受。
| 特性 | System V 共享内存 | POSIX 共享内存 |
|---|---|---|
| 创建方式 | shmget() + shmat() |
shm_open() + mmap() |
| 命名方式 | 整数key(IPC键值) | 文件名(/dev/shm下的文件) |
| 大小限制 | 受SHMMAX内核参数限制 |
受文件系统大小限制 |
| 持久性 | 内核持久,直到被删除或重启 | 文件系统持久,重启后还在 |
| 接口复杂度 | 4个函数,稍显繁琐 | 2个函数,配合mmap使用 |
| 可移植性 | 所有Unix系统都支持 | Linux/BSD支持,老Unix可能没有 |
我个人更推荐POSIX共享内存,原因有三:
- 接口更简洁,配合
mmap用起来很顺手 - 用文件名命名,比整数key直观多了
- 可以像操作文件一样
ftruncate调整大小
但如果你在维护老项目,或者需要跨平台兼容(比如AIX、HP-UX),System V还是得会。
4. POSIX 共享内存实战
来看一个完整的例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#define SHM_NAME "/my_shared_mem"
#define SHM_SIZE 4096
int main() {
// 创建共享内存对象
int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
if (fd == -1) {
perror("shm_open");
exit(1);
}
// 设置大小
ftruncate(fd, SHM_SIZE);
// 映射到进程地址空间
void *ptr = mmap(NULL, SHM_SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
exit(1);
}
// 写入数据
strcpy((char *)ptr, "Hello from POSIX shared memory!");
// 清理
munmap(ptr, SHM_SIZE);
close(fd);
shm_unlink(SHM_NAME);
return 0;
}
/dev/shm目录下。你可以用ls -l /dev/shm查看当前系统上所有的共享内存对象。调试时特别有用。
5. 知识体系总览
下面这张图帮你理清共享内存的整个知识脉络:
共享内存是进程间通信的利器,但用不好就是一把双刃剑。记住三点:
- 效率最高,但需要自己处理同步
- System V和POSIX两套API,选一个学透就行
- 调试时多用
ipcs -m和ls /dev/shm查看状态
好了,共享内存就聊到这里。代码写得再多,不如自己动手跑一遍。找个项目试试,你会爱上这种“零拷贝”的快感。