共享内存:进程间最快的数据通道

各位同学,今天我们来聊聊System V共享内存。说实话,这是我在嵌入式项目中用得最多的IPC机制。为什么?因为它快。不是一般的快,是那种让你感觉不到数据在传输的快。

共享内存的原理其实很简单:让两个或多个进程共享同一块物理内存区域。数据写进去,对方立刻就能看到,不需要内核在中间当传话筒。我当年第一次用共享内存做数据采集系统时,看到延迟从毫秒级降到微秒级,着实被震撼到了。

核心函数一览

System V共享内存有四个核心函数,我习惯把它们分成两组:

  • 创建/获取shmget() — 分配或获取一块共享内存
  • 挂接/分离shmat()shmdt() — 把共享内存映射到进程地址空间
  • 控制/销毁shmctl() — 设置属性、获取信息、删除共享内存

说白了,shmget是申请房间,shmat是拿到房间钥匙走进去,shmdt是走出来,shmctl是退房或者查房。

shmget() — 创建或获取共享内存

这个函数的原型长这样:

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

参数说明:

  • key:一个整数标识符,不同进程通过相同的key找到同一块共享内存。可以用IPC_PRIVATE创建私有共享内存(只能用于父子进程)。
  • size:共享内存的大小,单位字节。系统会按页对齐,但你别指望能多用。
  • shmflg:权限标志,类似文件权限。常用组合:IPC_CREAT | 0666

返回值:成功返回共享内存标识符(一个整数),失败返回-1。

我的经验:key的生成我推荐用ftok()函数,它根据文件路径和项目ID生成一个唯一的key。我在项目中吃过亏,直接用固定数字当key,结果和其他程序冲突了。后来统一用ftok,再也没出过问题。

shmat() — 把共享内存挂接到进程

拿到共享内存的ID后,你得把它映射到自己的地址空间才能用:

void *shmat(int shmid, const void *shmaddr, int shmflg);

参数:

  • shmid:shmget返回的ID。
  • shmaddr:建议的挂接地址。传NULL让系统自己选,我建议你永远传NULL。
  • shmflg:标志位。常用0(可读写)或SHM_RDONLY(只读)。

返回值:成功返回映射后的指针,失败返回(void *)-1

注意:返回的指针不要自己指定地址!我曾经见过有人试图指定固定地址,结果在不同系统上直接段错误。让内核帮你选,它比你更懂内存布局。

shmdt() — 分离共享内存

用完了,记得分离:

int shmdt(const void *shmaddr);

参数就是shmat返回的那个指针。分离后,进程不能再访问这块内存。但注意,这只是断开映射,共享内存本身还在,其他进程仍然能用。

shmctl() — 控制共享内存

这个函数功能很丰富:

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

常用命令:

  • IPC_RMID:标记删除共享内存。注意是标记,真正删除要等到所有进程都分离后。
  • IPC_STAT:获取共享内存的状态信息。
  • IPC_SET:设置共享内存的属性。

避坑指南:我曾经在项目中犯过一个错误——进程崩溃退出前没调用shmdt,导致共享内存一直挂着。虽然系统会在进程退出时自动分离,但如果你用IPC_RMID删除时还有进程挂着,删除操作会失败。所以,一定要在进程退出前做好清理工作

完整示例:两个进程通过共享内存通信

下面是一个完整的例子。写端进程创建共享内存并写入数据,读端进程读取并打印。

写端(writer.c):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok("/tmp", 'A');
    if (key == -1) {
        perror("ftok");
        exit(1);
    }

    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    char *data = (char *)shmat(shmid, NULL, 0);
    if (data == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    const char *msg = "Hello from writer!";
    strcpy(data, msg);
    printf("Writer: 已写入数据: %s\n", msg);

    // 等待读端读取
    sleep(2);

    shmdt(data);
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

读端(reader.c):

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok("/tmp", 'A');
    if (key == -1) {
        perror("ftok");
        exit(1);
    }

    int shmid = shmget(key, SHM_SIZE, 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    char *data = (char *)shmat(shmid, NULL, 0);
    if (data == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    printf("Reader: 读取到数据: %s\n", data);

    shmdt(data);
    return 0;
}

共享内存的同步问题

嗯,这里要重点说一下。共享内存本身不提供任何同步机制。多个进程同时读写同一块内存,数据竞争是必然的。我见过最典型的bug就是:写端还没写完,读端就开始读,结果读到一半的数据。

解决方案通常有两种:

  • 信号量:用System V信号量或POSIX信号量做互斥。
  • 读写锁:适合读多写少的场景。

我个人习惯用POSIX信号量配合共享内存,接口更简洁,跨平台性也更好。

知识体系总览

下面这张图帮你理清共享内存的完整流程:

System V 共享内存完整流程 进程A(写端) 进程B(读端) 共享内存 (物理内存) 内核维护的 shmid_ds 结构体 权限、大小、挂接计数... shmat() 挂接 shmat() 挂接 shmctl() 管理 ① shmget() 创建/获取 → ② shmat() 挂接 → ③ 读写数据 → ④ shmdt() 分离 → ⑤ shmctl(IPC_RMID) 删除

共享内存 vs 其他IPC

我整理了一个对比表,方便你选型:

特性 共享内存 消息队列 管道 信号量
速度 最快(无内核拷贝) 中等 快(仅传递信号)
数据量 大(可达几GB) 有限(通常几KB) 有限 无(仅计数)
同步机制 需要额外实现 自带阻塞 自带阻塞 自带
适用场景 大数据量、高频通信 消息传递、解耦 父子进程简单通信 资源互斥、同步

几个实用技巧

  • 查看系统中的共享内存:用命令 ipcs -m,可以列出所有共享内存段。用 ipcrm -m shmid 删除。
  • 共享内存的生命周期:它不随进程退出而销毁。即使所有进程都分离了,共享内存仍然存在,直到你显式调用 shmctl(IPC_RMID) 或者系统重启。
  • 大小限制:可以用 cat /proc/sys/kernel/shmmax 查看系统允许的最大共享内存大小。我遇到过嵌入式设备默认只有32MB,需要调大的情况。

我的习惯:在调试阶段,我经常用 ipcs -m 检查是否有残留的共享内存。如果程序异常退出,共享内存可能没被删除,下次运行时会因为key冲突而出错。这时候手动 ipcrm -m 一下就好。

总结

共享内存是Linux下最高效的IPC方式,没有之一。它的核心思想就是让多个进程直接访问同一块物理内存,省去了内核拷贝的开销。但高效也意味着责任——你必须自己处理好同步和生命周期管理。

嗯,最后提醒一句:如果你在项目中用共享内存,记得在程序退出时做好清理。我曾经因为忘记删除共享内存,导致系统资源泄漏,最后不得不重启设备。从那以后,我每次写共享内存代码,都会在atexit()里注册清理函数。


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