Skip to content

内核到init

追踪 Linux kernel_init 如何执行 ramdisk 中的 /init,以及 PID 1 如何进入 first stage、SELinux setup 和 second stage。

基于android-17.0.0_r1
AndroidinitLinux内核启动

内核到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 kernelrdinit=、init=、CONFIG_DEFAULT_INITkernel_execve内核初始化完成后
首阶段FirstStageMainramdisk、设备树、fstab挂载逻辑和 /system/bin/initPID 1 首次执行
策略切换SetupSelinuxpolicy、overlay、/system/bin/initSELinux 与 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

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

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

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

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

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

cpp
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

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

cpp
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

cpp
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

cpp
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

cpp
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

cpp
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

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 一定成功。

建议先看测试输入和断言,再运行:

bash
sed -n '90,178p' system/core/init/init_test.cpp
rg -n "EventTriggerOrder|SimpleEventTrigger" system/core/init/init_test.cpp

设备上可执行的最小验证是收集 PID 1 的命令行和内核日志:

bash
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 入口直接作为新起点。