Air 系列应用交流/轻虚拟机|AirMV · 鸿蒙 PC 适配全记录

轻虚拟机|AirMV · 鸿蒙 PC 适配全记录

【Air 系列 · 鸿蒙 PC 适配全记录】20 / 56

■ 应用:轻虚拟机(AirMV)
■ 定位:QEMU 可视化虚拟机,运行完整操作系统
■ 上游:QEMU(qemu-vis) | 路线:引擎直连 TEXTURE | 版本:1.3.0
■ 开源: https://gitcode.com/Andy88/Harmony-QEMU
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917609736876046302
■ 官网(免费下载): https://www.shunidc.cn/

————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/

PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper

适配开源地址:https://gitcode.com/Andy88/Harmony-QEMU

环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743

▍一、为什么要适配 AirMV

在桌面 PC 上运行虚拟机是刚需场景:跑一套轻量 Linux 做验证、隔离运行来源不明的软件、临时试用一个发行版,都不必动真机环境。鸿蒙 PC 生态此前没有可用的本地虚拟机方案,AirMV(应用名"轻虚拟机",工程代号 qemu-vis)要补的就是这块空白——把 QEMU 9.x stable 的 x86_64 全系统虚拟机以原生方式嵌入 HarmonyOS 应用,guest 内跑 Alpine、Debian 级别的发行版,显示、输入、音频、快照、网络、共享目录全部落进单机沙箱。

这个工程有一段必须如实记录的前史:它的前身是 VirtualBox 7.2 Manager 的移植,走 Qt6 QPA 路线,最终终结——VirtualBox 的 VMM 依赖 SUPdrv 内核模块,在鸿蒙三方沙箱不可行;退而求其次的 IEM 用户态执行路线需要另行巨量移植。QEMU TCG 是成熟的纯用户态 x86 二进制翻译路线,因此接棒重启,VirtualBox 遗产整体物理归档。替换后的技术底座反而是更彻底的"原生嵌入":QEMU 编成单体 .so,不编可执行文件,显示不走 Qt 栈,由自研 OHOS 显示后端直连 XComponent。

当前工程包名 com.airmv.app,版本 1.1.0,deviceTypes 声明 tablet 与 2in1。权限收敛为两项:ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY(TCG 生成代码需要可写代码内存,属 AGC 特批敏感权限)与 ohos.permission.INTERNET(端口映射需要宿主侧绑 socket)。随包 native 库共 9 个:libqemu_core.so(JIT)与 libqemu_core_tci.so(降级)双库加 glib、pixman、zlib、libslirp 依赖;种子镜像 alpine-desktop.qcow2 随 rawfile 首启部署。真机验证以 MatePad 11.5 为主力,另在 MatePad Mini 悦读版完成过部署校准。

▍二、先划清边界:不把编译通过当成适配完成

虚拟机的边界问题是双层的:既要说清 VirtualBox 与 QEMU 两条路线的差异,也要说清 QEMU 内部 JIT 与 TCI 两种执行引擎的取舍。工程 PORTING.md 的"非目标"一节把口径立得很直:跨架构 guest 只承诺命令行 Linux 可用,轻桌面尽力而为,Windows 一律不承诺;不做 guest 与宿主的剪贴板共享;暂不加 i386 与 arm64 guest。

层次 传统桌面方案(VirtualBox 为参照) 鸿蒙侧当前方案
虚拟化引擎 VMM + SUPdrv 内核模块 QEMU 9.x 全用户态,编为 libqemu_core.so 嵌入
代码执行 VT-x 硬件虚拟化 TCG 翻译:JIT 库优先,dlopen 失败降级 TCI 解释
显示输出 VirtualBox Qt GUI 窗口 自研 OHOS DCL 后端,脏矩形直投 XComponent TEXTURE
网络栈 内核辅助 NAT SLIRP 纯用户态 + hostfwd 端口映射(特权端口自动升位)
磁盘与快照 VDI + VBoxManage snapshot qcow2 + QMP/HMP savevm、loadvm,拓扑指纹校验
宿主文件交换 内核共享文件夹驱动 vvfat FAT 桥:Download 目录与 guest /mnt/share 双向
进程模型 独立 VM 进程 VmAbility 独立 :vm 进程,退出即销毁保证全局态干净

这张表的右列没有一行是"原样搬运":每一层都是按鸿蒙沙箱的约束重新选型后的产物。工程同时把"能做什么"量化——JIT 生效时 guest 内 dd 实测 60.3 MB/s,冷启动约 100 秒,恢复会话秒级;TCI 时代则是 4 MB/s 量级与三分钟级冷启动。性能口径与功能口径一样,都写进了文档而不是宣传语。

▍三、鸿蒙版的整体架构

整体沿用 Air 系列双层架构:ArkTS 壳承担项目启动器、VM 运行页、手势映射与系统 Picker;NAPI 薄桥只链系统库;QEMU 平台层独立维护,经拓扑序 dlopen 接入。

text
ArkTS 壳(entry/src/main/ets)
├── Index.ets:项目启动器(列表/新建/导入/设置面板)
└── VmPage.ets:XComponent TEXTURE + 工具条 + 手势 + 性能遥测
VmAbility(process ":vm" 独立进程,singleton)
│ NAPI:libqemu_bridge(napi_init.cpp + qemu_core.cpp 薄壳)
▼ OnSurfaceCreated → 等镜像部署 → 拓扑序 dlopen(RTLD_GLOBAL)→ dlsym
native 侧(qemu-ohos 平台层)
├── libqemu_core.so QEMU 9.2.4 x86_64-softmmu 裁剪单体(嵌入 API,JIT)
├── libqemu_core_tci.so TCI 解释执行降级库
├── 依赖全家桶 glib / pixman / zlib / libslirp
├── 显示:OHOS DCL 后端 → 脏矩形 → EGL 纹理 → XComponent
├── 输入:事件队列 → BQL 排水定时器 → qemu_input 注入(virtio-tablet 绝对指针)
└── 音频:QEMU AudioOps → OHAudio


仓库主要目录如下:

text
AirMV/
├── AppScope/ # 应用级配置(com.airmv.app / 1.1.0,应用名「轻虚拟机」)
├── PORTING.md # 移植档案:里程碑、风险登记册、P 系列踩坑
├── README.md # 上游 QEMU 与仓库说明
├── ai_log/ # M0 到 M6 各阶段真机验收总账
├── signature/ # 签名材料(本地维护,不入库)
├── entry/
│ ├── libs/arm64-v8a/ # 9 个 .so(JIT/TCI 双库 + glib/pixman/zlib/libslirp)
│ └── src/main/
│ ├── module.json5 # 双权限声明;VmAbility 独立 :vm 进程
│ ├── ets/pages/Index.ets # 项目启动器
│ ├── ets/pages/VmPage.ets # VM 运行页与工具条
│ ├── ets/vmability/VmAbility.ets # VM 专用 Ability(singleton)
│ ├── cpp/napi_init.cpp # 薄桥 + 原生手势层 + UI 热区
│ ├── cpp/qemu_core.cpp # 会话生命周期/HMP/注入封装
│ ├── cpp/permission_qemuvis.json # .permission 段种子
│ └── resources/rawfile/vm/ # alpine-desktop.qcow2 种子 + pc-bios + 版本戳
└── build-profile.json5 # nativeLib strip:false 等打包纪律


▍四、核心适配过程

### 1. QEMU 裁剪与嵌入形态

QEMU 以 9.x stable 锁 commit 交叉编译,configure 基线为 --target-list=x86_64-softmmu 加一组裁剪开关:--disable-modules 把设备全编进单体(QEMU 的模块机制走固定路径 dlopen,沙箱不可控),禁掉 VNC、SDL、GTK、OpenGL 等宿主前端,启用 slirp 与自研 ohos 音频驱动。产物 qemu-system-x86_64 改链为 libqemu_core.so,导出 qemu_init、qemu_main_loop、qemu_cleanup 嵌入 API 及输入、QMP 符号。构建管线处理了三类交付问题:libqemu_core.so 附 .permission ELF 段满足可写代码内存授权;全部 NEEDED 去版本号,用仓库自带 elf_edit.py 原位编辑而非 patchelf;nativeLib.strip:false 与 compressNativeLibs:false 保持 ELF 布局稳定。

### 2. JIT、TCI 与一次著名的翻案

执行引擎的定案过程值得完整记录。M0 阶段的 JIT 探针四模式全部失败(执行被静默替换为固定 stub),工程据此转出 TCI 解释器路线;此后权限链逐步补齐——AGC 特批 ALLOW_WRITABLE_CODE_MEMORY、.permission 段、调试证书真签、module.json5 声明收敛——探针依然恒定失败。2026 年 9 月 16 日的 P29 完成翻案:以单变量实验绕过探针强制加载 JIT 库,TCG 原生后端在真机直接跑通,dd 基准 60.3 MB/s 是 TCI 的 15 倍,冷启动从 314 秒降到约 100 秒,全链功能正常。结论固化为方法论:探针测的是探针自身的调用上下文,不等于真实引擎上下文,"运行时可执行"类判定必须以真引擎直跑为准。当前管线恒定 JIT 优先,dlopen 硬失败才落 TCI 应急,双库随包。

### 3. 显示、输入与音频三件套

显示侧自研 OHOS DCL(DisplayChangeListener)后端:QEMU 的脏矩形回调经共享帧缓冲进入渲染线程,按脏带合并后 glTexSubImage2D 逐带上传,避免全幅拷贝;纯 resize 不销毁 EGL window surface,消除反复重建的黑帧。输入侧踩过 BQL 大锁的坑——从 UI 线程跨线程直调 qemu_input_* 触发 bql_locked() 断言杀死 VM 线程,改为事件队列加独立 10ms 排水定时器,与显示刷新解耦;指针走 virtio-tablet 绝对坐标,位置与按键同一次事件原子下发。触摸手势在 ArkTS 层合成:单指点按为左键,长按 600ms 为右键,双指拖动为滚轮(25px 一格),全部进同一队列排水。音频经 QEMU AudioOps 对接 OHAudioRenderer,guest 内 aplay 出声留有串口与帧计数证据。

### 4. 生命周期、快照与多项目

QEMU 的嵌入入口在进程内不可重入(全局设备拓扑、RCU 线程、atexit 注册),"退出 VM 回启动器再启另一台"在单 Ability 架构下无解。工程解法是给 VmAbility 配 process ":vm" 独立进程:启动器常驻主进程,VM 跑在子进程,退出即销毁,下次启动天然干净。快照链围绕 qcow2 的 savevm/loadvm 建立:拓扑指纹随快照落盘,内存、vCPU、网卡、光驱、共享目录任何变更导致指纹失配时自动回落冷启动,根治改配置后 loadvm 报错的死循环;qcow2 头的快照字段在进程退出收尾才落盘,启动器以 4 秒延迟补扫兜住秒开徽标。前后台语义用 windowStageEvent 的 HIDDEN/SHOWN 双通道驱动自动暂停与续跑,消除了后台 40% CPU 空转。

### 5. 网络、端口映射与共享目录

网络为 SLIRP 用户态实现,guest 内 DHCP 与出网全部可用。端口映射有两条实证结论:非 root 绑不了 1024 以下端口,native 侧自动把宿主端口升位 10000(guest 的 22 映射为宿主 10022);此前怀疑的"回程丢包"经局域网直连验证为 hdc 隧道伪影,ssh -p 10022 root@平板IP 能走完完整 SSH 密钥交换。宿主与 guest 的文件交换用 QEMU vvfat 块层 FAT 桥:宿主 Download/<包名> 目录映射为 guest 的 /dev/vdb1(vvfat 硬盘模式自带 MBR 分区表),guest 侧以 udev 规则监听设备到达事件驱动挂载,宿主侧经 DOWNLOAD picker 静默授权加冷启预热保持持久权限。

▍五、适配中遇到的几个难点

### 难点一:运行时可执行内存的长周期拉锯

从 M0 探针失败到 P29 翻案,JIT 问题横跨了权限申请、ELF 段注入、签名方式、系统策略四条线。工程先后排除了签名变量(去码后探针结果不变)、坚盾守护模式(关闭状态同样失败)等嫌疑,最终靠用户侧的对照实证指路才定位到"探针上下文假阴性"。这段历史的工程价值在于留下了一条可复用的判定纪律:凡涉及系统对运行时行为的拦截类问题,单点探针的阴性结论不能作为选型依据,必须构造真引擎的直接实验。选库逻辑也因此设计为运行时自适应,系统策略未来变化时应用零改动。

### 难点二:嵌入 QEMU 的线程与锁纪律

QEMU 主循环、vCPU、音频、显示 blit 各成一线,ArkTS 主线程还有 6 秒 appfreeze 看门狗盯着。输入注入直接踩中 BQL 断言(tcg_handle_interrupt: assertion failed),VM 线程当场死亡。解法是把所有注入收敛为事件队列:UI 线程只入队,QEMU 主循环侧的定时器在持锁状态下统一排水;排水与显示刷新解耦成独立定时器,避免显示静止时输入无人处理。ArkTS 侧同步立规:事件上抛一律走 napi_threadsafe_function,输入与显示回调热路径零 IO、只碰原子计数器。这套线程分工让双看门狗在整个验证周期内零触发。

### 难点三:快照恢复后的输入路由错位

恢复会话后指针定位正常但按键全死,根因在 QEMU 的设备注册序:板载 i8042 的 PS/2 aux 先于命令行的 virtio-tablet 注册,鼠标按键事件全被 PS/2 截走,而 PS/2 输入流在 loadvm 后不再被 guest 接收。修复是给 ps2.c 打本地补丁让 ps2_mouse_realize 默认不注册 aux handler,按键统一路由 virtio-tablet,快照恢复后行为稳定。这个难点与拓扑指纹同属一类教训:快照迁移对设备拓扑是敏感的,任何变更(哪怕只加一个光驱)都让旧快照作废,应用层必须先比对指纹再决定 loadvm 还是冷启动。

### 难点四:触摸、鼠标与热区的三重叠加

VM 页面的指针体系要同时服务真实触摸、外接鼠标与悬浮 UI 三方。踩坑链包括:原生 XComponent 与 ArkTS 兜底通道同时开启时每次点按成双击(兜底必须休眠互斥);鼠标左键最初根本无注入通道(代码只认右键),补通道后又发现 TEXTURE 型 XComponent 的鼠标 press 在坐标变换层被丢弃,必须把 onMouse 挂到页面根 Stack;悬浮工具条的热区一度比真实控件大 2.5 到 3.5 倍,把大片屏幕变成输入死区。终态方案是"UI 热区抑制":顶带与底带的触摸在原生手势层按窗口像素坐标整指抑制,热区宽度按控件实测边界收紧,另加鼠标与触摸的孪生去重防止系统合成双投递。回归实测触摸落点偏差不超过 4 像素。

### 难点五:冷启动体验与白哑屏

冷启动期 guest 画面要经历 syslinux 清屏的白哑屏、X 启动的黑屏、再到桌面,首帧即翻转显示会让用户对着纯白屏幕等待一分钟以上。工程以 trace 里的 loadvm=cold-boot 标记为零竞态判定(该行在 QEMU 主函数前落盘),冷启动期置顶深色启动页显示项目名与进度,XFCE 就绪信号(串口 TTF 标记约 62 秒)到达才放行;恢复路径直通画面不受影响。配套修复了两个 napi 读接口的边界:boot 日志改尾读防止旧会话残留误判,串口未变化时返回缓存以区分"文件为空"。新默认配置 2 vCPU 加 2048MB 内存把冷启动压到此前的一半左右。

▍六、编译、安装与启动

Native 侧的平台层独立维护:QEMU 补丁集与交叉配置在 qemu-ohos/(patches、build/cross-ohos.ini),改 wrapper 源码后需把 ohos-*.c 同步进 QEMU 构建树再执行 build-qemu.sh libqemu_core.so;产物经 sync-libs.sh 灌入 entry/libs/arm64-v8a/(含 strip-debug、.permission 段附加、真签与 SHA-256 清单),libqemu_core.so 重建后必须重跑同步,否则 HAP 里是旧库——这是踩坑登记里以"交接说已编入"为反面教材的实例。

ArkTS 工程的标准构建:

bash
export JAVA_HOME=/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home
export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk

hvigorw --mode module -p module=entry assembleHap --no-daemon


签名注意两点:API26 收紧调试 profile 校验后,自建 CA 材料装机报错,改用官方云签名(devecocli auth login 加 signature generate);ALLOW_WRITABLE_CODE_MEMORY 的授权与包名绑定,module.json5 多声明未获批的权限会直接导致装机失败。产物 entry/build/default/outputs/default/entry-default-signed.hap,安装与启动:

bash
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.airmv.app


▍七、当前功能边界

已覆盖的能力,均有里程碑验收记录:

- x86_64 guest 全系统虚拟机:q35 机型,JIT 优先 TCI 兜底,种子 Alpine XFCE 桌面开箱即用;
- 显示管线:letterbox 双模式精确呈现、方向正确、模式切换链留档、脏带局部上传、vsync 显式开启;
- 完整输入:键盘(含 IME 双键码体系兜底)、触摸、长按右键、双指滚轮、外接鼠标左键直通;
- 音频:OHAudio 后端,guest 内播放有串口与帧计数证据;
- 快照:savevm/loadvm、秒开徽标、拓扑指纹校验、恢复会话完整还原窗口状态;
- 项目管理:多项目启动器、qcow2 创建、镜像导入(格式识别与空间预检)、ISO 光驱运行中热换;
- 网络:SLIRP 出网、hostfwd 端口映射与局域网直连 SSH、特权端口自动升位;
- 共享目录:Download 与 guest /mnt/share 双向,udev 事件驱动挂载,写探针验收;
- 性能专项:JIT 下冷启动约 100 秒、TTF 约 62 秒、静止期零合成帧、后台自动暂停。

尚未覆盖或处于观察项的能力:Windows guest 与图形加速不在承诺范围(无 virtio-gpu 3D,性能口径明示);guest 与宿主剪贴板共享(vdagent 路线)后期评估;i386 与 arm64 guest 按需再加 target;墨 Ink 屏设备亮屏后的触摸交互响应一项验证轮被中止,如实留痕待补;冷启动时长在个别条件下存在波动,已立观测点。共享目录的 vvfat 卷容量约 504MB,且宿主新增文件需 guest 重挂才可见,这些已知限制均记录在踩坑登记中。

因此,当前版本更准确的定位是"轻量 Linux 命令行与轻桌面可用的 x86_64 虚拟机基础版":从镜像管理、启动、交互、快照到网络与文件交换的完整闭环已在真机反复验证,性能与兼容的边界以文档口径对外,不越线承诺。

▍八、总结

AirMV 的适配史本身就是一部选型决策史:VirtualBox 路线因内核模块不可行而终结,QEMU 纯用户态路线接棒;TCI 因探针误判而当选,又因真引擎实证翻案回 JIT。两次转向的依据都不是猜测,而是可复现的实验与归档的证据链。最终产物把全系统虚拟机塞进了应用沙箱:QEMU 是一个 .so,VM 是一个独立进程,guest 画面是一条脏矩形直投管线,全部系统调用都发生在鸿蒙为三方应用划定的框内。

后续工作的重心集中在收尾与观测:S1 到 S14 的逐项验收对照、墨 Ink 屏触摸响应的补验、冷启动波动的根因观测,以及共享目录与快照互斥等已知限制的体验优化。工程档案里 48 项 P 系列踩坑与 10 篇真机验收记录,既是这台虚拟机的制造日志,也是后来者在鸿蒙上做同类系统级移植时最值得先读的材料。
楼主 8 天前
想参与讨论?登录 或 30 秒注册。