1. System V 共享内存基础概念
共享内存是Linux进程间通信(IPC)中最快的一种方式。不同于管道或消息队列需要在内核和用户空间之间复制数据,共享内存允许两个或多个进程直接访问同一块物理内存区域。System V IPC是最早出现在UNIX System V上的进程通信机制,如今已成为POSIX标准的一部分。
在实际项目中,我们曾用共享内存实现过实时日志收集系统。多个工作进程将日志直接写入共享内存区,监控进程从同一区域读取数据,避免了频繁的磁盘I/O操作,性能比传统文件日志提升了20倍以上。
2. 共享内存核心操作详解
2.1 创建共享内存段
使用shmget()系统调用创建新的共享内存段:
c复制int shmget(key_t key, size_t size, int shmflg);
关键参数说明:
- key:通常使用ftok()生成的唯一键值
- size:共享内存段大小(字节)
- shmflg:权限标志组合,常用IPC_CREAT|0666
实际经验:size应该设置为系统页大小的整数倍(通过getconf PAGESIZE获取)。我们曾遇到过因size设置不当导致的内存浪费问题——某项目设置了513KB的共享内存,实际占用却是2个内存页(假设页大小4KB),造成近50%的空间浪费。
2.2 连接共享内存
进程通过shmat()将共享内存段附加到自己的地址空间:
c复制void *shmat(int shmid, const void *shmaddr, int shmflg);
参数shmaddr的三种典型用法:
- NULL:让系统自动选择映射地址(推荐方式)
- 指定地址:要求映射到特定虚拟地址(需SHM_RND标志)
- (void *)-1:只读方式附加(SHM_RDONLY)
踩坑记录:某次在x86_64系统上硬编码了32位地址导致段错误。现代ASLR机制下,应当始终让内核选择映射地址。
2.3 读写同步控制
虽然共享内存本身没有内置同步机制,但实际使用时必须配合信号量:
c复制// 创建信号量集
int sem_id = semget(key, 1, IPC_CREAT|0666);
// 初始化二进制信号量
union semun arg;
arg.val = 1;
semctl(sem_id, 0, SETVAL, arg);
// P操作(加锁)
struct sembuf sb = {0, -1, SEM_UNDO};
semop(sem_id, &sb, 1);
// V操作(解锁)
sb.sem_op = 1;
semop(sem_id, &sb, 1);
3. 实战案例:进程间数据共享
3.1 生产者-消费者模型实现
生产者进程代码片段:
c复制struct shared_data {
int counter;
char buffer[1024];
};
// 创建共享内存
int shm_id = shmget(IPC_PRIVATE, sizeof(struct shared_data), IPC_CREAT|0666);
// 附加内存
struct shared_data *shm_ptr = shmat(shm_id, NULL, 0);
// 写入数据
sem_wait(sem_id); // 获取锁
shm_ptr->counter++;
strcpy(shm_ptr->buffer, "Producer Data");
sem_post(sem_id); // 释放锁
消费者进程对应代码:
c复制// 获取已有共享内存
int shm_id = shmget(key, 0, 0);
// 附加内存
struct shared_data *shm_ptr = shmat(shm_id, NULL, SHM_RDONLY);
// 读取数据
sem_wait(sem_id);
printf("Counter: %d\n", shm_ptr->counter);
printf("Data: %s\n", shm_ptr->buffer);
sem_post(sem_id);
3.2 性能优化技巧
- 内存对齐:结构体成员按cache line大小(通常64字节)对齐,避免伪共享
c复制struct __attribute__((aligned(64))) shared_data {
int counter;
char padding[60];
char buffer[1024];
};
-
批量处理:减少锁的获取/释放次数,如每次写入多条记录而非单条
-
无锁设计:对于计数器等简单类型,可使用原子操作(需GCC扩展)
c复制__atomic_add_fetch(&shm_ptr->counter, 1, __ATOMIC_RELAXED);
4. 系统管理与问题排查
4.1 常用管理命令
查看系统共享内存状态:
bash复制ipcs -m
删除特定共享内存段:
bash复制ipcrm -m <shmid>
监控共享内存使用:
bash复制watch -n 1 'ipcs -m | grep -v "0x00000000"'
4.2 常见问题解决方案
问题1:EINVAL错误
- 可能原因:共享内存已被删除
- 解决方案:检查/proc/sys/kernel/shm_rmid_forced值,或改用更稳定的ftok()键值
问题2:ENOMEM错误
- 可能原因:达到系统限制
- 调整内核参数:
bash复制sysctl -w kernel.shmmax=4294967296 # 最大单个共享内存段(4GB)
sysctl -w kernel.shmall=2097152 # 系统总共享内存页数(8GB)
问题3:内存泄漏检测
使用pmap工具检查进程内存映射:
bash复制pmap -x <pid> | grep shm
5. 进阶话题:共享内存持久化
通过mmap()实现共享内存的磁盘持久化:
c复制int fd = open("shm_file", O_RDWR|O_CREAT, 0666);
ftruncate(fd, size);
void *ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
这种方案结合了共享内存的高效性和文件存储的持久性,特别适合以下场景:
- 需要快速重启恢复的应用
- 跨主机共享数据(配合NFS)
- 大数据处理中的中间结果存储
在实际数据库系统中,我们曾用此技术实现WAL(Write-Ahead Logging)的加速,将日志先写入内存映射文件,再由后台线程定期刷盘,使写入吞吐量提升了3倍。