马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
一、共享内存是什么:多进程之间的一块公共物理内存
进程间通信(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 读同一段。上面为了演示把写和释放写在同一进程里,真实场景是要拆成两个可执行文件的。
八、共享内存的正确打开方式
- 大小取 4KB 整数倍:内核按页分配,非整数倍会向上取整。
- 一定要配同步:共享内存本身没有锁。实用组合是共享内存传数据 + 信号量/读写锁传"可以读了"这个信号,或者干脆用 POSIX 的
shm_open + 匿名 mmap 配合 semaph 系列。
- 记得释放:程序退出路径(含异常分支)都要走到
shmctl(IPC_RMID),否则对象会残留在内核里。
- 权限位要给够:
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 编译。) |