百篇博客分析|本篇为:(消息映射篇) | 剖析LiteIpc(下)进程通讯机制
进程通讯相关篇为:
- v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志
- v27.05 鸿蒙内核源码分析(互斥锁) | 同样是锁它确更丰满
- v28.04 鸿蒙内核源码分析(进程通讯) | 九种进程间通讯方式速揽
- v29.05 鸿蒙内核源码分析(信号量) | 谁在解决任务间的同步
- v30.07 鸿蒙内核源码分析(事件控制) | 多对多任务如何同步
- v33.03 鸿蒙内核源码分析(消息队列) | 进程间如何异步传递大数据
- v76.01 鸿蒙内核源码分析(共享内存) | 进程间最快通讯方式
- v77.02 鸿蒙内核源码分析(消息封装) | 剖析LiteIpc(上)进程通讯内容
- v78.01 鸿蒙内核源码分析(消息映射) | 剖析LiteIpc(下)进程通讯机制
基本概念
LiteIPC
是OpenHarmony LiteOS-A
内核提供的一种新型IPC
(Inter-Process Communication,即进程间通信)机制,为轻量级进程间通信组件,为面向服务的系统服务框架提供进程间通信能力,分为内核实现和用户态实现两部分,其中内核实现完成进程间消息收发、IPC内存管理、超时通知和死亡通知等功能;用户态提供序列化和反序列化能力,并完成IPC
回调消息和死亡消息的分发。
我们主要讲解内核态实现部分,本想一篇说完,但发现它远比想象中的复杂和重要,所以分通讯内容和通讯机制上下两篇说。上篇可翻看 鸿蒙内核源码分析(消息封装篇) | 剖析LiteIpc(上)进程通讯内容 ,本篇为通讯机制,介绍liteipc在内核层的实现过程。
空间映射
映射 一词在系列篇中多次出现,虚拟地址的基础就是映射,共享内存的实现也要靠映射,LiteIPC
通讯的底层实现也离不开映射,有意思的是将用户态的线性区和内核态的线性区进行了映射。也就是说当用户访问用户空间中的某个虚拟地址时,这个虚拟地址其实和内核空间某个地址都指向了同一个物理内存。可能有人会问这也行 ? 在了解完一下 物理地址,内核虚拟地址,用户虚拟地址之间的关系后就会明白这当然可以。
-
虚拟地址包括内核空间地址和用户进程空间地址,它们的范围对外暴露,由内核开发人员划定,内核专门提供了判断函数。即看到一个虚拟地址可以知道是内核在用还是用户(应用)进程在用。
/// 虚拟地址是否在内核空间 STATIC INLINE BOOL LOS_IsKernelAddress(VADDR_T vaddr) { return ((vaddr >= (VADDR_T)KERNEL_ASPACE_BASE) && (vaddr <= ((VADDR_T)KERNEL_ASPACE_BASE + ((VADDR_T)KERNEL_ASPACE_SIZE - 1)))); } /// 虚拟地址是否在用户空间 STATIC INLINE BOOL LOS_IsUserAddress(VADDR_T vaddr) { return ((vaddr >= USER_ASPACE_BASE) && (vaddr <= (USER_ASPACE_BASE + (USER_ASPACE_SIZE - 1)))); }
-
物理地址是由物理内存提供,系统集成商根据实际的物理内存大小来划定物理地址范围。至于具体的某段物理内存是给内核空间使用还是给用户空间使用没有要求,所谓映射是指虚拟地址 <--> 物理地址的映射,是 N:1的关系,一个物理地址可以被多个虚拟地址映射,而一个虚拟地址只能映射到一个物理地址。
-
以上是
LiteIPC
的实现概念基础,明白后就不难理解结构体IpcPool
存在的必要性了/** * @struct IpcPool | ipc池 * @brief LiteIPC的核心思想就是在内核态为每个Service任务维护一个IPC消息队列,该消息队列通过LiteIPC设备文件向上层 * 用户态程序分别提供代表收取IPC消息的读操作和代表发送IPC消息的写操作。 */ typedef struct { VOID *uvaddr; ///< 用户态空间地址,由kvaddr映射而来的地址,这两个地址的关系一定要搞清楚,否则无法理解IPC的核心思想 VOID *kvaddr; ///< 内核态空间地址,IPC申请的是内核空间,但是会通过 DoIpcMmap 将这个地址映射到用户空间 UINT32 poolSize; ///< ipc池大小 } IpcPool;
文件访问
LiteIPC
的运行机制是首先将需要接收IPC
消息的任务通过ServiceManager
注册成为一个Service
,然后通过ServiceManager
为该Service
任务配置访问权限,即指定哪些任务可以向该Service
任务发送IPC
消息。LiteIPC
的核心思想就是在内核态为每个Service
任务维护一个IPC
消息队列,该消息队列通过LiteIPC
设备文件向上层用户态程序分别提供代表收取IPC
消息的读操作和代表发送IPC
消息的写操作。
设备文件的接口层(VFS
)实现为g_liteIpcFops
,跟踪这几个函数就能够整明白整个实现LiteIPC
过程
#define LITEIPC_DRIVER "/dev/lite_ipc" ///< 虚拟设备,文件访问读取
STATIC const struct file_operations_vfs g_liteIpcFops = {
.open = LiteIpcOpen, /* open | 创建Ipc内存池*/
.close = LiteIpcClose, /* close */
.ioctl = LiteIpcIoctl, /* ioctl | 包含读写操作 */
.mmap = LiteIpcMmap, /* mmap | 实现线性区映射*/
};
LiteIpcOpen | 创建消息内存池
LITE_OS_SEC_TEXT STATIC int LiteIpcOpen(struct file *filep)
{
LosProcessCB *pcb = OsCurrProcessGet();
if (pcb->ipcInfo != NULL) {
return 0;
}
pcb->ipcInfo = LiteIpcPoolCreate();
if (pcb->ipcInfo == NULL) {
return -ENOMEM;
}
return 0;
}
///创建IPC消息内存池
LITE_OS_SEC_TEXT_INIT STATIC ProcIpcInfo *LiteIpcPoolCreate(VOID)
{
ProcIpcInfo *ipcInfo = LOS_MemAlloc(m_aucSysMem1, sizeof(ProcIpcInfo));//从内核堆内存中申请IPC控制体
if (ipcInfo == NULL) {
return NULL;
}
(VOID)memset_s(ipcInfo, sizeof(ProcIpcInfo), 0, sizeof(ProcIpcInfo));
(VOID)LiteIpcPoolInit(ipcInfo);
return ipcInfo;
}
解读
- 进来先获取当前进程
OsCurrProcessGet()
,即为每个进程创建唯一的IPC
消息控制体,ProcIpcInfo
在进程控制块中,负责管理IPC
消息 - 初始化消息内存池,此处只申请结构体本身占用内存,真正的内存池在
LiteIpcMmap
中完成
LiteIpcMmap | 映射
///将参数线性区设为IPC专用区
LITE_OS_SEC_TEXT STATIC int LiteIpcMmap(struct file *filep, LosVmMapRegion *region)
{
int ret = 0;
LosVmMapRegion *regionTemp = NULL;
LosProcessCB *pcb = OsCurrProcessGet();
ProcIpcInfo *ipcInfo = pcb->ipcInfo;
//被映射的线性区不能在常量和私有数据区
if ((ipcInfo == NULL) || (region == NULL) || (region->range.size > LITE_IPC_POOL_MAX_SIZE) ||
(!LOS_IsRegionPermUserReadOnly(region)) || (!LOS_IsRegionFlagPrivateOnly(region))) {
ret = -EINVAL;
goto ERROR_REGION_OUT;
}
if (IsPoolMapped(ipcInfo)) {//已经用户空间和内核空间之间存在映射关系了
return -EEXIST;
}
if (ipcInfo->pool.uvaddr != NULL) {//ipc池已在进程空间有地址
regionTemp = LOS_RegionFind(pcb->vmSpace, (VADDR_T)(UINTPTR)ipcInfo->pool.uvaddr);//在指定进程空间中找到所在线性区
if (regionTemp != NULL) {
(VOID)LOS_RegionFree(pcb->vmSpace, regionTemp);//先释放线性区
}
// 建议加上 ipcInfo->pool.uvaddr = NULL; 同下
}
ipcInfo->pool.uvaddr = (VOID *)(UINTPTR)region->range.base;//将指定的线性区和ipc池虚拟地址绑定
if (ipcInfo->pool.kvaddr != NULL) {//如果存在内核空间地址
LOS_VFree(ipcInfo->pool.kvaddr);//因为要重新映射,所以必须先释放掉物理内存
ipcInfo->pool.kvaddr = NULL; //从效果上看, 这句话可以不加,但加上看着更舒服, uvaddr 和 kvaddr 一对新人迎接美好未来
}
/* use vmalloc to alloc phy mem */
ipcInfo->pool.kvaddr = LOS_VMalloc(region->range.size);//从内核动态空间中申请线性区,分配同等量的物理内存,做好 内核 <-->物理内存的映射
if (ipcInfo->pool.kvaddr == NULL) {//申请物理内存失败, 肯定是玩不下去了.
ret = -ENOMEM; //返回没有内存了
goto ERROR_REGION_OUT;
}
/* do mmap */
ret = DoIpcMmap(pcb, region);//对uvaddr和kvaddr做好映射关系,如此用户态下通过操作uvaddr达到操作kvaddr的目的
if (ret) {
goto ERROR_MAP_OUT;
}
/* ipc pool init */
if (LOS_MemInit(ipcInfo->pool.kvaddr, region->range.size) != LOS_OK) {//初始化ipc池
ret = -EINVAL;
goto ERROR_MAP_OUT;
}
ipcInfo->pool.poolSize = region->range.size;//ipc池大小为线性区大小
return 0;
ERROR_MAP_OUT:
LOS_VFree(ipcInfo->pool.kvaddr);
ERROR_REGION_OUT:
if (ipcInfo != NULL) {
ipcInfo->pool.uvaddr = NULL;
ipcInfo->pool.kvaddr = NULL;
}
return ret;
}
解读
- 这个函数一定要看明白,重要部分已加上注释,主要干了两件事。
- 通过
LOS_VMalloc
向内核堆空间申请了一段物理内存,参数是线性区的大小,并做好了映射,因是内核堆空间,所以分配的虚拟地址就是个内核地址,并将这个地址赋给了pool.kvaddr
ipcInfo->pool.kvaddr = LOS_VMalloc(region->range.size);//从内核动态空间中申请线性区,分配同等量的物理内存,做好 内核 <-->物理内存的映射
- 通过
DoIpcMmap
将参数pcb
(用户进程)的IPC
消息池的pool.uvaddr
也映射到LOS_VMalloc
分配的物理内存上,
详看ret = DoIpcMmap(pcb, region);//对uvaddr和kvaddr做好映射关系,如此用户态下通过操作uvaddr达到操作
DoIpcMmap
的实现,因为太重要此处代码不做删改。LITE_OS_SEC_TEXT STATIC INT32 DoIpcMmap(LosProcessCB *pcb, LosVmMapRegion *region) { UINT32 i; INT32 ret = 0; PADDR_T pa; UINT32 uflags = VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_USER; LosVmPage *vmPage = NULL; VADDR_T uva = (VADDR_T)(UINTPTR)pcb->ipcInfo->pool.uvaddr;//用户空间地址 VADDR_T kva = (VADDR_T)(UINTPTR)pcb->ipcInfo->pool.kvaddr;//内核空间地址 (VOID)LOS_MuxAcquire(&pcb->vmSpace->regionMux); for (i = 0; i < (region->range.size >> PAGE_SHIFT); i++) {//获取线性区页数,一页一页映射 pa = LOS_PaddrQuery((VOID *)(UINTPTR)(kva + (i << PAGE_SHIFT)));//通过内核空间查找物理地址 if (pa == 0) { PRINT_ERR("%s, %d\n", __FUNCTION__, __LINE__); ret = -EINVAL; break; } vmPage = LOS_VmPageGet(pa);//获取物理页框 if (vmPage == NULL) {//目的是检查物理页是否存在 PRINT_ERR("%s, %d\n", __FUNCTION__, __LINE__); ret = -EINVAL; break; } STATUS_T err = LOS_ArchMmuMap(&pcb->vmSpace->archMmu, uva + (i << PAGE_SHIFT), pa, 1, uflags);//将物理页映射到用户空间 if (err < 0) { ret = err; PRINT_ERR("%s, %d\n", __FUNCTION__, __LINE__); break; } } /* if any failure happened, rollback | 如果中间发生映射失败,则回滚*/ if (i != (region->range.size >> PAGE_SHIFT)) { while (i--) { pa = LOS_PaddrQuery((VOID *)(UINTPTR)(kva + (i << PAGE_SHIFT)));//查询物理地址 vmPage = LOS_VmPageGet(pa);//获取物理页框 (VOID)LOS_ArchMmuUnmap(&pcb->vmSpace->archMmu, uva + (i << PAGE_SHIFT), 1);//取消与用户空间的映射 LOS_PhysPageFree(vmPage);//释放物理页 } } (VOID)LOS_MuxRelease(&pcb->vmSpace->regionMux); return ret; }
- 至次
LiteIPC
的准备工作已经完成,接下来就是操作/控制阶段了
LiteIpcIoctl | 控制
LITE_OS_SEC_TEXT int LiteIpcIoctl(struct file *filep, int cmd, unsigned long arg)
{
UINT32 ret = LOS_OK;
LosProcessCB *pcb = OsCurrProcessGet();
ProcIpcInfo *ipcInfo = pcb->ipcInfo;
// 整个系统只能有一个ServiceManager,而Service可以有多个。ServiceManager有两个主要功能:一是负责Service的注册和注销,
// 二是负责管理Service的访问权限(只有有权限的任务(Task)可以向对应的Service发送IPC消息)。
switch (cmd) {
case IPC_SET_CMS:
return SetCms(arg); //设置ServiceManager , 整个系统只能有一个ServiceManager
case IPC_CMS_CMD: // 控制命令,创建/删除/添加权限
return HandleCmsCmd((CmsCmdContent *)(UINTPTR)arg);
case IPC_SET_IPC_THREAD:
return SetIpcTask();//将当前任务设置成当前进程的IPC任务ID
case IPC_SEND_RECV_MSG://发送和接受消息,代表消息内容
ret = LiteIpcMsgHandle((IpcContent *)(UINTPTR)arg);//处理IPC消息
break;
}
return ret;
}
解读
LiteIPC
中有两个主要概念,一个是ServiceManager
,另一个是Service
。整个系统只能有一个ServiceManager
,而Service
可以有多个。ServiceManager
有两个主要功能:一是负责Service
的注册和注销,二是负责管理Service
的访问权限(只有有权限的任务(Task
)可以向对应的Service
发送IPC
消息)。IPC_SET_CMS
为设置ServiceManager
命令,IPC_CMS_CMD
为对Service
的管理命令。IPC_SEND_RECV_MSG
为消息的处理过程,消息的封装结合上篇理解,接收和发送消息对应的是LiteIpcRead
和LiteIpcWrite
两个函数。- 本处只说明
LiteIpcWrite
写消息指的是从用户空间向内核空间写数据,在消息内容体中已经指明这个消息时写给哪个任务的,如此达到了进程间(其实也是任务间)通讯的目的。/// 写IPC消息队列,从用户空间到内核空间 LITE_OS_SEC_TEXT STATIC UINT32 LiteIpcWrite(IpcContent *content) { UINT32 ret, intSave; UINT32 dstTid; IpcMsg *msg = content->outMsg; LosTaskCB *tcb = OS_TCB_FROM_TID(dstTid);//目标任务实体 LosProcessCB *pcb = OS_PCB_FROM_PID(tcb->processID);//目标进程实体 if (pcb->ipcInfo == NULL) { PRINT_ERR("pid %u Liteipc not create\n", tcb->processID); return -EINVAL; } //这里为什么要申请msg->dataSz,因为IpcMsg中的真正数据体 data是个指针,它的大小是dataSz . 同时申请存储偏移量空间 UINT32 bufSz = sizeof(IpcListNode) + msg->dataSz + msg->spObjNum * sizeof(UINT32);//这句话是理解上层消息在内核空间数据存放的关键!!! @note_good IpcListNode *buf = (IpcListNode *)LiteIpcNodeAlloc(tcb->processID, bufSz);//向内核空间申请bufSz大小内存 if (buf == NULL) { PRINT_ERR("%s, %d\n", __FUNCTION__, __LINE__); return -ENOMEM; }//IpcListNode的第一个成员变量就是IpcMsg ret = CopyDataFromUser(buf, bufSz, (const IpcMsg *)msg);//将消息内容拷贝到内核空间,包括消息控制体+内容体+偏移量 if (ret != LOS_OK) { PRINT_ERR("%s, %d\n", __FUNCTION__, __LINE__); goto ERROR_COPY; } if (tcb->ipcTaskInfo == NULL) {//如果任务还没有IPC信息 tcb->ipcTaskInfo = LiteIpcTaskInit();//初始化这个任务的IPC信息模块,因为消息来了要处理了 } ret = HandleSpecialObjects(dstTid, buf, FALSE);//处理消息 if (ret != LOS_OK) { PRINT_ERR("%s, %d\n", __FUNCTION__, __LINE__); goto ERROR_COPY; } /* add data to list and wake up dest task *///向列表添加数据并唤醒目标任务 SCHEDULER_LOCK(intSave); LOS_ListTailInsert(&(tcb->ipcTaskInfo->msgListHead), &(buf->listNode));//把消息控制体挂到目标任务的IPC链表头上 OsHookCall(LOS_HOOK_TYPE_IPC_WRITE, &buf->msg, dstTid, tcb->processID, tcb->waitFlag); if (tcb->waitFlag == OS_TASK_WAIT_LITEIPC) {//如果这个任务在等这个消息,注意这个tcb可不是当前任务 OsTaskWakeClearPendMask(tcb);//撕掉对应标签 OsSchedTaskWake(tcb);//唤醒任务执行,因为任务在等待读取 IPC消息 SCHEDULER_UNLOCK(intSave); LOS_MpSchedule(OS_MP_CPU_ALL);//设置调度方式,所有CPU核发生一次调度,这里非要所有CPU都调度吗? //可不可以查询下该任务挂在哪个CPU上,只调度对应CPU呢? 注者在此抛出思考 @note_thinking LOS_Schedule();//发起调度 } else { SCHEDULER_UNLOCK(intSave); } return LOS_OK; ERROR_COPY: LiteIpcNodeFree(OS_TCB_FROM_TID(dstTid)->processID, buf);//拷贝发生错误就要释放内核堆内存,那可是好大一块堆内存啊 return ret; }
- 大概流程就是从
LiteIpc
内存池中分配内核空间装用户空间的数据,注意一定要从LiteIpcNodeAlloc
分配,原因代码中也已注明。 - 有数据了就将数据挂到目标任务的
IPC
双向链表上,如果任务在等待读取消息(OS_TASK_WAIT_LITEIPC
)则唤醒目标任务执行,并发起调度LOS_Schedule
。 - 调度到目标任务后,继续执行
LiteIpcRead
,此时读函数正在经历一个死循环等待将消息读到用户空间。
- 大概流程就是从
百文说内核 | 抓住主脉络
- 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
- 与代码需不断
debug
一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx
代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。 - 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,公众号回复 百文 可方便阅读。
按功能模块:
前因后果 | 基础工具 | 加载运行 | 进程管理 |
---|---|---|---|
总目录 调度故事 内存主奴 源码注释 源码结构 静态站点 参考文档 |
双向链表 位图管理 用栈方式 定时器 原子操作 时间管理 |
ELF格式 ELF解析 静态链接 重定位 进程映像 |
进程管理 进程概念 Fork 特殊进程 进程回收 信号生产 信号消费 Shell编辑 Shell解析 |
编译构建 | 进程通讯 | 内存管理 | 任务管理 |
编译环境 编译过程 环境脚本 构建工具 gn应用 忍者ninja |
自旋锁 互斥锁 进程通讯 信号量 事件控制 消息队列 共享内存 消息封装 消息映射 |
内存分配 内存管理 内存汇编 内存映射 内存规则 物理内存 |
时钟任务 任务调度 任务管理 调度队列 调度机制 线程概念 并发并行 CPU 系统调用 任务切换 |
文件系统 | 硬件架构 | ||
文件概念 文件系统 索引节点 挂载目录 根文件系统 VFS 文件句柄 管道文件 |
汇编基础 汇编传参 工作模式 寄存器 异常接管 汇编汇总 中断切换 中断概念 中断管理 |
百万注源码 | 处处扣细节
-
百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。
-
< gitee | github | coding | codechina > 四大码仓推送 | 同步官方源码,公众号中回复 百万 可方便阅读。