Android 系统启动机制(二):init.rc 不是普通脚本,service、action 和 property 怎样驱动启动?

看到下面这段配置时,很多人会自然地把它理解成 shell 脚本:

sql 复制代码
on zygote-start
    start zygote
​
service zygote /system/bin/app_process64 ... --zygote --start-system-server
    class main
    socket zygote stream 660 root system

但 init.rc 没有 shell 的变量、管道、子 shell 和逐行立即执行语义。文件中的 on 与 service 也不是两个待顺序调用的函数。

先把配置到进程的模型压成一句话:

init 在启动时把 rc 文本解析成 Action、Service 等内存对象;运行时由 event、property 和显式命令选择对象。Action 中的 command 依次执行,start 最终让对应 Service 走 fork / exec;进程退出后,仍由同一个 Service 对象决定清理、停住还是重启。

本文只追下面这条闭环:

perl 复制代码
rc 文本
  → 内存对象
  → trigger 匹配
  → Action 排队
  → Command 执行
  → Service fork / exec
  → SIGCHLD / Reap / restart

完整 property service 协议、APEX rc 版本选择、subcontext、SELinux domain 转换和全部 option 查询表不在本文展开。

源码基线: Android 16 QPR2 android-16.0.0_r4,system/core commit e9d1fa3705d7fbd0ac1c942bc4c276161bfbfed1。

一、先建立对象地图:五个词和一个同名陷阱

1. Service:描述一个由 init 管理的进程单元

xml 复制代码
service <name> <pathname> [arguments...]
    <option>
    <option>

它至少记录:

perl 复制代码
服务名;
可执行文件与参数;
UID / GID、capability、SELinux、namespace 等进程属性;
socket、file 等要在 fork 前准备的资源;
退出后是否重启、何时重启、失败是否升级处理。

AOSP 对它的直接定义是:"init 启动,并可在退出时重启的程序",见 Android 16 init/README.md。

2. Action:满足条件后要执行的一组 Command

xml 复制代码
on <trigger> [&& <trigger>]*
    <command>
    <command>

Action 不是新进程。它是 init 进程中的内存对象,保存触发条件和命令列表。

3. Trigger:决定 Action 何时入队的条件

常见两类:

csharp 复制代码
event trigger:early-init、init、late-init、boot、zygote-start 等事件
property trigger:property:sys.boot_completed=1 等属性条件

一个 Action 可以组合一个 event 条件和多个 property 条件。&& 表示这些条件必须共同满足,不是启动多个线程并行执行。

4. Command:Action 中的一次具体操作

例如:

arduino 复制代码
start zygote
class_start main
setprop name value
mkdir ...
write ...
trigger boot

Command 由 init 的 builtin function map 映射到 C++ 实现。它不是默认交给 /bin/sh 执行的字符串。

5. Property:状态数据,也是事件来源

Property 可以被读取、展开到参数,也可以让 property:name=value Action 入队。它既不是普通环境变量,也不是所有代码都能任意写入的全局 HashMap;属性写入受 property service、SELinux 和属性上下文约束。

同名陷阱:init.rc 中的 service 到底不是什么?

Android 同时使用了太多名为 Service 的概念,必须先消歧:

名字 本质 谁管理 是否必然是进程
init service 进程启动与监督配置单元 init 描述一个目标进程
Android 组件 Service 应用组件 ActivityManager 等 通常运行在应用已有进程,也可配置独立进程
Binder Service 已发布的 Binder 对象 ServiceManager 保存名字到引用 否,一个进程可发布多个对象
SystemService system_server 内的 Framework 生命周期对象 SystemServiceManager(部分服务) 否,通常多个对象共处 system_server

所以:

bash 复制代码
service zygote /system/bin/app_process64 ...

不能翻译成:

创建一个 Android 组件 Service 或注册一个 Binder Service。

准确说法是:

建立一个名为 zygote 的 init 进程配置单元;它被触发后,init 将创建执行 app_process64 的子进程,并监督该子进程的退出状态。

坐标一旦分清,正文才进入真正的执行问题:这些对象何时建立,又怎样被事件选中?

二、rc 文本先变成内存对象,尚未创建进程

Android 16 CreateParser() 给 parser 注册三类 section:

less 复制代码
parser.AddSectionParser("service", ServiceParser(...));
parser.AddSectionParser("on", ActionParser(...));
parser.AddSectionParser("import", ImportParser(...));

随后 LoadBootScripts() 解析主配置以及 /system、/system_ext、/vendor、/odm、/product 对应 init 目录。

解析结束后,service 段经 ServiceParser 进入 ServiceList,on 段经 ActionParser 进入 ActionManager,import 段则继续解析目标文件或目录。

此时,大多数 service 还没有进程,Action 也还没有执行。配置文本只是变成了可供后续事件选择的对象图。

三、Action 怎样从"已解析"变成"待执行"?

second-stage init 会主动排入一组启动事件:

csharp 复制代码
early-init
init
charger 或 late-init

具体顺序可在 Android 16 SecondStageMain() 核对。

init.rc 又可以在某个 Action 中执行 trigger xxx,产生下一层事件。例如默认启动序列在 late-init 后逐步触发文件系统和数据分区相关事件,最终走到 zygote-start。

Android 16 ActionManager::QueueEventTrigger() 只是把事件放入队列;主循环调用 ExecuteOneCommand() 时,才查找匹配 Action 并逐条执行 Command。

为什么每次只执行一条 Command?

init 不把一个长 Action 从头到尾不可中断地跑完,而是在命令之间留出机会处理子进程退出、属性变化、控制消息、关机请求和服务重启计时。

AOSP README 也明确说明,Action 和 Command 按队列顺序执行,init 会在命令之间处理设备、属性和进程重启等活动。

这不是说所有 Command 都异步。某些 builtin 可能阻塞,exec 也有等待子进程结束的同步语义;这里只描述主调度模型。

四、property trigger 为什么不是"每轮轮询所有属性"?

假设配置是:

ini 复制代码
on property:sys.example.ready=1
    start example_service

稳定模型是:

scss 复制代码
属性发生变化
    ↓
PropertyChanged(name, value)
    ↓
ActionManager.QueuePropertyChange(name, value)
    ↓
匹配 property trigger
    ↓
Action 入选并执行

启动过程中还有一次"按当前属性值检查全部 property Action"的阶段,避免配置解析前已经存在的属性状态被漏掉。对应 Android 16 queue_property_triggers_action。

所以 property trigger 是事件驱动加一次初始状态对齐,不是 init 永久循环扫描所有键值。

还要注意:

ini 复制代码
on boot && property:ro.config.low_ram=true

表示发生 boot event 时,属性条件也必须满足。之后只改变该属性,并不会重新制造一次已经过去的 boot event。

五、start zygote 怎样跨过 fork / exec 边界?

Android 16 主 Zygote 配置位于 init.zygote64.rc:

perl 复制代码
service zygote /system/bin/app_process64 -Xzygote /system/bin \
        --zygote --start-system-server --socket-name=zygote
    class main
    priority -20
    user root
    group root readproc reserved_disk
    socket zygote stream 660 root system
    socket usap_pool_primary stream 660 root system

默认 init.rc 在合适阶段触发 zygote-start,对应 Action 显式执行:

sql 复制代码
on zygote-start
    wait_for_prop odsign.verification.done 1
    start statsd
    start zygote
    start zygote_secondary

源码位置:init.rc。

start zygote 最终找到 ServiceList 中的 zygote 对象并进入 Service::Start()。Android 16 的最小创建边界是:

scss 复制代码
pid = fork();
​
if (pid == 0) {
    RunService(...);
    _exit(127);
}
​
// RunService 内完成进程属性设置,最后:
execv(path, argv);

对应源码见 Service::Start() 和 ExpandArgsAndExecv()。

这次边界必须分成两段:

perl 复制代码
fork:出现 init 父进程和子进程
exec:子进程用 app_process64 替换自己的程序映像

不能把它压成"init 调用了 app_process 函数",因为两者已经在不同 Linux 进程中执行。

fork / exec 已经解释了进程怎样出生,但 service 段还携带三类会改变启动行为的配置:预建资源、生命周期分组和退出策略。它们不是三条新的主线,而是同一个 Service 对象在创建前后生效的约束。

六、同一个 Service 对象还携带资源与生命周期约束

1. socket zygote ... 到底是谁创建的?

socket option 不是告诉 app_process "以后自己找机会创建 socket"。Service::Start() 在 fork 前遍历配置的 socket,先创建对应描述符,再把它们交给子进程。

因此,init 父进程先创建并配置 /dev/socket/zygote 对应 fd;fork 后 app_process/Zygote 子进程继承指定 fd,ZygoteServer 再从 init 传入的环境中接管 server socket。

这让 socket 的路径、权限、属主和 SELinux 标签由 init 启动配置统一控制。

"init 创建 socket"不等于"init 处理 Zygote 协议"。真正读取 fork 命令的是 Zygote 进程中的 ZygoteServer / ZygoteConnection。

2. class main 会在解析时直接启动 Zygote 吗?

class 是给 service 分组,class_start <name> 会尝试启动该组中所有已启用 service。Android 16 do_class_start() 明确跳过 disabled service。

但"属于 class main"和"只能由 class_start main 启动"不是一回事。Zygote 虽然声明 class main,Android 16 默认启动序列仍在 on zygote-start 中显式 start zygote,以便把它放到数据准备和 odsign 验证之后的明确里程碑。

class 更像生命周期分组,不是 Java 类,也不是进程优先级类别。

3. service option 怎样改变进程生命周期?

Option 本文需要保留的语义 不能误读为
disabled 跳过 class_start 的自动启动,仍可显式 start 配置无效或永远禁止启动
oneshot 退出后不自动重启,之后仍可显式再启动 整个系统生命周期最多运行一次
restart_period 为非 oneshot 服务定义重启调度周期,并受崩溃限速约束 退出后必定按字面秒数立即重启
critical 反复失败达到条件后可升级到 fatal 恢复 崩溃一次就重启设备

精确默认秒数、失败窗口、fatal target 与节流保护属于 Android 16 的版本化策略,不是"rc 怎样变成进程"这条主线的固定语义。需要查表时以 init/README.md 和 Service::Reap() 为准。

到这里,配置已经不只决定"怎样创建进程";它还决定进程退出后,init 怎样继续管理同一个启动单元。

七、进程退出以后,为什么同一份配置还能继续生效?

init 通过 SIGCHLD 路径得到退出信息,再由 Service::Reap() 做状态转换:

scss 复制代码
子进程退出
    ↓ SIGCHLD
ReapAnyOutstandingChildren()
    ↓ 根据 pid 找到 Service
Service::Reap(siginfo)
    ├─ 清理进程组和非持久 socket
    ├─ 执行 reap callback
    ├─ 更新 running / stopped / restarting
    ├─ oneshot → disabled
    ├─ disabled / reset → 停住
    └─ 普通非 oneshot → 执行 onrestart,等待重启时机

所以 init service 不是"启动命令的别名",而是跨越进程创建、运行和退出的状态对象。

八、现场证据能证明 rc 推进到了哪一步?

常用观察:

css 复制代码
adb shell getprop init.svc.zygote
adb shell getprop ro.boottime.zygote
adb shell ps -A -o PID,PPID,NAME,ARGS
adb shell "logcat -b all -d | grep -E 'init|starting service|restarting'"

具体可用列、日志权限和 tag 会随设备 build 类型变化,先用 adb shell ps --help 与当前日志验证,不要把某台 userdebug 设备的输出当成所有 user build 都可见。

证据 能够证明 不能证明
rc 文件存在 service zygote 配置声明存在 该配置已经被当前设备解析或触发
starting service 'zygote' Service::Start() 已进入创建路径 子进程已成功进入 Java
init.svc.zygote=running init 当前记录子进程为运行 Zygote 预加载已完成
zygote 反复 restarting 配置已触发,但子进程不能稳定存活 根因一定在 init;也可能是 app_process、ART、预加载或 system_server 崩溃
ps 中出现 zygote 对应进程当前存在 socket loop 与 system_server 一定正常

九、回到核心问题:一段 rc 怎样变成受监督的 Zygote 进程?

完整答案可以压成一条闭环:init 启动时把 service、on、import 段解析成对象;event 与 property change 让匹配 Action 入队;主循环逐条执行 Command;start / class_start 找到 Service,准备 socket 与进程属性后 fork;子进程 exec 目标文件;init 最后通过 SIGCHLD 回收退出状态,并按 option 决定后续生命周期。

现在已经知道 init 启动的是:

css 复制代码
/system/bin/app_process64 --zygote --start-system-server ...

但 ps 中为什么看到的是 zygote64?当前进程又怎样从 C++ main() 开始执行 Java ZygoteInit.main()?

这个疑问把阅读位置从 init 带到刚被 exec 的 Native 程序:init 启动的是 app_process,为什么最后看到的却是 Zygote?

源码与延伸阅读

相关推荐
千里马学框架6 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台6 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone6 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc6 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo6 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077006 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼6 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone6 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen6 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone6 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui