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中。它的图示如下:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了