【Linux系统】进程间通信:System V IPC——共享内存
前文中我们介绍了管道——匿名管道和命名管道来实现进程间通信,在介绍怎么进行通信时,我们有提到过不止管道的方式进行通信,还有System V IPC,今天这篇文章我们就来学习一下System V IPC中的共享内存
1. 为何引入共享内存?——管道通信的局限性
管道(匿名/命名管道)作为传统IPC机制存在显著缺陷:
- 数据拷贝开销大:管道需通过内核缓冲区中转,数据需从用户空间→内核→用户空间两次拷贝
- 单向通信限制:匿名管道仅支持单向数据流,双向通信需建立两个管道
- 效率瓶颈:频繁读写时内核缓冲区切换成为性能瓶颈
- 适用场景有限:命名管道虽突破亲缘关系限制,但仍依赖文件系统路径,且同步机制弱
⚡ 共享内存的破局:
通过多进程直接访问同一物理内存区域,消除数据拷贝,实现零复制(Zero-Copy)通信,速度提升10-100倍
2. 共享内存核心概念:打破进程隔离的革命性设计
共享内存是 System V IPC(Inter-Process Communication)机制的一种,它允许多个不相关的进程(父子进程或完全独立的进程)访问同一块物理内存区域。这是最快的进程间通信(IPC)形式,因为它完全避免了内核空间和用户空间之间数据的复制。
本质定义
物理内存共享:多个进程通过页表映射,直接访问同一块物理内存区域,实现零拷贝数据交换。
- 底层实现:操作系统内核维护共享内存区域,各进程通过修改自身的页表项(Page Table Entry),将虚拟地址映射到相同的物理页帧(Page Frame)上
- 典型场景:适用于大数据量进程间通信,如视频处理管道中,解码进程直接将帧数据写入共享内存,渲染进程立即读取
- 对比传统IPC:相比管道/消息队列需要2次数据拷贝(用户态→内核态→用户态),共享内存仅需1次虚拟地址映射
逻辑视图:在进程虚拟地址空间中表现为普通内存段(如malloc分配),实则由操作系统管理共享物理页。
- 地址空间布局:通常位于堆与栈之间的内存映射区域(mmap区域)
- API抽象:通过
shmget
创建、shmat
附加后,进程可通过指针直接读写,如:
int *shared_counter = (int*)shmat(shm_id, NULL, 0);
*shared_counter += 1; // 修改对其他进程立即可见
核心特性
特性 | 技术内涵 | |
---|---|---|
高效性 | 消除内核中转与数据拷贝,吞吐量达管道通信的 5-20倍(GB/s级) | |
双向性 | 支持多进程并发读写(需同步机制保障) | |
非亲缘性 | 任意进程(无关父子关系)可通过唯一标识符(Key)访问 | |
持久性 | 生命周期独立于进程,需显式销毁(否则残留内核直至重启) | |
无内置同步 | 需开发者结合信号量/互斥锁解决竞态条件(如写覆盖、脏读) |
与进程地址空间的融合
// 进程视角:共享内存如同本地变量
char *shm_ptr = shmat(shm_id, NULL, 0); // 映射共享内存到虚拟地址空间
strcpy(shm_ptr, "Hello from Process A"); // 直接写入
关键理解:
- 进程通过
shmat
将物理共享页插入自身页表(虚拟→物理映射)。- 修改操作直接作用于物理内存,其他进程立即可见。
3. 共享内存工作原理
那操作系统是怎么管理共享内存的呢?先描述再组织
通过一个内核结构体来描述共享内存,再由操作系统统一管理这些内核结构体
共享内存数据结构:
struct shmid_ds {struct ipc_perm shm_perm; /* operation perms */int shm_segsz; /* size of segment (bytes) */__kernel_time_t shm_atime; /* last attach time */__kernel_time_t shm_dtime; /* last detach time */__kernel_time_t shm_ctime; /* last change time */__kernel_ipc_pid_t shm_cpid; /* pid of creator */__kernel_ipc_pid_t shm_lpid; /* pid of last operator */unsigned short shm_nattch; /* no. of current attaches */unsigned short shm_unused; /* compatibility */void shm_unused2; /* ditto - used by DIPC */void shm_unused3; /* unused */
};
一、管理机制:描述与组织的双重架构
1. 描述层:内核数据结构定义
每个共享内存段由两个关键结构体描述:
struct shmid_ds
(用户可见元信息)
用户提供的结构体包含基础属性,但内核实际使用扩展结构体:struct shmid_ds {struct ipc_perm shm_perm; // 权限控制(UID/GID/模式)size_t shm_segsz; // 段大小(字节)time_t shm_atime; // 最后一次映射时间time_t shm_dtime; // 最后一次解除映射时间time_t shm_ctime; // 最后一次修改时间pid_t shm_cpid; // 创建者PIDpid_t shm_lpid; // 最后一次操作者PIDunsigned short shm_nattch; // 当前映射进程数// ... 兼容性保留字段 };
struct shmid_kernel
(内核私有管理结构)struct shmid_kernel {struct kern_ipc_perm shm_perm; // IPC权限控制块struct file *shm_file; // 关联的shm文件对象unsigned long shm_nattch; // 映射计数size_t shm_segsz; // 段大小struct pid *shm_cprid; // 创建者PID(内核态)struct pid *shm_lprid; // 最后操作者PID// ... 其他内核级字段 };
关键扩展:
shm_file
:指向虚拟文件系统shm
中的文件对象,实现物理内存与文件系统的关联。kern_ipc_perm
:嵌入的IPC权限控制块,包含键值(key)、所有者UID等。
2. 组织层:全局管理架构
内核通过三级结构统一管理所有共享内存段:
层级 | 数据结构 | 功能 |
---|---|---|
全局入口 | struct ipc_ids shm_ids | 维护系统内所有共享内存的ID空间 |
ID索引层 | struct kern_ipc_perm*[] | 指针数组,每个元素指向一个shmid_kernel |
共享内存实例 | struct shmid_kernel | 描述单个共享内存段的完整状态 |
动态管理演进:
- 早期内核:静态数组管理(固定数量上限,)。
- 现代内核: 动态红黑树(Red-Black Tree) ,支持O(log N)复杂度的查找/插入/删除。
二、内核操作流程剖析
1. 创建共享内存(shmget
)
int shmget(key_t key, size_t size, int shmflg) {// 1. 根据key查找或新建shmid_kernel// 2. 在shm文件系统中创建匿名文件struct file *file = shmem_file_setup("SYSV<key>", size, flags);// 3. 初始化shmid_kernel:绑定file,设置size/权限等// 4. 将shmid_kernel插入全局红黑树
}
关键动作:
- 通过
shmem_file_setup
在tmpfs中创建虚拟文件。 - 文件操作函数集指向
shmem_vm_ops
,实现物理页帧分配。
2. 映射共享内存(shmat
)
void *shmat(int shmid, void *addr, int flag) {// 1. 根据shmid找到shmid_kernelstruct shmid_kernel *shp = find_shm(shmid);// 2. 在进程地址空间创建VMA区域vma = vm_area_alloc(current->mm);vma->vm_file = shp->shm_file; // 关联shm文件vma->vm_ops = &shmem_vm_ops; // 设置内存操作函数// 3. 更新shm_nattch引用计数shp->shm_nattch++;
}
虚拟内存映射:
- 进程的
vm_area_struct
映射到shm_file
的物理页。 - 页表项(PTE)指向共享物理帧,实现零拷贝访问。
3. 生命周期管理
操作 | 内核行为 |
---|---|
删除(shmctl(IPC_RMID) ) | 标记为SHM_DEST ,当shm_nattch=0 时触发物理内存回收 |
进程退出 | 自动调用shmdt 解除映射,递减shm_nattch |
系统重启 | 所有共享内存被销毁(因物理内存重置) |
三、物理内存与虚拟地址的协同管理
1. 物理内存分配
- 首次访问触发缺页异常:
进程读写映射的虚拟地址 → 缺页中断 → 内核调用shmem_fault
分配物理页帧。 - 页帧来源:内核伙伴系统(Buddy System)分配连续物理页。
2. 多进程共享的一致性
机制 | 原理 |
---|---|
写时复制(COW) | 若进程尝试写入只读映射的共享内存,触发COW生成私有副本 |
内存屏障 | 使用mb()/rmb() 指令保证多核CPU缓存一致性 |
原子操作 | 引用计数(如shm_nattch )通过原子指令增减 |
四、与传统文件映射的差异
特性 | 共享内存 | 文件映射(mmap) |
---|---|---|
数据持久性 | 进程退出后数据消失 | 文件内容持久化到磁盘 |
同步机制 | 需手动同步(如msync) | 内核自动回写脏页 |
初始化成本 | 无磁盘I/O | 需加载文件数据到内存 |
适用场景 | 高频临时数据交换 | 持久化数据共享 |
五、设计哲学总结
- 抽象与隔离:
- 通过
shmid_ds
向用户暴露可控接口,隐藏shmid_kernel
等内核细节。
- 通过
- 零拷贝思想:
- 虚拟地址直接映射物理帧,消除数据复制。
- 动态扩展性:
- 红黑树管理替代静态数组,支持海量共享内存段。
- 资源自治:
- 引用计数(
shm_nattch
)实现自销毁机制,避免资源泄漏。
- 引用计数(
共享内存的工作原理 (关键步骤)
创建或获取共享内存段 (
shmget
):一个进程(通常是第一个需要该共享内存的进程)调用
shmget(key_t key, size_t size, int shmflg)
。key
: 一个唯一标识共享内存段的键值。可以使用ftok()
基于路径名生成,或者指定为IPC_PRIVATE
(创建仅供亲缘进程使用的新段)。size
: 请求的共享内存段的大小(字节)。如果是获取已存在的段,此参数通常为 0。shmflg
: 标志位,指定创建选项(IPC_CREAT
,IPC_EXCL
)和权限(如0666
)。成功时返回共享内存标识符
shmid
(一个非负整数),用于后续操作。内核在内存中分配一块指定大小的物理内存区域。
将共享内存段附加到进程地址空间 (
shmat
):任何需要使用该共享内存的进程调用
shmat(int shmid, const void *shmaddr, int shmflg)
。shmid
: 由shmget
返回的标识符。shmaddr
: 通常设为NULL
,让内核选择附加地址。也可以指定一个地址(但需谨慎,通常不推荐)。shmflg
: 标志位(如SHM_RDONLY
表示只读附加)。成功时返回一个指向共享内存段在当前进程地址空间中起始位置的
void*
指针。进程现在可以通过这个指针像访问普通内存一样读写共享内存区域。
使用共享内存:
多个进程通过它们各自
shmat
返回的指针(指向同一物理内存的不同虚拟地址)直接读写共享内存区域。关键点:共享内存本身不提供任何同步机制! 多个进程同时读写同一区域会导致数据竞争(Race Condition) 和数据不一致。必须结合其他同步机制使用:
System V 信号量 (
semget
,semop
,semctl
)POSIX 信号量 (
sem_init
,sem_wait
,sem_post
)互斥锁 (
pthread_mutex_t
) 和条件变量 (pthread_cond_t
)(需要放在共享内存中并初始化为进程间共享属性PTHREAD_PROCESS_SHARED
)。文件锁 (
fcntl
)
分离共享内存段 (
shmdt
):当进程不再需要访问共享内存时,调用
shmdt(const void *shmaddr)
。shmaddr
: 之前shmat
返回的指针。该调用将共享内存段从当前进程的地址空间中分离出去。进程不能再通过该指针访问共享内存。注意:分离操作并不会销毁共享内存段本身。
控制/销毁共享内存段 (
shmctl
):使用
shmctl(int shmid, int cmd, struct shmid_ds *buf)
进行控制操作。最重要的
cmd
是IPC_RMID
:标记共享内存段为待销毁。当最后一个使用该段的进程分离 (
shmdt
) 之后,内核才会真正销毁该段并回收资源。即使所有进程都已分离,但只要没有调用
IPC_RMID
,段依然存在(可能造成资源泄漏)。
其他
cmd
包括获取/设置段信息 (IPC_STAT
,IPC_SET
)。
4. 共享内存函数
4.1 shmget
函数核心解析(系统级共享内存管理)
shmget
是System V IPC中创建或获取共享内存段的核心函数,其本质是向内核申请一块多进程可共同访问的物理内存区域。
1. 函数原型与基础机制
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
- 返回值:成功返回共享内存标识符(非负整数),失败返回-1并设置
errno
- 内核行为:
- 根据
key
查找或创建共享内存段 - 分配物理内存并初始化元数据结构
shmid_ds
- 返回逻辑标识符(非物理地址)
- 根据
2. 参数解析
参数 | 技术内涵 | 内核行为 |
---|---|---|
key | 唯一标识符: • IPC_PRIVATE :强制创建新段• ftok() :基于文件路径+项目ID生成 | 红黑树检索 key ,存在则返回 shmid ;不存在且 IPC_CREAT 置位则创建新段 |
size | 内存段大小(字节): • 新创建时需 >0 • 自动对齐页大小(4KB) | 调用 shmem_file_setup() 在 tmpfs 创建匿名文件,映射物理页 |
shmflg | 位掩码标志: • 权限位:低9位(如 0666 )• IPC_CREAT :不存在则创建• IPC_EXCL :存在则报错 | 初始化 shmid_ds.shm_perm 结构,设置 UID/GID 和权限 |
高级标志:
SHM_HUGETLB
:使用2MB/1GB大页减少TLB MissSHM_NORESERVE
:不预留Swap空间(Linux特有)
3. 内核数据结构初始化
创建新段时,内核初始化struct shmid_ds
元数据结构:
struct shmid_ds {struct ipc_perm shm_perm; // 权限控制块size_t shm_segsz; // 段大小(=size参数)time_t shm_atime; // 最后一次attach时间time_t shm_dtime; // 最后一次detach时间time_t shm_ctime; // 最后一次修改时间pid_t shm_cpid; // 创建者PIDpid_t shm_lpid; // 最后操作者PIDunsigned short shm_nattch; // 当前附加进程数
};
初始化规则:
shm_perm.cuid/uid
= 调用进程有效UIDshm_perm.cgid/gid
= 调用进程有效GIDshm_perm.mode
=shmflg
的低9位权限shm_atime
/shm_dtime
= 0(未映射)shm_ctime
= 当前系统时间
💡 物理内存分配:内核调用
alloc_pages()
分配连续物理页,内容初始化为0
4. 错误处理
错误码 | 触发条件 | 解决方案 |
---|---|---|
EACCES | 权限不足 | 检查 shmflg 权限位 |
EEXIST | IPC_CREAT+IPC_EXCL 且段已存在 | 移除 IPC_EXCL 或更换 key |
EINVAL | size 无效(> SHMMAX 或 < 页大小) | 调整 size 为页大小整数倍 |
ENOENT | key 不存在且未设 IPC_CREAT | 增加 IPC_CREAT 标志 |
⚠️ 系统限制:
SHMMAX
:单段最大尺寸(默认32MB-128MB)SHMMNI
:系统最大段数(默认4096)
深入解析 shmget
的 key
参数:跨进程共享内存的标识核心
一、key
参数的核心作用与设计哲学
key
是 shmget
函数中唯一标识共享内存段的整数标识符,其本质是操作系统用于区分不同共享内存段的全局键值。它的作用类似于文件系统中的路径名,但以整数形式存在,核心价值在于:
- 跨进程标识:不同进程通过相同
key
访问同一物理内存区域。 - 资源复用:避免重复创建相同内存段,减少资源浪费。
- 权限控制:与
shmflg
权限位协同管理进程访问权限。
设计哲学:
key
体现了操作系统对共享资源的 “命名空间抽象” —— 用轻量级整数替代复杂路径,实现高效资源定位。
二、key
的生成方式与典型场景(附代码示例)
1. ftok()
动态生成(推荐方案)
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
- 机制:基于文件路径(
pathname
)和项目ID(proj_id
)生成唯一key
。 - 原理:
取文件索引节点号(st_ino
)的低8位 + 设备号(st_dev
)的低8位 +proj_id
的低8位,组合成32位整数。 - 示例:
// 服务端创建共享内存 key_t config_key = ftok("/etc/app_config", 123); // 基于配置文件生成key int shmid = shmget(config_key, 4096, IPC_CREAT | 0666);// 客户端访问同一内存 key_t client_key = ftok("/etc/app_config", 123); // 相同参数生成相同key int client_shmid = shmget(client_key, 0, 0); // size=0表示获取已有段
2. 硬编码常量(简单场景)
#define APP_SHM_KEY 0x1234 // 预定义全局常量// 进程A
int shmid_A = shmget(APP_SHM_KEY, 1024, IPC_CREAT | 0600);// 进程B
int shmid_B = shmget(APP_SHM_KEY, 0, 0); // 通过相同key访问
风险:可能与其他应用冲突(需确保全局唯一性)。
3. 特殊值 IPC_PRIVATE
(私有段)
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600);
- 行为:强制创建新共享内存段,仅限亲缘进程使用(如
fork()
的子进程)。 - 典型场景:父进程创建临时共享区后
fork()
,子进程通过继承shmid
访问(无需key
)。
总结key
作用与本质
- 唯一标识:
key
是共享内存在系统中的全局唯一编号(类型为key_t
,本质是unsigned int
),用于区分不同共享内存段 。 - 进程间同步:不同进程通过相同
key
访问同一内存段,实现通信 。
- 唯一标识:
生成方式
ftok()
函数:常用方法,基于文件路径和项目ID生成唯一key。IPC_PRIVATE
:指定此值时,系统自动分配新key(用于父子进程间通信)。
使用场景
- 创建新内存段:当
key
不与现有段关联,且指定IPC_CREAT
标志时,系统创建新共享内存 。 - 访问现有段:若
key
已存在,则返回其标识符(shmid
),此时size
参数应为0 。
- 创建新内存段:当
权限与控制
- 权限位:
shmflg
的低9位定义权限(如0666
表示所有用户可读写)。 - 控制标志:
IPC_CREAT
:若内存段不存在则创建 。IPC_EXCL
:与IPC_CREAT
联用,若段已存在则返回错误 。
- 权限位:
错误处理
- 常见错误:
EACCES
:权限不足 。ENOENT
:key
不存在且未指定IPC_CREAT
。ENOMEM
:内存不足或超出系统限制(如Linux默认单段最大32MB)。
- 常见错误:
关键注意点
- 唯一性冲突:若不同应用误用相同
key
,会导致非预期通信。建议通过ftok
选择唯一文件路径 。 - 大小对齐:
size
会被对齐到系统页大小(如4KB)的整数倍 。 - 特殊值
IPC_PRIVATE
:仅适用于进程组内通信(如fork()
后的父子进程)。
4.2 shmat
函数:连接共享内存到进程地址空间
功能:将共享内存映射到进程的虚拟地址空间,使进程可访问共享数据。
原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid
:由shmget
返回的标识符。shmaddr
:指定连接地址:NULL
:系统自动选择合适地址(推荐)。- 非
NULL
:若未设置SHM_RND
,则直接使用该地址;若设置SHM_RND
,则地址自动向下对齐到SHMLBA
(通常为页大小)的整数倍。
shmflg
:模式标志:0
:读写模式。SHM_RDONLY
:只读模式。
返回值:
- 成功:返回共享内存首地址指针。
- 失败:返回
(void*)-1
并设置errno
。
4.3 shmdt
函数:断开共享内存连接
功能:将共享内存段从当前进程的地址空间分离(解除映射),但不会删除共享内存。
原型:
int shmdt(const void *shmaddr);
参数:
shmaddr
:由shmat
返回的地址指针。
返回值:
- 成功:返回
0
。 - 失败:返回
-1
并设置errno
。
- 成功:返回
底层机制:通过
do_munmap()
释放对应的虚拟内存区间。
4.4 shmctl
函数:控制共享内存
功能:管理共享内存段,包括删除、状态查询或权限修改。
原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid
:共享内存标识符。cmd
:控制命令:IPC_RMID
:标记删除共享内存。当所有进程均断开连接(shmdt
)后,内存才会被实际释放。IPC_STAT
:获取共享内存状态(保存到buf
指向的shmid_ds
结构体)。IPC_SET
:修改共享内存权限(需权限)。SHM_LOCK
/SHM_UNLOCK
:锁定内存禁止换页(仅限特权进程)。
buf
:指向shmid_ds
结构体的指针(用于输入/输出数据)。
返回值:
- 成功:返回
0
。 - 失败:返回
-1
并设置errno
.
- 成功:返回
4.5 拓展——命令行中如何实现上面系统调用函数相同的效果
1. ipcs
命令:查看共享内存信息(对应函数状态监控)
功能:
查看系统中所有共享内存段的状态(包括shmget
创建的共享内存),相当于通过shmctl(shmid, IPC_STAT, buf)
获取信息 。常用参数:
ipcs -m # 仅显示共享内存段信息
输出字段:
SHMID
:共享内存标识符(由shmget
返回的shmid
)KEY
:创建时指定的键值(如ftok
生成或IPC_PRIVATE
)OWNER
:创建者用户BYTES
:内存大小(与shmget
的size
参数一致)NATTCH
:当前挂载进程数(即通过shmat
连接的进程数)
高级用法:
ipcs -m -i <SHMID> # 查看指定SHMID的详细信息 ipcs -m -u # 汇总共享内存使用统计
2. ipcrm
命令:删除共享内存(对应 shmctl(shmid, IPC_RMID, NULL)
)
功能:
删除指定的共享内存段,效果等同于调用shmctl
的IPC_RMID
命令(标记删除,当所有进程调用shmdt
后实际释放内存)。语法:
ipcrm -m <SHMID> # 删除指定SHMID的共享内存
批量删除(根据用户或键值):
# 删除用户alice创建的所有共享内存 ipcs -m | awk '/alice/{print $2}' | xargs -n1 ipcrm -m# 删除键值为0x12345的共享内存 ipcs -m | awk '/0x12345/{system("ipcrm -m "$2)}'
⚠️ 需注意:删除时若仍有进程挂载(
NATTCH > 0
),内存不会立即释放,需等待所有进程调用shmdt
。
3. pmap
命令:查看进程挂载的共享内存(对应 shmat
映射)
功能:
显示进程虚拟地址空间中挂载的共享内存区域,相当于查看shmat
返回的映射地址 。语法:
pmap -x <PID> # 查看指定进程的内存映射
输出示例:
Address Kbytes RSS Mode Mapping 7f2a1a000000 1024 rw-s /SYSV00000000 # 共享内存标识(KEY为0x00000000)
rw-s
中的s
表示共享内存段/SYSV
后跟16进制键值(如00000000
对应IPC_PRIVATE
)
4. 挂载/卸载共享内存的替代方法
- 挂载(模拟
shmat
):
命令行无法直接挂载共享内存到进程空间,但可通过调试器临时操作:gdb -p <PID> -ex "call shmat(<SHMID>, NULL, 0)" --batch
此操作需进程主动配合,仅用于调试 。
- 卸载(模拟
shmdt
):
同样需在进程内部触发,无直接命令替代。可通过终止进程自动卸载(进程退出时会自动调用shmdt
)。
对比总结:函数与命令行操作对应关系
函数功能 | 命令行工具 | 关键参数/操作 | 限制说明 |
---|---|---|---|
创建共享内存 (shmget ) | 无直接替代 | – | 需编程实现 |
查看共享内存状态 (IPC_STAT ) | ipcs -m | -i <SHMID> 查看详情 | 信息只读,不可修改 |
删除共享内存 (IPC_RMID ) | ipcrm -m <SHMID> | 需指定SHMID | 需root或所有者权限 |
查看进程映射 (shmat 地址) | pmap -x <PID> | 过滤/SYSV 字段 | 仅显示地址,无法主动挂载 |
卸载共享内存 (shmdt ) | 终止进程 | kill <PID> | 进程退出时自动卸载 |
4.6 shmid和key的区别
1. 核心定义与功能层级
概念 | 功能描述 | 层级归属 | 类比关系 |
---|---|---|---|
key | 由 ftok() 生成或用户指定的整数值,用于在系统层面唯一标识共享内存段,内核通过 key 区分不同共享内存 | 内核层标识符 | 类似文件的 inode 号 (唯一标识文件) |
shmid | 由 shmget() 系统调用返回的整数值,作为用户层操作共享内存的句柄,用于后续关联、去关联或控制操作 | 用户层标识符 | 类似文件的 文件描述符 fd (用户操作接口) |
📌 关键引用:
- "
key
是内核用来区分共享内存唯一性的字段,用户不能直接用key
管理共享内存;shmid
是内核返回的标识符,用于用户级管理" 。- "
key
和shmid
的关系如同inode
和fd
:inode
标识文件唯一性,fd
是用户操作接口" 。
2. 生成与使用场景对比
(1) key
的生成与作用
- 生成方式:
- 通过
ftok(pathname, proj_id)
生成(如key_t key = ftok(".", 'a');
)。 - 或直接指定整数(如
key = 1234
),需确保系统内唯一性。
- 通过
- 核心作用:
- 在
shmget()
中作为参数,供内核查找或创建共享内存段 。 - 不同进程通过相同
key
访问同一共享内存(实现进程间通信)。
- 在
(2) shmid
的生成与作用
- 生成方式:
- 由
shmget(key, size, flags)
返回(如int shmid = shmget(key, 4096, IPC_CREAT|0666);
)。
- 由
- 核心作用:
- 作为用户层操作的入口参数,用于:
- 关联内存:
shmat(shmid, NULL, 0)
将共享内存映射到进程地址空间 。 - 去关联:
shmdt(shmaddr)
解除映射 。 - 控制操作:
shmctl(shmid, IPC_RMID, NULL)
删除共享内存 。 - 命令行工具操作:
ipcrm -m shmid
删除共享内存 。
- 关联内存:
- 作为用户层操作的入口参数,用于:
⚠️ 注意:
- 用户无法直接用
key
操作共享内存(如执行shmat(key, ...)
会报错)。- 所有用户层 API 均依赖
shmid
而非key
。
3. 设计目的与架构思想
维度 | key | shmid |
---|---|---|
唯一性范围 | 全局唯一(整个操作系统内) | 进程内有效(不同进程的 shmid 可能不同) |
生命周期 | 持久存在,直至共享内存被删除 | 随进程结束失效,但共享内存仍存留 |
安全隔离 | 内核维护,用户不可直接操作 | 用户直接使用,但无系统级权限 |
设计目标 | 解耦:内核通过 key 管理资源唯一性 | 封装:为用户提供安全操作接口 |
📜 架构意义:
- "内核通过
key
保证共享内存的全局唯一性,用户通过shmid
操作资源,实现内核与用户层的解耦" 。- 类似设计广泛见于系统资源管理(如信号量、消息队列)。
key
解决“资源是谁”的问题(系统唯一标识),shmid
解决“如何操作”的问题(用户接口)。
5. 代码示例
和管道一样,我们也来段代码加深对共享内存通信的理解。
这里我们和命名管道一样,实现让两个毫无关系的进程通信,所以我们将一个进程看作服务端,另一个进程看作客户端,然后进行封装。
我们先来介绍一段宏
宏定义结构
#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while (0)
do { ... } while (0)
:
这是一种宏定义的惯用技巧,目的是将多条语句封装为单条逻辑块。- 作用1:避免宏展开后与上下文的分号冲突(例如在
if-else
语句中使用时)。 - 作用2:确保宏在任何位置(如
if
分支后)都能安全使用,不会因缺少大括号导致逻辑错误。
- 作用1:避免宏展开后与上下文的分号冲突(例如在
反斜杠
\
:
用于连接多行代码,使宏定义可跨行书写,提高可读性。
核心功能
perror(m)
:
输出系统错误信息。参数m
是自定义的错误提示字符串(如"open error"
),实际输出格式为:m: 具体错误原因
。
例如:perror("open error")
可能输出open error: No such file or directory
。- 原理:
perror
会读取全局变量errno
的值,将其转换为可读的错误描述。
- 原理:
exit(EXIT_FAILURE)
:
立即终止程序,并返回预定义的失败状态码(通常为非0值)。EXIT_FAILURE
:标准宏,表示程序异常退出(值由系统定义,通常为1
)。- 对比
EXIT_SUCCESS
:表示程序正常退出(值为0
)。
该宏是C语言中处理系统调用错误的通用模式,通过perror
提供清晰的错误诊断,并通过exit(EXIT_FAILURE)
确保程序在致命错误时立即终止。其设计兼顾了安全性、可读性和可移植性。
5.1 创建并获取共享内存
我们让服务端创建共享内存段,客户端则获取共享内存段
但是创建共享内存段之前,得先将共享内存段的唯一标识符key生成,也就需要通过ftok函数来生成key,但是要基于文件路径(pathname
)和项目ID(proj_id
)生成唯一 key
。所以我们先定义全局变量pathname和proj_id,pathname为当前路径,proj_id则取66的十六进制,定义全局变量方便我们在服务端和客户端构造函数时通过传参来生成唯一key(注意:需要两者参数相同,才能获得同一个key,内核才能通过key找到同一个共享内存段)。服务端创建好共享内存段后,客户端就不再需要创建了,只需要获取即可,所以我们要实现二者隔离
代码如下:
#pragma once#include <iostream>
#include <cstdio>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while (0)const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int proj_id = 0x66;#define CREATER "creater"
#define USER "user"class Shm
{
private:int _shmid;int _size;key_t _key;std::string _usertype;void CreatShm(int flg){_shmid = shmget(_key, _size, flg);if(_shmid < 0){ERR_EXIT("shmget");}printf("shmid: %d\n", _shmid);}void Creat(){CreatShm(IPC_CREAT | IPC_EXCL | 0666);}void Get(){CreatShm(IPC_CREAT);}public:Shm(const std::string& pathneme, int projid, const std::string& usertype): _shmid(gdefaultid), _size(gsize), _usertype(usertype){_key = ftok(pathname.c_str(), projid);if (_key < 0){ERR_EXIT("ftok");}printf("key: 0x%x\n", _key);if(_usertype == CREATER){// 用户是服务端则创建共享内存段Creat();}else if(_usertype == USER){// 用户是客户端则获取共享内存段Get();}}~Shm() {}
};
5.2 将共享内存段附加到进程地址空间
创建好共享内存段之后,就需要将进程地址空间和共享内存段建立连接。这里我们让操作系统来给我们映射到进程的虚拟地址空间(第二个参数为空指针,详细请看上文shmat函数),同时如果shmat连接失败会返回 (void*)-1 ,所以我们要把它强制转换为long long再做判断(注意我们是64位机器,指针是8个字节大小,而int只有4个字节,所以要强制转换为long long),如果shmat成功就会返回共享内存首地址指针(挂接的进程虚拟地址),所以我们再增加一个获取该指针的成员变量,方便我们将地址打印出来查看
代码如下:
// 新增一个成员变量void* _start_mem;// 共享内存首地址指针void Attach(){_start_mem = shmat(_shmid, NULL, 0);if((long long)_start_mem < 0){ERR_EXIT("shmat");}printf("attach success\n");}public:Shm(const std::string& pathneme, int projid, const std::string& usertype): _shmid(gdefaultid), _size(gsize), _start_mem(nullptr), _usertype(usertype){_key = ftok(pathname.c_str(), projid);if (_key < 0){ERR_EXIT("ftok");}printf("key: 0x%x\n", _key);if(_usertype == CREATER){// 用户是服务端则创建共享内存段Creat();}else if(_usertype == USER){// 用户是客户端则获取共享内存段Get();}Attach();}void* VirtualAddr(){printf("VirtualAddr: %p\n", _start_mem);return _start_mem;}
当然我们也可以来一个获取共享内存段大小的接口
代码如下:
int Size(){return _size;}
5.3 分离共享内存段
在我们使用共享内存通信完之后,需要将内存进行回收,避免内存泄漏,但在回收共享内存之前,需要将共享内存段从当前进程的地址空间中分离出去。进程不能再通过该指针访问共享内存。注意:分离操作并不会销毁共享内存段本身。
void Detach(){int n = shmdt(_start_mem);if(n < 0){ERR_EXIT("shmdt");}printf("Detach success\n");}
5.4 销毁共享内存段
销毁前先将挂接的共享内存分离,然后再销毁,当然谁创建的就由谁来删除
注意:IPC_RMID
:标记删除共享内存。当所有进程均断开连接(shmdt
)后,内存才会被实际释放
代码如下:
void Destroy(){Detach();if(_shmid == gdefaultid)return;if(_usertype == CREATER){int n = shmctl(_shmid, IPC_RMID, NULL);if(n < 0){ERR_EXIT("shmctl");}printf("shmctl delete shm: %d success!\n", _shmid);}}
析构时,调用Destroy函数
5.5 测试代码
此时我们就可以使用共享内存,怎么使用呢?多个进程通过它们各自 shmat
返回的指针(指向同一物理内存的不同虚拟地址)直接读写共享内存区域。就如同我们malloc出来的一段内存对这段内存空间进行使用,我们现在就可以使用这段共享内存来读写。
服务端:
服务端读内存中的数据
#include "Shm.hpp"int main()
{Shm shm(pathname, proj_id, CREATER);char* mem = (char*)shm.VirtualAddr();while(true){printf("%s\n", mem);sleep(1);}return 0;
}
客户端:
客户端对共享内存写
#include "Shm.hpp"int main()
{Shm shm(pathname, proj_id, USER);char* mem = (char*)shm.VirtualAddr();for(char c = 'A'; c <= 'Z'; c++){mem[c - 'A'] = c;sleep(1);}return 0;
}
运行结果:
可以看到运行结果正常,进程挂接数也从无到2,不过由于我们的服务端是死循环在读,所以不会自己调用析构函数,我们需要自己通过命令行ipcrm -m [shmid]来删除共享内存,不然下次再运行就会报错文件存在
不过共享内存也同样存在缺点
共享内存的缺点与挑战
缺乏内置同步: 这是最大的挑战和风险点。 开发者必须严格、正确地使用额外的同步机制(信号量、互斥锁)来协调多个进程对共享内存的并发访问,否则极易导致数据损坏、程序崩溃等难以调试的问题。
复杂性增加: 相比管道简单的
read/write
接口,共享内存的创建、附加、分离、销毁步骤更多,并且必须手动管理同步,增加了程序的复杂性。资源管理: 共享内存段独立于进程存在。如果进程异常终止而没有正确分离或标记删除 (
IPC_RMID
),共享内存段可能残留在系统中,造成资源泄漏(ipcs
命令查看,ipcrm
命令删除)。需要良好的编程习惯和可能的清理机制。安全性: 需要正确设置权限 (
shmflg
中的权限位),防止未授权进程访问敏感数据。内存模型: 不同进程附加的地址 (
shmat
返回值) 可能不同,不能直接传递指针(传递指针在接收进程地址空间无效)。通常传递的是相对于共享内存基址的偏移量。
源码:
Shm.hpp
#pragma once#include <iostream>
#include <cstdio>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while (0)const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int proj_id = 0x66;#define CREATER "creater"
#define USER "user"class Shm
{
private:int _shmid;int _size;key_t _key;std::string _usertype;void* _start_mem;// 共享内存首地址指针(挂接后的进程虚拟地址)void CreatShm(int flg){_shmid = shmget(_key, _size, flg);if(_shmid < 0){ERR_EXIT("shmget");}printf("shmid: %d\n", _shmid);}void Creat(){CreatShm(IPC_CREAT | IPC_EXCL | 0666);}void Get(){CreatShm(IPC_CREAT);}void Attach(){_start_mem = shmat(_shmid, nullptr, 0);if((long long)_start_mem < 0){ERR_EXIT("shmat");}printf("attach success\n");}void Detach(){int n = shmdt(_start_mem);if(n < 0){ERR_EXIT("shmdt");}printf("Detach success\n");}void Destroy(){Detach();if(_shmid == gdefaultid)return;if(_usertype == CREATER){int n = shmctl(_shmid, IPC_RMID, NULL);if(n < 0){ERR_EXIT("shmctl");}printf("shmctl delete shm: %d success!\n", _shmid);}}public:Shm(const std::string& pathneme, int projid, const std::string& usertype): _shmid(gdefaultid), _size(gsize), _start_mem(nullptr), _usertype(usertype){_key = ftok(pathname.c_str(), projid);if (_key < 0){ERR_EXIT("ftok");}printf("key: 0x%x\n", _key);if(_usertype == CREATER){// 用户是服务端则创建共享内存段Creat();}else if(_usertype == USER){// 用户是客户端则获取共享内存段Get();}Attach();}void* VirtualAddr(){printf("VirtualAddr: %p\n", _start_mem);return _start_mem;}int Size(){return _size;}~Shm() {Destroy();}
};