Skip to content

SecondStageInit

追踪 SecondStageMain 如何接收 SELinux 阶段交接、初始化属性与文件系统、注册信号和 property service,并把 rc 动作送入主循环。

基于android-17.0.0_r1
AndroidinitSecondStageInitPropertyServiceEpoll

SecondStageInit ​

本文面向已经读过 SELinux启动 并理解 exec、property、epoll 和 init.rc 基本概念的读者。前文停在 SetupSelinux() 执行 /system/bin/init second_stage;本文从 SecondStageMain() 的第一条初始化语句开始,追踪 PID 1 如何把“可执行进程”变成“拥有属性、信号、挂载命名空间、脚本动作和服务表的 init”。

本文不把主循环的每一种 epoll 事件、SIGCHLD 的全部回收策略或 PropertyService 的 socket 协议字段提前讲完,它们分别由后续 IN006~IN008 和属性系统专题负责。本文的边界是启动顺序和所有权:哪个状态在何时建立,谁消费它,失败会阻止哪一个后续阶段。

读完后,你应能从 SecondStageMain() 判断 /second_stage_resources 为什么必须在 PropertyInit() 之后卸载,解释 property service 的两个公开 socket 与 init 内部 socketpair 的区别,定位 early-init、init、late-init/charger 何时入队,并用源码测试和设备日志验证动作是否真正被消费。

1. 阶段边界 ​

Second stage 是首个可以读取完整 Android 配置并运行长期事件循环的 init 阶段。它接收前一阶段留下的三类输入:

输入生产者Second stage 消费者消费后状态
INIT_FORCE_DEBUGGABLEFirstStageInitdebug property 选择立即 unsetenv
/second_stage_resourcesFirstStageInitPropertyInit属性读取后卸载
INIT_AVB_VERSIONFirstStageMountro.boot.avb_version 发布设置属性后 unsetenv
FIRST_STAGE_STARTED_AT/SELINUX_STARTED_AT前两阶段RecordStageBoottimes发布 boot time 后清理

它还建立新的 owner:ActionManager 持有 rc 动作,ServiceList 持有服务定义,Epoll 持有事件 fd,PropertyService 线程持有 property socket。它们都在 SecondStageMain() 中被创建或注册,但真正的动作执行要等主循环消费队列。

2. 入口准备 ​

2.1 日志与环境 ​

Android 17 入口首先处理 reboot panic、记录时间、设置 shutdown 回调、重建标准描述符和内核日志:

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

cpp
int SecondStageMain(int argc, char** argv) {
    if (REBOOT_BOOTLOADER_ON_PANIC && !AttemptingToBootNewSlot()) {
        InstallRebootSignalHandlers();
    }

    // No threads should be spin up until signalfd is registered.
    boot_clock::time_point start_time = boot_clock::now();

    trigger_shutdown = [](const std::string& command) {
        shutdown_state.TriggerShutdown(command);
    };

    SetStdioToDevNull(argv);
    InitKernelLogging(argv);
    LOG(INFO) << "init second stage started!";

    SelinuxSetupKernelLogging();

    if (setenv("PATH", _PATH_DEFPATH, 1) != 0) {
        PLOG(FATAL) << "Could not set $PATH to '" << _PATH_DEFPATH
                    << "' in second stage";
    }

PATH 在 first stage 已设置,但这里再次设置是为了支持“second-stage init 比 first-stage init 更新”的 system-only OTA 情况。trigger_shutdown 是关机路径的 owner 注入点;它不立即执行命令,而是把命令交给 shutdown_state,主循环稍后检查。

2.2 SIGPIPE ​

init 安装一个空操作 handler,而不是 SIG_IGN:

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

cpp
struct sigaction action = {.sa_flags = SA_RESTART};
action.sa_handler = [](int) {};
sigaction(SIGPIPE, &action, nullptr);

写 socket 得到 EPIPE 时,调用点自行处理;使用自定义 handler 的原因是 handler 不会跨 exec 继承,而 SIG_IGN 会继承给 init fork/exec 的子进程。这个选择的消费者不是 SecondStageMain 自己,而是所有随后启动的 service。

2.3 启动标记 ​

Second stage 把 PID 1 的 OOM 调整值写入 proc,并创建权限为 0000 的 /dev/.booting:

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

cpp
if (auto result = WriteFile(
        "/proc/1/oom_score_adj",
        StringPrintf("%d", DEFAULT_OOM_SCORE_ADJUST));
    !result.ok()) {
    LOG(ERROR) << "Unable to write " << DEFAULT_OOM_SCORE_ADJUST
               << " to /proc/1/oom_score_adj: " << result.error();
}

close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));

OOM 写入失败只记录 error,启动不会在这里 fatal;.booting 是后台 firmware loader 等组件的存在性信号,代码没有在这里删除它,生命周期由后续启动脚本或消费者决定。不能把“文件创建成功”理解为 boot 已完成。

3. Debug资源 ​

3.1 标志判断 ​

首阶段可能设置 INIT_FORCE_DEBUGGABLE,Second stage 只有在设备 unlocked 且值为 true 时才保留 debug property:

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

cpp
const char* force_debuggable_env = getenv("INIT_FORCE_DEBUGGABLE");
bool load_debug_prop = false;
if (force_debuggable_env && AvbHandle::IsDeviceUnlocked()) {
    load_debug_prop = "true"s == force_debuggable_env;
}
unsetenv("INIT_FORCE_DEBUGGABLE");

环境变量只是一次性决策输入。load_debug_prop 是局部状态,后面控制 debug ramdisk 的卸载和 PropertyInit() 的加载,不会被继续传给服务。

3.2 卸载时机 ​

如果不加载 debug property,先卸载 /debug_ramdisk,防止 PropertyInit 读取不应该生效的 .prop 文件:

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

cpp
if (!load_debug_prop) {
    UmountDebugRamdisk();
}

PropertyInit();
UmountSecondStageRes();

if (load_debug_prop) {
    UmountDebugRamdisk();
}

两个顺序不是重复操作:/second_stage_resources 必须留到 PropertyInit() 读取 ramdisk build.prop 后才能卸载;debug ramdisk 则在“是否允许读取”两种分支中分别于 PropertyInit 前或后卸载。Umount* 失败只记录 error,代码不在这里回滚属性初始化。

4. 属性初始化 ​

4.1 区域建立 ​

PropertyInit() 先安装 property audit callback,建立共享内存目录、序列化 property info 和 bionic property area:

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

cpp
void PropertyInit() {
    selinux_callback cb;
    cb.func_audit = PropertyAuditCallback;
    selinux_set_callback(SELINUX_CB_AUDIT, cb);

    mkdir("/dev/__properties__", S_IRWXU | S_IXGRP | S_IXOTH);
    CreateSerializedPropertyInfo();
    if (__system_property_area_init()) {
        LOG(FATAL) << "Failed to initialize property area";
    }
    if (!property_info_area.LoadDefaultPath()) {
        LOG(FATAL) << "Failed to load serialized property info file";
    }

这里的 property area 是读取端直接 mmap 的共享状态,property service socket 是写入端请求通道;两者不是同一个对象。目录创建或 area 初始化失败会阻止 SecondStageMain 继续,因为后续 rc 触发器和服务依赖属性状态。

4.2 输入优先级 ​

启动输入的顺序是 device tree、bootconfig、kernel cmdline,再导出 boot props、加载默认值和派生默认值:

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

cpp
// DT 优先于命令行
ProcessKernelDt();
ProcessBootconfig();
ProcessKernelCmdline();

ExportKernelBootProps();
PropertyLoadBootDefaults();
PropertyLoadDerivedDefaults();

注释明确说明:如果命令行和 DT 同时提供参数,DT 属性优先。bootconfig 与 cmdline 的 androidboot.* 会转换成 ro.boot.*,而 ExportKernelBootProps() 再把内核变量映射到 init 内部使用的属性。读日志时看到 ro.boot.*,不能直接断言它来自 cmdline。

4.3 Debug属性消费者 ​

PropertyInit() 的 boot property 加载会消费 /second_stage_resources/system/etc/ramdisk/build.prop,以及在允许时消费 /debug_ramdisk/adb_debug.prop。这解释了前面两个卸载点:卸载过早会改变可见输入,卸载过晚会让后续服务继续看到不应存在的文件。

5. 基础挂载 ​

5.1 临时文件系统 ​

Android 17 的 MountExtraFilesystems() 只挂载 init 主流程要求的三个 tmpfs:

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

相关函数/类型:MountExtraFilesystems

cpp
static void MountExtraFilesystems() {
#define CHECKCALL(x) \
    if ((x) != 0) PLOG(FATAL) << #x " failed.";

    CHECKCALL(mount("tmpfs", "/apex", "tmpfs",
                    MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=0"));

    if (NeedsTwoMountNamespaces()) {
        CHECKCALL(mount("tmpfs", "/bootstrap-apex", "tmpfs",
                        MS_NOEXEC | MS_NOSUID | MS_NODEV,
                        "mode=0755,uid=0,gid=0"));
    }

    CHECKCALL(mount("tmpfs", "/linkerconfig", "tmpfs",
                    MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=0"));
#undef CHECKCALL
}

/apex 是 APEX 激活点,/bootstrap-apex 仅在需要两个 mount namespace 时建立,/linkerconfig 保存按 namespace 生成的 linker 配置。bpf、cgroup 和 tracefs 不由这段代码挂载,不能把常见 Android 启动清单直接套进当前源码。

5.2 Label缓存 ​

挂载完成后先建立 file context handle,再递归恢复首阶段创建或映射的路径:

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

相关函数/类型:SelabelInitialize

cpp
void SelabelInitialize() {
    sehandle = selinux_android_file_context_handle();
    selinux_android_set_sehandle(sehandle);
}

SelabelInitialize() 的 owner 是缓存的 sehandle;SelinuxRestoreContext() 的消费者是 /dev/block、/dev/dm-user、/apex、/bootstrap-apex 和 /linkerconfig 等路径的后续访问。缓存避免每次 restorecon 重新打开 file_contexts,但不改变 policy 本身。

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

相关函数/类型:SelinuxRestoreContext

cpp
void SelinuxRestoreContext() {
    LOG(INFO) << "Running restorecon...";
    selinux_android_restorecon("/dev", 0);
    selinux_android_restorecon("/dev/block",
                               SELINUX_ANDROID_RESTORECON_RECURSE);
    selinux_android_restorecon("/dev/dm-user",
                               SELINUX_ANDROID_RESTORECON_RECURSE);
    selinux_android_restorecon("/dev/device-mapper", 0);
    selinux_android_restorecon("/apex", 0);
    selinux_android_restorecon("/bootstrap-apex", 0);
    selinux_android_restorecon("/linkerconfig", 0);
}

这一步是启动期 label 批处理,不是把每个新文件都自动标记;后续 ueventd、property service 和 init builtin 各有自己的 label 使用点。

6. 事件设施 ​

6.1 Epoll注册 ​

Second stage 在创建线程前打开 epoll,并把子进程回收设为第一个 callback:

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

cpp
Epoll epoll;
if (auto result = epoll.Open(); !result.ok()) {
    PLOG(FATAL) << result.error();
}

epoll.SetFirstCallback(ReapAnyOutstandingChildren);
InstallSignalFdHandler(&epoll);
InstallInitNotifier(&epoll);

“first callback”是一个并发顺序契约:init 在处理其他 pending function 前先回收已退出子进程,避免 ctl.start 观察到旧的 running 状态并重复启动服务。epoll 本身只拥有 fd 和回调,不拥有 Service 状态。

6.2 信号fd ​

InstallSignalFdHandler() 为 SIGCHLD 设置 SA_NOCLDSTOP,注册 fork 后解除阻塞的 atfork handler,并把 SIGCHLD fd 注册到 epoll:

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

cpp
const struct sigaction act{
    .sa_flags = SA_NOCLDSTOP,
    .sa_handler = SIG_DFL
};
sigaction(SIGCHLD, &act, nullptr);

const int result = pthread_atfork(nullptr, nullptr, &UnblockSignals);
if (result != 0) {
    LOG(FATAL) << "Failed to register a fork handler: "
               << strerror(result);
}

Result<void> cs_result = RegisterSignalFd(
        epoll, SIGCHLD, Service::GetSigchldFd());
if (!cs_result.ok()) {
    PLOG(FATAL) << cs_result.error();
}

不可 reboot-capable 的设备还会创建 SIGTERM signalfd。SIGCHLD fd 的实际消费者是 HandleSignalFd() → ReapAnyOutstandingChildren() → Service::Reap(),不是普通 signal handler 直接做业务。

6.3 Init通知 ​

InstallInitNotifier() 提供其他 init 内部路径唤醒主线程的 fd。典型生产者包括 PropertyChanged、控制消息队列和服务状态变化;主线程被唤醒后仍需在主循环中消费动作,通知本身不等于 action 执行。

7. 属性服务 ​

7.1 内部通道 ​

StartPropertyService() 先建立 init 与 property service 线程之间的 SOCK_SEQPACKET socketpair:

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

cpp
void StartPropertyService(int* epoll_socket) {
    InitPropertySet("ro.property_service.version", "2");

    int sockets[2];
    if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC,
                   0, sockets) != 0) {
        PLOG(FATAL) << "Failed to socketpair() between property_service and init";
    }
    *epoll_socket = from_init_socket = sockets[0];
    init_socket = sockets[1];
    StartSendingMessages();

其中一端返回给 SecondStageMain,作为 init epoll 的 property_fd;另一端由 property service 线程监听,用于请求 init 执行加载持久化属性等内部消息。它不是应用调用 setprop 的公开 socket。

7.2 公共socket ​

随后启动两个监听 socket:system 专用的 property_service_for_system 和普通 property_service:

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

cpp
StartThread(PROP_SERVICE_FOR_SYSTEM_NAME, 0660, AID_SYSTEM,
            property_service_for_system_thread, true);
StartThread(PROP_SERVICE_NAME, 0666, 0,
            property_service_thread, false);

auto async_persist_writes = android::base::GetBoolProperty(
        "ro.property_service.async_persist_writes", false);
if (async_persist_writes) {
    persist_write_thread = std::make_unique<PersistWriteThread>();
}

StartThread() 创建 socket、listen(fd, 8),然后启动 PropertyServiceThread;是否监听 init 内部 socket由 listen_init 参数决定。公开 socket 的消费者是 __system_property_set()/setprop 客户端,内部 socket 的消费者是 init 自身。

8. 启动配置 ​

8.1 Namespace ​

Property service 和 epoll 就绪后,Second stage 才建立 APEX 相关 mount namespace:

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

cpp
if (!SetupMountNamespaces()) {
    PLOG(FATAL) << "SetupMountNamespaces failed";
}
InitializeSubcontext();

SetupMountNamespaces() 把 / 设为 shared,把 /apex 和 /linkerconfig 设为 private;需要可更新 APEX 时创建 bootstrap/default 两个 namespace,并保存 namespace fd。这个阶段不是文章开头的基础 tmpfs 挂载,而是决定随后启动的 service 看见哪套 APEX 和 linker config。

8.2 解析脚本 ​

LoadBootScripts() 创建 Parser,并按固定顺序读取启动配置:

源码文件: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);
    }
}

Parser 的结果分别进入 ActionManager 和 ServiceList。目录解析失败有的会进入 late_import_paths,不等于立即 fatal;具体 action/service 是否存在,要到实际消费时才知道。

8.3 Subcontext ​

InitializeSubcontext() 在脚本解析前设置 vendor init 的隔离执行环境。由此 parser 可以把特定 builtin 路由给 subcontext,而不是让所有 vendor rc 命令都在 PID 1 的主上下文执行。本文只确定初始化时机,不展开 IPC 和 vendor policy 域。

9. 动作入队 ​

9.1 前置动作 ​

脚本加载完成后,Second stage 将系统动作按顺序入队:

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

cpp
am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups");
am.QueueEventTrigger("early-init");
am.QueueBuiltinAction(ConnectEarlyStageSnapuserdAction,
                      "ConnectEarlyStageSnapuserd");
am.QueueBuiltinAction(wait_for_coldboot_done_action,
                      "wait_for_coldboot_done");
am.QueueBuiltinAction(SetMmapRndBitsAction, "SetMmapRndBits");

wait_for_coldboot_done_action 等待 sys.init.cold_boot_done=true,使依赖 /dev 的后续动作不会跑在 ueventd coldboot 之前。ConnectEarlyStageSnapuserdAction 则把 first-stage snapuserd PID 和 socket 状态交给二阶段 service 表。

9.2 主触发器 ​

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

cpp
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");

early-init 已在前一个代码块入队;随后是 init、charger/late-init,再把当前属性状态转换成 property trigger。入队顺序和执行顺序通常一致,但真正执行要等 ActionManager::ExecuteOneCommand(),且每次只消费一个 command。

9.3 ActionManager ​

SecondStageMain() 在这里的职责到“把启动事件交给 ActionManager”为止。队列如何匹配多个 action、为什么一次只执行一条命令、property waiter 和 SVC_EXEC 如何抑制后续动作,统一由下一篇 Init主循环 展开。这样可以把“基础设施何时就绪”和“就绪后怎样调度”分成两条独立源码主线。

10. 循环交接 ​

10.1 时间点 ​

RecordStageBoottimes() 在脚本入队前后都需要的基础设施已就绪后发布阶段属性:

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

cpp
RecordStageBoottimes(start_time);

if (const char* avb_version = getenv("INIT_AVB_VERSION");
    avb_version != nullptr) {
    SetProperty("ro.boot.avb_version", avb_version);
}
unsetenv("INIT_AVB_VERSION");

随后设置 USB 默认值、处理 GSI/DSU 状态、保存 kallsyms fd,再加载脚本。这里体现的是“启动观测属性先于脚本动作”的生效时机。

10.2 调度入口 ​

完成阶段计时、脚本加载和事件入队后,SecondStageMain() 才进入永久循环。本文在这个交接点结束:此时 ActionManager、ServiceList、Epoll、property service 和 control-message 队列都已经拥有稳定的所有者,后续循环只消费这些状态,不再重新执行二阶段初始化。

下一篇 Init主循环 从 while (true) 开始,专门解释 shutdown 优先级、单命令推进、服务超时与重启时间、epoll timeout 和 control message 的调度顺序。

11. 失败路径 ​

失败点源码行为是否进入主循环
property area/info 初始化LOG(FATAL)否
debug/second-stage 资源卸载记录 error通常是
/apex//linkerconfig 挂载LOG(FATAL)否
file context handle由 restorecon 结果决定可能继续
epoll openPLOG(FATAL)否
SIGCHLD/signalfd 注册PLOG/FATAL否
property socketpair/threadPLOG/FATAL否
rc 文件解析部分路径延迟 import通常是
SetupMountNamespacesPLOG(FATAL)否
单个 action/service由 Action/Service 记录错误是

这里要区分“初始化基础设施失败”和“启动动作失败”。前者没有可靠的事件消费者,必须终止;后者已经有 ActionManager/ServiceList,可以记录错误、重试或继续执行其他动作。

12. 状态观测 ​

system/core/init/init_test.cpp 对 ActionManager 做局部证明:

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

cpp
TEST(init, EventTriggerOrder) {
    std::string init_script = R"init(
on boot
execute_first

on boot && property:ro.hardware=*
execute_second

on boot
execute_third
)init";
    // 每个 builtin 通过 EXPECT_EQ 断言执行序号。
}

SimpleEventTrigger 证明 event 能触发 builtin,EventTriggerOrderMultipleFiles 证明跨文件顺序;这些测试不启动真实 SecondStageMain,因此不能证明 /system/etc/init 在设备上可读,也不能证明 property socket、SELinux 或 epoll 已注册。

源码核对:

bash
rg -n "int SecondStageMain|PropertyInit|StartPropertyService|LoadBootScripts" \
  system/core/init/init.cpp system/core/init/property_service.cpp
rg -n "QueueEventTrigger|QueueBuiltinAction|ExecuteOneCommand" \
  system/core/init/action_manager.cpp system/core/init/init_test.cpp
rg -n "InstallSignalFdHandler|CreateAndRegisterSignalFd" \
  system/core/init/init.cpp

设备验证:

bash
adb shell 'getprop ro.property_service.version'
adb shell 'getprop ro.boottime.init ro.boottime.init.first_stage ro.boottime.init.selinux'
adb shell 'ls -ld /dev/__properties__ /apex /linkerconfig'
adb shell 'dmesg | grep -E "init second stage started|processing action|property_service|SetupMountNamespaces"'

断言闭环是:property version 证明服务初始化过,目录存在证明基础挂载完成,processing action 证明 ActionManager 已消费队列。它们不能单独证明所有服务都启动或 boot_completed 已发布。

13. 设备边界 ​

本文基于 Android 17 真实源码证明了 SecondStageMain 的启动顺序、debug/ramdisk 资源生命周期、PropertyInit 输入优先级、三类额外 tmpfs、SELinux label cache、signal fd、property service 内外通道、mount namespace、rc 加载、action 入队和进入主循环前的调度条件。

本文没有展开 epoll 内部实现、SIGCHLD 的完整回收状态机、property set wire 协议、Parser 语法、Service fork/exec、APEX 激活或 boot_completed 的 framework 路径。下一篇从主循环的 ExecuteOneCommand、timeout 和 epoll.Wait 深入,避免把“准备设施”和“消费事件”混成一篇。