IPC
(Inter-Process Communication)与RPC
(Remote Procedure Call)用于实现跨进程通信,不同的是前者使用 Binder 驱动,用于设备内的跨进程通信,后者使用软总线驱动,用于跨设备跨进程通信。
在分析鸿蒙的进程间通信首先我们先了解一下进程间通信的方式
匿名管道
匿名管道是一种半双工的通信方式,它可以在两个进程之间传递数据。匿名管道的特点是数据只能单向流动,而且通常只用于具有亲缘关系的进程之间进行通信,例如父子进程之间命名管道
命名管道与管道类似,但是它可以在不具有亲缘关系的进程之间进行通信。与管道不同的是,命名管道具有一个唯一的名称,可以在文件系统中进行访问信号
信号是一种异步通信方式,它允许一个进程向另一个进程发送一个信号。信号通常用于处理异步事件,例如键盘中断、终端关闭等共享内存
共享内存是一种高效的进程通信方式,它允许多个进程访问同一块物理内存,从而实现数据共享。共享内存的优点是速度快,但是需要处理并发访问和同步问题信号量
信号量是一种进程间同步和互斥的机制,它可以用于控制进程对共享资源的访问。信号量通常用于进程之间的同步和互斥,例如保护共享内存中的数据套接字
套接字是一种进程间通信方式,它可以在不同的计算机之间进行通信。套接字通常用于实现分布式系统和网络通信消息队列
消息队列是一种进程间通信方式,它允许进程之间传递消息。消息队列通常用于进程之间传递结构化的数据,例如进程之间传递命令和数据等
实现原理
IPC 和 RPC 通常采用客户端-服务器(Client-Server)模型,在使用时,请求服务的(Client)一端进程可获取提供服务(Server)一端所在进程的代理(Proxy),并通过此代理读写数据来实现进程间的数据通信,更具体的讲,首先请求服务的(Client)一端会建立一个服务提供端(Server)的代理对象,这个代理对象具备和服务提供端(Server)一样的功能,若想访问服务提供端(Server)中的某一个方法
鸿蒙 IPC 进程间通信机制
鸿蒙 IPC 进程间通信机制是鸿蒙操作系统中用于进程间通信的基础设施,它提供了多种通信方式,包括消息队列、共享内存和远程过程调用等。这些通信方式可根据不同的场景和需求来选择和使用,以实现进程间的数据传输和共享
消息队列
在鸿蒙操作系统中,消息队列由消息队列管理器来管理。进程可以通过向消息队列发送消息来实现数据传输,同时也可以通过接收消息来获取其他进程发送的数据。消息队列管理器负责将消息存储在队列中,并将其发送给目标进程共享内存
在鸿蒙操作系统中,共享内存由共享内存管理器来管理。进程可以通过映射共享内存到自己的地址空间中来获取共享内存的访问权限。通过读写共享内存中的数据,进程可以实现对数据的传输和共享远程过程调用
在鸿蒙操作系统中,远程过程调用由远程过程调用框架来实现。进程可以通过注册自己的函数或方法来提供服务,其他进程可以通过调用这些函数或方法来访问服务。远程过程调用框架负责将调用请求发送给目标进程,并将返回结果返回给调用方
着重讲一下鸿蒙内核进程间通信-共享内存
进程间通信的本质就是让不同的进程能够看到同一份资源,这样就提供了进程间通信的基础,而共享内存也是如此:
共享内存就是再物理内存中申请一片空间然后,如果有两个进程想要通信,那么就可以通过页表挂接到地址空间中,这样两个进程就能看到同一片资源。这样就可以通过对这片空间进行读写就可以了
管理部分
- 初始化共享内存,共享内存是以资源池的方式管理的,上来就为全局变量
g_shmSegs
向内核堆空间申请了g_shmInfo.shmmni
个struct shmIDSource
#define SHM_MNI 192 //共享内存总数 默认192
// 共享内存模块设置信息
struct shminfo {
unsigned long shmmax, shmmin, shmmni, shmseg, shmall, __unused[4];
};
STATIC struct shminfo g_shmInfo = { //描述共享内存范围的全局变量
.shmmax = SHM_MAX,//共享内存单个上限 4096页 即 16M
.shmmin = SHM_MIN,//共享内存单个下限 1页 即:4K
.shmmni = SHM_MNI,//共享内存总数 默认192
.shmseg = SHM_SEG,//每个用户进程可以使用的最多的共享内存段的数目 128
.shmall = SHM_ALL,//系统范围内共享内存的总页数,4096页
};
//共享内存初始化
UINT32 ShmInit(VOID)
{
// ..
ret = LOS_MuxInit(&g_sysvShmMux, NULL);//初始化互斥
g_shmSegs = LOS_MemAlloc((VOID *)OS_SYS_MEM_ADDR, sizeof(struct shmIDSource) * g_shmInfo.shmmni);//分配shm段数组
(VOID)memset_s(g_shmSegs, (sizeof(struct shmIDSource) * g_shmInfo.shmmni),
0, (sizeof(struct shmIDSource) * g_shmInfo.shmmni));//数组清零
for (i = 0; i < g_shmInfo.shmmni; i++) {
g_shmSegs[i].status = SHM_SEG_FREE;//节点初始状态为空闲
g_shmSegs[i].ds.shm_perm.seq = i + 1;//struct ipc_perm shm_perm;系统为每一个IPC对象保存一个ipc_perm结构体,结构说明了IPC对象的权限和所有者
LOS_ListInit(&g_shmSegs[i].node);//初始化节点
}
g_shmUsedPageCount = 0;
return LOS_OK;
}
- 系列篇多次提过,每个功能模块都至少有一个核心结构体来支撑模块的运行,进程是
PCB
,任务是TCB
,而共享内存就是shmIDSource
struct shmIDSource {//共享内存描述符
struct shmid_ds ds; //是内核为每一个共享内存段维护的数据结构
UINT32 status; //状态 SHM_SEG_FREE ...
LOS_DL_LIST node; //节点,挂VmPage
#ifdef LOSCFG_SHELL
CHAR ownerName[OS_PCB_NAME_LEN];
#endif
};
映射使用部分
- 第一步: 创建共享内存 要实现共享内存,首先得创建一个内存段用于共享,干这事的是
ShmGet
/*!
* @brief ShmGet
* 得到一个共享内存标识符或创建一个共享内存对象
* @param key 建立新共享内存对象 标识符是IPC对象的内部名。为使多个合作进程能够在同一IPC对象上汇聚,需要提供一个外部命名方案。
为此,每个IPC对象都与一个键(key)相关联,这个键作为该对象的外部名,无论何时创建IPC结构(通过msgget、semget、shmget创建),
都应给IPC指定一个键, key_t由ftok创建,ftok当然在本工程里找不到,所以要写这么多.
* @param shmflg IPC_CREAT IPC_EXCL
IPC_CREAT: 在创建新的IPC时,如果key参数是IPC_PRIVATE或者和当前某种类型的IPC结构无关,则需要指明flag参数的IPC_CREAT标志位,
则用来创建一个新的IPC结构。(如果IPC结构已存在,并且指定了IPC_CREAT,则IPC_CREAT什么都不做,函数也不出错)
IPC_EXCL: 此参数一般与IPC_CREAT配合使用来创建一个新的IPC结构。如果创建的IPC结构已存在函数就出错返回,
返回EEXIST(这与open函数指定O_CREAT和O_EXCL标志原理相同)
* @param size 新建的共享内存大小,以字节为单位
* @return
*
* @see
*/
INT32 ShmGet(key_t key, size_t size, INT32 shmflg)
{
SYSV_SHM_LOCK();
if (key == IPC_PRIVATE) {
ret = ShmAllocSeg(key, size, shmflg);
} else {
ret = ShmFindSegByKey(key);//通过key查找资源ID
ret = ShmAllocSeg(key, size, shmflg);//分配一个共享内存
}
SYSV_SHM_UNLOCK();
return ret;
}
- 第二步: 进程线性区绑定共享内存 shmat()函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。,
ShmAt
的第一个参数其实是ShmGet
成功时的返回值 ,ShmatVmmAlloc
负责分配一个可用的线性区并和共享内存映射好
/*!
* @brief ShmAt
* 用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。
* @param shm_flg 是一组标志位,通常为0。
* @param shmaddr 指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
* @param shmid 是shmget()函数返回的共享内存标识符
* @return
* 如果shmat成功执行,那么内核将使与该共享存储相关的shmid_ds结构中的shm_nattch计数器值加1
shmid 就是个索引,就跟进程和线程的ID一样 g_shmSegs[shmid] shmid > 192个
* @see
*/
VOID *ShmAt(INT32 shmid, const VOID *shmaddr, INT32 shmflg)
{
struct shmIDSource *seg = NULL;
LosVmMapRegion *r = NULL;
ret = ShmatParamCheck(shmaddr, shmflg);//参数检查
SYSV_SHM_LOCK();
seg = ShmFindSeg(shmid);//找到段
ret = ShmPermCheck(seg, acc_mode);
seg->ds.shm_nattch++;//ds上记录有一个进程绑定上来
r = ShmatVmmAlloc(seg, shmaddr, shmflg, prot);//在当前进程空间分配一个线性区并映射到共享内存
r->shmid = shmid;//把ID给线性区的shmid
r->regionFlags |= VM_MAP_REGION_FLAG_SHM;//这是一个共享线性区
seg->ds.shm_atime = time(NULL);//访问时间
seg->ds.shm_lpid = LOS_GetCurrProcessID();//进程ID
SYSV_SHM_UNLOCK();
return (VOID *)(UINTPTR)r->range.base;
}
- 第三步: 控制/使用 共享内存,这才是目的,前面的都是前戏
/*!
* @brief ShmCtl
* 此函数可以对shmid指定的共享存储进行多种操作(删除、取信息、加锁、解锁等)
* @param buf 是一个结构指针,它指向共享内存模式和访问权限的结构。
* @param cmd command是要采取的操作,它可以取下面的三个值 :
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
IPC_RMID:删除共享内存段
* @param shmid 是shmget()函数返回的共享内存标识符
* @return
*
* @see
*/
INT32 ShmCtl(INT32 shmid, INT32 cmd, struct shmid_ds *buf)
{
SYSV_SHM_LOCK();
switch (cmd) {
case IPC_STAT:
case SHM_STAT://取段结构
ret = LOS_ArchCopyToUser(buf, &seg->ds, sizeof(struct shmid_ds));//把内核空间的共享页数据拷贝到用户空间
if (cmd == SHM_STAT) {
ret = (unsigned int)((unsigned int)seg->ds.shm_perm.seq << 16) | (unsigned int)((unsigned int)shmid & 0xffff); /* 16: use the seq as the upper 16 bits */
}
break;
case IPC_SET://重置共享段
ret = ShmPermCheck(seg, SHM_M);
//从用户空间拷贝数据到内核空间
ret = LOS_ArchCopyFromUser(&shm_perm, &buf->shm_perm, sizeof(struct ipc_perm));
seg->ds.shm_perm.uid = shm_perm.uid;
seg->ds.shm_perm.gid = shm_perm.gid;
seg->ds.shm_perm.mode = (seg->ds.shm_perm.mode & ~ACCESSPERMS) |
(shm_perm.mode & ACCESSPERMS);//可访问
seg->ds.shm_ctime = time(NULL);
#ifdef LOSCFG_SHELL
(VOID)memcpy_s(seg->ownerName, OS_PCB_NAME_LEN, OS_PCB_FROM_PID(shm_perm.uid)->processName,
OS_PCB_NAME_LEN);
#endif
break;
case IPC_RMID://删除共享段
ret = ShmPermCheck(seg, SHM_M);
seg->status |= SHM_SEG_REMOVE;
if (seg->ds.shm_nattch <= 0) {//没有任何进程在使用了
ShmFreeSeg(seg);//释放 归还内存
}
break;
case IPC_INFO://把内核空间的共享页数据拷贝到用户空间
ret = LOS_ArchCopyToUser(buf, &g_shmInfo, sizeof(struct shminfo));
ret = g_shmInfo.shmmni;
break;
case SHM_INFO:
shmInfo.shm_rss = 0;
shmInfo.shm_swp = 0;
shmInfo.shm_tot = 0;
shmInfo.swap_attempts = 0;
shmInfo.swap_successes = 0;
shmInfo.used_ids = ShmSegUsedCount();//在使用的seg数
ret = LOS_ArchCopyToUser(buf, &shmInfo, sizeof(struct shm_info));//把内核空间的共享页数据拷贝到用户空间
ret = g_shmInfo.shmmni;
break;
default:
VM_ERR("the cmd(%d) is not supported!", cmd);
ret = EINVAL;
goto ERROR;
}
SYSV_SHM_UNLOCK();
return ret;
}
- 第四步: 完事了解绑/删除,好聚好散还有下次,在
ShmDt
中主要干了解除映射LOS_ArchMmuUnmap
这件事,没有了映射就不再有关系了,并且会检测到最后一个解除映射的进程时,会彻底释放掉这段共享内存ShmFreeSeg
/**
* @brief 当对共享存储的操作已经结束时,则调用shmdt与该存储段分离
如果shmat成功执行,那么内核将使与该共享存储相关的shmid_ds结构中的shm_nattch计数器值减1
* @attention 注意:这并不从系统中删除共享存储的标识符以及其相关的数据结构。共享存储的仍然存在,
直至某个进程带IPC_RMID命令的调用shmctl特地删除共享存储为止
* @param shmaddr
* @return INT32
*/
INT32 ShmDt(const VOID *shmaddr)
{
LosVmSpace *space = OsCurrProcessGet()->vmSpace;//获取进程空间
(VOID)LOS_MuxAcquire(&space->regionMux);
region = LOS_RegionFind(space, (VADDR_T)(UINTPTR)shmaddr);//找到线性区
shmid = region->shmid;//线性区共享ID
LOS_RbDelNode(&space->regionRbTree, ®ion->rbNode);//从红黑树和链表中摘除节点
LOS_ArchMmuUnmap(&space->archMmu, region->range.base, region->range.size >> PAGE_SHIFT);//解除线性区的映射
(VOID)LOS_MuxRelease(&space->regionMux);
/* free it */
free(region);//释放线性区所占内存池中的内存
SYSV_SHM_LOCK();
seg = ShmFindSeg(shmid);//找到seg,线性区和共享段的关系是 1:N 的关系,其他空间的线性区也会绑在共享段上
ShmPagesRefDec(seg);//页面引用数 --
seg->ds.shm_nattch--;//使用共享内存的进程数少了一个
if ((seg->ds.shm_nattch <= 0) && //无任何进程使用共享内存
(seg->status & SHM_SEG_REMOVE)) {//状态为删除时需要释放物理页内存了,否则其他进程还要继续使用共享内存
ShmFreeSeg(seg);//释放seg 页框链表中的页框内存,再重置seg状态
} else {
seg->ds.shm_dtime = time(NULL);//记录分离的时间
seg->ds.shm_lpid = LOS_GetCurrProcessID();//记录操作进程ID
}
SYSV_SHM_UNLOCK();
开发案例
添加依赖
// FA模型需要从@kit.AbilityKit导入featureAbility
// import { featureAbility } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
绑定 Ability
首先,构造变量 want,指定要绑定的 Ability 所在应用的包名、组件名,如果是跨设备的场景,还需要绑定目标设备 NetworkId(组网场景下对应设备的标识符,可以使用distributedDeviceManager
获取目标设备的 NetworkId);然后,构造变量 connect,指定绑定成功、绑定失败、断开连接时的回调函数;最后,FA 模型使用 featureAbility 提供的接口绑定 Ability,Stage 模型通过 context 获取服务后用提供的接口绑定 Ability。
// FA模型需要从@kit.AbilityKit导入featureAbility
// import { featureAbility } from "@kit.AbilityKit";
import { Want, common } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { distributedDeviceManager } from '@kit.DistributedServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';
let dmInstance: distributedDeviceManager.DeviceManager | undefined;
let proxy: rpc.IRemoteObject | undefined;
let connectId: number;
// 单个设备绑定Ability
let want: Want = {
// 包名和组件名写实际的值
bundleName: "ohos.rpc.test.server",
abilityName: "ohos.rpc.test.server.ServiceAbility",
};
let connect: common.ConnectOptions = {
onConnect: (elementName, remoteProxy) => {
hilog.info(0x0000, 'testTag', 'RpcClient: js onConnect called');
proxy = remoteProxy;
},
onDisconnect: (elementName) => {
hilog.info(0x0000, 'testTag', 'RpcClient: onDisconnect');
},
onFailed: () => {
hilog.info(0x0000, 'testTag', 'RpcClient: onFailed');
}
};
// FA模型使用此方法连接服务
// connectId = featureAbility.connectAbility(want, connect);
let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // UIAbilityContext
// 建立连接后返回的Id需要保存下来,在解绑服务时需要作为参数传入
connectId = context.connectServiceExtensionAbility(want,connect);
// 跨设备绑定
try{
dmInstance = distributedDeviceManager.createDeviceManager("ohos.rpc.test");
} catch(error) {
let err: BusinessError = error as BusinessError;
hilog.error(0x0000, 'testTag', 'createDeviceManager errCode:' + err.code + ', errMessage:' + err.message);
}
// 使用distributedDeviceManager获取目标设备NetworkId
if (dmInstance != undefined) {
let deviceList = dmInstance.getAvailableDeviceListSync();
let networkId = deviceList[0].networkId;
let want: Want = {
bundleName: "ohos.rpc.test.server",
abilityName: "ohos.rpc.test.service.ServiceAbility",
deviceId: networkId,
flags: 256
};
// 建立连接后返回的Id需要保存下来,在断开连接时需要作为参数传入
// FA模型使用此方法连接服务
// connectId = featureAbility.connectAbility(want, connect);
// 第一个参数是本应用的包名,第二个参数是接收distributedDeviceManager的回调函数
connectId = context.connectServiceExtensionAbility(want,connect);
}
服务端处理客户端请求
服务端被绑定的Ability
在onConnect
方法里返回继承自rpc.RemoteObject
的对象,该对象需要实现onRemoteMessageRequest
方法,处理客户端的请求
import { rpc } from '@kit.IPCKit';
import { Want } from '@kit.AbilityKit';
class Stub extends rpc.RemoteObject {
constructor(descriptor: string) {
super(descriptor);
}
onRemoteMessageRequest(code: number, data: rpc.MessageSequence, reply: rpc.MessageSequence, option: rpc.MessageOption): boolean | Promise<boolean> {
// 根据code处理客户端的请求
return true;
}
onConnect(want: Want) {
const robj: rpc.RemoteObject = new Stub("rpcTestAbility");
return robj;
}
}
客户端处理服务端响应
客户端在onConnect
回调里接收到代理对象,调用sendMessageRequest
方法发起请求,在期约(用于表示一个异步操作的最终完成或失败及其结果值)或者回调函数里接收结果。
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
// 使用期约
let option = new rpc.MessageOption();
let data = rpc.MessageSequence.create();
let reply = rpc.MessageSequence.create();
// 往data里写入参数
let proxy: rpc.IRemoteObject | undefined;
if (proxy != undefined) {
proxy.sendMessageRequest(1, data, reply, option)
.then((result: rpc.RequestResult) => {
if (result.errCode != 0) {
hilog.error(0x0000, 'testTag', 'sendMessageRequest failed, errCode: ' + result.errCode);
return;
}
// 从result.reply里读取结果
})
.catch((e: Error) => {
hilog.error(0x0000, 'testTag', 'sendMessageRequest got exception: ' + e);
})
.finally(() => {
data.reclaim();
reply.reclaim();
})
}
// 使用回调函数
function sendRequestCallback(err: Error, result: rpc.RequestResult) {
try {
if (result.errCode != 0) {
hilog.error(0x0000, 'testTag', 'sendMessageRequest failed, errCode: ' + result.errCode);
return;
}
// 从result.reply里读取结果
} finally {
result.data.reclaim();
result.reply.reclaim();
}
}
let options = new rpc.MessageOption();
let datas = rpc.MessageSequence.create();
let replys = rpc.MessageSequence.create();
// 往data里写入参数
if (proxy != undefined) {
proxy.sendMessageRequest(1, datas, replys, options, sendRequestCallback);
}
断开连接
IPC 通信结束后,FA 模型使用 featureAbility 的接口断开连接,Stage 模型在获取 context 后用提供的接口断开连接。
// FA模型需要从@kit.AbilityKit导入featureAbility
// import { featureAbility } from "@kit.AbilityKit";
import { Want, common } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
function disconnectCallback() {
hilog.info(0x0000, 'testTag', 'disconnect ability done');
}
// FA模型使用此方法断开连接
// featureAbility.disconnectAbility(connectId, disconnectCallback);
let proxy: rpc.IRemoteObject | undefined;
let connectId: number;
// 单个设备绑定Ability
let want: Want = {
// 包名和组件名写实际的值
bundleName: "ohos.rpc.test.server",
abilityName: "ohos.rpc.test.server.ServiceAbility",
};
let connect: common.ConnectOptions = {
onConnect: (elementName, remote) => {
proxy = remote;
},
onDisconnect: (elementName) => {
},
onFailed: () => {
proxy;
}
};
// FA模型使用此方法连接服务
// connectId = featureAbility.connectAbility(want, connect);
connectId = this.context.connectServiceExtensionAbility(want,connect);
this.context.disconnectServiceExtensionAbility(connectId);