先验知识
AMP(Asummetric Multiprocessing System[异构多处理系统])
- 目前的芯片,很多都包含了多个核心,而且之间的架构不同。
比如手机的芯片,包含了CPU、GPU、DSP等不同的处理单元。 - 不同的核心上系统可能不同,Linux/Android,RTOS
- 不同的架构,不同的系统,就组成了AMP
- AMP中的核心,一般会组成主从结构,主核心先启动,准备好环境后加载从核心,而主从核心会通过IPC方式通讯。
IPC(Inter Process Communication[跨进程通信])
这是一个比较泛指的概念,所有不同进程间的通信都可以称为IPC
它包括信息传递,资源共享
什么是RPC
RPC(Remote Procedure Call[远程过程调用])
特指一种隐藏了过程调用时实际通信细节的IPC方法,是IPC的一种。
PRC的工作方式就是“Client-Server”,比如CPU(客户端)去调用DSP(服务端)中的函数
- 客户端调用本地函数,将要处理的参数打包到一个消息中
- 客户端把这个消息发送给服务端提供的方法
- 服务端从消息中解出参数并执行
- 服务端将返回值或者数据再打包发送回客户端
RPC只是一种技术手段,并没有统一的标准。因此具有很多不同的实现,比如gRPC、Dubbo、Thrift和FastRPC等。
FastRPC
FastRPC是一个基于XML-RPC的RPC(远程过程调用)协议,XML-RPC使用二进制格式进行数据序列化。它像XML-RPC一样使用HTTP作为传输协议。FastRPC也是这个协议的一个实现,FastRPC框架允许用户透明地在应用程序和DSP处理器之间进行远程方法调用。
CPU和DSP之间的通信是通过中断共享内存完成的。由于CPU和DSP不共享缓存,因此需要对它们之间处理的所有缓冲区执行维护操作。这些过程至少需要几百微秒。根据系统时钟设置和启用的CPU睡眠模式,每次调用DSP的开销可能会延长到几毫秒。因此,最好将大型任务卸载到DSP上,而不是调用它来处理小型的琐碎任务。
下图描述了对单个方法的调用,其中客户机和对象驻留在不同的处理器上。其中客户端驻留在APPS上(例如,Snapdragon CPU),对象驻留在aDSP(application DSP)上(例如,Hexagon)。stub和skel是自动生成的,您只需要从应用程序处理器调用该函数并在aDSP上提供该函数的实现。
Client | 启动远程调用的用户模式进程 |
---|---|
Stub | 与Client连接,用于自动生成交互代码并编排参数,存根函数是自动生成的代码,用于将函数调用转换为RPC消息。通常情况下,存根代码将编译为单独的本机库,然后将其与客户端链接。存根代码使用libadsprpc.so和libcdsprpc.so库通过相关的ioctl来调用应用程序处理器(AP)上的DSP RPC驱动程序(/dev/adsprpc-smd或/dev/cdsprpc-smd)。 |
ADSPRPC Driver | ADSPRPC核心驱动,用于接收远程调用,安排队列然后在向远程端发送信号后等待响应。DSP RPC内核驱动程序接收远程消息调用,通过共享内存驱动程序(SMD)通道,将队列中的消息发送到DSP上的DSP RPC框架,然后等待响应。 |
ADSPRPC Framework | ADSPRPC框架从队列中取出消息并为其调度处理,DSP RPC框架从队列中删除消息,然后将其分派给骨架动态库(skeleton dynamic library)进行处理。 |
Skel | 为未编排的参数自动生成代码,skel是一个自动生成的库,用于对参数进行解组(unmarshal)并调用目标方法的实现。 |
Object | 实现调用的方法 |
FastRPC的设计围绕几个特性,使移植任务尽可能容易:
● 远程调用的初始化看起来就像在本地调用一样,通过IDL(接口描述语言)提供函数定义。
● 工具自动生成接口头和库来处理参数传递、缓存一致性。
● 对于FastRPC调用动态模块,框架会为你加载aDSP模块。
● FastRPC是同步的。这消除了让内核在异步调用中管理aDSP和APPS之间状态的复杂性。
在支持FastRPC的Android设备上,以下是将你的工作转移到aDSP的步骤:
- 下载并安装Hexagon SDK。
- 用IDL(接口描述语言)描述你的工作的API,IDL语法允许定义函数调用的名称以及在应用程序和DSP处理器之间交换的每个参数的类型。
- 自动生成你的头文件,以及存根和skel库。
- 在DSP上实现你的API,将它和skel库链接到一个共享对象,将共享对象推送到设备。
- 将存根链接到你的android原生应用,并调用你的API。
- (可选)为API创建java绑定,并从APK调用API。
使用ION内存分配器
DSP硬件不太适合处理不连续内存,不连续内存指用户从简单的“malloc”调用中获得的那种内存。Android提供了一个名为ION的内存分配器,它可以为客户端共享缓冲区。有关ION的更多深入文档,请参见此链接。
不同版本的Android提供了略微不兼容的ION实现。下文描述了ION不同版本之间API的一些差异。
ICS和后续的安卓版本
与JB/KK/L等后来的Android版本相比,ICS实现包含不兼容的ion_alloc ioctl结构。
ICS
struct ion_allocation_data {
size_t len;
size_t align;
unsigned int flags;
struct ion_handle *handle;
};
调用者应该设置flags变量来指定堆id
alloc.flags = (0x1 << HEAP_ID);
JB/KK/L
struct ion_allocation_data {
size_t len;
size_t align;
unsigned int heap_mask;
unsigned int flags;
struct ion_handle *handle;
};
调用者应该设置heap_mask变量来指定堆id
alloc.heap_mask = (0x1 << HEAP_ID);
Heap IDS
ION将内存隔离到不同的堆中,用户可以使用这些堆分配内存。Android的堆是在构建时配置的,并且受容量限制。要找出设备上存在哪些堆,请查看设备的dtsi文件。
例如,8998的adsp连续堆定义在kernel/arch/arm/boot/dts/msm8998.dtsi中
adsp_mem: adsp_region {
compatible = "shared-dma-pool";
alloc-ranges = <0 0x00000000 0 0xffffffff>;
reusable;
alignment = <0 0x400000>;
size = <0 0x800000>;
};
它在内核/arch/arm/boot/dts/msm8998-ion.dtsi中注册为堆id 22
qcom,ion-heap@22 { /* ADSP HEAP */
reg = <22>;
memory-region = <&adsp_mem>;
qcom,ion-heap-type = "DMA";
};
这些可能会在不同的Android版本和不同的用例之间发生变化。实现者应该注意让他们使用的堆id在应用程序外部可配置。或者使用一个适合他们的用例。
RPCMem
这个库是使用Android的ION内存分配器的示例代码。
用户应该查阅Android版本中的rpcmem.h和ion文档以获得更多信息。
示例
int main() {
void* buf = 0;
int heapid = RPCMEM_HEAP_ID_SYSTEM;
//call this once at the start of your program
//Note: rpcmem_init is not thread safe
rpcmem_init();
// Refer rpcmem.h for recommendation on heap id
// For remote subsystems with SMMU (ADSP and CDSP), heapid = RPCMEM_HEAP_ID_SYSTEM;
// For remote subsystesm without SMMU (SLPI, mDSP), heapid = RPCMEM_HEAP_ID_CONTIG;
buf = rpcmem_alloc(heapid, RPCMEM_DEFAULT_FLAGS, 4096);
assert(buf);
memset(buf, 0xff, 4096);
rpcmem_free(buf);
//call this once at the end
rpcmem_deinit();
return 0;
}
DSP保护域
由于DSP是一个实时处理器,其稳定性严重影响整个用户体验,因此在DSP软件架构中存在不同的保护域(PDs)。这些PDs确保了核心软件的稳定性和高通专有硬件信息的安全性。在DSP上有各种类型的保护域。每种类型都有不同的特权。
➢ 内核: 访问所有pd的所有内存
➢ Guest OS: 访问自己PD的内存,DSP上所有用户PD的内存,以及一些系统寄存器
➢ User: 只访问自己PD的内存
DSP系统库根据需要对客户操作系统或内核进行系统调用,以访问操作系统服务。FastRPC客户端程序运行在用户pd中。
动态PD VS静态PD
用户pd可以在启动时静态创建,但它们通常是由CPU应用程序在运行时动态创建的,需要将模块卸载到DSP。静态和动态pd都支持共享对象的动态加载。
静态PD
静态pd是在dsp上创建的,以支持特定的用例,如音频和传感器。静态pd允许在CPU上运行的守护进程的帮助下动态加载共享对象。
动态用户PD
每个通过FastRPC使用DSP的CPU用户进程在DSP上会有一个对应的动态用户PD。FastRPC内核驱动管理动态用户PD的生命周期。也就是说,如果CPU用户进程退出,相应的DSP上的用户PD也会被清除。
DSP支持不同类型的动态用户PD环境,包括有符号PD和无符号PD。
每个DSP只能支持有限数量的动态用户PD并发。这个限制取决于每个支持的芯片和DSP的硬件配置。
有签名PD VS 无签名PD
DSPs支持不同类型的动态执行环境(动态PDs),包括有签名和无签名的PDs。
签名的PDs在所有的DSP上都是可用的,它们要求PD中加载的模块(共享对象)都使用数字签名。在PD中加载共享对象时,必须对该签名进行验证。
无签名PDs只被cDSP支持,它们允许DSP模块在没有任何数字签名的情况下加载。
无签名PD是一个沙箱的低权限进程,它允许无签名模块在cDSP上运行。在发生安全泄露的情况下,沙箱会阻止对完整系统功能和数据的访问。无签名的PDs被设计为支持一般的计算应用程序,并且对底层驱动程序有有限的访问权限。无签名PD中的可用/不可用服务将来可能会改变,这取决于持续的安全检查。
未签名的PD可用服务
● 线程创建和线程服务
● 定时器创建和定时器服务
● HVX上下文
● 时钟频率控制
● VTCM
● 缓存的操作
● 映射对应HLOS应用程序分配的HLOS内存
无符号PD的局限性
● 访问受限的驱动器:UBWC/DMA和相机流光对未签名的PD不可用
● 不能访问L2缓存锁定API
● 线程的局限性
线程优先级上限(任何无符号PD线程的最高优先级):64。
每个unsigned PD允许的最大线程数:128
请求无签名卸载
客户端请求一个无签名的动态模块卸载,请求如下:
#pragma weak remote_session_control
if (remote_session_control)
{
struct remote_rpc_control_unsigned_module data;
data.enable = 1;
data.domain = CDSP_DOMAIN_ID;
remote_session_control(DSPRPC_CONTROL_UNSIGNED_MODULE, (void*)&data, sizeof(data));
}
基准测试示例演示了如何使用这种方法在无符号PD中运行代码。
客户端的DSP进程在第一次启动时被实例化为有签名或无签名的PD,并且这个状态不能被改变。因此,对remote_session_control的调用必须在对DSP进行任何FastRPC调用之前进行。来自remote_session_control()函数的成功消息允许客户端在不签名的情况下将动态共享对象卸载到cDSP。
FastRPC域
域是用于加载和执行代码的远程环境。每个HLOS进程可以在不同的DSP上运行并发的PD(会话)。一个HLOS进程只能有一个PD运行在任何给定的DSP上。HLOS进程加载的模块被加载到特定DSP上相应的PD中。下图概述了域的功能。这里的M#对应于PD上加载的模块。如果用户希望将计算offload给多个DSP,可以使用域,而不需要将二进制文件重新链接到不同版本的fastRPC库。
注:从8998起支持域名功能。