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 和逐行立即执行语义。文件中的 onservice 也不是两个待顺序调用的函数。

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

init 在启动时把 rc 文本解析成 ActionService 等内存对象;运行时由 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_r4system/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 进入 ServiceListon 段经 ActionParser 进入 ActionManagerimport 段则继续解析目标文件或目录。

此时,大多数 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.mdService::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 启动时把 serviceonimport 段解析成对象;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?

源码与延伸阅读

相关推荐
新鲜势力呀1 小时前
深入理解 Elliot CUDA 编程核心:PHP 实现 CUDA 并行计算思想与实践
android·开发语言·php
恋猫de小郭2 小时前
Flutter 3.47 首坑,analysis_options 问题连环回归
android·前端·flutter
ue星空3 小时前
第一个安卓程序
android
淡淡的香烟4 小时前
Androidiot蓝牙配网简单封装
android·物联网
gxgldyh4 小时前
Android Framework源码解析(九):App进程诞生全流程——从AMS请求到Zygote fork源码深度拆解
android·framework·zygote·android 启动流程·android app创建流程
牛哇网络工作室11 小时前
UnityHDRP写实数字人全流程基础5—语音输入和语音识别
android·unity·c#·游戏引擎·aigc·语音识别·xcode
一笑的小酒馆11 小时前
Androidiot蓝牙配网简单封装
android
愚公搬代码14 小时前
【愚公系列】《Android应用案例开发大全》016-LBS类应用掌上杭州(辅助工具类的开发)
android·前端
g105655913915 小时前
LAMP博客平台Wordpress实战
android·mysql·nginx·php