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_DEBUGGABLE | FirstStageInit | debug property 选择 | 立即 unsetenv |
/second_stage_resources | FirstStageInit | PropertyInit | 属性读取后卸载 |
INIT_AVB_VERSION | FirstStageMount | ro.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
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
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
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
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
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
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
// 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
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
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
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
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
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
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
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
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
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
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
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
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 open | PLOG(FATAL) | 否 |
| SIGCHLD/signalfd 注册 | PLOG/FATAL | 否 |
| property socketpair/thread | PLOG/FATAL | 否 |
| rc 文件解析 | 部分路径延迟 import | 通常是 |
| SetupMountNamespaces | PLOG(FATAL) | 否 |
| 单个 action/service | 由 Action/Service 记录错误 | 是 |
这里要区分“初始化基础设施失败”和“启动动作失败”。前者没有可靠的事件消费者,必须终止;后者已经有 ActionManager/ServiceList,可以记录错误、重试或继续执行其他动作。
12. 状态观测
system/core/init/init_test.cpp 对 ActionManager 做局部证明:
源码文件:system/core/init/init_test.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 已注册。
源码核对:
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设备验证:
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 深入,避免把“准备设施”和“消费事件”混成一篇。
