Linux 内核开发指南
编制日期 2026-10-05
BSP 总文档 · 覆盖环境搭建 / 配置裁剪 / 设备树 / 驱动开发 / 调试调优 / 测试发布全流程 · 驱动专项文档为子文档挂在第 6 章下
这份文档解决的是「一个 BSP 团队如何把内核从源码变成可量产固件」的问题: 它不教你怎么写某一个驱动(那是第 6 章下挂的专项子文档),而是把 环境 → 配置 → 设备树 → 驱动 → 文件系统 → 调试 → 调优 → 测试 → 发布串成一条可复用的流水线, 并把每一步的基线(源码基线 / 配置基线 / 交付基线)固化下来。 新人按 1→15 章通读一遍即可上手;老手直接翻第 15 章速查与附录 F 检查清单。 第 17–23 章是从「能跑」到「能量产、能长期维护」的七块拼图: 硬件 Bring-up、存储子系统深度、安全启动、量产支持、CI/CD、可观测性、内核生命周期—— 按项目阶段取用,不必通读。
BSP 工程师:第 2、4、5、7、14 章是主战场;附录 A/B/C 是日常手册。
驱动工程师:第 3、5、6 章 + 对应专项子文档;第 8 章决定你定位问题的速度。
测试工程师:第 13 章全套用例 + 第 15 章排查入口;附录 D 的脚本可直接复用。
硬件工程师:第 5.6 节(GPIO 复用 / 电源域 / 中断冲突)与第 10、11、17 章必须看, 多数「驱动调不通」最后都定位到硬件设计。
新板首次点亮:直接翻第 17 章,按六步顺序走,不要凭感觉跳级。
要交付量产 / 要过安全合规:第 19、20、21、23 章是一组,缺一项都会在后期付出代价。
这份文档的三条硬约束
| 约束 | 具体要求 | 落地位置 |
|---|---|---|
| 配置基线 | 永不直接提交 .config,一律 make savedefconfig 生成 defconfig 入仓 | 第 4.2 节 |
| 源码基线 | 内核源码来源、分支、补丁集三要素可追溯,每个发布版本有 tag | 第 1.4、14.1 节 |
| 交付基线 | 发布前过一遍检查清单,产出 changelog + Known Issues + 交付物清单 | 第 14.4、14.5 节、附录 F |
与其它文档的分工
| 文档层级 | 覆盖范围 | 典型例子 |
|---|---|---|
| 本指南(总文档) | 内核全流程:环境、配置、设备树、驱动、文件系统、调试、调优、测试、发布 | 本文档 |
| 驱动专项子文档 | 单一器件的驱动实现细节:寄存器、命令序列、MTD 对接、时序 | SPI NAND 驱动开发指南 |
| 硬件设计指南 | 原理图、电源、封装、信号完整性 | SD NAND 电路设计指南 |
| 市场 / 选型报告 | 容量档位、应用场景、厂商格局 | 存储芯片应用市场分析 |
第 1 章 文档总览
这一章先把边界划清楚:这份文档给谁用、基于什么平台与版本、用到哪些术语、内核源码从哪来。边界不清是 BSP 项目后期返工的头号原因。
1.1 编写目的、适用范围与读者
本指南用于规范嵌入式 Linux 产品内核层(BSP)开发的全流程动作与交付标准, 目标是让不同背景的工程师在同一套基线上协作,并把「个人经验」沉淀成「团队可复用的流程」。
| 角色 | 关注重点 | 必读章节 |
|---|---|---|
| BSP 工程师 | 环境、配置裁剪、设备树、启动、镜像打包、版本发布 | 第 2、4、5、7、14 章 |
| 驱动工程师 | 总线模型、驱动框架、资源管理、调试定位 | 第 3、5、6、8 章 |
| 测试工程师 | 测试用例、性能基准、稳定性方法、回归范围 | 第 13 章、附录 D |
| 硬件工程师 | 引脚复用、电源域、中断分配、启动介质、功耗 | 第 5.6、10、11 章 |
| 项目管理 / 评审 | 基线、交付物、检查清单、Known Issues | 第 14 章、附录 F |
适用范围:基于 Linux 内核的嵌入式产品(ARM32 / ARM64 / RISC-V 均可参照), 存储介质覆盖 SPI NOR、SPI NAND、eMMC、SD / SD NAND 等。 不适用范围:裸机 / RTOS 开发、纯用户态应用开发、芯片 RTL 设计。
1.2 平台与版本基线
版本基线必须写进文档的「第一页」,后续所有命令、补丁、配置都以此为准。 下表为示例基线,项目立项时按实际情况替换并冻结。
| 项目 | 基线选择 | 说明 |
|---|---|---|
| 内核版本 | Linux 5.4 / 6.1(示例) | 优先选 LTS 主线版本;厂商 SDK 内核需在 1.4 说明与主线差异 |
| CPU 架构 | ARM32(arm)/ ARM64(arm64) | 决定 ARCH 与工具链前缀 |
| 芯片平台 | i.MX6ULL / RK3568 / 全志 / STM32MP1 等 | 以项目选型为准,不写死本文档 |
| Bootloader | U-Boot(版本随 SDK) | 与内核 DTB、bootargs 强耦合,必须配对升级 |
| 构建系统 | Buildroot / Yocto / 厂商 SDK | 见 2.4 三者差异 |
| 启动介质 | SPI NOR / SPI NAND / eMMC / SD | 决定分区表与文件系统选型,见第 7 章 |
| 工具链 | gcc-linaro / arm-gnu-toolchain / SDK 自带 | 版本与内核版本强相关,见 2.2 |
立项评审通过后,内核版本、工具链版本、U-Boot 版本三者必须同时冻结并写入本表;任一变更都要走变更评审,并在 14.5 Known Issues 中记录影响面。中途偷偷升级工具链是「昨天还能编译,今天编不过」最常见的来源。
1.3 术语、缩写与参考文档
| 缩写 | 全称 / 含义 | 备注 |
|---|---|---|
| BSP | Board Support Package,板级支持包 | 本文档主题 |
| DTS / DTB / DTC | 设备树源码 / 二进制 / 编译器 | 第 3.3、5 章 |
| DTBO | Device Tree Overlay 二进制 | 第 5.4 节 |
| MTD | Memory Technology Device,裸闪存子系统 | 第 6.6 节 |
| UBI / UBIFS | 闪存卷管理层 / 闪存文件系统 | 第 7 章 |
| rootfs | 根文件系统 | 第 7 章 |
| bootargs | U-Boot 传给内核的启动参数 | 第 7.3 节 |
| Oops / panic | 内核异常 / 致命错误 | 第 8.3 节 |
| ko | 可动态加载的内核模块 | 第 3.5、6.5 节 |
| LTS | Long Term Support,长期支持版本 | 第 1.2 节 |
参考文档(详见附录 E):内核源码树内 Documentation/、
README、各子系统 *-api.rst;芯片厂商 SDK Release Note 与 TRM;
器件 datasheet(以正式版本为准)。
1.4 内核源码来源与分支策略
内核源码只有两种来源,必须先定性再开发——它决定了后续所有补丁与升级方式。
kernel.org / 镜像站拉取官方 LTS 分支。优点:可长期维护、社区补丁可直接合入、安全修复及时。代价:芯片平台支持可能不完整,需自行补齐驱动与 DTS。分支管理策略(建议)
main # 基线分支,只接受经过评审的合入,每个发布版本打 tag
release/vX.Y # 发布分支,从 main 切出,只接受 bugfix,不再合入新特性
dev/<feature> # 特性分支,开发者个人分支,完成后提 MR/PR 合入 main
hotfix/<issue> # 紧急修复分支,修完同时回流 main 与对应 release 分支无论选哪种来源,每季度至少同步一次上游 LTS 的安全补丁;厂商 SDK 内核则跟踪原厂 Release Note 的 CVE 修复。同步前先在 dev/ 分支验证编译与启动,再合入 main。
第 2 章 开发环境搭建
环境是后面所有工作的地基。这一章的目标是:任何一个新人在一台干净的机器上,照着命令跑完就能编出可启动的内核。
2.1 宿主机系统与依赖包
推荐 Ubuntu 20.04 / 22.04 LTS(64 位),磁盘预留 100 GB 以上(内核源码 + 构建产物体积可观)。 以下为最小依赖集,已覆盖内核编译、menuconfig、设备树编译、模块签名等常见需求。
sudo apt update
sudo apt install -y build-essential libncurses-dev flex bison libssl-dev libelf-dev \
bc cpio kmod rsync wget git python3 python3-pip device-tree-compiler \
u-boot-tools zlib1g-dev liblz4-tool zstd file cscope exuberant-ctags \
picocom minicom tftpd-hpa nfs-kernel-server openssh-server
# 调试与性能工具(第 8、9 章会用到)
sudo apt install -y trace-cmd kernelshark linux-tools-common linux-tools-generic \
gdb-multiarch stress stress-ng iperf3 sysstat用普通用户编译,需要 root 权限的动作前加 sudo。以 root 编译会导致源码树文件属主混乱,后期 git clean、补丁生成都会踩坑;内核构建本身也不推荐用 sudo make。
2.2 交叉编译工具链选型
工具链必须与内核版本和目标架构同时匹配,尤其注意浮点 ABI
(ARM32 的 gnueabihf 与 gnueabi 不兼容)。
| 目标架构 | 典型前缀 | 说明 |
|---|---|---|
| ARM32 | arm-linux-gnueabihf- | 带硬浮点,i.MX6ULL / STM32MP1 等常用 |
| ARM64 | aarch64-linux-gnu- | RK3568 及多数 64 位平台 |
| RISC-V | riscv64-linux-gnu- | 注意 ISA 扩展与 -march 设置 |
| SDK 自带 | 以厂商 Release Note 为准 | 与 SDK 内核版本绑定,不建议混用外部工具链 |
# 方式一:发行版仓库安装(够用,版本较固定)
sudo apt install -y gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu
# 方式二:官方工具链(推荐,版本可控)
wget https://developer.arm.com/-/media/Files/downloads/gnu/.../arm-gnu-toolchain-*.tar.xz
sudo tar -xf arm-gnu-toolchain-*.tar.xz -C /opt
export PATH=/opt/arm-gnu-toolchain-*/bin:$PATH
# 验证(必须看到版本号与正确的 target)
arm-linux-gnueabihf-gcc --version
aarch64-linux-gnu-gcc --version
# 写入环境脚本,避免每次手敲(放在 build/env.sh,团队共用)
cat > build/env.sh <<'EOF'
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
export PATH=/opt/arm-gnu-toolchain-13.2/bin:$PATH
export KBUILD_OUTPUT=${PWD}/build/out # 可选:把编译产物放到独立目录
EOF
source build/env.sh现象:编译报 undefined reference to `__aeabi_...'、unrecognized command line option '-mno-unaligned-access';或者编出来了但内核启动即崩溃(浮点 ABI 不一致)。处置:不要临时换工具链硬编,回到基线表确认版本,必要时连内核一起升/降。
2.3 内核源码获取与补丁管理
# 主线内核(浅克隆加速,够编译即可)
git clone --depth 1 --branch v6.1 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
cd linux && git log --oneline -1
# 厂商 SDK:以原厂文档为准,一般提供完整 git 仓库或 tarball + 补丁集
# 补丁管理:所有本地修改一律做成 patch 入仓,禁止直接改源码不留痕
git format-patch -1 <commit> -o patches/ # 生成补丁
git am patches/0001-*.patch # 应用补丁
quilt series # 若 SDK 使用 quilt 管理补丁,按其规范操作
2.4 构建系统:Buildroot / Yocto / 厂商 SDK
三者不是互斥关系,而是不同规模产品的不同选择。
| 维度 | Buildroot | Yocto / OpenEmbedded | 厂商 SDK |
|---|---|---|---|
| 定位 | 轻量根文件系统与工具链生成器 | 完整发行版构建框架 | 原厂一键编译脚本 |
| 上手成本 | 低(menuconfig 一套配置) | 高(layer / recipe 概念多) | 最低(照文档跑脚本) |
| 灵活性 | 中,适合单一产品形态 | 高,适合多产品线与长期演进 | 低,绑定原厂目录结构 |
| 可复现性 | 好(defconfig + 版本锁定) | 最好(recipe 精确到 checksum) | 差(依赖 SDK 包本身) |
| 构建耗时 | 分钟级到半小时级 | 首次数小时(有 sstate 缓存后快) | 视 SDK 而定 |
| 推荐场景 | 中小项目 / 单一产品 / 快速验证 | 多产品线 / 需要 OTA / 需要长期维护 | 前期验证 / 原厂方案直出 |
做单一产品、团队人手少:选 Buildroot,把 buildroot 的 defconfig、外部树(BR2_EXTERNAL)与内核 defconfig 一起入仓,整机构建可复现。多条产品线、要做 OTA 与长期演进:选 Yocto,把自定义层单独建仓。厂商 SDK 只建议在前期验证阶段用,量产项目应把关键配置抽出来落到 Buildroot/Yocto。
2.5 内核、设备树与模块的独立编译
日常开发中,全量重编太慢,掌握「单独编」能省大量时间。
source build/env.sh
# 1) 配置(首次)
make <soc>_defconfig # 载入芯片默认配置,例如 imx_v6_v7_defconfig
make menuconfig # 交互调整
make savedefconfig # 生成精简 defconfig(提交这一个,不要提交 .config)
cp defconfig arch/arm/configs/myboard_defconfig
# 2) 编译内核与模块(-j 按 CPU 核数调整)
make -j$(nproc) zImage # ARM32;ARM64 通常是 Image
make -j$(nproc) modules
make -j$(nproc) dtbs # 编译全部设备树
make mysoc-myboard.dtb # 只编指定的一个 dtb(快)
# 3) 单独编译某个外部模块
make -C /path/to/kernel M=$(pwd) modules
make -C /path/to/kernel M=$(pwd) modules_install INSTALL_MOD_PATH=/path/to/rootfs
# 4) 清理(注意别误删 .config)
make clean # 清目标文件
make mrproper # 连 .config 一起清,慎用
2.6 调试通道:串口 / TFTP / NFS / SSH
开发期强烈建议内核走 TFTP、rootfs 走 NFS,把「改一行代码」到「看到结果」压缩到一分钟内, 同时避免反复擦写 Flash 导致器件损耗。
| 通道 | 用途 | 关键配置 |
|---|---|---|
| 串口控制台 | 看 U-Boot / 内核启动日志、进 U-Boot 命令行 | 波特率常见 115200 8N1,按板卡确认 |
| TFTP | 下载 zImage / dtb 到内存运行 | 服务端 /srv/tftp,U-Boot 设 serverip |
| NFS | 挂载根文件系统,改文件即时生效 | 内核需开 CONFIG_ROOT_NFS,/etc/exports 配权限 |
| SSH | 登录后跑性能工具、拷贝日志 | rootfs 内置 dropbear / openssh |
# U-Boot 侧:网络启动内核 + NFS 根文件系统(开发期最常用)
setenv ipaddr 192.168.1.100; setenv serverip 192.168.1.10
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.10:/srv/nfs/rootfs,v3,tcp rw ip=dhcp'
tftpboot 0x80800000 zImage
tftpboot 0x83000000 myboard.dtb
bootz 0x80800000 - 0x83000000 # ARM64 用 booti
# 宿主机:把编译产物同步到 TFTP 目录
cp arch/arm/boot/zImage /srv/tftp/ && cp arch/arm/boot/dts/myboard.dtb /srv/tftp/
2.7 推荐工具集
| 类别 | 工具 | 用途 |
|---|---|---|
| 编辑 / 浏览 | vim / VS Code + clangd | 配合 compile_commands.json 获得跳转补全 |
| 代码索引 | cscope / ctags | make cscope 生成索引,查函数调用极快 |
| 版本管理 | git + gitk / tig | 分支、补丁、bisect 定位引入问题的提交 |
| 静态检查 | make checkstack / sparse / coccinelle | 编译期发现隐患 |
| 动态调试 | trace-cmd / kernelshark / perf | 第 8.5 节 |
| 内核调试 | gdb-multiarch + KGDB | 第 8.4 节 |
| 串口终端 | picocom / minicom | picocom -b 115200 /dev/ttyUSB0 |
| 压力测试 | stress-ng / fio / iperf3 | 第 13 章 |
第 3 章 内核基础原理
这一章是给新人的理论底座:搞清楚内核怎么启动、设备树起什么作用、子系统怎么分层、模块怎么加载。理解了这些,后面调驱动才知道自己在改哪一层。
3.1 内核空间与用户空间、系统调用
CPU 以特权级区分两种运行态:用户空间跑应用程序,各自拥有独立虚拟地址空间, 一旦越界访问或执行特权指令会被 MMU / CPU 拦截;内核空间所有进程共享同一份地址空间, 驱动代码就运行在这里,一处出错会影响整机。
用户态进入内核态的合法途径只有三类:系统调用(open/read/write/ioctl 等)、
异常(缺页、未定义指令)、中断(外设触发)。
驱动开发者打交道最多的是 ioctl 与 read/write 在内核侧的
file_operations 实现。
内核态没有内存保护:一个野指针写坏的是整个内核,现象往往是随机的死机而不是立刻崩溃。因此第 6.7 节的资源自动释放(devm_*)与第 8.6 节的内存排查不是可选项,而是必备动作。
3.2 内核启动全流程
启动链路是排障的总地图。拿到一块不启动的板子,第一件事永远是看串口停在哪一步, 而不是猜。下图给出七阶段与每一步的日志特征。
| 阶段 | 关键动作 | 常见失败点 |
|---|---|---|
| ROM / SPL | 片内 ROM 加载 SPL 到 SRAM | 启动引脚电平、Boot 介质焊接、SPL 镜像不对 |
| U-Boot SPL | 初始化 DDR / 时钟 / 串口 | DDR 参数不匹配 → 卡死无输出 |
| U-Boot 主体 | 加载 zImage + dtb,装配 bootargs | bootargs 写错、dtb 地址冲突、网口不通 |
| 内核入口 | 自解压并跳转到内核入口 | 镜像损坏、加载地址与链接地址不一致 |
| 内核初始化 | 解析 dtb、子系统 initcall 分级初始化 | dtb 不匹配、时钟/电源域未开导致驱动崩溃 |
| 挂载 rootfs | 按 root= 参数挂载根文件系统 | root 参数错、文件系统驱动未编译进内核 |
| 用户态 init | 启动 init / systemd 并拉起业务 | rootfs 缺库、init 路径不存在 |
3.3 设备树 DTS 原理
设备树解决的是「同一份内核如何描述不同硬件」的问题:
硬件描述(DTS)从内核代码中剥离出来,编译成 DTB 后由 Bootloader 传给内核,
内核展开成 device_node,再按 compatible 与驱动的
of_match_table 做匹配,匹配成功才调用 probe()。
compatible 是硬件的标识字符串,格式惯例为 "厂商,型号",可带多个从具体到通用的候选(如 "xtx,xt25w1g", "jedec,spi-nor")。驱动侧在自己的 of_match_table 里声明支持哪些字符串,两边能对上才会 probe。驱动调不通时先核对这一对字符串,比看寄存器快得多。
3.4 内核子系统地图
下表是子系统清单与本文定位——每个子系统单独写专项文档,这里只给出职责边界与常用接口入口。
| 子系统 | 职责 | 驱动常用接口 / 文档入口 |
|---|---|---|
| Platform 总线 | 无物理总线的片上外设挂接点 | platform_driver_register、of_device_id |
| SPI / I2C / UART | 串行总线与控制器、从设备驱动 | spi_driver、i2c_driver、serdev |
| MTD | 裸闪存(NOR / NAND)统一抽象 | mtd_info、nand_controller、spi_nand |
| 块设备 | eMMC / SD / UBI 之上的块接口 | gendisk、blk_mq |
| 字符设备 | 按字节流访问的设备 | cdev、file_operations |
| 时钟 CLK | 时钟树与门控 | clk_get / clk_prepare_enable |
| 电源管理 PM | suspend/resume、Runtime PM、电源域 | dev_pm_ops、pm_runtime_* |
| 中断 IRQ | 中断号映射、上下半部、线程化中断 | request_threaded_irq、devm_request_irq |
| DMA | 外设与内存之间的直接搬运 | dmaengine、dma_map_single |
| GPIO / Pinctrl | 引脚复用与电平控制 | gpiod_get、pinctrl |
3.5 内核模块:内置 vs 动态加载
| 方式 | 配置形态 | 优点 | 代价 / 注意 |
|---|---|---|---|
| 内置编译 | CONFIG_XXX=y | 启动即可用,无加载顺序问题;rootfs 挂载前必需的驱动(存储、文件系统)必须内置 | 镜像变大,改动需重编内核 |
| 动态模块 | CONFIG_XXX=m | 可现场加载调试,迭代快,镜像小 | 需要处理依赖与加载顺序,modules_install 到 rootfs |
# 模块常用操作
insmod demo.ko # 加载(不处理依赖)
modprobe demo # 加载(自动处理依赖,需先 depmod)
lsmod # 查看已加载模块
modinfo demo.ko # 查看模块信息(依赖、参数、版本)
rmmod demo # 卸载
cat /proc/modules # 内核视角的模块列表
# 模块参数(驱动内 module_param 声明)
insmod demo.ko debug=1启动链路上必需的驱动一律内置:存储控制器、MTD/块设备、根文件系统类型、以及它们依赖的时钟与 GPIO/pinctrl。调试期才用得上的驱动做成模块,量产后按需关闭。把存储驱动做成模块而文件系统内置(或反之)是「VFS: Unable to mount root fs」的高频成因。
第 4 章 内核配置与裁剪
配置裁剪是 BSP 工程师的核心工作:把几千个 CONFIG 收敛成一份可维护、可追溯、体积与启动时间都达标的基线。
4.1 .config 与 Kconfig 原理
内核配置由 Kconfig(各级目录下的配置描述)生成菜单,选择结果落在源码树根目录的
.config(CONFIG_XXX=y/m/n)。常见命令:
make <soc>_defconfig # 载入芯片/板级默认配置(起点)
make menuconfig # 图形化调整(需要 ncurses)
make oldconfig # 沿用旧 .config,只提示新增项(升级内核时用)
make olddefconfig # 同上,新增项取默认值,不交互(CI 里用这个)
make savedefconfig # 生成精简 defconfig(只保留非默认项)→ 入仓
make nconfig / xconfig # 其它交互前端.config 包含上千项(绝大多数是默认值),提交它会导致:① 评审时看不出真正改了什么;② 升级内核时冲突一大片;③ 别人 make oldconfig 后产生隐性差异。正确做法:只提交 make savedefconfig 产出的 defconfig(通常几十行),并在 arch/<arch>/configs/ 或项目 build/ 目录下管理。
4.2 配置基线管理规范
- 选起点:以最接近目标硬件的
*_defconfig为起点,不要从零开始。 - 调配置:用
menuconfig调整,每改一批就记录「改了什么、为什么」。 - 存基线:
make savedefconfig产出 defconfig,与源码同仓提交。 - 编译验证:
make -j$(nproc)全量编译,确认无警告转错误。 - 度量收益:记录镜像体积、启动时间、内核内存占用三项指标。
- 写 changelog:配置变更与代码变更一样要有评审与记录(见 14.2)。
# 推荐:把 defconfig 与构建脚本一起放进项目的 build/ 目录
build/
├── env.sh # ARCH / CROSS_COMPILE / PATH
├── myboard_defconfig # 配置基线(唯一来源)
├── build_kernel.sh # 一键编译:载入 defconfig → 编译 → 拷贝产物
└── patches/ # 内核补丁集
# build_kernel.sh 关键片段
#!/bin/sh
set -e
source "$(dirname "$0")/env.sh"
cp "$1" .config || cp build/myboard_defconfig .config
make olddefconfig
make -j"$(nproc)" zImage dtbs modules
make modules_install INSTALL_MOD_PATH=../rootfs
4.3 裁剪原则与收益
裁剪的三条原则:没有对应硬件的驱动全删;量产版本关闭调试组件; 按需保留文件系统与协议栈。收益体现在体积、启动时间、内存占用与攻击面四个方面。
| 裁剪对象 | 典型 CONFIG | 收益 | 风险提示 |
|---|---|---|---|
| 无用驱动 | CONFIG_DRM、CONFIG_SND 等未使用子系统 | 体积下降最明显 | 删错会导致外设不可用,逐项验证 |
| 文件系统 | CONFIG_ISO9660、CONFIG_NFS_FS 等 | 体积 + 启动时间 | 根文件系统类型必须保留且内置 |
| 网络协议栈 | CONFIG_IPV6、无关隧道与过滤模块 | 体积 + 攻击面 | 确认业务是否用到 |
| 调试组件 | CONFIG_DEBUG_KERNEL、CONFIG_KGDB、CONFIG_FTRACE | 体积 + 性能 | 开发期保留,量产前关闭 |
| 模块符号表 | CONFIG_MODULES 相关 | 体积 | 关掉后无法加载任何 ko |
裁剪后必须跑一遍第 13 章的基础功能用例。「编过了」不等于「能用」——很多配置项编译期无依赖,运行期才发现某个驱动被间接依赖(例如 UBI 依赖 MTD、MTD 依赖特定的 NAND ECC 引擎)。裁剪前后各跑一次 lsmod 与 /sys 节点对比最稳妥。
4.4 内核编译产出
| 产出 | 路径 | 用途 |
|---|---|---|
| 内核镜像 | arch/<arch>/boot/zImage 或 Image | 被 U-Boot 加载运行 |
| uImage | 由 mkimage 在 zImage 上加 64 字节头 | 老版本 U-Boot 需要 |
| 设备树 | arch/<arch>/boot/dts/*.dtb | 描述硬件,与内核配对加载 |
| 内核模块 | *.ko,安装到 /lib/modules/<ver>/ | 可动态加载的驱动 |
| 符号表 | System.map、vmlinux | 定位 Oops 地址(第 8.3 节必备) |
发布版本把 vmlinux、System.map、.config 与源码 commit id 一起归档。没有它们,后面出现 Oops 时无法用 addr2line 还原调用栈,只能靠猜。归档成本几乎为零,缺了它排查成本极高。
4.5 镜像烧录与启动介质
烧录方式取决于介质与阶段:开发期用 TFTP/NFS 避免擦写; 小批量用 U-Boot 命令或 USB 烧写工具;量产用编程器或产线烧录工装。
# U-Boot 下烧录到 SPI NOR(示例,偏移按分区表调整)
sf probe 0
sf erase 0x100000 0x400000
tftpboot 0x80800000 zImage
sf write 0x80800000 0x100000 ${filesize}
# U-Boot 下烧录到 SPI NAND(需先确认坏块与 ECC 布局)
mtd list
nand erase.spread 0x200000 0x600000
nand write 0x80800000 0x200000 ${filesize}
# eMMC:通常在能启动的系统中用 dd 或专用升级工具写入分区
dd if=rootfs.ext4 of=/dev/mmcblk0p3 bs=4M conv=fsync① 分区偏移与大小与 U-Boot、内核 MTD 分区表一致;② SPI NAND 必须使用 skip-bad-block 的写入方式(nand write.trimffs / erase.spread),否则坏块会推移后续数据;③ 烧录后做一次回读校验(CRC 或 sha256),不要烧完就断电。写错偏移覆盖掉 U-Boot 会导致板子变砖,只能重新用编程器救。
附录 B .config 参考配置片段
以下为常见场景的配置片段示例。写入 defconfig 时注意:defconfig 只保留非默认项,片段中的每一条都要确认与当前内核版本的选项名一致。
B.1 存储相关(裸闪存场景)
CONFIG_MTD=y
CONFIG_MTD_SPI_NOR=y
CONFIG_MTD_RAW_NAND=y # 并行 NAND,SPI NAND 不需要这一项
CONFIG_MTD_SPI_NAND=y # SPI NAND(走 spi-nand 框架)
CONFIG_MTD_UBI=y # UBI 卷管理
CONFIG_UBIFS_FS=y # UBIFS(根文件系统必须内置)
CONFIG_MTD_BLOCK=y # 需要 mtdblock 设备时
B.2 存储相关(块设备场景)
CONFIG_MMC=y
CONFIG_MMC_BLOCK=y
CONFIG_MMC_SDHCI=y
CONFIG_MMC_SDHCI_OF_ARASAN=y # 按实际控制器选择
CONFIG_EXT4_FS=y # 或 CONFIG_F2FS_FS=y
CONFIG_SQUASHFS=y
CONFIG_OVERLAY_FS=y # 只读底 + 可写上层的组合需要
B.3 调试相关(开发版开启,量产关闭)
CONFIG_DEBUG_KERNEL=y
CONFIG_DEBUG_INFO=y # addr2line 需要
CONFIG_FRAME_POINTER=y # 栈回溯更准确
CONFIG_DEBUG_FS=y # debugfs(动态调试依赖)
CONFIG_DYNAMIC_DEBUG=y
CONFIG_FTRACE=y
CONFIG_FUNCTION_TRACER=y
CONFIG_PERF_EVENTS=y
CONFIG_KGDB=y # 视调试需求
CONFIG_LOCKDEP=y # 锁问题排查
CONFIG_DEBUG_KMEMLEAK=y # 内存泄漏排查
CONFIG_MAGIC_SYSRQ=y # 卡死时打印栈,强烈建议保留
B.4 量产版建议关闭 / 收紧
# CONFIG_DEBUG_KERNEL is not set
# CONFIG_KGDB is not set
# CONFIG_DEBUG_FS is not set # 减小攻击面
# CONFIG_MODULES is not set # 若无需现场加载模块
CONFIG_DEBUG_RODATA=y # 只读数据不可写(按架构名可能不同)
CONFIG_STACKPROTECTOR=y
CONFIG_RANDOMIZE_BASE=y不同内核版本的选项名可能变化(例如部分安全选项名在不同架构下不同)。写入 defconfig 后一定要 make olddefconfig 再编译,并确认没有选项被静默丢弃。
附录 C DTS 代码样例
三份可直接改用的最小样例:SPI 存储器件、固定分区、GPIO 与中断。
C.1 SPI 存储器件 + 固定分区
&spi0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi0_pins>;
#address-cells = <1>;
#size-cells = <0>;
flash@0 {
compatible = "vendor,part", "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <50000000>;
spi-rx-bus-width = <4>;
#address-cells = <1>;
#size-cells = <1>;
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
partition@0 { label = "uboot"; reg = <0x000000 0x100000>; read-only; };
partition@100000 { label = "env"; reg = <0x100000 0x020000>; };
partition@120000 { label = "kernel"; reg = <0x120000 0x500000>; };
partition@620000 { label = "rootfs"; reg = <0x620000 0x900000>; };
};
};
};
C.2 保留内存(pstore / ramoops)
/ {
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
ramoops@80000000 {
compatible = "ramoops";
reg = <0x80000000 0x100000>;
record-size = <0x20000>;
console-size = <0x20000>;
ftrace-size = <0x20000>;
};
};
};
C.3 带中断的 I2C 外设
&i2c1 {
status = "okay";
clock-frequency = <400000>;
sensor@48 {
compatible = "vendor,part";
reg = <0x48>;
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
vdd-supply = <®_3v3>; /* 电源域/供电引用 */
wakeup-source; /* 允许作为唤醒源 */
};
};
附录 D 常用调试命令脚本
四个可直接改用的脚本骨架:一键编译、启动耗时量测、存储压力与掉电测试、回归用例。
D.1 一键编译脚本
#!/bin/sh
# build/build_kernel.sh —— 用法:./build_kernel.sh [defconfig]
set -e
cd "$(dirname "$0")/.."
source build/env.sh
CFG=${1:-build/myboard_defconfig}
cp "$CFG" .config
make olddefconfig
make -j"$(nproc)" zImage dtbs modules
make modules_install INSTALL_MOD_PATH=../rootfs
mkdir -p out
cp arch/"$ARCH"/boot/zImage out/
cp arch/"$ARCH"/boot/dts/*.dtb out/
cp .config System.map vmlinux out/ # 定位 Oops 必需
sha256sum out/* > out/manifest.sha256
echo "build ok: $(git rev-parse --short HEAD)"
D.2 启动耗时量测
#!/bin/sh
# 串口时间戳量测(需 grabserial)
grabserial -d /dev/ttyUSB0 -b 115200 -e 60 -m 'login:' -t | tee boot_$(date +%s).log
# 内核侧 initcall 耗时(bootargs 追加 initcall_debug printk.time=1)
dmesg > boot_dmesg.log
scripts/bootgraph.pl boot_dmesg.log > boot.svg
D.3 存储压力与掉电测试
#!/bin/sh
# stress_storage.sh <挂载点> <轮次>
MNT=$1; N=${2:-100}
i=0
while [ $i -lt $N ]; do
f="$MNT/dd_$i.bin"
dd if=/dev/urandom of="$f" bs=1M count=8 conv=fsync 2>/dev/null
sync
a=$(sha256sum "$f" | cut -d' ' -f1)
sync
b=$(sha256sum "$f" | cut -d' ' -f1)
[ "$a" = "$b" ] || echo "MISMATCH at round $i" | tee -a err.log
rm -f "$f"
i=$((i+1))
done
echo "done $N rounds, errors: $(wc -l < err.log 2>/dev/null || echo 0)"
# 掉电测试:配合继电器工装,在随机时刻断电后重新上电,校验 err.log 与文件系统可挂载
D.4 回归用例骨架
#!/bin/sh
# tests/run_all.sh —— 输出 PASS/FAIL 汇总,供 CI 使用
pass=0; fail=0
run() { # run <用例名> <命令>
if eval "$2" >/dev/null 2>&1; then echo "PASS $1"; pass=$((pass+1));
else echo "FAIL $1"; fail=$((fail+1)); fi
}
run "boot_ok" "dmesg | grep -q 'VFS: Mounted root'"
run "storage_mtd" "cat /proc/mtd | grep -q rootfs"
run "net_ping" "ping -c1 -W2 192.168.1.1"
run "fs_rw" "touch /data/.t && rm /data/.t"
run "no_oom" "! dmesg | grep -q 'Out of memory'"
echo "---- summary: PASS=$pass FAIL=$fail ----"
[ "$fail" -eq 0 ]
附录 E 参考资料与内核文档
| 类别 | 资料 | 说明 |
|---|---|---|
| 内核自带 | Documentation/ 目录 | 各子系统的权威说明,优先于任何二手资料 |
| 内核自带 | Documentation/devicetree/ | 设备树 binding 与写法规范 |
| 内核自带 | Documentation/process/ | 编码规范、提交规范、补丁流程 |
| 内核自带 | scripts/checkpatch.pl | 补丁检查工具 |
| 官方站点 | kernel.org(源码与 LTS 版本状态) | 版本基线与 CVE 信息以官方为准 |
| 厂商资料 | 芯片 TRM / SDK Release Note | 平台相关寄存器与驱动支持范围 |
| 器件资料 | 器件 datasheet | 一切电气与时序参数以正式 datasheet 为准 |
| 工具文档 | Buildroot / Yocto 官方手册 | 构建系统配置细节 |
| 工具文档 | fio / perf / trace-cmd 手册 | 性能与调试工具的完整参数 |
① 内核源码树内的 Documentation/ 与源码注释(最权威、最贴合当前版本);② 芯片厂商 TRM 与 SDK 文档;③ 器件 datasheet;④ 社区邮件列表与 LWN。博客与问答仅用于「找线索」,不要直接照抄其中的命令与参数。
附录 F 内核发布前检查清单
发布评审逐条过,任一条不满足都要有书面说明才能放行。建议把这份清单接入评审流程并存档。
F.1 配置与编译
- defconfig 已入仓,源码树内不提交
.config。 - 全量编译无 error;关键 warning 已确认或修复。
- 内核版本、工具链版本、U-Boot 版本与 1.2 基线表一致。
- 裁剪后镜像体积、启动时间、内存占用三项指标已记录并与基线对比。
- 根文件系统所依赖的存储与文件系统驱动为内置(非模块)。
F.2 设备树与启动
- DTS 编译无 warning;
dtc -I dts -O dtb单独验证通过。 - 板级
/proc/device-tree节点与预期一致。 - 所有外设的
compatible与驱动of_match_table已逐项核对。 - 时钟、电源域、GPIO 复用已确认(尤其新改动的节点)。
- bootargs 的 root / rootfstype / rootwait 已在实际介质上验证。
F.3 驱动与文件系统
- 所有改动过
checkpatch.pl,ERROR 清零。 - 驱动 probe / remove 配对,资源用
devm_*或手工释放完整。 - suspend / resume(含 Runtime PM)已验证,唤醒后外设功能正常。
- 文件系统参数(UBIFS / ubinize)与
mtdinfo实测值一致。 - 存储读写循环测试通过,无数据不一致。
F.4 调试配置与量产配置
- 量产版本已关闭 kgdb / ftrace / debugfs 等调试组件(若策略如此)。
- 保留
CONFIG_MAGIC_SYSRQ等现场救命项。 - 看门狗已启用,喂狗周期覆盖最长阻塞操作。
- pstore / 日志持久化已验证:死机后重启能取到现场。
- panic 自动重启策略(
kernel.panic)已设置。
F.5 OTA 与可靠性
- A/B 分区(或等效回滚机制)已验证:升级中断电可自愈。
- 升级包 sha256 与签名校验已验证;写入后回读比对通过。
- 掉电测试已跑够轮次(数百次),数据完整性通过。
- 回滚后启动标记清理正确,不会出现双槽反复横跳。
- 回滚动作不擦除共享 data 区。
F.6 交付与文档
- 源码 tag / commit id 已记录,构建可复现。
vmlinux、System.map、.config已归档。- manifest(版本号 + sha256)随镜像一起交付。
- changelog 与 Known Issues 已更新,每条 Known Issue 有规避方案或修复计划。
- 第 13 章测试报告(含性能基准对比)已产出并归档。
- 烧录与升级说明文档已交付给产线与售后。
把这份清单做成评审模板:每项三个状态 —— 通过 / 不适用(需注明原因)/ 带风险放行。带风险放行的项要写进 Known Issues 并指定责任人。清单本身也要随项目迭代,每次事故复盘后回头补一项。