本文首发地址 https://h89.cn/archives/720.html

座舱域控制器这几年流行拆虚拟机:Android 占一个管娱乐,QNX 或 Safety Linux 占一个管仪表、管安全,而 GPU、视频编解码器、音频 DSP 这些硬件资源,全部归属 Host 侧。

于是问题来了:Android 要渲染导航、要播视频、要放语音,活全在对面那台"机器"上,数据怎么过去?

走网络?内部以太网延迟毫秒级,一帧 1080p 视频几十 MB,带宽再高也是泪。 走 Binder?Binder 是进程间通信,跨不过虚拟机边界。 把 GPU 整个透传给 Android?那 QNX 那边拿什么渲染仪表盘。

高通的解法,是一个叫 HAB(Multimedia Hypervisor Abstraction Layer,多媒体 Hypervisor 抽象层) 的框架。它给跨 VM 通信配了三件套:Socket 式通信、共享内存、虚拟中断。

这套东西里藏着一个挺反直觉的设计,先剧透一句:用户态代码薄到只有两三千行,因为"智能"全在内核。

封面

一、跨 VM 通信的三道坎

为什么不能直接抄现成方案?跨 VM 通信和跨进程、跨机器都不太一样,卡在三件事上:

  1. 通道建立。两个虚拟机要"对上线"。进程间有端口,网络间有 IP,虚拟机之间用什么标识?谁先启动谁后启动?对面没起来怎么办?
  2. 数据搬运。通信对象不是几十字节的命令,是视频帧、渲染结果,动辄几 MB 几十 MB。靠拷贝,每多一次就多一轮内存带宽消耗,跨 VM 还叠一层地址翻译。
  3. 同步通知。数据写完了,怎么告诉对方"你可以读了"?轮询浪费 CPU,睡死过去又怕误事。

HAB 的三件套正好一一对应这三道坎:

难题 为什么难 HAB 的方案
通道建立 没有 VM 级端口概念 Socket 式通信 habmm_socket_*
数据搬运 大数据拷贝开销大 共享内存 habmm_export/import
同步通知 轮询浪费、睡死误事 虚拟中断 + 事件通知

这三项在正文里会反复出现,先记住它们。

二、全景:三层架构,智能全在内核

HAB 的用户态侧是一个叫 libuhab 的库,给应用暴露 habmm_* 这组 API。往下看是三层:

应用
  ↓ habmm_* API
libuhab(用户态库:开设备、缓存 fd、数引用计数)
  ↓ ioctl
msm_hab(内核驱动 /dev/hab:阻塞、超时、对端握手、export_id 分配)
  ↓
Gunyah Doorbell(gh_dbl 内核模块:虚拟中断)

第一眼看到这个分层我有点意外:一个"跨虚拟机通信框架",用户态库不搞自己的协议栈、不维护连接状态、不起线程、连把锁都只用来保护 fd 簿记,剩下的活全是 ioctl 透传给内核驱动。

阻塞?内核。超时?内核。对端 FE/BE 的握手等待?内核。export_id 分配?还是内核。

为什么这么设计?回头想想就通了:跨 VM 通信的"对面"不是一个进程、一台机器,是一个只能通过 hypervisor 交互的世界。用户态要是自己实现握手,得先定义一套协议、再调 hypervisor 的门铃。那不如直接把活扔给驱动,驱动离 hypervisor 最近。

而且这套代码是跨平台的:Linux 用 ioctl、QNX 用消息传递、INTEGRITY 走 pmem、Windows 还有分支。一份 API,四套底层。用户态薄,恰恰是跨平台最省事的选择:平台差异全部收敛到内核以下。

三、第一件套:Socket 式通信

命令和状态怎么传?HAB 的思路是照抄 socket 语义,只换一层"地址"。

habmm_socket_open 建通道,参数里有个 mm_ip_id,可以理解成跨 VM 的"端口号"。601 是 MM_MISC(杂项),602 是 hab_aios_tx(某条通信通道),每个编号对应一条预定义好的物理通道。两端用同一个编号 open,就能对上线。

open 是阻塞的:对面没起来,你就等着,超时兜底。打开成功返回一个 vcid(虚拟通道 ID),比如 0x259FF001,高 12 位还编码了 mmid,方便库层定位用哪个设备节点。

实际用起来是这个味道:

虚拟机 A:
open 601 1000 0
> for mmid 601, new vcid created 0x259FF001
send 0x259FF001 4096 0x1234

虚拟机 B:
open 601 1000 0
> for mmid 601, new vcid created 0x259FF002
recv 0x259FF002 4096

和 POSIX socket 的差异,列个表:

维度 POSIX socket HAB socket
通信域 进程间 / 网络间 虚拟机间
通道标识 IP:端口 mm_ip_id(预定义编号)
阻塞语义 connect/send 用户态配置 open 阻塞等对端,内核处理
超时 SO_RCVTIMEO recv 的 timeout:0=立即、-1=永久、其他=毫秒
大数据 send/recv 拷贝 走共享内存通道(见下节)

拓扑上也给了三种:一个后端对一个前端(默认)、一个后端对一个 Guest、一个后端对多个 Guest。简单说就是"谁都能开口子"和"后端统一接客"两种部署模式都支持。

不过这里有个注脚:多 FE 定向收发的 sendto/recvfrom 在用户态库是空壳,头文件里语义定义得明明白白,实现只 return 0。接口先行、实现欠账,这个 pattern 我在高通代码里见过好几次:先定契约,用不用得上后面再说。

四、第二件套:共享内存

命令走 socket,数据走内存。视频帧这种级别的数据,靠 send/recv 拷贝是扛不住的。

流程是这样:

  1. 导出方 habmm_export 把一块内存"挂"到通道上,拿到 export_id(由内核分配)
  2. export_id 通过 socket send 发给对面
  3. 导入方 habmm_import 拿着 export_id 换回一块映射好的内存
  4. 用完,unimport / unexport 各自释放

单次导出上限 128MB,对视频帧绰绰有余。导入支持两种形态:拿 fd(走 dma-buf 框架,可以直接喂给驱动)或直接 mmap 得到虚拟地址。

内存来源也有讲究:Linux 5.15+ 走 dma-heap,老内核走 ION。测试代码开的是 /dev/dma_heap/qcom,system(cached)和 qcom,system-uncached(uncached)。缓存策略对跨 VM 共享是致命的:两边缓存不同步,你写了我看不见。这也是测试要做 cached/uncached 两套用例的原因。

怎么验证共享内存真通了?高通的验证方式很质朴:导出方在内存头部写 0xBEEF、尾部写 0xDEAD,导入方回读比对。还有更狠的:故意用非对齐指针去写,验证"对端改的字节我真能看到"。这些测试背后是血泪,跨 VM 共享内存最容易踩的坑就是 cache 一致性和非对齐访问。

2025 年还加了一种 LOOPBACK 内存类型:把"对方导给我的内存"原样再导回去,用于回环测试和特殊场景。注意,这次改动只动了头文件、加了个 flag,用户态逻辑一行没改,又一个"接口先行"。

五、第三件套:虚拟中断与事件通知

数据在共享内存里写好了,怎么通知对方?

轮询?隔着 hypervisor 高频访问共享内存成本不低,要么忙等浪费 CPU,要么延迟感人。

HAB 提供虚拟中断:habmm_virq_register 注册(分 TX/RX 两个方向)、habmm_send_virq 触发、habmm_virq_unregister 注销。触发方写数据 → 发 VIRQ;接收方在回调里被唤醒,去读共享内存。

这里藏着 HAB 和 Gunyah 的衔接点:VIRQ 底层不是自研中断机制,直接对接 Gunyah Doorbell(内核模块 gh_dbl)。门铃一响,hypervisor 把虚拟中断注入对方 VM。所以 HAB 不是和 Gunyah 对着干的另一套方案,而是建在 Gunyah Doorbell 之上的一层,这正好解释了为什么 HAB 在 Gunyah 方案里照样能跑。

除了点对点 VIRQ,QNX/INTEGRITY 侧还有一套事件通知:Host 可以注册监控 Guest 的健康状态,看门狗超时(WDOG_BITE)、容器崩溃(CONTAINER_CRASH)、虚拟机重启(RESTARTING)、虚拟机停止(STOPPED)都能收到回调。对座舱这种"Android 崩了仪表不能崩"的场景,Host 第一时间感知 Guest 挂掉、接管善后,这个能力很实在。

三件套的配合长这样:共享内存搬数据(视频帧),VIRQ 负责"写好了,来读"(一帧完成的通知),socket 传小控制命令(起停、参数)。各干各的,互不干扰。

六、落地实证:谁在用 HAB

说了这么多,这套东西在真实系统里到底谁在用?

测试平台的 Android 侧 lsmod 打印能说明一切:

msm_hab  299008  12  msm_drm, msm_hw_fence, qcom_hgsl, msm_virtio_video,
                       qcedev_fe_dlkm, msm_virtio_snd, qcom_scm, crypto_qti_virt

msm_drm(显示)、msm_virtio_video(视频编解码)、msm_virtio_snd(音频)、qcom_hgsl(GPU)、qcedev_fe_dlkm(安全加解密)——GPU、显示、视频、音频、加密,这些跨 VM 通道全挂在 HAB 上。这就是名字里 "Multimedia" 前缀的来历:它不是通用 RPC 框架,是为多媒体这种高带宽、低延迟需求量身做的。

配套测试工具也相当认真:uhabtest 交互程序,echo/ping 双端对跑。有意思的是它内部不是简单的"收到就回",而是一个带命令字的状态机——SEND(回显)、IMP(导入共享内存并逐字节 +1 验证)、PROF(跨 VM 时间戳)、TPUT(吞吐)、SCHE(调度延迟,用 32768Hz 的硬件定时器计时)。同一个通道上能测出环回延迟、内存导入耗时、吞吐、跨 VM 调度延迟四种量纲。另有一套 gtest,但它是集成测试不是单测——import 之前要先 recv 到对端的 export_id,必须两端对跑,单机跑不起来。

七、边界、坑与选型

三件套讲完了,说说边界和踩过的坑。

一个已修复的坑。 早期在 Gunyah 下,Linux→Android 方向的内存导出/导入会失败(返回 -EINVAL)。根因是各 VM 有独立的物理地址空间:Linux 侧导出的页帧号(PFN),拿到 Android VM 里做 pfn_valid() 校验直接不通过——"你的物理地址在我的世界里不存在"。所以当时约定只让 Android 往外导(Linux 侧清楚 Android 的内存布局,反向就抓瞎)。这个问题后来高通修复了,具体机制我没拿到细节,但"没有统一物理地址,跨 VM 共享就绕不开地址翻译"这个前提,修多少版本都在。而这正是 Gunyah Memory Extent 的用武之地:由 hypervisor 托管物理内存、做映射翻译。

边界。 HAB 只覆盖 Socket 通信、内存共享、虚拟中断三类。高通跨 VM 通信还有一种常被提到的分类——Passthrough(外设透传)和 Vdev(敏感地址陷入模拟),那套不在本次拆解的代码里,别把 HAB 和它们混为一谈。

选型。 跨 VM 方案怎么选?HAB 适合"多媒体数据流 + 命令/通知分离"的座舱场景;纯控制面、低频小数据,Message Queue 更轻;要跨 VM 动态搬大块物理内存,Gunyah Memory Extent 是更底层的答案。HAB 不是银弹,但它把"通道、数据、通知"拆得够清楚,这套拆法本身才是真正值得搬走的——不管你是不是做高通车载。


参考资料:


本文链接:高通 HAB 跨 VM 通信全解:socket、共享内存、虚拟中断三件套 - https://h89.cn/archives/720.html

版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。

标签: android, Hypervisor, Gunyah, HAB, 座舱, QNX, 共享内存, 虚拟中断, socket, libuhab, VIRQ, 跨VM通信

欸谨特公众号
微信扫码关注:欸谨特
Agent · 效率工具 · 实战笔记

添加新评论