Skip to content

Android Emulator配置链

追踪 Goldfish/Ranchu Emulator 的产品选择、镜像构建、启动属性、qemu-props 桥接以及图形和网络服务的生效链。

基于android-17.0.0_r1
AndroidEmulatorGoldfishRanchuinit

Android Emulator配置链 ​

启动一个 Android Emulator 时,-gpu、设备类型和虚拟硬件参数并不会直接“改掉 Android”。宿主机 emulator 把一部分输入放进启动参数或 qemud 服务,guest 镜像则由产品文件、BoardConfig、init.ranchu.rc 和 vendor 服务决定如何读取、转换并消费这些值。真正适合源码阅读的问题是:一个 TARGET_PRODUCT 选择,怎样变成 guest 中的 ro.hardware.*、debug.* 属性,以及图形、网络和虚拟串口服务?

本文面向已经读过 lunch目标与编译变体、系统镜像结构 和 Perfetto追踪 的读者。需要知道 Make 产品继承、动态分区、Android init 触发器和 vendor service;不展开 SDK emulator 可执行文件、QEMU/KVM 内部、快照格式或宿主机 GPU 驱动。那些对象不在本文章固定的 AOSP project tag 证据范围内。读完后,读者应能从 sdk_phone64_x86_64 追到图形 allocator 的服务选择,也能判断一个属性没有生效时该查构建输入、qemud 桥接还是 init 消费者。

1. 边界对象 ​

Goldfish 是 AOSP 中的虚拟设备实现,Ranchu 是 Android guest 使用的硬件/服务命名。它们不是同一层的对象:产品文件选择“要打包什么”,BoardConfig 选择“怎样生成镜像”,init.ranchu.rc 在运行时启动服务,服务进程才消费属性并提供 AIDL/HAL 接口。

对象owner生效阶段典型消费者
TARGET_PRODUCTMake 产品图编译时产品继承、包和属性列表
TARGET_DEVICE=emu64xbuild/make/core/board_config.mk 与 Goldfish BoardConfig编译时ABI、kernel、分区和 fstab
ro.boot.*bootloader/emulator 输入early bootinit.ranchu.rc
vendor.qemu.*qemu-props/virtio 端口解析属性服务可用后init 触发器、HAL、虚拟设备
ro.hardware.grallocinit.ranchu.rcallocator 启动前minigbm 或 ranchu allocator

图中的箭头不是“变量自动传递”的意思。Make 变量只在构建阶段存在;运行时能看到的是打进镜像的 property、rc、二进制和文件。ro.boot.* 则来自 emulator/boot 链路,必须经过 init 或 qemu-props 才会变成其他属性。

2. 产品入口 ​

2.1 名称映射 ​

Android 17 的 Goldfish 产品入口在 device/generic/goldfish/AndroidProducts.mk。它列出多个架构、16K 页大小和 minigbm 变体;文件本身不决定最终服务,而是把可被 lunch 选择的产品文件暴露给构建系统。

源码文件:device/generic/goldfish/AndroidProducts.mk

makefile
PRODUCT_MAKEFILES := \
    $(LOCAL_DIR)/64bitonly/product/sdk_phone64_x86_64.mk \
    $(LOCAL_DIR)/64bitonly/product/sdk_phone16k_x86_64.mk \
    $(LOCAL_DIR)/64bitonly/product/sdk_phone64_x86_64_minigbm.mk \
    $(LOCAL_DIR)/64bitonly/product/sdk_phone64_x86_64_riscv64.mk \
    $(LOCAL_DIR)/64bitonly/product/sdk_tablet_arm64.mk \
    $(LOCAL_DIR)/64bitonly/product/sdk_tablet_x86_64.mk \
    $(LOCAL_DIR)/64bitonly/product/sdk_phone64_arm64.mk \
    $(LOCAL_DIR)/64bitonly/product/sdk_phone64_arm64_minigbm.mk \
    $(LOCAL_DIR)/64bitonly/product/sdk_phone16k_arm64.mk \
    $(LOCAL_DIR)/64bitonly/product/sdk_phone64_arm64_riscv64.mk \
    $(LOCAL_DIR)/64bitonly/product/sdk_slim_x86_64.mk \
    $(LOCAL_DIR)/64bitonly/product/sdk_slim_arm64.mk

这里的 owner 是产品图,不是 emulator 二进制。sdk_phone64_x86_64_minigbm.mk 继承普通 x86_64 产品,再复制 advancedFeatures.ini.minigbm 和 config.ini.nexus5,最后改变产品名。这个继承关系使“minigbm 是一个完整独立产品”成为错误的心智模型。

2.2 构建变量 ​

固定产品文件把动态分区和设备身份传给后续层:

源码文件:device/generic/goldfish/64bitonly/product/sdk_phone64_x86_64.mk

makefile
PRODUCT_USE_DYNAMIC_PARTITIONS := true
BOARD_EMULATOR_DYNAMIC_PARTITIONS_SIZE ?= $(shell expr 1800 \* 1048576 )
BOARD_SUPER_PARTITION_SIZE := $(shell expr $(BOARD_EMULATOR_DYNAMIC_PARTITIONS_SIZE) + 8388608 )

$(call inherit-product, $(SRC_TARGET_DIR)/product/core_64_bit_only.mk)
$(call inherit-product, device/generic/goldfish/board/emu64x/details.mk)
$(call inherit-product, device/generic/goldfish/product/phone.mk)

PRODUCT_BRAND := Android
PRODUCT_NAME := sdk_phone64_x86_64
PRODUCT_DEVICE := emu64x
PRODUCT_MODEL := Android SDK built for x86_64

PRODUCT_DEVICE 会被构建系统转换为 TARGET_DEVICE,再由 build/make/core/board_config.mk 按 device/generic/goldfish/board/emu64x/BoardConfig.mk 搜索 BoardConfig。也就是说,产品名和 BoardConfig 不是运行时属性;它们在构建时决定镜像形状。

3. 镜像布局 ​

3.1 Board边界 ​

emu64x/BoardConfig.mk 只声明 x86_64 身份并继承公共板级配置。真正重要的约束位于 BoardConfigCommon.mk:emulator 使用非 A/B 更新模型,默认构建 super.img,并把 system、system_dlkm、system_ext、product、vendor 放入动态分区组。

源码文件:device/generic/goldfish/board/BoardConfigCommon.mk

makefile
TARGET_BOOTLOADER_BOARD_NAME := goldfish_$(TARGET_ARCH)
NUM_FRAMEBUFFER_SURFACE_BUFFERS := 3
BUILD_EMULATOR_OPENGL := true
BUILD_QEMU_IMAGES := true
USE_OPENGL_RENDERER := true

TARGET_USERIMAGES_SPARSE_EXT_DISABLED := true
TARGET_USERIMAGES_SPARSE_F2FS_DISABLED := true
AB_OTA_UPDATER := none
BOARD_BUILD_SUPER_IMAGE_BY_DEFAULT := true

BOARD_SUPER_PARTITION_GROUPS := emulator_dynamic_partitions
BOARD_EMULATOR_DYNAMIC_PARTITIONS_PARTITION_LIST := \
  system \
  system_dlkm \
  system_ext \
  product \
  vendor
BOARD_EMULATOR_DYNAMIC_PARTITIONS_SIZE ?= 8589934592
EMULATOR_RO_PARTITION_FS ?= ext4
BOARD_SYSTEMIMAGE_FILE_SYSTEM_TYPE := $(EMULATOR_RO_PARTITION_FS)
BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE := $(EMULATOR_RO_PARTITION_FS)

这段代码解释了两个常见误判。第一,-writable-system 不能从这里推导出来,它是宿主机 emulator 对镜像挂载的额外行为;第二,动态分区大小和 userdata 大小不是同一个资源。emu64x/BoardConfig.mk 另设 BOARD_USERDATAIMAGE_PARTITION_SIZE := 576716800,不会覆盖 super 分区组大小。

3.2 文件安装 ​

product/generic.mk 将运行时所需文件安装到 vendor 或 system_ext。下面的片段是启动链的关键部分:

源码文件:device/generic/goldfish/product/generic.mk

makefile
PRODUCT_PACKAGES += \
    vulkan.ranchu \
    libandroidemu \
    qemu-props \
    android.hardware.graphics.composer3-service.ranchu \
    android.hardware.wifi-service \
    libGoldfishProfiler

PRODUCT_PACKAGES += \
    android.hardware.graphics.allocator-service.minigbm \
    mapper.minigbm

ifneq ($(filter %_minigbm, $(TARGET_PRODUCT)),)
PRODUCT_VENDOR_PROPERTIES += ro.hardware.gralloc=minigbm
else
PRODUCT_PACKAGES += android.hardware.graphics.allocator-service.ranchu
endif

PRODUCT_COPY_FILES += \
    device/generic/goldfish/init/init.ranchu.rc:$(TARGET_COPY_OUT_VENDOR)/etc/init/hw/init.ranchu.rc \
    device/generic/goldfish/init/init.net.ranchu.sh:$(TARGET_COPY_OUT_VENDOR)/bin/init.net.ranchu.sh \
    device/generic/goldfish/init/init.setup.ranchu.sh:$(TARGET_COPY_OUT_VENDOR)/bin/init.setup.ranchu.sh

这里有一个容易漏掉的分支:minigbm 产品仍然把 minigbm allocator 放进镜像,同时公共 init.ranchu.rc 负责按属性启动它;普通产品则额外安装 ranchu allocator。安装与启动是两个 owner,只有把两者连起来,才能解释为什么一个 binary 存在但没有运行。

4. 属性桥接 ​

4.1 early-init ​

init.ranchu.rc 的 early-init 是第一条运行时主线。它把 ro.boot.* 输入复制成 framework/native 代码实际读取的属性,并设置 vendor.qemu.dev.bootcomplete=0 作为后续握手状态。

源码文件:device/generic/goldfish/init/init.ranchu.rc

相关函数/类型:on early-init

text
on early-init
    setprop ro.config.low_ram ${ro.boot.config.low_ram}
    setprop ro.cpuvulkan.version ${ro.boot.qemu.cpuvulkan.version}
    setprop ro.hardware.egl ${ro.boot.hardwareegl:-emulation}
    setprop ro.hardware.vulkan ${ro.boot.hardware.vulkan}
    setprop ro.hardware.gralloc ${ro.boot.hardware.gralloc:-ranchu}
    setprop ro.opengles.version ${ro.boot.opengles.version}
    setprop dalvik.vm.heapsize ${ro.boot.dalvik.vm.heapsize:-192m}
    setprop debug.hwui.renderer ${ro.boot.debug.hwui.renderer:-skiagl}
    setprop debug.renderengine.backend ${ro.boot.debug.renderengine.backend:-skiaglthreaded}
    setprop debug.angle.feature_overrides_enabled ${ro.boot.hardware.angle_feature_overrides_enabled}
    setprop debug.angle.feature_overrides_disabled ${ro.boot.hardware.angle_feature_overrides_disabled}
    setprop vendor.qemu.dev.bootcomplete 0

默认值写在 ${property:-fallback} 中,因此“没有传入 ro.boot.hardware.gralloc”并不等于 allocator 没有选择:它会变成 ranchu。相反,ro.hardware.vulkan 没有 fallback,空值会继续向消费者传播。调试时必须先区分“属性缺失”和“属性被默认覆盖”。

4.2 qemu-props ​

有些 emulator 输入仍通过 qemud boot-properties 服务传入。qemu-props 是 vendor root oneshot 程序,由 init 在 on init 启动。它最多重试五次打开服务,发送 list,按 key=value 接收直到 NUL 结束,再调用 property_set。

源码文件:device/generic/goldfish/qemu-props/qemu-props.cpp

cpp
int setBootProperties() {
    using android::base::unique_fd;
    unique_fd qemud;

    for (int tries = 5; tries > 0; --tries) {
        qemud = unique_fd(qemud_channel_open(kBootPropertiesService));
        if (qemud.ok()) {
            break;
        } else if (tries > 1) {
            sleep(1);
        } else {
            return FAILURE(1);
        }
    }

    if (qemud_channel_send(qemud.get(), "list", -1) < 0) {
        return FAILURE(1);
    }

    while (true) {
        char temp[PROPERTY_KEY_MAX + PROPERTY_VALUE_MAX + 2];
        const int len = qemud_channel_recv(qemud.get(), temp, sizeof(temp) - 1);
        if (len < 0 || len > (sizeof(temp) - 1) || !temp[0]) break;
        temp[len] = '\0';
        char* prop_value = strchr(temp, '=');
        if (!prop_value) continue;
        *prop_value = 0;
        ++prop_value;
        if (check_if_property_in_list(temp, k_properties_to_ignore)) continue;

        char renamed_property[sizeof(temp)];
        const char* final_prop_name = temp;
        using namespace std::literals;
        static constexpr std::string_view k_vendor_prefix = "vendor."sv;
        if (need_prepend_prefix(temp, k_vendor_prefix)) {
            snprintf(renamed_property, sizeof(renamed_property), "%.*s%s",
                     int(k_vendor_prefix.size()), k_vendor_prefix.data(), temp);
            final_prop_name = renamed_property;
        } else {
            final_prop_name = temp;
        }
        if (property_set(final_prop_name, prop_value) < 0) {
            ALOGW("could not set property '%s' to '%s'", final_prop_name, prop_value);
        } else {
            ALOGI("successfully set property '%s' to '%s'", final_prop_name, prop_value);
        }
    }
    return 0;
}

代码中的忽略表包括 dalvik.vm.heapsize、ro.opengles.version 和 qemu.adb.secure;这三个值不会由该程序覆盖。qemu.sf.lcd_density 与 qemu.hw.mainkeys 是保留的 system property,其余没有 vendor. 前缀的非 system property 会被加上 vendor.。因此日志里看到 vendor.qemu.* 并不表示宿主机原始键名就是这个形式。

源码文件:device/generic/goldfish/qemu-props/qemu-props.cpp

相关函数/类型:main

cpp
// qemu-props.cpp::main
int main(const int argc, const char* argv[])
{
    if ((argc == 2) && !strcmp(argv[1], "bootcomplete")) {
        sendMessage("bootcomplete");
        return 0;
    }

    int r = setBootProperties();
    if (r) {
        return r;
    }
    parse_virtio_serial();
    sendHeartBeat();
    while (s_QemuMiscPipe >= 0) {
        if (android::base::WaitForProperty(
                    "vendor.qemu.dev.bootcomplete", "1",
                    /*relative_timeout=*/std::chrono::seconds(5))) {
            break;
        }
        sendHeartBeat();
    }
    while (s_QemuMiscPipe >= 0) {
        usleep(30 * 1000000);
        sendHeartBeat();
    }
    closeMiscPipe();
    return 0;
}

main 不是只执行一次就结束:属性设置完成后,它通过 QemuMiscPipe 发送 heartbeat,直到 init 把 bootcomplete 状态改为 1。如果 pipe 读写失败,closeMiscPipe() 把 fd 置为 -1,循环自然退出。这是可恢复边界:属性已经写入的部分不会回滚,但心跳资源会被关闭。

5. 服务分支 ​

5.1 图形选择 ​

ro.hardware.gralloc 的消费者直接写在同一个 rc 文件中:minigbm 启动 vendor.graphics.allocator,其余明确值或空值启动 ranchu allocator。两个 service 都声明 onrestart restart surfaceflinger,说明 allocator 进程异常重启后,SurfaceFlinger 也会被拉起以重新建立图形依赖。

源码文件:device/generic/goldfish/init/init.ranchu.rc

相关函数/类型:vendor.graphics.allocator

text
service vendor.graphics.allocator /vendor/bin/hw/android.hardware.graphics.allocator-service.minigbm
    override
    class hal animation
    user system
    group graphics drmrpc
    onrestart restart surfaceflinger
    disabled

service vendor.graphics.allocator.ranchu /vendor/bin/hw/android.hardware.graphics.allocator-service.ranchu
    override
    class hal animation
    user system
    group graphics drmrpc
    onrestart restart surfaceflinger
    disabled

on property:ro.hardware.gralloc=minigbm
    start vendor.graphics.allocator
    start vendor.camera-provider-ranchu.minigbm

on property:ro.hardware.gralloc=ranchu
    start vendor.graphics.allocator.ranchu
    start vendor.camera-provider-ranchu

on property:ro.hardware.gralloc=
    start vendor.graphics.allocator.ranchu
    start vendor.camera-provider-ranchu

allocator 本身不是“返回一个文件描述符”这么简单。hals/gralloc/allocator.cpp 的 GoldfishAllocator::allocate2 先检查 count、宽高、layerCount、usage 和扩展字段,再按 PixelFormat 计算 plane layout;含 GPU usage 时通过 rcCreateColorBufferDMA 创建 host color buffer,失败则返回 NO_RESOURCES。含 CPU usage 时才保留 guest image allocation 和 stride。这个条件决定了同一个格式请求在不同 usage 下是否会分配 guest 映射内存。

源码文件:device/generic/goldfish/hals/gralloc/allocator.cpp

cpp
ndk::ScopedAStatus allocate2(const BufferDescriptorInfo& desc,
                             const int32_t count,
                             AllocationResult* const outResult) override {
    if (count <= 0) {
        return toBinderStatus(FAILURE_V(AllocationError::BAD_DESCRIPTOR,
                                        "%s: count=%d", "BAD_DESCRIPTOR",
                                        count));
    }
    if (desc.width <= 0) {
        return toBinderStatus(FAILURE_V(AllocationError::BAD_DESCRIPTOR,
                                        "%s: width=%d", "BAD_DESCRIPTOR",
                                        desc.width));
    }
    if (desc.height <= 0) {
        return toBinderStatus(FAILURE_V(AllocationError::BAD_DESCRIPTOR,
                                        "%s: height=%d", "BAD_DESCRIPTOR",
                                        desc.height));
    }
    if (!validateUsage(desc.usage)) {
        return toBinderStatus(FAILURE_V(AllocationError::BAD_DESCRIPTOR,
                                        "%s: usage=0x%" PRIX64, "BAD_DESCRIPTOR",
                                        toUsage64(desc.usage)));
    }
    // ...layerCount、reservedSize、additionalOptions 与 PixelFormat 分支...
    if (needCpuBuffer(usage)) {
        req.needImageAllocation = true;
        req.stride0 = (req.planeSize == 1) ?
                req.plane[0].strideInBytes / req.plane[0].sampleIncrementInBytes : 0;
    } else {
        req.needImageAllocation = false;
        req.planeSize = 0;
        req.stride0 = 0;
    }
    if (!needGpuBuffer(usage)) {
        req.glFormat = -1;
        req.glType = -1;
    }
    std::vector<std::unique_ptr<cb_handle_t>> cbs(count);
    {
        HostConnectionSession connSession(mHostConn.get());
        ExtendedRCEncoderContext* const rcEnc = connSession.getRcEncoder();
        LOG_ALWAYS_FATAL_IF(!rcEnc);
        const bool hasSharedSlots =
            rcEnc->featureInfo_const()->hasSharedSlotsHostMemoryAllocator;

        for (int i = 0; i < count; ++i) {
            std::unique_ptr<cb_handle_t> cb = allocateImpl(
                req, *rcEnc, ++mBufferIdGenerator, hasSharedSlots);
            if (cb) {
                cbs[i] = std::move(cb);
            } else {
                for (--i; i > 0; --i) {
                    unallocate(std::move(cbs[i]));
                }
                return toBinderStatus(FAILURE(AllocationError::NO_RESOURCES));
            }
        }
    }
}

这里的清理分支很关键,但不能只看意图下结论:批量分配中途失败后,循环尝试释放此前得到的 cb_handle_t;然而条件是 i > 0,按字面不会调用 unallocate(cbs[0])。unique_ptr 析构是否足以释放该 handle 持有的全部 fd 和映射,还需要继续核对 cb_handle_t 的析构与所有权实现。因此本文只把它作为“存在显式失败清理”的证据,不声称所有批次位置都已由这段循环证明无泄漏。它同样不能证明宿主机 GPU 模式、virGL 或 SwiftShader 的内部实现。

5.2 图形消费者 ​

android.hardware.graphics.composer3-service.ranchu 是另一个独立 vendor service。其 Main.cpp 创建 Composer,向 ServiceManager 注册 IComposer/default,再启动 Binder thread pool;SurfaceFlinger 通过 AIDL HWC3 接口消费它。allocator 和 composer 都由产品打包,但它们的启动入口、接口和失败处理不同,不能合并成“GPU 服务”。

源码文件:device/generic/goldfish/hals/hwc3/Main.cpp

cpp
int main(int /*argc*/, char** /*argv*/) {
    ALOGI("RanchuHWC (HWComposer3/HWC3) starting up...");
    auto composer = ndk::SharedRefBase::make<Composer>();
    CHECK(composer != nullptr);
    const std::string instance = std::string() + Composer::descriptor + "/default";
    binder_status_t status = AServiceManager_addService(
            composer->asBinder().get(), instance.c_str());
    CHECK(status == STATUS_OK);
    ABinderProcess_setThreadPoolMaxThreadCount(5);
    ABinderProcess_startThreadPool();
    ABinderProcess_joinThreadPool();
    return EXIT_FAILURE;
}

CHECK 失败会终止进程;rc 中的 onrestart restart surfaceflinger 只覆盖 service 重启后的消费者恢复,不会把注册失败变成成功。诊断时应分别看 service 是否启动、ServiceManager 是否有实例、SurfaceFlinger 是否重新连接。

5.3 网络分支 ​

VirtIO Wi-Fi 不是默认无条件启动。on post-fs-data && property:ro.boot.qemu.virtiowifi=1 才会启动 ranchu-net;脚本读取 vendor.net.wifi_mac_prefix 后调用 mac80211_create_radios,还可依据 vendor.net.shared_net_ip 配置第二个接口。

sh
wifi_virtio=`getprop ro.boot.qemu.virtiowifi`
case "$wifi_virtio" in
    1) wifi_mac_prefix=`getprop vendor.net.wifi_mac_prefix`
      if [ -n "$wifi_mac_prefix" ]; then
          /vendor/bin/mac80211_create_radios 1 $wifi_mac_prefix || exit 1
      fi
      ;;
esac

my_ip=`getprop vendor.net.shared_net_ip`
case "$my_ip" in
    "")
    ;;
    *) ifconfig eth1 "$my_ip" netmask 255.255.255.0 up
    ;;
esac

exit 1 会让 oneshot 服务失败,但不会撤销已经创建的 radio;因此恢复操作不能只重启脚本,还要检查 sysfs/netlink 中是否已有接口。这个脚本也没有实现 NAT、DNS 或宿主机端口转发,那些属于 guest 之外的边界。

6. 虚拟串口 ​

qemu-props 在设置普通属性后调用 parse_virtio_serial()。vport_parser.cpp 扫描 /sys/class/virtio-ports/*/name,把每个端口名称转换为 vendor.qemu.vport.<name>=/dev/<id>。随后 rc 用属性触发 symlink,例如 GNSS、Bluetooth 和 UWB。

源码文件:device/generic/goldfish/qemu-props/vport_parser.cpp

cpp
static void set_port_prop(const char* filename, const char* portname) {
    std::ifstream myfile(filename);
    if (!myfile.is_open()) {
        ALOGW("could not open '%s'", filename);
        return;
    }
    const std::string portdev = std::string{"/dev/"} + portname;
    for (std::string line; std::getline(myfile, line); ) {
        std::string serialname = android::base::Trim(line);
        if (serialname.empty()) continue;
        serialname = std::string("vendor.qemu.vport.") + serialname;
        if (property_set(serialname.c_str(), portdev.c_str()) < 0) {
            ALOGW("could not set property '%s' to '%s'", serialname.c_str(), portdev.c_str());
        }
    }
}

void parse_virtio_serial() {
    read_virio_ports_dir("/sys/class/virtio-ports");
}

例如 init.ranchu.rc 对 vendor.qemu.vport.gnss=* 创建 /dev/gnss0,对 UWB 则创建 /dev/hvc2 并启动 vendor.uwb_hal。属性触发器是异步的:property_set 返回成功只证明属性服务接受了写入,不证明 symlink 已经创建或 HAL 已经完成注册。

7. 启动收尾 ​

dev.bootcomplete=1 到达后,rc 只在 vendor.qemu.dev.bootcomplete=0 时执行一次:先把状态设为 1,再启动 qemu-props-bootcomplete 和 ranchu-setup。前者向宿主机发送 bootcomplete,后者依据 ro.boot.qemu.allowsuspend 写 wake lock。这个顺序避免重复发送,但也意味着把 vendor.qemu.dev.bootcomplete 手动改回 0 会重新触发一次收尾动作。

相关源码:

  • device/generic/goldfish/init/init.ranchu.rc
  • dev.bootcomplete
text
on property:dev.bootcomplete=1 && property:vendor.qemu.dev.bootcomplete=0
    setprop vendor.qemu.dev.bootcomplete 1
    start qemu-props-bootcomplete
    start ranchu-setup

service qemu-props-bootcomplete /vendor/bin/qemu-props "bootcomplete"
    user root
    group root
    oneshot
    disabled

init.setup.ranchu.sh 的恢复边界也很具体:属性为空或不是 1 时写 emulator_wake_lock,只有明确为 1 才写 wake unlock。它不管理图形、网络或 userdata;不要把“启动完成”理解成所有 HAL 已经健康。

这条链没有独立的用户取消状态:产品与 BoardConfig 已固化进镜像,只能修改输入后重建;qemu-props、网络和 setup 是 init 管理的进程或 oneshot service,停止表现为进程退出,而不是事务回滚。已经写入的属性、创建的接口和 symlink 不会因为调用方“取消启动”自动恢复,清理必须由对应资源 owner 完成。

8. 配置测试 ​

8.1 产品实验 ​

在固定 tag 的 Goldfish checkout 中,下面的只读命令可以复述产品到 BoardConfig 的第一段链:

bash
git show android-17.0.0_r1:AndroidProducts.mk
git show android-17.0.0_r1:64bitonly/product/sdk_phone64_x86_64.mk
git show android-17.0.0_r1:board/emu64x/BoardConfig.mk
git show android-17.0.0_r1:board/BoardConfigCommon.mk

预期断言是:产品文件含 PRODUCT_DEVICE := emu64x,BoardConfig 含 TARGET_ARCH := x86_64,公共配置含动态分区列表和 AB_OTA_UPDATER := none。这证明构建输入关系,不证明已经执行完整 m 或产生了可启动镜像。

8.2 属性实验 ​

源码级第二个实验检查同一个属性的写入方和消费者:

bash
git grep -n 'ro.boot.hardware.gralloc\|ro.hardware.gralloc' \
  android-17.0.0_r1 -- device/generic/goldfish
git show android-17.0.0_r1:init/init.ranchu.rc
git show android-17.0.0_r1:qemu-props/qemu-props.cpp

断言应能得到三步:ro.boot.hardware.gralloc 在 early-init 变成 ro.hardware.gralloc;属性值触发 allocator service;qemu-props 的 vendor. 前缀规则不会改写这个 init 已经处理的 ro.boot.* 复制关系。它证明静态调用链和条件分支,不证明某台宿主机一定发送了某个 boot property。

8.3 分支实验 ​

若有可启动的自建镜像,可分别选择普通产品和 %_minigbm 产品,记录以下只读结果:

bash
adb shell getprop ro.hardware.gralloc
adb shell 'ps -A | grep -E "allocator|composer3"'
adb shell 'ls -l /dev/gnss0 /dev/hvc2 2>/dev/null'
adb shell 'getprop vendor.qemu.dev.bootcomplete'

普通产品的源码预期是 ranchu allocator;minigbm 产品预期是 minigbm allocator;虚拟串口和 Wi-Fi 还要满足各自的 ro.boot.qemu.* 条件。该实验能验证产品差异和运行时触发器,但不能把结果外推到未使用同一 Goldfish tag、kernel 和宿主机 emulator 的镜像。

9. 故障定位 ​

现象先查失败含义恢复边界
ro.hardware.gralloc 为空init.ranchu.rc 的 fallback 与 property 日志输入缺失时应回退 ranchu;若仍为空,检查 init 是否加载重启 guest,不能靠重启 allocator 补写 early-init 属性
allocator 未运行rc property trigger、产品包列表binary 未安装或属性值没有命中分支修产品/属性后重建镜像
qemu-props 退出qemud open、property_set warning五次连接失败会直接返回;部分属性可能已写入重新启动 qemu-props 不会回滚已写属性
Wi-Fi radio 缺失ro.boot.qemu.virtiowifi、MAC prefix、脚本退出码条件未满足或 mac80211_create_radios 失败清理已有接口后重新运行脚本
HWC3 注册失败AServiceManager_addService、rc onrestartservice 进程终止,SurfaceFlinger 需要重连重启 HWC3/SF;不能修复错误的 manifest

10. 源码导航 ​

读者可以用下面的顺序继续阅读,而不必重新回到旧的 emulator 命令手册:

  1. 从 device/generic/goldfish/AndroidProducts.mk 选一个产品;
  2. 沿 sdk_phone64_x86_64.mk → product/phone.mk → product/base_phone.mk → product/generic.mk 找继承和包;
  3. 用 PRODUCT_DEVICE=emu64x 定位 board/emu64x/BoardConfig.mk,再看 BoardConfigCommon.mk 的分区与 kernel 输入;
  4. 在 init.ranchu.rc 找同名 ro.boot.*、property trigger 和 service;
  5. 对图形继续看 hals/gralloc/allocator.cpp 与 hals/hwc3/Main.cpp,对串口继续看 qemu-props/vport_parser.cpp;
  6. 遇到“属性成功但设备没变化”,分别验证 property_set、init trigger、service 注册和最终消费者,不能把四层日志合成一个结论。

本文能证明 Goldfish/Ranchu guest 侧的配置生效链;不能证明 SDK emulator/QEMU 二进制内部的 GPU 选择算法、快照实现、宿主机 NAT 或 KVM 行为。那些问题需要对应的 prebuilts/android-emulator、external/qemu 固定源码或独立黑盒实验,不能从本篇的 AOSP guest 源码推出。