1. Init 进程到底解决了什么问题
做 Android 系统开发这些年,如果要挑一个最容易被忽略却又绕不开的起点,我会选 Init 进程。它不像 Binder、AMS 那样经常被挂在嘴边,但只要是涉及“开机”“服务拉起”“属性读写”“异常重启”的问题,追到最后都会回到它身上。
Init 是内核启动完成后创建的第一个用户空间进程,进程号为 1。它的特殊性在于:内核把所有硬件资源、驱动、调度机制都准备好了,但“用户空间的服务该怎么拉起,按什么顺序拉,拉挂了要不要重启”,这些事内核不管。这就是 Init 存在的意义——它是用户空间的总调度员,是那根把所有 Android 组件串起来的线。
这篇内容会把 Init 的核心机制拆开讲清楚:它的启动流程、rc 文件的解析逻辑、属性服务的工作原理,以及它和 Zygote 之间那层关键的配合关系。无论你是刚入门的系统开发、做 BSP 的工程师,还是想深入理解 Android Framework 启动链路的应用层开发者,这篇都能帮你把“从内核态到用户态”这段路走明白。
1.1 用户空间的第一进程
先明确一个概念:Linux 内核完成初始化后,会主动去执行一个用户空间程序,这个程序的 PID 固定为 1,名为 Init。在标准 Linux 发行版里,它通常是 systemd 或者 SysVinit;在 Android 里,它就是 AOSP 中的system/core/init。
PID 为 1 意味着几件很特别的事:
- 它没有父进程,内核直接派生的。
- 它是所有孤儿进程的收养者。如果某个子进程的父进程死了,内核会把该进程 reparent 到 PID 1,由 Init 负责回收它的僵尸进程(
waitpid)。 - 如果它挂了,内核会触发 panic,系统直接重启。
所以 Init 的代码风格一直是“宁可保守也不冒险”,任何关键路径上的错误都会导致系统起不来或不停重启。这也是为什么做系统开发时,遇到启动问题第一件事就是去查 Init 相关的日志。
Android 的 Init 在 AOSP 里的源码路径是system/core/init/,核心文件包括:
init.cpp:主入口,含 first stage 与 second stage 的启动逻辑。property_service.cpp:属性服务的实现。service.cpp:service 对象的解析与管理。action.cpp/parser.cpp:rc 文件解析与 Action 执行框架。signal_handler.cpp:子进程信号处理与重启策略。
从 Android 10 开始,Init 被拆成了两个阶段:first_stage_init和second_stage_init。前者跑在极其受限的环境里,只做最基础的事情;后者才进入真正完整的 Android 用户空间初始化流程。这个拆分后面会详细讲,这里先有个整体印象。
1.2 Init 的三大核心职责
把 Init 的全部工作浓缩成一句话:用可配置的方式,按顺序拉起所有 Android 用户空间服务,并持续监控它们的运行状态。展开来看,主要是三块。
第一块是解析并执行 rc 脚本。/init.rc是 Init 的核心配置文件,里面用一套类 Shell 的语法定义了各类 Action 和 Service。Init 在启动时会读入这些文件,按照触发条件(Trigger)的顺序执行动作(Action),并根据配置启动守护服务(Service)。Zygote、surfaceflinger、servicemanager 这些和你日常开发息息相关的重要进程,严格来说都不是 Init 直接代码写的,而是写在 rc 文件里被 Init 拉起来的。
第二块是属性服务。Android 里有一种全局的键值对机制叫“系统属性”(system property),进程之间通过property_get/property_set来读写。Init 在启动早期就会把这套机制搭好,把属性区域建在共享内存里,通过 socket 接收来自各个进程的写请求,并负责权限检查、值类型校验、持久化等。平时你敲的getprop、setprop,底层就是在和 Init 的 property service 打交道。
第三块是子进程的监控与重启。Init 是所有系统级 service 的直接父进程。当某个 service 进程异常退出时,内核会向 Init 发送 SIGCHLD 信号,Init 的信号处理模块会找出是哪个 service 死了,然后根据它的配置决定是重新拉起、忽略,还是触发系统重启。如果你见过一个服务挂了之后系统疯狂重启的现象,那大概率就是 rc 里把该服务标记成了critical。
这三块职责也不是孤立存在的——rc 解析解决“何时启动什么”,属性服务解决“运行中的状态如何共享”,信号处理解决“进程死后怎么办”。它们互相配合,构成了 Android 用户空间运行的底座。
2. Init 进程的启动流程全拆解
很多讲 Init 的文章上来直接讲 rc 文件语法,但我觉得那样会漏掉最精彩的部分:Init 自己是怎么活过来的。从内核态切到用户态,这一步是被很多工程师当成黑盒直接跳过的。
2.1 从内核启动到 Init 的交接
内核态到 Init 之间的交接路径大致是这样的:
start_kernel() -> rest_init() -> kernel_init() -> run_init_process()kernel_init()在完成一系列内核层面的初始化后,会去根目录查找可执行文件,Android 里就是/init。它被直接替换成 Init 进程的代码,也就是说 Init 不是被 fork 出来的普通进程,而是以“替换内核 init 线程上下文”的方式诞生的,这保证了它的 PID 必然是 1。
Android 的/init在早期版本里是一个可执行的 ELF 文件,现在的主流版本是通过ramdisk(或者 vendor_boot 里的 ramdisk)提供。设备厂商在制作 boot 分区时,会把 init 可执行文件、init.rc、以及各种.rc文件塞进 ramdisk 里,内核启动后先挂载 ramdisk,再执行其中的 init。
从 Android 10 开始,init被拆成了两个阶段,设计上是这么考虑的:早期需要先把/system、/vendor这些分区 mount 上来,这通常需要 device-mapper、文件系统驱动等能力,但那时候文件系统还没准备好,一些依赖还没就位,所以第一阶段只干最基础的活,等基础环境就绪,再完整切换到第二阶段。
举个例子——如果第一阶段就想去读/system/etc/xxx.conf,那注定失败,因为/system分区的 mount 是在第一阶段末尾或第二阶段才完成的。Android 把这套过程放进first_stage_init的实现里,通过MountHandler、Devices这类组件去处理挂载、设备节点创建等事务。
2.2 Init 主循环:事件驱动的业务模型
Init 启动完成后并不会“一次性把所有事情做完然后退出”,它需要持续运行,响应各种异步事件。它的主循环是一个基于 epoll 的事件驱动模型,和你在应用层写的Looper思路类似,但挂在它监听列表上的,是一组“文件描述符”。
这些被监听的 fd 主要来自几个方向:
- 属性服务的 socket 描述符。其他进程通过本地 socket 连接进来,请求设置属性,Init 需要及时读取并处理。
- 子进程终止的信号。Linux 里子进程退出会产生 SIGCHLD,Init 用 signalfd 把它转成可读事件加入 epoll,方便在事件循环里统一处理。
- 设备节点的 uevent。早期阶段需要处理冷插拔(coldplug)和热插拔(hotplug)事件,创建、删除设备节点,这些消息也会通过 netlink socket 接入主循环。
- 各种显式注册的 epoll 源,比如
wait_for_prop命令中等待属性变化时的条件触发。
主循环的大致逻辑是:
while (true) { epoll_wait(...) // 处理每个 fd 上的事件 // 1. 属性写入请求 -> property_set // 2. 子进程退出 -> 触发重启/记录日志 // 3. uevent -> 创建/删除设备节点 // 4. 定时任务 -> 执行周期动作 // 每次事件处理完后,重新评估当前需要执行的 Action }每次事件处理完后,Init 会重新检查当前的系统状态(比如某些 property 是否满足条件),如果条件满足,就会被调度执行。比如你setprop sys.usb.config adb之后,通过属性触发就能让adbd重新启动,就是靠这套事件驱动的 Action 评估机制实现的。
理解这个模型对排查问题很重要。很多人以为 Init 是线性执行的,其实它是一个持续的、事件触发的循环。这也解释了为什么 rc 里某些on property:xxx=1的触发块,在系统运行过程中也能动态生效——它们不是只在开机阶段执行一次的。
2.3 关键阶段的执行细节
我挑几个最关键的阶段展开说,这样后面看日志时不会一头雾水。
First stage 的职责大概是这样:它首先建立基本环境,包括:
- 挂载
/dev、/proc、/sys这些基础文件系统。 - 初始化 SELinux,加载第一阶段的安全策略。
- 挂载
/system、/vendor等逻辑分区。现在的 Android 设备基本都在用动态分区(super),所以这里会走FirstStageMount,通过读取/system等分区信息结合 device-mapper 建立映射。 - 创建设备节点。早期 Android 通过在
/dev下预先放置节点或借助 uevent 实现,现在主要依赖devtmpfs和 uevent 动态创建。 - 设置
/dev/__properties__目录,为第二阶段启动做准备。
Second stage 才是我们平时研究的重点。它会:
- 重新设置部分 SELinux 上下文,保证自己以正确的域(domain)运行。
- 解析并导入所有 rc 文件,包括
/init.rc和system、vendor、odm各分区中的附加 rc 文件。 - 初始化 property service,加载默认属性、以及各种
*.prop文件。 - 进入主事件循环。
在实际板子上,你可以在串口日志里看到类似这样的阶段标记:
[ 1.234567] init: init first stage started! [ 1.345678] init: [libfs_mgr] Created logical partition system [ 2.123456] init: init second stage started! [ 2.234567] init: Parsing file /init.rc当你在日志里看到 “init second stage started!”,说明 Init 已经进入完整的用户空间初始化流程。后面的Parsing file /init.rc、starting service 'zygote'之类的日志,都属于这个阶段的产物。
3. 核心机制一:rc 文件解析与 Action 执行框架
Init 的配置体系是 rc 文件。这个文件里的语法和 Shell 脚本有相似之处,但它不是 Shell,而是一套专门为 Init 设计的声明式语言。理解它能帮你搞清楚服务是怎么被组织、调度、重启的。
3.1 rc 文件的语法结构
rc 文件的基础单元是“行”,以关键词开头,后面跟参数,用空格分隔。注释以#开头。常见的顶层关键词有四类:on、service、import、option。
一个 Action 的写法:
on early-init # 设置一些系统属性 setprop sys.usb.config none on property:ro.crypto.state=encrypted # 挂载 /data 分区前的处理on后面跟的是触发条件(trigger),可以是固定阶段名(如early-init、init、late_init),也可以是属性条件(如property:sys.boot_completed=1)。当条件满足时,下面缩进的所有命令会被依次执行。
Service 定义长这样:
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server class main priority -20 user root group root readproc socket zygote stream 660 root system onrestart restart zygoteservice关键字后面跟服务名、可执行文件路径和参数。下面的缩进行就是 Options——它们不是给 service 传的参数,而是告诉 Init 如何管理这个服务。
Import 语句则是用来引入其他 rc 文件的:
import /init.environ.rc import /system/etc/init/*.rc这里要特别注意:不同厂商、不同分区(system/vendor/odm)下的 rc 文件都会被扫描和解析,但导入顺序有讲究。/init.rc里通过 import 引入其他文件,vendor 分区里通常也会有自己的一套 rc 文件,用于拉起硬件相关的服务。
3.2 Trigger 与 Action 的执行时机
不懂 Trigger 的优先级,你很难理解 Init 为什么会在某个时间点做某件事。我整理一下 Android 常见的执行阶段顺序:
| Trigger | 大致时机 | 典型用途 |
|---|---|---|
| early-init | 第二阶段开始后最早阶段 | 创建基础目录、设置最低级的属性 |
| init | 基础环境就绪 | 挂载 /dev 下的文件系统、创建关键目录 |
| early-fs | 文件系统准备阶段 | 挂载不可信任的 /system 分区前的准备工作 |
| fs | /system 挂载后 | 检查、挂载各类文件系统 |
| post-fs | 文件系统挂载完成后 | 设置磁盘相关属性和服务的准备工作 |
| post-fs-data | /data 分区挂载并准备完成后 | 加解密逻辑、启动 insmod 等 |
| zygote-start | 准备启动 Java 世界 | 触发 zygote 服务的启动 |
| late_init | 其他主要服务启动完成 | 各种收尾工作 |
on块不一定只能挂一个固定阶段。你可以写on fs && property:ro.crypto.state=encrypted,表示既要到达fs阶段,又要满足某个属性才会执行。这里&&是“且”的意思,两个条件都满足才会触发。
Property 型 Trigger 是理解“运行时动态触发”的关键。举个实际例子:某个供应商的守护进程希望等 USB 连接状态变化后执行命令,就可以写成:
on property:sys.usb.config=adb start adbd当某个进程通过setprop sys.usb.config adb设置属性后,Init 的事件循环会捕捉到属性变化,重新评估所有依赖该属性的on块,满足条件就执行命令。所以别再把on property:当成“只在开机时执行”的东西。
3.3 service 的启动、重启与禁用
Service 是 Init 里最核心的抽象。定义一个 service 后,Init 会 fork 一个子进程去跑指定的可执行文件,并持续监控它。如果子进程退出,Init 会收到 SIGCHLD,然后根据该 service 的配置决定怎么做。
常见的 Options 及其含义:
| Option | 作用 | 注意事项 |
|---|---|---|
| class | 服务分组 | 默认是 default,main/core 是常用分组 |
| disabled | 不随 class 自动启动 | 需要显式start或ctl.start |
| oneshot | 执行完就退出,不算异常 | 配合 disabled 常用于一次性初始化任务 |
| user | 以指定用户运行 | 不要轻易给 root,安全风险高 |
| group | 以指定组运行 | 可附加多个组 |
| critical | 退出会导致系统重启 | 慎用,否则一个服务挂了就无限重启 |
| onrestart | 服务重启时额外执行命令 | 常用于 zygote 挂掉时重启 surfaceflinger |
| socket | 自动创建 socket 文件 | Android 服务间通信常用方式 |
类(class)机制很实用。class main的服务会在系统启动的 main 阶段被统一拉起,class core则会更早一点。通过class_start和class_stop命令,可以一次性启动或停止整组服务。比如你在调试时想重启所有 main 类服务,可以这样:
stop start这在 Init 的 rc 配置里对应的是class_start main和class_stop main。用adb shell stop && adb shell start都能达到类似效果,后者的本质就是通过属性控制 Init 执行 class 的启停。
禁用服务也是一个常见操作。如果某个服务你不希望它自动启动,可以直接在 rc 里给该 service 加上disabled,再通过on property:xxxx按需启动。不过要注意,修改 rc 文件后需要重新打包 boot/recovery 镜像或 vendor 镜像才能生效,实机调试时一般先通过 adb 尝试setprop ctl.start <service>验证,而不是反复刷机。
4. 核心机制二:属性服务与系统属性的读写
如果你看过 Binder 的源码,一定知道系统服务之间通信走的是 Binder;但如果你问一个系统属性是怎么全局共享的,答案反而是 Init 提供的属性服务。它是一套独立于 Binder 的机制,简单但极其重要。
4.1 属性服务的工作原理
Android 属性(property)本质是一个全局的键值对表,存储在共享内存里。任何进程都可以通过property_get()读取,但写入则必须经过 Init 的 property service 审核。
工作流程是这样的:
- 调用进程通过
__system_property_set()发起写请求。 - 这个请求会通过本地 socket 发送给 Init 的 property service。
- Init 校验请求方是否有权限写这个 key(通过 SELinux 判断)。
- 校验通过后,在共享内存的属性区域更新该 key 的值。
- 如果 key 以
persist.开头,还会把值写入/data/property/下的持久化文件,保证重启后不丢失。
共享内存的设计让读操作非常快——不需要跨进程通信,直接读共享内存的映射区域就行。这和一些你熟悉的“[进程间通过 mmap 共享数据]”思路完全一致,只是 Android 把它做成了统一框架。
补充一点:property_get()是 libcutils 提供的 C API,Java 层对应的是android.os.SystemProperties.get()。Framework 里很多配置都通过属性传递,比如ro.build.version.sdk、sys.boot_completed、dalvik.vm.heapsize等,你随时可以通过getprop查看。
4.2 属性权限与 SELinux 管控
大多数系统属性写入失败的案例,最后都指向同一个原因:SELinux 拒绝了写请求。属性权限不是简单“能写/不能写”的开关,而是通过 type 和 context 的组合来精确控制的。
系统里有一份属性上下文映射文件,路径通常是:
/system/etc/selinux/plat_property_contexts/vendor/etc/selinux/vendor_property_contexts
文件内容大致长这样:
ro.build.version.sdk u:object_r:build_prop:s0 sys.usb.config u:object_r:usb_prop:s0 persist.sys.timezone u:object_r:timezone_prop:s0当一个进程尝试写某个属性时,SELinux 会检查该进程的 domain 是否对这个属性 type 有set权限。比如shelldomain 能不能写sys.usb.config,取决于 sepolicy 里是否有类似这样的规则:
allow shell usb_prop:property_service set;如果没有权限,日志里会出现典型的avc: denied { set }记录。排查这类问题最简单的思路就是:先getenforce看 SELinux 模式,再在日志里搜avc: denied,确认是哪个 source 对哪个 target 的什么权限被拒绝,最后在对应策略文件里补充 allow 规则。调试阶段可以临时setenforce 0验证是否 SELinux 导致,但绝不能把这个作为长期方案。
还有一个常见坑:属性的命名约定。ro.前缀代表只读(read-only),一旦设置,之后就不能再修改,只能重启生效;persist.前缀代表持久化,重启后仍保留;net.前缀和网络相关;sys.开头的通常由系统内部设置。你自定义属性时,建议遵循这套前缀规范,不要发明奇怪前缀,否则可能触发 CTS 或 vendor 测试问题。
4.3 实际调试中的属性操作技巧
属性在开发和测试阶段是排查问题的一把利器。我整理几个实际工作中常用的场景。
场景一:查看当前系统各分区构建版本。
adb shell getprop | grep ro.build场景二:模拟某个系统状态触发 rc 逻辑。比如某些设备支持通过设置sys.usb.config切换 USB 功能:
adb shell setprop sys.usb.config adb场景三:判断系统是否进入可交互状态。
adb shell getprop sys.boot_completed值为 1 表示系统整体启动完成,这对判断“系统是卡在 boot 阶段还是 Framework 阶段”非常有用。
场景四:用persist.属性保存自己的测试标记。比如写一个自定义属性,用于判断某个模块是否需要输出 debug 日志:
adb shell setprop persist.vendor.debug.myapp 1应用层可以通过SystemProperties.getInt("persist.vendor.debug.myapp", 0)读取,重启后依然有效。这在联调和复现问题上特别方便——不用反复改代码、编译、刷机。
还有一个值得注意的细节:属性值有长度限制,PROP_VALUE_MAX是 92 字节。如果你试图写入超过 92 字节的字符串,会被拒绝或截断。这个问题在设置一些 base64 编码、长路径之类的值时会遇到,要记得先裁剪。
5. 核心机制三:Zygote 与 Init 的配合
Zygote 是 Android Java 世界的起点,但它的出生完全是由 Init 一手操办的。不了解 Init 和 Zygote 之间的关系,你看系统启动日志时会少一半信息量。
5.1 Zygote 的角色与启动基础
Zygote 本身并不神秘,它就是一个由app_process启动的 Java 进程。但它的特殊之处在于:所有普通 Android 应用进程,都是从 Zygote fork 出来的。这样做的好处是应用进程天然带有 Zygote 预加载好的类、资源和系统服务连接,不用每个应用都重新加载一遍,启动速度和内存占用都得到很大优化。
在 rc 文件里,Zygote 的定义大致是:
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server class main priority -20 user root group root readproc socket zygote stream 660 root system onrestart restart zygote这里有两个细节值得展开。--start-system-server参数告诉 Zygote 启动完成后要立即 fork 出system_server;socket zygote stream 660 root system则是在/dev/socket/下创建名为zygote的 socket 文件,权限是 660,属主 root、属组 system。应用进程要 fork,就是通过连接这个 socket 来发送请求的。
另外注意onrestart restart zygote。这条规则的意思不是“如果 zygote 重启了就重启它自己”,而是说:当 zygote 这个 service 发生重启(即进程被杀后重新拉起)时,Init 要额外执行restart zygote命令。
这可能看起来有点绕,实际含义是:Zygote 如果异常退出,Init 会依照 service 定义自动重新拉起它。但由于 system_server 是从 Zygote fork 出来的,Zygote 重启后 system_server 肯定也处于异常状态,所以需要顺带把和 system_server 强绑定的服务一起重启。一个比较经典的配置是这样的:
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server class main ... onrestart restart zygote而像 surfaceflinger 这种属于 main class 的核心服务,因 zygote 重启而被动重启的情况也多见。调试时如果在 logcat 里看到某个服务莫名其妙重启了,不妨回头看看 rc 里的onrestart链。
5.2 从 Init 视角看 Zygote 的启动时机
Zygote 并不是 Init 启动最早的进程。拿主流的 Android 11+ 设备举例,启动顺序大致是:
- Init second stage 开始,解析 rc。
- 执行
early-init、init、early-fs、fs、post-fs等阶段。 - 挂载好
/data等分区后,进入post-fs-data。 - 进入
zygote-start阶段,调用class_start main,这一下会把 main class 的 service 都拉起来,包括 zygote、surfaceflinger、servicemanager 等。
也就是说,Zygote 是 main class 服务中的“排头兵”,但它的启动并不凌驾于所有 Init 阶段之上。它有依赖条件——比如/data分区必须已经可用,某些/system下的库和 socket 目录也必须就绪。
Zygote 启动后,它会去加载 Java 层的核心类库、注册系统的 JNI 方法、预加载部分资源和主题,然后进入 socket 监听状态。这时候你如果去连那个/dev/socket/zygote的 socket,理论上就能请求 fork 新进程了。
system_server 是所有系统服务(AMS、PMS、WMS 等)的宿主。它是 Zygote 启动早期就 fork 出来的子进程。从进程树上看,system_server 的 PPID(父进程 ID)是 Zygote 的 PID,而 Zygote 的 PPID 是 Init 的 PID。在设备上可以通过ps -A -o PID,PPID,NAME | grep -E 'zygote|system_server'验证这一串父子关系。
5.3 从 Init 到应用进程的完整链路
这里顺便把整条链路串一下,帮你建立整体观:
- 内核启动,Init 成为用户空间第一个进程。
- Init 解析 rc,进入 zygote-start 阶段,通过
class_start main拉起 Zygote。 - Zygote 启动后 fork 出 system_server。
- system_server 里的 ActivityManagerService(AMS) 启动后,会向 Zygote 发起 socket 连接。
- 用户点击 App 图标,AMS 通过 socket 发消息给 Zygote,让它 fork 一个新进程,新进程就是我们的 App。
这就是那条经典的“从硬件到应用”的链路。你会发现,中间每一步都有一个清晰的“最终负责人”:硬件归内核管,服务的启动归 Init 管,Java 世界的初始化归 Zygote 管,App 进程的创建归 Zygote+AMS 管。
在实际排查问题时,这条链路的价值非常直接。如果开机后没有应用能启动,先看 Zygote 是不是正常起来了;如果 Zygote 起来了但 system_server 循环重启,看它挂在哪个服务上;如果 App 都正常但某个系统属性没生效,回过来查 Init 的 property 日志。
这种逐层排查的思维,比死记硬背 API 有用得多。每个组件都有它自己的上游和下游,搞清上下游关系,很多疑难问题的边界就自然收缩了。
6. 常见问题与排查技巧实录
最后这部分写点实操。Init 相关的问题在开发、测试、售后阶段都会遇到,我挑几个典型的,把排查思路和命令写出来。
6.1 开机卡死,怎么看 Init 阶段日志
开机卡死是系统开发里最让人头疼的问题之一。好在 Init 阶段的日志相对好拿,关键是找对地方。
如果你有串口,那最好办。Init 在关键节点会打印日志,比如:
[ 3.456789] init: starting service 'zygote'... [ 3.456890] init: Sending signal 0 to service 'zygote' (pid 1234) [ 3.456912] init: Service 'zygote' restarting...如果没有串口,可以通过last_kmsg或 pstore 拿内核日志。设备重启后进入 fastboot 或救援模式时,通过/sys/fs/pstore/或adb shell cat /proc/last_kmsg查看。注意 Android 12 之后last_kmsg已经普遍被 pstore/ramoops 替代,路径可能是/sys/fs/pstore/console-ramoops-0。
拿到日志后,第一件事是定位卡在哪个阶段。你可以这样做:
- 看是否有
init second stage started!。没有的话,问题在第一阶段,通常是 mount 分区、SELinux 策略或者 initramfs 问题。 - 看是否出现了
Parsing file /init.rc。有解析日志但后面卡住,重点查 rc 文件里某个 service 是否卡死了。 - 看是否出现
starting service 'zygote'。如果 zygote 一直没起来,多半是 Zygote 启动参数、SELinux 上下文,或它依赖的库缺失。 - 看
sys.boot_completed是不是一直为 0。即使没有串口,也可以用adb shell getprop来确认系统是否走到 Framework 启动阶段。
有个细节要留意:Init 自己的日志是写进/dev/kmsg的,所以它和内核日志混在一起,串口日志里看到的init: xxx就是它。logcat在早期阶段是起不来的,所以不要在 Init 阶段指望 logcat,靠串口和 kmsg 最可靠。
6.2 rc 文件语法错误的排查
rc 文件是文本配置,写错了 Init 一般不会直接崩,而是跳过或者报错。但“跳过”意味着服务没被拉起,这种问题隐蔽性很强。
常见的错误有:
- 漏了缩进。
on块下的命令必须有缩进,没有缩进会被当成一个新关键字。 - Service 参数写错路径。常见的像可执行文件路径不存在,或者参数顺序不对。
- Option 拼写错误。比如把
oneshot写成oneshoton,Init 会直接报 unknown option。 - 忘了 import。如果 rc 文件没有被正确 import,里面定义的服务根本不会被解析。
排查 rc 解析错误的方法是看 Init 的启动日志。Init 解析到出错的语句时,会打印类似信息:
init: /init.rc: 123: invalid command 'star' init: /system/etc/init/foo.rc: 45: parse error: unknown option 'wrong'这里会给出文件路径和行号,直接去看对应行就行。如果日志被冲掉了,可以在编译时把 init 的 log level 调高,或者把 rc 文件放在一个独立分区里反复修改、验证。
另外一个很容易被忽略的问题:rc 文件最后必须有一个换行符。如果文件末尾没有换行,最后一个 token 可能解析失败。这在手动修改 rc 文件时特别常见,建议养成“写完 rc 后习惯性 cat -A 看一眼”的习惯。
6.3 属性设置失败与 SELinux 问题定位
属性设置失败是系统联调里的高频问题。症状一般有两种:设置后马上读还是旧值,或者设置后其他进程读不到。
第一步永远是区分“是权限被拒还是值被截断”。最快的验证方法:
adb shell setprop test.foo bar adb shell getprop test.foo如果返回空或者bar没生效,先看当前getprop输出里是否有这个 key。如果完全没有,那大概率是权限或者前缀问题。如果值存在但和你设置的不一致,那可能是别的高优先级进程又改了一次,或者触发了属性回调的联动修改。
SELinux 拒绝的信息长这样:
avc: denied { set } for property=test.foo scontext=u:r:shell:s0 tcontext=u:object_r:default_prop:s0 tclass=property_service看到这种日志,解决办法是调整 sepolicy,让shell(或相应的 domain)对该属性 type 有 set 权限。具体做法是把属性加入对应的 property_contexts,再在 sepolicy 中添加 allow 规则。
还有一个经常踩的坑:ro.开头的属性一旦设置就无法修改。如果你在代码里看到setprop ro.something 1,别被误导——这条命令只会在进程启动早期执行一次,之后你再怎么 set 都不会生效。如果想在运行期修改,请使用persist.或sys.前缀。
6.4 Init 自带的几个调试工具
Init 里有一些不太常被提到,但排查问题特别有用的命令和属性。
wait_for_prop命令可以阻塞执行,直到某个属性达到预期值。rc 里经常这样用:
on boot wait_for_prop sys.boot_completed 1 # 等 boot 完成后执行后续动作调试时如果你想让某个初始化步骤等一个异步事件,可以临时加一行wait_for_prop,不需要写代码。不过这命令只在 Init 的解析流程里有效,不要在 adb shell 里直接敲。
ctl.start、ctl.stop、ctl.restart是三个神奇的系统属性,通过它们可以在运行时控制 service 的启停。比如你想重启 adbd:
adb shell setprop ctl.restart adbd这个操作的本质是让属性服务触发 Init 对该 service 执行重启逻辑。类似的,setprop ctl.stop surfaceflinger会停掉 surfaceflinger。调试渲染和 HAL 相关服务时,这三个属性极其好用。
exec_start命令则用于主动运行一段 rc 中定义的 service,通常配合oneshot使用。它和start的区别是exec_start会等待该命令执行完成再继续。这在制作带顺序依赖的启动动作时很有用。
注意:
ctl.属性和exec_start往往受 SELinux 约束。不同厂商的开机脚本策略差异很大,在别人代码里看到这类命令时,先确认当前 domain 是否有对应的权限,别想当然认为所有机器都能用。
6.5 我个人的一点排障建议
排查 Init 问题,我个人的体会是:先确立“当前系统到底走到了哪一步”,再用二分法缩小范围。内核日志先看阶段标记,阶段到了再看具体服务的启动状态,服务启动失败再看它的依赖和 SELinux 日志。这三步走完,绝大多数问题都能定位到具体文件或具体配置上。
另外一个容易忽略的建议:多熟悉你的平台厂商改过的 rc。AOSP 的 init.rc 是一回事,高通的、MTK 的、展锐的都有自家改动。同样的class_start main,在不同平台上实际拉起的服务个数和顺序很可能有差异。遇到问题时,先看看厂商有没有在 rc 里挂额外的 service,再去怀疑 AOSP 代码本身。
Init 进程不像 Binder、HwBinder 那样有大量架构知识,它的核心价值在于“把事情按正确顺序做对”。正因为它朴素,反而值得每个做系统层开发的人仔细读一遍源码——不需要逐行读懂,只要把init.cpp里主流程和service.cpp的管理逻辑过一遍,以后再看到开机日志,你就能在脑子里画出一条清晰的推进线了。