内核到init
本文面向能阅读 C/C++ 函数、理解进程和文件系统基本概念的读者。你不需要先掌握完整的 Android 启动架构,但应知道内核启动后会创建用户空间进程。建议先读系统镜像结构,其中只需掌握 boot 镜像、ramdisk 和 /init 的文件来源。
本文只回答一个可复核的问题:Android 17 的 Linux 内核在何处、以什么优先级选择用户空间 init,/init 被执行后又如何在同一 PID 内切换到 Android init 的三个阶段。本文不展开 bootloader 如何解包镜像、fstab 的每个语法、SELinux policy 规则细节、服务重启策略或 zygote 启动;这些是后续文章的边界。
读完后,你应能从 kernel_init() 反查到 /init,根据 argv[1] 判断 system/core/init/main.cpp 进入哪一种模式,并用日志、/proc/1/cmdline 和源码测试验证自己的判断。
1. 问题边界
启动链横跨两个独立源码项目。Linux 入口在 kernel/common/init/main.c,Android PID 1 实现在 platform/system/core/init。前者的所有者是内核;后者的所有者是 Android init。内核只负责把一个可执行文件装载成用户进程,挂载分区、加载 SELinux policy 和解析 rc 文件都发生在 Android init 内。
| 阶段 | 所有者 | 输入 | 直接消费者 | 生效时机 |
|---|---|---|---|---|
| 用户空间选择 | Linux kernel | rdinit=、init=、CONFIG_DEFAULT_INIT | kernel_execve | 内核初始化完成后 |
| 首阶段 | FirstStageMain | ramdisk、设备树、fstab | 挂载逻辑和 /system/bin/init | PID 1 首次执行 |
| 策略切换 | SetupSelinux | policy、overlay、/system/bin/init | SELinux 与 second stage | 首阶段挂载结束后 |
| 二阶段 | SecondStageMain | 属性、rc 文件、文件系统 | ActionManager、ServiceList、事件循环 | 策略切换后的 re-exec |
这里的“同一 PID”不是推断。execv() 只替换当前进程的地址空间和程序映像,不创建新进程;因此从日志和 /proc/1/status 观察到的 PID 仍是 1。
2. 内核入口
2.1 参数来源
Android 内核把默认的 ramdisk init 设置为 /init:
源码文件:kernel/common/init/main.c
/* kernel/common/init/main.c */
static char *execute_command;
static char *ramdisk_execute_command = "/init";命令行参数可以覆盖它。Android 17 使用 __setup("rdinit=", ...) 解析 ramdisk init,使用 __setup("init=", ...) 解析普通 init:
源码文件:kernel/common/init/main.c
static int __init rdinit_setup(char *str)
{
unsigned int i;
ramdisk_execute_command = str;
/* See "auto" comment in init_setup */
for (i = 1; i < MAX_INIT_ARGS; i++)
argv_init[i] = NULL;
return 1;
}
__setup("rdinit=", rdinit_setup);
static int __init init_setup(char *str)
{
unsigned int i;
execute_command = str;
for (i = 1; i < MAX_INIT_ARGS; i++)
argv_init[i] = NULL;
return 1;
}
__setup("init=", init_setup);两者语义不同:rdinit= 指向早期用户空间,通常就是 ramdisk 的 /init;init= 是在早期 init 不可用时请求内核直接执行的路径。不能把二者都理解为“最后一次覆盖字符串”,因为 kernel_init() 先尝试 ramdisk_execute_command,只有失败才会继续看 execute_command。
2.2 执行函数
run_init_process() 负责准备内核维护的参数和环境,并调用 kernel_execve():
源码文件:kernel/common/init/main.c
static int run_init_process(const char *init_filename)
{
const char *const *p;
argv_init[0] = init_filename;
pr_info("Run %s as init process\\n", init_filename);
pr_debug(" with arguments:\\n");
for (p = argv_init; *p; p++)
pr_debug(" %s\\n", *p);
pr_debug(" with environment:\\n");
for (p = envp_init; *p; p++)
pr_debug(" %s\\n", *p);
return kernel_execve(init_filename, argv_init, envp_init);
}这里的消费者是用户空间 /init,不是 shell,也不是 Android 的 main.cpp 直接调用。kernel_execve() 成功后不会返回;只有装载失败时才返回错误码,调用者才有机会尝试下一个路径。
2.3 选择顺序
Android 17 的 kernel_init() 在完成内核初始化、等待 initramfs 并打开控制台后,按照固定顺序尝试入口:
源码文件:kernel/common/init/main.c
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\\n",
ramdisk_execute_command, ret);
}
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).", execute_command, ret);
}
if (CONFIG_DEFAULT_INIT[0] != '\\0') {
ret = run_init_process(CONFIG_DEFAULT_INIT);
if (ret)
pr_err("Default init %s failed (error %d)\\n",
CONFIG_DEFAULT_INIT, ret);
else
return 0;
}
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/admin-guide/init.rst for guidance.");try_to_run_init_process() 对不存在的文件(-ENOENT)保持静默尝试,对“文件存在但无法执行”的错误打印错误日志。这个区别决定了故障排查顺序:看到 No working init found 只能说明所有候选都未成功,不能据此断言 /init 不存在;应先找前面的 Failed to execute /init 和具体 errno。
3. 首次执行
3.1 模式分发
/init 在 Android 17 通常链接到同一个 init 可执行文件。system/core/init/main.cpp 不通过不同文件名区分三阶段,而是依据 argv[1] 分发:
源码文件:system/core/init/main.cpp
int main(int argc, char** argv) {
setpriority(PRIO_PROCESS, 0, -20);
if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}
if (argc > 1) {
if (!strcmp(argv[1], "subcontext")) {
const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();
return SubcontextMain(argc, argv, &function_map);
}
if (!strcmp(argv[1], "selinux_setup")) {
return SetupSelinux(argv);
}
if (!strcmp(argv[1], "second_stage")) {
return SecondStageMain(argc, argv);
}
}
#if defined(FIRST_STAGE_INIT) || defined(RECOVERY)
return FirstStageMain(argc, argv);
#else
LOG(FATAL) << "Second-stage init requires an argument to main()";
#endif
}在 first-stage 构建中,内核执行的 /init 没有 second_stage 参数,因此进入 FirstStageMain。首阶段不是“启动所有服务”的阶段,它只建立足够的设备、挂载和文件系统条件,让后续 /system/bin/init 能被可靠执行。
3.2 最小环境
FirstStageMain 首先清理继承环境并建立最小挂载点。以下是 Android 17 first_stage_init.cpp 的实际节选:
源码文件: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));
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));
CHECKCALL(mount("proc", "/proc", "proc", 0,
"hidepid=2,gid=" MAKE_STR(AID_READPROC)));
CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));
CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));
}这些调用的所有者是首阶段 init,消费者分别是后续设备发现、属性服务、SELinux 初始化和诊断工具。CHECKCALL 把错误先收集起来,而不是在每个挂载失败处立即退出;后续代码会统一记录并决定是否继续。因而“看到某个 mount 返回错误”不能直接等同于“启动必然停止”,要继续追踪 errors 的消费位置。
3.3 分区挂载
首阶段挂载由 FirstStageMount::DoFirstStageMount() 负责。它先处理空或不兼容的 fstab,再调用 MountPartitions():
源码文件:system/core/init/first_stage_mount.cpp
bool FirstStageMount::DoFirstStageMount() {
if (!IsDmLinearEnabled() && fstab_.empty()) {
LOG(INFO) << "First stage mount skipped "
<< "(missing/incompatible/empty fstab in device tree)";
return true;
}
if (!MountPartitions()) return false;
return true;
}在 FirstStageMain 中,创建设备或挂载失败会进入 LOG(FATAL);恢复模式则跳过普通首阶段挂载。正常设备因此可能在同一入口表现出三种结果:成功挂载、明确 fatal、或因 recovery/特殊设备模式而跳过。读源码时必须把模式条件和错误分支一起记录。
3.4 执行切换
挂载结束后,首阶段准备环境并执行同一文件的 selinux_setup 模式:
源码文件: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";这里的生效时机是首阶段挂载完成之后;消费者是 main() 的 SetupSelinux 分支。execv 成功时不会执行后面的 PLOG(FATAL),而 PID、打开的可继承描述符和部分环境仍按 exec 语义保留。
4. 策略切换
4.1 加载策略
SetupSelinux() 先初始化日志和内核日志,再按 Android 或 Microdroid 路径加载 policy,随后设置 enforcing。它还会恢复 /system/bin/init 的 SELinux 上下文:
源码文件:system/core/init/selinux.cpp
相关函数/类型:SetupSelinux
bool use_overlays = EarlySetupOverlays();
if (IsMicrodroid()) {
LoadSelinuxPolicyMicrodroid();
} else {
LoadSelinuxPolicyAndroid();
}
SelinuxSetEnforcement();
if (selinux_android_restorecon("/system/bin/init", 0) == -1) {
PLOG(FATAL) << "restorecon failed of /system/bin/init failed";
}policy 的所有者是 SELinux 初始化代码和 policy 文件;消费者是后续 second-stage 进程、服务域和文件访问检查。restorecon 的位置很关键:它发生在重新执行 /system/bin/init 之前,确保二阶段入口文件拥有可执行的正确上下文。
4.2 再次执行
策略准备后,SetupSelinux 使用显式参数再次 exec:
源码文件:system/core/init/selinux.cpp
相关函数/类型:SetupSelinux
const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));
PLOG(FATAL) << "execv(\"" << path << "\") failed";第二次 exec 仍由 PID 1 完成。此时 main() 看到 argv[1] == "second_stage",进入 SecondStageMain;若 /system/bin/init 不可执行,唯一正常返回路径是 fatal 日志,系统不会退回 first stage。
5. 二阶段
5.1 基础服务
SecondStageMain 把首阶段遗留的最小环境扩展成 Android init 运行环境。关键顺序是:属性初始化、二阶段资源清理、额外文件系统、SELinux label、epoll、信号处理、属性服务:
源码文件:system/core/init/init.cpp
相关函数/类型:SecondStageMain
PropertyInit();
UmountSecondStageRes();
MountExtraFilesystems();
SelabelInitialize();
SelinuxRestoreContext();
Epoll epoll;
if (auto result = epoll.Open(); !result.ok()) {
PLOG(FATAL) << result.error();
}
epoll.SetFirstCallback(ReapAnyOutstandingChildren);
InstallSignalFdHandler(&epoll);
InstallInitNotifier(&epoll);
StartPropertyService(&property_fd);Epoll 是事件消费者,ReapAnyOutstandingChildren 被设为第一个回调,以便 init 在处理其他请求前回收退出的子进程,避免服务状态仍显示为退出中时被重复启动。属性服务则成为后续 setprop 请求和 property trigger 的入口。
5.2 脚本加载
LoadBootScripts() 创建解析器并按顺序读取启动配置:默认入口是 /system/etc/init/hw/init.rc,随后扫描 system、system_ext、vendor、odm 和 product 的 init 目录;ro.boot.init_rc 非空时则使用指定脚本:
源码文件:system/core/init/init.cpp
相关函数/类型:LoadBootScripts
static void LoadBootScripts(ActionManager& action_manager,
ServiceList& service_list) {
Parser parser = CreateParser(action_manager, service_list);
std::string bootscript = GetProperty("ro.boot.init_rc", "");
if (bootscript.empty()) {
parser.ParseConfig("/system/etc/init/hw/init.rc");
if (!parser.ParseConfig("/system/etc/init")) {
late_import_paths.emplace_back("/system/etc/init");
}
parser.ParseConfig("/system_ext/etc/init");
if (!parser.ParseConfig("/vendor/etc/init")) {
late_import_paths.emplace_back("/vendor/etc/init");
}
if (!parser.ParseConfig("/odm/etc/init")) {
late_import_paths.emplace_back("/odm/etc/init");
}
parser.ParseConfig("/product/etc/init");
} else {
parser.ParseConfig(bootscript);
}
}解析结果的所有者是 ActionManager 和 ServiceList;消费者是后续 action 执行器和服务启动器。目录不存在并不在这里统一 fatal,部分路径被放入 late_import_paths,因此缺少 vendor/odm 配置的影响要到后续导入时机判断。
5.3 事件循环
脚本加载完成后,init 把启动事件排入队列。正常模式进入 late-init,充电模式进入 charger:
源码文件:system/core/init/init.cpp
am.QueueEventTrigger("early-init");
am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");
am.QueueEventTrigger("init");
if (bootmode == BootMode::CHARGER_MODE) {
am.QueueEventTrigger("charger");
} else {
am.QueueEventTrigger("late-init");
}
am.QueueBuiltinAction(queue_property_triggers_action,
"queue_property_triggers");
while (true) {
if (am.HasMoreCommands()) {
am.ExecuteOneCommand();
}
auto epoll_timeout = std::chrono::milliseconds{1000};
auto epoll_result = epoll.Wait(epoll_timeout);
if (!epoll_result.ok()) {
LOG(ERROR) << epoll_result.error();
}
}上段省略了与关机、property waiter 和 timeout 相关的条件,但保留了事件进入队列和被消费的真实顺序。ActionManager 拥有待执行动作,Epoll 拥有文件描述符事件;二者在主循环中交替推进。事件入队不等于动作已生效,只有 ExecuteOneCommand() 消费后才会调用 builtin 或启动服务。
6. 故障路径
6.1 内核失败
可执行文件不存在时,try_to_run_init_process() 只在候选路径上继续;文件存在但权限、格式或解释器错误时,内核打印 Starting init: ... exists but couldn't execute it。所有候选失败后进入 panic。恢复动作不是修改 Android rc,而是检查 boot/init_boot 中是否实际包含可执行 /init、文件架构是否匹配、rdinit/init 是否覆盖了预期路径。
6.2 首阶段失败
设备创建、必需分区挂载和 /system/bin/init exec 都是 fatal 边界。DoFirstStageMount() 返回 false 会触发 Failed to mount required partitions early ...;execv 失败则打印对应 errno。此时 second stage 尚未开始,不能用 property service 或 init rc 的日志解释根因。
6.3 策略失败
policy 加载、enforcing 设置和 /system/bin/init restorecon 失败都发生在 second stage 之前。因为 re-exec 只有成功才会返回,SetupSelinux 的 fatal 通常意味着路径、上下文或 policy 不一致,而不是“二阶段 action 没执行”。
6.4 二阶段失败
Epoll::Open()、SELinux label 初始化、配置解析和事件执行属于二阶段。脚本缺失可能被记录为延迟导入,单个 action 或 service 的失败则由对应 builtin/service 逻辑处理。服务退出后的恢复由 SIGCHLD 回收和 Service 状态机承担,本文只证明回收回调的注册顺序,不展开重启策略。
7. 启动观测
system/core/init/init_test.cpp 直接测试 ActionManager 的事件语义,而不是测试真实设备的完整启动。例如 EventTriggerOrder 和 EventTriggerOrderMultipleFiles 断言多个配置文件中的触发顺序;SimpleEventTrigger 证明简单 event trigger 能入队并执行。这些测试能证明解析与队列的局部契约,不能证明 bootloader 已把正确 ramdisk 交给内核,也不能证明某个设备的 fstab、SELinux policy 或 vendor rc 一定成功。
建议先看测试输入和断言,再运行:
sed -n '90,178p' system/core/init/init_test.cpp
rg -n "EventTriggerOrder|SimpleEventTrigger" system/core/init/init_test.cpp设备上可执行的最小验证是收集 PID 1 的命令行和内核日志:
adb shell 'tr "\\0" " " < /proc/1/cmdline; echo'
adb shell 'cat /proc/1/status | sed -n "1,8p"'
adb shell 'dmesg | grep -E "Run /init|Failed to execute|init second stage started|No working init"'
adb shell 'logcat -b kernel -d | grep -E "Run /init|Failed to execute"'正常设备通常在 first stage 观察到空参数或构建相关参数,在策略切换和二阶段观察到 selinux_setup、second_stage 的瞬时日志;/proc/1/status 的 PID 应保持 1。日志缺失不能单独证明阶段未执行,因为早期 stdout/stderr 会被重定向到 /dev/kmsg,还要结合 kernel buffer 和持久化 bootstat。
8. 阅读边界
本文已经证明的范围是:Android 17 kernel/common 的 init 选择顺序;/init 到 FirstStageMain 的模式入口;首阶段到 SetupSelinux、再到 SecondStageMain 的同 PID exec 链;二阶段脚本加载、事件入队和主循环的所有权关系。
尚未证明的范围包括 bootloader 如何构造 bootconfig、具体设备的 ramdisk 文件布局、AVB/dm-verity 的校验细节、SELinux policy 的 allow 规则、rc parser 的语法、Service 的 fork/exec 与重启,以及 zygote 之后的 framework 启动。下一篇从 FirstStageMain 的设备节点和挂载准备展开,读者可以把本文的 /init 入口直接作为新起点。
