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启动流程 解析Service定义 检查SELinux上下文 fork + execve 监控子进程状态 关键点 • 解析class、user、group • 检查seclabel • 设置rlimit • 设置环境变量 • 处理onrestart • 处理critical标志 • 处理oneshot标志 • SIGCHLD信号处理

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进程会自动启动开机动画。这种事件驱动的方式,让系统启动流程变得非常灵活。

避坑指南:我曾经在项目中遇到一个性能问题——某个服务在循环中频繁调用property_set,导致属性系统内部发生大量IPC通信,最终拖慢了整个系统。建议:不要在高频路径中频繁设置属性,属性系统不是为高性能设计的。

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进程从启动到完成核心工作的流程总结一下:

  1. 内核启动Init进程——传递内核参数和文件描述符
  2. 挂载基础文件系统——tmpfs、devpts、proc、sysfs
  3. 解析init.rc——构建动作和服务的内部数据结构
  4. 执行early-init动作——创建设备节点、设置权限
  5. 加载SELinux策略——设置安全上下文
  6. 执行init动作——设置环境变量、创建符号链接
  7. 启动属性服务——属性系统开始工作
  8. 执行boot动作——启动核心服务(servicemanager、surfaceflinger等)
  9. 进入事件循环——监听属性变化、子进程信号、设备热插拔

这个流程看起来简单,但每一步都可能出问题。我建议你在调试启动问题时,可以打开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