原文见 https://source.android.com/devices/architecture/hidl/
HAL接口定义语言(HIDL)是一种接口描述语言,指定接口和他的使用者,它定义了类型和方法的调用。更广泛的说,HIDL是一个用于在可独立编译的代码库之间进行通信的系统。
HIDL旨在用于进程间通信(IPC)。进程之间的通信被称为绑定。对于必须链接到某个进程的库,也可以使用passthough模式(Java中不支持)。
HIDL指定数据结构和方法,组织在接口(类似于一个类)中,这些接口被收集到包中。对于c++和Java程序员来说,HIDL的语法看起来很熟悉,不过有不同的关键字。HIDL还使用了java风格的注解。
HIDL设计
HIDL的目标是可以替换框架,而不必重新构建HALs。HALs将由供应商或SOC制造商构建,并放置在设备上 /vendor 分区,能够被一个OTA替换,而无需重新编译HALs。
HIDL平衡了下列考虑:
HIDL语法
HIDL语言类似于C(但不使用C预处理器)。
例子
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 = | 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
| bitfield // TYPE is user-defined enum
| fmq_sync
| fmq_unsync
| 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)。包名在根目录下形成一个或多个子目录;定义包的所有文件都在同一目录中。例如,包 [email protected] 可以在 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 声明的接口,隐式扩展自 [email protected]::IBase 。IBase接口,隐式导入,声明了一些不应该且不能在用户定义的接口中重新声明的保留方法。这些方法包括:
导入
import语句是HIDL机制,用于访问包接口和另一个包中的类型。import 涉及两个实体:
importing 实体由 import 语句的位置决定,当语句在包的 types.hal 内,被导入的东西可以被整个包看到;这是一个包级导入。当语句位于接口文件中时,importing 实体就是接口本身;这是一个接口级的导入。
imported 实体由 import 关键字后的值决定。该值不需要是完全限定的名称;如果省略了一个组件,它会自动填充当前包中的信息。对于完全限定值,支持以下导入案例:
importing 实体可以访问以下组件:
import语句使用完全限定类型名称语法提供包或接口的名称和版本导入:
import [email protected]; // import a whole package
import [email protected]::IQuux; // import an interface and types.hal
import [email protected]::types; // import just types.hal
接口继承
一个接口可以使过去定义接口的扩展,扩展可以试下面三种类型:
一个接口只能扩展一个其他的接口,在包中的每个接口都有一个非零的小版本号,必须在包的前一个版本中扩展一个接口。例如,如果包 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名称和版本下。
版本
包是有版本的,接口与其包的版本相同。版本是用两个整数表示的。
对于与框架的更广泛的兼容性,HAL的多个主要版本可以同时出现在设备上。虽然多个小版本也可以出现在一个设备上,因为小版本是向后兼容的,所以没有理由支持每个主要版本的最新版本。
接口布局总结
这部分总结如何管理一个HIDL接口包和整合HIDL部分的信息
术语 | 定义 |
---|---|
Application Binary Interface (ABI) | 应用程序编程接口+任何需要的二进制连接 |
Fully-qualified name (fqName) | 名称以区别hidl类型。例如:[email protected]::IFoo |
Package | 包包含HIDL接口和类型。例子:[email protected] |
Package root | 包含HIDL接口的根包。示例:[email protected] |
Package root path | 根包在Android源代码树中的位置 |
每个文件都可以从包的根映射和完全限定的名称中找到。
根包被指定成 hidl-gen 作为参数 -r android.hardware:hardware/interfaces。例如,如果包是 [email protected]::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选项的接口的散列。
接口位于设备独立的位置
实际上,建议在分支之间共享接口。这允许在不同的设备和用例中最大限度地重用代码并最大限度地测试代码。
该文档描述了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是否已更改:
ABI stability
应用程序二进制接口(ABI)包括二进制链接/调用约定/等。如果ABI/API改变,接口就不再与用官方接口编译的通用 system.img 一起工作。
确保接口版本化和ABI稳定是有几个原因的关键:
当为已经在 current.txt 中有条目的接口添加新的哈希时,请确保只添加表示保持ABI稳定性的接口的哈希。回顾以下类型的变化:
Changes allowed | Changes not allowed |
---|---|
改变注释 | 重新排序参数、方法等 |
改变参数名 | 重命名接口或将其移动到新的包。 |
改变返回值名 | 重命名包 |
改变注解 | 在接口中的任何地方添加方法/结构字段等 |
会打破一个C++虚函数表任何事 | |
… |
本节介绍如何通过调用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”) 在任一接口上获得该接口的注册服务。这就是为什么在大多数情况下,不需要为注册或发现提供名称参数(意思是“默认”)。
供应商接口对象也在返回接口的传输方法中扮演一个角色。对于[email protected] 中的接口 IFoo,IFoo::getService 返回的接口总是使用在条目存在的设备清单中声明的 android.hardware.foo 的传输方法;如果传输方法不可用,则返回 nullptr。
在某些情况下,可能需要立即继续,即使没有得到服务。当客户端想要管理服务通知本身时,或者在诊断程序需要获取所有 hwservices 并检索它们(如ATRACH)时,这可能发生。在这种情况下,提供额外的 API tryGetService在C++中或 getService 在java中。java 中 legacy API getService 必须使用服务通知。使用此API并不能避免服务器在客户机请求它使用这些无重试API时注册自己的竞态条件。
Service death notifications
当服务死亡时,希望得到通知的客户可以接收由框架交付的死亡通知。要接收通知,客户必须:
伪代码示例(c++和Java类似):
class IMyDeathReceiver : hidl_death_recipient {
virtual void serviceDied(uint64_t cookie,
wp& service) override {
log("RIP service %d!", cookie); // Cookie should be 42
}
};
....
IMyDeathReceiver deathReceiver = new IMyDeathReceiver();
m_importantService->linkToDeath(deathReceiver, 42);
同一死亡接收人可以在多个不同的服务上注册。
Data transfer
数据可以通过调用接口中.hal文件定义的方法发送到服务。有两种方法:
不返回值但未声明为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不需要释放存储。
Non-RPC data transfer
HIDL有两种方法可以在不使用RPC调用的情况下传输数据:共享内存和快速消息队列(FMQ),它们都只在c++中支持。
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 */);
Creating the second MessageQueue object
消息队列的第二部分是使用从第一步创建获得的MQDescriptor对象。MQDescriptor对象通过调用HIDL RPC发送到将保存消息队列的第二端的进程。MQDescriptor包含关于队列的信息,包括:
MQDescriptor对象可以被用于构建一个 MessageQueue 对象。
MessageQueue::MessageQueue(const MQDescriptor& 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
(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* 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* 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()可以用来确定在单个操作中可以传输多少数据。在一个同步队列:
如果所有请求的数据都可以(并且是)转移到/自队列,那么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);
从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的一部分:
通过HIDL发送队列
在创建部分:
接收部分
本页面描述了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
想要使用绑定功能进行通信的供应商进程需要以下内容:
为了满足需求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;