23、Init进程与rc脚本:init.rc解析、Service启动、属性系统与SELinux上下文
Android系统启动,说白了就是一场精心编排的接力赛。Bootloader把接力棒交给内核,内核再交给Init进程。而Init进程,就是用户空间的第一个进程——PID为1的那个。它要是挂了,整个系统就崩了。我在项目中遇到过好几次因为Init进程卡死导致手机变砖的情况,嗯,那滋味可不好受。
Init进程的使命
Init进程到底干了什么?我总结了三件大事:
- 解析并执行init.rc脚本——这是它的核心工作
- 管理Service的启动与重启——Service挂了它得管
- 维护属性系统——全局键值对,谁都能读,但不是谁都能写
你想想看,一个进程要管这么多事,它的设计必然得足够精巧。Init进程的代码在system/core/init/目录下,核心逻辑其实不复杂,但细节特别多。
init.rc脚本解析
init.rc是一种基于动作(Action)和触发器(Trigger)的脚本语言。说白了,就是告诉Init进程:当某个条件满足时,执行哪些命令。
它的基本语法结构是这样的:
on <trigger>
<command> <args>
service <name> <path> [<args>]
<option> <args>
举个例子,系统启动时挂载关键分区:
on early-init
mount tmpfs tmpfs /dev
mkdir /dev/shm 0777
on init
export PATH /sbin:/system/sbin:/system/bin
symlink /system/etc /etc
我个人习惯把init.rc的触发器分为三类:
| 触发器类型 | 触发时机 | 典型用途 |
|---|---|---|
| early-init | Init启动后立即触发 | 挂载tmpfs、创建基础目录 |
| init | early-init完成后 | 设置环境变量、创建符号链接 |
| boot | 属性系统就绪后 | 启动核心服务 |
这里有个坑——触发器的执行顺序是严格定义的。我曾经在调试一个启动卡死的问题时,发现某个vendor的rc脚本在early-init阶段就去访问/data分区,但那时候/data还没挂载,直接导致Init进程阻塞。嗯,这就是典型的触发器使用不当。
Service启动机制
Service是init.rc里最重要的概念。一个Service定义了一个需要被Init进程管理的子进程:
service servicemanager /system/bin/servicemanager
class core
user system
group system
critical
onrestart restart zygote
这段代码的意思是:启动servicemanager进程,如果它挂了,就重启它,同时还要重启zygote。为什么?因为servicemanager是binder服务的大管家,它挂了所有binder服务都得重来。
Service的启动流程我画了一张图,方便你理解:
Service有几个关键选项,我列一下:
- class:服务分组,可以一次性启动/停止一组服务
- critical:关键服务,挂了会导致系统重启
- oneshot:只启动一次,退出后不重启
- onrestart:当本服务重启时,执行指定动作
我记得有一次调试一个系统服务频繁重启的问题,发现是因为critical标志没加,导致服务挂了之后Init进程没有及时重启它,整个系统处于半瘫痪状态。加了critical之后,服务一挂系统直接重启,反而更稳定了。
属性系统
属性系统是Android里一个轻量级的全局键值对存储。每个属性都是一个字符串键值对,比如ro.build.version.sdk=34。
属性系统的核心机制:
- 只读属性(ro.开头):一旦设置就不能修改
- 持久化属性(persist.开头):会写入
/data/property,重启后保留 - 控制属性(ctl.开头):用于控制Service的启动和停止
属性系统的操作接口:
// 读取属性
#include <cutils/properties.h>
char value[PROP_VALUE_MAX];
property_get("ro.build.version.sdk", value, "unknown");
// 设置属性
property_set("persist.sys.timezone", "Asia/Shanghai");
// 监听属性变化
property_wait("sys.boot_completed", "1", 5000);
这里有个有意思的设计——属性变化会触发init.rc中的动作。比如:
on property:sys.boot_completed=1
start bootanim
当sys.boot_completed被设置为1时,Init进程会自动启动开机动画。这种事件驱动的方式,让系统启动流程变得非常灵活。
SELinux上下文
SELinux是Android安全模型的基石。每个进程、每个文件都有一个安全上下文(Security Context),格式是:
user:role:type:level
比如:u:r:init:s0 表示Init进程的安全上下文。
在init.rc中,SELinux上下文通过seclabel选项指定:
service ueventd /system/bin/ueventd
class core
seclabel u:r:ueventd:s0
如果没有指定seclabel,Init进程会尝试从可执行文件的xattrs中读取安全上下文。如果还找不到,就使用父进程(也就是Init进程自身)的上下文。
SELinux的策略文件在/sepolicy或/system/etc/selinux/目录下。Init进程在启动时会加载这些策略,然后整个系统就在SELinux的管控下运行了。
我遇到过最典型的SELinux问题就是avc: denied错误。比如:
avc: denied { read } for pid=1234 comm="my_service" name="data" dev="mmcblk0p25" scontext=u:r:my_service:s0 tcontext=u:object_r:userdata_file:s0 tclass=dir
这表示my_service进程没有权限读取/data目录。解决方案是在SELinux策略中添加对应的allow规则。
setenforce 0临时关闭SELinux,但绝对不要在产品版本中这样做。我曾经见过一个团队因为图省事直接关闭SELinux,结果被安全审计打了个大大的叉。
Init进程的完整启动流程
我把Init进程从启动到完成核心工作的流程总结一下:
- 内核启动Init进程——传递内核参数和文件描述符
- 挂载基础文件系统——tmpfs、devpts、proc、sysfs
- 解析init.rc——构建动作和服务的内部数据结构
- 执行early-init动作——创建设备节点、设置权限
- 加载SELinux策略——设置安全上下文
- 执行init动作——设置环境变量、创建符号链接
- 启动属性服务——属性系统开始工作
- 执行boot动作——启动核心服务(servicemanager、surfaceflinger等)
- 进入事件循环——监听属性变化、子进程信号、设备热插拔
这个流程看起来简单,但每一步都可能出问题。我建议你在调试启动问题时,可以打开Init进程的日志:
adb logcat -b events | grep -i init
adb logcat -b system | grep -i init
或者直接看内核日志:
adb shell dmesg | grep -i init
Init进程的日志会告诉你每个动作的执行情况,哪个Service启动失败了,哪个属性设置出错了。嗯,这些信息对于定位启动问题非常有帮助。
最后说一句,Init进程虽然看起来只是一个启动管理器,但它的设计思想——事件驱动、声明式配置、安全管控——贯穿了整个Android系统。理解了Init进程,你就理解了Android系统启动的底层逻辑。
公众号:蓝海资料掘金营,微信deep3321