news 2026/9/8 10:17:10

Android Init进程全解析:启动流程、rc文件与属性服务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android Init进程全解析:启动流程、rc文件与属性服务

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_initsecond_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 接收来自各个进程的写请求,并负责权限检查、值类型校验、持久化等。平时你敲的getpropsetprop,底层就是在和 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的实现里,通过MountHandlerDevices这类组件去处理挂载、设备节点创建等事务。

2.2 Init 主循环:事件驱动的业务模型

Init 启动完成后并不会“一次性把所有事情做完然后退出”,它需要持续运行,响应各种异步事件。它的主循环是一个基于 epoll 的事件驱动模型,和你在应用层写的Looper思路类似,但挂在它监听列表上的,是一组“文件描述符”。

这些被监听的 fd 主要来自几个方向:

  1. 属性服务的 socket 描述符。其他进程通过本地 socket 连接进来,请求设置属性,Init 需要及时读取并处理。
  2. 子进程终止的信号。Linux 里子进程退出会产生 SIGCHLD,Init 用 signalfd 把它转成可读事件加入 epoll,方便在事件循环里统一处理。
  3. 设备节点的 uevent。早期阶段需要处理冷插拔(coldplug)和热插拔(hotplug)事件,创建、删除设备节点,这些消息也会通过 netlink socket 接入主循环。
  4. 各种显式注册的 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 才是我们平时研究的重点。它会:

  1. 重新设置部分 SELinux 上下文,保证自己以正确的域(domain)运行。
  2. 解析并导入所有 rc 文件,包括/init.rcsystemvendorodm各分区中的附加 rc 文件。
  3. 初始化 property service,加载默认属性、以及各种*.prop文件。
  4. 进入主事件循环。

在实际板子上,你可以在串口日志里看到类似这样的阶段标记:

[ 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.rcstarting service 'zygote'之类的日志,都属于这个阶段的产物。

3. 核心机制一:rc 文件解析与 Action 执行框架

Init 的配置体系是 rc 文件。这个文件里的语法和 Shell 脚本有相似之处,但它不是 Shell,而是一套专门为 Init 设计的声明式语言。理解它能帮你搞清楚服务是怎么被组织、调度、重启的。

3.1 rc 文件的语法结构

rc 文件的基础单元是“行”,以关键词开头,后面跟参数,用空格分隔。注释以#开头。常见的顶层关键词有四类:onserviceimportoption

一个 Action 的写法:

on early-init # 设置一些系统属性 setprop sys.usb.config none on property:ro.crypto.state=encrypted # 挂载 /data 分区前的处理

on后面跟的是触发条件(trigger),可以是固定阶段名(如early-initinitlate_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 zygote

service关键字后面跟服务名、可执行文件路径和参数。下面的缩进行就是 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 自动启动需要显式startctl.start
oneshot执行完就退出,不算异常配合 disabled 常用于一次性初始化任务
user以指定用户运行不要轻易给 root,安全风险高
group以指定组运行可附加多个组
critical退出会导致系统重启慎用,否则一个服务挂了就无限重启
onrestart服务重启时额外执行命令常用于 zygote 挂掉时重启 surfaceflinger
socket自动创建 socket 文件Android 服务间通信常用方式

类(class)机制很实用。class main的服务会在系统启动的 main 阶段被统一拉起,class core则会更早一点。通过class_startclass_stop命令,可以一次性启动或停止整组服务。比如你在调试时想重启所有 main 类服务,可以这样:

stop start

这在 Init 的 rc 配置里对应的是class_start mainclass_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 审核。

工作流程是这样的:

  1. 调用进程通过__system_property_set()发起写请求。
  2. 这个请求会通过本地 socket 发送给 Init 的 property service。
  3. Init 校验请求方是否有权限写这个 key(通过 SELinux 判断)。
  4. 校验通过后,在共享内存的属性区域更新该 key 的值。
  5. 如果 key 以persist.开头,还会把值写入/data/property/下的持久化文件,保证重启后不丢失。

共享内存的设计让读操作非常快——不需要跨进程通信,直接读共享内存的映射区域就行。这和一些你熟悉的“[进程间通过 mmap 共享数据]”思路完全一致,只是 Android 把它做成了统一框架。

补充一点:property_get()是 libcutils 提供的 C API,Java 层对应的是android.os.SystemProperties.get()。Framework 里很多配置都通过属性传递,比如ro.build.version.sdksys.boot_completeddalvik.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_serversocket 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+ 设备举例,启动顺序大致是:

  1. Init second stage 开始,解析 rc。
  2. 执行early-initinitearly-fsfspost-fs等阶段。
  3. 挂载好/data等分区后,进入post-fs-data
  4. 进入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 到应用进程的完整链路

这里顺便把整条链路串一下,帮你建立整体观:

  1. 内核启动,Init 成为用户空间第一个进程。
  2. Init 解析 rc,进入 zygote-start 阶段,通过class_start main拉起 Zygote。
  3. Zygote 启动后 fork 出 system_server。
  4. system_server 里的 ActivityManagerService(AMS) 启动后,会向 Zygote 发起 socket 连接。
  5. 用户点击 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

拿到日志后,第一件事是定位卡在哪个阶段。你可以这样做:

  1. 看是否有init second stage started!。没有的话,问题在第一阶段,通常是 mount 分区、SELinux 策略或者 initramfs 问题。
  2. 看是否出现了Parsing file /init.rc。有解析日志但后面卡住,重点查 rc 文件里某个 service 是否卡死了。
  3. 看是否出现starting service 'zygote'。如果 zygote 一直没起来,多半是 Zygote 启动参数、SELinux 上下文,或它依赖的库缺失。
  4. 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.startctl.stopctl.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的管理逻辑过一遍,以后再看到开机日志,你就能在脑子里画出一条清晰的推进线了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 10:16:58

Python类型提示完全指南:从变量注解到泛型的工程实践

1. 类型提示不是给解释器看的&#xff0c;是给未来的自己看的 很多刚接触Python的人第一次看到类型提示&#xff0c;脑子里冒出来的问题基本都一样&#xff1a;Python不是动态语言吗&#xff1f;我明明可以不写类型&#xff0c;为什么还要多此一举&#xff1f;甚至有人觉得这是…

作者头像 李华
网站建设 2026/9/8 10:13:23

C#异步编程核心:async/await与Task机制详解及实战

干C#这行这么多年&#xff0c;我最大的一个感触就是&#xff1a;真正让程序“快起来”的&#xff0c;往往不是把某个算法优化几毫秒&#xff0c;而是把线程资源用在刀刃上。异步编程模式配合async/await和Task&#xff0c;正好是C#里解决I/O密集型任务&#xff08;网络请求、文…

作者头像 李华
网站建设 2026/9/8 10:13:17

Ray深度解析:从Spark/Celery对比到分布式任务与Actor实战

先说结论&#xff1a;Ray不是要替代谁&#xff0c;而是把“分布式”这件事下沉成了一个Python原生的运行时。它既没有像Spark那样把一切都抽象成RDD/DataFrame的批计算模型&#xff0c;也没有像Celery那样把任务做成一条投递队列就完事。它站在两者之间的空白地带——让普通的P…

作者头像 李华
网站建设 2026/9/8 10:13:10

计算机思维核心四环节:分解、模式识别、抽象与算法设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 10:12:41

用Claude+Higgsfield打造自动化AI视频制作工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 10:12:15

MATLAB三维重建实战:基本矩阵求解与点云恢复全流程解析

简介&#xff1a;面向计算机视觉三维重建方向的课程设计与毕业设计需求&#xff0c;这份MATLAB源码包聚焦基本矩阵求解与三维点恢复&#xff0c;提供完整可运行的工程实现。压缩包共5个文件&#xff0c;包含3个MATLAB脚本&#xff08;主流程、功能测试与可视化界面&#xff09;…

作者头像 李华