第4章:init进程解析(上):init.rc脚本语法与解析流程
大家好,欢迎来到我们的Android系统启动流程课程。今天我们来聊聊init进程的核心——init.rc脚本。说实话,这个文件我看了不下百遍,每次系统出问题,第一反应就是去翻它。嗯,它就像是Android系统的“开机自启动清单”,但远比清单复杂得多。
4.1 init.rc 是什么?
init.rc 是 Android 系统启动时,init 进程解析的第一个脚本文件。它定义了系统服务如何启动、文件权限如何设置、属性如何初始化等等。说白了,它就是一套“启动指令集”。
我记得刚接触 Android 时,以为 init.rc 就是个简单的 shell 脚本。后来才发现,它有自己的语法规则,完全独立于 bash。你想想看,如果直接用 shell 脚本,那权限控制、服务依赖、触发条件这些机制实现起来得多麻烦?
核心要点:init.rc 不是 shell 脚本,而是 Android 特有的配置语言。它由一系列“语句”组成,每条语句对应一个动作或服务定义。
4.2 语法基础:语句与注释
init.rc 的语法非常简洁。每条语句占一行,用空格分隔关键字和参数。如果一行写不下,可以用反斜杠 \ 续行。
# 这是一条注释
on early-init
write /proc/1/oom_score_adj -1000
# 服务定义
service ueventd /sbin/ueventd
class core
critical
seclabel u:r:ueventd:s0
注释用 # 开头,这个和 shell 一样。我个人习惯在关键配置前加一段注释,说明这个服务是干什么的。曾经有一次,我在项目里看到一个没有注释的 service 定义,排查了半天才发现是早期遗留的测试服务。
4.3 动作(Action)与触发器(Trigger)
动作是 init.rc 的核心机制。它的格式是:
on <trigger>
<command1>
<command2>
...
当触发器条件满足时,后面的命令序列就会执行。常见的触发器有:
| 触发器名称 | 触发时机 | 我遇到的坑 |
|---|---|---|
| early-init | init 进程启动后最早阶段 | 此时很多设备还没准备好,别做复杂操作 |
| init | early-init 完成后 | 适合挂载文件系统、创建目录 |
| late-init | init 阶段之后 | 可以启动核心服务了 |
| boot | 属性服务初始化完成后 | 大部分服务都在这里启动 |
| property:<name>=<value> | 当指定属性被设置为特定值时 | 我曾经用它做动态权限切换,效果不错 |
小技巧:如果你想让某个操作在特定属性设置后才执行,用 property:xxx=yyy 触发器。我在做设备定制时,经常用这个机制实现“按需启动”。
4.4 服务(Service)定义
服务是 init.rc 里最常见的元素。每个服务对应一个后台进程。基本格式:
service <name> <path> [<arguments>...]
[option1]
[option2]
...
举个例子:
service servicemanager /system/bin/servicemanager
class core
user system
group system
critical
onrestart restart healthd
onrestart restart zygote
onrestart restart audioserver
onrestart restart media
onrestart restart surfaceflinger
onrestart restart inputflinger
onrestart restart drm
onrestart restart cameraserver
onrestart restart logd
看到没?servicemanager 挂了,会连带重启一堆服务。这就是 Android 的服务依赖链。我曾经在生产环境遇到过 servicemanager 异常退出,结果整个系统服务全部重启,用户端直接黑屏了几秒钟。嗯,从那以后我特别关注 critical 标记的服务。
常用的服务选项:
- class <name>:服务分类,如 core、main、late_start。同一类的服务可以一起启动/停止。
- user/group:指定运行的用户和组。安全考虑,不要用 root 跑普通服务。
- critical:标记为关键服务。如果它退出超过4次,系统会重启到 recovery 模式。
- onrestart:当服务重启时,执行指定命令。常用于级联重启。
- disabled:服务不会自动启动,需要显式调用
start命令。 - oneshot:服务只启动一次,退出后不会重启。
警告:不要轻易给服务加 critical 标记。我曾经在一个项目里,把某个测试服务标记为 critical,结果它频繁崩溃导致系统反复进入 recovery。排查了两天才发现是它的问题。
4.5 命令(Command)详解
动作块里可以执行各种命令。常用的有:
| 命令 | 作用 | 示例 |
|---|---|---|
| start/stop | 启动/停止服务 | start servicemanager |
| setprop | 设置系统属性 | setprop ro.debuggable 1 |
| write | 写入文件 | write /proc/sys/kernel/randomize_va_space 2 |
| symlink | 创建符号链接 | symlink /system/etc /etc |
| mkdir | 创建目录 | mkdir /data 0771 system system |
| chmod/chown | 修改权限/所有者 | chmod 0755 /system/bin/servicemanager |
| exec | 执行一个程序并等待完成 | exec /system/bin/toolbox -- ls /data |
这里我要特别提一下 exec 命令。它会阻塞 init 进程,直到执行完毕。如果你在 on boot 里放了一个耗时的 exec,那整个启动流程都会被拖慢。我见过有人用 exec 执行一个网络请求,结果开机时间增加了十几秒。千万别这么干!
4.6 解析流程:从文本到执行
init 进程解析 init.rc 的流程,我把它总结为三步:
- 词法分析:把文本拆分成 token(关键字、参数、操作符)。
- 语法分析:根据语法规则,构建出动作列表和服务列表。
- 执行阶段:按照触发器的触发顺序,依次执行动作块里的命令。
源码里对应的函数是 load_script() 和 parse_line()。我建议你去看一下 system/core/init/init_parser.cpp 这个文件。解析器其实不复杂,核心就是一个状态机。
// 简化后的解析逻辑
void parse_line(const std::string& line) {
auto tokens = tokenize(line);
if (tokens.empty()) return;
if (tokens[0] == "on") {
// 开始一个新的 action
current_action = new Action(tokens[1]);
} else if (tokens[0] == "service") {
// 开始一个新的 service
current_service = new Service(tokens[1], tokens[2]);
} else {
// 添加到当前的 action 或 service
if (current_action) {
current_action->AddCommand(tokens);
} else if (current_service) {
current_service->AddOption(tokens);
}
}
}
这段代码虽然简化了,但核心思想就是这样。解析器逐行读取,遇到 on 或 service 就创建新对象,后续的行就追加到当前对象里。
关键点:init.rc 的解析是顺序的。后面的动作如果触发了相同的触发器,会追加到已有动作的命令列表里。所以,你可以在多个 .rc 文件里定义同一个 on boot,它们都会被执行。
4.7 多文件引入与导入机制
Android 10 之后,init.rc 支持 import 语句。你可以把不同功能的配置拆分到多个文件里:
import /init.${ro.hardware}.rc
import /vendor/etc/init/hw/init.${ro.hardware}.rc
import /system/etc/init/
这样做的好处很明显:厂商定制只需要修改自己的 .rc 文件,不用动系统核心配置。我在做 ODM 项目时,就把所有硬件相关的服务都放在 init.${ro.hardware}.rc 里,系统升级时直接替换就行,省心不少。
不过要注意,import 的顺序会影响解析结果。如果两个文件定义了相同的服务,后解析的会覆盖前面的。我曾经因为 import 顺序搞反,导致某个服务一直启动失败,查了半天才发现是配置被覆盖了。
4.8 实战:如何调试 init.rc
最后,分享几个调试技巧:
- 查看解析日志:
logcat -b events | grep init,可以看到 init 解析了哪些文件。 - 检查服务状态:
getprop | grep init.svc,查看所有服务的运行状态。 - 手动触发动作:
setprop ctl.start myservice,可以手动启动一个服务。 - 语法检查:用
init --parse参数,可以只解析不执行,检查语法错误。
我的经验:每次修改 init.rc 后,先用 init --parse 检查一遍。我曾经因为少写了一个空格,导致整个服务定义失效。这种低级错误,工具一查就能发现。
好了,这一章我们详细讲了 init.rc 的语法和解析流程。下一章我们会深入源码,看看 init 进程到底是怎么一行一行解析这些配置的。到时候我会带大家走一遍 load_script() 的完整执行路径,敬请期待。
公众号:蓝海资料掘金营,微信deep3321