vlambda博客
学习文章列表

v78.01 鸿蒙内核源码分析(消息映射) | 剖析LiteIpc(下)进程通讯机制

v78.01 鸿蒙内核源码分析(消息映射) | 剖析LiteIpc(下)进程通讯机制

百篇博客分析|本篇为:(消息映射篇) | 剖析LiteIpc(下)进程通讯机制

进程通讯相关篇为:

  • v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志

  • v27.05 鸿蒙内核源码分析(互斥锁) | 同样是锁它更丰满

  • v28.04 鸿蒙内核源码分析(进程通讯) | 九种进程间通讯方式速揽

  • v29.05 鸿蒙内核源码分析(信号量) | 谁在解决任务间的同步

  • v30.07 鸿蒙内核源码分析(事件控制) | 多对多任务如何同步

  • v33.03 鸿蒙内核源码分析(消息队列) | 进程间如何异步传递大数据

  • v76.01 鸿蒙内核源码分析(共享内存) | 进程间最快通讯方式

  • v77.01 鸿蒙内核源码分析(消息封装) | 剖析LiteIpc(上)进程通讯内容

  • v78.01 鸿蒙内核源码分析(消息映射) | 剖析LiteIpc(下)进程通讯机制

基本概念

LiteIPCOpenHarmony LiteOS-A内核提供的一种新型IPC(Inter-Process Communication,即进程间通信)机制,为轻量级进程间通信组件,为面向服务的系统服务框架提供进程间通信能力,分为内核实现和用户态实现两部分,其中内核实现完成进程间消息收发、IPC内存管理、超时通知和死亡通知等功能;用户态提供序列化和反序列化能力,并完成IPC回调消息和死亡消息的分发。

我们主要讲解内核态实现部分,本想一篇说完,但发现它远比想象中的复杂和重要,所以分通讯内容和通讯机制上下两篇说。上篇可翻看 鸿蒙内核源码分析(消息封装篇) | 剖析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))));
    }
  • 以上是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;
}

解读

  • 这个函数一定要看明白,重要部分已加上注释,主要干了两件事。

  • 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为消息的处理过程,消息的封装结合上篇理解,接收和发送消息对应的是LiteIpcReadLiteIpcWrite两个函数。

  • 本处只说明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 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。

按功能模块:

  • 前因后果 >> 总目录 | 调度故事 | 内存主奴 | 源码注释 | 源码结构 | 静态站点 | 参考文档 |

  • 基础工具 >> 双向链表 | 位图管理 | 用栈方式 | 定时器 | 原子操作 | 时间管理 |

  • 加载运行 >> ELF格式 | ELF解析 | 静态链接 | 重定位 | 进程映像 |

  • 进程管理 >> 进程管理 | 进程概念 | Fork | 特殊进程 | 进程回收 | 信号生产 | 信号消费 | Shell编辑 | Shell解析 |

  • 编译构建 >> 编译环境 | 编译过程 | 环境脚本 | 构建工具 | gn应用 | 忍者ninja |

  • 进程通讯 >> 自旋锁 | 互斥锁 | 进程通讯 | 信号量 | 事件控制 | 消息队列 | 共享内存 | 消息封装 | 消息映射 |

  • 内存管理 >> 内存分配 | 内存管理 | 内存汇编 | 内存映射 | 内存规则 | 物理内存 |

  • 任务管理 >> 时钟任务 | 任务调度 | 任务管理 | 调度队列 | 调度机制 | 线程概念 | 并发并行 | CPU | 系统调用 | 任务切换 |

  • 文件系统 >> 文件概念 | 文件系统 | 索引节点 | 挂载目录 | 根文件系统 | VFS | 文件句柄 | 管道文件 |

  • 硬件架构 >> 汇编基础 | 汇编传参 | 工作模式 | 寄存器 | 异常接管 | 汇编汇总 | 中断切换 | 中断概念 | 中断管理 |

  • 设备驱动 >> 字符设备 | 控制台 | 远程登录 |

百万注源码 | 处处扣细节

  • 百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。

关注不迷路 | 代码即人生