2. 共享内存基础:System V共享内存与POSIX共享内存对比、shmget/shmat/shmdt接口详解

共享内存,说白了就是让两个或多个进程直接读写同一块物理内存。这是IPC(进程间通信)里速度最快的方式——没有中间商赚差价,数据不用在内核和用户态之间来回拷贝。

我刚开始做多进程开发时,总觉得消息队列和管道就够用了。直到有一次做一个实时行情处理系统,数据量一上来,消息队列的拷贝开销直接让CPU飙到80%。嗯,从那以后,我对共享内存就「真香」了。

2.1 System V 共享内存 vs POSIX 共享内存

Linux下有两套共享内存的API:一套是古老的System V,一套是较新的POSIX。我个人习惯,新项目用POSIX,维护老代码时经常碰到System V。

对比维度 System V 共享内存 POSIX 共享内存
接口风格 shmget / shmat / shmdt shm_open / mmap / munmap
标识方式 整数 key(ftok生成) 文件名(/dev/shm下的文件)
生命周期 内核持久化,直到显式删除 文件持久化,进程退出后仍存在
大小限制 SHMMAX 系统参数控制 受文件系统大小限制
可移植性 所有Unix都支持 Linux/macOS支持,Windows有限

核心区别一句话:System V用整数key找共享内存,POSIX用文件名找。POSIX的接口更接近文件操作,我个人觉得更自然一些。

2.2 shmget —— 创建或获取共享内存

shmget是System V共享内存的入口函数。你想想看,进程之间要共享一块内存,总得先有个「门牌号」吧?shmget就是干这个的。

#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
// 返回值:成功返回共享内存ID,失败返回-1

参数详解:

  • key:共享内存的标识符。通常用ftok()生成,也可以传IPC_PRIVATE(创建私有共享内存)。
  • size:共享内存的大小(字节)。创建时必须指定,获取时传0即可。
  • shmflg:权限标志。IPC_CREAT表示创建,IPC_EXCL表示排他创建,后面跟八进制权限(如0666)。

我的习惯:创建共享内存时,我一般用 IPC_CREAT | 0666。0666表示所有用户可读写,省得后面权限出问题。生产环境记得收紧权限。

常见用法:

// 创建一块1024字节的共享内存
key_t key = ftok("/tmp/shmfile", 'A');
int shmid = shmget(key, 1024, IPC_CREAT | 0666);
if (shmid == -1) {
    perror("shmget failed");
    exit(1);
}

我曾经踩过的坑:ftok的路径必须存在且可访问,否则返回的key是-1。有一次我路径写错了,排查了半天才发现是ftok失败了。建议用绝对路径,比如/tmp/xxx。

2.3 shmat —— 把共享内存挂到进程地址空间

shmget只是拿到了一个ID,就像你拿到了一个仓库的钥匙。但你还得把仓库「映射」到你的院子里才能用。shmat就是干这个的。

void *shmat(int shmid, const void *shmaddr, int shmflg);
// 返回值:成功返回映射地址,失败返回(void*)-1

参数详解:

  • shmid:shmget返回的共享内存ID。
  • shmaddr:建议的映射地址。传NULL让系统自动选,我建议永远传NULL。
  • shmflg:标志位。SHM_RDONLY表示只读映射,0表示读写映射。
// 把共享内存挂到进程地址空间
char *data = (char *)shmat(shmid, NULL, 0);
if (data == (void *)-1) {
    perror("shmat failed");
    exit(1);
}

// 现在可以直接读写data指向的内存了
strcpy(data, "Hello from process A!");

注意:shmat返回的地址是虚拟地址,不同进程里这个地址可能不一样。但没关系,它们指向的是同一块物理内存。这就是共享内存的魔力。

2.4 shmdt —— 分离共享内存

用完了共享内存,记得「分离」。shmdt不会删除共享内存,只是让当前进程不再能访问它。

int shmdt(const void *shmaddr);
// 返回值:成功返回0,失败返回-1
// 分离共享内存
if (shmdt(data) == -1) {
    perror("shmdt failed");
    exit(1);
}

我的建议:进程退出前一定要调用shmdt。虽然进程退出时系统会自动分离,但显式调用是个好习惯。我曾经见过一个程序,子进程异常退出没分离,导致共享内存泄漏——虽然内存还在,但引用计数乱了。

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

下面是一个完整的例子。进程A写入数据,进程B读取数据。你想想看,这比管道和消息队列简单多了吧?

进程A(写入者):

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

int main() {
    key_t key = ftok("/tmp/shmfile", 'A');
    int shmid = shmget(key, 1024, IPC_CREAT | 0666);
    
    char *data = (char *)shmat(shmid, NULL, 0);
    strcpy(data, "Hello from process A!");
    printf("Process A wrote: %s\n", data);
    
    shmdt(data);
    return 0;
}

进程B(读取者):

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

int main() {
    key_t key = ftok("/tmp/shmfile", 'A');
    int shmid = shmget(key, 1024, 0666);  // 注意:这里没有IPC_CREAT
    
    char *data = (char *)shmat(shmid, NULL, 0);
    printf("Process B read: %s\n", data);
    
    shmdt(data);
    return 0;
}

我曾经踩过的坑:进程B读取时,如果进程A还没写入,进程B读到的是未初始化的数据(全是0或者垃圾值)。所以共享内存通常需要配合信号量或互斥锁来同步。这个我们后面章节会详细讲。

2.6 删除共享内存

共享内存不会因为进程退出而自动删除。你得手动删,不然它一直占着内存。

// 用shmctl删除共享内存
shmctl(shmid, IPC_RMID, NULL);

也可以用命令行工具:

ipcs -m        # 查看所有共享内存
ipcrm -m shmid # 删除指定共享内存

我的习惯:开发阶段我经常用 ipcrm -a 清掉所有共享内存。但生产环境千万别这么干!我建议在程序里用atexit注册一个清理函数,确保退出时删除共享内存。

2.7 小结

嗯,到这里你应该对System V共享内存有了基本了解。说白了就是三步走:shmget创建、shmat映射、shmdt分离。最后别忘了shmctl删除。

POSIX共享内存的接口我们后面章节再细讲。但核心思想是一样的——让多个进程直接读写同一块物理内存,实现最高效的IPC。

下一章我们会讲共享内存的同步问题。你想想看,多个进程同时写一块内存,不出乱子才怪。到时候我会分享我在项目中用信号量做同步的实战经验。