Android 系统启动机制(一):init 到底是什么?为什么它不是普通 Native Service?

很多资料把 Android init 解释成"系统启动入口"。这句话方向没错,却隐藏了两个问题:

css 复制代码
它只是一个 main(),还是一个会长期存在的进程?
它和 netd、vold、SurfaceFlinger 这些 Native Service 有什么本质区别?

先把 init 放回正确层级:

Android init 是 Kernel 启动的首个用户空间进程,也就是 PID 1。它先建立后续代码能够运行的基础环境,再解析 init 配置、执行启动动作、创建并监督关键子进程。它本身是 Native 进程,但不是由另一个 init service 启动的普通 Native Service。

"普通 Native Service"不是 AOSP 的统一类型名。本文用它泛指由 init 按 rc 配置拉起、负责某项具体系统能力的 Native daemon/service,例如 voldnetd 和 SurfaceFlinger。

本文只解决 init 的身份和职责边界;完整 rc 语法、属性服务内部协议、ueventd、SELinux 策略编译和 first-stage mount 细节都留给后续或独立专题。

源码基线: Android 16 QPR2 android-16.0.0_r4system/core commit e9d1fa3705d7fbd0ac1c942bc4c276161bfbfed1

一、先拆掉第一个误解:不是 init "抢到"了 PID 1

Linux Kernel 建立第一个用户空间任务时会选择 init 程序并执行。Android 的启动镜像与命令行让这个入口落到 /init;因此这个进程获得 PID 1。

因果关系是:

csharp 复制代码
Kernel 把 init 作为首个用户空间程序启动
        ↓
它成为 PID 1

而不是:

csharp 复制代码
某个普通进程先启动
        ↓
发现自己叫 init
        ↓
申请成为 PID 1

进程名不赋予 PID 1 身份。理论上 Kernel 可以执行另一个程序作为 PID 1;Android 的用户空间设计、配置和恢复机制则共同约定由 Android init 承担这个角色。

二、PID 1 特殊在哪里?

把 init 只当成"开机时执行一次的 main()"会漏掉它的长期责任。

1. 它是 Android 用户空间进程树的根

init 直接或间接创建后续大量进程:

swift 复制代码
init (PID 1)
├─ ueventd
├─ servicemanager
├─ surfaceflinger
├─ netd
├─ vold
└─ app_process64 → Zygote
                    ├─ system_server
                    └─ App processes

这里的树表示父子关系,不表示所有进程都由 init 逐个直接 fork。比如 system_server 和普通 App 都来自 Zygote;但 Zygote 本身由 init 的 service 机制启动。

2. 它要回收退出的子进程

子进程退出后会留下退出状态,父进程必须 wait 才能完成回收。PID 1 还会接管没有可用父进程的孤儿后代,因此 init 不能只负责"点火"然后退出。

Android 16 second-stage init 把 ReapAnyOutstandingChildren 设为 epoll 的首要回调,再安装 SIGCHLD 处理路径;这样能在处理新的控制请求前先更新已经退出的服务状态。对应源码可见 SecondStageMain()Service::Reap()

3. 它不能像普通服务一样随意退出

普通守护进程退出后,init 可以按配置重启它;PID 1 自己退出则意味着用户空间根进程消失,Linux 不会把它当成普通服务崩溃处理。

这也是"init 监督其他进程"与"谁监督 init"不对称的原因。Android 可以通过 watchdog、fatal 路径和重启机制处理 init 发现的灾难,但不能再用一层普通 init service 包住 PID 1。

Linux 对 PID 1 的部分默认信号处理也有特殊规则;Android init 仍会主动安装 SIGCHLD 与关机等处理。本文不展开 Linux 信号细节,只保留身份边界:

PID 1 不是"权限更高的普通 daemon",而是 Linux 用户空间生命周期的根角色。

三、Android init 不是只执行一次:它分成三个连续阶段

Android 16 的 init/main.cpp 根据参数进入三个阶段:

kotlin 复制代码
if (argv[1] == "selinux_setup") {
    return SetupSelinux(argv);
}
if (argv[1] == "second_stage") {
    return SecondStageMain(argc, argv);
}
return FirstStageMain(argc, argv);

这是整理后的最小骨架。完整关系是:

bash 复制代码
FirstStageMain
    ↓ exec /system/bin/init selinux_setup
SetupSelinux
    ↓ exec /system/bin/init second_stage
SecondStageMain
    ↓ 长期事件循环

关键边界:这里的 exec 不是创建新进程

fork 创建新进程;exec 用新程序替换当前进程的代码与地址空间映像。

因此:

ini 复制代码
第一阶段 init PID = 1
SELinux setup PID   = 1
第二阶段 init PID  = 1

PID 没变,执行的程序映像和参数变了。

Android 16 first-stage init 最后执行:

arduino 复制代码
execv("/system/bin/init", {"/system/bin/init", "selinux_setup", nullptr});

源码位置:first_stage_init.cpp。SELinux 阶段完成后又用 second_stage 参数执行同一入口,见 selinux.cpp

所以不能写成:

first-stage init 启动了另一个 second-stage init 进程。

更准确的是:

PID 1 通过连续 exec 切换程序映像,从 first stage 进入 SELinux setup,再进入长期运行的 second stage。

四、为什么要拆阶段?因为后面的代码一开始还不可用

开机最早期,完整的 /system/vendor、动态分区、设备节点和 SELinux 环境可能还没有准备好。此时不能直接假设普通 Android 用户空间已经存在。

第一阶段:只准备"加载完整系统所需的最小条件"

AOSP init/README.md 把早期序列拆成 first-stage init、SELinux setup 和 second-stage init。第一阶段的稳定职责包括:

bash 复制代码
挂载 /dev、/proc 等早期基础文件系统;
完成 first-stage mount;
让包含系统代码的分区可访问;
在需要时切换根文件系统;
为下一阶段保留必要资源。

具体 /init 是 ramdisk 中的静态程序、还是指向 /system/bin/init,取决于设备启动布局。不要把某一种设备打包形式写成所有 Android 设备的唯一实现。

SELinux setup:让后续用户空间在正确安全策略下运行

这一阶段加载策略、恢复 init 自身安全上下文,然后 exec 到 second stage。它不是"开机以后再慢慢补安全",而是在大量普通服务创建前建立安全边界。

第二阶段:进入 Android init 的长期工作模式

第二阶段才开始完整建立:

csharp 复制代码
property service;
ActionManager 与 ServiceList;
init.rc 及各分区配置解析;
early-init、init、late-init 等触发序列;
子进程回收与服务重启;
控制消息、属性变化和关机事件;
epoll 主事件循环。

Android 16 SecondStageMain() 加载启动配置后,依次排入内建 action 和 early-initinitlate-init 等事件;随后循环执行一个 command、计算下次唤醒时间并进入 epoll 等待,见 init.cpp

五、为什么 init 不是普通 Native Service?

init 和 SurfaceFlinger 都是 C/C++ 用户态进程,但"都用 Native 写"不代表系统角色相同。

维度 Android init 普通 Native Service
谁启动 Kernel 作为首个用户空间程序启动 通常由 init 根据 rc 启动
PID 固定承担 PID 1 角色 普通动态 PID
配置依赖 先存在,之后才能解析 rc 可以由 rc service 描述
主要责任 基础环境、触发器、进程创建与监督 提供某项具体系统能力
退出处理 不能由另一层普通 init 自动重启 可由 init 按规则重启
生命周期 从用户空间起点持续到关机 按服务配置启停

把 init 写进自己的 rc 会形成循环:

csharp 复制代码
要解析 rc,必须先有 init;
要按 rc 启动 init,又必须先解析 rc。

所以 init 是配置执行器和进程监督者,不是被自己管理的一个配置单元。

六、init 为什么既是启动器,又是监督者?

如果 init 只负责 fork 一次就不再管理,关键进程退出后,资源清理、状态更新、重启限速和失败升级都没有统一责任方。Android init 因此用同一个 Service 对象保存进程的创建配置与退出策略;Service::Reap() 回收子进程后更新 init.svc.<name>,再决定停住、排队重启或进入更强恢复路径。

这一节只说明 init 为什么必须长期监督。oneshotdisabled、重启调度与完整 SIGCHLD → Reap 状态链,以第二篇:init.rc 从配置到进程为唯一事实源。

七、init 的主循环不是无休止地扫配置文件

第二阶段完成解析后,不会周期性重读全部 rc。配置先变成 ActionService 等对象;运行时由事件、属性变化、控制消息和 SIGCHLD 驱动 Action 排队,主循环逐条执行 Command,没有立即工作时进入 epoll 等待。

因此 Android init 更像事件驱动的用户空间总调度器,而不是按顺序执行完就退出的 shell 脚本解释器。完整队列模型留给下一篇。

不过这个类比有边界:init 不是通用任务编排平台,也不负责 Framework 对象的生命周期。它管理的是用户空间基础动作和进程单元。

八、看到 init 进程,能证明什么?

在设备上可以先观察:

csharp 复制代码
adb shell ps -A -o USER,PID,PPID,NAME | grep -E '(^| )init$'
adb shell getprop init.svc.zygote
adb shell getprop ro.boottime.init.first_stage
adb shell getprop ro.boottime.init.selinux

Windows PowerShell 可以把 grep 换成 Select-String,但要注意:adb shell 内部命令和主机侧管道属于两个环境。

证据边界如下:

观察 能够证明 不能证明
PID 1 名为 init Android init 进程仍存在 所有 boot action 已完成
init.svc.zygote=running init 记录的 zygote service 当前处于 running Zygote 已成功 fork system_server
first-stage / selinux boottime 有值 init 已记录对应阶段耗时 后续系统服务已经 ready
init 日志出现 starting service 'zygote' zygote 启动命令已进入 Service::Start() exec app_process 和 Java Runtime 一定成功

如果只看到 init 存在,最多能证明用户空间根进程还活着。要证明 Zygote、system_server 和 Framework 能力,必须继续寻找下一阶段证据。

九、init 的职责边界停在哪里?

可以把答案压缩成六点:

markdown 复制代码
1. init 是 Native 可执行程序,也是运行该程序的 PID 1 进程名称;
2. 它由 Kernel 作为首个 Android 用户空间程序启动;
3. first stage、SELinux setup、second stage 通过 exec 连续发生,PID 仍为 1;
4. 前两段建立完整用户空间能够运行的基础条件;
5. second stage 解析配置并进入事件循环;
6. 它根据配置创建、回收和监督后续关键进程。

本文还没有回答:

init.rc 中几行文本,怎样变成可执行 Action 和会被监督的进程?

要跨过这个断点,就要把 rc 从"看起来像脚本"还原成内存对象、事件队列和进程生命周期:init.rc 不是普通脚本,service、action 和 property 怎样驱动启动?

源码与延伸阅读

相关推荐
必须会一定会1 小时前
GLM-5.3 迁移实战:强制思考、1M 上下文与官方编程基准
android·开发语言·kotlin
小孔龙1 小时前
Android MessageQueue:从单锁队列到 DeliQueue
android
math_hongfan2 小时前
事务下单与状态流转:ArkTS 的 JOIN 联表在鸿蒙订单里实战
android·学习·华为·harmonyos
嘟哩DuliDuli2 小时前
AI 账单变高的技术原因:重复上下文和用量归属
android·人工智能·安全·ai·软件工程
阿pin17 小时前
Android随笔-kotlin withContext
android·kotlin·withcontext
树码小子17 小时前
开启 IDEA 中的断言功能
android·java·intellij-idea
Kslient18 小时前
HeaderBehavior
android
Android-Flutter19 小时前
android 动画详解
android·kotlin
小孔龙1 天前
GraphicBuffer 跨进程共享
android