共享内存:进程间通信的“高速公路”

共享内存,说白了就是让两个或多个进程直接访问同一块物理内存。这是Linux下效率最高的IPC方式——没有之一。为什么?因为数据不需要在内核和用户空间之间来回拷贝,进程A写进去,进程B立马就能看到。

我在做视频流处理项目时,就吃过这个甜头。当时用管道传数据,CPU占用率居高不下,换成共享内存后,性能直接翻倍。嗯,今天我们就来聊聊这个“高速公路”怎么修、怎么用、怎么维护。

1. System V 共享内存:经典三件套

System V 共享内存是Unix老祖宗传下来的API,核心就四个函数:shmgetshmatshmdtshmctl。我习惯把它们叫做“创建-挂接-分离-控制”四步曲。

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;
}
小技巧:POSIX共享内存文件默认在/dev/shm目录下。你可以用ls -l /dev/shm查看当前系统上所有的共享内存对象。调试时特别有用。

5. 知识体系总览

下面这张图帮你理清共享内存的整个知识脉络:

共享内存知识体系 System V 共享内存 POSIX 共享内存 shmget() 创建/获取 shmat() 挂接到进程 读写共享内存 shmdt() 分离 shmctl() 控制/删除 shm_open() 创建/打开 ftruncate() 设置大小 mmap() 映射到进程 读写共享内存 munmap() + shm_unlink() ⚠ 同步问题:信号量 / 原子操作

共享内存是进程间通信的利器,但用不好就是一把双刃剑。记住三点:

  • 效率最高,但需要自己处理同步
  • System V和POSIX两套API,选一个学透就行
  • 调试时多用ipcs -mls /dev/shm查看状态

好了,共享内存就聊到这里。代码写得再多,不如自己动手跑一遍。找个项目试试,你会爱上这种“零拷贝”的快感。

公众号:蓝海资料掘金营,微信 deep3321