4、内核裁剪方法论:裁剪原则与策略、功能模块裁剪、文件系统裁剪、驱动裁剪

内核裁剪这事儿,说白了就是做减法。我刚开始接触Tina Linux时,总想把所有功能都塞进去,结果编译出来的内核镜像快赶上我半个硬盘了。后来被产品经理追着要「启动速度再快2秒」,才真正开始研究怎么科学地做裁剪。

你想想看,一个完整的内核源码树里,真正跑起来能用到的代码可能不到20%。剩下的那些,要么是你不用的硬件驱动,要么是永远用不上的网络协议栈。裁剪的目标,就是把这80%的「脂肪」减掉,只留下能跑业务的「肌肉」。

核心原则:裁剪不是删代码,而是配置管理。内核源码不动,只通过Kconfig和Makefile控制哪些代码被编译进去。

内核裁剪知识体系 内核裁剪方法论 裁剪原则与策略 功能模块裁剪 文件系统裁剪 驱动裁剪 最小化原则 模块化原则 可追溯原则 分层裁剪 网络协议栈 文件系统支持 调度器选项 内存管理 只读根文件系统 SquashFS initramfs busybox 字符设备 块设备 网络设备 I2C/SPI/UART 裁剪流程:需求分析 → 配置裁剪 → 编译验证 → 功能测试 → 迭代优化

4.1 裁剪原则与策略

先说说原则。我个人的经验是,裁剪之前先问自己三个问题:这个功能真的需要吗?能不能用模块代替?去掉之后影响面有多大?

三个核心原则:

  • 最小化原则:只编译当前硬件平台必须的功能。比如你的板子只有以太网,就别把Wi-Fi驱动编进去。
  • 模块化原则:能编成模块的就别编进内核。模块可以按需加载,启动时少占内存。
  • 可追溯原则:每次裁剪都做记录。我曾经因为删了一个看起来「没用」的驱动,结果整个USB子系统罢工了,查了两天才找到原因。

我的习惯:每次裁剪前,先用make savedefconfig生成当前配置的快照。裁剪出问题后,对比两个defconfig文件,一眼就能看出改了哪里。

裁剪策略上,我建议分四步走:

  1. 硬件摸底:列出板子上所有外设的型号和接口类型。这一步别偷懒,我见过有人凭印象裁剪,结果漏掉了板载音频Codec,产品量产了才发现没声音。
  2. 功能分级:把内核功能分成「必须」「可选」「不需要」三级。必须的包括CPU架构、内存管理、中断控制器;可选的比如网络协议栈、文件系统;不需要的就是那些你永远用不上的。
  3. 逐层裁剪:从顶层功能往下走。先关掉整个子系统(比如CONFIG_NET),再细化到子功能(比如CONFIG_IPV6)。
  4. 迭代验证:每裁剪一轮,编译一次,跑一遍基本功能测试。别想着一次到位,我试过一口气关了50个选项,结果编译都过不了。

4.2 功能模块裁剪

功能模块裁剪是重头戏。Tina Linux基于Linux内核,Kconfig里几千个选项,你得知道哪些能关、哪些不能动。

常见的可裁剪模块:

模块 裁剪建议 典型节省空间
网络协议栈 只用IPv4就关掉IPv6 ~200KB
文件系统 只保留ext4和squashfs ~150KB
调度器 单核CPU用CFS就够了 ~50KB
电源管理 没有电池的设备关掉ACPI ~100KB
安全模块 关掉SELinux/AppArmor ~80KB

举个例子,我在一个IoT网关项目里,只需要基本的TCP/IP通信。我把CONFIG_IPV6、CONFIG_BRIDGE、CONFIG_NETFILTER全关了,光网络子系统就省了将近500KB。

注意:关掉CONFIG_SYSFS或CONFIG_PROC_FS要谨慎。很多用户态工具依赖这两个虚拟文件系统,关了可能导致系统无法正常启动。

实际操作步骤:

# 1. 进入内核配置界面
make menuconfig

# 2. 关闭不需要的网络功能
Networking support → Networking options → 
  [ ] IPv6 protocol
  [ ] 802.1d Ethernet Bridging
  [ ] Network packet filtering

# 3. 精简文件系统支持
File systems → 
  [ ] Second extended fs
  [ ] Ext3 journalling file system
  [*] Ext4 journalling file system
  [ ] Btrfs
  [ ] XFS
  [*] SquashFS

# 4. 保存配置
make savedefconfig

4.3 文件系统裁剪

文件系统裁剪分两层:内核里的文件系统驱动,和根文件系统本身的内容。

内核层:只保留你实际用的文件系统类型。嵌入式设备常用SquashFS做只读根文件系统,配合overlayfs做可写层。我一般只保留ext4、squashfs和overlayfs,其他全关。

根文件系统层:这才是大头。一个完整的Linux根文件系统动辄几百MB,但嵌入式设备可能只需要几MB。

我常用的裁剪手段:

  • 用busybox代替coreutils:busybox一个二进制文件就能提供ls、cp、mv等几十个命令,比分开的coreutils省了至少5MB。
  • 删掉man pages和文档:产品设备上谁看文档?删掉能省1-2MB。
  • 只保留必要的库文件:用ldd检查每个二进制依赖哪些动态库,没用的库删掉。
  • 使用initramfs:把根文件系统压缩后跟内核打包在一起,启动时解压到内存里,省掉挂载块设备的步骤。

避坑指南:我曾经为了省空间,把/lib/modules里的内核模块全删了。结果插上一个USB设备时,系统报modprobe: module not found。后来我学乖了,至少保留跟当前硬件相关的模块。

一个精简的根文件系统结构:

/
├── bin/          # busybox + 几个关键工具
│   ├── busybox
│   ├── sh -> busybox
│   └── init -> busybox
├── sbin/         # 系统管理工具
│   ├── init
│   ├── mount
│   └── ifconfig
├── etc/          # 配置文件
│   ├── inittab
│   ├── fstab
│   └── init.d/
├── lib/          # 动态库
│   ├── libc.so.6
│   ├── ld-linux.so.3
│   └── modules/  # 内核模块(按需保留)
├── dev/          # 设备节点
├── proc/         # 挂载点
├── sys/          # 挂载点
└── tmp/          # 临时文件

这个结构大概2-3MB,启动时间能控制在3秒以内。

4.4 驱动裁剪

驱动裁剪是最容易出问题的地方。为什么?因为驱动跟硬件绑定,你删掉一个驱动,可能某个外设就彻底不工作了。

驱动裁剪的原则:

  • 只保留板载外设驱动:你的板子上有什么芯片,就留什么驱动。比如用RTL8188 WiFi芯片,就把其他WiFi驱动全关掉。
  • 关掉未使用的总线驱动:如果板子没有PCIe接口,关掉CONFIG_PCI。没有USB Host,关掉CONFIG_USB。
  • 驱动编成模块:不常用的驱动编成模块,启动时不加载,需要时再insmod。

我曾经踩过的坑:在一个项目里,我把CONFIG_I2C关掉了,因为觉得板子上没有I2C设备。结果系统启动到一半就挂了——原来板载的PMIC(电源管理芯片)是通过I2C通信的。从那以后,我每次裁剪驱动前都会先看一遍原理图。

驱动裁剪的检查清单:

驱动类型 检查项 常见陷阱
字符设备 串口、GPIO、SPI、I2C PMIC、传感器可能依赖I2C
块设备 MMC/SD、NAND Flash、SATA 根文件系统挂载需要对应驱动
网络设备 以太网PHY、WiFi、蓝牙 PHY驱动常被忽略
输入设备 触摸屏、按键、鼠标 触摸屏需要input子系统
多媒体 音频Codec、摄像头、显示 V4L2、ALSA框架不能少

实际操作:

# 查看当前加载的驱动
lsmod

# 查看硬件设备占用的驱动
cat /proc/devices
cat /proc/bus/input/devices

# 在menuconfig中搜索驱动
make menuconfig
# 按 / 键搜索,比如搜索 "rtl8188"

嗯,这里要注意:驱动裁剪完一定要做功能测试。我习惯写一个测试脚本,把板子上所有外设都跑一遍。比如测试串口就发一段数据,测试WiFi就ping一下网关。只有测试通过了,才敢说裁剪完成。