百篇博客分析|本篇为:(共享内存篇) | 进程间最快通讯方式
进程通讯相关篇为:
- v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志
- v27.05 鸿蒙内核源码分析(互斥锁) | 同样是锁它确更丰满
- v28.04 鸿蒙内核源码分析(进程通讯) | 九种进程间通讯方式速揽
- v29.05 鸿蒙内核源码分析(信号量) | 谁在解决任务间的同步
- v30.07 鸿蒙内核源码分析(事件控制) | 多对多任务如何同步
- v33.03 鸿蒙内核源码分析(消息队列) | 进程间如何异步传递大数据
- v76.01 鸿蒙内核源码分析(共享内存) | 进程间最快通讯方式
- v77.01 鸿蒙内核源码分析(消息封装) | 剖析LiteIpc进程通讯内容
运行机制
共享好端端的一词,近些年被玩坏了,共享单车,共享充电宝,共享办公室,共享雨伞... 甚至还有共享女朋友,真是人有多大胆,共享有多大产。但凡事太尽就容易恶心到人,自己也一度被 共享内存 恶心到了,一直不想碰它,拖到了现在才写。
共享内存的原理简单,目的是为了进程间通讯,方法是通过映射到同一块物理内存。它是一种稀缺资源由内核按资源池方式管理,数量有限,默认是 192
个,用资源ID唯一标识,用户进程需要时通过系统调用向内核申请共享内存大小,管理器从资源池中分配一个可用资源ID,并向物理内存申请对应的物理页框。
如何使用共享内存就涉及到了内存模块最重要的概念 映射,不清楚的可以翻看系列相关篇。有共享需求的进程在各自的进程空间中划出一个线性区映射到共享内存段,那如何找到这个共享内存段呢 ? 由系统调用提供操作接口,简单说是先通过参数key
创建共享资源ID(shmid
),再由shmid
来连接/删除/控制 共享内存。详见本篇末尾的4
个系统调用 Shm***
。
如何实现?
这是笔者看完内核共享内存模块画出来的图,尽量用一张图表达一个模块的内容,因为百文是在给源码注释的过程中产生的,所以会画出这种比较怪异的图,有代码,也有模型,姑且称之为 代码模型图:
图分 管理 和 映射使用 两部分解读。 为了精简,代码展示只留下骨干,删除了判断,检查的代码。
管理部分
初始化共享内存,共享内存是以资源池的方式管理的,上来就为全局变量
g_shmSegs
向内核堆空间申请了g_shmInfo.shmmni
个struct shmIDSource
#define SHM_MNI 192 //共享内存总数 默认192 // 共享内存模块设置信息 struct shminfo { unsigned long shmmax, shmmin, shmmni, shmseg, shmall, __unused[4]; }; STATIC struct shminfo g_shmInfo = { //描述共享内存范围的全局变量 .shmmax = SHM_MAX,//共享内存单个上限 4096页 即 16M .shmmin = SHM_MIN,//共享内存单个下限 1页 即:4K .shmmni = SHM_MNI,//共享内存总数 默认192 .shmseg = SHM_SEG,//每个用户进程可以使用的最多的共享内存段的数目 128 .shmall = SHM_ALL,//系统范围内共享内存的总页数,4096页 }; //共享内存初始化 UINT32 ShmInit(VOID) { // .. ret = LOS_MuxInit(&g_sysvShmMux, NULL);//初始化互斥 g_shmSegs = LOS_MemAlloc((VOID *)OS_SYS_MEM_ADDR, sizeof(struct shmIDSource) * g_shmInfo.shmmni);//分配shm段数组 (VOID)memset_s(g_shmSegs, (sizeof(struct shmIDSource) * g_shmInfo.shmmni), 0, (sizeof(struct shmIDSource) * g_shmInfo.shmmni));//数组清零 for (i = 0; i < g_shmInfo.shmmni; i++) { g_shmSegs[i].status = SHM_SEG_FREE;//节点初始状态为空闲 g_shmSegs[i].ds.shm_perm.seq = i + 1;//struct ipc_perm shm_perm;系统为每一个IPC对象保存一个ipc_perm结构体,结构说明了IPC对象的权限和所有者 LOS_ListInit(&g_shmSegs[i].node);//初始化节点 } g_shmUsedPageCount = 0; return LOS_OK; }
系列篇多次提过,每个功能模块都至少有一个核心结构体来支撑模块的运行,进程是
PCB
,任务是TCB
,而共享内存就是shmIDSource
struct shmIDSource {//共享内存描述符 struct shmid_ds ds; //是内核为每一个共享内存段维护的数据结构 UINT32 status; //状态 SHM_SEG_FREE ... LOS_DL_LIST node; //节点,挂VmPage #ifdef LOSCFG_SHELL CHAR ownerName[OS_PCB_NAME_LEN]; #endif };
首先shmid_ds是真正描述共享内存信息的结构体,记录了本次共享内存由谁创建,大小,用户/组,访问时间等等。
//每个共享内存段在内核中维护着一个内部结构shmid_ds struct shmid_ds { struct ipc_perm shm_perm;///< 操作许可,里面包含共享内存的用户ID、组ID等信息 size_t shm_segsz; ///< 共享内存段的大小,单位为字节 time_t shm_atime; ///< 最后一个进程访问共享内存的时间 time_t shm_dtime; ///< 最后一个进程离开共享内存的时间 time_t shm_ctime; ///< 创建时间 pid_t shm_cpid; ///< 创建共享内存的进程ID pid_t shm_lpid; ///< 最后操作共享内存的进程ID unsigned long shm_nattch; ///< 当前使用该共享内存段的进程数量 unsigned long __pad1; //保留扩展用 unsigned long __pad2; }; //内核为每一个IPC对象保存一个ipc_perm结构体,该结构说明了IPC对象的权限和所有者 struct ipc_perm { key_t __ipc_perm_key; //调用shmget()时给出的关键字 uid_t uid; //共享内存所有者的有效用户ID gid_t gid; //共享内存所有者所属组的有效组ID uid_t cuid; //共享内存创建 者的有效用户ID gid_t cgid; //共享内存创建者所属组的有效组ID mode_t mode; //权限 + SHM_DEST / SHM_LOCKED /SHM_HUGETLB 标志位 int __ipc_perm_seq; //序列号 long __pad1; //保留扩展用 long __pad2; };
status 表示这段共享内存的状态,因为是资源池的方式,只有
SHM_SEG_FREE
的状态才可供分配,进程池和任务池也是这种管理方式。#define SHM_SEG_FREE 0x2000 //空闲未使用 #define SHM_SEG_USED 0x4000 //已使用 #define SHM_SEG_REMOVE 0x8000 //删除
node双向链表上挂的是一个个的物理页框
VmPage
,这是核心属性,数据将被存在这一个个物理页框中。ShmAllocSeg
为具体的分配函数STATIC INT32 ShmAllocSeg(key_t key, size_t size, INT32 shmflg) { // ... count = LOS_PhysPagesAlloc(size >> PAGE_SHIFT, &seg->node);//分配共享页面,函数内部把node都挂好了. if (count != (size >> PAGE_SHIFT)) {//当未分配到足够的内存时,处理方式是:不稀罕给那么点,舍弃! (VOID)LOS_PhysPagesFree(&seg->node);//释放节点上的物理页框 seg->status = SHM_SEG_FREE;//共享段变回空闲状态 return -ENOMEM; } ShmSetSharedFlag(seg);//将node的每个页面设置为共享页 g_shmUsedPageCount += size >> PAGE_SHIFT; seg->status |= SHM_SEG_USED; //共享段贴上已在使用的标签 seg->ds.shm_perm.mode = (UINT32)shmflg & ACCESSPERMS; seg->ds.shm_perm.key = key;//保存参数key,如此 key 和 共享ID绑定在一块 seg->ds.shm_segsz = size; //共享段的大小 seg->ds.shm_perm.cuid = LOS_GetUserID(); //设置用户ID seg->ds.shm_perm.uid = LOS_GetUserID(); //设置用户ID seg->ds.shm_perm.cgid = LOS_GetGroupID(); //设置组ID seg->ds.shm_perm.gid = LOS_GetGroupID(); //设置组ID seg->ds.shm_lpid = 0; //最后一个操作的进程 seg->ds.shm_nattch = 0; //绑定进程的数量 seg->ds.shm_cpid = LOS_GetCurrProcessID(); //获取进程ID seg->ds.shm_atime = 0; //访问时间 seg->ds.shm_dtime = 0; //detach 分离时间 共享内存使用完之后,需要将它从进程地址空间中分离出来;将共享内存分离并不是删除它,只是使该共享内存对当前的进程不再可用 seg->ds.shm_ctime = time(NULL);//创建时间 #ifdef LOSCFG_SHELL (VOID)memcpy_s(seg->ownerName, OS_PCB_NAME_LEN, OsCurrProcessGet()->processName, OS_PCB_NAME_LEN); #endif return segNum; }
映射使用部分
第一步: 创建共享内存 要实现共享内存,首先得创建一个内存段用于共享,干这事的是
ShmGet
/*! * @brief ShmGet * 得到一个共享内存标识符或创建一个共享内存对象 * @param key 建立新共享内存对象 标识符是IPC对象的内部名。为使多个合作进程能够在同一IPC对象上汇聚,需要提供一个外部命名方案。 为此,每个IPC对象都与一个键(key)相关联,这个键作为该对象的外部名,无论何时创建IPC结构(通过msgget、semget、shmget创建), 都应给IPC指定一个键, key_t由ftok创建,ftok当然在本工程里找不到,所以要写这么多. * @param shmflg IPC_CREAT IPC_EXCL IPC_CREAT: 在创建新的IPC时,如果key参数是IPC_PRIVATE或者和当前某种类型的IPC结构无关,则需要指明flag参数的IPC_CREAT标志位, 则用来创建一个新的IPC结构。(如果IPC结构已存在,并且指定了IPC_CREAT,则IPC_CREAT什么都不做,函数也不出错) IPC_EXCL: 此参数一般与IPC_CREAT配合使用来创建一个新的IPC结构。如果创建的IPC结构已存在函数就出错返回, 返回EEXIST(这与open函数指定O_CREAT和O_EXCL标志原理相同) * @param size 新建的共享内存大小,以字节为单位 * @return * * @see */ INT32 ShmGet(key_t key, size_t size, INT32 shmflg) { SYSV_SHM_LOCK(); if (key == IPC_PRIVATE) { ret = ShmAllocSeg(key, size, shmflg); } else { ret = ShmFindSegByKey(key);//通过key查找资源ID ret = ShmAllocSeg(key, size, shmflg);//分配一个共享内存 } SYSV_SHM_UNLOCK(); return ret; }
第二步: 进程线性区绑定共享内存 shmat()函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。,
ShmAt
的第一个参数其实是ShmGet
成功时的返回值 ,ShmatVmmAlloc
负责分配一个可用的线性区并和共享内存映射好/*! * @brief ShmAt * 用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。 * @param shm_flg 是一组标志位,通常为0。 * @param shmaddr 指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。 * @param shmid 是shmget()函数返回的共享内存标识符 * @return * 如果shmat成功执行,那么内核将使与该共享存储相关的shmid_ds结构中的shm_nattch计数器值加1 shmid 就是个索引,就跟进程和线程的ID一样 g_shmSegs[shmid] shmid > 192个 * @see */ VOID *ShmAt(INT32 shmid, const VOID *shmaddr, INT32 shmflg) { struct shmIDSource *seg = NULL; LosVmMapRegion *r = NULL; ret = ShmatParamCheck(shmaddr, shmflg);//参数检查 SYSV_SHM_LOCK(); seg = ShmFindSeg(shmid);//找到段 ret = ShmPermCheck(seg, acc_mode); seg->ds.shm_nattch++;//ds上记录有一个进程绑定上来 r = ShmatVmmAlloc(seg, shmaddr, shmflg, prot);//在当前进程空间分配一个线性区并映射到共享内存 r->shmid = shmid;//把ID给线性区的shmid r->regionFlags |= VM_MAP_REGION_FLAG_SHM;//这是一个共享线性区 seg->ds.shm_atime = time(NULL);//访问时间 seg->ds.shm_lpid = LOS_GetCurrProcessID();//进程ID SYSV_SHM_UNLOCK(); return (VOID *)(UINTPTR)r->range.base; }
第三步: 控制/使用 共享内存,这才是目的,前面的都是前戏
/*! * @brief ShmCtl * 此函数可以对shmid指定的共享存储进行多种操作(删除、取信息、加锁、解锁等) * @param buf 是一个结构指针,它指向共享内存模式和访问权限的结构。 * @param cmd command是要采取的操作,它可以取下面的三个值 : IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。 IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值 IPC_RMID:删除共享内存段 * @param shmid 是shmget()函数返回的共享内存标识符 * @return * * @see */ INT32 ShmCtl(INT32 shmid, INT32 cmd, struct shmid_ds *buf) { SYSV_SHM_LOCK(); switch (cmd) { case IPC_STAT: case SHM_STAT://取段结构 ret = LOS_ArchCopyToUser(buf, &seg->ds, sizeof(struct shmid_ds));//把内核空间的共享页数据拷贝到用户空间 if (cmd == SHM_STAT) { ret = (unsigned int)((unsigned int)seg->ds.shm_perm.seq << 16) | (unsigned int)((unsigned int)shmid & 0xffff); /* 16: use the seq as the upper 16 bits */ } break; case IPC_SET://重置共享段 ret = ShmPermCheck(seg, SHM_M); //从用户空间拷贝数据到内核空间 ret = LOS_ArchCopyFromUser(&shm_perm, &buf->shm_perm, sizeof(struct ipc_perm)); seg->ds.shm_perm.uid = shm_perm.uid; seg->ds.shm_perm.gid = shm_perm.gid; seg->ds.shm_perm.mode = (seg->ds.shm_perm.mode & ~ACCESSPERMS) | (shm_perm.mode & ACCESSPERMS);//可访问 seg->ds.shm_ctime = time(NULL); #ifdef LOSCFG_SHELL (VOID)memcpy_s(seg->ownerName, OS_PCB_NAME_LEN, OS_PCB_FROM_PID(shm_perm.uid)->processName, OS_PCB_NAME_LEN); #endif break; case IPC_RMID://删除共享段 ret = ShmPermCheck(seg, SHM_M); seg->status |= SHM_SEG_REMOVE; if (seg->ds.shm_nattch <= 0) {//没有任何进程在使用了 ShmFreeSeg(seg);//释放 归还内存 } break; case IPC_INFO://把内核空间的共享页数据拷贝到用户空间 ret = LOS_ArchCopyToUser(buf, &g_shmInfo, sizeof(struct shminfo)); ret = g_shmInfo.shmmni; break; case SHM_INFO: shmInfo.shm_rss = 0; shmInfo.shm_swp = 0; shmInfo.shm_tot = 0; shmInfo.swap_attempts = 0; shmInfo.swap_successes = 0; shmInfo.used_ids = ShmSegUsedCount();//在使用的seg数 ret = LOS_ArchCopyToUser(buf, &shmInfo, sizeof(struct shm_info));//把内核空间的共享页数据拷贝到用户空间 ret = g_shmInfo.shmmni; break; default: VM_ERR("the cmd(%d) is not supported!", cmd); ret = EINVAL; goto ERROR; } SYSV_SHM_UNLOCK(); return ret; }
第四步: 完事了解绑/删除,好聚好散还有下次,在
ShmDt
中主要干了解除映射LOS_ArchMmuUnmap
这件事,没有了映射就不再有关系了,并且会检测到最后一个解除映射的进程时,会彻底释放掉这段共享内存ShmFreeSeg
/** * @brief 当对共享存储的操作已经结束时,则调用shmdt与该存储段分离 如果shmat成功执行,那么内核将使与该共享存储相关的shmid_ds结构中的shm_nattch计数器值减1 * @attention 注意:这并不从系统中删除共享存储的标识符以及其相关的数据结构。共享存储的仍然存在, 直至某个进程带IPC_RMID命令的调用shmctl特地删除共享存储为止 * @param shmaddr * @return INT32 */ INT32 ShmDt(const VOID *shmaddr) { LosVmSpace *space = OsCurrProcessGet()->vmSpace;//获取进程空间 (VOID)LOS_MuxAcquire(&space->regionMux); region = LOS_RegionFind(space, (VADDR_T)(UINTPTR)shmaddr);//找到线性区 shmid = region->shmid;//线性区共享ID LOS_RbDelNode(&space->regionRbTree, ®ion->rbNode);//从红黑树和链表中摘除节点 LOS_ArchMmuUnmap(&space->archMmu, region->range.base, region->range.size >> PAGE_SHIFT);//解除线性区的映射 (VOID)LOS_MuxRelease(&space->regionMux); /* free it */ free(region);//释放线性区所占内存池中的内存 SYSV_SHM_LOCK(); seg = ShmFindSeg(shmid);//找到seg,线性区和共享段的关系是 1:N 的关系,其他空间的线性区也会绑在共享段上 ShmPagesRefDec(seg);//页面引用数 -- seg->ds.shm_nattch--;//使用共享内存的进程数少了一个 if ((seg->ds.shm_nattch <= 0) && //无任何进程使用共享内存 (seg->status & SHM_SEG_REMOVE)) {//状态为删除时需要释放物理页内存了,否则其他进程还要继续使用共享内存 ShmFreeSeg(seg);//释放seg 页框链表中的页框内存,再重置seg状态 } else { seg->ds.shm_dtime = time(NULL);//记录分离的时间 seg->ds.shm_lpid = LOS_GetCurrProcessID();//记录操作进程ID } SYSV_SHM_UNLOCK();
总结
看到这里你应该不会问共享内存的作用和为啥它是最快的进程间通讯方式了,如果还有这两个问题说明还要再看一遍 :P ,另外细心的话会发现共享内存会有个小缺点,就是同时访问的问题,所以需要使用互斥锁来保证同时只有一个进程在使用,
SYSV_SHM_LOCK
和SYSV_SHM_UNLOCK
在以上的四个步骤中都有出现。STATIC LosMux g_sysvShmMux; //互斥锁,共享内存本身并不保证操作的同步性,所以需用互斥锁 /* private macro */ #define SYSV_SHM_LOCK() (VOID)LOS_MuxLock(&g_sysvShmMux, LOS_WAIT_FOREVER) //申请永久等待锁 #define SYSV_SHM_UNLOCK() (VOID)LOS_MuxUnlock(&g_sysvShmMux) //释放锁
百文说内核 | 抓住主脉络
- 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
- 与代码需不断
debug
一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx
代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。 - 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,公众号回复 百文 可方便阅读。
按功能模块:
前因后果 | 基础工具 | 加载运行 | 进程管理 |
---|---|---|---|
总目录 调度故事 内存主奴 源码注释 源码结构 静态站点 参考文档 |
双向链表 位图管理 用栈方式 定时器 原子操作 时间管理 |
ELF格式 ELF解析 静态链接 重定位 进程映像 |
进程管理 进程概念 Fork 特殊进程 进程回收 信号生产 信号消费 Shell编辑 Shell解析 |
编译构建 | 进程通讯 | 内存管理 | 任务管理 |
编译环境 编译过程 环境脚本 构建工具 gn应用 忍者ninja |
自旋锁 互斥锁 进程通讯 信号量 事件控制 消息队列 共享内存 消息封装 |
内存分配 内存管理 内存汇编 内存映射 内存规则 物理内存 |
时钟任务 任务调度 任务管理 调度队列 调度机制 线程概念 并发并行 CPU 系统调用 任务切换 |
文件系统 | 硬件架构 | ||
文件概念 文件系统 索引节点 挂载目录 根文件系统 VFS 文件句柄 管道文件 |
汇编基础 汇编传参 工作模式 寄存器 异常接管 汇编汇总 中断切换 中断概念 中断管理 |
||
### 百万注源码 | 处处扣细节 |
- 百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。
< gitee | github | coding | codechina > 四大码仓推送 | 同步官方源码,公众号中回复 百万 可方便阅读。
关注不迷路 | 代码即人生