FirstStageInit
本文面向已经理解文件描述符、mount、设备节点和 exec 基本语义的读者。请先读内核到init:上一篇已经证明内核如何执行 /init,以及 PID 1 如何最终进入 FirstStageMain();本文不再重复内核候选路径,而是把 system/core/init/first_stage_init.cpp 展开成一条可逐行追踪的资源生命周期。
本文解决的问题是:普通 Android 启动、recovery ramdisk、charger 和首阶段控制台共用同一个 FirstStageMain() 时,哪些对象由首阶段创建,哪些状态必须跨 exec 保留,哪些资源必须在交给 SetupSelinux() 前清理。本文只跟到 execv("/system/bin/init", {"selinux_setup"}),fstab、AVB、dm-verity 和逻辑分区算法留给后续的 FirstStageMount 专题。
读完后,你应能从一条首阶段日志定位到具体分支,解释 /dev、/proc、/sys、/mnt 和 /second_stage_resources 的消费者,并判断模块加载失败、recovery 分流或旧 ramdisk 清理分别会不会阻止启动。
1. 执行约束
FirstStageMain() 运行在 Android 用户空间尚未建立的窗口。此时没有属性服务、没有 ueventd coldboot、没有 rc action,也不能假设 /system、/vendor 已经可访问。它只能依赖 first-stage 静态链接可执行文件、内核接口和 ramdisk 中的资源。
Android 17 的构建定义印证了这个边界。init_first_stage_defaults 把 first_stage_init.cpp、first_stage_mount.cpp、devices.cpp、switch_root.cpp 等编进 first-stage init,并链接静态库。首阶段需要的能力必须进入这个闭包,不能等待二阶段动态加载。
| 资源 | 首阶段所有者 | 直接消费者 | 生命周期 |
|---|---|---|---|
/dev tmpfs | FirstStageMain | 日志、设备发现、modprobe | 跨两个 exec 保留 |
/proc | FirstStageMain | cmdline、bootconfig、进程接口 | 持续到系统运行 |
/sys | FirstStageMain | 模块、块设备、SELinux | 持续到系统运行 |
/mnt tmpfs | FirstStageMain | first-stage mount | 由后续挂载体系接管 |
| 旧 ramdisk | 内核解包,init 清理 | 首阶段文件 | 切根后释放 |
| 环境变量 | FirstStageMain | SELinux/second stage | 读取后主动清除 |
首阶段的正确结束条件不是“所有 Android 服务都可用”,而是 /system/bin/init 已可执行、SELinux 所需输入已可见、跨阶段数据已转存,并且旧根中不再需要的内容可以回收。
2. 临时根
2.1 环境清理
入口先记录启动时间,清除 umask 和环境,再设置受控 PATH。这里使用 CHECKCALL 累积错误,而不是在第一次失败时立即退出:
源码文件:system/core/init/first_stage_init.cpp
相关函数/类型:FirstStageMain
int FirstStageMain(int argc, char** argv) {
boot_clock::time_point start_time = boot_clock::now();
std::vector<std::pair<std::string, int>> errors;
#define CHECKCALL(x) \
if ((x) != 0) errors.emplace_back(#x " failed", errno);
umask(0);
CHECKCALL(clearenv());
CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));clearenv() 的结果是首阶段不信任内核执行 /init 时可能携带的普通环境;随后创建的 FIRST_STAGE_STARTED_AT、INIT_MODULE_DURATION_MS 和 INIT_FORCE_DEBUGGABLE 都是 init 自己定义的交接协议。因为 exec 保留环境,这些值能够被后续阶段消费。
2.2 内核接口
首阶段创建最小文件系统,并在读取 cmdline 和 bootconfig 后收紧 /proc/bootconfig 权限:
源码文件:system/core/init/first_stage_init.cpp
CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"));
CHECKCALL(mkdir("/dev/pts", 0755));
CHECKCALL(mkdir("/dev/socket", 0755));
CHECKCALL(mkdir("/dev/dm-user", 0755));
CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));
#define MAKE_STR(x) __STRING(x)
CHECKCALL(mount("proc", "/proc", "proc", 0,
"hidepid=2,gid=" MAKE_STR(AID_READPROC)));
#undef MAKE_STR
std::string cmdline;
android::base::ReadFileToString("/proc/cmdline", &cmdline);
chmod("/proc/bootconfig", 0440);
std::string bootconfig;
android::base::ReadFileToString("/proc/bootconfig", &bootconfig);
gid_t groups[] = {AID_READPROC};
CHECKCALL(setgroups(arraysize(groups), groups));
CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));
CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));读取发生在 chmod 之后并不矛盾:PID 1 以 root 身份运行,而且紧接着把自己加入 AID_READPROC。hidepid=2 和 bootconfig 的 0440 是后续普通进程的可见性边界,不是为了阻止 init 自己读取。
cmdline 与 bootconfig 是多个分支的共同输入:console 参数、charger 模式、force-normal-boot、模块并行方式和休眠恢复都从这里解析。它们由内核提供,由首阶段读取,但真正的语义所有者分散在各个解析函数中。
2.3 基础节点
ueventd 尚未运行,所以首阶段手工创建自己立刻需要的字符设备:
源码文件:system/core/init/first_stage_init.cpp
CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)));
if constexpr (WORLD_WRITABLE_KMSG) {
CHECKCALL(mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11)));
}
CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)));
CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9)));
CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));
CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)));/dev/kmsg 是 KernelLogger 的输出端;/dev/null 用来固定 0、1、2 三个标准文件描述符;/dev/ptmx 是早于 ueventd 运行的 log wrapper 所需入口。这里不能套用“所有 /dev 都由 ueventd 创建”的常规结论,首阶段正是这个结论的例外窗口。
2.4 暂存目录
接着创建 first-stage mount 和跨阶段数据使用的 tmpfs:
源码文件:system/core/init/first_stage_init.cpp
CHECKCALL(mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
"mode=0755,uid=0,gid=1000"));
CHECKCALL(mkdir("/mnt/vendor", 0755));
CHECKCALL(mkdir("/mnt/product", 0755));
CHECKCALL(mount("tmpfs", "/debug_ramdisk", "tmpfs",
MS_NOEXEC | MS_NOSUID | MS_NODEV,
"mode=0755,uid=0,gid=0"));
CHECKCALL(mount("tmpfs", kSecondStageRes, "tmpfs",
MS_NOEXEC | MS_NOSUID | MS_NODEV,
"mode=0755,uid=0,gid=0"));MS_NOEXEC | MS_NOSUID | MS_NODEV 表明这些目录是数据和挂载暂存区,不是可执行代码来源。/debug_ramdisk 保存调试属性和 policy 输入,/second_stage_resources 保存普通 ramdisk 属性;二者消费者不同,不能合并理解。
Microdroid 且 OpenDice 变更启用时还会创建 /microdroid_resources。这是编译与运行条件共同决定的分支,普通 Android 设备不应把它列为固定启动依赖。
3. 日志边界
3.1 延迟报错
所有 CHECKCALL 完成后,init 才建立日志并统一处理错误:
源码文件:system/core/init/first_stage_init.cpp
#undef CHECKCALL
SetStdioToDevNull(argv);
InitKernelLogging(argv);
if (!errors.empty()) {
for (const auto& [error_string, error_errno] : errors) {
LOG(ERROR) << error_string << " " << strerror(error_errno);
}
LOG(FATAL) << "Init encountered errors starting first stage, aborting";
}
LOG(INFO) << "init first stage started!";这样设计的原因不是忽略错误,而是在 /dev/kmsg 尚未创建前尽量完成基础操作,然后一次输出完整失败集合。最终 LOG(FATAL) 仍会终止启动。排查时不要只截取最后一行 fatal;它只说明集合非空,真正的失败调用位于此前逐条 LOG(ERROR)。
3.2 标准描述符
SetStdioToDevNull() 的实现先打开 /dev/null,再覆盖 0、1、2:
源码文件:system/core/init/first_stage_console.cpp
void SetStdioToDevNull(char** argv) {
int fd = open("/dev/null", O_RDWR);
if (fd == -1) {
int saved_errno = errno;
android::base::InitLogging(argv, &android::base::KernelLogger, InitAborter);
errno = saved_errno;
PLOG(FATAL) << "Couldn't open /dev/null";
}
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > STDERR_FILENO) close(fd);
}这一步同时处理两种内核状态:内核可能已把标准描述符接到 /dev/console,也可能根本没有打开它们。前者会在 SELinux re-exec 时留下访问不匹配,后者可能让未来第一个 open() 意外占用 fd 0。因此首阶段主动固定三个编号,日志再经 KernelLogger 写 /dev/kmsg。
二阶段还会再次调用 SetStdioToDevNull()。原因是首阶段仍处在 kernel SELinux context,它打开的 fd 不一定允许二阶段 context 继续访问;“exec 会保留 fd”是机制事实,“保留的 fd 一定可用”却不是。
4. 模块装载
4.1 模式清单
启动模式决定默认模块清单:
源码文件:system/core/init/first_stage_init.cpp
std::string GetModuleLoadList(BootMode boot_mode,
const std::string& dir_path) {
std::string module_load_file;
switch (boot_mode) {
case BootMode::NORMAL_MODE:
module_load_file = "modules.load";
break;
case BootMode::RECOVERY_MODE:
module_load_file = "modules.load.recovery";
break;
case BootMode::CHARGER_MODE:
module_load_file = "modules.load.charger";
break;
}
if (module_load_file != "modules.load") {
struct stat fileStat{};
std::string load_path = dir_path + "/" + module_load_file;
if (stat(load_path.c_str(), &fileStat)) {
module_load_file = "modules.load";
}
}
return module_load_file;
}recovery 或 charger 专用清单不存在时回退 modules.load。因此看到设备只提供通用清单并不等于专用模式无法启动;真正要检查的是回退后的模块是否覆盖该模式所需驱动。
GetBootMode() 先判断 charger,再判断 recovery;recovery 文件存在但 force_normal_boot 生效时返回 normal:
源码文件:system/core/init/first_stage_init.cpp
static BootMode GetBootMode(const std::string& cmdline,
const std::string& bootconfig) {
if (IsChargerMode(cmdline, bootconfig))
return BootMode::CHARGER_MODE;
else if (IsRecoveryMode() && !ForceNormalBoot(cmdline, bootconfig))
return BootMode::RECOVERY_MODE;
return BootMode::NORMAL_MODE;
}4.2 目录匹配
LoadKernelModules() 读取 uname().release,从 /lib/modules 选择目录。Android 17 不只比较完整 release,还处理 16K/64K 页大小后缀和主次版本回退:
源码文件:system/core/init/first_stage_init.cpp
struct utsname uts{};
if (uname(&uts)) {
LOG(FATAL) << "Failed to get kernel version.";
}
int major = 0, minor = 0;
if (sscanf(uts.release, "%d.%d", &major, &minor) != 2) {
LOG(FATAL) << "Failed to parse kernel version " << uts.release;
}
std::unique_ptr<DIR, decltype(&closedir)> base_dir(
opendir(MODULE_BASE_DIR), closedir);
if (!base_dir) {
LOG(INFO) << "Unable to open /lib/modules, skipping module loading.";
return true;
}/lib/modules 不存在被视为“没有模块需要加载”,返回成功;uname 失败或 release 无法解析则 fatal。遍历目录时若找到 uts.release + page_size_suffix 的精确目录,就清空候选并禁止回退;否则收集主次版本匹配且页大小兼容的目录,排序后依次尝试。
这个顺序解释了一个常见现象:同一 ramdisk 中放了多个 GKI 模块目录时,精确 release 目录拥有最高优先级,而不是按目录顺序碰运气。
4.3 并行模式
bootconfig 可以选择 NORMAL、PERFORMANCE 或 CONSERVATIVE 并行加载,也可打开并行测试。没有配置时走 LoadListedModules():
源码文件:system/core/init/first_stage_init.cpp
auto want_parallel_mode = Modprobe::LoadParallelMode::NONE;
auto want_parallel_test = false;
if (bootconfig.find("androidboot.load_modules_parallel = \"true\"") !=
std::string::npos)
want_parallel_mode = Modprobe::LoadParallelMode::NORMAL;
else if (bootconfig.find(
"androidboot.load_modules_parallel = \"performance\"") !=
std::string::npos)
want_parallel_mode = Modprobe::LoadParallelMode::PERFORMANCE;
else if (bootconfig.find(
"androidboot.load_modules_parallel = \"conservative\"") !=
std::string::npos)
want_parallel_mode = Modprobe::LoadParallelMode::CONSERVATIVE;最终消费者是 Modprobe:
源码文件:system/core/init/first_stage_init.cpp
Modprobe m({MODULE_BASE_DIR}, GetModuleLoadList(boot_mode, MODULE_BASE_DIR));
bool retval = (want_parallel_mode != Modprobe::LoadParallelMode::NONE)
? m.LoadModulesParallel(std::thread::hardware_concurrency(),
want_parallel_mode, want_parallel_test)
: m.LoadListedModules(!want_console);
modules_loaded = m.GetModuleCount();首阶段把模块耗时写入 INIT_MODULE_DURATION_MS,二阶段 RecordStageBoottimes() 再发布为 ro.boottime.init.modules 并 unsetenv()。这是一条完整的 owner 到 consumer 链,而不只是一个日志数字。
4.4 失败降级
源码文件:system/core/init/first_stage_init.cpp
if (!LoadKernelModules(boot_mode, want_console, want_parallel_mode,
want_parallel_test, module_count)) {
if (want_console != FirstStageConsoleParam::DISABLED) {
LOG(ERROR) << "Failed to load kernel modules, starting console";
} else {
LOG(FATAL) << "Failed to load kernel modules";
}
}控制台关闭时,模块失败阻止启动;控制台已请求时,错误降为 LOG(ERROR),让读者进入首阶段 shell 收集证据。这个分支改变的是故障恢复能力,不是把缺失驱动变成成功:后续挂载仍可能因为块设备不存在而 fatal。
5. 启动分流
5.1 控制台
FirstStageConsole() 从 bootconfig 或 cmdline 读取 androidboot.first_stage_console,并限制值域。只有构建时 ALLOW_FIRST_STAGE_CONSOLE 允许,运行参数才会生效。
当值为 CONSOLE_ON_FAILURE 时,init 会先尝试创建 first-stage mount 所需设备,再调用 StartConsole():
源码文件:system/core/init/first_stage_init.cpp
bool created_devices = false;
if (want_console == FirstStageConsoleParam::CONSOLE_ON_FAILURE) {
if (!IsRecoveryMode()) {
fsm = CreateFirstStageMount(cmdline);
if (fsm) {
created_devices = fsm->DoCreateDevices();
if (!created_devices) {
LOG(ERROR) << "Failed to create device nodes early";
}
}
}
StartConsole(cmdline);
}StartConsole() fork 子进程,尝试执行 /first_stage.sh,有 kernel console 时再执行 /system/bin/sh;父进程等待 console 子进程退出后继续。created_devices 防止正常路径重复创建同一批设备。这里的取消路径是用户退出 shell,恢复路径是父进程回到挂载流程,而不是另起一个 init。
5.2 Recovery
recovery 判断不是读取属性,而是检查 /system/bin/recovery:
源码文件:system/core/init/first_stage_init.cpp
相关函数/类型:IsRecoveryMode
bool IsRecoveryMode() {
return access("/system/bin/recovery", F_OK) == 0;
}正常 recovery 模式跳过 DoFirstStageMount();但共享 recovery ramdisk 可以通过 androidboot.force_normal_boot=1 请求普通 Android 启动。此时首阶段先把需要保留的 snapuserd 复制到 /first_stage_ramdisk/system/bin,再切换根:
源码文件:system/core/init/first_stage_init.cpp
相关函数/类型:FirstStageMain
if (ForceNormalBoot(cmdline, bootconfig)) {
mkdir("/first_stage_ramdisk", 0755);
PrepareSwitchRoot();
if (mount("/first_stage_ramdisk", "/first_stage_ramdisk", nullptr,
MS_BIND, nullptr) != 0) {
PLOG(FATAL) << "Could not bind mount /first_stage_ramdisk to itself";
}
SwitchRoot("/first_stage_ramdisk");
}bind mount 自身是因为 SwitchRoot() 要求目标必须是 mount point。PrepareSwitchRoot() 优先选择 generic ramdisk 的 snapuserd_ramdisk,否则使用 vendor ramdisk 的 snapuserd,通过硬链接放到新根中的固定路径。这个准备动作只在 force-normal-boot 分支发生。
5.3 普通挂载
源码文件:system/core/init/first_stage_init.cpp
if (IsRecoveryMode()) {
LOG(INFO) << "First stage mount skipped (recovery mode)";
} else {
if (!fsm) {
fsm = CreateFirstStageMount(cmdline);
}
if (!fsm) {
LOG(FATAL) << "FirstStageMount not available";
}
if (!created_devices && !fsm->DoCreateDevices()) {
LOG(FATAL) << "Failed to create devices required for first stage mount";
} else if (REBOOT_BOOTLOADER_ON_PANIC && !AttemptingToBootNewSlot()) {
InstallRebootSignalHandlers();
}
if (!fsm->DoFirstStageMount()) {
LOG(FATAL) << "Failed to mount required partitions early ...";
}
}对象创建、设备创建和分区挂载是三个独立失败点。REBOOT_BOOTLOADER_ON_PANIC 的 handler 只在设备创建成功之后、且当前不是尝试新 slot 时安装,防止新 slot 启动失败被直接转成 bootloader 重启而破坏回退语义。
6. 跨阶段交接
6.1 Ramdisk属性
首阶段可能在 ramdisk 中看到 /system/etc/ramdisk/build.prop。它不能假设切根后这个路径仍指向同一文件,因此复制到 tmpfs:
源码文件:system/core/init/first_stage_init.cpp
相关函数/类型:PrepareSecondStageResources
if (access(kBootImageRamdiskProp, F_OK) == 0) {
std::string dest = GetRamdiskPropForSecondStage();
std::string dir = android::base::Dirname(dest);
std::error_code ec;
if (!fs::create_directories(dir, ec) && !!ec) {
LOG(FATAL) << "Can't mkdir " << dir << ": " << ec.message();
}
if (!fs::copy_file(kBootImageRamdiskProp, dest, ec)) {
LOG(FATAL) << "Can't copy " << kBootImageRamdiskProp << " to "
<< dest << ": " << ec.message();
}
}GetRamdiskPropForSecondStage() 生成 /second_stage_resources/system/etc/ramdisk/build.prop。二阶段 property service 的 LoadPropertiesFromSecondStageRes() 读取它;不存在只接受 ENOENT,存在却不可读则触发检查,解析失败记录 warning。这里的复制失败是首阶段 fatal,消费失败则按二阶段错误策略处理。
6.2 调试资源
/force_debuggable 存在时,首阶段把 adb_debug.prop 和 userdebug_plat_sepolicy.cil 分别复制到 /debug_ramdisk,并设置 INIT_FORCE_DEBUGGABLE=true:
源码文件:system/core/init/first_stage_init.cpp
相关函数/类型:PrepareSecondStageResources
if (access("/force_debuggable", F_OK) == 0) {
constexpr const char adb_debug_prop_src[] = "/adb_debug.prop";
constexpr const char userdebug_plat_sepolicy_cil_src[] =
"/userdebug_plat_sepolicy.cil";
std::error_code ec;
if (access(adb_debug_prop_src, F_OK) == 0 &&
!fs::copy_file(adb_debug_prop_src, kDebugRamdiskProp, ec)) {
LOG(WARNING) << "Can't copy " << adb_debug_prop_src << " to "
<< kDebugRamdiskProp << ": " << ec.message();
}
if (access(userdebug_plat_sepolicy_cil_src, F_OK) == 0 &&
!fs::copy_file(userdebug_plat_sepolicy_cil_src,
kDebugRamdiskSEPolicy, ec)) {
LOG(WARNING) << "Can't copy " << userdebug_plat_sepolicy_cil_src;
}
setenv("INIT_FORCE_DEBUGGABLE", "true", 1);
}这段代码本身不直接授予 adb root。SELinux 阶段消费调试 policy,property service 消费 adb_debug.prop,而且源码注释明确把设备 unlocked 作为最终启用条件的一部分。首阶段只是搬运输入并设置标志。
6.3 启动计时
进入 SELinux setup 前,首阶段记录开始时间:
源码文件:system/core/init/selinux.cpp
setenv(kEnvFirstStageStartedAt,
std::to_string(start_time.time_since_epoch().count()).c_str(), 1);二阶段 RecordStageBoottimes() 读取 FIRST_STAGE_STARTED_AT 和 SELinux 阶段时间,发布:
源码文件:system/core/init/init.cpp
SetProperty("ro.boottime.init.first_stage",
std::to_string(selinux_start_time_ns - first_stage_start_time_ns));
SetProperty("ro.boottime.init.selinux",
std::to_string(second_stage_start_time.time_since_epoch().count() -
selinux_start_time_ns));读取后使用 unsetenv() 清除交接变量,避免它们继续污染 init 将来启动的服务环境。这是显式清理路径。
7. 旧根清理
首阶段在挂载前保存旧根目录句柄和设备号,挂载后再次读取新根设备号:
源码文件:system/core/init/first_stage_init.cpp
auto old_root_dir = std::unique_ptr<DIR, decltype(&closedir)>{
opendir("/"), closedir};
struct stat old_root_info{};
if (stat("/", &old_root_info) != 0) {
PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
old_root_dir.reset();
}
struct stat new_root_info{};
if (stat("/", &new_root_info) != 0) {
PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
old_root_dir.reset();
}
if (old_root_dir && old_root_info.st_dev != new_root_info.st_dev) {
FreeRamdisk(old_root_dir.get(), old_root_info.st_dev);
}只有根目录设备号发生变化才递归释放旧 ramdisk。FreeRamdisk() 使用已打开的旧根 fd,通过 readdir()、fstatat()、openat() 和 unlinkat() 遍历;遇到属于其他设备的挂载点会跳过,避免删除新挂载内容:
源码文件:system/core/init/first_stage_init.cpp
if (fstatat(dfd, de->d_name, &info, AT_SYMLINK_NOFOLLOW) != 0) {
continue;
}
if (info.st_dev != dev) {
continue;
}正在承担首阶段 snapshot 合并的 snapuserd 也会被保留。清理失败多数被设计为跳过而不是 fatal,因为此时更重要的目标是继续策略切换;代价是旧 ramdisk 页可能暂时不能回收。
最后把 stdout/stderr 指向 /dev/kmsg,并执行 SELinux 模式:
源码文件:system/core/init/first_stage_init.cpp
相关函数/类型:FirstStageMain
const char* path = "/system/bin/init";
const char* args[] = {path, "selinux_setup", nullptr};
auto fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);
execv(path, const_cast<char**>(args));
PLOG(FATAL) << "execv(\"" << path << "\") failed";成功 exec 后首阶段程序映像消失,PID 1、环境和未设置 CLOEXEC 的 fd 按 exec 规则保留。这里要区分原始 fd 和 dup2() 的目标:open() 返回的 fd 带 O_CLOEXEC,会在 exec 时关闭;复制到 stdout/stderr 的 fd 不继承 FD_CLOEXEC,会跨 exec 保留。SetupSelinux() 随后再次调用 SetStdioToDevNull() 和 InitKernelLogging(),重新建立适合新 SELinux context 的标准描述符与日志。
8. 故障矩阵
| 输入或状态 | 失败位置 | 行为 | 恢复入口 |
|---|---|---|---|
| 基础 mount/mknod | errors 集合 | 汇总后 fatal | 最早期 kernel log |
uname 无法解析 | 模块目录选择 | 立即 fatal | 核对 kernel release |
/lib/modules 不存在 | 模块加载 | 视为无模块并继续 | 确认设备是否内建驱动 |
| 模块依赖失败,无 console | LoadKernelModules | fatal | 修复模块清单或依赖 |
| 模块依赖失败,有 console | 同上 | 记录错误并进入 shell | 首阶段 shell 检查设备 |
| 休眠恢复节点失败 | MaybeResumeFromHibernation | 记录 error 并继续冷启动路径 | 检查 /sys/power/resume |
| FirstStageMount 创建失败 | CreateFirstStageMount | 随后 fatal | 检查 fstab/设备树输入 |
| 设备节点失败 | DoCreateDevices | fatal | 检查 uevent 和块设备 |
| 必要分区失败 | DoFirstStageMount | fatal | 检查 AVB、dm、fstab |
| 旧根无法 stat | 清理阶段 | 跳过释放并继续 | 内存占用与早期日志 |
/system/bin/init exec 失败 | 交接末端 | fatal | 检查挂载、权限、格式 |
这个矩阵体现了首阶段的错误策略:建立不可替代的执行前提时 fatal;诊断能力开启时允许部分模块错误降级;清理和休眠恢复失败多记录后继续。不能把所有 LOG(ERROR) 都解释为启动终止,也不能把所有 fallback 都解释为启动成功。
9. 测试闭环
FirstStageMain 本身依赖真实 mount、mknod、内核模块和切根,没有覆盖整条函数的普通 host 单元测试。可用的局部证明来自 libmodprobe:libmodprobe_test.cpp 构造临时的 modules.dep、modules.softdep、modules.load 和 blocklist,断言 LoadListedModules() 的加载顺序、数量和卸载结果;module_dependency_graph_test.cpp 的 SimpleDependency 逐次断言只有依赖满足的模块进入 ready 集合,SimpleDependencyFailed 则证明依赖失败会阻止下游模块就绪。
这些测试能证明 Modprobe 的依赖图和清单消费,不证明某台设备的 /lib/modules 目录选择、bootconfig、块设备和分区挂载。完整启动仍需设备证据。
在 Android 源码根目录可执行以下源码核对:
rg -n "int FirstStageMain|LoadKernelModules|ForceNormalBoot|FreeRamdisk" \
system/core/init/first_stage_init.cpp
rg -n "LoadPropertiesFromSecondStageRes|kDebugRamdiskProp" \
system/core/init/property_service.cpp
rg -n "RecordStageBoottimes|FIRST_STAGE_STARTED_AT" \
system/core/init/init.cpp有测试环境时运行局部测试目标:
atest libmodprobe_tests设备侧先采集输入、分支和消费者,避免只看最终 boot completed:
adb shell 'cat /proc/bootconfig 2>/dev/null | grep -E "first_stage|load_modules|force_normal|mode"'
adb shell 'getprop ro.boottime.init.first_stage'
adb shell 'getprop ro.boottime.init.modules'
adb shell 'dmesg | grep -E "init first stage|Loaded .* modules|First stage mount|freeing ramdisk|first_stage_console"'验证结果应形成闭环:bootconfig 是输入,首阶段日志显示选择的分支,ro.boottime.init.* 是二阶段消费后的状态。如果只有属性值而没有早期日志,它只能证明计时变量被消费,不能证明每个 mount 或模块都走了预期路径。
10. 设备边界
本文已经从 Android 17 真实源码证明:首阶段如何建立最小文件系统与标准描述符,如何根据 boot mode 和页大小选择模块目录,如何在 console、recovery 和 normal boot 之间分流,如何把属性、调试资源与计时跨 exec 交给消费者,以及如何按设备号清理旧 ramdisk。
本文没有展开 FirstStageMount::Create() 如何选择实现、DoCreateDevices() 如何完成 coldboot、MountPartitions() 如何处理 fstab/AVB/dm-verity,也没有证明任意厂商设备都包含相同模块和 bootconfig。下一篇将以 FirstStageMount 为 owner,从 fstab 输入追到每个 early-mount 分区的生效与失败位置。
