16、共享内存:进程间通信、shmget/shmat、同步问题

共享内存,说白了就是让两个或多个进程,能直接读写同一块物理内存。这是Linux下效率最高的进程间通信方式——没有之一。为什么?因为它不需要像管道那样拷贝数据,也不需要像消息队列那样反复进出内核。数据就在那里,谁都能看,谁都能改。

我刚开始接触共享内存时,觉得这东西挺神奇的。两个完全独立的进程,地址空间是隔离的,怎么能访问同一块内存呢?后来才明白,这其实是操作系统在背后做了手脚——把同一块物理内存映射到不同进程的虚拟地址空间里。

16.1 共享内存的核心概念

共享内存的工作原理,可以用一张图说清楚:

共享内存工作原理 进程A 虚拟地址空间 0x7f1234560000 (映射到物理页) 进程B 虚拟地址空间 0x7f6543210000 (映射到物理页) 物理内存(共享区域) 同一块物理页帧 数据在这里,谁都能读写 进程A和进程B的虚拟地址不同,但指向同一块物理内存

你看,进程A和进程B的虚拟地址可能完全不同,但操作系统把它们的页表项指向了同一个物理页帧。这样一来,A写进去的数据,B马上就能读到——零拷贝,零延迟。

核心要点:共享内存是IPC(进程间通信)中速度最快的方式,因为它避免了数据在内核空间和用户空间之间的来回拷贝。但代价是——你需要自己处理同步问题。

16.2 shmget:创建或获取共享内存

要使用共享内存,第一步就是调用shmget()。这个函数要么创建一个新的共享内存段,要么获取一个已经存在的。

#include <sys/shm.h>

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

参数说明:

  • key:一个整数,用来标识共享内存段。不同进程通过相同的key找到同一块内存。一般用ftok()生成。
  • size:共享内存的大小,单位字节。创建时必须指定,获取时填0即可。
  • shmflg:权限标志,类似文件权限。常用IPC_CREAT | 0666表示创建并设置读写权限。

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

我的习惯:创建共享内存时,我一般会加上IPC_EXCL标志,这样如果同key的内存已经存在,shmget()会返回错误,避免不小心覆盖已有数据。

16.3 shmat:把共享内存挂到进程地址空间

shmget()只是拿到了一个标识符,你还得用shmat()把它映射到进程的虚拟地址空间里,才能真正读写。

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

参数说明:

  • shmidshmget()返回的共享内存标识符。
  • shmaddr:建议的映射地址。填NULL让系统自动选择,我建议你永远填NULL——省心。
  • shmflg:通常填0表示可读写。如果填SHM_RDONLY则只读。

返回值:成功返回映射后的虚拟地址,失败返回(void*)-1

用完之后,记得调用shmdt()解除映射。但这只是从当前进程的地址空间里移除,共享内存本身还在,其他进程仍然可以访问。

注意:shmat()返回的地址,你直接当普通指针用就行。但千万别越界——你申请了1024字节,就别写到1025去。我曾经见过一个同事,因为越界写把另一个进程的数据给踩了,排查了整整两天。

16.4 一个完整的例子

光说不练假把式。我们写一个简单的例子:进程A写入数据,进程B读取数据。

写端(writer.c):

#include <stdio.h>
#include <stdlib.h>
#include <string.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(5);

    shmdt(data);
    // 注意:这里没有删除共享内存,读端还需要用
    return 0;
}

读端(reader.c):

#include <stdio.h>
#include <stdlib.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);
    // 读完后可以删除共享内存
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

先运行writer,再运行reader。你会看到reader成功打印出writer写入的字符串。

注意顺序:这个例子能跑通,是因为writer在写入后sleep了5秒,给了reader足够的时间来读取。但在实际项目中,你不能依赖sleep——你需要用信号量或互斥锁来同步。

16.5 同步问题:共享内存的阿克琉斯之踵

共享内存本身不提供任何同步机制。两个进程同时写同一块内存,数据就乱了。这就是经典的「竞态条件」。

我举个例子:假设两个进程都在更新一个计数器:

// 两个进程同时执行
counter = counter + 1;

在汇编层面,这行代码可能被拆成三步:

  1. 从内存读counter到寄存器
  2. 寄存器加1
  3. 写回内存

如果进程A执行完第2步,还没来得及写回,进程B就开始执行——那结果就错了。本来应该加2,结果只加了1。

怎么解决?常用的方法有:

方法 说明 适用场景
信号量(System V) semget/semop实现PV操作 多进程同步,经典方案
互斥锁(pthread) 需要把互斥锁放在共享内存中 多线程+多进程混合场景
文件锁 flockfcntl 简单场景,但性能差
原子操作 用GCC内置的__sync_fetch_and_add 简单计数器,性能最好

我的经验:如果只是传递少量数据(比如一个状态标志),用原子操作就够了。但如果是传递大块数据(比如视频帧),我建议用信号量配合双缓冲——一个缓冲区在写,另一个在读,用信号量切换。这样读写互不干扰。

16.6 共享内存的生命周期管理

共享内存不像普通内存,进程退出后它不会自动消失。你得手动删除,否则它会一直留在系统里,直到重启。

查看系统里有哪些共享内存:

ipcs -m

删除指定的共享内存:

ipcrm -m shmid

或者在代码里用shmctl()删除:

shmctl(shmid, IPC_RMID, NULL);

我曾经踩过的坑:有一次在服务器上调试程序,频繁创建共享内存但忘了删除。结果系统报"无法分配共享内存",一查发现共享内存段数量达到了系统上限。用ipcs -m一看,好家伙,几十个废弃的共享内存段挂在那里。从那以后,我养成了习惯——在程序退出时,由最后一个使用者负责清理。

16.7 共享内存 vs 其他IPC方式

我把几种常见的IPC方式做了个对比,方便你选型:

方式 速度 是否需要同步 典型场景
共享内存 最快 大数据量、高频通信
管道/命名管道 中等 否(自带同步) 流式数据、简单通信
消息队列 较慢 否(自带同步) 消息传递、解耦
Socket 较慢 否(自带同步) 跨网络通信

说白了,共享内存就是「用复杂度换速度」。你得到了最高的性能,但必须自己处理同步、生命周期、死锁等问题。如果你不需要那么高的性能,用管道或消息队列会更省心。

总结一下:共享内存是Linux下最快的IPC方式,核心函数就三个——shmget创建/获取、shmat映射、shmdt解除映射。同步问题必须用信号量或互斥锁解决,别指望共享内存自己会帮你做。最后,记得清理——ipcrmshmctl是你的好帮手。


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