【Linux】详解如何利用共享内存实现进程间通信

[复制链接]
发表于 2026-10-3 16:14:13 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×

一、共享内存是什么:多进程之间的一块公共物理内存

进程间通信(IPC)有好几种套路:管道要拷贝、消息队列有内核拷贝开销、信号只能传个信号量的数。共享内存是其中最"快"的一种,原因只有一条:数据不需要在不同进程之间拷贝。

共享内存(Shared Memory)是多进程间共享的那一部分物理内存。它允许多个进程访问同一块内存空间,从而在不同进程之间共享和传递数据。

实现方式很直白:操作系统把一段物理内存映射到多个进程的地址空间中。

  • 一个进程可以创建一个共享内存段,并将该段连接到(attach 到)自己的地址空间。
  • 其他进程也可以把这段共享内存连接到它们自己的地址空间中。
  • 于是所有进程访问的是同一段物理内存,一个进程写进去,另一个进程立刻就能读到。

代价也要说清楚:正因为没有内核参与中转,共享内存本身不提供任何同步机制。多个进程同时写一个地址就会互相覆盖,读写顺序错乱也不会报错——这就必须自己配信号量(通常是信号量集)来做互斥与同步。

二、内核怎么管共享内存

在内核中共享内存可以存在许多个,所以操作系统必须先创建描述共享内存的结构体,再把这些结构体组织起来管理(内核里通常用数组/哈希表按 key 索引)。

为了保证两个或者多个进程看到的是同一个共享内存,就要给每一个共享内存提供唯一性的标识——这就是 key。

key(整数,用户指定) → 内核中唯一的共享内存对象
shmid(内核返回的描述符) → 后续操作的句柄

这里有个容易混的概念:key 是用户空间约定的"名字",多个进程只要约定好同样的 key 就能找到同一块内存;shmid 是内核返回的标识符,只在当前系统内有效。

三、创建共享内存:shmget 的两个常见选项

创建共享内存的方法是 shmget,函数原型大致是:

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

三个参数逐个看:

  • key:共享内存在内核中的唯一标识,通常来自 ftok()。
  • size:要设置的共享内存的大小。在内核中,共享内存是以 4KB 为基本单位的,所以分配大小时最好也是 4KB 的整数倍,避免 kernel 帮你向上取整带来困惑。
  • shmflg:标志位。

shmflg 最常见的两个选项

IPC_CREAT      如果共享内存不存在,就创建之;如果已经存在,直接获取它
IPC_EXCL       不能单独使用,没意义(单独用等于普通打开,还多一位运算)
IPC_CREAT|IPC_EXCL   不存在就创建;已存在则报错返回 -1

重点记住组合用法: IPC_CREAT|IPC_EXCL 的语义是"如果不存在就创建,如果已存在就出错返回"。所以用它创建成功的一定是一个新的共享内存——这个特性常用来做"检测是否已有实例在跑"的判断:调用返回 -1 且 errno 为 EEXIST,就说明这块共享内存已经有人建过了。

如果 shmget 成功获取或创建了共享内存段,它会返回一个非负整数,这个整数就是共享内存段的标识符(shmid,也叫共享内存段的 ID)。这个标识符在后续的 shmat 和 shmdt 操作中会被使用。

shmget(...)  →  shmid(非负整数)
shmat(shmid, ...)  →  挂接到本进程地址空间的起始虚拟地址
shmdt(addr)        →  脱离
shmctl(shmid, IPC_RMID, ...)  →  标记删除

挂接与脱离

  • shmat:把共享内存段连接到调用进程的地址空间,返回指向该段的指针,用 (void*)-1 判断失败。
  • shmdt:把之前 shmat 返回的指针从本进程地址空间摘下来,注意它接受的是指针不是 shmid。
  • 真正释放要用 shmctl(shmid, IPC_RMID, NULL);shmdt 只是断开映射,内存还在。

四、key 从哪来:ftok

key 的获取通常不用自己瞎编一个整数,而是用 ftok:

key_t ftok(const char *pathname, int proj_id);

这里的 pathname 是一串已存在的文件路径,proj_id 是一个整数(通常只取低 8 位,建议传 1–255 的字符,比如 'a')。这两个参数由用户随意指定,操作系统底层通过特定的算法帮我们形成一个 key 值。

  • 形成失败返回 -1(比如 pathname 指向的文件不存在)。
  • 成功则把这个 key 值设置进描述共享内存的结构体中,用来标识这块共享内存的唯一性。
  • 给两个或者多个进程传入同样的 pathname 和 proj_id,就能让它们看到同一块共享内存。

实践中一个稳妥做法是:所有调用方统一用同一个真实存在的路径(比如 /etc/passwd 或项目里的某个固定文件)加同一个 proj_id,这样不会写错。

五、查看系统里的共享内存:ipcs

用 ipcs 指令可以查看系统中指定用户创建的共享内存、消息队列和信号量三类 IPC 对象。

ipcs -m    # 查看系统中指定用户创建的共享内存
ipcs -q    # 查看系统中指定用户创建的消息队列
ipcs -s    # 查看系统中指定用户创建的信号量

输出里通常包含 key、shmid、owner(属主)、perms(权限)、size(大小)、status(状态)几列。排查"为什么我的程序连不上那块内存"时,第一步就是 ipcs -m 看它还在不在、属主是不是自己。

六、删除共享内存:ipcrm

ipcrm -m shmid(共享内存 id)    # 删除用户指定的共享内存

注意 ipcrm -m 后面跟的是 shmid 而不是 key,用之前先从 ipcs -m 里确认。

还有一个线上高频问题:程序异常退出后共享内存没被回收,导致再次启动创建失败。解决办法通常是在 shmget 时改用 IPC_CREAT|IPC_EXCL 检测残留(EEXIST 就说明有孤儿对象),或者老老实实用 ipcrm -m 清掉。

Ipcs 输出示例

------ Shared Memory Segments --------
key        shmid   owner  perms  bytes  nattch  status
0x01020304 65538   root   600    4096   0                 

七、最小可用示例

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

int main()
{
    key_t key = ftok("/etc/passwd", 'a');
    if (key == -1) { perror("ftok"); return 1; }

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

    char *p = (char*)shmat(shmid, NULL, 0);
    if (p == (char*)-1) { perror("shmat"); return 1; }

    sprintf(p, "hello from another process");   // 写共享内存

    shmdt(p);
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

实际使用时标准姿势是:写端写完用 shmdt 脱离,读端随后attach 读同一段。上面为了演示把写和释放写在同一进程里,真实场景是要拆成两个可执行文件的。

八、共享内存的正确打开方式

  1. 大小取 4KB 整数倍:内核按页分配,非整数倍会向上取整。
  2. 一定要配同步:共享内存本身没有锁。实用组合是共享内存传数据 + 信号量/读写锁传"可以读了"这个信号,或者干脆用 POSIX 的 shm_open + 匿名 mmap 配合 semaph 系列。
  3. 记得释放:程序退出路径(含异常分支)都要走到 shmctl(IPC_RMID),否则对象会残留在内核里。
  4. 权限位要给够:shmget 的 shmflg 里捎带权限(如 0666 / 0600),不同用户之间访问会被 perms 拦住。

小结

共享内存这套 API 的内核线很简单:

  • ftok(pathname, proj_id) → 生成 key,多进程用同一对参数即可对齐同一块内存;
  • shmget(key, size, shmflg) → 拿/建共享内存,返回 shmid;IPC_CREAT|IPC_EXCL 可保证"一定是新的";size 取 4KB 整数倍;
  • shmat(shmid,...) → 挂接到本进程地址空间,拿到指针;shmdt(指针) 脱离;shmctl(shmid, IPC_RMID) 删除;
  • ipcs -m 查、ipcrm -m shmid 删;
  • 没有内建同步,共享内存负责快,信号量负责稳,两者一起才是一套可用的 IPC。

(本文由 qidao123.com企服评测-应用市场 编辑提示整理:示例基于 System V IPC 的 SysV 共享内存接口,适用于 Linux / Unix 环境下的 GCC 编译。)

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表