Skip to content

容器化启动

基于 AOSP Cuttlefish、crosvm、Android init 和 Zygote 源码,解析容器宿主与 Android guest 的启动边界、virtiofs、namespace 和条件化服务。

基于android-17.0.0_r1
AndroidSystemServerCuttlefishARCcrosvmvirtiofsnamespacecgroup源码阅读

容器化启动 ​

本文面向已经读过 SystemServer.run、ZygoteInit.main、服务启动顺序 和 Zygote 参数协议 的读者。本文不把 Cuttlefish 当作一套抽象的“虚拟 Android”,而是沿源码回答一个边界问题:宿主机上的 cvd/crosvm 如何构造 guest,guest 中的 init 如何创建服务进程,Android 的 SystemServer/Zygote 为什么仍然沿用普通启动路径,以及容器或虚拟设备到底改变了哪一层。

先做两个概念区分。Cuttlefish 的常见形态是一个运行 Android guest 的虚拟机,host 侧的 crosvm 进程可以再运行在 Docker 容器中;这和 ARC 的 Chrome OS Android 容器不是同一个实现。AOSP SystemServer.java 确实包含 Build.IS_ARC 和 org.chromium.arc 的条件分支,但这些分支只改变少数系统服务选择,不能推出 Cuttlefish 的 VM 拓扑。本文因此把 host 构造、guest init、guest Zygote 和 SystemServer 条件服务分别讲清楚。

读完后,你应能从 launch_cvd 的配置选项追到 crosvm 的 run 命令,解释 --shared-dir、--vsock、virtio-console 和 disk 参数如何成为 guest 设备;能从 init 的 Service::RunService() 找到 namespace、cgroup 和 executable 的生效时机;能从 Zygote 的 app-data isolation 代码解释 /data_mirror 和 bind mount 的作用;还能判断一条 ARC 分支是否真的与容器化有关。

1. 边界模型 ​

图的上半部分属于 Cuttlefish host,SystemServer 位于 guest;host crosvm 并不直接启动 SystemServer,而是提供 guest kernel 可见的硬件设备。图右侧的 host process monitor 只负责监控和重启 host 辅助进程,不能替代 guest init 的 Android service 生命周期。

层真实 owner主要状态典型故障
host 配置assemble_cvd / CuttlefishConfigflags、instance paths、VMM mode参数冲突、镜像路径错误
host VMMCrosvmManager / crosvmVM command、control socket、devicesKVM、sandbox、virtio 设备失败
guest initinit ServicePID、namespace、cgroup、service stateexec、SELinux、cgroup 创建失败
guest ZygoteZygote / native JNIfork、specialization、app-data mountUID、mount、SELinux 或 fd 失败
guest SystemServerSystemServer服务注册、设备形态条件framework service 初始化失败

2. Cuttlefish配置 ​

assemble_cvd 先把 gflags 解析成 CuttlefishConfig::InstanceSpecific,后续 host 命令只读取这个配置对象。--enable_sandbox、--enable_virtiofs 和 GPU mode 的默认值在 flags 阶段就可能被改写,不能只看命令行传入值。

源码文件:device/google/cuttlefish/host/commands/assemble_cvd/flags.cc

相关定义:enable_sandbox、enable_virtiofs

cpp
DEFINE_vec(
    enable_sandbox, fmt::format("{}", CF_DEFAULTS_ENABLE_SANDBOX),
    "Enable crosvm sandbox assuming /var/empty and seccomp directories exist. "
    "--noenable-sandbox will disable crosvm sandbox. "
    "When no option is given, sandbox is disabled if Cuttlefish is running "
    "inside a container, or if GPU is enabled, "
    "or if the empty /var/empty directory either does not exist and "
    "cannot be created. Otherwise, sandbox is enabled on the supported "
    "architecture when no option is given.");

DEFINE_vec(enable_virtiofs,
           fmt::format("{}", CF_DEFAULTS_ENABLE_VIRTIOFS),
           "Enable shared folder using virtiofs");

这里的“容器检测”只影响 host 侧 crosvm sandbox 默认值,并不表示 guest Android 会切换到另一套 Zygote。enable_virtiofs 只是允许构造共享目录设备,真正的 --shared-dir 参数稍后在 CrosvmManager 中添加。

host 容器判断的实现也很具体:

源码文件:device/google/cuttlefish/common/libs/utils/container.cpp

相关函数:IsRunningInContainer

cpp
static bool IsRunningInDocker() {
  // /.dockerenv is Docker's marker file for the host process.
  static std::string docker_env_path("/.dockerenv");
  static bool ret = FileExists(docker_env_path)
                 || DirectoryExists(docker_env_path);
  return ret;
}

bool IsRunningInContainer() {
  // 当前实现只识别 Docker;其他容器不会自动得到同样结论。
  return IsRunningInDocker();
}

这段代码的状态 owner 是 host 进程的静态缓存 ret,输入是宿主文件系统中的 /.dockerenv。它不能检测 guest namespace,也不能从 ro.hardware 或 Android feature 推断 ARC。若未来支持其他容器,变化点会在这个 helper,而不是 SystemServer。

3. VMM命令 ​

CrosvmManager::StartCommands() 把一个实例的 kernel、内存、CPU、磁盘和 I/O 设备拼成 crosvm 命令。它的返回值是 MonitorCommand 列表,交给 run_cvd 的 host 进程监控器启动和跟踪。

源码文件:device/google/cuttlefish/host/libs/vm_manager/crosvm_manager.cpp

相关函数:CrosvmManager::StartCommands

cpp
Result<std::vector<MonitorCommand>> CrosvmManager::StartCommands(
    const CuttlefishConfig& config,
    std::vector<VmmDependencyCommand*>& dependencyCommands) {
  auto instance = config.ForDefaultInstance();
  auto environment = config.ForDefaultEnvironment();
  std::vector<MonitorCommand> commands;

  CrosvmBuilder crosvm_cmd;
  crosvm_cmd.Cmd().AddPrerequisite([&dependencyCommands]() -> Result<void> {
    for (auto dependencyCommand : dependencyCommands) {
      CF_EXPECT(dependencyCommand->WaitForAvailability());
    }
    return {};
  });

  // 恢复快照时只在第一次启动 crosvm 时加入 restore 参数。
  std::string first_time_argument;
  if (IsRestoring(config)) {
    // ... 从 snapshot metadata 计算 guest snapshot 路径。
    first_time_argument = "--restore=" + restore_path;
  }

  const bool gpu_capture_enabled =
      !instance.gpu_capture_binary().empty();
  if (!gpu_capture_enabled) {
    crosvm_cmd.ApplyProcessRestarter(
        instance.crosvm_binary(), first_time_argument,
        kCrosvmVmResetExitCode);
  }
  crosvm_cmd.Cmd().AddParameter("run");
  crosvm_cmd.AddControlSocket(
      instance.CrosvmSocketPath(),
      instance.crosvm_binary());

依赖命令先由 prerequisite 等待,crosvm 才开始;ProcessRestarter 负责 host VMM 异常退出后的重启,而不是 guest 内 init 的 service restart。快照恢复参数只在第一次 invocation 加入,guest 自己请求重启时不会重复 restore,这个时机边界直接影响恢复后的启动状态。

继续添加资源和虚拟设备:

cpp
  if (!config.kvm_path().empty()) {
    crosvm_cmd.AddKvmPath(config.kvm_path());
  }
  if (!instance.smt()) {
    crosvm_cmd.Cmd().AddParameter("--no-smt");
  }
  if (!instance.enable_usb()) {
    crosvm_cmd.Cmd().AddParameter("--no-usb");
  }
  crosvm_cmd.Cmd().AddParameter("--core-scheduling=false");
  crosvm_cmd.Cmd().AddParameter(
      "--vhost-user-connect-timeout-ms=", 30 * 1000);

  if (instance.vhost_net()) {
    crosvm_cmd.Cmd().AddParameter("--vhost-net");
  }
  if (instance.protected_vm()) {
    crosvm_cmd.Cmd().AddParameter("--protected-vm");
  }
  if (!instance.crosvm_use_balloon()) {
    crosvm_cmd.Cmd().AddParameter("--no-balloon");
  }
  if (!instance.crosvm_use_rng()) {
    crosvm_cmd.Cmd().AddParameter("--no-rng");
  }

  crosvm_cmd.Cmd().AddParameter("--mem=", instance.memory_mb());
  CF_EXPECT(crosvm_cmd.AddCpus(
      instance.cpus(), instance.vcpu_config_path()));

--mem 和 AddCpus() 只改变 guest 可见资源,不会改变 Android framework 的 user/process 模型。protected_vm、sandbox 和 vhost-user 是 host/VMM 约束;出错时 command builder 返回 Result,不会生成一个“看起来启动成功但没有设备”的半成品命令。

磁盘列表按实例配置转换为 read-only/read-write 或 vhost-user block:

cpp
  auto disk_num = instance.virtual_disk_paths().size();
  CF_EXPECT(VmManager::kMaxDisks >= disk_num,
            "Provided too many disks ("
                << disk_num << "), maximum "
                << VmManager::kMaxDisks << " supported");
  size_t disk_i = 0;
  for (const auto& disk : instance.virtual_disk_paths()) {
    if (instance.protected_vm()) {
      crosvm_cmd.AddReadOnlyDisk(disk);
    } else if (instance.vhost_user_block()
               && disk_i == 2) {
      // ... 启动 vhost-user block helper 并等待 socket。
      crosvm_cmd.Cmd().AddParameter(
          "--vhost-user=block,socket=", socket_path,
          ",pci-address=", pci_addr);
    } else {
      crosvm_cmd.AddReadWriteDisk(disk);
    }
    disk_i++;
  }

磁盘顺序是 guest 设备枚举的一部分。disk_i == 2 的 vhost-user block 特例说明虚拟设备路径仍有兼容边界;不能把所有磁盘都概括为“挂载 host 目录”。Android 的 /data、/system 依旧以 guest block device 形式出现。

4. virtio通道 ​

Cuttlefish 用 virtio-console、vsock、tap/vhost-net、vhost-user input/GPU 和可选 virtiofs 把 host 能力暴露给 guest。下面只摘出与启动诊断最相关的三类通道。

源码文件:device/google/cuttlefish/host/libs/vm_manager/crosvm_manager.cpp

相关位置:CrosvmManager::StartCommands 的 vsock、console、virtiofs 分支

cpp
  if (instance.vsock_guest_cid() >= 2) {
    if (instance.vhost_user_vsock()) {
      crosvm_cmd.AddVhostUser(
          "vsock", fmt::format(
              "{}/vsock_{}_{}/vhost.socket",
              TempDir(), instance.vsock_guest_cid(), getuid()));
    } else if (config.vhost_vsock_path().empty()) {
      crosvm_cmd.Cmd().AddParameter(
          "--vsock=cid=", instance.vsock_guest_cid());
    } else {
      crosvm_cmd.Cmd().AddParameter(
          "--vsock=cid=", instance.vsock_guest_cid(),
          ",device=", config.vhost_vsock_path());
    }
  }

  // /dev/hvc0 = kernel console
  crosvm_cmd.AddHvcReadOnly(
      instance.kernel_log_pipe_name(),
      instance.enable_kernel_log());

  // /dev/hvc2 = logcat
  crosvm_cmd.AddHvcReadOnly(
      instance.logcat_pipe_name());

vsock 是 host/guest 的 socket 通道,virtio-console 则把 kernel、userspace 和 logcat 分到不同 hvc 端口。它们的消费者是 guest 内的驱动和服务,不是 SystemServer 的 Binder;当 guest 无法启动时,先检查 hvc/logcat 和 vsock 连接,不能直接归因于 framework service。

virtiofs 的实现是一个共享目录参数,并且强制要求 crosvm sandbox:

cpp
  if (instance.enable_virtiofs()) {
    CF_EXPECT(instance.enable_sandbox(),
              "virtiofs is currently not supported "
              "without sandboxing");
    // security_ctx=false 避免 host fscreate 属性写入失败。
    crosvm_cmd.Cmd().AddParameter(
        "--shared-dir=", instance.PerInstancePath(kSharedDirName),
        ":shared:type=fs:security_ctx=false");
  }

这里的 failure path 很关键:enable_virtiofs=true 且 sandbox=false 会在 host 命令构造阶段失败,而不是 guest 启动后才发现目录不可用。security_ctx=false 只改变 host 侧 virtiofs 共享目录的安全上下文处理,不能替代 Android guest 的 SELinux labeling。

5. Host监控 ​

run_cvd 收集 CommandSource 返回的 host 子进程,并由 ProcessMonitor 决定是否在退出后重启。这是 Cuttlefish host 的恢复机制,与 guest Android init 的 Service 状态机平行存在。

源码文件:device/google/cuttlefish/host/commands/run_cvd/server_loop_impl.cpp

相关函数:ServerLoopImpl::Run

cpp
Result<void> ServerLoopImpl::Run() {
  // Monitor and restart host processes supporting the CVD.
  auto process_monitor_properties = ProcessMonitor::Properties();
  process_monitor_properties.RestartSubprocesses(
      instance_.restart_subprocesses());
  process_monitor_properties.StraceLogDir(
      instance_.PerInstanceLogPath(""));
  process_monitor_properties.StraceCommands(
      config_.straced_host_executables());

  for (auto& command_source : command_sources_) {
    if (command_source->Enabled()) {
      auto commands = CF_EXPECT(command_source->Commands());
      for (auto& command : commands) {
        process_monitor_properties.AddCommand(
            std::move(command));
      }
    }
  }

  ProcessMonitor process_monitor(
      std::move(process_monitor_properties),
      channel_to_secure_env);
  return process_monitor.Monitor();
}

restart_subprocesses 的 owner 是 host ProcessMonitor,作用对象是 crosvm、vhost-device、WebRTC 等 host helper。guest 内的 system_server 崩溃不会直接由这个 monitor 识别为 Android 用户进程崩溃;它通常先表现为 guest VM 的状态变化或 crosvm reset,再由 guest init/SystemServer 记录自己的状态。

6. Guest init ​

guest kernel 启动后,Android init 读取 rc 并创建 service。容器或 VM 并不自动为每个 Android service 建一个 mount namespace;只有 rc 的 namespace 配置或 init 的 mount namespace 状态要求时才会 clone。

源码文件:system/core/init/service.cpp

相关函数:Service::Start、Service::RunService

cpp
Result<void> Service::Start() {
    // ... 解析 executable、SELinux context 和 service flags。
    if (!mount_namespace_.has_value()) {
        // 记录该 service 应该在哪个 mount namespace 启动。
        SetMountNamespace();
    }

    std::vector<Descriptor> descriptors;
    for (const auto& socket : sockets_) {
        if (auto result = socket.Create(scon); result.ok()) {
            descriptors.emplace_back(std::move(*result));
        }
    }

    pid_t pid = -1;
    if (namespaces_.flags) {
        // rc 的 namespace 配置在这里决定 clone,而非普通 fork。
        pid = clone(nullptr, nullptr,
                    namespaces_.flags | SIGCHLD,
                    nullptr);
    } else {
        pid = fork();
    }

    if (pid == 0) {
        umask(077);
        cgroups_activated.CloseWriteFd();
        setsid_finished.CloseReadFd();
        RunService(std::move(descriptors),
                   std::move(cgroups_activated),
                   std::move(setsid_finished));
        _exit(127);
    }

Service::Start() 的 parent 负责记录 PID 和创建 cgroup,child 才进入 RunService()。如果 namespaces_.flags 非零,clone() 同时建立 pid/mount 等 namespace;否则 service 与 init 共享已有 namespace。这个分支决定的是 guest 内 Android daemon 的隔离,不是 Cuttlefish host 容器的 Docker namespace。

child 的第一件事是进入 namespace、发布 fd 并等待 cgroup 激活通知:

cpp
void Service::RunService(
        const std::vector<Descriptor>& descriptors,
        InterprocessFifo cgroups_activated,
        InterprocessFifo setsid_finished) {
    if (auto result = EnterNamespaces(
            namespaces_, name_, mount_namespace_);
            !result.ok()) {
        LOG(FATAL) << "Service '" << name_
                   << "' failed to set up namespaces: "
                   << result.error();
    }

    for (const auto& [key, value] : once_environment_vars_) {
        setenv(key.c_str(), value.c_str(), 1);
    }
    for (const auto& descriptor : descriptors) {
        descriptor.Publish();
    }

    if (auto result = WritePidToFiles(
            &writepid_files_); !result.ok()) {
        LOG(ERROR) << "failed to write pid to files: "
                   << result.error();
    }

    Result<uint8_t> byte = cgroups_activated.Read();
    if (!byte.ok()) {
        LOG(ERROR) << name_
                   << ": failed to read cgroup notification: "
                   << byte.error();
    }
    cgroups_activated.Close();
    if (*byte != kCgroupsActivated) {
        LOG(FATAL) << "Service '" << name_
                   << "' failed to start due to a fatal error";
        _exit(EXIT_FAILURE);
    }

这里有一个明确的同步屏障:service child 在 cgroup controller 激活前不执行最终的 task profile 和 execv()。如果通知管道关闭或收到错误标记,child 直接退出,parent 不会把它当作稳定运行的 daemon。namespace、环境变量、fd 发布和 cgroup 激活是连续阶段,顺序不能任意交换。

这张图把两个容易混淆的 parent/child 动作分开:parent 在 Start() 中创建 process group 并等待握手,child 在 RunService() 中进入 namespace 并等待激活。只有收到 kCgroupsActivated,child 才会继续到最终的进程执行阶段;因此 guest container 的 namespace 和 cgroup 不是 crosvm 的 host 参数直接“传进去”的,而是 Android init 在 guest 内重新建立的。

7. Guest namespace ​

init 的 mount namespace 不是“容器模式开关”,而是为了处理 APEX 激活和 vold 挂载传播。Android 17 可能同时维护 bootstrap/default 两个 mount namespace,service 在启动时被永久标记到其中一个。

源码文件:system/core/init/mount_namespace.cpp

相关函数:SetupMountNamespaces

cpp
// ... 先建立 /mnt/user、/mnt/installer 和 /mnt/androidwritable 的传播关系。
if (!(BindMount("/mnt/user", "/mnt/installer"))) return false;
if (!(BindMount("/mnt/user", "/mnt/androidwritable"))) return false;
if (!(ChangeMount("/mnt/installer", MS_SLAVE))) return false;
if (!(ChangeMount("/mnt/androidwritable", MS_SLAVE))) return false;
if (!(ChangeMount("/mnt/installer", MS_SHARED))) return false;
if (!(ChangeMount("/mnt/androidwritable", MS_SHARED))) return false;

bootstrap_ns_fd.reset(OpenMountNamespace());

bool needs_two_mnt_ns = NeedsTwoMountNamespaces();
if (needs_two_mnt_ns) {
    // clone 一个 default namespace,再切回 bootstrap namespace。
    if (unshare(CLONE_NEWNS) == -1) {
        PLOG(ERROR) << "Cannot create mount namespace";
        return false;
    }
    default_ns_fd.reset(OpenMountNamespace());
    if (setns(bootstrap_ns_fd.get(), CLONE_NEWNS) == -1) {
        PLOG(ERROR) << "Cannot switch back to bootstrap mount namespace";
        return false;
    }
} else {
    // 没有 APEX 分裂需求时两个 namespace 是同一个。
    default_ns_fd.reset(OpenMountNamespace());
}

NeedsTwoMountNamespaces() 的消费者是 init/apexd 的启动顺序;它与 guest 是否运行在 Cuttlefish VM 没有一一对应关系。Cuttlefish 只是提供 guest kernel 和 block/virtio 设备,namespace 是否拆分仍由 Android 分区和 APEX 状态决定。

service 记录 mount namespace 的时机如下:

源码文件:system/core/init/service.cpp

相关函数:Service::SetMountNamespace

cpp
void Service::SetMountNamespace() {
    // apexd 在当前 namespace 中工作,不预先固定它。
    if (args_[0] == "/system/bin/apexd") {
        return;
    }

    static const std::set<std::string>
            kUseDefaultMountNamespace = {
        "ueventd",
        "hwservicemanager",
        "servicemanager",
    };
    if (kUseDefaultMountNamespace.find(name_)
            != kUseDefaultMountNamespace.end()) {
        mount_namespace_ = NS_DEFAULT;
        return;
    }
    mount_namespace_ = IsDefaultMountNamespaceReady()
            ? NS_DEFAULT : NS_BOOTSTRAP;
}

注释中的“永久”很重要:service 以后因 crash 被重新启动时,仍沿用第一次确定的 mount namespace,保证同一个 daemon 不会因为重启时机不同而看到不同的 APEX 集合。这个状态是 init Service 对象的 owner,而不是 crosvm 配置。

8. Guest cgroup ​

service 的 cgroup 创建发生在 parent,child 通过 FIFO 等待激活;因此 cgroup 既是资源控制器,也是“启动已进入可运行阶段”的握手。

源码文件:system/core/init/service.cpp

相关位置:Service::Start 的 cgroup 创建与通知

cpp
time_started_ = boot_clock::now();
pid_ = pid;
flags_ |= SVC_RUNNING;
start_order_ = next_start_order_++;
process_cgroup_empty_ = false;

if (CgroupsAvailable()) {
    bool use_memcg =
            swappiness_ != -1
            || soft_limit_in_bytes_ != -1
            || limit_in_bytes_ != -1
            || limit_percent_ != -1
            || !limit_property_.empty();
    errno = -createProcessGroup(
            uid(), pid_, use_memcg);
    if (errno != 0) {
        Result<void> result = cgroups_activated.Write(
                kActivatingCgroupsFailed);
        return Error() << "createProcessGroup failed for service '"
                       << name_ << "': " << strerror(errno);
    }

    SetProcessProfiles(uid(), pid_,
                       {"NormalIoPriority"});
    if (use_memcg) {
        ConfigureMemcg();
    }
}

if (Result<void> result = cgroups_activated.Write(
        kCgroupsActivated); !result.ok()) {
    return Error() << "Sending cgroups activated notification failed: "
                   << result.error();
}

parent 先设置 pid_/SVC_RUNNING,再创建 process group;但 child 尚未通过 FIFO 前,RunService() 不会完成 exec。若 cgroup 创建失败,通知 child 失败并将 Start() 返回错误,避免产生一个没有资源策略的 service 假象。容器 host 是否拥有 cgroup v2 权限只影响 host 能否启动 Cuttlefish;guest service 的 cgroup 仍由 guest init 重新建立。

9. Zygote数据隔离 ​

guest 中的普通应用进程继续由 Zygote fork。容器化/虚拟化不会让应用直接访问 host 路径;Zygote 会在 app data isolation 开启时,把 /data_mirror/data_ce 和 /data_mirror/data_de 中的目录 bind mount 到应用可见目录。

源码文件:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

相关函数:createAndMountAppData、isolateAppDataPerPackage

cpp
static bool createAndMountAppData(
    std::string_view package_name,
    std::string_view mirror_pkg_dir_name,
    std::string_view mirror_data_path,
    std::string_view actual_data_path,
    fail_fn_t fail_fn, bool call_fail_fn) {
  char mirrorAppDataPath[PATH_MAX];
  char actualAppDataPath[PATH_MAX];
  snprintf(mirrorAppDataPath, PATH_MAX,
           "%s/%s", mirror_data_path.data(),
           mirror_pkg_dir_name.data());
  snprintf(actualAppDataPath, PATH_MAX,
           "%s/%s", actual_data_path.data(),
           package_name.data());

  PrepareDir(actualAppDataPath, 0700,
             AID_ROOT, AID_ROOT, fail_fn);

  if (call_fail_fn) {
    BindMount(mirrorAppDataPath,
              actualAppDataPath, fail_fn);
  } else if (!BindMount(mirrorAppDataPath,
                        actualAppDataPath)) {
    ALOGW("Failed to mount %s to %s: %s",
          mirrorAppDataPath, actualAppDataPath,
          strerror(errno));
    return false;
  }
  return true;
}

call_fail_fn 区分“失败必须终止 specialization”和“失败允许尝试解密后名称再重试”。实际路径由 mirror_data_path 和 actual_data_path 参数决定,函数没有访问 host 文件系统的特殊分支;它只操作 guest 当前 mount namespace 内的路径。

cpp
static void isolateAppDataPerPackage(
    int userId, std::string_view package_name,
    std::string_view volume_uuid,
    long long ce_data_inode,
    std::string_view actualCePath,
    std::string_view actualDePath,
    fail_fn_t fail_fn) {
  char mirrorCePath[PATH_MAX];
  char mirrorDePath[PATH_MAX];
  char mirrorCeParent[PATH_MAX];
  snprintf(mirrorCeParent, PATH_MAX,
           "/data_mirror/data_ce/%s",
           volume_uuid.data());
  snprintf(mirrorCePath, PATH_MAX,
           "%s/%d", mirrorCeParent, userId);
  snprintf(mirrorDePath, PATH_MAX,
           "/data_mirror/data_de/%s/%d",
           volume_uuid.data(), userId);

  createAndMountAppData(
      package_name, package_name, mirrorDePath,
      actualDePath, fail_fn, true);

  std::string ce_data_path = getAppDataDirName(
      mirrorCePath, package_name,
      ce_data_inode, fail_fn);
  if (ce_data_path.empty()) {
    ALOGE("Ignoring missing CE app data dir for %s",
          package_name.data());
    return;
  }
  if (!createAndMountAppData(
          package_name, ce_data_path, mirrorCePath,
          actualCePath, fail_fn, false)) {
    // CE 解锁后目录名可能变化,再查一次 inode。
    ce_data_path = getAppDataDirName(
        mirrorCePath, package_name,
        ce_data_inode, fail_fn);
    if (!ce_data_path.empty()) {
      mountAppData(package_name, ce_data_path,
                   mirrorCePath, actualCePath, fail_fn);
    }
  }
}

这段代码把 userId 放入 mirror 路径,先挂 DE,再根据 CE inode 找到加密目录。CE 目录名在锁定状态下可能不是 package name,因此失败后通过 inode 重新定位。这里的 failure path 是“记录并忽略缺失 CE app data”,不是把整个 Zygote 当作容器失败;应用可能在 CE 解锁后再次获得可见数据。

10. ARC分支 ​

ARC 在当前 AOSP 中有两种不同判断:Build.IS_ARC 是构建/运行时平台属性,isArc 是 PackageManager 的 org.chromium.arc feature。它们的消费者不同,不能合并成一个“容器模式”。

源码文件:frameworks/base/services/java/com/android/server/SystemServer.java

相关位置:startOtherServices 与 startBootstrapServices

java
boolean isArc = context.getPackageManager()
        .hasSystemFeature("org.chromium.arc");

// ... 设备类型和其他 config property 的读取。

t.traceBegin("StartAudioService");
if (!isArc) {
    mSystemServiceManager.startService(
            AudioService.Lifecycle.class);
} else {
    String className = context.getResources()
            .getString(
                    R.string.config_deviceSpecificAudioService);
    try {
        mSystemServiceManager.startService(
                className + "$Lifecycle");
    } catch (Throwable e) {
        reportWtf("starting " + className, e);
    }
}
t.traceEnd();

isArc 只决定 AudioService 的实现选择。它不改变 Zygote fork 协议,也不替代 crosvm 的 VM 构造。若 ARC 的 device-specific audio service 类名为空或启动异常,SystemServer 记录 wtf 并继续执行其他服务启动。

另一个条件使用 Build.IS_ARC 启动 ARC 专用服务:

java
if (Build.IS_ARC) {
    t.traceBegin("StartArcSystemHealthService");
    mSystemServiceManager.startService(
            ARC_SYSTEM_HEALTH_SERVICE);
    t.traceEnd();
}

if (Build.IS_ARC
        && SystemProperties.getInt(
                "ro.boot.dev_mode", 0) == 1) {
    t.traceBegin("StartArcPersistentDataBlock");
    mSystemServiceManager.startService(
            ARC_PERSISTENT_DATA_BLOCK_SERVICE_CLASS);
    t.traceEnd();
}

这里的 owner 是 SystemServer 的服务编排逻辑,输入是编译属性和 ro.boot.dev_mode;这些服务只在对应平台构建条件满足时注册。不能从这几个分支推导“所有容器环境都跳过 Camera/WMS/Audio”,实际服务是否启动还取决于其他资源和 feature 条件。

11. 启动关系图 ​

这条时序把 host 重启与 guest 启动分开:ProcessMonitor 可以重启 crosvm,但 guest 内的 init 状态是否保留取决于 VM 是否 reset、restore 或正常 reboot。SystemServer 只在 guest Android 层看到自己的 SystemProperties、Binder 和服务状态,不直接拥有 host CuttlefishConfig。

12. 失败边界 ​

失败现象owner先检查不应直接推出
virtiofs ... without sandboxingCrosvmManagerenable_virtiofs/enable_sandboxguest mount 已失败
KVM 或 crosvm command 失败host VMMStartCommands 的 Result、control socketSystemServer 代码错误
guest service namespace fatalinit RunServiceEnterNamespaces、SELinux、rc flagshost Docker namespace 错误
cgroup notification errorinit parent/child FIFOcreateProcessGroup、controllerZygote UID 错误
app data bind mount warningZygote native/data_mirror、CE inode、mount namespaceCuttlefish host 共享目录错误
ARC audio class 启动 wtfSystemServerconfig_deviceSpecificAudioService所有容器都不支持 Audio
guest VM resethost monitor + guest initcrosvm exit code、guest logcat一定是 framework crash

恢复动作也分层:host command 失败由 Result 返回和 ProcessMonitor 处理;guest service 失败由 init 的 service state 和 restart policy 处理;Zygote app-data 失败可能只影响一个应用的可见目录;SystemServer 条件服务失败则由 reportWtf 或服务启动异常路径处理。没有一条全局“容器恢复函数”覆盖这些情况。

13. 诊断方法 ​

以下命令按 host → guest kernel → guest Android 的顺序执行。每组命令都说明输入和输出,避免在错误层级排查。

bash
# 输入:host 的 Cuttlefish 实例目录和进程。输出:crosvm、run_cvd、host helper 状态。
ps -ef | rg 'launch_cvd|run_cvd|crosvm|vhost|process_monitor'
cvd fleet list

# 输入:$CUTTLEFISH_CONFIG 指定的实例配置。输出:vm_manager、sandbox、virtiofs、内存、CPU 和实例路径。
grep -E 'vm_manager|enable_sandbox|enable_virtiofs|cpus|memory_mb|crosvm_binary' \
  "$CUTTLEFISH_CONFIG"

# 输入:host crosvm control socket。输出:VM 运行状态或控制命令错误。
crosvm status /path/to/crosvm.sock

# 输入:guest ADB。输出:guest kernel/init/zygote/system_server 的进程和属性状态。
adb shell getprop init.svc.zygote
adb shell getprop sys.boot_completed
adb shell ps -A -o PID,PPID,UID,NAME | rg 'init|zygote|system_server'

$CUTTLEFISH_CONFIG 应指向实际生成的 Cuttlefish 实例配置文件;如果使用自定义实例目录,应设为该实例的配置路径。公开文章中的 /path/to 只是命令参数占位,不是源码路径;运行时需要填入 CrosvmSocketPath() 生成的实例 socket。

guest 侧继续检查设备节点、mount 和 cgroup:

bash
# 输入:guest 设备节点。输出:virtio console、vsock、block 等驱动是否出现。
adb shell ls -l /dev/hvc* /dev/vsock /dev/block

# 输入:guest mount 表。输出:bootstrap/default namespace 可见的挂载和 /data_mirror。
adb shell cat /proc/1/mountinfo | rg 'apex|data_mirror|mnt/user|mnt/installer'

# 输入:指定进程 PID。输出:其 mount namespace、cgroup 和 UID。
adb shell readlink /proc/$(pidof system_server)/ns/mnt
adb shell cat /proc/$(pidof system_server)/cgroup
adb shell cat /proc/$(pidof system_server)/status | rg '^Uid:'

# 输入:应用包名和 userId。输出:该用户的数据目录、组合 UID 和进程。
adb shell cmd package path com.example.app
adb shell cmd package list packages --uid 1010001
adb shell ps -A -o PID,UID,NAME | rg 'u10_|com.example.app'

如果 /dev/hvc2 不存在,先查 crosvm 的 HVC 参数和 guest kernel 驱动;如果 /data_mirror 存在但应用目录 bind mount 失败,查 Zygote 的 CE inode、mount namespace 和 package data 状态;如果 system_server UID 正常但服务缺失,再看 SystemServer 的 Build.IS_ARC/feature 条件,而不是继续修改 crosvm。

14. 源码与测试 ​

Cuttlefish host 的 assemble_cvd 单元测试主要验证 flags、配置和命令构造;guest init、Zygote 和 SystemServer 的测试则位于各自模块。跨 host/guest 的完整启动仍需要设备或 VM 实验,不能由单个 Java 测试替代。

源码文件:device/google/cuttlefish/host/commands/assemble_cvd/unittest/main_test.cc

相关测试入口:

bash
# 输入:Cuttlefish host 构建环境。输出:assemble_cvd 配置/flags 单元测试结果。
atest assemble_cvd_unittest

# 输入:frameworks/base service 测试构建环境。输出:UserManager、SystemServer 相关 guest service 测试结果。
atest FrameworksServicesTests:UserControllerTest

# 输入:设备或 Cuttlefish guest。输出:实际 init/zygote/system_server 进程与 namespace 状态。
adb shell dumpsys activity service SystemService
adb shell dumpsys user
adb shell cat /proc/1/mountinfo

assemble_cvd_unittest 的证明范围是 host 配置和命令生成;UserControllerTest 证明 Android 用户生命周期局部状态;adb 观察才可以把 host 虚拟设备、guest init 和 framework 运行时连起来。若 host 只运行在普通 Linux 而非 Docker,不能据此证明 IsRunningInContainer() 的分支;若没有启用 virtiofs,也不能把 /data_mirror 的 bind mount 与 --shared-dir 混为一谈。

15. 学习收束 ​

尝试不看正文复述以下路径:assemble_cvd 如何把 enable_sandbox/enable_virtiofs 变成 CuttlefishConfig;CrosvmManager::StartCommands() 如何加入 run、磁盘、CPU、内存、vsock、hvc 和 --shared-dir;guest init 如何在 Service::Start() 中选择 clone/fork、创建 cgroup 并让 child 等待激活;Zygote 如何在 guest mount namespace 中把按 user 的 /data_mirror bind 到应用目录;最后 SystemServer 如何只对 ARC feature/Build 条件选择少数服务。

然后对照一个故障现象回答:

  1. enable_virtiofs=true、enable_sandbox=false 时,错误发生在 host 命令构造还是 guest mount?
  2. guest system_server 能运行但 /dev/hvc2 不存在,应先查哪一层?
  3. 同一 APK 在 user 10 的 UID 是 1010001,Zygote 为什么仍不需要一个“user 10 专用”的进程?
  4. Build.IS_ARC=true 时 AudioService 选择改变,为什么不能据此断定 Cuttlefish 运行在 ARC 容器?

如果能把每个答案都指回对应的 owner、状态、消费者和生效时机,就已经区分了 host 容器、guest VM、Android init namespace、Zygote specialization 和 SystemServer 条件服务这五个层次。容器化 Android 的难点不在于新增一个“容器模式”,而在于同一条启动链上同时存在多个进程树、多个 namespace 和多套失败恢复责任。