
提到 Android 和 Linux 的启动过程很多人的第一反应是Android 不就是跑在 Linux 内核上吗启动流程应该大差不差吧。这句话对了一半。两者确实共享同一套内核机制但 Android 在用户空间那一层做了几乎是重构级的改造导致从按下电源键到看见桌面的这段旅程走的是另一条完全不同的路。这篇文章我从底层固件一直拆到用户空间把 Linux 和 Android 的启动过程放在一张桌上对比着看。适合做系统开发、嵌入式移植、App 层疑难问题排查的人也适合准备面试想系统梳理一遍启动链路的同学。读完你不仅能说清楚它俩哪儿像、哪儿不像还能知道出了问题该去哪一层抓日志、怎么定位。1. 先搞明白Linux 和 Android 的启动到底有啥关系1.1 Android 不是另一个 Linux内核同源框架各异先说结论Android 用的确实是 Linux 内核这一点没有争议。不管是高通、联发科还是麒麟芯片跑在底层负责进程调度、内存管理、驱动控制的那部分就是一套经过厂商深度定制的 Linux 内核。但用 Linux 内核不等于是个 Linux 发行版。真正的 Linux 发行版比如 Ubuntu、Debian、CentOS从内核到用户空间是一脉相承的体系内核启动完接着跑 init/systemd加载一堆系统服务然后给你一个 shell 或者图形登录界面。Android 把这条链路拦腰切断了自己不按这套来。它保留内核但在内核之上自己造了一套用户空间自己的 init、自己的属性系统、自己的进程孵化机制、自己的服务管理框架。所以你在服务器上熟悉的 systemd、bash、getty在 Android 里一概没有对应位置上站着的是 zygote、SystemServer、binder 这套东西。理解了这个前提再去看启动过程就不会被都是 Linux这句话误导。1.2 一次启动要经历的几个大阶段把两套系统放在同一套坐标系里看启动过程其实都能切成四个大阶段固件与引导加载器阶段CPU 上电后执行固化在芯片里的 BootROM 代码BootROM 去加载 bootloaderbootloader 再把内核镜像搬到内存里。内核初始化阶段内核解压自身初始化 CPU、内存、中断、设备驱动最后挂载根文件系统。用户空间首个进程接管内核启动完成后交出控制权执行第一个用户进程。Linux 下通常是 /sbin/initsystemdAndroid 下是 /init。系统服务与界面启动阶段第一个进程拉起后续的服务树最终呈现出可交互的系统。这四个阶段里Linux 和 Android 在前两个阶段高度相似差异从第三个阶段开始急剧扩大。后面两章我会把每一步的关键代码路径和背后逻辑都拆开讲。1.3 为什么要花力气研究启动过程启动过程不是只在刷机变砖、服务器开不了机的时候才有用。日常开发里开机慢是性能优化的永恒主题——Android 上你要分析冷启动时间就得知道 Zygote preload 了什么、SystemServer 里哪些服务拖着开机进度Linux 服务器上你要调优启动速度就得能看懂 systemd 的依赖关系和谁在拖后腿。另外很多疑难 bug 的根源就在启动期。比如某个服务启动顺序不对导致开机概率性失败、SELinux 策略在早期没有加载导致大量 avc denial、设备节点没被正确创建导致驱动访问异常——这些都得靠扎实的启动流程知识去定位。把启动链路吃透相当于拿到了整个系统问题排查的主干地图。2. Linux 启动过程深度拆解从按下电源键到登录界面2.1 第一阶段固件与引导加载器BIOS/UEFI - GRUBLinux 在 PC/服务器这条路径上的启动起点是固件。老一点的主板走 BIOS新一点的主板走 UEFI但干的活是一样的完成上电自检POST初始化最基础的硬件CPU、内存控制器、存储控制器然后去寻找一个可引导的设备。BIOS 的做法是扫描硬盘主引导记录MBR把 MBR 前 446 字节里的引导代码加载到内存执行UEFI 的做法更现代化一些它直接读取 EFI 系统分区ESP里的 .efi 引导程序。如果你装过双系统应该见过开机时那个蓝色、紫色的 GRUB 菜单那就是 bootloader——它负责把 Linux 内核从磁盘搬到内存并且把启动参数传给内核。这一步里有几个实际排查中经常踩的坑MBR 被覆盖装 Windows 时 GRUB 被干掉开机直接进不去 Linux这是双系统最常见的故障。ESP 分区格式不对UEFI 只认 FAT32 格式的 ESP 分区分区表损坏会导致找不到引导项。Secure Boot 不兼容某些主板开着 Secure Boot 又没注册签名密钥GRUB 直接拒载。在服务器领域bootloader 除了 GRUB 还有 LILO老古董、syslinux、以及嵌入式里非常常见的 U-Boot。但 x86 平台上 99% 的实际场景就是 GRUB2它读取 /boot/grub/grub.cfg解析 menuentry把 vmlinuz 内核镜像和 initramfs 镜像加载进内存然后跳转到内核入口。2.2 第二阶段内核初始化的关键路径内核被加载进内存后先从 arch/x86/boot/ 下的汇编代码开始执行做一段短暂的实模式初始化然后切换到保护模式解压内核自身。如果你见过开机瞬间屏幕上滚动的字符那基本就是内核在自我解压和初始化过程中打印的日志。解压完成之后真正的初始化主线在start_kernel()函数里。这个函数是内核启动的总调度室干的是整个系统最底层的活setup_arch()解析启动参数、初始化处理器相关配置、建立内存分页表build_all_zonelists()建立内存管理所需的 zone 列表mm_init()初始化内存管理子系统包括 slab 分配器sched_init()初始化进程调度器init_IRQ()建立中断描述符表calibrate_delay()计算出 BogoMIPS这个老参数现在作用不大了但流程还在。这一阶段内核打印的日志通过dmesg命令随时可以查看。排查硬件驱动问题、内存问题基本都从这里找线索。start_kernel()最后会调用rest_init()在这里创建第一个内核线程kernel_init也就是未来的 PID 1 进程。内核线程不是用户进程但它后续会执行/sbin/init完成从内核态到用户态的转换。这一步有个经典的机制叫initramfs内核会把内存里的 initramfs 解压成临时根文件系统运行其中的 /init 脚本加载真正根分区需要的文件系统驱动然后再切换根目录switch_root到真实的根分区。2.3 第三阶段init 进程与用户态接管从这一刻起内核的独角戏结束控制权交给用户空间。PID 1 进程登场。传统 SysVinit 时代它读取 /etc/inittab按照 runlevel 依次执行 /etc/rc.d/rcX.d/ 下的脚本当代主流发行版基本都换成了 systemdPID 1 就是 systemd 本体。systemd 的启动逻辑和传统 init 有本质区别它把启动服务这件事变成了启动目标target。比如开机默认进入default.target它依赖multi-user.target后者又依赖basic.target、sysinit.target……systemd 不再是按顺序机械地执行脚本而是把一堆 unit 按照依赖关系并行拉起。这也是为什么现在发行版开机比十几年前快一大截——脚本等串行步骤被并行替代了。systemd 启动过程里有一串非常关键的单元路径systemd-udevd内核 uevent 的守护进程动态创建 /dev 下的设备节点systemd-journald日志服务从此刻起所有日志统一走 journalsystemd-logind管理登录会话network.service或者NetworkManager拉起网络栈gettytty1在终端上显示登录提示符。分析启动速度我强烈建议用systemd-analyze工具族。systemd-analyze blame能直接列出每个服务耗时systemd-analyze critical-chain能看到关键链条上谁最慢。之前我优化一台老服务器的启动时间就是用 critical-chain 发现某个 NFS 挂载服务在等待一个无法响应的远程地址超时 90 秒——换个网络配置后启动时间从 110 秒直接降到 25 秒效果立竿见影。2.4 Linux 启动过程的关键排查命令把 Linux 启动排查命令整理成一个速查表实际运维和日常使用中非常有用排查目标命令说明查看内核启动日志dmesg或journalctl -k -b看硬件识别、驱动加载、内核报错查看本次启动的用户态日志journalctl -b只看本次开机产生的日志查看启动耗时排名systemd-analyze blame定位最拖沓的单元查看关键依赖链路systemd-analyze critical-chain找关键路径上的瓶颈查看上一次启动失败的服务systemctl --failed快速定位启动失败项追踪新设备加入事件udevadm monitor观察内核 uevent注意一个问题dmesg显示的环形缓冲区是有限的早期启动日志可能被后来的日志冲掉。如果遇到启动早期就崩溃的情况建议在内核启动参数里加earlyprintk或者用串口抓日志consolettyS0不然你会像盲人摸象一样什么都看不到。另外日志分内核态和用户态两层dmesg只看得到内核态用户态服务的报错要看journalctl别搞混了。3. Android 启动过程深度拆解从 BootROM 到桌面图标3.1 第一阶段BootROM 与两级引导加载器Android 设备没有 BIOS 也没有 UEFI它的固件直接固化在 SoC 内部这段代码叫 BootROM也叫 Primary Bootloader。BootROM 不可改写是芯片出厂时写死的它的职责只有一个验证并加载下一级引导代码。现代手机 SoC 的引导通常分好几级BootROM 先加载一个极小的 SBLSecondary Bootloader到芯片内部 SRAMSBL 初始化 DDR 内存控制器然后把大块头的引导程序比如高通的 ABL、U-Boot加载到内存。如果你了解过 Android 玩机圈你应该听过9008 模式EDL 模式fastboot 模式这些名词它们本质上是 bootloader 在不同阶段提供的救援/调试入口。Android 的 bootloader 阶段和 Linux/PC 的 GRUB 时代有一个关键差异Android 引入了可信启动链。每一级引导代码都要验证下一级的签名防止系统被篡改。所以刷机时经常遇到的问题——旧版本的 bootloader 刷不进新系统——往往不是技术限制而是签名校验层根本不让你过。bootloader 最终要加载的镜像叫 boot.img。它由mkbootimg工具打包结构大致是boot header 内核镜像 ramdisk 根文件系统镜像。ramdisk 里放着 Android 的 /init 程序和 init.rc 配置文件这是与普通 Linux initramfs 最大的不同——普通 Linux 的 initramfs 只是为了过渡用完就切走Android 的 ramdisk 是真正长期使用的根文件系统。3.2 第二阶段内核启动与 rootfs 挂载Android 内核的启动路径和标准 Linux 内核几乎完全一样一样经过自解压、start_kernel()、初始化内存管理、调度器、中断系统。既然底层相同为什么 Android 设备上很少看到像服务器那样滚屏的启动日志答案不是内核不打印而是 Android 的设备节点通常没有串口而且内核日志默认被 loglevel 压得很低或者干脆输出到 pstore 里等你死后去捞。真正拉开差距的是 rootfs 的形态。普通 Linux 是临时 initramfs 过渡到真实根分区Android 则是直接把 ramdisk 作为根文件系统跑起来然后在启动过程中逐个挂载 /system、/vendor、/data 等分区。注意Android 的 /system 用的文件系统从早期 ext4 到现在的 erofs、f2fs都能在挂载时加 dm-verity 校验——这也是 Android 安全模型比一般 Linux 发行版激进很多的地方。内核启动期间有一段逻辑是和安卓特定相关的android_dt解析设备树上的 bootconfig处理 cmdline 中诸如androidboot.*系列属性把厂商需要的参数传给用户空间。这些androidboot.serialno、androidboot.hardware最终会变成 Android 属性系统里只读的 ro.boot.* 属性驱动后续硬件加载逻辑。3.3 第三阶段Android init 与 init.rc 脚本内核初始化完成后执行根目录下的 /init。这个 init 不是 systemd不是 SysVinit它是一个专门为 Android 重写的、轻量级的用户空间启动器。它干的事有两层第一层是系统基础行为挂载 /proc、/sysfs、/devtmpfs 等基础文件系统创建并初始化 /dev 下的关键设备节点启动 property service属性服务这是一套全局键值存储系统所有进程都能通过getprop/setprop读写启动 ueventd监听内核 uevent 事件动态创建设备节点。第二层是解析并执行/init.rc文件。init.rc 用的是 Android Init Language语法和 Shell 大不一样有on trigger的触发块有service name path的服务定义块。整个文件按阶段编排比如on early-init、on init、on fs、on boot这些阶段每个阶段干不同的事情on fs mount_all /system/etc/fstab setprop ro.crypto.stateunencrypted service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server class main socket zygote stream 660 root system onrestart restart zygote如果你要给 Android 系统加一个自启动服务最正统的方式就是往 init.rc 体系里加一段 service 定义。这里有一个必须注意的点Android 的 init 对服务的运行用户、SELinux 上下文有要求不是随便加上就能跑起来——我见过很多人往 init.rc 里塞脚本结果开机后服务被 SELinux 拦住日志里全是 avc: denied。3.4 第四阶段Zygote 孵化与 SystemServer 启动init 启动的所有服务里最重要的就是 zygote。它的全名是 app_process一个专门为 Android 优化的进程。zygote 这个名字的意思是受精卵因为它负责孵化出后续的一切应用进程。Zygote 的启动过程本身是纯 native 的先初始化 Android RuntimeART注册一堆系统服务需要的 JNI 方法预加载各种类库和系统资源然后开启一个 socket 监听端口。之后它要做最关键的一步fork 出 SystemServer原名 system_server。这里有一个值得思考的问题为什么 Android 不直接让 init 启动 SystemServer非要经过 zygote 层答案在于效率。Android 上的每一个 App 本质都是一个跑在 ART 虚拟机上的进程而每次启动虚拟机是个昂贵的操作。zygote 先把 ART 虚拟机拉起一次预加载好所有核心库和资源后续 App 通过 fork 复制 zygote 的进程镜像继承预热好的虚拟机环境——这就是 Android 应用能够快速冷启动的核心机制也是写时复制COW技术在系统设计里的经典应用。SystemServer 是 Android Java 世界的第一个进程。它启动后做的工作可以列一长串启动 Binder 线程池、初始化 ActivityManagerService、PackageManagerService、WindowManagerService、PowerManagerService 等上百个系统服务服务。这些服务全部注册到 ServiceManager 里供上层 App 通过 Binder 调用。等所有服务就绪SystemServer 会发出 BOOT_COMPLETED 广播引导启动 Launcher——就是你的桌面。至此你看到了图标和壁纸设备算是真正开机完成了。3.5 Android 特有的 property service 和 ueventd这两个组件在标准 Linux 里找不到对应物但它们恰恰是理解 Android 启动过程的关键细节。Property Service属性服务是一套朴素的全局键值存储系统提供类似 Windows 注册表的机制。系统里所有配置信息从版本号、设备型号到当前 USB 状态都保存在属性里。它在 init 启动早期就建立了一块共享内存区域然后用 socket 对外提供读写接口。为什么不用配置文件因为配置文件在嵌入式设备上读写效率低、权限难管理、进程多了容易出并发问题。属性服务用共享内存 单一写者init的模型把并发风险降到最低。ueventd 则负责监听内核的 uevent 消息。内核每发现一个新设备都会发出 uevent 事件ueventd 收到后根据 /sys 目录下的信息在 /dev 下创建对应的设备节点。这一步对应标准 Linux 里 udev 的工作但 Android 不需要那么复杂的规则引擎autodev 机制 硬编码的设备权限表就够用了。做外设驱动调试时如果发现 /dev 下设备节点没出现先看 ueventd 有没有收到事件再看权限对不对基本能解决大半问题。4. 关键差异对比同样的内核不同的设计哲学4.1 启动差异对照表把两个系统的启动过程放在一个表格里差异一目了然对比维度Linux 发行版Android固件层BIOS/UEFIBootROM 多级 bootloader引导加载器GRUB2 为主高通/MTK 私有 bootloader fastboot内核标准 Linux 内核深度裁剪添加厂商驱动的内核根文件系统加载initramfs 过渡后 switch_rootramdisk 作为长期 rootfs第一个用户进程systemd或 sysvinit/initAndroid 专用服务管理systemd unit 依赖图并行启动init.rc 阶段化触发按序启动应用进程启动直接 exec 可执行文件Zygote fork 虚拟机进程日志体系journald / sysloglogcatSELinux通常 permissive 或半启用enforcing 强制模式设备节点管理udev 动态规则ueventd规则硬编码这个表格基本概括了两边从底层到顶层的全貌。核心的差别信号不是某一个点而是整体设计哲学的不同Linux 发行版追求通用性和灵活性服务管理、设备管理、日志体系都是一套通用的体系Android 追求定制的可控性和移动场景的适配性每个环节都被压扁、固化、优化到了特定用途上。4.2 为什么 Android 要把 init 改得面目全非很多人会问既然 Linux 有现成的 systemd为什么 Android 不用这里有几个层面的原因。历史原因Android 诞生于 2008 年前后那时 Linux 世界的标准 init 是 SysVinitsystemd 还没出现。Android 团队当时自研了一套 init到现在已经有十几年的连续演进形成了自己的体系和生态没有必要也不会轻易切换。技术原因systemd 的目标是通用 Linux 系统的复杂服务管理它要管理网络、登录会话、用户组、日志后端、设备热插拔还有很多依赖关系解析。但 Android 的启动场景摸清楚就是固定的那么几件事挂载几个分区、守护几个核心服务、把系统属性管好。用 systemd 这种重框架纯属杀鸡用牛刀还会引入 GPL 传染和启动时延。可控性原因Android 的设备碎片化严重各家厂商内核和硬件千差万别。自研 init 让 Google 和厂商能更精细地控制启动时序每一阶段做什么、每个服务的依赖是什么都是显式声明的。这种确定性对移动设备很关键——你不想让某个后台服务因为依赖顺序不确定性导致概率性开机失败。4.3 HAL 与 BinderLinux 没有的 Android 特色启动链路推进到用户空间后Android 有另外两个很显眼的特色组件——HAL 和 Binder。HAL硬件抽象层架在系统服务与内核驱动之间。内核设备驱动暴露的是通用的 Linux 接口但 Google 希望上层 Java 服务不直接依赖底层接口所以加了一层 HAL 把所有硬件能力转换成统一的接口。启动时系统服务通过 HAL 代理加载对应厂商的 .so 库。这层设计让厂商可以在不泄露驱动源码的情况下适配 Android 框架也直接影响了启动顺序——某些服务必须等 HAL 就绪才能继续。Binder 则是 Android 进程间通信的命脉。它替代了标准 Linux 大量使用的 D-Bus 和 socket IPC走内核的 /dev/binder 驱动用一次拷贝one-copy实现进程间数据传递。Binder 本身不参与影响启动过程但它决定了 SystemServer 启动后整个系统的通信骨架。排查 Android 应用 ANR 或者系统服务交互问题不理解 Binder 模型会很吃力。4.4 SELinux 在 Android 中的强制应用Linux 发行版里 SELinux 经常是半睡半醒的状态很多系统默认 permissive只记录不拦截。Android 则从 5.0 开始全面强制enforcing所有进程、所有文件访问都必须过 SELinux 这一关。这一点对启动过程影响巨大。init 在启动早期就要加载 SELinux 安全策略文件sepolicy然后整个用户空间的服务启动都要遵循该策略。比如 zygote 要从某个目录读文件如果 SELinux 上下文不对就会直接被拒表现为开机死在某个阶段或者出现诡异的功能缺失。排查技巧用adb shell dmesg | grep avc就能看到被拒绝的审计日志格式一般是avc: denied { read } for pidxxx commxxx namexxx。之前我调一个系统应用权限问题就是靠刷 permissive 内核确认是 SELinux 拦截然后根据 avc 日志补策略规则最终才让服务正常跑起来。遇到 Android 启动问题先别管别的第一件事先看 avc denial。5. 启动问题排查与实测心得5.1 Linux 启动慢问题出在哪Linux 启动慢的问题绝对是最常见的生产事故之一。慢的原因无非这么几类某个服务在等待超时尤其是网络相关服务经典的是 NFS 等待、DNS 反查、DHCP 等待磁盘性能问题机械盘 加密分区组合起来启动奇慢无比服务依赖写得不合理明明可以并行却在等无关的东西脚本里有 sleep 或者查询远程接口的逻辑。按我的实战顺序先systemd-analyze blame拿到耗时榜再systemd-analyze critical-chain看关键链条。timeout 类问题在日志里会有明显特征大多数情况下你会看到某个 service 的启动状态是 120 秒的超时等待。如果是老的 SysVinit 系统bootchartd也可以生成启动图表不过现在用 systemd 环境直接用上面的命令即可。5.2 Android 卡开机动画 / 无法开机怎么查Android 开机问题比 Linux 更黑盒因为设备上通常没有方便的终端和串口。常见的几种表现卡在开机第一屏就是厂商 logo 那屏大概率在 bootloader 或内核早期就挂了。此时先回忆最近改了什么是不是 boot.img 刷坏了、内核引导参数不对、签名校验没过。卡在开机动画不是第一屏而是动态 logo 转圈/跑动中Android 系统用户空间已经起来了但某个关键服务起不来。重点查 logcat 里 zygote 和 system_server 相关报错注意看 avc denial。开机后黑屏但能连接 ADB那就是 SystemServer 起来了但 WindowManager 或 Launcher 出问题adb logcat -b system里会有详细异常。没有 ADB 的时候怎么抓日志开发板上找串口很多 SoC 的 bootloader 阶段会初始化一个调试串口接上 USB-TTL 就能看到从 BootROM 开始的全量日志。没有串口又不开机那就只能靠内核的 pstore 机制了/sys/fs/pstore/下会有上次启动的 ramoops 日志。这些都是真实的嵌入式开发场景里救命的东西。5.3 几个实用工具和经验总结启动过程调试有几个工具是我强烈建议提前装好、提前学会的Linux 端systemd-analyze全家桶 dmesgjournalctl -xb。journalctl 的-b参数区分启动次数的用法很值得玩味配合-b -1看上一次启动。Android 端adb logcat -b all、adb shell getprop | grep boot、adb shell dmesg。判断开机完成的硬指标是sys.boot_completed属性是否为 1它被设置的时间点就是开机耗时的终点。内核调试bootchart工具Android 早期版本支持虽然现在不流行了但它生成的启动进程时序图至今依然有参考价值。现在 Android 高版本也可以开persist.sys.zygote.preload_common_resources这类开关观察 preload 影响。我个人一个比较深的体会是Android 的某个服务在开机时起不来不要把锅甩给运气先去看是不是 SELinux 策略没配对再看是不是依赖的某个属性还没 set 好。有一次我调试一个厂商自带的守护进程老是在开机后莫名其妙地死掉排查了快两天才发现是服务在 init.rc 的on boot阶段启动但某个 it depends on 的属性要等到on property:xxx1才会 set这就存在一个时序竞争。后来把服务的启动时机改成监听属性触发问题马上消失。Linux 那边我踩过的最大一个坑是日志淹 death。内核日志环形缓冲区默认只有几十 KB启动时大量驱动刷日志会把早期关键信息覆盖掉。所以线上环境建议在内核 cmdline 里加log_buf_len8M甚至更大或者直接想办法上串口抓完整日志。另外任何启动问题第一件事是把当前系统状态完整记录下来再动手改配置我见过太多人一开机失败就开始瞎试结果把原来还能进系统的问题改成彻底进不了系统。结尾研究启动过程这件事最大的收获其实是建立了系统分层的直觉——固件、bootloader、内核、init、服务框架、上层应用每一层都有自己独立的日志和问题域。Linux 和 Android 虽然内核同源但从第一个用户进程开始分道扬镳各自的排错姿势也完全不一样。遇到启动问题先别慌看现象、分层次、抓日志然后在这一层的设计逻辑里找原因大部分问题都不会难到哪儿去。我个人到现在还会保留一个习惯每次拿到新设备第一件事就是把从按下电源键到能操作的完整日志留一份归档这些信息在未来的某次故障排查中往往就是最宝贵的线索。