Linux 共享内存介绍
共享内存属于一种进程通信方式,让多个进程通过访问同一块内存实现通信。

整体流程与特点
流程
总的流程分为以下几步:已存在的共享内存通过 Key 直接用即可,不用重新申请。

需要先申请共享内存,然后与它建立联系,不想用了就去除关联,最后看情况释放共享内存即可。
特点
- 共享内存申请之后,如果不主动释放,那它就会等到系统关闭才主动释放。
- 共享内存的通信不会出现阻塞的情况(这是较于两种管道通信最大的区别)。即:可以同时实现数据写入,A 进程不用阻塞等待 B 进程。
- 通信成功的关键是:读写双方必须约定相同的数据类型和解析规则。例如:A 以字符串写入,B 就以字符串读取,如果 B 是整型读取,就会出现乱码。
- 共享内存的释放:由创建者销毁,且创建者标记'销毁'之后,也会等失去所有挂载连接之后才会释放这块共享内存。下面是指令版的查看共享内存和销毁:
ipcs -m 查看所有存在的共享内存
ipcrm -m "共享 ID" 手动直接销毁该共享内存
使用步骤
1. 申请共享内存
共享内存的申请涉及到两个函数:ftok() 与 shmget()。
1. ftok()
函数原型:
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
参数:都是自定义的,系统会根据它们调用算法形成唯一的 Key,这个 Key 是较于操作系统的。(只要这两个参数相同,就会生成相同的 Key->进而找到同一块共享内存)
返回值:
- 成功:返回一个
key_t类型的整数(就是生成的'唯一编号') - 失败:返回
-1(比如路径不存在,或权限不够)
作用:生成'唯一标识'较于系统的钥匙 Key。
2. shmget()
函数原型:
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数: 第一个参数:通过调用 ftok() 获取的较于操作系统的 Key。 第二个参数:你理想的共享内存大小(每次最好是 4KB,因为'块'以'4KB'为整数标准)。 第三个参数:操作标志(权限 + 行为),常用组合:(使用新内存或者旧内存!)
IPC_CREAT | 0666:如果 key 对应的共享内存不存在,就新建一个,权限设为 666(所有人可读写,类似文件权限)IPC_EXCL | IPC_CREAT:如果共享内存已存在,就报错(确保一定是新建的,避免误操作已有内存)- 单独传 0:只查找已存在的共享内存,不新建
作用:生成'唯一标识'较于用户的钥匙 ID(其实也算'Key',只是对于用户层面的)。
返回值:
- 成功:返回一个'共享内存 ID'(非负整数,后续操作共享内存都用这个 ID)
- 失败:返回
-1(比如权限不够、内存不足、key 不存在且没加 IPC_CREAT)
2. 挂接
挂接:与该共享内存建立联系,比如你想怎么使用,需要用到接口(shmat())。
函数原型:(需要强转)
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数: 第一个参数:'共享内存 ID'。 第二个参数:告诉操作系统挂载到共享内存的哪个地方,一般推荐 NULL 参数。 第三个参数:挂载权限,一般选择 0(可读可写)。
返回值:
- 成功:返回共享内存在当前进程地址空间中的实际挂载地址(
void*类型,可直接作为指针使用) - 失败:
-1(可以通过检查 errno 获取错误原因,如地址冲突、权限不足等)
作用:搭建'窗口',真正使用共享内存。
3. 去关联
即失去与该共享内存的关联,代表不再使用它。
函数原型:
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数:通过 shmat() 挂接该共享内存的指针。
返回值:
- 成功:返回 0(表示已成功解除关联)
- 失败:返回
-1(可通过 errno 查看错误原因,例如 shmaddr 不是有效的挂载地址、权限不足等)
作用:失去与该共享内存的关联。
4. 释放共享内存
函数原型:
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数: 第一个参数:'共享内存 ID'。 第二个参数:对共享内存执行的操作(我们暂时学习两个即可)。
| 命令 | 作用 |
|---|---|
| IPC_STAT | 读取共享内存的状态信息,存储到 buf 指向的 struct shmid_ds 结构体中 |
| IPC_RMID | 标记共享内存为'待销毁',当所有进程都卸载后,内核会彻底删除它 |
第三个参数: buf 是一个指向 struct shmid_ds 类型的指针,该结构体用于存储或设置共享内存的详细信息。
- 当 cmd=IPC_STAT 时:buf 用于接收状态(内核会将共享内存的信息写入 buf)
- 当 cmd=IPC_RMID 时:buf 无用,通常传 NULL 即可
返回值:
- 成功:返回 0(无论执行哪种 cmd,成功均返回 0)
- 失败:返回
-1(可通过 errno 查看错误原因,如权限不足、shmid 无效等)
作用:对共享内存执行的操作。
5. 示例
我们将 Key 的获取封装一下:
const char* ptr_ftok="Hello Linux";
const int pid_ftok = 1024;

创建共享内存:此处省略相关判断逻辑。

使用共享内存:此处省略相关判断逻辑。

共享内存效率与内核
为何是这样?共享内存与文件描述表都是在进程地址空间上的,可以直接跳过文件表访问共享内存。

信号量与高效
什么是'互斥'?
当共享内存被两个进程访问时,如果 A 需要持续写入 3 秒的数据,但是 B 进程在二秒的时候就进行了读取,这就导致通信结果不理想,因此为了避免这种情况,任何时刻,只能一个进程(属于执行流)访问该共享资源。
什么是'临界资源'?
被多个执行流(进程、线程)都能访问的共享资源,而这些资源,通常是代码、数据。
什么是'临界区'?
简单来说:临界区就是执行流正在访问共享资源中的代码数据。
因此就算有 100 行代码在'临界资源',但是被访问的那 5 行才叫临界区。
什么是'信号量'?
信号量是一个计数器,来记录该'临界资源'还可以被多少执行流访问,通常执行两个操作:
'P'操作:有执行流访问临界资源,计数 -1
'V'操作:该执行流退出访问临界资源,计数器 +1
什么是'原子性'?
'原子性'是较于'P'和'V'操作的执行特性:要么不执行,要么执行完。
什么是'二元信号量'?
如果信号量的计数器只能是 0(资源无法使用)或 1(可使用),更像资源的使用状态!
**效率:**信号量(可调用共享资源的执行流数量)+'原子性'——便形成了'互斥',共享访问达成高效。
(注:'信号量'的学习与后面的'信号'无关)

