看到下面这段配置时,很多人会自然地把它理解成 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/corecommite9d1fa3705d7fbd0ac1c942bc4c276161bfbfed1。
一、先建立对象地图:五个词和一个同名陷阱

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?