8、共享内存实战:共享内存的权限控制、共享内存的大小限制、共享内存的生命周期、共享内存与信号量的配合使用、共享内存的优缺点、共享内存的应用场景、共享内存的常见错误与调试、共享内存综合案例
共享内存,说白了就是让两个或多个进程直接读写同一块物理内存。它不像管道、消息队列那样需要数据拷贝,所以速度极快。我刚开始接触Linux进程间通信时,觉得共享内存简直就是神器——后来才发现,用不好它,坑也不少。
8.1 共享内存的权限控制
共享内存的权限控制,核心在于shmget()的第三个参数——权限标志位。它和文件权限类似,但又不完全一样。
权限标志位格式:
IPC_CREAT | IPC_EXCL | 0666
其中 0666 表示所有用户可读写,0600 仅属主可读写。
我在项目中遇到过一个问题:两个进程用共享内存通信,一个进程是root启动的,另一个是普通用户。结果普通用户死活打不开共享内存。后来一查,是权限设成了0600。改成0666后问题解决。嗯,权限这东西,真不能想当然。
小技巧:用 ipcs -m 查看当前系统的共享内存段,权限列会显示类似 666 的数字。如果发现权限不对,可以用 ipcrm -M key 删除重建。
8.2 共享内存的大小限制
共享内存不是你想开多大就能开多大的。系统有硬性限制。我见过新手一上来就申请1GB共享内存,结果shmget()直接返回-1。
| 限制参数 | 默认值 | 说明 |
|---|---|---|
SHMMAX |
通常为 32MB ~ 128MB | 单个共享内存段的最大字节数 |
SHMMIN |
1 | 单个共享内存段的最小字节数 |
SHMMNI |
4096 | 系统允许的最大共享内存段数量 |
SHMALL |
取决于内存大小 | 系统允许的共享内存总页数 |
你可以用 sysctl kernel.shmmax 查看当前值。想改的话,sysctl -w kernel.shmmax=268435456 就能临时改成256MB。不过我个人建议,别改太大——共享内存是物理内存,你占多了,其他进程就得去交换分区,系统性能会急剧下降。
注意:共享内存的大小必须是系统页大小的整数倍。用 getpagesize() 查看页大小,通常是4096字节。如果你申请5000字节,系统实际会分配8192字节,多出来的部分就是浪费。
8.3 共享内存的生命周期
共享内存的生命周期和进程不同步。这是它和管道、消息队列最大的区别。进程退出后,共享内存不会自动销毁。它一直留在内核里,直到你显式调用shmctl()删除,或者系统重启。
为什么会这样?因为共享内存的设计初衷就是让多个进程共享数据,哪怕所有进程都退出了,数据也应该保留,供后续启动的进程读取。你想想看,如果进程一退出内存就没了,那还叫「共享」吗?
我曾经在调试一个服务程序时,发现共享内存越用越多。查了半天,原来是服务异常退出时没来得及清理共享内存。每次重启都新建一个,旧的还留着。最后用 ipcs -m 一看,好家伙,几十个废弃的共享内存段。
建议:在程序入口处,先尝试用 shmget() 加 IPC_EXCL 标志创建共享内存。如果失败,说明已经存在,就用 shmctl() 删除旧的再重建。这样能避免重复创建的问题。
8.4 共享内存与信号量的配合使用
共享内存本身没有同步机制。两个进程同时写同一块内存,数据就乱了。这时候就需要信号量来帮忙。
我习惯的做法是:用信号量保护共享内存的读写操作。写之前申请信号量,写完后释放。读之前也申请信号量,读完后释放。这样就能保证同一时刻只有一个进程在操作共享内存。
// 伪代码示例
#include <sys/sem.h>
#include <sys/shm.h>
// 创建信号量
int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0666);
// 初始化信号量值为1
semctl(semid, 0, SETVAL, 1);
// 创建共享内存
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
void *shmaddr = shmat(shmid, NULL, 0);
// 写操作
struct sembuf sb = {0, -1, 0}; // P操作,申请资源
semop(semid, &sb, 1);
// 写入共享内存
memcpy(shmaddr, data, size);
sb.sem_op = 1; // V操作,释放资源
semop(semid, &sb, 1);
// 读操作
sb.sem_op = -1;
semop(semid, &sb, 1);
// 读取共享内存
memcpy(data, shmaddr, size);
sb.sem_op = 1;
semop(semid, &sb, 1);
核心要点:信号量的P操作和V操作必须成对出现。少一个,就会导致死锁。我见过最典型的错误就是:写操作时申请了信号量,但写完后忘记释放。结果读进程永远等不到信号量,整个系统卡死。
8.5 共享内存的优缺点
优点很明显:
- 速度快:没有数据拷贝,直接读写内存。比管道快10倍以上。
- 数据量大:可以传输任意大小的数据,不像管道有缓冲区限制。
- 持久化:进程退出后数据还在,适合需要持久化的场景。
缺点也不少:
- 同步复杂:必须自己用信号量或互斥锁控制并发访问。
- 生命周期管理麻烦:容易产生废弃的共享内存段,占用系统资源。
- 调试困难:多个进程同时操作同一块内存,出问题很难定位。
- 跨平台性差:System V共享内存是Linux特有的,Windows上不兼容。
8.6 共享内存的应用场景
共享内存最适合的场景是:高频数据交换和大数据量传输。
我做过一个视频采集系统,摄像头每秒产生30帧图像,每帧2MB。如果用管道传输,CPU占用率直接飙到80%。换成共享内存后,CPU占用率降到10%以下。说白了,共享内存就是为这种场景设计的。
其他常见场景:
- 数据库缓存:多个数据库进程共享同一块缓存数据。
- 实时监控系统:采集进程写数据,展示进程读数据。
- 游戏引擎:多个模块共享场景数据。
- 日志系统:多个进程写日志到同一块共享内存,再由一个进程统一写入磁盘。
8.7 共享内存的常见错误与调试
我总结了几种最常见的错误:
- 权限错误:
shmget()返回-1,errno为EACCES。检查权限标志位。 - 内存越界:写入的数据超过共享内存大小。这会导致其他进程读到脏数据,甚至段错误。
- 忘记
shmdt():进程退出前没有分离共享内存。虽然进程退出时会自动分离,但这是个好习惯。 - 信号量死锁:申请了信号量但没释放,或者释放了多次。
- 共享内存泄漏:进程退出前没有调用
shmctl()删除共享内存。用ipcs -m定期检查。
调试技巧:用 strace 跟踪进程的系统调用,能看到 shmget()、shmat()、shmdt() 的返回值。如果返回-1,strace 会直接显示错误原因,比看代码快多了。
8.8 共享内存综合案例
下面是一个完整的例子:两个进程通过共享内存交换数据,用信号量保证同步。
// shm_server.c - 创建共享内存并写入数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <unistd.h>
#define SHM_SIZE 1024
int main() {
// 创建信号量
int semid = semget(1234, 1, IPC_CREAT | 0666);
semctl(semid, 0, SETVAL, 1);
// 创建共享内存
int shmid = shmget(5678, SHM_SIZE, IPC_CREAT | 0666);
char *shmaddr = (char *)shmat(shmid, NULL, 0);
// 写入数据
struct sembuf sb = {0, -1, 0};
semop(semid, &sb, 1);
strcpy(shmaddr, "Hello from server!");
sb.sem_op = 1;
semop(semid, &sb, 1);
printf("Server: data written\n");
sleep(10); // 等待客户端读取
// 清理
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, NULL);
semctl(semid, 0, IPC_RMID);
return 0;
}
// shm_client.c - 读取共享内存中的数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/sem.h>
#define SHM_SIZE 1024
int main() {
// 获取信号量
int semid = semget(1234, 1, 0666);
// 获取共享内存
int shmid = shmget(5678, SHM_SIZE, 0666);
char *shmaddr = (char *)shmat(shmid, NULL, 0);
// 读取数据
struct sembuf sb = {0, -1, 0};
semop(semid, &sb, 1);
printf("Client: received: %s\n", shmaddr);
sb.sem_op = 1;
semop(semid, &sb, 1);
// 分离
shmdt(shmaddr);
return 0;
}
先运行shm_server,再运行shm_client。你会看到客户端成功读取到服务端写入的数据。这就是共享内存配合信号量的基本用法。
扩展思考:如果多个客户端同时读取,信号量会串行化所有操作。你可以用读写锁(信号量值为N)来优化:允许N个读者同时读,但写者必须独占。
共享内存这块内容,说难不难,说简单也不简单。关键是要理解它的生命周期和同步机制。我刚开始用共享内存时,也踩过不少坑。但只要掌握了上面这些要点,你就能在实际项目中游刃有余了。