第 2 章 从源码构建 6.x Linux 内核(第一部分)
从源码构建 Linux 内核:一场漫长的修行
从源代码开始构建 Linux 内核,是开启内核开发之旅最硬核的方式。
别被那些花哨的图形化安装工具骗了——在那层薄薄的伪装之下,隐藏着世界上最重要的软件工程之一。这个话题大到必须分成两章来讲,这是第一章,下一章还在路上。
⚠️ 前置工作:你真的准备好了吗?
如果你还没搞定工作空间,千万别往下一步走。
之前的“在线章节”(Online Chapter)我们花了大篇幅讲环境搭建——文档看哪、工具装哪、GitHub 怎么配。如果你跳过了那一步,现在去补上还来得及。这不是官僚主义,这是为了防止你在一个全是报错的终端前怀疑人生。
接下来这一章和下一章的任务非常明确:从零开始,用源码构建一个现代 Linux 内核。
在这章里,我们先搞定基础:
- 看懂内核版本号那些奇怪的时间线;
- 理解内核是怎么一步步开发出来的(以及你如果赶不上这班车会发生什么);
- 搞清楚“主线内核”、“LTS”、“厂商内核”这些到底都是什么鬼。
然后,我们上手干脏活:
- 下载纯净的内核源码(Vanilla Kernel);
- 把它解压开,看看这 3000 万行代码到底长什么样;
- 最关键的一步:配置它。
最后,我们会花点时间黑进 Kconfig 系统——也就是那个决定哪些代码能进内核的“看门人”——甚至给它加个我们自己的菜单项。
极简系统论:Linux 是怎么跑起来的
在开始之前,我们要先达成一个共识。
任何一个能跑起来的 Linux 系统——不管是天河一号还是你手边的树莓派——都少不了三个“老三样”:
- Bootloader(引导程序):第一个醒来干活的家伙,负责把内核喊起来。
- OS Kernel(内核):就是我们要折腾的主角。
- Root Filesystem(根文件系统):内核启动后需要的那一大堆库、配置和工具。
还有两个“选装件”,但在 ARM 这种嵌入式平台上几乎是标配:
- Device Tree Blob (DTB):给 ARM 准备的“硬件说明书”,告诉内核这板子上都有啥外设。
- Initramfs:一个塞进内存里的迷你文件系统,用来挂载真正的根文件系统,或者加载那些必须得有的驱动。
在这两章里,我们只干一件事:构建那个 OS 内核。Root Filesystem 的坑留给以后,下一章我们会顺带提一下 x86 的 GRUB 引导配置,那是为了让你能真正跑起来。
对于一个完整的 x86 构建流程,大约需要 6 到 7 步。这一章我们只走前 3 步,剩下的我们放到下一章去。
Preliminaries: 别急着敲命令,先搞懂规则
在开始之前,有几件事必须得先说明白。这不只是技术细节,这是 Linux 世界的“潜规则”。
去中心化的“办公室”
Linux 内核不是某家大公司关起门来开发的,它是一个完全去中心化的、在全球协作的巨大怪兽。
如果你非要找一个“办公室”,那可能是 Linux Foundation(Linux 基金会)。它负责搞定法律和资金问题,而 Kernel Organization 负责把代码免费发到全世界(https://www.kernel.org)。
这里有一个常见的误区:GPL 协议是不允许赚钱的? 大错特错。GPL 从来不阻止开发者收费。Linus 和基金会选择免费发给别人,但这不妨碍 Red Hat、SUSE 这些公司拿着内核卖支持、卖订阅、卖 SaaS。只要源代码最终公开,怎么商业化那是你的本事。
理解 Linux 内核版本命名规范
看一眼你的终端,输入 uname -r,输出的一串字符是什么意思?
在我们的 Ubuntu 22.04 虚拟机上,它大概是这个样子的:
$ uname -r
5.19.0-40-generic
这串字符背后有一套严格的语法。现代 Linux 内核的版本号格式如下:
major#.minor#[.patchlevel][-EXTRAVERSION]
或者用更简单的代数表示:w.x[.y][-z]。
方括号里的部分是可选的。让我们拆解一下这串 5.19.0-40-generic:
- Major # (w):
5—— 主版本号,现在是 6.x 时代了。 - Minor # (x):
19—— 次版本号。 - [patchlevel] (y):
0—— 补丁级别(也叫修订版)。 - [-EXTRAVERSION] (-z):
-40-generic—— 特定版本标识。generic表示这是 Ubuntu 通用版。
⚠️ 注意:发型版内核(比如 Ubuntu 的)可能会自己加料,不一定完全遵循这个命名法。但在 https://www.kernel.org/ 上发布的纯净内核 是严格遵守这个规矩的。
历史的回响:偶数奇数传说
在 2.6 内核之前(也就是上古时代),次版本号是有玄机的:
- 偶数:稳定版(比如 2.4、2.6)。
- 奇数:开发版/测试版(比如 2.5)。
但现在?这个规矩已经没了。现在的版本号只和时间有关,和功能无关。
“手脚模型” (Fingers-and-toes releases)
Linus 曾经开玩笑说,他的版本号策略是“数手指和脚趾”。
当一个次版本号(x)数到 20 左右(手指 10 个 + 脚趾 10 个),他就会把主版本号(w)加 1。
- 3.0 到 3.19(20 个小版本)
- 4.0 到 4.19(20 个小版本)
- 5.0 到 5.19(20 个小版本)
- 6.0 ……
每两个次版本之间大概间隔 6 到 10 周。这不是功能驱动的,这是有机演化。
内核开发工作流:从合并窗口到发布版
很多人以为内核开发是“想到哪写到哪”,完全乱来。这是天大的误解。
内核开发有着极其严格的节奏感。为了看懂这个节奏,我们可以去看看内核的 Git 记录。
查看 Git 历史记录
假设你 Clone 下来了最新的 Linus 主线树。用下面这条命令,你可以看到一条清晰的时间线:
$ git log --date-order --tags --simplify-by-decoration \
--pretty=format:'%ai %h %d'
2023-04-23 12:02:52 -0700 457391b03803 (tag: v6.3)
2023-04-16 15:23:53 -0700 6a8f57ae2eb0 (tag: v6.3-rc7)
2023-04-09 11:15:57 -0700 09a9639e56c0 (tag: v6.3-rc6)
[...]
2022-10-16 15:36:24 -0700 9abf2313adc1 (tag: v6.1-rc1)
2022-10-02 14:09:07 -0700 4fe89d07dcc2 (tag: v6.0)
这行命令只会在 Git 树上工作。这里我们纯粹是为了演示演化过程。
在这个输出里,你能看到一个完整的生命周期:
- v6.0 发布:也就是上一个稳定版。
- v6.1-rc1:合并窗口关闭,开始修 Bug。
- v6.1-rc7 / -rc8:发布候选版,不断的测试和修补。
- v6.1:最终稳定版发布。
时间轴上的两个关键阶段
1. Merge Window (合并窗口) —— 只有 2 周
当 v6.0 发布后(2022 年 10 月 2 日),为期 两周 的合并窗口随即开启。
这是整个社区最疯狂的时刻。子系统维护者们会把他们手里攒了很久的新代码一股脑合并进主线。如果你想加新功能,这是唯一的机会。
2. Bugfix Window (修复窗口) —— 6 到 10 周
两周后(2022 年 10 月 16 日),合并窗口关闭。v6.1-rc1 诞生。
接下来的日子里,任何新功能都被拒之门外。只能修 Bug,只能回退有问题的代码,只能解决 Regressions(倒退)。
这个过程会持续 6 到 10 周不等,直到 Linus 和核心维护者觉得“这就够了”,于是 v6.1 正式发布。
赶不上合并窗口怎么办?
如果你错过了一个窗口(比如没来得及把代码合进去),你只有一个选择:等。
等下一个窗口,大概 2.5 到 3 个月后。这就是内核开发者的“断舍离”。
探索内核源码树的类型
内核不是只有一个版本。在这个庞大的生态里,有几种不同的源码树,每一种都有它的用途。
1. Mainline / Stable (主线 / 稳定版)
这是正宫娘娘。所有的 Feature 最终都会汇聚到 Mainline,然后 Mainline 会被打上补丁变成 Stable 版本。通常也是企业发行版的基础。
2. Long Term Support (LTS)
这是“特别稳定”的版本。维护者承诺在很长一段时间内(通常是 2 年起步,甚至更长)持续为它打补丁。
按照惯例,每年 12 月发布的最后一个内核会被选为 LTS。我们这本书用的 6.1 LTS 就是这么个角色,它的预定寿命是 4 年(2022.12 - 2026.12)。
3. -next Trees (前沿树)
这是“ bleeding edge ”。如果你打算向内核提交代码,你必须在 linux-next 树上工作。这是所有准备进入下一个合并窗口的补丁的集合体。这里非常不稳定,不适合生产环境,但是贡献者的必经之路。
4. Distribution Kernels (发行版内核)
Red Hat、Ubuntu、Debian 他们用的内核。通常基于某个 Stable/LTS,然后加上自己的一大堆补丁和驱动。
这也是为什么有时候你看到某家云厂商还在用 4.x 内核——别急着嘲笑老旧,他们可能把数万个最新的安全补丁都已经 backport 回去了。
5. SoC Vendor Kernels (芯片厂商内核)
ARM、嵌入式厂商维护的内核。这是一个重灾区。
芯片厂通常会基于一个(往往很旧的)LTS 内核,然后往里面塞自己的 BSP、私有驱动。这导致他们的内核和主线差异巨大,维护起来简直是噩梦。
最佳实践:如果你做嵌入式,尽量用 Yocto 这种现代构建系统,它们会帮你把这些差异抹平。
6. Super LTS (SLTS)
这是给“民用基础设施”准备的(比如电网、大坝)。CIP 项目维护的 SLTS 内核,支持周期长达 10 年甚至更久。如果你做的设备要在那里跑 20 年不动窝,你就得关注这个。
⚠️ 重要更新:LTS 缩水了
在 2023 年 9 月,Linus 他们宣布了一个大消息:以后的 LTS 维护周期可能会缩短到 2 年。
为什么?
- 真的没人用那么老的内核:很多厂商自己都在维护自己的分支。
- 维护者累挂了:同时维护 7 个 LTS 版本(4.14, 4.19... 6.6),补丁要不停地往后移植,工作量指数级增长。
不过,我们这本用的 6.1 LTS 应该能苟到 2026 年 12 月,足够你折腾了。
到底该跑哪个内核?
这是个送命题,但内核社区的大佬们给出了标准答案:
“跑最新的稳定版。” —— Jon Corbet, 2023
“你得一路升级上来。如果你试图挑挑拣拣地打补丁,你只会得到一个既不安全又有已知 Bug 的怪胎。” —— Greg Kroah-Hartman
别试图自己当“发行版”,让大佬们替你把关吧。
开始构建:前 3 步实战
好了,理论讲够了。现在我们正式开始构建 6.x 内核的旅程。
我们把整个构建过程拆成了 6-7 步。这一章我们只做前三步:
- 获取 源码。
- 解压 源码。
- 配置 内核(这是重头戏)。
接下来的 4-7 步(编译、安装模块、配置 Bootloader、安装内核)留到下一章。
Step 1 – 获取 Linux 内核源码
你有两个选择:
- 下载压缩包:适合大部分只想学习或者使用特定版本的人。
- Clone Git 仓库:适合想贡献代码或者追到最新版的人。
对于本书,我们选择第一种:下载 6.1.25 LTS 的压缩包。
方法 A:直接下载
最简单的方法是用 wget 或者 curl:
wget --https-only -O ~/Downloads/linux-6.1.25.tar.xz \
https://mirrors.edge.kernel.org/pub/linux/kernel/v6.x/linux-6.1.25.tar.xz
这会把大概 130MB 的源码包扔进你的 ~/Downloads。
方法 B:Clone Git 仓库(可选)
如果你想体验 bleeding edge,或者想看看 linux-next 长啥样:
git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux
⚠️ 注意:完整的内核 Git 历史记录非常巨大,Clone 需要很长时间和大量磁盘空间(几个 GB)。如果你只是想看一眼,可以用 --depth 1 来浅克隆。
如果是想搞稳定版分支:
git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git
cd linux-stable
对于本书,我们还是推荐用第一种方法,下载确定版本的 6.1.25,这样大家的环境一致,不容易出幺蛾子。
Step 2 – 解压内核源码树
假设你现在手里已经有了那个 linux-6.1.25.tar.xz 文件。
解压它:
# 简单粗暴法:解压到当前目录
tar xf ~/Downloads/linux-6.1.25.tar.xz
# 推荐法:解压到一个专门的工作目录
mkdir -p ~/kernels
tar xf ~/Downloads/linux-6.1.25.tar.xz --directory=~/kernels/
现在,你的 ~/kernels/linux-6.1.25/ 下面就是那个传说中的 3000 万行代码宇宙了。
为了方便后续操作,我们设个环境变量:
export LKP_KSRC=~/kernels/linux-6.1.25
⚠️ 别用 GUI 解压工具:真的,练练 tar 命令。将来你在服务器上或者是纯终端环境下(比如通过 SSH 连接的虚拟机),你会感谢这个习惯。
📖 额外赠送:源码树结构速览
在正式配置之前,让我们花一分钟(真的是一分钟)来看看这个庞然大物里面都有啥。
这就像是你刚买了一套乐高,拆开盒子先看说明书上的零件清单。
$ du -h .
1.5G .
大概 1.5 GB。
那个传说中的版本号在哪里定义?看一眼根目录的 Makefile:
$ head Makefile
# SPDX-License-Identifier: GPL-2.0
VERSION = 6
PATCHLEVEL = 1
SUBLEVEL = 25
EXTRAVERSION =
NAME = Hurr durr I'ma ninja sloth
你看,6.1.25 就在这里。那个 NAME 是每个内核版本的代号(Linus 的恶趣味)。
下面是一个极简的“地图”,告诉你东西大概都在哪:
| 目录/文件 | 是干嘛的 |
|---|---|
| kernel/ | 核心核心:调度器、信号、cgroups、模块管理。 |
| mm/ | 内存管理。 |
| fs/ | 文件系统:VFS 虚拟层、ext4、btrfs、nfs 都在这里。 |
| drivers/ | 驱动程序。这里是最大的一块,也是变化最快的地方。 |
| net/ | 网络协议栈。TCP/IP 的灵魂就在这。 |
| block/ | 块设备 I/O 层和缓存。 |
| crypto/ | 加密算法库。 |
| init/ | 内核初始化代码。那个传说中的 start_kernel() 就在 init/main.c 里。 |
| usr/ | 用来生成 initramfs 的代码。 |
| arch/ | 架构相关代码。x86, arm, arm64, riscv... 都在这里。 |
| Documentation/ | 官方文档。别去百度了,答案多半在这里。 |
看一眼就够了,不用死记硬背。你以后会在这些目录里迷路很多次的,那是常事。
Step 3 – 配置 Linux 内核
这是最关键的一步。
Linux 内核之所以能跑在手机上,也能跑在超级计算机上,全靠这一步:配置。
如果不配置,你需要面对几千个配置选项。如果配置错了,内核可能起不来,或者缺了某些关键功能(比如网络、USB)。
配置的核心是一个叫 Kconfig 的系统,它负责生成一个叫 .config 的隐藏文件。
Kconfig / Kbuild 极简入门
这里有一套基础设施在默默工作:
- Kconfig: 负责那个“菜单界面”。它读取
Kconfig文件,显示选项,处理依赖关系(比如选了 A 才能选 B)。 - Kbuild: 负责编译。它读取
.config文件,根据里面的内容决定编译哪些文件。
所有的配置最终都汇总到一个文件里:
.config (就在源码树的根目录下)。
在这个文件里,所有的配置项都长这样:
CONFIG_FOO=y # y = 编进内核镜像
CONFIG_BAR=m # m = 做成模块
CONFIG_BAZ=n # n = 不要这个功能
y (yes) 意味着这个功能会被硬塞进内核镜像里,开机就有,但体积会变大。
m (module) 意味着做成一个 .ko 文件,需要的时候再加载。
n (no) 意味着这玩意对你来说是隐形的。
获取初始配置:三条路
对于新手,直接手写 .config 是不可能的。我们通常从三个起点出发:
- 厂家配置:直接复制当前系统正在用的配置。
- 默认配置:让内核给你一个通用的配置(通常很大)。
- 精简配置:基于当前系统已加载的模块来生成。
策略 1:厂家的配置(安全但臃肿)
如果你在 Ubuntu 上想自己编一个内核,最稳妥的办法就是先用现在的:
make mrproper
cp /boot/config-$(uname -r) ${LKP_KSRC}/.config
这会生成一个巨大的 .config,里面啥都有。这能保证内核能跑起来,但编译时间会很长,体积也会很大。
策略 2:默认配置
make defconfig
这会根据你的 CPU 架构(比如 x86_64)给出一套标准配置。
策略 3:精简配置 —— 推荐给学习者
如果你想练手,或者想要一个比较苗条的内核,可以用 localmodconfig。
它的逻辑是:看看我现在系统里加载了哪些模块(lsmod),然后只把这些功能编译进去。
# 先把当前加载的模块列表存下来
lsmod > /tmp/lsmod.now
# 基于这个列表生成配置
cd ${LKP_KSRC}
make LSMOD=/tmp/lsmod.now localmodconfig
在运行过程中,如果新内核(6.1.25)有旧内核(5.19)没有的新选项,它会问你。这时候一般狂按 Enter(选默认值)就行。
最终,你会得到一个 .config 文件。这就是你的定制配方。
Step 4(其实是 Step 3 的延续)—— 调整配置
拿到 .config 之后,我们通常还需要微调一下。
这时候就需要用那个传说中的神器了:make menuconfig。
启动图形化菜单
cd ${LKP_KSRC}
make menuconfig
你会看到一个蓝色的、基于 ncurses 的图形界面(在 SSH 下也能用)。
导航与操作
- 方向键:移动。
- Enter:进入子菜单。
- Space (空格):切换状态(在
y,m,n之间循环)。 /(斜杠):搜索(比如搜你想找的驱动)。- Esc Esc:退出当前菜单。
符号说明
[ ]: 不编译。[*]: 编进内核。<M>: 编成模块。---: 被强制选为编译进内核(通常是依赖项)。
实战演示:开启 .config 查看支持
我们来做一个小实验,开启一个很有用的功能:让内核运行时能查看自己的配置。
- 进入菜单。
- 找到 General setup -> Kernel .config support。
- 把它从
<M>(模块) 改成[*](内置)。 - 选中它下面的 Enable access to .config through /proc/config.gz,把它选上
[*]。 - 退出并保存。
这会启用两个配置宏:
CONFIG_IKCONFIG=yCONFIG_IKCONFIG_PROC=y
以后内核跑起来的时候,你只要 zcat /proc/config.gz 就能看到它当时的编译选项了,这对调试非常有帮助。
配置进阶:脚本化与差异化
如果你是搞自动化构建的,menuconfig 那套交互界面可能不适合你。
脚本修改配置
内核源码里自带了一个脚本:scripts/config。
你可以直接在命令行里修改 .config:
# 禁用某个选项
./scripts/config --disable IKCONFIG
# 启用某个选项
./scripts/config --enable IKCONFIG
# 查看某个选项的状态
./scripts/config --state IKCONFIG
# 输出: y
⚠️ 注意:用脚本改配置就像在黑暗里挥刀——它不管依赖关系。改完记得跑一下编译测试一下。
查看配置差异
你改了一堆配置,想看看到底动了哪里?用 diffconfig:
./scripts/diffconfig .config.old .config
它会列出哪些选项变了,从什么值变成了什么值,一目了然。
安全加固:Kconfig Hardened Check
如果你是搞安全或者做产品的,内核配置的安全性至关重要。
有一个叫 kconfig-hardened-check 的工具,专门用来检查你的内核配置是否符合 KSPP (Kernel Self Protection Project) 的安全建议。
# 安装
git clone https://github.com/a13xp0p0v/kconfig-hardened-check
cd kconfig-hardened-check
# 检查你的 .config
python3 kconfig-hardened-check.py --config ${LKP_KSRC}/.config
它会告诉你哪里开启了弱项,哪里应该加固。比如 CONFIG_BUG_ON_DATA_CORRUPTION 之类的。
定制 Kconfig 菜单:加上你自己的名字
假设你写了一个牛逼的驱动或者功能,想把它加进内核菜单里。怎么做?
这其实分为两步:
- Kconfig: 告诉菜单系统有个新选项。
- Makefile: 告诉编译系统对应的代码在哪。
实验:加个“LKP 测试选项”
我们来玩个真的。在 General setup 菜单里加一个我们自己的选项。
-
备份文件:
cp init/Kconfig init/Kconfig.orig -
编辑文件: 找到
init/Kconfig。找个空地(比如LOCALVERSION_AUTO附近),插进去这段:config LKP_OPTION1bool "Test case for LKP 2e book/Ch 2: creating ..."default nhelpThis is a dummy test option added for learning purposes.Selecting this does absolutely nothing except provethat you know how to edit Kconfig files.config: 定义了一个宏叫CONFIG_LKP_OPTION1。bool: 这是一个布尔选项(开/关)。help: 下面那几行就是帮助文档。
-
验证修改:
make menuconfig进入 General setup,往下翻,你应该能看到你的新选项了!
-
启用它: 选中它,按空格把它变成
[*]。保存退出。 -
检查结果:
grep "LKP_OPTION1" .config# CONFIG_LKP_OPTION1=y恭喜,你刚刚黑进了 Linux 内核的构建系统。
-
关联代码(高级): 如果你真的写了代码(比如
lkp_test.c),你还得修改 Makefile:在
init/Makefile(或者你代码所在目录的 Makefile)里加上:obj-$(CONFIG_LKP_OPTION1) += lkp_test.o这样,当你选
Y时,它会被编译进内核;选N时,编译器会无视它。
本章小结
这一章我们干了挺多事。
首先,我们搞清楚了 6.x 内核的版本号是怎么来的——那是 Linus 数手指头数出来的,代表了时间,而不是功能。我们也知道了 LTS 是个什么东西,以及为什么你要选它。
然后,我们上手了:下载了源码,解压了它,还花了一分钟认了认这 3000 万行代码的布局。
最重要的是,我们花了大篇幅去理解 Kconfig 和 Kbuild——这是构建内核的基石。我们学会了怎么获取一个基础配置(defconfig 或 localmodconfig),怎么通过 make menuconfig 调整它,甚至怎么写自己的 Kconfig 脚本给内核加个菜单项。
现在,你的工作目录里应该有一个配置好的内核源码树,里面躺着一个写满了你选择的 .config 文件。
别急着休息。下一章,我们要按下那个红色的按钮了。
我们要运行 make。
我们要把这 3000 万行代码变成一个二进制文件。
我们要把它装进系统里,重启,看着屏幕上滚动的 log 变成你自己编译的内核名字。
那才是真正的“Hello World”。
练习题
练习 1:版本号考古 ⭐(理解)
找一台你手边能接触到的 Linux 机器(云服务器、虚拟机、WSL 都行),运行 uname -r。
- 它的版本号是多少?
- 它是 Stable 版本吗?还是发行版魔改版?怎么判断的?
- 如果它是 5.x 的,你能猜到它大概是哪年发布的吗?(提示:数手指)
练习 2:配置项狩猎 ⭐⭐(应用)
使用 / 搜索功能在 make menuconfig 中找到以下选项,并记录它们的位置和依赖关系:
CONFIG_KASAN(Kernel Address Sanitizer)CONFIG_DEBUG_INFO_BTF(eBPF 相关)- 找一个和你电脑硬件相关的驱动(比如 WiFi 或者显卡驱动),看看它依赖什么?
练习 3:自定义模块构建 ⭐⭐⭐(实战)
这题有难度,如果你做出来了,说明你真的懂了。
- 编写一个极其简单的内核模块(只要
init和exit函数打印 Hello World)。 - 把这个文件放到内核源码树的
drivers/char/目录下。 - 修改该目录下的
Makefile和Kconfig,使得你可以通过make menuconfig选中你的模块。 - 编译成模块(.ko),并成功加载。
提示:你需要参考该目录下其他驱动的写法。
附录:常用命令速查
# 1. 准备环境(清理旧的配置)
make mrproper
# 2. 获取当前系统的模块列表(用于精简配置)
lsmod > /tmp/lsmod.now
# 3. 生成本地化配置(仅包含当前硬件需要的驱动)
make LSMOD=/tmp/lsmod.now localmodconfig
# 4. 打开图形化配置界面
make menuconfig
# 5. 查看配置差异(改完配置后)
./scripts/diffconfig .config.old .config
# 6. 脚本化修改配置(例如开启 IKCONFIG)
./scripts/config --enable IKCONFIG
./scripts/config --enable IKCONFIG_PROC
# 7. 检查内核配置安全性(需安装工具)
kconfig-hardened-check --config .config
练习题
练习 1:understanding
题目:根据 Linux 内核版本命名规范 'w.x[.y][-z]',内核版本号 '6.1.25-custom' 中的各个数字和字符分别代表什么含义?请结合“手脚模型”解释为什么主版本号从 5 变成了 6。
答案与解析
答案:w=6 (主版本号), x=1 (次版本号), y=25 (补丁级别/修订版本), -z=-custom (额外版本/本地版本)。
根据“Fingers-and-toes”模型,当次版本号(x)大约累积到 20 个(即从 0 到 19,手指加脚趾的总数)时,主版本号(w)就会增加。因此,内核从 5.19 演进到了 6.0。
解析:本题考察对内核版本号结构和演进规则的理解。
- 版本号结构:遵循 major.minor.patchlevel-extraversion 格式。
- 手脚模型:这是一种非严格时间基础的版本演进策略。Linus 形象地比喻为用完手脚(20个)计数后就进一位。历史上,3.x、4.x、5.x 系列都在第 20 个次版本结束时结束了该主版本周期(例如 5.19 之后是 6.0),这并不意味着巨大的架构变更,而是有机的演进。
练习 2:application
题目:在内核构建系统的 Kconfig 语言中,depends on 和 select 是两种常见的依赖关系机制。请简述这两种机制的区别,并解释为什么在编写配置选项时通常建议优先使用 depends on 而不是 select?
答案与解析
答案:区别:
depends on(正向依赖):当前选项依赖于指定选项。如果依赖项未满足(为 n),当前选项将不可见或不可选。select(反向依赖/自动选择):当前选项被选中时,自动强制选中另一个选项(无论用户是否意识到)。
建议原因:
select 会强制开启隐含依赖,可能导致用户无意中开启了不需要或不兼容的代码,增加内核大小或引发冲突。depends on 则是向用户展示依赖关系,由用户主动解决,更符合模块化设计原则。
解析:本题考察在实际编写内核配置(BSP 或驱动开发)时的最佳实践。
depends on是一种“可见性限制”,例如depends on NET表示只有在网络子系统开启时,此选项才会在菜单中显示。select是一种“强制的传递依赖”,例如选择了 USB 存储驱动可能会自动selectSCSI 支持。- 风险在于,如果被
select的选项有它自己的依赖冲突,用户可能会面临复杂的配置错误。因此,除非必须开启某些底层基础设施(如总线或核心库),否则应尽量避免使用select。
练习 3:thinking
题目:假设你是一家嵌入式公司的系统工程师,正在规划一款基于新型 ARM SoC 的工业控制设备,预计产品生命周期为 10 年。你需要选择合适的 Linux 内核源树作为基础。
选项 A:使用 SoC 厂商提供的基于旧版 LTS 的厂商内核(含 BSP 驱动)。 选项 B:使用最新的主线 LTS 内核(如 6.1 LTS),并通过 Yocto 等工具集成 BSP 层。
请结合内核维护周期、技术债务和安全性,分析选择选项 B 的利弊。
答案与解析
答案:结论:应选择选项 B(最新 LTS 内核 + Yocto/社区集成)。
利(理由):
- 长期支持与可维护性:厂商内核通常基于特定旧版本并分叉,长期维护会面临“技术债务”,难以跟上上游安全补丁。而主线 LTS(如 6.1)有社区支持,且 Yocto 等项目能提供持续的元数据更新。
- 安全性:工业设备需长期运行,旧内核容易积累未修复的 CVE 漏洞。主线 LTS 能更快获得安全修复。
- 驱动更新:利用设备树机制,可以更容易地将厂商的硬件支持移植到新内核,而不依赖于厂商修改的旧内核源码。
弊(挑战):
- 初期开发成本:如果 SoC 厂商未将驱动提交到主线,你可能需要自己移植驱动或编写设备树。
- 兼容性风险:新内核的 API 可能发生变化,需要验证所有 BSP 组件的兼容性。
解析:本题考察对内核生态、供应链安全和工程架构的综合思考能力。
- SoC 厂商内核:虽然“开箱即用”,但往往是一次性的,一旦厂商停止支持该旧内核,产品将面临巨大的安全风险和升级困难。
- 主线 LTS 策略:符合“上游优先”原则。虽然门槛较高,但利用
Device Tree(硬件描述与代码解耦)和现代构建系统(Yocto/Buildroot),可以实现长期、可持续的维护,这对于 10 年寿命周期的工业设备至关重要。 - Super LTS (SLTS):题目中提到的 CIP SLTS 也是解决此问题的方案之一,通常也是基于主线 LTS 的超长维护分支,选项 B 更符合这一思路。
要点提炼
理解 Linux 内核严格的开发节奏和版本规则是构建前的首要必修课。现代内核开发遵循固定的“合并窗口”与“修复窗口”周期,新功能仅在合并窗口开启的两周内被接受,其余时间仅修补 Bug。版本号(如 6.1)不再沿用旧时的奇偶数稳定/开发区分法,而是基于时间的“有机演化”;对于构建者而言,选择 LTS(长期支持) 版本(如本书使用的 6.1)通常是平衡稳定性与维护周期的最佳实践,这能确保你在数年的使用中获得持续的安全补丁支持。
内核构建的基石在于掌握 Kconfig/Kbuild 配置系统,它决定了哪些代码被编译以及如何编译。内核之所以能适配从手机到超级计算机的各种硬件,全靠这一步进行定制。核心配置最终汇总为根目录下的 .config 文件,其中的 y(编译进内核)、m(编译为模块)和 n(不编译)状态决定了内核的功能集与体积。盲目手动编写该文件不可行,最佳实践是利用当前系统的配置(如 /boot/config-xxx)或默认配置作为起点。
获取源码并正确解压是动手实战的第一步,推荐直接从 Kernel.org 下载确定版本(如 linux-6.1.25.tar.xz)的压缩包,以避免完整 Git 仓库克隆带来的巨大时间与空间开销。解压后,源码树呈现高度结构化的特征,核心代码如调度器(kernel/)、内存管理(mm/)、驱动(drivers/)和架构相关代码(arch/)各司其职。理解这一目录结构就像查看乐高零件清单,是后续进行针对性修改或驱动开发的基础。
对于学习者或开发者,使用 make menuconfig 是调整配置最直观且有效的方法。这个基于 ncurses 的图形化界面允许用户通过搜索、导航来微调内核选项,例如开启 Kernel .config support 以便在系统运行时通过 /proc/config.gz 查看当前配置。若要进行自动化构建,则需掌握 scripts/config 等脚本工具来修改 .config,并使用 diffconfig 来检查配置变更,确保只启用了必要的功能以减小内核体积。
真正的内核定制能力体现在能够“黑进”构建系统,即通过修改 Kconfig 和 Makefile 将自定义代码或选项集成进内核菜单。这一过程不仅涉及编写配置语法(定义 config 选项、依赖关系和帮助文档),还需要在构建脚本中正确关联对象文件(如 obj-$(CONFIG_FOO) += bar.o)。这种深度的干预是将内核开发从单纯的“编译”提升到“系统级编程”的关键,也是完成后续编译与安装模块的必要前提。