init.rc 是 Android init 进程的中央配置文件。它并不是一个可执行的脚本(如 bash),而是一套声明式的初始化语言,由 init 进程中的解析器在运行时加载并执行。本文将深入解析 init.rc 的完整语法规范,包括语句类型、触发器机制、Service 选项、Socket 管理,并结合实际源码(Android 11 / API 30)展示编写自定义 rc 服务的方法。
一、init.rc 语言设计理念
1.1 声明式 vs 过程式
传统 Linux init 系统(如 sysvinit)使用 shell 脚本进行初始化——本质上是过程式编程。Android 设计团队选择了一套自定义的声明式配置语言,原因包括:
- 确定性:声明式语言执行顺序明确,无循环依赖风险
- 解析效率:解析简单,不需要启动 shell 解释器
- 可扩展性:通过 import 机制支持模块化配置(每个 HAL 都可以带自己的 rc 文件)
- 安全性:语法受限,不会出现 shell 注入攻击
- 资源节省:不需要在每个 init 触发阶段启动 shell 环境
1.2 rc 文件的加载顺序
init 进程在第二阶段按以下顺序加载 rc 文件:
// system/core/init/init.cpp |
二、五大语句类型
init.rc 语言包含五种顶层语句类型。在解析器层面,每一行都以一个关键字开头,由 SectionParser 机制分发到对应的解析器。
2.1 语句类型一览
| 类型 | 关键字 | 说明 | SectionParser |
|---|---|---|---|
| Action | on |
定义由触发器激活的命令序列 | ActionParser |
| Command | (内置命令) | Action 块内的具体操作 | (Action 内部) |
| Service | service |
定义后台运行的守护进程 | ServiceParser |
| Import | import |
引入其他 rc 文件 | ImportParser |
| On | on |
与 Action 相同,关键字 on ← Action |
ActionParser |
2.2 Import 语句
Import 语句是最简单的语句类型:
import /init.environ.rc |
Import 解析过程中支持三个特殊机制:
- 变量展开:
${ro.zygote}会被替换为对应的系统属性值 - 路径通配:
import /vendor/etc/init/hw/*.rc加载所有匹配文件 - 递归加载:被 import 的文件中的 import 语句也会被处理
ImportParser 实现:
// system/core/init/import_parser.cpp |
三、Action 语句详解
3.1 Action 的基本语法
on <trigger> [&& <trigger>]* |
一个 Action 由一个或多个 trigger 条件以及一系列 command 组成。当 trigger 条件被触发时,该 Action 中的所有 command 被添加到执行队列的尾部(而不是立即执行)。
3.2 触发器 (Trigger) 类型
3.2.1 事件触发器 (Event Trigger)
最常用的触发器类型,由特定的系统事件名称标识。init 进程在启动过程中会按顺序触发这些事件:
# 系统启动事件的典型序列 |
事件触发的实现:
// system/core/init/action_manager.cpp |
3.2.2 属性触发器 (Property Trigger)
当系统属性变为特定值时触发:
on property:sys.boot_completed=1 |
属性触发器的内部实现:
// system/core/init/action.cpp |
3.2.3 组合触发器 (Android 12+)
Android 12 引入了 && 组合触发器语法:
on property:sys.boot_completed=1 && property:sys.user.0.ce_available=true |
3.2.4 所有内置事件触发器列表
// system/core/init/README.md 文档中描述的触发器 |
四、Command 语句详解
4.1 Command 的解析与分发
Command 定义在 Action 块内部,每行一个命令。init 进程通过 BuiltinFunctionMap 将命令名字符串映射到实际的函数指针:
// system/core/init/builtin_commands.cpp |
4.2 典型实现示例
// system/core/init/builtin_commands.cpp |
4.3 exec 命令与 exec_start 的区别
- exec:在 init 进程中直接 fork+exec,阻塞 init 主循环直到命令执行完毕。只应在早期初始化阶段使用。
- exec_start:本质上是启动一个 Service 对象并等待其完成。不阻塞 init 主循环,使用 epoll 异步等待。
# exec 的使用(阻塞 init) |
五、Service 语句详解
5.1 Service 语法
service <name> <pathname> [ <argument> ]* |
name:服务名称,在 rc 文件中必须唯一pathname:可执行文件的绝对路径argument:传递给可执行文件的命令行参数
5.2 Service 的所有 Option 详解
5.2.1 进程身份类
user <username> # 以指定用户身份运行(默认 root) |
5.2.2 运行行为类
oneshot # 不自动重启(默认会自动重启) |
Android 9+ 将 OOM 调整从 rc 文件 option 迁移到 init 框架统一管理:
oom_score_adjust <value> # OOM 调整值 (-1000 到 1000) |
5.2.3 重启策略类
onrestart <command> # 服务重启时执行的命令 |
5.2.4 资源限制类
rlimit <resource> <soft> <hard> # 设置资源限制 |
5.2.5 环境变量类
setenv <name> <value> # 设置环境变量 |
5.2.6 命名空间隔离类
namespace <pid|mnt> [ <path> ] # 进入指定命名空间 |
5.2.7 Lifecycle 管理
interface <interface> <instance> <type> |
5.3 Socket Option——服务间通信的关键
socket <name> <type> <perm> [ <user> [ <group> [ <context> ] ] ] |
该 option 指示 init 在启动 service 之前创建一个 Unix Domain Socket,并将 socket fd 传递给子进程(通过环境变量 ANDROID_SOCKET_<name>)。
type 参数:
stream:流式 socket (SOCK_STREAM)dgram:数据报 socket (SOCK_DGRAM)seqpacket:有序数据包 socket (SOCK_SEQPACKET)
示例——Zygote 的 socket 定义:
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server |
这创建了两个 socket:/dev/socket/zygote 和 /dev/socket/usap_pool_primary。Zygote 进程可以通过以下方式获取 fd:
// frameworks/base/core/jni/fd_utils.cpp |
init 端创建 socket 的实现:
// system/core/init/service.cpp |
六、init.rc 的解析流程深入
6.1 Parser 类的设计
// system/core/init/parser.cpp |
6.2 Tokenize(分词)规则
// system/core/init/tokenizer.cpp |
分词规则的要点:
- 空格和 tab 是分隔符
- 双引号内的内容作为单个 token(如
"hello world") #开头到行尾是注释\在行末表示续行符
七、完整实战:编写自定义 rc 服务
7.1 需求描述
假设我们需要编写一个自定义的 native daemon 服务 my_monitord,它需要:
- 以 system 用户身份运行
- 在 boot 阶段自动启动
- 创建自己的 socket 供其他进程通信
- crash 后自动重启,但不触发 recovery
- 启动前需要确保
/data/mymonitor/目录存在
7.2 rc 文件
# /system/etc/init/mymonitor.rc |
7.3 Native Daemon 接收 Socket
// my_monitord.cpp |
7.4 手动控制服务
启动后可以通过以下命令管理:
# 查看服务状态 |
八、rc 文件的位置规范(Android 10+ Treble)
8.1 rc 文件的标准路径
| 路径 | 职责 |
|---|---|
/system/etc/init/hw/ |
系统核心 rc(init.rc, init.usb.rc 等) |
/system/etc/init/ |
Android 系统服务 rc |
/vendor/etc/init/ |
SoC vendor HAL 服务 rc |
/vendor/etc/init/hw/ |
SoC vendor 硬件相关 rc |
/odm/etc/init/ |
ODM/OEM 定制 rc |
/product/etc/init/ |
Product 层 rc |
/system_ext/etc/init/ |
System Ext 层 rc |
8.2 最佳实践
- 每个 HAL 应配一个 rc 文件:不应将所有 HAL 服务塞进一个文件
- 使用
class_start而非硬编码启动顺序:让 init 框架管理启动并行度 - 通过属性触发器表达依赖:
on property:xxx.yyy=1 start my_service - 使用
oneshot+disabled控制一次性任务:不要让后台服务伪装成 oneshot - socket 而非 /dev/ 节点:鼓励使用 socket 进行进程间通信
九、rc 语法扩展(Android 12+)
9.1 Bootconfig 替代内核命令行
Android 12 引入 bootconfig 机制替代传统的 /proc/cmdline:
# 不再依赖内核命令行,改用 bootconfig 传递参数 |
9.2 条件触发器组合
# Android 12+ 支持多条件触发 |
9.3 Per-Service 命名空间
service my_service /system/bin/my_binary |
十、核心面试题
Q1:oneshot 和 disabled 的配合使用场景是什么?如果只设 oneshot 会怎样?
答:oneshot 表示服务退出后不自动重启,disabled 表示服务不会随 class_start 一起启动。两者的典型配合场景是需要手动触发的一次性任务(如固件更新、一次性文件系统转换)。如果只设 oneshot 而不设 disabled,该服务会在 class_start <its class> 时启动一次,如果失败了不会重试,如果成功了退出后也不会重启——这通常用于有明确结束条件的工作负载而不是常驻守护进程。
Q2:init.rc 中的 exec 命令与 exec_start 命令选择标准是什么?
答:exec 会阻塞 init 进程的主事件循环,这意味着在 exec 返回之前,init 不能处理 SIGCHLD 信号、不能响应 property 变化、不能接受属性服务 socket 消息。因此 exec 仅应在 early-init 等极早期阶段使用,此时还没有关键服务在运行。exec_start 是异步的,它启动一个 Service 并等待其完成,但不阻塞 init 主循环。所有在 post-fs-data 及之后的阶段都应该使用 exec_start 来避免阻塞 Property Service、SIGCHLD 处理等关键 loop。
Q3:Service socket option 创建的 socket 在 init 重启时会怎样?init 如何向子进程传递 socket fd?
答:Socket 由 init 通过 socket() + bind() + listen() 创建,fd 在 Service 启动前通过环境变量 ANDROID_SOCKET_<name> 传递给 fork 出的子进程。如果服务 crash 并重启,init 不会重建 socket——socket 在 init 进程的生命周期内持久存在。因此,如果服务 fork 出的子进程关闭了 socket fd,重启后新进程会获得新的 fd(指向同一个底层 socket)。如果 init 自身 crash(极其罕见),所有 socket 会丢失,但 init crash 导致内核 panic 重启整个设备。
Q4:同一个 device 可能同时有 /system/etc/init/ 和 /vendor/etc/init/ 中的同名 rc 文件,如果它们定义了同名的 Service,会发生什么?
答:ServiceParser 在解析到同名 Service 时会报错并忽略后续定义。具体而言,ServiceList::AddService() 会检查是否有重名,如果有重名,第二个 Service 定义将被丢弃并输出错误日志。这确保了服务名称在整个系统中的唯一性。开发者需要在构建时通过命名规范避免冲突(如使用 vendor.<name> 命名 vendor 服务)。
Q5:critical 标记的服务 crash 4 次/4 分钟触发 recovery 的机制是否在用户 build 和 eng build 上有区别?
答:机制本身在所有构建类型上相同——都是 service.cpp 中 Reap() 方法内的 SVC_CRITICAL 检查。但在 user build(ro.debuggable=0)上,LOG(FATAL) 触发的设备重启比 userdebug/eng build 更快、更激进。在 eng build 上,开发者有更多时间观察日志。实际产品化时,vendor 通常避免标记过多服务为 critical,以防止因单个非致命服务的不稳定导致设备反复重启。
AOSP 核心路径参考:
system/core/init/README.md— init.rc 官方语言规范文档system/core/init/parser.cpp— Parser 主逻辑system/core/init/action_parser.cpp— Action 解析器system/core/init/service_parser.cpp— Service 解析器system/core/init/import_parser.cpp— Import 解析器system/core/init/keyword_map.h— 关键字映射模板system/core/init/builtin_commands.cpp— 所有内置命令实现system/core/init/builtin_commands.h— BuiltinFunctionMap 定义system/core/init/service.cpp— Service 类完整实现system/core/init/action_manager.cpp— ActionManager 事件触发和命令队列system/core/init/tokenizer.cpp— rc 文件行分词器system/core/rootdir/init.rc— 系统主 rc 文件system/core/rootdir/init.zygote64.rc— Zygote Service 定义范例





