前文中我们介绍了管道——匿名管道和命名管道来实现进程间通信,在介绍怎么进行通信时,我们有提到过不止管道的方式进行通信,还有System V IPC,今天这篇文章我们就来学习一下System V IPC中的共享内存
管道(匿名/命名管道)作为传统IPC机制存在显著缺陷:
⚡ 共享内存的破局: 通过多进程直接访问同一物理内存区域,消除数据拷贝,实现零复制(Zero-Copy)通信,速度提升10-100倍
共享内存是 System V IPC(Inter-Process Communication)机制的一种,它允许多个不相关的进程(父子进程或完全独立的进程)访问同一块物理内存区域。这是最快的进程间通信(IPC)形式,因为它完全避免了内核空间和用户空间之间数据的复制。
本质定义
物理内存共享:多个进程通过页表映射,直接访问同一块物理内存区域,实现零拷贝数据交换。
逻辑视图:在进程虚拟地址空间中表现为普通内存段(如malloc分配),实则由操作系统管理共享物理页。
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将物理共享页插入自身页表(虚拟→物理映射)。那操作系统是怎么管理共享内存的呢?先描述再组织
通过一个内核结构体来描述共享内存,再由操作系统统一管理这些内核结构体
共享内存数据结构:
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; // 创建者PID
pid_t shm_lpid; // 最后一次操作者PID
unsigned 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 | 描述单个共享内存段的完整状态 |
动态管理演进:
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_kernel
struct 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的物理页。
3. 生命周期管理
操作 | 内核行为 |
|---|---|
删除(shmctl(IPC_RMID)) | 标记为SHM_DEST,当shm_nattch=0时触发物理内存回收 |
进程退出 | 自动调用shmdt解除映射,递减shm_nattch |
系统重启 | 所有共享内存被销毁(因物理内存重置) |
1. 物理内存分配
shmem_fault分配物理页帧。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 返回的指针(指向同一物理内存的不同虚拟地址)直接读写共享内存区域。
semget, semop, semctl)
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)。
shmget函数核心解析(系统级共享内存管理)shmget是System V IPC中创建或获取共享内存段的核心函数,其本质是向内核申请一块多进程可共同访问的物理内存区域。
1. 函数原型与基础机制
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);errnokey查找或创建共享内存段shmid_ds2. 参数解析
参数 | 技术内涵 | 内核行为 |
|---|---|---|
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; // 创建者PID
pid_t shm_lpid; // 最后操作者PID
unsigned 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_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()后的父子进程)。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。shmdt 函数:断开共享内存连接功能:将共享内存段从当前进程的地址空间分离(解除映射),但不会删除共享内存。
原型:
int shmdt(const void *shmaddr);参数:
shmaddr:由 shmat 返回的地址指针。返回值:
0。-1 并设置 errno。底层机制:通过 do_munmap() 释放对应的虚拟内存区间。
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.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> | 进程退出时自动卸载 |
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, ...) 会报错)。shmid 而非 key 。3. 设计目的与架构思想
维度 | key | shmid |
|---|---|---|
唯一性范围 | 全局唯一(整个操作系统内) | 进程内有效(不同进程的 shmid 可能不同) |
生命周期 | 持久存在,直至共享内存被删除 | 随进程结束失效,但共享内存仍存留 |
安全隔离 | 内核维护,用户不可直接操作 | 用户直接使用,但无系统级权限 |
设计目标 | 解耦:内核通过 key 管理资源唯一性 | 封装:为用户提供安全操作接口 |
📜 架构意义:
key 保证共享内存的全局唯一性,用户通过 shmid 操作资源,实现内核与用户层的解耦" 。key 解决“资源是谁”的问题(系统唯一标识),shmid 解决“如何操作”的问题(用户接口)。
和管道一样,我们也来段代码加深对共享内存通信的理解。
这里我们和命名管道一样,实现让两个毫无关系的进程通信,所以我们将一个进程看作服务端,另一个进程看作客户端,然后进行封装。

我们先来介绍一段宏
宏定义结构
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)do { ... } while (0):
这是一种宏定义的惯用技巧,目的是将多条语句封装为单条逻辑块。
if-else语句中使用时)。if分支后)都能安全使用,不会因缺少大括号导致逻辑错误。\:
用于连接多行代码,使宏定义可跨行书写,提高可读性。
核心功能
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)确保程序在致命错误时立即终止。其设计兼顾了安全性、可读性和可移植性。
我们让服务端创建共享内存段,客户端则获取共享内存段
但是创建共享内存段之前,得先将共享内存段的唯一标识符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() {}
};创建好共享内存段之后,就需要将进程地址空间和共享内存段建立连接。这里我们让操作系统来给我们映射到进程的虚拟地址空间(第二个参数为空指针,详细请看上文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;
}在我们使用共享内存通信完之后,需要将内存进行回收,避免内存泄漏,但在回收共享内存之前,需要将共享内存段从当前进程的地址空间中分离出去。进程不能再通过该指针访问共享内存。注意:分离操作并不会销毁共享内存段本身。
void Detach()
{
int n = shmdt(_start_mem);
if(n < 0)
{
ERR_EXIT("shmdt");
}
printf("Detach success\n");
}销毁前先将挂接的共享内存分离,然后再销毁,当然谁创建的就由谁来删除
注意: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函数
此时我们就可以使用共享内存,怎么使用呢?多个进程通过它们各自 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();
}
};