Skip to content

52_消息封装篇

kuangyufei edited this page Sep 5, 2022 · 1 revision

本篇关键词:、、、

下载 >> 离线文档.鸿蒙内核源码分析(百篇博客分析.挖透鸿蒙内核).pdf

通讯机制相关篇为:

基本概念

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

我们主要讲解内核态实现部分,本想一篇说完,但发现它远比想象中的复杂和重要,所以分两篇说,通讯内容和通讯机制。下篇可翻看 鸿蒙内核源码分析(消息映射篇) | 剖析LiteIpc(下)进程通讯机制 ,通讯的内容就是消息,围绕着消息展开的结构体多达10几个,捋不清它们之间的关系肯定是搞不懂通讯的机制,所以咱们得先搞清楚关系再说流程。下图是笔者读完LiteIPC模块后绘制的消息封装图,可以说LiteIPC是内核涉及结构体最多的模块,请消化理解,本篇将围绕它展开。

系列篇多次提过,内核的每个模块都至少围绕着一个重要结构体展开,抓住了它顺瓜摸藤就能把细节抹的清清楚楚,于LiteIPC,这个结构体就是IpcMsg

运行机制

typedef struct {//IPC 消息结构体
    MsgType        type;       	/**< cmd type, decide the data structure below | 命令类型,决定下面的数据结构*/
    SvcIdentity    target;    	/**< serviceHandle or targetTaskId, depending on type | 因命令类型不同而异*/
    UINT32         code;      	/**< service function code | 服务功能代码*/
    UINT32         flag;		///< 标签
#if (USE_TIMESTAMP == 1)
    UINT64         timestamp;	///< 时间戳,用于验证
#endif
    UINT32         dataSz;    	/**< size of data | 消息内容大小*/
    VOID           *data;		///< 消息的内容,真正要传递的消息,这个数据内容是指spObjNum个数据的内容,定位就靠offsets
    UINT32         spObjNum;	///< 对象数量, 例如 spObjNum = 3时,offsets = [0,35,79],代表从data中读取 0 - 35给第一个对象,依次类推
    VOID           *offsets;	///< 偏移量,注意这里有多少个spObjNum就会有多少个偏移量,详见 CopyDataFromUser 来理解
    UINT32         processID; 	/**< filled by kernel, processId of sender/reciever | 由内核提供,发送/接收消息的进程ID*/
    UINT32         taskID;    	/**< filled by kernel, taskId of sender/reciever | 由内核提供,发送/接收消息的任务ID*/
#ifdef LOSCFG_SECURITY_CAPABILITY	
    UINT32         userID;		///< 用户ID
    UINT32         gid;			///< 组ID
#endif
} IpcMsg;

解读

  • 第一个type,通讯的本质就是你来我往,异常当然也要考虑
      typedef enum {	
          MT_REQUEST,	///< 请求
          MT_REPLY,	///< 回复
          MT_FAILED_REPLY,///< 回复失败
          MT_DEATH_NOTIFY,///< 通知死亡
          MT_NUM
      } MsgType;
    
  • 第二个targetLiteIPC中有两个主要概念,一个是ServiceManager,另一个是Service。整个系统只能有一个ServiceManager,而Service可以有多个。ServiceManager有两个主要功能:一是负责Service的注册和注销,二是负责管理Service的访问权限(只有有权限的任务Task可以向对应的Service发送IPC消息)。首先将需要接收IPC消息的任务通过ServiceManager注册成为一个Service,然后通过ServiceManager为该Service任务配置访问权限,即指定哪些任务可以向该Service任务发送IPC消息。LiteIPC的核心思想就是在内核态为每个Service任务维护一个IPC消息队列,该消息队列通过LiteIPC设备文件向上层用户态程序分别提供代表收取IPC消息的读操作和代表发送IPC消息的写操作。
    /// SVC(service)服务身份证 
      typedef struct {
          UINT32         handle;  //service 服务ID, 范围[0,最大任务ID]
          UINT32         token;	//由应用层带入
          UINT32         cookie;	//由应用层带入
      } SvcIdentity;
    
  • codetimestamp由应用层设定,用于确保回复正确有效,详见CheckRecievedMsg
  • dataSzdataspObjNumoffsets这四个需连在一起理解,是重中之重。其实消息又分成三种类型(对象)
      typedef enum {
          OBJ_FD,		///< 文件句柄
          OBJ_PTR,	///< 指针
          OBJ_SVC		///< 服务,用于设置权限
      } ObjType;
      typedef union {
          UINT32      fd; 	///< 文件描述符
          BuffPtr     ptr;	///< 缓存的开始地址,即:指针,消息从用户空间来时,要将内容拷贝到内核空间
          SvcIdentity  svc;	///< 服务,用于设置访问权限
      } ObjContent;
      typedef struct { // IpcMsg->data 包含三种子消息,也要将它们读到内核空间
          ObjType     type; ///< 类型
          ObjContent  content;///< 内容
      } SpecialObj;
    
    这三种对象都打包在data中,总长度是dataSzspObjNum表示个数,offsets是个整型数组,标记了对应第几个对象在data中的位置,这样就很容易从data读到对象的数据。 UINT32 fd类型对象通讯的实现是通过两个进程间共享同一个fd来实现通讯,具体实现函数为HandleFd
    /// 按句柄方式处理, 参数 processID 往往不是当前进程
      LITE_OS_SEC_TEXT STATIC UINT32 HandleFd(UINT32 processID, SpecialObj *obj, BOOL isRollback)
      {
          int ret;
          if (isRollback == FALSE) { // 不回滚
              ret = CopyFdToProc(obj->content.fd, processID);//目的是将两个不同进程fd都指向同一个系统fd,共享FD的感觉
              if (ret < 0) {//返回 processID 的 新 fd
                  return ret;
              }
              obj->content.fd = ret; // 记录 processID 的新FD, 可用于回滚
          } else {// 回滚时关闭进程FD
              ret = CloseProcFd(obj->content.fd, processID);
              if (ret < 0) {
                  return ret;
              }
          }
    
    SvcIdentity svc用于设置进程<->任务之间彼此访问权限,具体实现函数为HandleSvc
    /// 按服务的方式处理,此处推断 Svc 应该是 service 的简写 @note_thinking
      LITE_OS_SEC_TEXT STATIC UINT32 HandleSvc(UINT32 dstTid, const SpecialObj *obj, BOOL isRollback)
      {
          UINT32 taskID = 0;
          if (isRollback == FALSE) {
              if (IsTaskAlive(obj->content.svc.handle) == FALSE) {
                  PRINT_ERR("Liteipc HandleSvc wrong svctid\n");
                  return -EINVAL;
              }
              if (HasServiceAccess(obj->content.svc.handle) == FALSE) {
                  PRINT_ERR("Liteipc %s, %d\n", __FUNCTION__, __LINE__);
                  return -EACCES;
              }
              if (GetTid(obj->content.svc.handle, &taskID) == 0) {//获取参数消息服务ID所属任务
                  if (taskID == OS_PCB_FROM_PID(OS_TCB_FROM_TID(taskID)->processID)->ipcInfo->ipcTaskID) {//如果任务ID一样,即任务ID为ServiceManager
                      AddServiceAccess(dstTid, obj->content.svc.handle);
                  }
              }
          }
          return LOS_OK;
      }
    
    BuffPtr ptr 是通过指针传值,具体实现函数为HandlePtr,对应结构体为BuffPtr
      typedef struct {
          UINT32         buffSz;  ///< 大小
          VOID           *buff;	///< 内容 内核需要将内容从用户空间拷贝到内核空间的动作 
      } BuffPtr;
    /// 按指针方式处理
      LITE_OS_SEC_TEXT STATIC UINT32 HandlePtr(UINT32 processID, SpecialObj *obj, BOOL isRollback)
      {
          VOID *buf = NULL;
          UINT32 ret;
          if ((obj->content.ptr.buff == NULL) || (obj->content.ptr.buffSz == 0)) {
              return -EINVAL;
          }
          if (isRollback == FALSE) {
              if (LOS_IsUserAddress((vaddr_t)(UINTPTR)(obj->content.ptr.buff)) == FALSE) { // 判断是否为用户空间地址
                  PRINT_ERR("Liteipc Bad ptr address\n"); //不在用户空间时
                  return -EINVAL;
              }
              buf = LiteIpcNodeAlloc(processID, obj->content.ptr.buffSz);//在内核空间分配内存接受来自用户空间的数据
              if (buf == NULL) {
                  PRINT_ERR("Liteipc DealPtr alloc mem failed\n");
                  return -EINVAL;
              }
              ret = copy_from_user(buf, obj->content.ptr.buff, obj->content.ptr.buffSz);//从用户空间拷贝数据到内核空间
              if (ret != LOS_OK) {
                  LiteIpcNodeFree(processID, buf);
                  return ret;
              }//这里要说明下 obj->content.ptr.buff的变化,虽然都是用户空间的地址,但第二次已经意义变了,虽然数据一样,但指向的是申请经过拷贝后的内核空间
              obj->content.ptr.buff = (VOID *)GetIpcUserAddr(processID, (INTPTR)buf);//获取进程 processID的用户空间地址,如此用户空间操作buf其实操作的是内核空间
              EnableIpcNodeFreeByUser(processID, (VOID *)buf);//创建一个IPC节点,挂到可使用链表上,供读取
          } else {
              (VOID)LiteIpcNodeFree(processID, (VOID *)GetIpcKernelAddr(processID, (INTPTR)obj->content.ptr.buff));//在内核空间释放IPC节点
          }
          return LOS_OK;
      }
    
  • processIDtaskID则由内核填充,应用层是感知不到进程和任务的,暴露给它是服务ID,SvcIdentity.handle,上层使用时只需向服务发送/读取消息,而服务是由内核创建,绑定在任务和进程上。所以只要有服务ID就能查询到对应的进程和任务ID。
  • userIDgid涉及用户和组安全模块,请查看系列相关篇。

进程和任务

再说两个结构体 ProcIpcInfoIpcTaskInfo LiteIPC实现的是进程间的通讯,所以在进程控制块中肯定有它的位置存在,即:ProcIpcInfo

typedef struct {
    IpcPool pool;				///< ipc内存池,IPC操作所有涉及内核空间分配的内存均有此池提供
    UINT32 ipcTaskID;			///< 指定能ServiceManager的任务ID
    LOS_DL_LIST ipcUsedNodelist;///< 已使用节点链表,上面挂 IpcUsedNode 节点, 申请IpcUsedNode的内存来自内核堆空间
    UINT32 access[LOSCFG_BASE_CORE_TSK_LIMIT];	///< 允许进程通过IPC访问哪些任务
} ProcIpcInfo;

而进程只是管家,真正让内核忙飞的是任务,在任务控制块中也应有LiteIPC一席之地,即:IpcTaskInfo

typedef struct {
    LOS_DL_LIST     msgListHead;///< 上面挂的是一个个的 ipc节点 上面挂 IpcListNode,申请IpcListNode的内存来自进程IPC内存池
    BOOL            accessMap[LOSCFG_BASE_CORE_TSK_LIMIT]; ///< 此处是不是应该用 LOSCFG_BASE_CORE_PROCESS_LIMIT ? @note_thinking 
    				///< 任务是否可以给其他进程发送IPC消息
} IpcTaskInfo;

两个结构体不复杂,把发送/回复的消息挂到对应的链表上,并提供进程<->任务间彼此访问权限功能accessaccessMap,由谁来设置权限呢 ? 上面已经说过了是 HandleSvc

IPC内存池

还有最后一个结构体IpcPool

typedef struct {//用户空间和内核空间的消息传递通过偏移量计算
    VOID   *uvaddr;	///< 用户空间地址,由kvaddr映射而来的地址,这两个地址的关系一定要搞清楚,否则无法理解IPC的核心思想
    VOID   *kvaddr;	///< 内核空间地址,IPC申请的是内核空间,但是会通过 DoIpcMmap 将这个地址映射到用户空间
    UINT32 poolSize; ///< ipc池大小
} IpcPool;

它是LiteIPC实现通讯机制的基础,是内核设计很巧妙的地方,实现了在用户态读取内核态数据的功能。请想想它是如何做到的 ?

百文说内核 | 抓住主脉络

  • 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
  • 与代码需不断debug一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。
  • 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,百篇博客系列目录如下。

按功能模块:

基础知识 进程管理 任务管理 内存管理
双向链表 内核概念 源码结构 地址空间 计时单位 优雅的宏 钩子框架 位图管理 POSIX main函数 调度故事 进程控制块 进程空间 线性区 红黑树 进程管理 Fork进程 进程回收 Shell编辑 Shell解析 任务控制块 并发并行 就绪队列 调度机制 任务管理 用栈方式 软件定时器 控制台 远程登录 协议栈 内存规则 物理内存 内存概念 虚实映射 页表管理 静态分配 TLFS算法 内存池管理 原子操作 圆整对齐
通讯机制 文件系统 硬件架构 内核汇编
通讯总览 自旋锁 互斥锁 快锁使用 快锁实现 读写锁 信号量 事件机制 信号生产 信号消费 消息队列 消息封装 消息映射 共享内存 文件概念 文件故事 索引节点 VFS 文件句柄 根文件系统 挂载机制 管道文件 文件映射 写时拷贝 芯片模式 ARM架构 指令集 协处理器 工作模式 寄存器 多核管理 中断概念 中断管理 编码方式 汇编基础 汇编传参 链接脚本 内核启动 进程切换 任务切换 中断切换 异常接管 缺页中断
编译运行 调测工具
编译过程 编译构建 GN语法 忍者无敌 ELF格式 ELF解析 静态链接 重定位 动态链接 进程映像 应用启动 系统调用 VDSO 模块监控 日志跟踪 系统安全 测试用例

百万注源码 | 处处扣细节

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

  • < gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码。

关注不迷路 | 代码即人生

期间不断得到小伙伴的支持,有学生,有职场新人,也有老江湖,在此一并感谢,大家的支持是前进的动力。尤其每次收到学生的赞助很感慨,后生可敬。 >> 查看捐助名单

据说喜欢 点赞 + 分享 的,后来都成了大神。:)

Clone this wiki locally