OpenHarmony轻量系统服务管理|跨进程通信过程详解

前言

  之前分析了在同一进程内不同服务间采用的通信机制是消息队列。然而,在不同进程间服务的通信机制并不是鸿蒙系统设计的消息队列,而是采用了共享内存。这是因为同一进程内的各个服务的地址空间是共用的,所以消息队列的首地址一旦分配就是唯一的。而不同进程间的各个服务的地址空间是独立的,消息队列就不再适用。并且共享内存是进程间通信效率最高的,它减少了数据的拷贝次数。

Endpoint

  鸿蒙系统中进程间通信的核心就是endpoint,也可以称为通信端点,它是当前进程与其他进程通信的进出口,所有进程间通信的交互都要经过它。每一个endpoint都有一个SvcIdentity字段来唯一标识当前进程的通信地址。当本进程的endpoint知道目标进程的endpoint地址后,就可以向它发送消息,完成进程间的交互。那么在多进程的环境下,我们如何知道目的进程的endpoint地址呢?   

  这就需要有一个知道所有endpoint通信地址的管理器来帮助我们发现地址。在鸿蒙的代码中指定了一个固定的SvcIdentity地址,作为公开的通信地址,我们把绑定这个地址的endpoint称为主endpoint或知名endpoint。所有的endpoint都要向这个主endpoint注册自己的通信地址。当本进程的endpoint需要向目的进程发送消息时,就可以向主endpoint查询目的进程的通信地址,有了地址以后我们就可以进行通信啦。它的结构定义如下:

  

复制代码
 1 //当前进程和其他进程间通信的通信端点
 2 struct Endpoint {
 3     const char *name;       //端点名称
 4     IpcContext *context;    //ipc上下文
 5     //作为当前进程中服务和功能与其他进程间通信的桥梁,充当查找指定服务时的路由功能
 6     Vector routers;         //routers中保存的是router对象
 7     ThreadId boss;          //主线程,用于接收其他进程发出的消息
 8     uint32 deadId;
 9     int running;            //标识endpoint的启用状态
10     SvcIdentity identity;   //endpoint的身份标识,作为当前进程对外暴露的通信地址
11     RegisterEndpoint registerEP;//指向注册通信端点函数的指针
12     TokenBucket bucket;     //令牌桶,作为消息接收和处理的流控机制
13 };
复制代码

SvcIdentity、PidHandle、Router

  刚介绍了endpoint的作用,并且提到了SvcIdentity,那么我们就趁热打铁介绍一下SvcIdentity是什么以及它的作用。先贴上它的结构定义:

  

复制代码
1 //作为进程间的通信地址
2 typedef struct {
3     uint32_t handle;    //当endpoint注册后,主endpoint会为它生成一个全局唯一的handle标识
4     uint32_t token;     //标识服务和功能在路由表中的表项下标
5     uint32_t cookie;    //暂未看到使用
6 #ifdef __LINUX__
7     IpcContext* ipcContext;    //在linux下才有这个字段,进程通信的上下文
8 #endif
9 } SvcIdentity;
复制代码

  在针对代码的分析中,发现SvcIdentity主要有两个作用,第一个就是通过handle字段唯一标识进程的通信地址。那么,Handle是如何产生的呢?这里不谈主endpoint的初始化、注册和启动过程,直接切入主题。

  这里还是先做一个场景假设。1号进程创建并初始化了主endpoint,2号进程刚启动。

  1. 2号进程先创建和初始化一个endpoint,称为IPC Client,然后向主endpoint发送注册消息,内核会在消息中填充2号进程的进程号(pid)、线程号(tid)和用户号(uid)。

  2. 主endpoint从共享内存中读取到这条消息,会根据线程号tid产生唯一的handle标识,然后将pid、uid和handle保存到pidhandle中,然后将handle作为响应消息发送给2号线程。

  自此2号进程的endpoint就成功注册,并得到了唯一的handle值,它作为当前进程的全局唯一标识。通过这个handle值可以唯一定位到一个进程。现在我们已经可以定位到指定进程了,但是鸿蒙系统业务的执行是通过服务来完成的,所以我们还需要知道如何定位到进程中的服务和功能。这就是SvcIdentity的第二个作用,通过token字段定位进程内的服务和功能。那么,token值是如何产生的呢?这就涉及到进程内服务和功能的注册了。

  在客户端进程中有一个全局变量g_remoteRegister,它维护了当前进程对外的endpoint。而endpoint中有一个vector类型的字段,名为routers。它维护了一系列router对象,每一个router对象都一一对应一个服务和功能。所以也可以称它为“路由表项”,通过它可以唯一定位指定的服务和功能。将服务和功能到endpoint的routers中作为一个“路由表项”,而它的下标就是SvcIdentity的token值。

  以下是pidhandle和router的结构体定义:

1 //用于标识endpoint和进程的关系
2 struct PidHandle {
3     pid_t pid;      //进程ID
4     uid_t uid;      //用户ID
5     uint32 handle;  //向主endpoint注册后得到的唯一标识
6     uint32 deadId;
7 };
复制代码
 1 //路由表项,在进程间通信时,充当服务发现的路由功能
 2 typedef struct Router
 3 {
 4 //这个字段用于在路由表中查找指定路由项时的key值
 5 SaName saName;              //标识服务名称和功能名称    
 6 //通过这个字段就可以定位到进程内部指定的服务、功能和消息队列
 7 Identity identity;          //标识服务名称、功能名称和消息队列
 8     IServerProxy *proxy;    //进程间通信的服务端代理接口
 9     PolicyTrans *policy;    //访问策略,做权限控制
10     uint32 policyNum;       //访问策略的总数
11 } Router;
复制代码

SAstore

  上面介绍了进程间通信的主要数据结构,也提到有一个主endpoint负责管理和维护全局的endpoint地址信息。在这部分就为大家介绍一下系统功能存储的数据结构(SAstore),定义如下:

  

复制代码
 1 //系统功能的存储结构
 2 struct SAStore {
 3     int saSize;      //维护的featureNode节点个数
 4     //root中维护了一组服务的信息,而服务下面还链接着一系列的功能
 5     ListNode *root;  //链表的根,挂着服务结点
 6     int16 mapSize;   //记录maps所指向的连续内存空间的个数,每个大小为sizeof(PidHandle)
 7     int16 mapTop;    //记录maps中存储的元素个数
 8     //maps中从小到大维护着一系列进程id和handle的对应关系
 9     PidHandle *maps; //指向一块有序的连续内存空间,按照PidHandle中的pid从小到大排列。
10 };
复制代码

  虽然看着它的字段并不多,但是它却是进程间通信最基础的信息存储部分,各个服务的交互都需要依赖于它。它的root字段指向一个双向链表,链表的每一个结点都是一个服务信息。而在服务信息中还包含了一个由功能信息组成的双向链表。handle值保存在服务信息结点中,token值保存在功能信息结点中。这就完成了服务和功能以及SvcIdentity信息的存储。对于进程和handle的对应关系存放在maps中。它的图示如下:

 

posted @   沉心慢慢  阅读(784)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示