我还在带团队做系统定制的时候经常被新来的同事问同一个问题手机从按下电源键到桌面亮起中间这一段黑屏时间里系统到底在干什么尤其是“Kernel启动”和“init启动”这两个概念很多人容易混在一起。有人以为看到内核日志就算Kernel阶段结束了有人以为init只是读个脚本启动服务结果遇到开机问题的时候完全不知道去哪里查。这篇文章我单独把这两段拎出来讲透从硬件复位到init把system_server拉起来整条链路的每个关键节点都会过一遍。适合正在入门Android系统开发、做开机优化或者需要排查启动类问题的工程师参考内容不会贴大段源码但会给你指出明确的代码路径和日志特征。1. 从按下电源键到内核接管启动链路的前半程是怎么走通的1.1 电源键之后BootROM、Bootloader与lk很多人以为按下电源键之后第一个运行的是Kernel实际上在那之前还有两段代码在跑。首先是固化在SoC内部的BootROM这段代码出厂就烧在芯片里不可修改。它做的事情非常初级初始化最基本的内存和时钟然后把Bootloader从存储介质加载到内存中执行。Bootloader在高通平台上通常是ABLUEFI加LKLittle Kernel的组合也有部分平台直接跑LK。ABL/UEFI主要负责显示、按键检测、分区表解析以及AVB校验LK则负责更底层的DDR初始化、串口输出、fastboot支持等。到了这一步屏幕可能已经亮起了品牌Logo但Android系统还没有真正开始跑。注意如果你在调启动问题BootROM阶段基本没有调试手段Bootloader阶段可以通过串口输出来看。当年我在一块开发板上抓日志串口波特率、电平不匹配导致前几百行全是乱码折腾了半天才发现是硬件参数问题而不是启动卡死。1.2 设备树DTB的传递硬件描述如何到达内核Bootloader完成初始化后会把Kernel镜像、设备树DTB/DTBO、Kernel cmdline按照约定地址加载到内存然后跳转到Kernel入口。这里有一个容易被忽视的细节dtb文件承载着硬件描述信息Kernel启动时靠它来识别内存大小、外设地址、pinmux配置等。在高通平台上boot分区里同时包含Kernel镜像和dtbdtb可以通过#address-cells、#size-cells等节点被Kernel解析。到了GKIGeneric Kernel Image时代设备树被单独拆到vendor_boot分区Kernel镜像与board-specific的dtb解耦这是后面第6章要展开讲的内容。这里提醒一句如果你是自己编译Kernel一定要确认dtb与Kernel版本匹配否则最典型的故障就是启动到一半黑屏或者串口无输出。我就踩过这样的坑换了新Kernel没重新编译dtb结果Kernel解压完就挂回头查代码才发现是__fdt_pointer寄存器传的物理地址根本不对。1.3 Kernel解压与最终准备Kernel镜像通常是自解压形式入口处有一段汇编代码会先进行解压然后进入_text真正开始执行。在32位时代有__virt_to_phys的地址转换问题64位下相对简单一些但__boot_cpu_mode、__create_page_tables这些汇编阶段依然是高密度踩雷区。一个常见误区是看到Kernel解压日志就以为系统已经起来了。实际上解压只是万里长征第一步后面还有非常多的初始化工作要做从start_kernel开始才算真正进入C语言阶段系统的进程调度器、内存管理器、驱动模型都在这个阶段完成初始化。2. start_kernel之后的那些事Kernel启动的关键阶段与硬件初始化2.1 start_kernel一切开始的地方start_kernel位于init/main.c是整个Kernel初始化的核心入口。从这个函数开始系统经历了从单核到多核、从无内存管理到页表完整建立、从零驱动到设备枚举完毕的全过程。里面有几个关键的里程碑setup_arch()完成架构相关的初始化比如页表、内存布局、异常向量表的建立mm_init()把页分配器、slab分配器跑起来sched_init()建立进程调度器。这些完成之后rest_init()创建了第一个真正的内核线程kernel_init同时启动了kthreadd守护进程。这一阶段最值得关注的是每个子系统的初始化顺序它们不是随意排的而是有严格依赖关系。比如时间子系统如果没有先初始化delay函数就不能用中断子系统没起来之前所有设备驱动都只能轮询。这也是为什么驱动挂死的时候第一反应应该是看它被放在哪个initcall层级。2.2 SMP启动与BSP/AP核的分工系统冷启动时首先只有一个CPU核在运行这个核叫引导处理器BSP。其他应用处理器核AP处于WFIWait For Interrupt状态等待被唤醒。Kernel通过secondary_start_kernel、cpu_up等调用利用PSCI或spin-table机制把AP核逐个唤醒。这块在实际项目里很容易出问题。常见的就是某个核因为供电域没配置好或者GIC中断配置错误导致CPU1: failed to come online之类的错误整机性能异常。排查手法一般是看dmesg里有没有CPU hotplug相关的报错同时确认设备树里cpu节点的enable-method配置正确。曾经有个项目在低功耗模式切换时偶发死机查到最终原因是一个AP核唤醒后cache没有正确invalid拿check_cache相关的工具反复验证才定位到。这类问题的共性特征就是偶发、难复现、且每次出现时日志都停在SMP唤醒附近。2.3 设备驱动初始化initcall机制与分层驱动加载Kernel引以为傲的驱动模型不是简单地把driver_init跑一遍就完事而是通过initcall机制在do_initcalls()阶段统一调用。这些initcall按照优先级分成多个层级从pure_initcall到machine_initcall再到core_initcall、postcore_initcall、arch_initcall、subsys_initcall、fs_initcall、device_initcall、late_initcall每个层级都对应特定的依赖阶段。initcall层级典型用途出现顺序pure_initcall纯参数、常量初始化最先core_initcall内核核心子系统如中断、时间第二梯队postcore_initcall依赖核心子系统的架构初始化第三梯队arch_initcall架构相关资源第四梯队subsys_initcall总线、子系统框架第五梯队fs_initcall文件系统注册第六梯队device_initcall绝大多数设备驱动第七梯队late_initcall依赖于设备驱动的后期初始化最后这块是Android开机时间的最大变量之一。如果你做过开机优化肯定会发现很多系统服务其实在等待某个设备节点出现而该设备节点的驱动恰好被放在了一个靠后的initcall层级。简单地调整initcall顺序是下策真正应该做的是分析设备依赖并合理设计probe流程。我见过有人为了让某个外设模组提前加载直接把驱动从module_init强行改成subsys_initcall结果导致该驱动依赖的regulator还没注册probe直接失败最后还是老老实实把依赖关系捋清楚了。关于驱动加载还要提一下request_module机制。Kernel在发现设备节点时会尝试通过uevent触发用户空间加载模块但Android设备一般把所有必要驱动编入内核而非模块化这样既能节省启动时间也避免根文件系统尚未挂载时模块加载失败的问题。现代Android系统里虽然也有/vendor/lib/modules/但绝大多数场景下仅剩Wi-Fi、指纹等少数驱动仍以模块形式存在。3. init进程的系统接管之路属性、脚本与服务管理3.1 Kernel如何拉起init从kernel_init到run_init_processKernel在kernel_init函数里完成最后阶段的准备之后会尝试执行根文件系统中的init程序。它会依次尝试/sbin/init、/etc/init、/bin/init、/bin/sh如果都没找到就panic并输出No working init found。Android系统的init位于根目录下的/init本质上是符号链接或直接编译生成的可执行文件源码在system/core/init。这段流程中比较关键的一步是run_init_process它会用init替换掉当前内核线程的镜像从而让第一个用户空间进程正式诞生。这里有个常被忽略的概念init是PID 1它的特殊之处在于如果它退出了整个系统会触发kernel panic。所以我们在写init脚本时一定要非常谨慎任何阻塞操作都有可能拖慢整个开机过程严重时还会导致系统watchdog重启。init进程启动后logcat中会出现这样一行标志日志[ 0.000000] Kernel command line: ... init: Starting service logd... init: Control message: Started service logd...从这里开始系统的运行就逐步从Kernel主导切换为init主导。3.2 首阶段与次阶段first_stage_init与关键文件系统挂载init内部也分阶段。第一阶段first_stage_init负责最基础的环境搭建包括挂载/dev、/proc、/sysfs创建设备节点以及加载SELinux早期policy等。这一阶段对时序要求极高很多早期设备节点必须在init真正进入主逻辑之前创建好否则后面ueventd无法正常工作。第二阶段second_stage_init才是真正的大管家建立属性服务、挂载其它文件系统、启动ueventd、解析并执行init.rc脚本。这两阶段在Android 10之后发生了明显的架构变化——first_stage_init被移到了独立的first_stage_mount它与ueventd之间通过设备节点和uevent进行协调。Android 10之后引入了系统分区作为根文件系统的机制system分区会被直接挂载为/。这个改动让根目录和system分区的内容合一也影响了init的启动路径。以前我们习惯在根文件系统里放的init和相关脚本现在统统放进了system的/system/bin/路径下而/init则是一个指向/system/bin/init的符号链接。3.3 属性系统Android的全局注册表init启动过程中有一项非常核心但容易被忽略的工作属性系统。你可以把它理解成一个全局的轻量级KV存储系统服务、应用进程都能通过它获取设备信息、系统状态比如ro.build.version.release、sys.boot_completed这类。属性系统的实现分为两个部分一部分是共享内存区域的读写进程直接通过__system_property_set/__system_property_get访问另一部分是属性变更通知由init守护进程监听/dev/socket/property_servicesocket。当某个进程尝试修改属性时请求会先发送到init由init统一校验权限并执行修改普通应用不能随意修改ro.前缀的只读属性。在实际的项目中属性系统导致问题的典型场景是权限不足下的静默失败即一个服务尝试设置ctl.start属性来拉起另一个服务但SELinux权限没有放行服务悄悄不启动。排查此类问题时需要用setenforce 0缓解模式来快速验证是权限问题还是代码问题同时结合dmesg中的avc denial日志来定位这类avc: denied { set } for propertyctl.start_service的报错在启动阶段特别常见。3.4 init.rc语法与Service管理机制init启动的核心动作就是解析init.rc以及它在import语句中引入的所有rc文件。rc语法的几个核心概念动作Action、命令Command、服务Service、触发器Trigger和关键字Keyword。一个典型的service声明示例service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server class main priority -20 user root group root socket zygote stream 660 root system onrestart restart zygoteclass main决定了这个服务的启动时机系统会通过class_start main把整个main类的服务都拉起来。不同服务被划分到不同classcore类先启动main类随后late_start类最后。这种分批启动机制让系统服务之间的依赖关系变得可管理。如果服务频繁重启logcat中会出现类似于init: Service zygote (pid 1234) killing process...的报错。排查时优先确认服务启动时用到的用户、组、SELinux context是否正确。有一次我排查zygote反复重启的bug最终定位到group root没配置导致它无法访问GPU设备节点报错信息看起来完全像是内存不足实际却是权限问题。4. Zygote孵化器启动前的最后几件事SELinux、Socket与系统服务骨架4.1 SELinux在启动阶段的作用与加载时序SELinux是Android安全模型的地基init启动过程中的一个重要任务就是加载SELinux策略。Kernel启动时SELinux处于一个临时的permissive状态此时所有权限违规都会被记录但不会阻止。init会在second_stage_init的后期加载真正的policy文件然后把系统切换到enforcing模式。这个切换时机非常关键策略加载得太早文件系统还没准备好加载得太晚系统服务的启动顺序就会被打乱。Android官方标准的做法是要求所有用户空间服务必须在策略加载完成之后再启动所以在init.rc中会有相应的on fs和on post-fs这类触发点确保SELinux切换发生在合适的时机。排查SELinux问题最直接的方式是抓avc denial日志。早期Android版本中你可以直接用dmesg查看新版本需要配合logcat -b events和/sys/fs/selinux/deny来确认。许多启动失败和功能异常的根本原因都是某个daemon没有追加到正确的SELinux domain中。4.2 Socket与IPC系统服务之间如何建立连接init负责预创建很多关键socket节点比如/dev/socket/zygote、/dev/socket/property_service等。这些socket在rc文件中通过socket关键字声明init会负责bind和listen并设置好正确的文件权限。系统服务与init通信的经典渠道是/dev/socket/init它承载了服务控制指令如ctl.start/ctl.stop。重要的机制在于init本身维持着一个epoll循环来监听多个socket这样它才能及时响应服务状态变更和属性请求。一个排错经验如果你发现服务迟迟没有启动但rc文件的逻辑看起来没有问题可以试着用adb shell ps -A | grep init确认init自身还活着然后通过adb shell getprop | grep sys.init查看属性推进情况。很多时候问题不是init没有执行启动命令而是/dev/socket目录的挂载方式有问题导致socket节点没有及时出现。4.3 Zygote的启动方式与system_server的诞生Zygote是Android应用进程的孵化器也是init启动的最后一个重量级服务。它的启动过程不是简单的fork而是通过execve执行/system/bin/app_process64并传入一组特定的参数来表明它是一个zygote进程。init.rc中对zygote的配置有一个容易被忽略的细节socket zygote stream 660 root system这条声明会提前创建一个名为/dev/socket/zygote的Unix socket。system_server和后续应用进程正是通过这个socket与zygote通信请求fork新进程。Zygote启动后会立即通过startSystemServer()进入system_server的孵化流程也就是Android所有核心服务ActivityManager、PackageManager、WindowManager等的宿主进程。到了这一步系统的整体骨架已经成型之后的演进就是SystemServer内部的服务注册和启动Android开机动画的结束与sys.boot_completed属性被置为1也标志着这一漫长启动流程基本收官。5. 启动问题定位实战日志获取、耗时分析与常见坑5.1 如何获取一份高质量的启动日志排查启动问题第一步是拿日志。最常见的做法是adb logcat -b all但如果问题发生在adbd还没起来之前就需要借助Kernel日志和串口输出了。serial console输出的dmesg是Kernel阶段最权威的信息源而init阶段的详细日志通常记录在/dev/kmsg和logd缓冲中。如果你需要一份完整的冷启动日志建议用以下命令组合adb reboot adb wait-for-device adb logcat -b all -d full_boot.log adb shell dmesg kernel_boot.log对于Kernel阶段看不到日志的场景可以在bootloader中开启串口输出并确保consolettyMSM0,115200这类参数配置正确。这是我踩过最多次的坑Kernel cmdline里面没有配console参数导致串口一句话都不打问题完全黑盒。5.2 耗时分析boot_progress事件与bootchartAndroid在启动过程中会向外发布一系列boot_progress事件这些事件被记录在logcat的events缓冲区中可以用logcat -b events查看。常见的事件标签包括boot_progress_start、boot_progress_ams_ready、boot_progress_enable_screen等。更直观的方式是开启bootchart。bootchart会在init进程启动早期就开始记录每个进程的CPU占用和I/O等待时间生成一张可视化的图表。开启方式有两种一种是在/data/bootchart目录下放置一个名为start的空文件然后重启另一种是利用init.svc.bootanim的时序来辅助分析。个人经验是bootchart最擅长揭示“看起来不慢但实际很慢”的问题。比如某个服务反复重启拉高了CPU这类问题靠肉眼看logcat是很难分辨的bootchart能在15秒的CPU总耗时中直接暴露异常进程。5.3 常见启动失败的类型与定位方法启动失败大致可以分成三类Kernel panic、init阶段失败、SystemServer进程反复重启。Kernel panic的直接表现是屏幕停住或串口打印Kernel panic - not syncing最省事的方法是先把kexec相关的crash dump机制配好配合pstore来保留上次启动的log。这里特别提醒一点很多Kernel panic在reboot之后日志就丢失了务必打开/sys/fs/pstore保留console-ramoops。init阶段失败则五花八门常见的是init.rc语法错误导致整个脚本无法解析或者某个依赖的ueventd没有建立好设备节点。曾经有项目因为新加的rc文件里多了一个非法缩进导致整个import链加载失败开机卡在了logo界面上。这类问题排查时建议逐个检查rc文件的import顺序并把/system/etc/init/目录中新增的rc文件隔离测试。SystemServer反复重启是第三类高频问题。这类问题的特征是logcat中不同进程的死因都相同绝大多数是Binder通信超时或死锁。我在调这类问题时的固定招数是先把ro.debuggable打开随后抓取debuggerd的tombstone通过线程栈分析锁竞争关系。6. Android版本演进对启动流程的改造Treble、动态分区与GKI6.1 Treble架构对init启动路径的冲击Android 8.0开始强制推行Treble架构把system和vendor分区彻底分离。这个改动对启动流程的直接影响是init必须同时处理来自system和vendor两个分区的rc文件且vendor进程的安全边界变得更加严格。init在解析时会依次扫描/system/etc/init/、/vendor/etc/init/和/odm/etc/init/目录下的所有rc文件。这里有一个不易察觉的细节vendor分区中的rc文件与system分区的rc文件不能相互import对方路径下的文件这会导致SELinux权限异常和服务启动顺序错乱。我在实际项目中甚至见过vendor的rc文件引用了system路径下的sh脚本结果被SELinux拦截服务启动静默失败。此类问题的排查路径是查看dmesg中是否有avc denial并结合add service权限来逐层放行。6.2 动态分区与AVB启动校验链的加固动态分区Dynamic Partition改变了系统分区的布局方式。传统的system、vendor、product都是独立分区动态分区把它们合并进了一个super分区由内核通过dm-linear实现逻辑分区的映射。这个改动影响整个启动过程Kernel在挂载system分区之前必须先通过dm解析super分区的逻辑布局。如果super分区的metadata坏了就会出现fstype错误或者文件系统无法识别的问题导致init阶段直接挂载失败。AVBAndroid Verified Boot2.0则给启动链加上了验证机制。从bootloader到boot镜像、从system到vendor每一级都需要验证哈希。如果任何一个分区的哈希根不匹配系统就会进入recovery或卡在验证失败界面。早期做整机OTA升级时经常遇到这种坑仅更新了vendor分区但vbmeta中记录的哈希没重新计算导致升级后无法开机。6.3 GKI通用内核镜像带来的新玩法Android 12之后GKI成为趋势。Kernel镜像中不再包含特定SoC和board的驱动代码这些代码被移到了vendor模块中。这就意味着Kernel镜像可以真正地跨设备通用厂商只负责提供与GKI内核接口兼容的模块。GKI对启动流程的直接改变是设备树、vendor ramdisk和kernel模块被拆到了vendor_boot分区init的启动路径也变成了先加载vendor_boot中的第一阶段ramdisk然后再加载system ramdisk。这个变化让很多习惯了传统boot分区结构的工程师一时不适应因为以前修改kernel cmdline直接改boot分区即可现在还要同步修改vendor_boot。6.4 经验心得现代Android启动流程调试的几个注意点走到这一步如果你准备在新平台上手启动流程调试我会建议你首先确认的就是手上的设备是否遵循GKI以及vendor_boot与kernel的版本匹配关系。我在支持一个项目时设备因为vendor_boot里的kernel modules与GKI主内核版本不匹配开机阶段反复重启logcat里全是module加载失败的信息。这个问题用传统定位思路很容易忽略因为它看起来像是init脚本错误或者文件系统损坏。其次建议对所有新增rc文件进行语法和SELinux域的提前验证。/system/etc/init/目录下的文件解析失败会导致整个目录被ignore这比某个服务启动失败更难排查。提前用adb shell init --help了解当前平台的init支持哪些诊断命令可以在关键时刻省下大量的定位时间。最后一点也是我最近几年最深刻的体会现代Android的启动阶段Kernel和init之间的边界越来越模糊了像bootconfig、device tree overlay、ueventd和init之间的交互越来越多如果你想真正弄懂启动流程最好把system/core/init、kernel的init/main.c、以及first_stage_mount的实现都放在同一个代码窗口里一起看。只有把它们当成一个整体启动链条里的那些隐蔽问题才会现出原形。