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用法:

  1. 启动依赖控制:服务A启动后设置一个属性,服务B的on property检测到该属性后启动
  2. 热插拔处理:比如USB插入、HDMI连接等场景
  3. 调试开关:通过adb设置某个属性,动态开启/关闭调试日志
  4. 系统状态同步:比如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的执行逻辑,方便你理解整个流程:

init.rc 执行逻辑流程图 解析 init.rc 文件 构建 Action/Service 链表 按 trigger 顺序执行 Action 遇到 start 命令 → fork 子进程 持续监听 property 变化 → 触发 on property 阶段一 解析 阶段二 构建 阶段三 执行 阶段四 服务 阶段五 监听

4.5 实战中的几个建议

最后,结合我自己的经验,给你几个实用建议:

  • 能用on property就别硬等:用sleep来等某个服务就绪是非常不靠谱的做法,用属性触发才是正道
  • 注意SELinux上下文:init.rc里创建的文件、socket,默认SELinux上下文可能不对,记得用seclabel选项指定
  • 调试技巧:用adb shell dmesg | grep init可以看init的执行日志,排查启动问题很有用
  • 别滥用critical:这个选项就像核武器,用多了系统反而更脆弱

好了,init.rc的核心内容就这些。你想想看,其实它就是一个事件驱动的配置系统,理解了trigger和property这两个核心概念,剩下的就是语法细节了。多读几遍系统自带的init.rc文件,比看任何教程都管用。


公众号:蓝海资料掘金营,微信deep3321