容器化启动
本文面向已经读过 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 / CuttlefishConfig | flags、instance paths、VMM mode | 参数冲突、镜像路径错误 |
| host VMM | CrosvmManager / crosvm | VM command、control socket、devices | KVM、sandbox、virtio 设备失败 |
| guest init | init Service | PID、namespace、cgroup、service state | exec、SELinux、cgroup 创建失败 |
| guest Zygote | Zygote / native JNI | fork、specialization、app-data mount | UID、mount、SELinux 或 fd 失败 |
| guest SystemServer | SystemServer | 服务注册、设备形态条件 | 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
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
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
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,这个时机边界直接影响恢复后的启动状态。
继续添加资源和虚拟设备:
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:
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 分支
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:
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
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
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 激活通知:
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
// ... 先建立 /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
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 创建与通知
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
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 内的路径。
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
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 专用服务:
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 sandboxing | CrosvmManager | enable_virtiofs/enable_sandbox | guest mount 已失败 |
| KVM 或 crosvm command 失败 | host VMM | StartCommands 的 Result、control socket | SystemServer 代码错误 |
| guest service namespace fatal | init RunService | EnterNamespaces、SELinux、rc flags | host Docker namespace 错误 |
| cgroup notification error | init parent/child FIFO | createProcessGroup、controller | Zygote UID 错误 |
| app data bind mount warning | Zygote native | /data_mirror、CE inode、mount namespace | Cuttlefish host 共享目录错误 |
| ARC audio class 启动 wtf | SystemServer | config_deviceSpecificAudioService | 所有容器都不支持 Audio |
| guest VM reset | host monitor + guest init | crosvm 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 的顺序执行。每组命令都说明输入和输出,避免在错误层级排查。
# 输入: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:
# 输入: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
相关测试入口:
# 输入: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/mountinfoassemble_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 条件选择少数服务。
然后对照一个故障现象回答:
enable_virtiofs=true、enable_sandbox=false时,错误发生在 host 命令构造还是 guest mount?- guest
system_server能运行但/dev/hvc2不存在,应先查哪一层? - 同一 APK 在 user 10 的 UID 是 1010001,Zygote 为什么仍不需要一个“user 10 专用”的进程?
Build.IS_ARC=true时 AudioService 选择改变,为什么不能据此断定 Cuttlefish 运行在 ARC 容器?
如果能把每个答案都指回对应的 owner、状态、消费者和生效时机,就已经区分了 host 容器、guest VM、Android init namespace、Zygote specialization 和 SystemServer 条件服务这五个层次。容器化 Android 的难点不在于新增一个“容器模式”,而在于同一条启动链上同时存在多个进程树、多个 namespace 和多套失败恢复责任。
