4、init进程深度解析:init.rc语法与执行逻辑,Service与Action的启动顺序控制,on property触发器机制
好,咱们今天来啃一块硬骨头——init进程。说实话,Android系统启动过程中,init是第一个用户空间进程,它的地位相当于整个系统的「总指挥」。我当年刚接触这块时,看着init.rc里密密麻麻的配置,说实话有点懵。但后来发现,只要掌握了它的语法和执行逻辑,这东西其实挺有规律的。
4.1 init.rc文件结构:一套声明式脚本语言
init.rc不是普通的shell脚本,它是一套声明式的配置语言。什么意思呢?就是你告诉init进程「在什么条件下,做什么事」,而不是「先做A,再做B」这种过程式写法。
一个典型的init.rc文件由以下几类语句组成:
- Import:引入其他rc文件,类似C语言的#include
- Action:定义一组命令,在特定触发条件下执行
- Service:定义一个守护进程,init负责启动、监控、重启它
- Option:修饰Service或Action的行为参数
- Command:具体的执行指令,如start、exec、chmod等
我习惯把init.rc看作一张「状态机配置表」——每个Action对应一个状态迁移,每个Service对应一个常驻任务。这样理解起来就顺多了。
核心要点:init.rc的解析是一次性的,在init进程启动时全部读入内存,构建成链表结构。后续所有触发都是在这个链表上查找匹配的Action来执行。
4.2 Action与Service的启动顺序控制
这是面试高频题,也是实际开发中容易踩坑的地方。咱们先看Action的执行逻辑。
4.2.1 Action的触发与执行
每个Action都有一个trigger条件,格式是:
on <trigger>
<command1>
<command2>
...
常见的trigger有:
| Trigger名称 | 触发时机 | 我遇到的坑 |
|---|---|---|
| early-init | init启动后最早阶段 | 此时文件系统还没挂载全,别做文件操作 |
| init | early-init之后,挂载基本文件系统 | 可以创建目录、设置权限了 |
| late-init | 核心服务启动前 | 我习惯在这里放一些依赖检查 |
| boot | 系统启动完成,属性服务就绪 | 大部分服务都在这个阶段启动 |
| property:<name>=<value> | 当指定属性被设置为特定值时 | 这个后面单独讲,非常强大 |
Action的执行顺序是严格按照trigger的触发顺序来的。同一个trigger下的多个Action,按它们在rc文件中出现的顺序执行。这一点很重要——我曾经在项目中把两个同trigger的Action写反了顺序,结果一个服务启动时依赖的目录还没创建,直接挂了。
4.2.2 Service的启动与生命周期
Service的定义格式:
service <name> <path> [<arguments>]*
<option>
<option>
...
Service本身不会自动启动,它需要被显式start。通常是在某个Action里调用start <service_name>来启动它。
这里有个关键点:Service的启动是异步的。init进程fork出子进程后,不会等待它执行完,而是继续处理下一个命令。所以如果你在Action里写了:
on boot
start servicemanager
start zygote
这两个start命令会几乎同时发出,但servicemanager和zygote的启动顺序并不保证。那怎么办?
我的做法:利用class选项和class_start命令来控制启动组。比如把核心服务放在core类,把普通服务放在main类,分阶段启动。
Service的常用选项:
- class <name>:将服务分组,方便批量启动/停止
- oneshot:服务退出后不重启
- disabled:服务不会随class自动启动,需要显式start
- critical:服务崩溃4次会导致系统重启到recovery
- socket <type> <name> <perm>:自动创建socket并传给服务
嗯,这里要注意critical选项。我见过一个项目,把某个不太稳定的服务设成了critical,结果测试时频繁触发重启到recovery,排查了好久才发现是这个问题。所以critical要慎用,只给那些系统绝对依赖的服务用。
4.3 on property触发器机制
这个机制是init.rc里最灵活、也最容易出彩的地方。说白了就是:当某个系统属性的值发生变化时,自动触发一段脚本执行。
语法很简单:
on property:sys.boot_completed=1
# 系统启动完成后的收尾工作
start some_service
或者更复杂的条件:
on property:persist.sys.usb.config=mtp
# 当USB模式切换到MTP时
setprop sys.usb.state mtp
这个机制的原理是:init进程内部有一个属性变化监听线程,当属性服务(property_service)通知有属性变化时,init会遍历所有注册的on property触发器,匹配的就执行。
性能警告:不要在on property触发器里做耗时操作。因为属性变化是同步通知的,如果某个触发器执行时间过长,会阻塞后续所有属性变化的处理。我曾经遇到过一个案例,某个第三方应用频繁修改一个属性,导致init进程CPU飙升到30%+,系统明显卡顿。
4.3.1 实际应用场景
我总结了几种常见的on property用法:
- 启动依赖控制:服务A启动后设置一个属性,服务B的on property检测到该属性后启动
- 热插拔处理:比如USB插入、HDMI连接等场景
- 调试开关:通过adb设置某个属性,动态开启/关闭调试日志
- 系统状态同步:比如boot_completed后执行一些收尾工作
举个例子,我在一个车载项目中用过这样的设计:
# 车载系统启动流程
on property:vendor.car.audio_ready=1
start car_hmi_service
on property:vendor.car.can_ready=1
start car_navigation_service
这样audio服务和CAN总线服务各自独立启动,谁先准备好谁先触发,互不阻塞。比硬编码启动顺序优雅多了。
4.4 核心执行逻辑流程图
下面我用一张图来总结init.rc的执行逻辑,方便你理解整个流程:
4.5 实战中的几个建议
最后,结合我自己的经验,给你几个实用建议:
- 能用on property就别硬等:用
sleep来等某个服务就绪是非常不靠谱的做法,用属性触发才是正道 - 注意SELinux上下文:init.rc里创建的文件、socket,默认SELinux上下文可能不对,记得用
seclabel选项指定 - 调试技巧:用
adb shell dmesg | grep init可以看init的执行日志,排查启动问题很有用 - 别滥用critical:这个选项就像核武器,用多了系统反而更脆弱
好了,init.rc的核心内容就这些。你想想看,其实它就是一个事件驱动的配置系统,理解了trigger和property这两个核心概念,剩下的就是语法细节了。多读几遍系统自带的init.rc文件,比看任何教程都管用。