ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Android Init启动流程详解:从内核到Zygote的完整链路

2026/9/24 23:01:38 拓冰建站 浏览量
Android Init启动流程详解:从内核到Zygote的完整链路 Android Init 启动流程说实话很多做上层应用开发的朋友可能一辈子都用不到它。但只要你接触过上层的系统稳定性问题、开机流程优化、或者是做过 BSP 适配你早晚要回来啃这一块。作为一个被 Init 折腾过无数回的过来人我觉得有必要把这条链路彻底聊透。这篇文章不打算写得像源码分析笔记那样逐行贴代码而是想把 Init 到底在做什么、为什么要这么设计、以及我们实际调试时能用什么手段捋清楚。它的核心关键词就三个Android、Init、启动流程。你会看到我从内核是怎么把接力棒交给它的到它如何解析 rc 脚本拉起 zygote再到实际排查卡死和修改不生效的问题全流程串一遍。不管是做系统开发、搞开机优化还是准备系统方向面试这篇文章都能给你一个能直接拿来用的框架。1. 整体设计与启动链路拆解1.1 内核到 init为什么它是第一个用户态进程先从头理一遍。手机按电源键后首先是 BootROM 加载 bootloaderbootloader 再引导内核。内核本身要做的是一堆硬件初始化它只管把 CPU、内存、中断、设备驱动这些最基本的东西点亮。等内核准备得差不多了就会去挂载一个临时的根文件系统然后执行其中的/init这个可执行文件。这个/init就是 Android 系统中的第一个用户态进程它的 PID 固定为 1。这里有个很关键的设计含义PID 为 1 的进程是系统中所有进程的老祖宗任何进程的父进程如果挂掉了最终都会由它来“接盘”。Linux 内核在启动完自己的逻辑后会主动把控制权交给用户态而这个交接点落到了 init 头上目的就是让 init 做用户态所有东西的总调度。注意Android 的 init 并不是标准的 Linux init它比 SysV init、systemd 那些要“专一”得多。它不负责管理一堆复杂的服务依赖而是围绕 Android 这套体系做几件核心的事初始化文件系统节点、挂载分区、加载 SELinux 策略、启动属性服务、解析 init.rc 脚本再按脚本里的定义拉起 zygote、servicemanager、vold 这些关键进程。它本质上是一个“开机总指挥”所有用户态世界的运行和秩序都由它一手安排。从工程角度来看把 init 设计成独立进程而不是直接在内核态做这些事情主要有两个好处一是用户态的代码好维护、好调试不用动不动就改内核编译也快得多二是靠多个进程隔离各自的任务即使某个系统服务挂了init 可以通过 watchdog 或 restart 机制重新拉起不会像内核直接崩掉那么致命。1.2 init 的职责边界一个顶五个Android 的 init 一个人要扮演好几个角色这也是它有别于普通 Linux init 的核心地方。具体拆开看它至少干了五类活文件系统与设备节点管理ramdisk、devtmpfs、sysfs、procfs 这类基础文件系统要在 init 阶段挂载之后还要通过 ueventd 子进程处理内核广播的 uevent 事件动态创建/dev下的设备节点。分区挂载与校验尤其从 Android 10 开始推进动态分区到 Android 12 以后 first stage mount 逻辑越来越重init 要负责按 fstab 配置把 system、vendor、product 等分区挂上还要做 AVB 校验确保引导过程可信。属性服务Android 的属性系统本质是一个存在 init 里的共享全局字典通过 socket 对外提供读写能力上层用getprop、setprop操作就是走这个通道。init 对这个大字典拥有最终裁判权。服务进程管家init.rc 里service定义的那些进程一旦退出init 会根据配置决定要不要重新拉起。像是 zygote 这类关键进程如果反复崩溃init 甚至会触发重启整个系统。事件触发器与动态行为控制init 有一套基于 trigger 的事件机制比如开机到某个阶段就触发on boot属性满足某个条件就触发on property:xxxyyy。这套机制让系统启动变得可编排。理解这些职责你就明白了为什么 Init 的代码越写越长也越来越“重”。很多人以为它就是“启动一下 zygote”实际上它承载了启动链路里所有脏活累活。把这些基础盘理顺后面我们再看具体机制就不会迷路。2. 核心机制与关键细节解析2.1 两阶段设计first stage 与 second stage如果你翻过 Android 10 以上的源码会发现 init 的代码被分成了first_stage_init和second_stage_init两条线。很多人第一次看到的时候一脸懵为何一个 init 还要搞两段原因得从分区布局的演进说起。早期的 Android根文件系统 ramdisk 里放着 init、init.rc 等system 分区是独立的init 一上来就能从 system 分区加载各种配置。但随着动态分区、A/B 无缝升级、系统分区只读化这些设计出现init 在真正执行复杂逻辑之前必须先把自己需要依赖的分区给挂载好。也就是说它得先“独立生存”一小段靠内核自带的驱动和最小设备节点把 system、vendor 这类分区盘活然后才能加载完整配置。所以 first stage 干的事情非常克制挂载基础文件系统tmpfs、devpts、proc、sysfs创建/dev和/sys下的关键节点加载并切换 SELinux 策略然后根据 fstab 用 first stage mount 机制把系统分区挂上。它要尽量不依赖外部库所以早期的 first stage 还是一个静态链接的二进制直到后来加了ueventd和 watchdogd 的子进程才慢慢扩展。完成这些基础工作后init 重新执行自己进入 second stage也就是我们熟知的完整初始化设置环境变量、启动属性服务、解析 init.rc、拉起 zygote。两阶段设计让我觉得最“解渴”的一点是它把“能开机”和“开好机”分开了。第一阶段只要保证系统能够到达用户态的第二个入口就算是底线保障第二阶段才是真正把 Android 用户空间服务的复杂网络铺开。这给系统定制留下了很好的扩展点比如你想做早期开机日志就可以在 first stage 里加输出比在 second stage 里等属性服务就绪要早得多。2.2 init.rc 解析模型Action、Service 与 Triggerinit 最核心的外部接口就是.rc文件。它们分布在系统各个分区的/etc/init/目录下系统启动时init 除了解析自己根目录下的/init.rc还会递归读取/system/etc/init、/vendor/etc/init、/odm/etc/init等目录下的所有 rc 文件。rc 文件的核心模型是“声明式”的大体分为三类on触发器加命令、service服务加选项、还有import导入语句。这里的on就是 trigger触发点它定义“当某个事件发生时执行哪些命令”。事件是什么呢最典型的有early-init、init、early-fs、fs、post-fs、post-fs-data、zygote-start、boot这些阶段标记也有property:ro.crypto.stateencrypted这种属性条件。整个开机过程就是 init 在合适的时机触发对应 trigger然后执行其下所有命令。比如一个典型的段落大致长这样on init mkdir /data 0700 root root mount tmpfs tmpfs /storage rec chmod 0771 /storageon init表示在init这个阶段创建一个/data目录并挂载 tmpfs 到/storage。注意 rc 里命令的顺序是有讲究的不能乱写。service段落则是声明一个可执行进程service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server class main priority -20 user root group root readproc reserved_disk socket zygote stream 660 root system onrestart restart zygote writepid /dev/cpuset/foreground/tasks这一句定义了一个叫zygote的服务它的可执行路径是/system/bin/app_process64启动参数是--zygote --start-system-server。class main表示它属于main服务类这样 init 在开机时通过class_start main命令就能一次性把它连同同类的其他服务一起拉起来而不是零散地逐个启动。这也是很多人初看 init.rc 觉得“别人能启动我加的为什么起不来”的关键你光写了service xxx ...但没人执行start xxx或者class_start把你这个类启动。服务选项里的onrestart也很重要它表示当该服务重启时会追加执行后面的命令。比如上面onrestart restart zygote意思是如果 zygote 再次被重启还会再执行一次restart zygote。实际上更多场景是onrestart restart audioserver来确保音频服务跟着重建因为音频需要 binder、需要 zygote 的支持。理解 Action 和 Service 的区别要抓住一点Action 是“触发式”的Service 是“常驻式”的。Action 只在触发那一刻跑一次适合做文件系统准备、目录创建、权限设置这类一次性初始化Service 则是长期运行的进程init 负责监控它的生死如果它退出时没有声明oneshotinit 会尝试重新拉起。掌握了这个模型你就不会在自定义启动脚本时把两者混为一谈。2.3 属性系统藏在内核 socket 里的全局通信Android 属性服务是一个很巧妙的设计它把整个系统共享的一组键值对维护在 init 进程内部通过一个 Unix socket 来对外提供读写接口。为什么不让每个进程各自维护因为很多系统级配置需要全局一致性比如存储加密状态、网络是否可用、系统是否已经启动完成上层需要一个统一、可靠且带权限控制的信息源。属性名的格式有讲究比如以ro.开头的是只读属性一旦设置就不可再改以persist.开头的属性会持久化到/data/property目录重启后依然保留以ctl.开头的比较特殊是用来让上层向 init 发送控制命令的如setprop ctl.start myservice实际上就是告诉 init 去启动myservice这个 service。还有个常见的sys.前缀表示系统运行时由各进程动态设置、非持久化的属性。从上层看你执行getprop看到的所有键值对就是这么来的。setprop想要修改某个属性必须满足 SELinux 的权限要求也就是说不是随便哪个进程都能改任意属性。init 在收到 setprop 请求时会做权限校验如果发送方的 context 没有相应的property_type写入权限请求会被直接拒绝这也是很多人发现“为什么我 setprop 没生效”的幕后原因之一。这个机制的意义在于它为系统层和应用层之间的动态协作提供了一个解耦空间。比如 hệ thống 启动到某个状态init 设置sys.boot_completed1上层应用监听这个属性变化就知道可以开始做初始化了。系统服务之间也经常靠属性传递小消息避免每个人都去直接绑服务、查接口。做过系统稳定性的人都知道很多“开机问题”其实绕到最后都跟属性时序有关所以捋清 init 的属性服务实际上也为后续系统调试打了一个很好的底子。3. 实操亲手跟踪一次启动3.1 确认环境真机或模拟器怎么入手理论讲再多不如自己动手跑一遍。准备条件其实不苛刻一台已解锁 bootloader 的 Pixel 手机或者一个普通的 Android Studio 模拟器就可以。如果你用的是模拟器先去开发者选项里打开“Android debugging”再用adb root拿到 root 权限。adb root对模拟器和 userdebug 固件有效如果是普通量产机的 user 版本通常不能 root这部分功能就受限所以做系统调试建议直接上模拟器或者 userdebug/eng 版本的固件。拿到 root 后第一件事是确认 init 进程是否真的以 PID 1 在跑adb shell ps -A | head -n 20你会看到类似下面这样的输出USER PID PPID VSZ RSS WCHAN ADDR S NAME root 1 0 1028916 24544 do_epoll_wait 0 S init输出里 PID 为 1、名字为 init 的就是我们要找的“总指挥”。它的 PPID 是 0说明它就是所有进程的祖先。从这里开始你看到的整棵进程树追根溯源都是 init 的后代。再往下你能看到ueventd、logd、servicemanager、zygote等进程它们的 PPID 几乎都是 1说明这些进程都直接由 init 拉起或者在 zygote 启动后变成了它的后代。3.2 看进程树从 PID 1 开始当我们想确认“某个服务是不是被 init 拉起来了”最直接的方式就是查它的进程。假如你修改了 init.rc添加了一个叫myservice的脚本并且在开机后执行adb shell ps -A | grep myservice如果什么都没有那基本可以确定要么是服务没被触发启动要么是启动后立即崩溃。此时可以看看 init 的日志init 自身打日志一般记录在logcat -b system中或者通过dmesg看到一部分早期输出。另外init也会把 rc 解析和命令执行过程中的错误记录到/dev/fscklogs/fsck.log附近不过最通用的还是看logcat -b all -d | grep -i init。除了ps还有一个很实用的入口是查看/proc下的进程信息adb shell cat /proc/1/cmdline adb shell cat /proc/1/status | head这些信息能帮你确认 init 的运行状态、SELinux context、内存占用等。如果你对某个子进程的启动参数有疑问例如想知道 zygote 到底是以什么参数被拉起来的可以看adb shell cat /proc/zygote_pid/cmdline你会看到app_process64相关的一大串参数这些参数就是从 rc 文件里定义的service zygote那行传过来的。通过这样的方式你可以验证自己修改的配置有没有真正生效也方便在排查时把“我看不到现象”和“系统里到底怎么跑的”对应起来。3.3 抓启动日志logcat、dmesg 和 bootchart真正做启动流程优化时光知道进程有没有起来还不够还要知道每一步花了多少时间。有两个抓手一是 logcat 的启动日志二是 bootchart 的耗时统计。先看 logcat。重启前先清空日志然后重启设备再执行adb logcat -b all -d boot_log.txt启动日志量非常大但你可以抓住几个关键节点。logcat -b events里有am_proc_start这类事件能标明某个进程是在开机后第几秒被创建的。如果你是优化开机时间就应该关注从 kernel 启动到boot_completed这个属性置为 1 之间到底卡在哪些进程和文件系统操作上。bootchart 更直接。Android 从 8.0 开始内置了 bootchart 功能只要创建标志文件并在启动后抓取即可adb shell echo 1 /data/bootchart/enabled adb reboot # 开机完成后 adb shell ls /data/bootchart/ adb pull /data/bootchart/header /tmp/然后将/data/bootchart/下面的文件打包在宿主机上用pybootchartgui绘制成图就能看到整个开机阶段 CPU、IO、进程启动时长的分布。bootchart 对找“开机慢”的瓶颈非常有用我第一次用的时候一眼就看到某个自定义服务因为等待属性卡了 2 秒这个效率比人肉翻 log 高太多了。4. 常见问题排查与避坑实录4.1 init 起不来或卡死怎么快速定位init 本身其实很少完全崩溃因为它一旦崩了内核就会进入 panic 或者不断重启。但如果你的设备卡在开机 logo 或者黑屏不动需要排查的方向主要有三个。第一先确认系统到底有没有进到 init。可以在 bootloader 界面使用fastboot或串口连接观察有没有输出。如果连 init 的第一条日志都没看到多半是内核阶段的问题比如 ramdisk 没打包进去、init 二进制路径错误或权限不对。这种情况在定制 ROM 时很常见尤其是你想把 init 替换成自己编译的版本时一定要确认 make 生成的是system/core/init路径下的 init 文件并且打包时放到了根文件系统根目录。第二进到了 init 但卡在某个阶段。此时串口或 logcat 会停留在某条日志上。最常见的卡点有挂载 system 分区时等待超时、SELinux 策略加载失败、某个 rc 文件里出现语法错误导致解析终止。rc 语法错误尤其隐蔽比如空格和 tab 混用、命令名写错、路径不存在。init 在解析出错时通常会在日志中打印Parser相关的错误信息但很多人不留意。第三zygote 起不来导致系统一直重启。你可以执行adb shell getprop sys.boot_completed看是不是一直为 0。如果一直为 0再看看 logcat 里是不是有Fatal signal 11这类 zygote 崩溃日志。init 通常会在 zygote 反复崩溃几次之后通过hardware reboot重启系统所以现象就是“设备无限重启”这种时候优先排查新增的 native 库或者 selinux 权限是不是把 zygote 给堵了。4.2 SELinux 引发的“莫名失败”在 init 启动流程里SELinux 绝对算是一言不合就“断你路”的隐形杀手。很多服务进程启动后立刻被杀或者某些命令执行不了最后都是 SELinux 权限问题。Android 在 enforing 模式下一切访问控制都由 sepolicy 策略决定init 自身在加载完策略后就切换到了 enforcing 模式后续每个进程的 security context 都受到严格限制。当你发现某个服务起不来时第一步是抓 avc denialsadb shell dmesg | grep avc adb logcat -b all -d | grep avc输出里会看到类似这样的记录avc: denied { read } for pid1234 commmyservice nameconfig.txt devmmcblk0p30 ino123 scontextu:r:myservice:s0 tcontextu:object_r:vendor_configs_file:s0 tclassfile permissive0这行日志的意思很清楚myservice这个进程context 为u:r:myservice:s0想读config.txt文件context 为vendor_configs_file但策略不允许所以被拒绝。拿到这条日志再去修改对应的.te策略文件加上类似allow myservice vendor_configs_file:file read;的规则然后重新编 boot 镜像或 selinux policy 即可。很多人图省事直接执行adb shell setenforce 0来切到 permissive 模式绕过权限检查。这在调试阶段确实很快但要注意两点一是 user 版本固件没有setenforce权限二是切到 permissive 后系统行为可能不一样比如某些进程因为依赖 SELinux 保护逻辑而出现异常或数据损坏。所以我的建议是只把setenforce 0当作临时验证手段别把它当解决方案。排查到具体 deny 规则后还是老老实实去改 sepolicy。4.3 修改 init.rc 不生效的 5 种原因我自己踩过很多次“改完 rc 文件但开机没效果”的坑总结下来逃不开下面五种原因改错了地方系统里存在多个 init.rc 或同名 rc 文件尤其是 vendor、odm、product 分区里各有一套。你改了 A 目录的文件init 实际读的是 B 目录。解决方法是先执行adb shell ls /system/etc/init/ /vendor/etc/init/ /odm/etc/init/确认你要改的文件具体在哪个分区。没有 remountsystem分区默认只读你直接adb push改好的 rc 文件会失败必须先执行adb remount或者重新打包 system.img 刷入。注意adb remount对 userdebug 版本有效模拟器上也要先adb root。语法或路径错误init 解析 rc 文件时如果遇到语法错误或路径不存在可能跳过整段命令甚至导致服务无法注册。建议先写一个非常小的改动验证逻辑比如在on boot里加一条write /data/local/tmp/init_test 1看文件是否被创建。文件 SELinux context 不对即使文件放对了如果重新打包后文件的 SELinux context 没设置对进程还是打不开。要确保新增 rc 文件被打包到既有分区目录时context 与那个目录匹配必要时在 file_contexts 里补充规则。服务类没有启动再次强调service只是定义真正执行要等到start或class_start。如果你新增的服务不在任何 class 里也不被任何ontrigger 的 start 命令触发那它就永远只是“注册了但没跑”。遇到“改了不生效”时按这个清单逐项过一遍比盲目重编系统效率高得多。4.4 Android 12 起的分区挂载变化init、AVB 与 fstab从 Android 10 到 Android 12系统分区布局经历了比较大的调整这也直接影响 init 启动流程。很多人还停留在“init 先跑 ramdisk再挂 system”的旧观念里但新版已经变成init 在 first stage 就尝试挂载 system 和 vendor 等动态分区而且挂载过程中还涉及 AVB 校验。简单说动态分区就是 system、vendor 这些分区不再以独立物理分区存在而是被塞进了一个 super 分区里面。init 必须根据 fstab 配置先找到 super 分区再通过 dm-linear 把这些动态分区映射出来然后挂载。这个过程对设备树、内核 cmdline 和 fstab 的依赖都很大一旦配置不对就会出现“内核起来了但挂不上 system 分区”的情况。fstab 的格式因版本而异新版通常采用类似下面的配置/system ext4 erofs wait,first_stage_mount,avbvbmeta_system logical字段含义依次是挂载点、文件系统类型、设备这里是 logical 表示动态分区、挂载选项、以及分区类型。关键选项first_stage_mount表示这个分区要在 init 第一阶段就被挂载avbvbmeta_system表示要用 AVB 校验。想手动调试的人也常通过adb disable-verity来关闭验证但这只针对 userdebug 设备而且关掉后安全性会下降。另外要重点提一下 APEX。从 Android 10 开始一些系统组件被打包成 APEX 文件放在/system/apex里init 在启动早期需要借助apexd服务来激活这些 apex 模块再基于这些模块去启动上层服务。APEX 的挂载和激活是启动流程里比较晚加入的一环也经常是“开机后某些系统接口不存在”的排查点。做系统定制时如果发现某个模块的版本和你预期不符记得先去logcat里搜apexd的日志看看 APEX 有没有被正确激活。5. 写在最后一点实操体会最后想说一个我自己觉得特别重要的心得学 init 启动流程一定不要只停留在读文档和看源码的层面。我见过太多人把init.rc的各个 trigger 背得滚瓜烂熟但真让他去排查一个开机卡死问题还是无从下手。原因在于启动流程是“时间线”上的东西很多问题只有在真实的器件上跑一遍你才能感受到它的时序敏感性和环境复杂性。我建议新手拿到一个系统后先做个操作清空 logcat重启设备然后用logcat -b all -d完整拉一份启动日志再从里面搜索init、zygote、SystemServer、boot_completed这几个关键词自己画出这条时间线。这个过程做多了你对整个系统的启动节奏会有非常直观的感觉以后再看到“开机慢”“服务起不来”“属性没读到”这类问题脑子里会立刻浮现出到底卡在哪一环。启动流程这门课归根到底是要靠实践来内化成自己的知识框架的。