3、Linux内核启动优化:内核裁剪与模块化,DTS(设备树)优化,initcall调优,快速启动模式(fastbootd)原理
好,咱们接着聊内核启动优化。这部分内容,说白了就是让Linux内核在Android系统启动时跑得更快、更稳。我做了这么多年系统优化,内核这块可以说是「兵家必争之地」。你想想看,内核启动占用了整个开机流程中相当可观的一段时间,如果能从这里挤出几秒钟,那用户体验的提升是立竿见影的。
3.1 内核裁剪与模块化
内核裁剪,听起来高大上,其实道理很简单——用不上的功能就别编译进去。我见过不少项目,内核配置直接拿参考板子的来用,里面一堆驱动和子系统根本用不到。结果呢?内核镜像大了,启动时间长了,内存占用也高了。
我个人习惯的做法是:先跑一遍完整的启动流程,然后用trace-cmd或者bootgraph抓一下内核的初始化耗时。你会发现,很多模块的初始化时间其实是可以砍掉的。
举个例子,你的设备用的是eMMC存储,那SATA、NVMe这些驱动就完全没必要编译进去。再比如,你的设备没有以太网口,那网卡驱动也可以去掉。这些看起来不起眼,但积少成多,效果很明显。
我曾经在一个项目中,通过裁剪内核,把镜像大小从12MB降到了7MB,启动时间直接缩短了800ms。怎么做到的?其实就是把那些「可能用得上但实际没用」的驱动都去掉了。
make localmodconfig可以基于当前系统加载的模块生成最小配置。但要注意,这个命令只适用于当前硬件环境,如果换了一台设备,配置可能就不对了。
3.2 DTS(设备树)优化
设备树,也就是DTS,是描述硬件信息的文件。内核在启动时会解析DTS,然后根据里面的信息初始化对应的驱动。DTS的优化,主要就是减少解析时间和精简内容。
嗯,这里要注意一点:DTS里经常会有很多「历史遗留」的节点。比如某个GPIO控制器,在旧版本内核里需要,但新版本已经不需要了。这些节点如果不清理,内核还是会去解析,白白浪费时间。
我建议的做法是:
- 删除未使用的节点:检查每个节点是否真的有对应的驱动在使用
- 合并重复的节点:有些节点可能被多个设备引用,可以合并成一个
- 使用
status = "disabled"标记不用的节点:这样内核会跳过它们
另外,DTS的编译选项也很重要。我建议使用DTC_FLAGS=-@来生成带符号的设备树,这样在调试时更方便。但生产环境可以考虑去掉这个选项,能稍微减小dtb文件的大小。
of_find_node_by_name间接引用了。
3.3 initcall调优
initcall是内核初始化过程中调用的函数。内核启动时,会按照一定的顺序执行这些initcall。调优的目标就是让那些「不着急」的initcall往后排,让「着急」的往前排。
内核的initcall分为几个等级:
| 等级 | 宏定义 | 说明 |
|---|---|---|
| 0 | pure_initcall |
最早期,用于核心子系统 |
| 1 | core_initcall |
核心驱动和子系统 |
| 2 | postcore_initcall |
核心之后的初始化 |
| 3 | arch_initcall |
架构相关 |
| 4 | subsys_initcall |
子系统初始化 |
| 5 | fs_initcall |
文件系统 |
| 6 | device_initcall |
设备驱动(最常用) |
| 7 | late_initcall |
最晚,用于非关键功能 |
调优的思路很简单:把那些启动时不需要的驱动放到late_initcall里。比如蓝牙、Wi-Fi这些,完全可以等系统启动完成后再加载。
我个人的做法是:先用initcall_debug内核参数启动,然后分析日志,找出那些耗时较长的initcall。如果它们不是启动必需的,就调整它们的等级。
# 在kernel cmdline中添加initcall_debug
# 然后抓取内核日志
dmesg | grep "initcall" | sort -k5 -n -r | head -20
这样就能看到哪些initcall最耗时。我曾经发现一个USB驱动的initcall花了200ms,但那个USB口根本没人用。果断把它改成了late_initcall,启动时间直接少了200ms。
device_initcall改到late_initcall,结果导致另一个依赖它的驱动初始化失败。嗯,后来花了半天才排查出来。
3.4 快速启动模式(fastbootd)原理
fastbootd,说白了就是Android系统里的一个「轻量级启动模式」。它跟传统的fastboot不一样,fastboot是在bootloader里运行的,而fastbootd是在内核启动后、在用户空间里运行的。
为什么要搞这么个东西?原因很简单:有些操作在bootloader里做不了。比如动态分区、逻辑卷管理这些,bootloader根本不懂。fastbootd的出现,就是为了解决这个问题。
它的工作原理是这样的:
- 系统启动时,如果检测到需要进入fastbootd模式,就会加载一个精简的内核
- 然后启动一个最小化的用户空间,里面只包含fastbootd服务
- fastbootd会监听USB命令,处理刷机、分区等操作
- 操作完成后,重启进入正常模式
从优化角度看,fastbootd的启动速度也很重要。毕竟用户刷机时,谁也不想等太久。
我建议的做法是:
- 使用最小化的内核配置:只保留fastbootd需要的驱动,比如USB、存储、分区相关
- 精简用户空间:只包含fastbootd及其依赖的库,其他的一概不要
- 使用ramdisk:把整个用户空间打包成ramdisk,加载速度比从存储设备读取快得多
我曾经优化过一个项目,fastbootd的启动时间从原来的3秒降到了1.2秒。怎么做到的?其实就是把内核里那些用不到的驱动都去掉了,然后用户空间只保留了必要的二进制文件。
好了,内核启动优化这部分就聊到这里。记住,优化的核心不是「做更多」,而是「做更少」。去掉不需要的,精简必要的,你的启动时间自然就降下来了。