尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android P HAL层添加HIDL服务:从接口定义到客户端调用完整指南

Android P HAL层添加HIDL服务:从接口定义到客户端调用完整指南 Android P HAL层添加HIDL实例详细实现步骤点进来的人大概率不是在做STM32——那个HAL库是单片机外设驱动库跟本文Android里的HAL完全两回事。这里的HAL是指Android系统的Hardware Abstraction Layer硬件抽象层标题里的“HIDL”是Android 8.0之后Treble架构引入的HAL接口描述语言。简单说这篇博文解决的是一个很实际的问题在Android P9.0系统里如果我想新增一个自定义硬件模块让上层Java或Native代码能通过标准接口调用它完整的落地流程是什么。这个需求常见于做BSP定制、车载系统、智能硬件、厂商功能扩展的开发者。你以为新增一个HAL服务要动framework、改系统镜像、趟一堆坑其实流程已经很成熟定义HIDL接口、生成代码、实现服务端、配置SELinux和VINTF映射关系、编译验证。只要按步骤来基本不会跑偏。下面我以给一个虚拟硬件模块添加HIDL服务为例把整个链路从零走一遍。这个模块我命名为hello包路径是vendor.example.hello1.0接口里提供一个helloWorld()方法跑完这套流程你就能在shell里用客户端程序调通它。1. HIDL是什么为什么Android P必须用它来加HAL实例1.1 从传统HAL到Treble/HIDL一次架构上的“离婚”在Android 8.0之前HAL模块是.so库C framework进程通过dlopen动态加载运行在同一个进程空间里。这意味着HAL代码崩了framework跟着崩HAL想更新必须连framework一起升级厂商代码和系统代码搅在一起更新系统又怕碰坏驱动。Treble架构把framework和vendor厂商实现分区隔开HIDL就是它们之间的“合同”。从Android 8.0开始所有HAL接口都要逐步迁移到HIDL描述Android P作为9.0版本在这个体系下已经是完全体状态新增HAL服务正规路径就是写HIDL接口、实现服务、用binder IPC把能力暴露给上层。所以我给你的第一个建议是别再手动写老的hw_get_module式HAL模块了P版本上新增功能直接上HIDL后续维护、跨版本升级都省心。1.2 Binderized与Passthrough两种HIDL服务怎么选HIDL服务有两种形态很多教程一上来就直接写代码没讲清楚导致后面踩坑。第一种是Binderized形式。HAL作为一个独立进程跑framework通过binder机制跨进程调用它。好处是进程隔离HAL崩了不会拖垮framework而且客户端可以随时重启这个服务缺点是跨进程有一定性能开销。第二种是Passthrough形式。HAL库被加载到调用方进程里走的是直连内存调用延迟低适用于少数对实时性极其敏感的硬件访问但安全性差、没有进程隔离。Android P时代官方推荐几乎所有HAL都做成Binderized。你在网上搜“Android P HAL层添加HIDL实例”教程里默认教的也是Binderized。除非你要做的硬件接口要求几微秒级别的响应否则就选Binderized后续的SELinux配置和VINTF校验也更标准。1.3 我们到底要添加一个什么“实例”这里先厘清概念。HIDL服务不是说你写一个类就完了而是四个层面的东西要一起落地接口定义.hal文件描述这个HAL模块对外提供哪些方法。接口代码编译生成的C代码由hidl-gen工具从.hal文件自动生成不需要手写。服务实现impl你继承生成的接口Stub写具体业务逻辑。服务进程与注册一个可执行文件启动时把自己的能力注册到hwservicemanager客户端通过名字查找并调用。任何一个环节缺失上层都拿不到这个“实例”。后面我会按这个顺序展开每个环节该怎么做、为什么要这么做都写清楚。2. 动手前准备定义HIDL接口与目录规划2.1 约定包号与目录结构HIDL包名规则是vendor.modulemajor.minor比如vendor.example.hello1.0。主版本号变化表示接口不兼容次版本号变化表示向后兼容扩展。比如我的接口改了函数签名就要升级主版本变成vendor.example.hello2.0如果只是新增一个方法不影响旧调用方可以升次版本到vendor.example.hello1.1。目录结构也要保持和包名一致这是hidl-gen扫描源码时的硬性要求。在AOSP源码树里建如下目录vendor/example/hello/1.0/ ├── Android.bp ├── IHello.hal ├── types.hal └── default/ ├── Android.bp ├── Hello.h ├── Hello.cpp ├── service.cpp └── hello.rc这里的vendor/example/hello对应包名中的vendor.example.hello1.0对应版本号。不要自作聪明地乱放路径否则hidl-gen会用-r参数定位不到你的包到时候报错你会一脸懵。2.2 编写IHello.hal接口在IHello.hal里定义接口package vendor.example.hello1.0; interface IHello { helloWorld() generates (int32_t result); getVersion() generates (uint8_t major, uint8_t minor); };语法很好懂package声明包名interface声明接口名方法名后面用generates指定返回值。int32_t、uint8_t这类都是HIDL内置的基础类型和C/Java的基本类型一一对应。有人问HIDL方法是不是只能同步返回不是。如果你想做异步回调可以在参数里传IHelloCallback这种接口类型但同步返回在绝大多数HAL场景下够用了而且调用逻辑更清晰。我习惯把最核心的调用做成同步返回复杂数据的拉取再考虑回调。2.3 types.halHIDL自定义数据结构的定义方式如果你的接口需要返回结构化数据比如设备名称、版本信息、一条日志记录应该在types.hal中定义package vendor.example.hello1.0; struct HelloInfo { string name; int32_t id; bool isValid; };注意所有用到的类型、结构体、枚举都要在types.hal里声明不能在接口方法里临时定义。HIDL的数据类型体系和C不是一套不能直接塞一个C struct进去。生成代码时会自动把HelloInfo映射成C结构体字段名保持一致所以你在C实现里可以直接访问info.name。2.4 用hidl-gen生成代码骨架写完.hal文件后第一步是用hidl-gen生成接口代码和实现骨架。先在你的AOSP环境里执行source build/envsetup.sh lunch 你的target make hidl-gen -j8再生成接口的Android.bp和C头文件库hidl-gen -Landroidbp \ -rvendor.example.hello:vendor/example/hello \ -randroid.hardware:hardware/interfaces \ -randroid.hidl:system/libhidl/transport \ vendor.example.hello1.0这条命令会在当前路径下生成一个Android.bp把它放到vendor/example/hello/1.0/下。-r参数是把包名映射到源码目录每个固定的HIDL根目录都要有一项否则hidl-gen找不到依赖的接口定义。接着生成C实现骨架hidl-gen -Lc-impl \ -rvendor.example.hello:vendor/example/hello \ -randroid.hardware:hardware/interfaces \ -randroid.hidl:system/libhidl/transport \ -o vendor/example/hello/1.0/default \ vendor.example.hello1.0-Lc-impl会帮你生成一个Hello.h和Hello.cpp模板里面已经写好了类继承关系、函数声明和空实现。这是新手最容易省掉的一步我强烈建议用生成器先生成再改不要手写继承代码。因为HIDL生成的Stub类全名非常长手写容易漏::android::hardware这些命名空间编译报错一头雾水。3. 实现服务端从接口到可注册的服务进程3.1 继承Stub实现接口方法hidl-gen生成的Hello.h模板里类已经继承自IHello接口了你需要做的就是把虚函数实现填上。完成后的Hello.h长这样#pragma once #include vendor/example/hello/1.0/IHello.h #include hidl/MQDescriptor.h #include hidl/Status.h namespace vendor { namespace example { namespace hello { namespace V1_0 { namespace implementation { using ::android::hardware::Return; using ::android::hardware::hidl_string; struct Hello : public IHello { Hello() default; Returnint32_t helloWorld() override; Returnvoid getVersion(uint8_t* major, uint8_t* minor) override; }; } // namespace implementation } // namespace V1_0 } // namespace hello } // namespace example } // namespace vendor对应的Hello.cpp#include Hello.h namespace vendor { namespace example { namespace hello { namespace V1_0 { namespace implementation { Returnint32_t Hello::helloWorld() { // 这里就是你的硬件操作逻辑 // 比如读取底层驱动节点、控制GPIO、访问设备文件等等 ALOGI(helloWorld invoked); return 1; } Returnvoid Hello::getVersion(uint8_t* major, uint8_t* minor) { *major 1; *minor 0; return Returnvoid(); } } // namespace implementation } // namespace V1_0 } // namespace hello } // namespace example } // namespace vendor注意getVersion的输出参数是裸指针这是HIDL代码生成的固定形式不要在里头做复杂的生命周期管理。返回值统一用ReturnT包裹标志着这次调用成功或失败。如果你的实现里出现了某些错误可以使用Returnint32_t()的静态方法比如Returnint32_t::fromStatus(...)来返回错误码而不是抛异常。3.2 service.cpp注册服务与线程模型服务端实现类写完需要一个入口程序把实例注册到hwservicemanager。新建service.cpp#include vendor/example/hello/1.0/IHello.h #include hidl/LegacySupport.h #include log/log.h #include Hello.h using ::android::hardware::configureRpcThreadpool; using ::android::hardware::joinRpcThreadpool; using ::android::sp; using vendor::example::hello::V1_0::IHello; using vendor::example::hello::V1_0::implementation::Hello; int main() { spIHello service new Hello(); android::status_t status service-registerAsService(default); LOG_ALWAYS_FATAL_IF(status ! android::OK, Failed to register hello service: %d, status); configureRpcThreadpool(1, true /* callerWillJoin */); joinRpcThreadpool(); return 0; }这里有个值得展开的点为什么需要configureRpcThreadpool和joinRpcThreadpool。Binderized服务进程本质上就是一个binder serverbinder通信需要一个线程池来响应客户端请求。configureRpcThreadpool(1, true)启动了1个线程joinRpcThreadpool让主线程加入线程池等待这样进程不会退出服务会一直在后台待命。如果你忘了这两行进程启动后注册完服务就直接退出了客户端调用时hwservicemanager发现服务已死返回null问题非常隐蔽。我在初学HIDL时就在这个坑里卡过后来对比系统自带HAL服务的源码才发现漏了线程池启动。3.3 Android.bp编译脚本精讲我们把整个HAL服务拆成两个编译单元一个是实现库一个是可执行服务。先看vendor/example/hello/1.0/Android.bp这是接口库通常由hidl-gen生成但你还是需要理解它的作用package { default_applicable_licenses: [vendor.example.hello_license], } genrule { name: vendor.example.hello1.0_genc, tools: [hidl-gen], cmd: hidl-gen -o $(genDir) -Lc -randroid.hidl:system/libhidl/transport -randroid.hardware:hardware/interfaces -rvendor.example.hello:$(LOCAL_PATH) vendor.example.hello1.0, srcs: [ types.hal, IHello.hal, ], out: [ vendor/example/hello/1.0/types.h, vendor/example/hello/1.0/HelloAll.h, vendor/example/hello/1.0/IHello.h, vendor/example/hello/1.0/IHello.cpp, vendor/example/hello/1.0/types.cpp, ], } cc_library_shared { name: vendor.example.hello1.0, defaults: [hidl-gen-defaults], generated_headers: [vendor.example.hello1.0_genc], generated_sources: [vendor.example.hello1.0_genc], export_generated_headers: [vendor.example.hello1.0_genc], }简单理解就是编译接口库时hidl-gen先把.hal文件翻译成C代码然后编译出一个名为vendor.example.hello1.0.so的共享库。客户端和服务端都链它。然后是default/Android.bpcc_library_shared { name: vendor.example.hello1.0-impl, relative_install_path: hw, proprietary: true, srcs: [ Hello.cpp, ], shared_libs: [ libhidlbase, libhidltransport, liblog, libutils, vendor.example.hello1.0, ], } cc_binary { name: vendor.example.hello1.0-service, relative_install_path: hw, proprietary: true, init_rc: [hello.rc], srcs: [ service.cpp, ], shared_libs: [ libbase, libbinder, libhidlbase, libhidltransport, liblog, libutils, vendor.example.hello1.0, vendor.example.hello1.0-impl, ], }relative_install_path: hw的意思是最终产物会装到/vendor/lib64/hw/vendor.example.hello1.0-impl.so和/vendor/bin/hw/vendor.example.hello1.0-service。这是Android对HAL模块文件的默认放置位置检查文件和排查问题时可以直接去这两个目录看。proprietary: true在Android P里表示这是vendor分区的私有模块链接的库必须也是vendor可用的。如果你不加这个标志构建系统可能会把模块扔到system分区导致运行时报dlopen failed或者找不到库。3.4 添加hello.rc与VINTF映射配置服务要有init启动脚本。在default/hello.rc里写service vendor.hello-1-0 /vendor/bin/hw/vendor.example.hello1.0-service class hal user system group system注意服务名vendor.hello-1-0只是init管理用的名字和HIDL注册名“default”不冲突。class hal表示这个服务归类为halinit在hal class启动阶段会拉起它。如果你希望服务崩溃后自动重启可以加restarton:...策略但一般HAL服务不设置oneshot否则crash后不会自动恢复.接下来是关键一步Android P要求所有HIDL服务都必须在VINTF manifest里声明否则hwservicemanager会拒绝注册。找到你的设备manifest文件通常在device/厂商/板型/manifest.xml或者在vendor分区覆盖为/vendor/etc/vintf/manifest.xml加入manifest version1.0 typedevice hal formathidl namevendor.example.hello/name version1.0/version interface nameIHello/name instancedefault/instance /interface /hal /manifestname对应包名version对应版本interface下的name是接口名instance是实例名。客户端getService时传的实例名必须跟这里一致。Android P上的hwservicemanager在registerAsService时会做VINTF校验manifest里没声明注册直接失败这个问题我后面在踩坑部分还会专门展开。4. SELinux策略与客户端验证让服务能注册、能被调用4.1 SELinux策略文件配置如果整个系统跑在SELinux enforcing模式下Android默认就是你光把服务编译出来还远远不够。不加策略的话init启动服务会被拒hwservicemanager注册会被拒客户端调用也会被拒。我见过太多人卡在这一步而且报错信息不一定直观所以先给结论SELinux策略文件需要四类配置缺一不可。在设备的sepolicy目录新建hello.tetype vendor_hello, domain; type vendor_hello_exec, exec_type, file_type, vendor_file_type; init_daemon_domain(vendor_hello); hal_server_domain(vendor_hello, vendor_hello); binder_call(vendor_hello, servicemanager); add_service(vendor_hello, servicemanager); allow vendor_hello vendor_hello:process { fork }; allow vendor_hello vendor_hello:file { read open execute execute_no_trans };第一行声明了vendor_hello是安全域vendor_hello_exec是它对应的可执行文件类型。init_daemon_domain允许init启动这个域hal_server_domain是给HAL服务域用的宏帮忙连接了hwservicemanager。在hwservice_contexts中新加一行vendor.example.hello::IHello u:object_r:hal_hello_service:s0同时在某个te文件里声明这个新的hwservice类型type hal_hello_service, hwservice_manager_type;如果不加这个hwservicemanager在注册服务时查不到类型would报service not found或者权限问题。最后在file_contexts里给可执行文件打标签/vendor/bin/hw/vendor\.example\.hello1\.0-service u:object_r:vendor_hello_exec:s0改完SELinux策略后要确认BOARD_SEPOLICY_DIRS包含了你放te文件的目录然后重新编译boot image或vendor image。如果你是调试阶段可以把设备先切到permissive模式看看效果但上线前必须把策略收全这是系统安全的基本要求。4.2 编译刷机与lshal验证编译阶段你可以单独编译服务模块mmm vendor/example/hello/1.0/default/或者全量编译make -j8编译完成后把生成的vendor.example.hello1.0-service和vendor.example.hello1.0-impl.so打包到vendor镜像刷机重启。重启后在设备的adb shell里执行adb shell lshal | grep vendor.example.hello正常情况下你会看到类似这样的输出vendor.example.hello1.0::IHello default这表示服务已经注册成功。如果这里没有输出或者显示服务未注册后面写再多客户端代码也白搭。4.3 写一个客户端测试程序服务端通了最后写一个小的客户端测试程序验证整个链路。客户端和HIDL服务之间的调用非常简单因为接口库已经帮你把binder细节都封装好了。新建一个目录vendor/example/hello/client/里面放client.cpp#include stdio.h #include vendor/example/hello/1.0/IHello.h using ::android::sp; using ::android::hardware::Return; using vendor::example::hello::V1_0::IHello; int main() { spIHello svc IHello::getService(default); if (svc nullptr) { fprintf(stderr, Failed to get hello service\n); return 1; } Returnint32_t ret svc-helloWorld(); if (ret.isOk()) { printf(helloWorld result: %d\n, ret); } else { printf(helloWorld failed: %s\n, ret.description().c_str()); } uint8_t major 0; uint8_t minor 0; Returnvoid verRet svc-getVersion(major, minor); if (verRet.isOk()) { printf(version: %u.%u\n, major, minor); } return 0; }对应Android.bpcc_binary { name: hello_client, proprietary: true, srcs: [client.cpp], shared_libs: [ libbase, liblog, vendor.example.hello1.0, ], }编译后将hello_clientpush到设备adb push hello_client /data/local/tmp/ adb shell chmod x /data/local/tmp/hello_client adb shell /data/local/tmp/hello_client如果一切正常输出应该是helloWorld result: 1 version: 1.0到这里一个完整的HIDL服务从定义到调用就彻底打通了。你后续要做的事情就是把helloWorld()里的占位逻辑替换成你的真实硬件访问代码比如读写设备节点、通过ioctl操作驱动、调用内核提供的接口等。5. 踩坑记录P版本HIDL服务添加的常见问题与排查思路HIDL服务开发整体不难但链路上的坑不少。很多报错不是一次性能看懂的我把自己实际调试时踩过的坑和排查思路整理成下面这个速查表按优先级排了序当你遇到“服务死活调不通”时从表头开始逐项检查。现象可能原因排查与解决方法init启动服务失败logcat报avc: deniedSELinux策略缺失或标签打错dmesg | grep avc查看denied上下文对照第4.1节补齐策略registerAsService失败logcat报VINTF相关错误VINTF manifest没有声明服务检查manifest.xml中添加的hal节点确认包名、版本、接口、实例名服务进程启动后立即退出漏了joinRpcThreadpool确认service.cpp有configureRpcThreadpool和joinRpcThreadpool客户端getService返回null服务未起来或实例名不一致用lshal查看服务是否存在检查registerAsService(default)里的名字客户端调用方法超时或crashimpl库链接不完整确认cc_binary里链了vendor.example.hello1.0-impl缺这个会报undefined symbol编译时找不到vendor.example.hello1.0库接口库没编译或未导出头文件先编译vendor/example/hello/1.0目录确认export_generated_headers配置完整刷机后/vendor/bin/hw/下没有service模块未打进vendor分区检查Android.bp是否设置proprietary: true重新全量打包vendor image这里单独说一说最容易迷惑人的VINTF问题。Android P的hwservicemanager在服务注册时会反向校验你的manifest描述和实际注册的接口是否匹配。系统里所有HAL服务的声明汇总在/vendor/etc/vintf/manifest.xml。如果你改了manifest但没重新生成vendor image或者路径不对注册时就会报类似VINTF parse error的错。排查时可以直接在设备上执行adb shell lshal这个命令会列出所有已注册的HIDL服务同时也能看到哪些是“空壳”声明了但没注册。如果列表里你的服务名后面是空说明manifest有了但服务没起来回到init那块排查如果服务名后面有实例名但客户端还是拿不到多半是客户端编译时链接的接口库版本和服务端不一致。还有一个调试小技巧把SELinux临时切到permissive模式快速验证是不是策略问题adb root adb shell setenforce 0但如果切到permissive之后服务正常、切回enforcing又挂那没得说老老实实补te规则。千万别带着permissive上线系统安全框架会被直接击穿。按我自己的习惯一个HIDL服务从新建目录到验证通过整体耗时控制在半天以内是比较正常的。如果你在某个环节反复卡住优先怀疑是不是少了某个配置文件而不是怀疑代码逻辑。毕竟这流程里每一步都有固定的“标准答案”照着补就行。这个内容其实还能继续扩展比如在多实例场景下你可以在manifest里声明多个instance比如default和special然后用registerAsService(special)分别注册客户端按需获取再比如使用HIDL的1.1扩展版本时实现类要同时继承V1_1::IHello和V1_0::IHello这部分涉及到的细节又是另外一篇东西了。先把基础链路跑通后面做复杂扩展就顺了。
返回列表