原文见 https://source.android.com/devices/architecture/hidl/
OverView
HAL接口定义语言(HIDL)是一种接口描述语言,指定接口和他的使用者,它定义了类型和方法的调用。更广泛的说,HIDL是一个用于在可独立编译的代码库之间进行通信的系统。
HIDL旨在用于进程间通信(IPC)。进程之间的通信被称为绑定。对于必须链接到某个进程的库,也可以使用passthough模式(Java中不支持)。
HIDL指定数据结构和方法,组织在接口(类似于一个类)中,这些接口被收集到包中。对于c++和Java程序员来说,HIDL的语法看起来很熟悉,不过有不同的关键字。HIDL还使用了java风格的注解。
HIDL设计
HIDL的目标是可以替换框架,而不必重新构建HALs。HALs将由供应商或SOC制造商构建,并放置在设备上 /vendor 分区,能够被一个OTA替换,而无需重新编译HALs。
HIDL平衡了下列考虑:
- 互操作性 在用各种架构、工具链和构建配置编译的进程之间创建可靠的可互操作的接口。HIDL接口是版本化的,在发布后不能更改。
- 有效性 HIDL仅在RPC的参数中使用,避免了棘手的内存所有权问题。不能有效地从方法返回的值通过回调函数返回。无论是将数据传递到HIDL,还是从HIDL中接收数据,都不会改变数据的所有权,调用函数始终保持所有权。数据只需要在被调用函数的持续时间内持续,并在被调用函数返回后立即销毁。
HIDL语法
HIDL语言类似于C(但不使用C预处理器)。
- /* */ 多行注释
- // 单行注释
- [empty] 意味着这个术语可能是空的
- ? 紧接着文字或术语意味着它是可选的
- … 指示序列包含零个或多个项,用标点符号分隔开。HIDL中没有变量参数
- 逗号分离序列元素
- 分号终止每个元素,包括最后一个元素
- 大写是非终结符
- 斜体是一个关键字,例如整数或标识符(标准C解析规则)
- 常量表达式符合C语言标准(such as 1 + 1 and 1L << 3)
- import_name是一个包或接口名称,符合HIDL版本中描述
- 小写的单词是文字量。
例子
ROOT =
PACKAGE IMPORTS PREAMBLE { ITEM ITEM ... } // not for types.hal
PREAMBLE = interface identifier EXTENDS
| PACKAGE IMPORTS ITEM ITEM... // only for types.hal; no method definitions
ITEM =
ANNOTATIONS? oneway? identifier(FIELD, FIELD ...) GENERATES?;
| struct identifier { SFIELD; SFIELD; ...}; // Note - no forward declarations
| union identifier { UFIELD; UFIELD; ...};
| enum identifier: TYPE { ENUM_ENTRY, ENUM_ENTRY ... }; // TYPE = enum or scalar
| typedef TYPE identifier;
VERSION = integer.integer;
PACKAGE = package android.hardware.identifier[.identifier[...]]@VERSION;
PREAMBLE = interface identifier EXTENDS
EXTENDS = <empty> | extends import_name // must be interface, not package
GENERATES = generates (FIELD, FIELD ...)
// allows the Binder interface to be used as a type
// (similar to typedef'ing the final identifier)
IMPORTS =
[empty]
| IMPORTS import import_name;
TYPE =
uint8_t | int8_t | uint16_t | int16_t | uint32_t | int32_t | uint64_t | int64_t |
float | double | bool | string
| identifier // must be defined as a typedef, struct, union, enum or import
// including those defined later in the file
| memory
| pointer
| vec<TYPE>
| bitfield<TYPE> // TYPE is user-defined enum
| fmq_sync<TYPE>
| fmq_unsync<TYPE>
| TYPE[SIZE]
FIELD =
TYPE identifier
UFIELD =
TYPE identifier
| struct identifier { FIELD; FIELD; ...} identifier;
| union identifier { FIELD; FIELD; ...} identifier;
SFIELD =
TYPE identifier
| struct identifier { FIELD; FIELD; ...};
| union identifier { FIELD; FIELD; ...};
| struct identifier { FIELD; FIELD; ...} identifier;
| union identifier { FIELD; FIELD; ...} identifier;
SIZE = // Must be greater than zero
constexpr
ANNOTATIONS =
[empty]
| ANNOTATIONS ANNOTATION
ANNOTATION =
| @identifier
| @identifier(VALUE)
| @identifier(ANNO_ENTRY, ANNO_ENTRY ...)
ANNO_ENTRY =
identifier=VALUE
VALUE =
"any text including \" and other escapes"
| constexpr
| {VALUE, VALUE ...} // only in annotations
ENUM_ENTRY =
identifier
| identifier = constexpr
术语
符号 | 描述 |
---|---|
binderized | 表明HIDL是在进程之间的远程过程调用中使用的,它是通过一个类似于binder的机制实现的。 |
callback,asynchronous | 由HAL用户提供的接口,传递给HAL(通过HIDL方法),并由HAL调用,以在任何时候返回数据。 |
callback, synchronous | 从服务端的HIDL方法实现向客户机返回数据。不用于返回void或一个原始值 |
client | 调用指定接口的方法的进程。HAL或框架进程可以是一个接口的客户端或另一个接口的服务端 |
extends | 表示将方法和/或类型添加到另一个接口的接口。接口只能扩展一个其他接口。可用于同一包名(例如供应商扩展)中的较小版本增量,或建立在旧包上的新包 |
generates | 指示将值返回给客户端的接口方法。若要返回一个非原始值,或一个以上的值,则生成同步回调函数 |
interface | 方法和类型的集合,类似C++或JAVA中的类,接口中的所有方法都以相同的方向调用:客户端进程调用由服务端实现的方法 |
oneway | 当应用于HIDL方法时,指示该方法不返回任何值,并且不阻塞 |
package | 同一版本中,接口和数据类型的集合 |
passthrough | HIDL模式,服务端是个共享库,在passthrough 模式下,客户端和服务器是相同的进程,但分开的代码库。仅用于将legacy 代码库引入HIDL模型中 |
server | 实现接口方法的进程 |
transport | 在服务器和客户端之间移动数据的HIDL基础结构 |
version | 包的版本。由两个整数组成,主数和小数。小的版本迭代可以添加(但不改变)类型和方法 |
接口&包
HIDL是围绕接口构建的,是面向对象语言中用来定义行为的抽象类型。每个接口都是包的一部分。
包
包名可以有子级,如 package.subpackage.发布的HIDL包的根目录是 hardware/interfaces 或vendor/vendorName(例如 vendor/google)。包名在根目录下形成一个或多个子目录;定义包的所有文件都在同一目录中。例如,包 android.hardware.example.extension.light@2.0 可以在 hardware/interfaces/example/extension/light/2.0.中发现。
下表列出了包前缀和位置:
包前缀 | 位置 |
---|---|
android.hardware.* | hardware/interfaces/* |
android.frameworks.* | frameworks/hardware/interfaces/* |
android.system.* | system/hardware/interfaces/* |
android.hidl.* | system/libhidl/transport/* |
包目录包含扩展名为HAL的文件。每个文件必须包含一个包声明,命名包和版本是文件的一部分。文件types.hal,如果存在,不定义接口,而是定义对包中的每个接口都可访问的数据类型。
接口定义
除了types.hal,其他 .hal 文件定义一个接口。接口通常定义如下:
interface IBar extends IFoo { // IFoo is another interface
// embedded types
struct MyStruct {/*...*/};
// interface methods
create(int32_t id) generates (MyStruct s);
close();
};
没有显式 extends 声明的接口,隐式扩展自 android.hidl.base@1.0::IBase 。IBase接口,隐式导入,声明了一些不应该且不能在用户定义的接口中重新声明的保留方法。这些方法包括:
- ping
- interfaceChain
- interfaceDescriptor
- notifySyspropsChanged
- linkToDeath
- unlinkToDeath
- setHALInstrumentation
- getDebugInfo
- debug
- getHashChain
导入
import语句是HIDL机制,用于访问包接口和另一个包中的类型。import 涉及两个实体:
- importing 实体可以是一个包或者接口
- imported 实体也可以是一个包或者接口
importing 实体由 import 语句的位置决定,当语句在包的 types.hal 内,被导入的东西可以被整个包看到;这是一个包级导入。当语句位于接口文件中时,importing 实体就是接口本身;这是一个接口级的导入。
imported 实体由 import 关键字后的值决定。该值不需要是完全限定的名称;如果省略了一个组件,它会自动填充当前包中的信息。对于完全限定值,支持以下导入案例:
- 全包导入 如果值是包名和版本,整个包背包含进 importing 实体
- 部分导入 如果值是一个接口,包的 types.hal 和 接口被引入 importing 实体。如果值是 types.hal 中定义的 UDT,只有该 UDT 被引入 importing 实体。
- 只引入类型 如果值使用上面部分导入的语法,但是接口前加关键字,只有指定包的 types.hal 中的 UDTs 被引入。
importing 实体可以访问以下组件:
- 被导入包的types.hal中共有UDTs
- 被导入包的接口,以便调用它们,将句柄传递给它们并/或从它们继承
import语句使用完全限定类型名称语法提供包或接口的名称和版本导入:
import android.hardware.nfc@1.0; // import a whole package
import android.hardware.example@1.0::IQuux; // import an interface and types.hal
import android.hardware.example@1.0::types; // import just types.hal
接口继承
一个接口可以使过去定义接口的扩展,扩展可以试下面三种类型:
- 接口可以将功能添加到另一个接口,并将其API添加到其中
- 包可以将功能添加到另一个接口,并将其API添加到其中
- 接口可以从包或特定接口导入类型
一个接口只能扩展一个其他的接口,在包中的每个接口都有一个非零的小版本号,必须在包的前一个版本中扩展一个接口。例如,如果包 derivative 中一个接口 IBar 版本4.0 扩展于 original 包中的一个接口 IFoo 版本1.2,并且版本1.3的包 original 已经被创建,IBar 版本4.1 不能扩展 IFoo 版本1.3,相应的 IBar 版本4.1 必须扩展 IBar 版本4.0, IBar 5.0可以扩展IFoo 1.3。
接口扩展并不意味着在生成的代码中库依赖或cross-HAL包含,它们只是在HIDL级别导入数据结构和方法定义。HAL的每一种方法都必须在HAL中实现。
厂商扩展
在某些情况下,供应商扩展将作为核心接口基础对象的一个子类实现。相同的对象将在base HAL名称和版本下注册,并在扩展的(供应商)HAL名称和版本下。
版本
包是有版本的,接口与其包的版本相同。版本是用两个整数表示的。
- 主要 版本不是向后兼容的。增加主版本号将次要版本号重置为0。
- 次要 版本是向后兼容的。递增较小的数字表示新版本完全向后兼容以前的版本。可以添加新的数据结构和方法,但不可能更改现有的数据结构或方法签名。
对于与框架的更广泛的兼容性,HAL的多个主要版本可以同时出现在设备上。虽然多个小版本也可以出现在一个设备上,因为小版本是向后兼容的,所以没有理由支持每个主要版本的最新版本。
接口布局总结
这部分总结如何管理一个HIDL接口包和整合HIDL部分的信息
术语 | 定义 |
---|---|
Application Binary Interface (ABI) | 应用程序编程接口+任何需要的二进制连接 |
Fully-qualified name (fqName) | 名称以区别hidl类型。例如:android.hardware.foo@1.0::IFoo |
Package | 包包含HIDL接口和类型。例子:android.hardware.foo@1.0 |
Package root | 包含HIDL接口的根包。示例:android.hardware.foo@1.0 |
Package root path | 根包在Android源代码树中的位置 |
每个文件都可以从包的根映射和完全限定的名称中找到。
根包被指定成 hidl-gen 作为参数 -r android.hardware:hardware/interfaces。例如,如果包是 vendor.awesome.foo@1.0::IFoo 并且 hidl-gen 定为 -r vendor.awesome:some/device/independent/path/interfaces,那么接口文件位于 $ANDROID_BUILD_TOP/some/device/independent/path/interfaces/foo/1.0/IFoo.hal。
在实践中,建议供应商或OEM厂商将其标准接口放在 vendor.awesome 中。在选择了包路径之后,就不能更改,因为他是接口ABI的一部分。
包路径映射应该是唯一的
例如,如果已经有 -rsome.package:PATH_A 和 -rsome.package:PATH_B,PATH_A必须等于PATH_B,以获得一致的接口目录。
根包必须有一个版本控制文件
如果你创建了一个包路径如 -r vendor.awesome:vendor/awesome/interfaces,你应该创建一个文件 ANDROID_BUILD_TOP/vendor/awesome/interfaces/current.txt,其中应该包含在hidl-gen中使用-Lhash选项的接口的散列。
接口位于设备独立的位置
实际上,建议在分支之间共享接口。这允许在不同的设备和用例中最大限度地重用代码并最大限度地测试代码。
Interface Hashing
该文档描述了HIDL接口哈希,这是一种防止接口意外更改并确保接口更改的机制。这一机制是必需的,因为HIDL接口是版本化的,这意味着在一个接口被发布之后,除了在应用程序二进制接口(ABI)保存方式(比如注释纠正)之外,它不能被更改。
布局
每个根包目录必须包含一个 current.txt 文件列出了所以发布的 HIDL 接口文件。
# current.txt files support comments starting with a ‘#' character
# this file, for instance, would be vendor/foo/hardware/interfaces/current.txt
# Each line has a SHA-256 hash followed by the name of an interface.
# They have been shortened in this doc for brevity but they are
# 64 characters in length in an actual current.txt file.
d4ed2f0e...995f9ec4 vendor.awesome.foo@1.0::IFoo # comments can also go here
# types.hal files are also noted in types.hal files
c84da9f5...f8ea2648 vendor.awesome.foo@1.0::types
# Multiple hashes can be in the file for the same interface. This can be used
# to note how ABI sustaining changes were made to the interface.
# For instance, here is another hash for IFoo:
# Fixes type where "FooCallback" was misspelled in comment on "FooStruct"
822998d7...74d63b8c vendor.awesome.foo@1.0::IFoo
Hashing with hidl-gen
您可以将散列手动或使用hidl-gen添加到 current.txt 文件。下面的代码片段提供了命令的示例,你可以使用hidl-gen来管理current.txt .txt文件(hashes被缩短):
$ hidl-gen -L hash -r vendor.awesome:vendor/awesome/hardware/interfaces -r android.hardware:hardware/interfaces -r android.hidl:system/libhidl/transport vendor.awesome.nfc@1.0::types
9626fd18...f9d298a6 vendor.awesome.nfc@1.0::types
$ hidl-gen -L hash -r vendor.awesome:vendor/awesome/hardware/interfaces -r android.hardware:hardware/interfaces -r android.hidl:system/libhidl/transport vendor.awesome.nfc@1.0::INfc
07ac2dc9...11e3cf57 vendor.awesome.nfc@1.0::INfc
$ hidl-gen -L hash -r vendor.awesome:vendor/awesome/hardware/interfaces -r android.hardware:hardware/interfaces -r android.hidl:system/libhidl/transport vendor.awesome.nfc@1.0
9626fd18...f9d298a6 vendor.awesome.nfc@1.0::types
07ac2dc9...11e3cf57 vendor.awesome.nfc@1.0::INfc
f2fe5442...72655de6 vendor.awesome.nfc@1.0::INfcClientCallback
$ hidl-gen -L hash -r vendor.awesome:vendor/awesome/hardware/interfaces -r android.hardware:hardware/interfaces -r android.hidl:system/libhidl/transport vendor.awesome.nfc@1.0 >> vendor/awesome/hardware/interfaces/current.txt
hidl-gen包含hashes生成的每个接口定义库,可以通过调用IBase::getHashChain来检索。当hidl-gen正在编译一个接口时,它检查根包的目录下的current.txt 文件,以查看HAL是否已更改:
- 如果没有找到HAL的散列,则该接口被认为是未发布的(在开发中),并且编译进行。
- 如果发现哈希,则根据当前接口检查它们:1.如果接口与哈希匹配,编译将继续进行 2.如果接口与哈希不匹配,则停止编译,因为这意味着先前发布的接口正在更改(对于ABI保存的更改,current.txt 文件必须在编译之前进行修改,所有其他更改都应在界面的次要版本或主要版本升级中进行)
ABI stability
应用程序二进制接口(ABI)包括二进制链接/调用约定/等。如果ABI/API改变,接口就不再与用官方接口编译的通用 system.img 一起工作。
确保接口版本化和ABI稳定是有几个原因的关键:
- 它确保您的实现可以通过供应商测试套件(VTS),这使您能够跟踪只做框架的OTAS。
- 作为一个OEM,它使您能够提供一个直接使用和兼容的板支持包(BSP)。
- 它可以帮助您跟踪接口可以发布。考虑 current.txt 一个接口目录的映射,它允许您查看包根中提供的所有接口的历史和状态。
当为已经在 current.txt 中有条目的接口添加新的哈希时,请确保只添加表示保持ABI稳定性的接口的哈希。回顾以下类型的变化:
Changes allowed | Changes not allowed |
---|---|
改变注释 | 重新排序参数、方法等 |
改变参数名 | 重命名接口或将其移动到新的包。 |
改变返回值名 | 重命名包 |
改变注解 | 在接口中的任何地方添加方法/结构字段等 |
会打破一个C++虚函数表任何事 | |
… |
Services & Data Transfer
本节介绍如何通过调用HAL文件中接口定义的方法来注册和发现服务以及如何向服务发送数据。
注册服务
HIDL接口服务端(实现接口的对象)可以注册为命名的服务。注册名不需要与接口或包名相关。如果没有指定名称,则使用“默认”名称;这应该用于不需要注册同一接口的两个实现的HALS。例如,在每个接口中定义的C++调用服务:
status_t status = myFoo->registerAsService();
status_t anotherStatus = anotherFoo->registerAsService("another_foo_service"); // if needed
HIDL接口的版本包含在接口本身中。它与服务注册自动关联,并且可以通过每个HIDL接口上的方法调用(android::hardware::IInterface::getInterfaceVersion())来检索。服务端对象不需要注册,可以通过HIDL方法参数传递到另一个进程,这将使HIDL方法调用到服务端。
Discovering services
客户端代码的请求是按名称和版本对给定接口进行的,在希望的HAL类上调用getService:
// C++
sp<V1_1::IFooService> service = V1_1::IFooService::getService();
sp<V1_1::IFooService> alternateService = 1_1::IFooService::getService("another_foo_service");
// Java
V1_1.IFooService; service = V1_1.IFooService.getService(true /* retry */);
V1_1.IFooService; alternateService = 1_1.IFooService.getService("another", true /* retry */);
每个版本的HIDL接口都被视为一个单独的接口。因此,IFooService版本1.1和IFooService版本2.2都可以注册为“foo_service”,并且 getService(“foo_service”) 在任一接口上获得该接口的注册服务。这就是为什么在大多数情况下,不需要为注册或发现提供名称参数(意思是“默认”)。
供应商接口对象也在返回接口的传输方法中扮演一个角色。对于android.hardware.foo@1.0 中的接口 IFoo,IFoo::getService 返回的接口总是使用在条目存在的设备清单中声明的 android.hardware.foo 的传输方法;如果传输方法不可用,则返回 nullptr。
在某些情况下,可能需要立即继续,即使没有得到服务。当客户端想要管理服务通知本身时,或者在诊断程序需要获取所有 hwservices 并检索它们(如ATRACH)时,这可能发生。在这种情况下,提供额外的 API tryGetService在C++中或 getService 在java中。java 中 legacy API getService 必须使用服务通知。使用此API并不能避免服务器在客户机请求它使用这些无重试API时注册自己的竞态条件。
Service death notifications
当服务死亡时,希望得到通知的客户可以接收由框架交付的死亡通知。要接收通知,客户必须:
- HIDL 类/接口 hidl_death_recipient 的子类(C++代码,不是HIDL)
- 重载它的 serviceDied() 方法
- 实例化 hidl_death_recipient 子类的一个对象
- 在服务上调用linkToDeath()方法来监视,传入IDeathRecipient的接口对象。此方法不接受死亡接收者或其调用的代理的所有权。
伪代码示例(c++和Java类似):
class IMyDeathReceiver : hidl_death_recipient {
virtual void serviceDied(uint64_t cookie,
wp<IBase>& service) override {
log("RIP service %d!", cookie); // Cookie should be 42
}
};
....
IMyDeathReceiver deathReceiver = new IMyDeathReceiver();
m_importantService->linkToDeath(deathReceiver, 42);
同一死亡接收人可以在多个不同的服务上注册。
Data transfer
数据可以通过调用接口中.hal文件定义的方法发送到服务。有两种方法:
- 阻塞方法等待直到服务器产生结果
- 单向方法只向一个方向发送数据,不阻塞。如果RPC调用中的数据量超过了实现限制,那么调用可能会阻塞或返回一个错误指示(行为尚未确定)。
不返回值但未声明为oneway的方法仍然阻塞。
在HIDL接口中声明的所有方法都被调用在单一方向上,无论是从HAL还是到HAL。接口没有指定要调用哪个方向。HAL结构应该在HAL包中提供两个(或多个)接口,并为每个进程提供适当的接口。客户端和服务器端是就接口的调用方向而言(即HAL可以是一个接口的服务端和另一个接口的客户端)。
Callbacks
回调这个词指的是两个不同的概念,分别是同步回调和异步回调。
同步回调用于一些返回数据的HIDL方法。HIDL方法通过回调函数返回多个值(或返回一个非原始类型的值)。如果只返回一个值,并且它是一个原始类型,则不使用回调,并且从方法返回值。服务端实现HIDL方法,客户端实现回调。
异步回调允许HIDL接口的服务端发起调用。这是通过将第二个接口传入第一个接口的实例来完成的。第一个接口的客户端必须充当第二个接口的服务器。第一个接口的服务端可以调用第二个接口对象上的方法。例如,HAL实现可以异步地将信息发送到正在使用它的进程中,通过调用在该进程创建和服务的接口对象上的方法。用于异步回调的接口中的方法可能是 blocking(并可能向调用者返回值)或oneway。
为了简化内存的所有权,方法调用和回调只接受参数,不支持 out 参数或inout参数。
Per-transaction limits
HIDL生成的头文件声明了目标语言(c++或Java)中的必要类型、方法和回调。hidl定义的方法和回调的原型对于客户端和服务端代码都是一样的。HIDL系统提供了调用方的方法的代理实现,该方法负责组织IPC传输的数据,以及将数据传递到方法的开发人员实现的callee方面的存根代码。
函数的调用者(HIDL方法或回调)拥有传入函数的数据结构的所有权,并在调用后保留所有权;在所有情况下,callee不需要释放存储。
- 在c++中,数据可能是只读的(试图写入它可能会导致分段错误),并且在调用期间是有效的。客户端可以深度复制数据,将其传播到调用之外。
- 在Java中,代码接收数据的本地副本(一个普通的Java对象),它可以保存和修改或允许垃圾收集。
Non-RPC data transfer
HIDL有两种方法可以在不使用RPC调用的情况下传输数据:共享内存和快速消息队列(FMQ),它们都只在c++中支持。
- 共享内存。内置的HIDL类型内存用于传递表示已分配的共享内存的对象。可以在接收过程中使用,以映射共享内存。
- 快消息队列(FMQ)。HIDL提供了一个模板消息队列类型,实现了不等待消息传递。它不会在passthrough或binder化模式中使用内核或调度器(设备间通信不会具有这些属性)。通常,HAL设置队列的末尾,创建一个对象,该对象可以通过内置HIDL类型MQDescriptorSync或MQDescriptorUnsync的参数通过RPC传递。接收进程可以使用该对象设置队列的另一端。同步队列不允许溢出,并且只能有一个阅读器。不同步队列被允许上溢,并且可以有许多阅读器,每个阅读器必须及时读取数据或丢失数据。两种类型都不允许下溢(从空队列读取),每种类型只能有一个写入器。
Fast Message Queue (FMQ)
HIDL的远程过程调用(RPC)基础设施使用绑定机制,这意味着调用涉及开销,需要内核操作,并可能触发调度器操作。但是,对于数据必须在开销较小且没有内核参与的进程之间传输,则使用快速消息队列(FMQ)系统。
FMQ使用所需的属性创建消息队列。MQDescriptorSync或MQDescriptorUnsync对象可以通过HIDL RPC调用发送,并由接收进程使用,以访问消息队列。
快速消息队列仅在c++中得到支持。
MessageQueue类型
Android支持两种队列类型(称为 flavors):
- 非同步的队列被允许上溢,并且可以有多个读取对象;每个读取对象必须及时读取数据或丢失数据。
- 同步队列不允许溢出,并且只能有一个阅读器。
两种队列类型都不允许下溢(从空队列读取),并且只能有一个写入器。
Unsynchronized
一个非同步的队列只有一个写入器,但是可以有任意数量的读取器。队列有一个写位置;但是,每个阅读器都记录自己的独立读取位置。
写到队列总是成功(不检查溢出),只要它们不大于配置的队列容量(大于队列容量立即失败)。由于每个阅读器可能有不同的读取位置,而不是等待每个读取器读取所有数据,所以每当新的写需要空间时,数据就被允许从队列中掉下来。
读取操作负责在数据从队列末尾掉落之前取出数据。读取比可用的数据要多的读取操作会立即失败(如果非阻塞)或等待足够的数据可用(如果阻塞)。
如果一个读取没有跟上写入的进度,那么写入的数据量和未读到的数据量大于队列容量,那么下一个读取数据不会返回数据;相反,它会重置读取器读取的位置,使其等于最新的写入位置,然后返回失败。如果在检查可用数据置之后溢出,但在下一次读取之前,它显示的数据比队列容量要多,表明溢出已经发生。(如果在检查可用数据和试图读取该数据之间的队列溢出,惟一显示溢出的是读取失败。)
Synchronized
同步队列有一个写入器和一个读取器,它们有一个写入位置和一个读取位置。不可能比队列有更多的数据,或者读取比队列当前保存的数据更多的数据。根据阻塞或非阻塞写入或读取函数的调用,试图超过可用空间或数据的尝试将立即返回失败或阻塞,直到完成所需的操作。试图读取或写入比队列容量更多的数据总是会立即失败。
Setting up an FMQ
消息队列需要多个MessageQueue对象:一个写入,一个或多个读取。没有明确的对象用于写作或读取的配置;它取决于用户,确保没有对象同时用于读取和写入,最多有一个写入,并且,对于同步队列,最多只有一个读取。
创建第一个消息队列对象
一句话创建并配置消息队列
#include <fmq/MessageQueue.h>
using android::hardware::kSynchronizedReadWrite;
using android::hardware::kUnsynchronizedWrite;
using android::hardware::MQDescriptorSync;
using android::hardware::MQDescriptorUnsync;
using android::hardware::MessageQueue;
....
// For a synchronized non-blocking FMQ
mFmqSynchronized =
new (std::nothrow) MessageQueue<uint16_t, kSynchronizedReadWrite>
(kNumElementsInQueue);
// For an unsynchronized FMQ that supports blocking
mFmqUnsynchronizedBlocking =
new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
(kNumElementsInQueue, true /* enable blocking operations */);
- MessageQueue< T, flavor>(numElements) 创建并初始化一个支持消息队列功能的对象
- MessageQueue< T, flavor>(numElements, configureEventFlagWord) 创建并初始化一个对象,该对象支持带有阻塞的消息队列功能
- flavor 可以为同步队列的kSynchronizedReadWrite,也可以为不同步的队列的kUnsynchronizedWrite。
- uint16_t(在本例中)可以是任何不包含嵌套缓冲区(没有字符串或vec类型)、句柄或接口的hidl定义类型。
- kNumElementsInQueue表示队列的大小;它确定将为队列分配的共享内存缓冲区的大小。
Creating the second MessageQueue object
消息队列的第二部分是使用从第一步创建获得的MQDescriptor对象。MQDescriptor对象通过调用HIDL RPC发送到将保存消息队列的第二端的进程。MQDescriptor包含关于队列的信息,包括:
- 用于映射缓冲区和写指针的信息
- 用于映射读指针的信息(如果队列是同步的)
- 用于映射事件标记字的信息(如果队列是阻塞的)
- 对象类型(< T, flavor>),队列元素的 HIDL 定义类型和队列 flavor(同步或非同步)
MQDescriptor对象可以被用于构建一个 MessageQueue 对象。
MessageQueue<T, flavor>::MessageQueue(const MQDescriptor<T, flavor>& Desc, bool resetPointers)
resetPointers 参数指示是否在创建MessageQueue对象时将读和写位置重置为0。在非同步队列中,在创建过程中,读取位置(位于非同步队列中的每个MessageQueue对象的本地位置)总是设置为0。通常,MQDescriptor在创建第一个消息队列对象时被初始化。为了对共享内存进行额外的控制,您可以手动设置MQDescriptor(在system/libhidl/base/include/hidl/MQDescriptor.h中定义MQDescriptor),然后创建本节中描述的每个MessageQueue对象。
Blocking queues and event flags
默认情况下,队列不支持阻塞读/写。有两种阻塞读/写调用:
短式,有三个参数(数据指针、条目数、超时)。支持阻塞单个队列上的读/写操作。在使用此式时,队列将在内部处理事件标志和位掩码,并且第一个消息队列的第二个参数初始化必须使用true。例如:
// For an unsynchronized FMQ that supports blocking
mFmqUnsynchronizedBlocking =
new (std::nothrow) MessageQueue<uint16_t, kUnsynchronizedWrite>
(kNumElementsInQueue, true /* enable blocking operations */);长式,有六个参数(包括事件标志和位掩码)。支持在多个队列之间使用共享的EventFlag对象,并允许指定要使用的通知位掩码。在这种情况下,必须为每个读和写调用提供事件标志和位掩码。
对于长式,EventFlag可以在每个 readBlocking() 和 writeBlocking() 调用中显式地提供。其中一个队列可以使用内部事件标志进行初始化,然后使用getEventFlagWord()从该队列的MessageQueue对象中提取它,并用于在每个进程中创建EventFlag对象,以便与其他FMQs一起使用。或者,EventFlag对象可以用任何合适的共享内存初始化。
通常,每个队列只能使用非阻塞、短式阻塞或长式阻塞中的一个。混合它们不是错误,但是需要仔细的编程才能得到想要的结果。
Using the MessageQueue
MessageQueue对象的公共API是:
size_t availableToWrite() // Space available (number of elements).
size_t availableToRead() // Number of elements available.
size_t getQuantumSize() // Size of type T in bytes.
size_t getQuantumCount() // Number of items of type T that fit in the FMQ.
bool isValid() // Whether the FMQ is configured correctly.
const MQDescriptor<T, flavor>* getDesc() // Return info to send to other process.
bool write(const T* data) // Write one T to FMQ; true if successful.
bool write(const T* data, size_t count) // Write count T's; no partial writes.
bool read(T* data); // read one T from FMQ; true if successful.
bool read(T* data, size_t count); // Read count T's; no partial reads.
bool writeBlocking(const T* data, size_t count, int64_t timeOutNanos = 0);
bool readBlocking(T* data, size_t count, int64_t timeOutNanos = 0);
// Allows multiple queues to share a single event flag word
std::atomic<uint32_t>* getEventFlagWord();
bool writeBlocking(const T* data, size_t count, uint32_t readNotification,
uint32_t writeNotification, int64_t timeOutNanos = 0,
android::hardware::EventFlag* evFlag = nullptr); // Blocking write operation for count Ts.
bool readBlocking(T* data, size_t count, uint32_t readNotification,
uint32_t writeNotification, int64_t timeOutNanos = 0,
android::hardware::EventFlag* evFlag = nullptr) // Blocking read operation for count Ts;
//APIs to allow zero copy read/write operations
bool beginWrite(size_t nMessages, MemTransaction* memTx) const;
bool commitWrite(size_t nMessages);
bool beginRead(size_t nMessages, MemTransaction* memTx) const;
bool commitRead(size_t nMessages);
availableToWrite() 和 availableToRead()可以用来确定在单个操作中可以传输多少数据。在一个同步队列:
- availableToWrite() 总是返回队列容量
- 每个读取器都有自己的读取位置和计算 availableToRead()
- 对一个慢的读取器,队列允许溢出,这可能导致 availableToRead() 返回值大于队列容量,在溢出后的第一个读取将失败,并导致该读取器的读取位置与当前写入指针相等,不管是否通过 availableToRead() 报告溢出。
如果所有请求的数据都可以(并且是)转移到/自队列,那么read()和write()方法将返回true。这些方法不会阻塞;它们要么成功(返回true),要么立即返回失败(false)。
readBlocking() 和 writeBlocking() 方法会等待请求的操作完成,或者直到它们超时(一个timeOutNanos值为0,意味着永不超时)。
阻塞操作是使用事件标记位实现的。默认情况下,每个队列创建并使用自己的标记词来支持短式的 readBlocking() 和 writeBlocking()。多个队列共享一个字是可能的,这样一个进程就可以等待写或读到任何队列。可以通过调用getEventFlagWord()来获取队列事件标记词的指针,该指针(或任何指向合适共享内存位置的指针)可以用来创建一个EventFlag对象,以传递给不同队列的长式的 readBlocking() 和 writeBlocking()。readNotification和writeNotification参数告诉我们,事件标志中的哪些位应该用于在该队列上读取和写入信号。readNotification和writeNotification是32位的位掩码。
readblock()在 writeNotification 的位上等待;如果该参数为0,则调用总是失败。如果readNotification值为0,调用将不会失败,但是成功读取将不会设置任何通知位。在同步队列中,这意味着相应的writeblock()调用将永远不会醒来,除非在其他地方设置了bit。在一个非同步的队列中,writeblock()不会等待(它仍然应该被用来设置写通知位),并且它适合于不设置任何通知位。类似地,如果readNotification为0,writeblocking() 将失败,而成功的写入将设置指定的writeNotification位。
要同时等待多个队列,请使用EventFlag对象的wait()方法来等待通知的位掩码。wait()方法返回一个状态字,其中的位导致了唤醒。然后使用该信息来验证对应的队列有足够的空间或数据来满足所需的写/读操作,并执行非阻塞 write()/read()。要获得一个post操作通知,可以使用另一个调用EventFlag的wake()方法。对于EventFlag抽象的定义,请参考system/libfmq/include/fmq/EventFlag.h。
Zero copy operations
read/write/readBlocking/writeBlocking() api将一个指向输入/输出缓冲区的指针作为参数,并使用memcpy()内部调用,以在相同和FMQ环缓冲区之间复制数据。为了提高性能,Android 8.0和更高版本包括一组api,这些api提供直接指针访问环缓冲区,消除了使用memcpy调用的需要。
使用以下公共api为零拷贝FMQ操作:
bool beginWrite(size_t nMessages, MemTransaction* memTx) const;
bool commitWrite(size_t nMessages);
bool beginRead(size_t nMessages, MemTransaction* memTx) const;
bool commitRead(size_t nMessages);
- beginWrite方法为FMQ环缓冲区提供了基本指针。在写入数据之后,使用commitWrite()提交它。beginread /commitRead方法采用同样的方法。
- beginRead/Write方法以输入要读/写的消息的数量作为输入,并返回一个布尔值,指示读/写是否可能。如果读或写是可能的,那么memTx结构就会使用基本指针来填充,该指针可以用于直接指针访问环缓冲区共享内存。
- MemRegion 结构包含了关于内存块的详细信息,包括基本指针(内存块的基本地址)和长度(根据消息队列的hidl定义类型的内存块的长度)。
- MemTransaction 结构包含两个MemRegion结构,第一个和第二个是作为一个读或写入到环形缓冲区的,可能需要一个绕到队列的开头。这意味着需要两个基本指针来读写FMQ环缓冲区中的数据。
从MemRegion结构中获取基本地址和长度:
T* getAddress(); // gets the base address
size_t getLength(); // gets the length of the memory region in terms of T
size_t getLengthInBytes(); // gets the length of the memory region in bytes
在MemTransaction对象中获取对第一个和第二个MemRegions的引用:
const MemRegion& getFirstRegion(); // get a reference to the first MemRegion
const MemRegion& getSecondRegion(); // get a reference to the second MemRegion
以零拷贝api写入FMQ:
MessageQueueSync::MemTransaction tx;
if (mQueue->beginRead(dataLen, &tx)) {
auto first = tx.getFirstRegion();
auto second = tx.getSecondRegion();
foo(first.getAddress(), first.getLength()); // method that performs the data write
foo(second.getAddress(), second.getLength()); // method that performs the data write
if(commitWrite(dataLen) == false) {
// report error
}
} else {
// report error
}
以下辅助方法也是MemTransaction的一部分:
- T* getSlot(size_t idx) 返回在该MemTransaction对象的一部分内的slot idx的指针。如果MemTransaction对象表示内存区域以读取/写入T类型的N项,那么idx的有效范围在0到N-1之间。
- bool copyTo(const T* data, size_t startIdx, size_t nMessages = 1) 从索引startIdx开始,在对象所描述的内存区域中写入nMessages 个T类型项。该方法使用memcpy(),而不是零拷贝操作。如果MemTransaction对象表示内存来读/写T类型的N项,那么idx的有效范围在0到N-1之间。
- bool copyFrom(T* data, size_t startIdx, size_t nMessages = 1) 帮助方法从startIdx开始的对象所描述的内存区域中读取nMessages个T类型项。该方法使用memcpy(),而不是零拷贝操作。
通过HIDL发送队列
在创建部分:
- 如上所述创建消息队列对象。
- 使用isValid()验证对象是否有效。
- 如果您需要通过将EventFlag传递到长式的 readBlocking()/ writeBlocking()来 等待多个队列,您可以从MessageQueue对象中提取事件标志指针(使用getEventFlagWord()),该对象被初始化以创建标志,并使用该标志创建必要的EventFlag对象。
- 使用MessageQueue getDesc()方法获取描述符对象。
- 在.hal文件中,给方法一个fmq_sync或fmq_unsync的参数,其中T是一个合适的hidl定义类型。使用此方法将getDesc()返回的对象发送到接收进程。
接收部分
- 使用描述符对象创建MessageQueue对象。一定要使用相同的队列风格和数据类型,否则模板将无法编译。
- 如果您提取了一个事件标志,则从接收过程中的相应MessageQueue对象中提取标志。
- 使用MessageQueue对象传输数据。
Using Binder IPC
本页面描述了Android O中绑定器驱动程序的更改,提供了使用binder IPC的详细信息,并列出了所需的SELinux策略。
Changes to binder driver
Android O开始,Android框架和HALs现在使用绑定器进行通信。由于这一通信极大地增加了绑定量,Android O包括了几个改进的设计,以保持绑定IPC的快速。SoC供应商和集成了最新版本的驱动程序的OEMs应该查看这些改进的列表,相关的SHAs用于3.18、4.4和4.9内核,并要求用户空间更改。
多个绑定域(上下文)
为了清晰地划分框架(设备独立)和供应商(特定于设备的)代码之间的绑定,Android O引入了绑定上下文的概念。每个绑定上下文都有自己的设备节点和它自己的上下文(服务)管理器。您只能通过它所属的设备节点访问上下文管理器,并且在通过特定上下文传递绑定节点时,它只能通过另一个进程从相同的上下文访问,从而完全隔离各个域。有关使用的详细信息,请参见vndbinder和vndservicemanager。
Scatter-gather
在之前的Android版本中,绑定调用中的每个数据都被复制了三次:
- 一次在调用过程中将其序列化为一个包。
- 一次在内核驱动程序中,将包复制到目标进程。
- 一次在目标进程中不序列化包。
Android O使用scatter-gather优化来减少从3到1的拷贝数。数据不首先在包中序列化数据,而是保留在原来的结构和内存布局中,然后驱动程序立即将其复制到目标进程中。数据在目标过程中,结构和内存布局是相同的,可以读取数据而不需要另一个副本。
Fine-grained locking
在以前的Android版本中,绑定器驱动程序使用全局锁来保护对关键数据结构的并发访问。虽然对锁的争用很少,但主要的问题是,如果一个低优先级的线程获得了锁,然后被抢占,它可能会严重延迟需要获得相同锁的高优先级线程。这导致了jank在这个平台上。
解决此问题的最初尝试包括在持有全局锁时禁用抢占。然而,这比真正的解决方案更糟糕,最终被抛弃。随后的尝试集中于使锁更细粒度,这一版本自2017年1月以来一直在Pixel设备上运行。虽然这些改变大部分都是公开的,但在以后的版本中有了很大的改进。
在确定细粒度锁实现中的小问题之后,我们设计了一个改进的解决方案,使用不同的锁架构,并提交了3.18、4.4和4.9常见分支的更改。我们继续在大量不同的设备上测试这个实现;由于我们不知道有任何未解决的问题,这是Android O设备的推荐实现。
Real-time priority inheritance
绑定器驱动程序始终支持良好的优先级继承。随着Android在实时优先级上运行的进程越来越多,在某些情况下,如果一个实时线程调用了一个绑定调用,那么处理该调用的进程中的线程也会在实时优先级上运行。为了支持这些用例,Android O现在在绑定器驱动程序中实现了实时优先级继承。
除了事务级优先级继承之外,节点优先级继承允许节点(绑定服务对象)指定一个最小优先级,在这个节点上执行调用该节点。以前的Android版本已经支持节点优先级继承,并且具有良好的值,但是Android O增加了对实时调度策略节点继承的支持。
Userspace changes
Android O包含了所有用户空间的更改,这些更改都需要与通用内核中的当前绑定驱动程序一起工作,只有一个例外:初始实现为/dev/binder禁用实时优先级继承使用了ioctl。后续开发将优先级继承的控制权转换为更细粒度的方法,即每个绑定模式(而不是每个上下文)。因此,ioctl不在Android公共分支中,而是提交到我们的普通内核中。
这种更改的效果是,默认情况下,每个节点都禁用实时优先级继承。Android性能团队发现,为hwbinder域中的所有节点启用实时优先级继承是有好处的。为了达到同样的效果,cherry在用户空间中选择这个更改。
Using binder IPC
从历史上看,供应商进程已经使用了binder进程间通信(IPC)进行通信。在Android O中,/dev/binder设备节点成为了框架进程的专有部分,这意味着供应商进程不再能够访问它。供应商进程可以访问/dev/hwbinder,但必须转换它们的AIDL接口以使用HIDL。对于想要在供应商流程之间继续使用AIDL接口的供应商,Android支持binder IPC,如下所述。
vndbinder
Android O支持一个新的绑定域,以供供应商服务使用,使用/dev/vndbinder而不是/dev/ binder.com。加上/dev/vndbinder, Android现在有以下三个IPC域:
IPC 域 | 描述 |
---|---|
/dev/binder | 框架/应用程序与AIDL接口之间的IPC |
/dev/hwbinder | 框架/供应商进程与HIDL接口之间的IPC |
/dev/vndbinder | 供应商进程与AIDL接口之间的IPC |
为了 /dev/vndbinder 出现,确保内核配置项 CONFIG_ANDROID_BINDER_DEVICES 被设置为“binder,hwbinder,vndbinder”(这是Android常见内核树的默认设置)。
通常,供应商进程不直接打开绑定器驱动程序,而是链接到libbinder用户空间库,该库打开绑定器驱动程序。为::ProcessState()选择了libbinder的绑定驱动程序。供应商进程在调用ProcessState、IPCThreadState或在进行任何绑定调用之前应该调用此方法。若要使用,请在供应商流程(客户端和服务器)的main()之后进行以下调用:
ProcessState::initWithDriver("/dev/vndbinder");
vndservicemanager
以前,绑定服务是通过servicemanager注册的,在那里可以通过其他进程检索它们。在Android O中,servicemanager现在只被框架和应用程序所使用,而供应商进程已经无法访问它。
但是,供应商服务现在可以使用 vndservicemanager,这是一个新的servicemanager实例,它使用/dev/vndbinder而不是/dev/binder,它与framework servicemanager相同的源构建。供应商进程不需要对vndservicemanager进行更改;当供应商进程打开/dev/vndbinder时,服务查找将自动转到vndservicemanager。
vndservicemanager二进制文件包含在Android的默认设备makefile中。
SELinux policy
想要使用绑定功能进行通信的供应商进程需要以下内容:
- 访问/dev/vndbinder
- 绑定器{传输,调用}挂钩到vndservicemanager
- binder_call(A, B)对于任何供应商域A想要通过供应商绑定接口调用供应商域B
- 在vndservicemanager中对{add, find}服务的允许。
为了满足需求1和2,使用vndbinder_use()宏:
vndbinder_use(some_vendor_process_domain);
为了满足需求3,对于供应商流程A和B的binder_call(A, B)需要讨论绑定器可以保持在适当的位置,并且不需要重新命名。
为了满足需求4,您必须更改服务名称、服务标签和规则的处理方式。
Service names
以前,供应商在service_context文件中处理注册的服务名称,并为访问该文件添加相应的规则。示例 device/google/marlin/sepolicy 中 service_context 文件:
> AtCmdFwd u:object_r:atfwd_service:s0
> cneservice u:object_r:cne_service:s0
>qti.ims.connectionmanagerservice u:object_r:imscm_service:s0
>rcs u:object_r:radio_service:s0
>uce u:object_r:uce_service:s0
>vendor.qcom.PeripheralManager u:object_r:per_mgr_service:s0
在Android O中,vndservicemanager会加载vndservice_context文件。向vndservicemanager(已经在旧service_context文件中)迁移的供应商服务应该添加到新的vndservice_context文件中。
Service labels
以前,服务标签如 u:object_r:atfwd_service:s0 是定义在 service.te 文件。例子:
type atfwd_service, service_manager_type;
在Android O中,必须将类型更改为vndservice_manager_type并将规则移动到vndservice.te 文件。例子:
type atfwd_service, vndservice_manager_type;
Servicemanager rules
以前,规则允许域访问从servicemanager添加或查找服务。例子:
allow atfwd atfwd_service:service_manager find;
allow some_vendor_app atfwd_service:service_manager add;
在Android O中,这样的规则可以保留并使用相同的类。例子:
allow atfwd atfwd_service:service_manager find;
allow some_vendor_app atfwd_service:service_manager add;