从源码构建Linux内核是开始内核开发之旅的一个有趣方式!请放心,这段旅程既漫长又艰难,但这正是其中的乐趣,不是吗?内核构建这个话题本身就足够庞大,因此我们将其分为两章来讨论,这一章和下一章。
回顾我们在在线发布的扩展版第一章中所学到的内容(www.packtpub.com/sites/defau…):主要是如何为Linux内核编程设置工作空间。您还了解了用户和内核文档的来源以及几个与内核/驱动开发密切相关的有用项目。到现在为止,我假设您已经完成了在线发布的第一章内容,并且设置好了工作空间环境;如果还没有,请在继续之前完成这些步骤。
本章和下一章的主要目的是详细描述如何从源码构建一个现代的Linux内核。在本章中,您将首先学习必要的基础知识:内核版本命名法、开发流程以及不同类型的源码树。接着我们将进行实际操作:您将学习如何将一个稳定的原生Linux内核源码树下载到虚拟机(VM)中的Linux系统上。这里所说的“原生内核”是指由Linux内核社区在其仓库(www.kernel.org)中发布的默认内核源码。之后,您将了解内核源码的布局——即内核代码库的概览。接下来是实际的内核构建步骤。
在继续之前,先了解一条关键信息:任何现代的Linux系统,无论是超级计算机还是小型嵌入式设备,都需要三个基本组件:
- 一个引导程序
- 一个操作系统(OS)内核
- 一个根文件系统
此外,它还可能有两个可选组件:
- 如果处理器系列是ARM或PPC(32位或64位),则需要一个设备树Blob(DTB)镜像文件
- 一个initramfs(或initrd)镜像文件
在这两章中,我们只关注如何从源码构建操作系统(Linux)内核。我们不会深入讨论根文件系统的细节。在下一章中,我们将学习如何最低限度地配置x86特定的GNU GRUB引导程序。
完整的内核构建过程——至少对于x86[_64]来说——共需要六到七个步骤。除了必要的准备工作,我们将在本章中讨论前三个步骤,剩余的将在下一章中介绍。
在本章中,我们将讨论以下主题:
- 内核构建的准备工作
- 从源码构建内核的步骤
- 第一步——获取Linux内核源码树
- 第二步——提取内核源码树
- 第三步——配置Linux内核
- 自定义内核菜单、Kconfig并添加我们自己的菜单项
您可能会想:如何为另一种CPU架构(如ARM 32位或64位)构建Linux内核?我们将在下一章中详细讨论这一点!
技术要求
我假设您已经阅读了在线章节《内核工作空间设置》,并且已经适当地准备了一个运行Ubuntu 22.04 LTS(或同等版本)的x86_64虚拟机,并安装了所有必需的软件包。如果还没有,我强烈建议您先完成这一步。
为了最大限度地利用本书内容,我还强烈建议您克隆本书的GitHub仓库(github.com/PacktPublis…),并以实践操作的方式进行学习。
内核构建的准备工作
在我们开始构建和使用Linux内核的旅程时,了解一些基本信息是非常重要的,这将帮助您更好地理解和操作。首先,Linux内核及其相关项目是完全去中心化的——这是一个虚拟的在线开源社区!与Linux相关的最接近“办公室”的地方就是这里:Linux内核(以及数十个相关项目)的管理由Linux基金会(linuxfoundation.org/)负责。此外,它还管理着Linux Kernel Organization,这是一家将Linux内核免费向公众发布的私营基金会(www.kernel.org/nonprofit.h…)。
您知道吗?根据GNU GPLv2许可证的条款——Linux内核就是在这一许可证下发布的,并将在可预见的未来继续使用这一许可证——并不会以任何方式阻止原始开发者为他们的工作收费!只是Linus Torvalds以及现在的Linux基金会选择将Linux内核软件免费提供给所有人。这并不妨碍商业组织为内核增加价值(以及与内核捆绑的产品),并通过初始收费或如今典型的SaaS/IaaS订阅模式来收费。
因此,开源绝对是可行的商业模式,这一点已经并正在每天得到证明。那些核心业务在其他领域的客户只是希望获得物有所值的产品,而围绕Linux建立的企业可以通过为客户提供专家级的支持、详细的服务水平协议(SLA)以及升级来实现这一点。
我们将在本节讨论的一些关键点包括以下内容:
- 内核发布版本号的命名规则
- 典型的内核开发流程
- 仓库中存在的不同类型的内核源码树
掌握这些信息后,您将更好地应对内核构建过程。好了,让我们逐一了解这些关键点。
理解Linux内核版本命名规则
要查看内核版本号,只需在Shell中运行uname -r命令。那么,如何准确解读uname -r的输出呢?在我们的x86_64架构的Ubuntu 22.04 LTS虚拟机中,我们运行uname命令,并传递-r选项以仅显示当前的内核发布版本或版本号:
$ uname -r
5.19.0-40-generic
当然,当您阅读这段文字时,Ubuntu 22.04 LTS的内核很可能已经升级到一个更新的版本;这是完全正常的。我在编写本章时,遇到的内核版本是5.19.0-40-generic,这是当时Ubuntu 22.04.2 LTS的内核版本。
现代Linux内核的版本号命名规则如下:
major#.minor#[.patchlevel][-EXTRAVERSION]
这通常也写作或描述为w.x[.y][-z]。方括号内的patchlevel和EXTRAVERSION(或y和-z)组件表示它们是可选的。下表总结了版本号各组件的含义:
| 版本号组件 | 含义 | 示例数字 |
|---|---|---|
| Major #(或 w) | 主版本号,当前我们使用的是6.x内核系列,因此主版本号是6。 | 2, 3, 4, 5, 6 |
| Minor #(或 x) | 次版本号,层级上在主版本号之下。 | 0及以上 |
| [patchlevel](或 y) | 层级上在次版本号之下——也称为ABI或修订版本——当需要进行重要的漏洞/安全修复时,应用于稳定内核。 | 0及以上 |
| [-EXTRAVERSION](或 -z) | 也称为localversion,通常由发行版内核和供应商用于跟踪其内部更改。 | 各不相同;Ubuntu使用w.x.y-<z>-generic |
表2.1:Linux内核版本命名规则
因此,我们可以解释Ubuntu 22.04 LTS发行版的内核版本号5.19.0-40-generic:
- Major #(或w) : 5
- Minor #(或x) : 19
- [patchlevel](或y) : 0
- [-EXTRAVERSION](或-z) : -40-generic
请注意,发行版内核可能不会严格遵循这些命名规则;这取决于各自的发行版。然而,在www.kernel.org/上发布的常规或原生内核确实遵循这些命名规则(至少在Linus决定更改之前是这样的)。
历史上,在2.6版本之前的内核中(也就是说,现在已经是很古老的内容了),次版本号有特殊意义:如果是偶数,表示这是一个稳定的内核版本;如果是奇数,则表示这是一个不稳定或测试版的内核版本。现在这种规则已经不再适用了。
作为配置内核的一个有趣练习的一部分,我们稍后将更改我们构建的内核的localversion(即-EXTRAVERSION)组件。
“手指和脚趾”版本
接下来,理解一个简单的事实是非常重要的:对于现代Linux内核,当内核的主版本号和/或次版本号发生变化时,并不意味着某种巨大的或关键的新设计、架构或特性出现了;不,正如Linus所说的,这仅仅是有机的演进。
当前使用的内核版本命名法更多是基于时间而不是基于特性。因此,每隔一段时间就会出现一个新的主版本号。具体是多久呢?Linus喜欢称其为“手指和脚趾”模型;当他用完手指和脚趾来数次版本号(w.x.y版本中的x组件)时,他就会将主版本号从w更新为w+1。因此,在迭代了20个次版本号(从0到19)之后,我们会得到一个新的主版本号。
实际上,这种情况自3.0内核以来一直如此,因此我们有以下情况:
- 3.0到3.19(20个次版本)
- 4.0到4.19(20个次版本)
- 5.0到5.19(20个次版本)
- 6.0到……(还在继续;您明白了吧!)
请参见图2.1来了解这一点。每个次版本的发布周期大约为6到10周。
内核开发工作流程——理解基础知识
在这里,我们将简要概述典型的内核开发工作流程。对于像您这样对内核开发感兴趣的人来说,至少应该对这个过程有基本的了解。
详细描述可以在官方内核文档中找到,链接如下:www.kernel.org/doc/html/la…。
一个常见的误解,尤其是在Linux内核的早期阶段,是认为Linux内核是以一种临时、随意的方式开发的。这完全不是真的!内核开发过程已经演变成一个运作良好的系统,拥有详尽的文档记录,并对内核贡献者应该了解的内容有明确的预期。完整的细节请参阅前面的链接。
为了让我们一窥典型的开发周期,假设我们已经将最新的主线Linux Git内核树克隆到了我们的系统中。
关于强大的Git源码管理工具(SCM)的使用细节超出了本书的范围。请参阅进一步阅读部分,获取有关学习如何使用Git的有用链接。显然,我强烈建议至少熟悉Git的基本使用方法。
如前所述,在撰写本文时,6.1内核是一个长期稳定版本(LTS),其预期的EOL日期(生命周期结束日期)最远(2026年12月),因此我们将在接下来的材料中使用它。
那么,它是如何演变而来的呢?显然,它是从早期的候选发布(rc)内核以及之前的稳定内核版本演变而来的,在这种情况下,就是v6.1-rc’n’内核和之前的稳定版v6.0内核。我们可以通过两种方式来看待这种演变:通过命令行和通过内核的GitHub页面以图形方式查看。
通过命令行查看内核的Git日志
我们使用git log命令来获取按日期排序的内核Git树中标签的可读日志。这里,因为我们主要关注6.1 LTS内核的发布,所以特意截取了以下输出的相关部分进行展示:
需要注意的是,git log命令(我们在下面的代码块中使用的命令,以及实际上任何其他git子命令)仅适用于Git树。我们使用以下命令只是为了展示内核的演变过程。稍后,我们将展示如何克隆一个Git内核源码树。
$ 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)
2023-04-02 14:29:29 -0700 7e364e56293b (tag: v6.3-rc5)
[ … ]
2023-03-05 14:52:03 -0800 fe15c26ee26e (tag: v6.3-rc1)
2023-02-19 14:24:22 -0800 c9c3395d5e3d (tag: v6.2)
2023-02-12 14:10:17 -0800 ceaa837f96ad (tag: v6.2-rc8)
[ … ]
2022-12-25 13:41:39 -0800 1b929c02afd3 (tag: v6.2-rc1)
2022-12-11 14:15:18 -0800 830b3c68c1fb (tag: v6.1)
2022-12-04 14:48:12 -0800 76dcd734eca2 (tag: v6.1-rc8)
2022-11-27 13:31:48 -0800 b7b275e60bcd (tag: v6.1-rc7)
2022-11-20 16:02:16 -0800 eb7081409f94 (tag: v6.1-rc6)
2022-11-13 13:12:55 -0800 094226ad94f4 (tag: v6.1-rc5)
2022-11-06 15:07:11 -0800 f0c4d9fc9cc9 (tag: v6.1-rc4)
2022-10-30 15:19:28 -0700 30a0b95b1335 (tag: v6.1-rc3)
2022-10-23 15:27:33 -0700 247f34f7b803 (tag: v6.1-rc2)
2022-10-16 15:36:24 -0700 9abf2313adc1 (tag: v6.1-rc1)
2022-10-02 14:09:07 -0700 4fe89d07dcc2 (tag: v6.0)
2022-09-25 14:01:02 -0700 f76349cf4145 (tag: v6.0-rc7)
[ … ]
2022-08-14 15:50:18 -0700 568035b01cfb (tag: v6.0-rc1)
2022-07-31 14:03:01 -0700 3d7cb6b04c3f (tag: v5.19)
2022-07-24 13:26:27 -0700 e0dccc3b76fb (tag: v5.19-rc8)
[ … ]
在上面的输出中,您可以首先看到,当我运行这个git log命令时(2023年4月末),6.3内核刚刚发布!您还可以看到,在这个发布之前有七个rc内核版本,它们的编号是6.3-rc1、6.3-rc2、……、6.3-rc7。
进一步深入,我们找到了所需的内容——您可以清楚地看到,6.1稳定版(LTS)内核的最初发布日期是2022年12月11日,而它的前一个版本6.0是在2022年10月2日发布的。您还可以通过查看其他有用的内核资源(如kernelnewbies.org/LinuxVersio…)来验证这些日期。
对于最终导致6.1内核的开发系列来说,后一日期(2022年10月2日)标志着为期大约两周的下一个稳定内核的合并窗口期的开始。在此期间,开发人员可以向内核树提交新代码。实际上,真正的工作早在这之前就已经开始了;这些工作的成果现在由子系统维护者合并到主线中。
我们尝试在图2.1中绘制这一过程的时间线,您可以看到早期的内核(从3.0开始)有20个次版本。我们为目标内核6.1 LTS展示了更多细节。
在6.1内核的合并窗口开始两周后(2022年10月2日),即2022年10月16日,合并窗口关闭,rc内核的工作开始,6.1-rc1是第一个rc版本。-rc(也称为预发布)树主要致力于合并补丁和修复(回归和其他)错误,最终由主要维护者(Linus Torvalds和Andrew Morton)决定成为“稳定”内核树。
rc内核或预发布版本的数量有所不同;通常,这个“错误修复”窗口大约持续6到10周,之后发布新的稳定内核。在上面的输出中,我们可以看到八个候选版本内核(从6.1-rc1到6.1-rc8)最终在2022年12月11日发布了v6.1稳定版,共耗时70天,即10周。您可以访问kernelnewbies.org/LinuxVersio…的6.x部分来确认这一点。
为什么图2.1从2.6.12内核开始?答案很简单:这是从该版本开始维护Git内核历史记录的第一个版本。
另一个问题是,为什么在图中显示5.4(LTS)内核?因为它也是一个LTS内核(预计EOL日期为2025年12月),而且它是我们在本书第一版中使用的内核版本!
通过GitHub页面查看内核的Git日志
可以通过Linus的GitHub仓库的发布/标签页面以更直观的方式查看内核日志,链接如下:github.com/torvalds/li…。
图2.2显示了v6.1内核标签的截屏。这是如何做到的呢?多次点击“Next”按钮(此处未显示),即可进入剩余页面,在这些页面中可以找到v6.1-rc'n'候选发布版本的内核。或者,您也可以直接导航到github.com/torvalds/li…。
前面的截屏部分展示了v6.1的两个候选版本内核,6.1-rc7和6.1-rc8,最终在2022年12月12日发布了LTS 6.1版本。
工作从未真正停止过:如图所示,到2023年1月初,v6.2-rc3候选版本内核已经发布,最终在2023年2月19日发布了v6.2内核。接着,6.3-rc1内核在2023年3月6日发布,随后又发布了六个候选版本,最终在2023年4月23日发布了稳定的6.3内核。这个过程还将继续……
同样,在您阅读此内容时,内核版本可能已经大大超前了。但这没关系——6.1 LTS内核将会被维护相对较长的一段时间(回忆一下,预计的EOL日期是2026年12月),因此它对产品和项目来说是一个非常重要的版本!
内核开发工作流程概述
以6.x内核系列为例,内核开发的工作流程如下。您可以同时参考图2.3,我们在其中绘制了6.0内核如何演变为6.1 LTS的过程。
-
发布一个6.x稳定版本(在我们的例子中,x为0)。因此,6.x+1主线内核的两周合并窗口开启。
-
合并窗口保持开放大约两周,各子系统维护者将新的补丁合并到主线内核中。这些维护者在很长一段时间内一直仔细地接受贡献者的补丁并更新他们的代码树。
-
大约两周后,合并窗口关闭。
-
接下来是“错误修复”期;rc(或主线、预发布)内核开始发布。它们的演变过程如下:发布6.x+1-rc1、6.x+1-rc2、...、6.x+1-rcn。这一过程可能持续6到8周。
-
接着进入“最终确认”期,通常持续约一周。最终发布新的6.x+1稳定内核版本。
-
该版本交给“稳定团队”:
- 重要的错误或安全修复会导致6.x+1.y版本的发布,如6.x+1.1、6.x+1.2、...、6.x+1.n。我们将主要使用6.1.25内核进行工作,这里的y值为25。
-
该版本将被维护,直到下一个稳定版本发布或达到生命周期结束(EOL)日期,6.1 LTS的预计EOL日期为2026年12月。在此之前,错误和安全修复将继续应用。
整个过程将不断重复。
所以,假设您正在尝试提交一个补丁,但错过了合并窗口。对此无能为力:您(或更可能是负责推动您补丁系列的子系统维护者)只能等到下一个合并窗口大约在2.5到3个月后到来。这就是流程;我们并不急于求成。
练习
请按照我们所做的步骤,跟随当前最新稳定内核的演变过程。(快速提示:查看内核发布历史的另一种方式:en.wikipedia.org/wiki/Linux_…)。
因此,当您现在看到Linux内核发布时,版本名称和涉及的过程将变得更加清晰。接下来,我们将继续了解不同类型的内核源码树。
探索内核源码树的类型
Linux内核源码树有几种类型,其中一个关键的是长期支持(LTS)内核。LTS内核是一个“特殊”的发布版本,因为内核维护者将在其生命周期内持续向其回溯移植重要的错误修复和安全修复。根据惯例,每年最后发布的内核通常会被“标记”为LTS版本,通常在12月。
LTS内核的“寿命”通常至少为2年,并且可以延长至更长时间。我们将在本书中使用的6.1.y LTS内核是第23个LTS内核,预计生命周期为4年——从2022年12月到2026年12月。
以下是摘自维基百科页面“Linux内核版本历史”(en.wikipedia.org/wiki/Linux_…)的图片片段,已经涵盖了所有内容:
有趣的是,5.4 LTS内核将维持到2025年12月,而5.10 LTS的维持时间与6.1 LTS相同,直到2026年12月。
需要注意的是,代码库中有几种类型的发布内核。以下是一个不完整的列表,按稳定性从低到高排序(因此它们的生命周期从最短到最长):
- -next 树:这是最新的(箭头的顶端!),子系统树在这里收集新补丁以进行测试和审核。这是上游内核贡献者将要处理的内容。如果您打算将补丁提交到内核(贡献),您必须使用最新的 -next 树。
- 预发布版本,也称为 -rc 或主线:这些是发布之前生成的候选内核版本。
- 稳定内核:顾名思义,这是主要版本。发行版和其他项目通常会选择这些内核(至少是最初)。这些内核也被称为原生内核(vanilla kernels)。
- 发行版和LTS内核:发行版内核(显然)是由发行版提供的内核。它们通常以原生/稳定内核为基础开始。LTS内核是特别维护的、生命周期较长的内核,尤其适用于工业/生产项目和产品。特别是对于企业级发行版,您会发现许多似乎使用“旧”内核。这种情况甚至可能出现在一些安卓设备供应商中。现在,即使
uname -r显示内核版本是4.x,也不一定意味着它已经过时。不,发行版/供应商/OEM通常有一个内核工程团队(或外包给一个团队)定期用新的和相关的补丁(尤其是关键的安全和错误修复)更新旧的内核。因此,尽管版本可能看起来已经过时,但事实并非如此!
然而,这意味着对内核工程团队来说工作量非常大,而且仍然很难跟上最新稳定内核的步伐;因此,最好还是使用原生内核。
在本书中,我们将始终使用最新的、生命周期最长的LTS内核。
在撰写本文时,6.1.x LTS内核是最新的LTS内核,预计的EOL日期为2026年12月,这确保了本书的内容在未来几年内仍然是最新和有效的!
这里还需要提到另外两种类型的内核源码树:超级LTS(SLTS)内核和芯片(SoC)供应商内核。SLTS内核由民用基础设施平台(Civil Infrastructure Platform,www.cip-project.org/)维护,维护时间比LTS内核更长。这是Linux基金会的一个项目。引用他们网站上的内容:
“...CIP项目致力于建立一个开源的‘基础层’,以工业级软件为基础,使其能够在民用基础设施项目中使用和实施软件构件。目前,民用基础设施系统通常是从零开始构建的,几乎没有重复利用现有的软件构件。CIP项目旨在创建可重复使用的构件,以满足工业和民用基础设施的安全性、可靠性等要求。”
事实上,截至本文撰写时,最新的CIP内核——SLTS v6.1和SLTS v6.1-rt——基于6.1 LTS内核,其预计的EOL日期为2033年8月(10年)!此外,SLTS 5.10和SLTS 5.10-rt内核的预计EOL日期为2031年1月。有关最新信息,请参见此Wiki网站:wiki.linuxfoundation.org/civilinfras…。
LTS 内核的新规定
一个快速而重要的更新!在2023年9月于西班牙毕尔巴鄂举行的开源峰会上,Jonathan Corbet在他著名的“内核报告”演讲中宣布了一个重要消息:从现在开始,LTS内核的维护期限将仅为两年。
如果您想进一步了解,可以参考以下资源:
- The Kernel Report, Jon Corbet, September 2023, OSS EU, Spain;YouTube链接: Kernel Report 视频;幻灯片(PDF):Kernel Report 幻灯片
- Long-term support for Linux kernels is about to get a lot shorter, Liam Proven, The Register, 2023年9月:文章链接
这个消息可能会让很多人感到意外。为什么只维护两年?简要来说,给出了两个原因:
首先,为什么要维护一个多年没人真正使用的内核系列?许多企业内核供应商以及SoC供应商都维护自己的内核。
其次,一个不幸而严重的问题:维护者疲劳。内核社区很难持续维护所有这些LTS内核——目前有7个主要的LTS版本(4.14、4.19、5.4、5.10、5.15、6.1和6.6)!随着时间的推移,维护的负担只会越来越重。例如,4.14 LTS系列已经有大约300次更新和接近28,000个提交。此外,旧的错误最终会浮现出来,需要大量工作来在现代LTS内核中修复它们。不仅如此,后来内核中出现的新错误必须被修复,然后再向旧的仍在维护的LTS内核回移植。这一切都累积起来了。
截至目前,我们正在使用的6.1 LTS内核预计将按原计划维护到2026年12月。
继续前进,我们简要讨论一下前面提到的第二种内核源码树类型:硅芯片/芯片组(SoC)供应商内核。这些供应商往往为他们支持的各种板卡/芯片组维护自己的内核。他们通常基于现有的原生LTS内核(不一定是最新的!),然后在其基础上构建,添加供应商特定的补丁、板卡支持包(BSP)内容、驱动程序等等。当然,随着时间的推移,他们的内核与最新的稳定内核之间的差异可能会变得相当显著,导致难以维护的问题,比如需要不断地将关键的安全/错误修复补丁回移植到他们的内核中。
在使用此类芯片项目时,也许最好的方法是基于现有的行业强度解决方案,例如Yocto项目(www.yoctoproject.org/),该项目在保持最近的LTS内核与供应商层同步关键安全/错误修复补丁方面做得非常出色。例如,截至本文撰写时,最新的稳定Yocto版本——Nanbield 4.3——同时支持6.1 LTS和更近期的6.5非LTS内核;当然,特定版本可能会因架构(处理器家族)而有所不同。
因此,内核源码树的类型是多种多样的。不过,我建议您参考kernel.org的发布页面获取有关发布内核类型的详细信息。进一步了解更多内容,请访问开发过程的工作原理。
通过非交互式脚本化方式查询存储库 kernel.org,可以使用curl。以下是截至2023年12月6日Linux的状态输出:
$ curl -L https://www.kernel.org/finger_banner
The latest stable version of the Linux kernel is: 6.6.4
The latest mainline version of the Linux kernel is: 6.7-rc4
The latest stable 6.6 version of the Linux kernel is: 6.6.4
The latest stable 6.5 version of the Linux kernel is: 6.5.13 (EOL)
The latest longterm 6.1 version of the Linux kernel is: 6.1.65
The latest longterm 5.15 version of the Linux kernel is: 5.15.141
The latest longterm 5.10 version of the Linux kernel is: 5.10.202
The latest longterm 5.4 version of the Linux kernel is: 5.4.262
The latest longterm 4.19 version of the Linux kernel is: 4.19.300
The latest longterm 4.14 version of the Linux kernel is: 4.14.331
The latest linux-next version of the Linux kernel is: next-20231206
$
当您阅读本文时,内核几乎肯定已经进一步演变,并且会出现更高版本。对于这样的书籍,我能做的最好选择是撰写时接近最新的、具有最长预计EOL日期的稳定LTS内核:6.1.x LTS。
当然,这已经发生了!6.6内核于2023年10月29日发布,并且截至撰写本文时(就在印刷前),它实际上已被标记为LTS内核,预计EOL日期为2026年12月(与6.1 LTS相同)。
为了说明这一点,值得注意的是,本书的第一版使用的是5.4.0内核,因为5.4是当时寿命最长的LTS内核系列。今天,当我撰写本文时,5.4 LTS仍在维护中,预计EOL日期为2025年12月,最新的稳定版本是5.4.268。
我应该运行哪个内核?
在我们了解了各种类型的内核后,确实会产生这样一个问题:我应该运行哪个内核呢?答案是有些复杂的,因为它取决于具体的环境。这是用于嵌入式设备、桌面还是服务器?是新项目还是遗留项目?预期的维护周期是多长?是否是由供应商维护的SoC内核?安全要求是什么?不过,从资深内核维护者的口中得出的“正确答案”是这样的:
运行最新的稳定更新。这是我们目前知道如何创建的最稳定、最安全、最好的内核。这是我们能做到的最好选择。您应该运行它。
——Jon Corbet,2023年9月
提示:访问 kernel.org;您是否看到页面上带有内核版本号的大黄色按钮?那就是截至今天的最新稳定内核。
您必须使用所有的稳定/LTS版本来拥有一个安全和稳定的系统。如果您试图随意挑选补丁,您不仅不会解决所有已知和未知的问题,反而可能会使系统更不安全,并且包含已知的错误。
——Greg Kroah-Hartman
Greg Kroah-Hartman 在2018年8月发表的实用博客文章《我应该使用哪个稳定内核》,也表达了类似的观点。
好了,现在我们已经掌握了内核版本命名规则和内核源码树类型的知识,是时候开始我们的内核构建之旅了。
从源码构建内核的步骤
作为便捷的快速参考,以下是从源码构建Linux内核所需的主要关键步骤。由于每个步骤的解释都非常详细,您可以参考此摘要来了解整体流程。这些步骤如下:
-
获取Linux内核源码树,通过以下两种方式之一:
- 下载特定的内核源码树作为压缩文件。
- 克隆一个(内核)Git树。
-
将内核源码树解压到您的主目录中的某个位置(如果通过克隆Git树获得内核,可以跳过此步骤)。
-
配置内核:为您的内核配置找到一个起点(方法各不相同)。然后编辑它,根据新内核的需求选择支持选项。推荐的方法是使用
make menuconfig进行配置。 -
构建内核映像、可加载模块和任何必需的设备树Blob(DTB) ,使用
make [-j'n'] all命令。这将构建压缩内核映像(arch/<arch>/boot/[b|z|u]{Ii}mage)、未压缩的内核映像(vmlinux)、System.map文件、内核模块对象和任何已配置的DTB文件。 -
安装刚构建的内核模块(在x86上) ,使用
sudo make [INSTALL_MOD_PATH=<prefix-dir>] modules_install命令。此步骤默认将内核模块安装在/lib/modules/$(uname -r)/下(可以利用INSTALL_MOD_PATH环境变量更改此路径)。 -
引导加载程序(x86) :设置GRUB引导加载程序和initramfs(以前称为initrd)映像,使用命令
sudo make [INSTALL_PATH=</new/boot/dir>] install。此命令将在/boot下创建并安装initramfs或initrd映像(可以利用INSTALL_PATH环境变量更改此路径)。它还会更新引导加载程序配置文件,以引导新内核(作为第一个条目)。 -
自定义GRUB引导加载程序菜单(可选)。
本章是关于内核构建主题的两部分中的第一部分,将涵盖步骤1到3,同时包含大量必要的背景材料。下一章将涵盖剩余的步骤4到7。那么让我们从第1步开始吧。
步骤 1 – 获取 Linux 内核源码树
在本节中,我们将看到获取 Linux 内核源码树的两种主要方法:
- 从 Linux 内核公共仓库(www.kernel.org)下载并解压特定的内核源码树。
- 克隆 Linus Torvalds 的源码树(或其他人的)——例如,linux-next Git 树。
如何决定使用哪种方法?对于大多数在项目或产品上工作的开发人员来说,决策通常已经做出——项目使用非常具体的 Linux 内核版本。因此,您将下载该特定的内核源码树,并根据需要可能应用项目特定的补丁并加以使用。
对于那些打算向主线内核贡献代码或将代码上游合并的人来说,第二种方法——克隆 Git 树——是要走的路。当然,这其中还有更多内容;我们在“探索内核源码树的类型”一节中描述了一些细节。
在接下来的部分中,我们将演示获取内核源码树的两种方法。首先,我们描述从内核仓库下载特定内核源码树(而非 Git 树)的方法。为此,我们选择了 6.1.25 LTS Linux 内核。因此,出于本书的实际目的,这是一种可使用的方法。在第二种方法中,我们将克隆一个 Git 树。
下载特定内核源码树
首先,内核源码在哪里?简短的回答是,它位于公共内核仓库服务器上,可以通过 www.kernel.org 访问。该网站的主页显示了最新的稳定版 Linux 内核版本,以及最新的长期支持版本和 linux-next 版本。以下截图显示了该网站在2023年4月25日的样子。日期格式为 yyyy-mm-dd:
温馨提醒:我们还提供了一个PDF文件,其中包含本书中使用的截图/图表的全彩图片。您可以在此处下载:packt.link/gbp/9781803…。
有很多方法可以从该服务器和/或其镜像站点下载压缩的内核源码文件。让我们看看其中的两种方法:
- 交互式的、也许是最简单的方法是访问上述网站,并在您的网页浏览器中点击适当的tarball链接。浏览器会将
.tar.xz格式的镜像文件下载到您的系统中。 - 您还可以通过导航到 mirrors.edge.kernel.org/pub/linux/k… 并选择主要版本,下载任何压缩格式的内核源码树;实际上,对于主版本号为6的内核,其URL为 mirrors.edge.kernel.org/pub/linux/k…;在此页面中浏览或搜索您想要的内核。例如,请查看以下截图:
tar.gz 和 tar.xz 文件的内容是相同的,只是压缩类型不同。通常情况下,下载 .tar.xz 文件会更快,因为它们的体积较小。
另外,您也可以使用 wget 工具通过命令行下载内核源码树。我们还可以使用功能强大的 curl 工具来完成此操作。例如,要下载稳定的 6.1.25 LTS 内核源码压缩文件,可以在一行中输入以下命令:
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
这将安全地将 6.1.25 版本的压缩内核源码树下载到您计算机的 ~/Downloads 文件夹中。所以,继续操作吧,把 6.1.25(LTS)内核源码下载到您的系统中!
克隆Git树
对于开发人员来说,如果你希望将代码贡献到上游内核,你必须在最新版本的Linux内核代码库上工作。不过,在内核社区中,关于什么构成“最新版本”存在细微的差异。正如前面提到的,linux-next树及其中特定的分支或标签是为此目的而工作的理想选择。
在本书中,我们不打算深入探讨设置linux-next树的详细过程。这一过程已经有很好的文档记录,请参阅本章的进一步阅读部分以获取详细链接。关于如何克隆linux-next树的详细页面在这里:与linux-next合作。正如该页面所述,linux-next树(git.kernel.org/cgit/linux/…)是针对下一个内核合并窗口的补丁存放区。如果你正在进行前沿的内核开发,你可能希望从该树而不是Linus]() Torvalds的主线树或一般内核仓库(www.kernel.org)中的源码树开始工作。
对于我们的目的,克隆主线Linux Git仓库(实际上是Linus Torvalds的Git树)已经足够了。操作如下:
git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux
请注意,克隆完整的Linux内核树是一个耗时、耗网络和耗磁盘的操作!确保你有足够的磁盘空间(至少几GB)。
执行git clone --depth n <...>(其中n是整数值)可以限制历史(提交)的深度,从而降低下载和磁盘使用量。正如git-clone手册页提到的关于--depth选项的说明:“创建一个具有指定提交历史深度的浅克隆。”此外,供参考,若要撤销“浅克隆”并获取全部内容,只需执行git pull --unshallow。
git clone命令可能需要一段时间才能完成。进一步地,你可以通过运行git clone来指定你想要获取内核Git树的最新稳定版本;目前,如果你打算在这个主线Git树上工作,我们建议直接克隆带有完整历史的稳定内核Git树(再次提示,请在一行中输入以下命令):
git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git
接着,切换到克隆的目录:
cd linux-stable
同样,如果你打算在这个Git树上工作,请跳过步骤2(解压内核源码树),因为git clone操作无论如何都会解压源码树。相反,请继续进行随后的步骤3(配置Linux内核)。不过,这意味着你使用的内核源码树版本将与我们在本书中使用的6.1.25版本有很大不同。因此,我建议你将这部分内容视为如何通过git获取最新稳定Git树的演示,仅此而已。
最后,内核维护者还提供了一种下载给定内核的方式,他们提供了一个脚本,用于安全地下载指定的Linux内核源码树,并验证其PGP签名。脚本可在此处获取:get-verified-tarball。
步骤 2 – 解压内核源码树
在前一节的步骤1中,您学习了如何获取Linux内核源码树。一种方法——也是我们在本书中遵循的方法——是从kernel.org网站(或其镜像站点之一)下载压缩的源码文件。另一种方法是使用Git克隆一个最新的内核源码树。
所以,我假设您现在已经将6.1.25(LTS)内核源码树以压缩形式下载到您的Linux系统中。现在,我们继续进行步骤2,这是一个简单的步骤,学习如何解压它。
如前所述,本节内容适用于那些从仓库(www.kernel.org)下载了特定压缩Linux内核源码树并打算构建它的读者。在本书中,我们主要使用6.1长期支持内核系列,特别是6.1.25 LTS内核。
另一方面,如果您已经按照前面章节的说明克隆了主线Linux Git树,可以跳过本节并继续下一节——步骤3:配置Linux内核。
好了,现在下载完成了,我们继续进行下一步。下一步是解压内核源码树——请记住,它是一个经过tar打包和压缩的文件(通常为.tar.xz格式)。为了避免重复,我们假设您已经将Linux内核版本6.1.25的代码库下载为压缩文件并放在~/Downloads目录中:
$ cd ~/Downloads ; ls -lh linux-6.1.25.tar.xz
-rw-rw-r-- 1 c2kp c2kp 129M Apr 20 16:13 linux-6.1.25.tar.xz
解压和提取此文件的简单方法是使用通用的tar工具来执行以下操作:
tar xf ~/Downloads/linux-6.1.25.tar.xz
这将把内核源码树解压到~/Downloads目录中的一个名为linux-6.1.25的目录中。但是如果我们希望将其解压到另一个文件夹中,例如~/kernels,那么可以这样操作:
mkdir -p ~/kernels
tar xf ~/Downloads/linux-6.1.25.tar.xz --directory=~/kernels/
这将把内核源码提取到~/kernels/linux-6.1.25/文件夹中。为了方便起见并养成良好的习惯,我们可以设置一个环境变量来指向我们新的内核源码树的根位置:
export LKP_KSRC=~/kernels/linux-6.1.25
请注意,接下来我们将假设变量LKP_KSRC保存了6.1.25 LTS内核源码树的位置。
虽然您始终可以使用GUI文件管理器应用程序(如Nautilus)来解压压缩文件,但我强烈建议您熟悉使用Linux命令行界面(CLI)来执行这些操作。
当您需要快速查找常用命令的最常用选项时,不要忘记使用tldr!例如,想要查看常用的tar命令,只需运行tldr tar,或在这里查找:tldr tar。
您注意到了吗?我们可以将内核源码树解压到主目录下的任何目录或其他地方。这与过去不同,以前内核树通常解压到一个根目录可写的位置,通常是/usr/src/。
如果您现在只想继续进行内核构建操作,可以跳过以下部分并继续。如果您有兴趣(我真诚希望如此!),下一节将简要但重要地讨论内核源码树的结构和布局。
简要浏览内核源码树
想象一下!整个Linux内核源码现在就在您的系统上!太棒了——让我们快速浏览一下它:
它有多大?
在解压后的内核源码树的根目录下运行du -h .命令,您会发现这个内核源码树(其版本为6.1.25)大约有1.5GB大小!
供您参考,Linux内核的代码行数(SLOCs)已经非常庞大,并且还在不断增长。目前估计约有3000万行源码。当然,并不是所有的代码都会在构建内核时被编译。
我们如何通过查看源码来确定这是哪个版本的Linux内核?
这很简单:一种快速的方法是查看项目Makefile的前几行。顺便提一下,内核在许多地方都使用Makefile;大多数目录下都有一个。我们将这个位于内核源码树根目录的Makefile称为顶层Makefile:
bash
复制代码
$ head Makefile
# SPDX-License-Identifier: GPL-2.0
VERSION = 6
PATCHLEVEL = 1
SUBLEVEL = 25
EXTRAVERSION =
NAME = Hurr durr I'ma ninja sloth
# *DOCUMENTATION*
# To see a list of typical targets execute "make help"
# More info can be located in ./README
显然,这就是6.1.25内核的源码。在“理解Linux内核发布命名规则”一节中,我们已经讨论了VERSION、PATCHLEVEL、SUBLEVEL和EXTRAVERSION标签的含义——它们直接对应于w.x.y.z命名规则。NAME标签只是给这个发布版本取了个昵称(从这里可以看到,这是内核开发者的幽默感。我个人更喜欢5.x内核的NAME,它是“Dare mighty things”)。
现在,让我们来看看这个内核源码树的结构。以下表格总结了Linux内核源码树根目录中更重要的文件和目录的广泛分类和用途。可以与图2.7进行交叉引用:
| 文件或目录名称 | 用途 |
|---|---|
| 顶层文件 | |
| README | 项目的README文件。它告诉我们官方内核文档存放在哪里——剧透!它在名为Documentation的目录中,并告诉我们如何开始使用它。现代的内核文档也在线上,做得非常好:www.kernel.org/doc/html/la…。 |
| COPYING | 该文件详细说明了内核源码发布的许可证条款。绝大多数内核源码文件是根据著名的GNU GPL v2(写作GPL-2.0)许可证发布的。现代趋势是使用易于grep的、与行业对齐的SPDX许可证标识符。完整列表请见:spdx.org/licenses/。 |
| MAINTAINERS | 常见问题:内核组件(或文件)XYZ出了问题——我该联系谁以获得支持?这个文件正是为此而存在的——列出了所有内核子系统及其维护者的列表。它详细到各个组件级别,比如特定的驱动程序或文件,以及它的状态、当前维护者、邮件列表、网站等等。非常有用!还有一个辅助脚本可以找到合适的联系人或团队:scripts/get_maintainer.pl。 |
| Makefile | 这是内核的顶层Makefile;内核的Kbuild构建系统以及内核模块都使用此Makefile进行构建。 |
| 主要子系统目录 | |
| kernel/ | 核心内核子系统:此处的代码处理大量核心内核功能,包括进程/线程生命周期管理、CPU任务调度、锁定、cgroups、计时器、中断、信号、模块、跟踪、RCU原语、[e]BPF等。 |
| mm/ | 大部分内存管理(mm)代码位于此处。我们将在第6章“内核内部要点——进程和线程”中讨论一部分,并在第7章“内存管理内部要点”和第8章“内核内存分配——模块作者的第一部分”中介绍一些相关内容。 |
| fs/ | 此处的代码实现了两个关键的文件系统功能:抽象层——内核虚拟文件系统切换(VFS),以及各个文件系统驱动程序(例如ext[2 |
| block/ | 到VFS/FS的底层块I/O代码路径。它包括实现页面缓存的代码、通用块I/O层、I/O调度程序、新的blk-mq功能等。 |
| net/ | 完整实现网络协议栈,遵循RFC(请求评论)的每一个细节——whatis.techtarget.com/definition/…。包括TCP、UDP、IP以及许多其他网络协议的高质量实现。想看看IPv4的TCP/IP的代码级实现吗?就在这里:net/ipv4/,查看tcp*.c和ip*.c源文件,以及其他相关文件。 |
| ipc/ | 进程间通信(IPC)子系统代码;实现了SysV和POSIX消息队列、共享内存、信号量等IPC机制。 |
| sound/ | 音频子系统代码,也称为高级Linux声音架构(ALSA)层。 |
| virt/ | 虚拟化(虚拟机监控程序)代码;流行而强大的内核虚拟机(KVM)在此实现。 |
| 架构/基础设施/驱动/杂项 | |
| Documentation/ | 官方内核文档就在这里;重要的是要熟悉它。README文件提到了它的在线版本。 |
| LICENSES/ | 所有许可证的文本,按不同类别分类。 |
| arch/ | 这里是与架构相关的代码(arch指的是CPU)。Linux起初是为i386设计的小型业余项目。现在它可能是移植最多的操作系统。请参见后续表格中的第4点,查看架构端口。 |
| certs/ | 支持生成签名模块的代码;这是一个强大的安全功能,正确使用时可以确保即使是恶意rootkit也无法简单地加载任何他们想要的内核模块。 |
| crypto/ | 此目录包含加密算法(如加密/解密算法或转换)的内核级实现和为需要加密服务的消费者提供的内核API。 |
| drivers/ | 这里是内核级设备驱动程序代码。这被认为是非核心区域;它被分类为多种类型的驱动程序。这往往是最常被贡献的区域;此外,该代码占用了源码树中最多的磁盘空间。 |
| include/ | 该目录包含与架构无关的内核头文件。在arch/<cpu>/include/目录下还有一些与架构相关的头文件。 |
| init/ | 与架构无关的内核初始化代码;或许我们能接近内核主函数的地方就在这里:init/main.c:start_kernel(),其中的start_kernel()函数被认为是内核初始化期间的早期C入口点。 |
| io_uring/ | 实现新的io_uring快速I/O框架的内核基础设施;请参见第5点。 |
| lib/ | 类似于内核的库的部分。重要的是要理解内核不像用户空间应用程序那样支持共享库。这里的一些代码会自动链接到内核映像文件中,因此在运行时可以被内核使用。lib/目录中存在许多有用的组件:压缩/解压缩、校验和、位图、数学、字符串例程、树算法等。 |
| rust/ | 支持Rust编程语言的内核基础设施;请参见第6点。 |
| samples/ | 各种内核功能和机制的示例代码;学习的好资源! |
| scripts/ | 存放了各种脚本,其中一些在内核构建期间使用,许多用于其他目的,如静态/动态分析、调试等。它们大多是Bash和Perl脚本。(供参考,特别是用于调试目的,我在《Linux Kernel Debugging, 2022》中介绍了许多这些脚本。) |
| security/ | 存放了内核的Linux安全模块(LSM),这是一个强制访问控制(MAC)框架,旨在对用户应用程序访问内核空间施加比默认内核更严格的访问控制。默认模型称为自主访问控制(DAC)。目前,Linux支持多个LSM;著名的包括SELinux、AppArmor、Smack、Tomoyo、Integrity和Yama。请注意,LSM默认是关闭的。 |
| tools/ | 各种用户模式工具的源码存放在这里,主要是与内核“紧密耦合”的应用程序或脚本,因此需要放在特定的内核代码库中。Perf,一个现代的CPU分析工具,eBPF工具和一些跟踪工具都是很好的例子。 |
| usr/ | 支持生成和加载initramfs映像的代码;这允许内核在内核初始化期间执行用户空间代码。这通常是必需的;我们将在第3章“从源码构建6.x Linux内核——第2部分”中的“理解initramfs框架”部分中讨论initramfs。 |
一些重要的解释:
-
README:这个文件还提到了一些文档,介绍了构建和运行内核的最低可接受版本的软件信息:
Documentation/process/changes.rst。有趣的是,内核提供了一个Awk脚本(scripts/ver_linux),它会打印系统上当前软件的版本,帮助您检查所安装的软件版本是否符合要求。 -
内核许可:从务实的角度来看,内核是根据GNU GPL-2.0许可证发布的(GNU GPL是GNU通用公共许可证),任何直接使用内核代码库的项目自动归于该许可证之下。这是GPL-2.0的“派生作品”属性。法律上,这些项目或产品现在必须根据相同的许可证条款发布其内核软件。实际上,情况要模糊得多;许多在Linux内核上运行的商业产品确实包含了专有的用户空间和/或内核空间代码。它们通常通过将内核(通常是设备驱动程序)工作以可加载内核模块(LKM)格式重新构造来实现这一点。可以在LKM上使用双许可证模型。LKM是第4章“编写您的第一个内核模块——第1部分”和第5章“编写您的第一个内核模块——第2部分”的主题,我们将在其中讨论内核模块许可的一些信息。
-
MAINTAINERS:只需浏览一下您内核源码树根目录中的这个文件!有趣的东西... 为了说明它的用处,让我们运行一个辅助Perl脚本:
scripts/get_maintainer.pl。请注意,严格来说,它仅适用于Git树。这里,我们通过指定文件或目录并使用-f开关,让脚本显示内核CPU任务调度代码库的维护者:bash 复制代码 $ scripts/get_maintainer.pl --nogit -f kernel/sched结果:
bash 复制代码 Ingo Molnar <mingo@redhat.com> (maintainer:SCHEDULER) Peter Zijlstra <peterz@infradead.org> (maintainer:SCHEDULER) Juri Lelli <juri.lelli@redhat.com> (maintainer:SCHEDULER) Vincent Guittot <vincent.guittot@linaro.org> (maintainer:SCHEDULER) Dietmar Eggemann <dietmar.eggemann@arm.com> (reviewer:SCHEDULER) Steven Rostedt <rostedt@goodmis.org> (reviewer:SCHEDULER) Ben Segall <bsegall@google.com> (reviewer:SCHEDULER) Mel Gorman <mgorman@suse.de> (reviewer:SCHEDULER) Daniel Bristot de Oliveira <bristot@redhat.com> (reviewer:SCHEDULER) Valentin Schneider <vschneid@redhat.com> (reviewer:SCHEDULER) linux-kernel@vger.kernel.org (open list:SCHEDULER) -
Linux架构(CPU)移植:截至6.1,Linux操作系统已经移植到所有这些处理器上。大多数都有MMU。您可以在
arch/文件夹下看到与架构相关的代码,每个目录代表特定的CPU架构:bash 复制代码 $ cd ${LKP_KSRC} ; ls arch/输出:
bash 复制代码 alpha/ arm64/ ia64/ m68k/ nios2/ powerpc/ sh/ x86/ arc/ csky/ Kconfig microblaze/ openrisc/ riscv/ sparc/ x86_64/ arm/ hexagon/ loongarch/ mips/ parisc/ s390/ um/ xtensa/实际上,当进行交叉编译时,
ARCH环境变量设置为这些文件夹之一的名称,以便为该架构编译内核。例如,当为AArch64构建目标“foo”时,我们通常会执行类似make ARCH=arm64 CROSS_COMPILE=<...> foo的命令。 作为内核或驱动程序开发人员,浏览内核源码树是您必须习惯并最终享受的事情!然而,当代码行数接近3000万行时,搜索特定函数或变量可能是一项艰巨的任务!请务必学习使用高效的代码浏览工具。我建议使用ctags和cscope这两个开源工具。事实上,内核的顶层Makefile中有专门为此设计的目标:make [ARCH=<cpu>] tags ; make [ARCH=<cpu>] cscope是必须要做的!(对于cscope,如果ARCH设置为空(默认值),它会为x86[_64]生成索引。例如,要生成与AArch64相关的标签,请运行make ARCH=arm64 cscope。)此外,当然还有其他一些代码浏览工具;另一个不错的选择是opengrok。 -
io_uring:毫不夸张地说,io_uring和eBPF被认为是现代Linux系统提供的两个新(ish)“魔法功能”(io_uring文件夹中的内容就是对该功能的内核支持)!数据库/网络相关人员对io_uring的热情源自一个简单的原因:性能。该框架在真实的高I/O场景中显著提高了性能,无论是磁盘还是网络工作负载。其用户空间和内核空间之间共享的环形缓冲区架构、零拷贝模式,以及相比典型的旧AIO框架使用更少系统调用的能力(包括轮询模式操作),使其成为一个令人羡慕的功能。所以,为了让您的用户空间应用程序进入真正快速的I/O路径,请查看io_uring。本章的进一步阅读部分提供了一些有用的链接。
-
内核中的Rust:是的,Rust编程语言的基本支持已经进入Linux内核(首先是在6.0版本)。为什么?Rust确实在内存安全性方面比我们敬爱的C语言具有广为宣传的优势。事实上,即使在今天,C/C++编写的代码(无论是操作系统/驱动程序还是用户空间应用程序)所面临的最大的编程相关安全问题之一,往往源于内存安全问题(如著名的BoF(缓冲区溢出)缺陷)。这些问题可能会发生在开发人员在他们的C/C++代码中生成内存损坏缺陷(bug)时。这导致了软件中的漏洞,聪明的黑客总是在寻找这些漏洞并加以利用!尽管如此,至少目前,Rust在内核中的应用非常有限——没有核心代码使用它。目前内核中的Rust支持是为了在将来支持以Rust编写的模块。(当然,samples/rust/中有一些示例Rust代码。)Rust在内核中的使用肯定会随着时间的推移而增加....进一步阅读部分有一些关于这个主题的链接——如果感兴趣,请查阅。
我们现在已经完成了步骤2,提取内核源码树!作为额外收获,您还了解了内核源码布局的基础知识。现在让我们继续第三步,学习如何在构建内核之前配置Linux内核。
步骤 3 – 配置 Linux 内核
配置内核可能是内核构建过程中最关键的一步。Linux 被广泛认可为操作系统的一个重要原因之一就是它的多功能性。一个常见的误解是,企业级服务器、数据中心、工作站和小型嵌入式 Linux 设备都有各自独立的 Linux 内核代码库——其实不然,它们都使用相同的统一 Linux 内核源码!因此,为特定的用例(服务器、桌面、嵌入式或混合/自定义)仔细配置内核是一项强大的功能,也是一个必要条件。这正是我们在这里要深入探讨的内容。
您可能会发现,讨论如何获得一个可用的内核配置往往是一个漫长而曲折的过程,但最终是值得的;请花时间和精力仔细阅读。
另外,无论如何,在构建内核时都必须进行内核配置步骤。即使您觉得不需要对现有或默认配置进行任何更改,也非常重要的是,在构建过程中至少运行一次此步骤。否则,一些在此步骤中自动生成的头文件会丢失,导致后续出现问题。至少,您应该执行make old[def]config步骤。这将设置内核配置,使其与现有系统的配置相匹配,并仅在有新选项时向用户请求配置选项的回答。
接下来,我们将介绍一些内核构建系统所需的背景知识。
注意:如果您完全是内核配置的新手,您可能会觉得以下细节在第一次阅读时有些难以理解。在这种情况下,我建议您先跳过此部分,直接进入内核配置的实际操作部分,然后再回到这一节。
简要理解 Kconfig/Kbuild 构建系统
Linux 内核用于配置内核的基础设施被称为 Kconfig 系统,而用于构建内核的基础设施是 Kbuild。Kconfig 和 Kbuild 系统通过将工作分解为逻辑流,结合在一起,完成复杂的内核配置和构建过程,而无需深入到复杂的细节中:
-
Kconfig - 用于配置内核的基础设施;它由两个逻辑部分组成:
- Kconfig 语言:用于指定各个 Kconfig[.*] 文件中的语法,这些文件实际上指定了选择内核配置选项的“菜单”。
- Kconfig 解析器:工具,智能解析 Kconfig[.*] 文件,找出依赖关系和自动选择,并生成菜单系统。其中常用的是
make menuconfig,它内部调用了mconf工具(代码位于scripts/kconfig目录下)。
-
Kbuild - 支持基础设施,将源代码构建为内核二进制组件。它主要使用递归
make风格的构建,从内核顶层 Makefile 开始,递归解析嵌入源码中各子目录内的数百个 Makefile 的内容(按需解析)。
图 2.8 尝试以简化的方式传达有关 Kconfig/Kbuild 系统的信息。虽然有些细节尚未涵盖,但您可以在阅读以下内容时将其牢记在心。
为了帮助您更好地理解,我们来看一下 Kconfig/Kbuild 系统中的几个关键组件:
- CONFIG_FOO 符号
- 菜单规范文件(Kconfig[.*])
- Makefile 文件
- 整体内核配置文件 .config
这些组件的用途总结如下:
| Kconfig/Kbuild 组件 | 简要用途 |
|---|---|
| Kconfig: 配置符号: CONFIG_FOO | 每个可配置的内核选项 FOO 都由一个 CONFIG_FOO 宏表示。根据用户的选择,该宏将解析为 y、m 或 n: y=yes:这意味着将配置或功能 FOO 构建到内核映像中。 m=模块:这意味着将其作为单独的对象、内核模块(.ko 文件)构建。 n=no:这意味着不构建该功能。 注意,CONFIG_FOO 是一个字母数字字符串。我们将很快看到如何通过 make menuconfig UI 查找精确的配置选项名称。 |
| Kconfig 文件:Kconfig. 文件* | 这是定义 CONFIG_FOO 符号的地方。Kconfig 语法指定了它的类型(布尔值、三态、[字母]数字等)和依赖树。此外,对于基于菜单的配置 UI(通过 `make [menu |
| Kbuild: Makefile 文件 | Kbuild 系统使用递归的 make Makefile 方法。内核源码树根目录的 Makefile 被称为顶层 Makefile,通常在每个子文件夹中都有一个 Makefile 用于构建那里的源代码。6.1 内核源码中共有 2700 多个 Makefile! |
| .config 文件 | 最终,内核配置精炼为此文件;.config 是最终的内核配置文件。它作为一个简单的 ASCII 文本文件生成并存储在内核源码树的根文件夹中。请妥善保存它,因为它是您产品的关键部分。注意,配置文件名可以通过环境变量 KCONFIG_CONFIG 覆盖。 |
表 2.3:Kconfig+Kbuild 构建系统的主要组件
Kconfig+Kbuild 系统的工作原理——简要说明
现在我们已经了解了一些细节,下面是一个简化的说明,解释这个系统如何协同工作:
-
首先,用户(即您)使用 Kconfig 提供的某种菜单系统配置内核。
-
通过这个菜单系统 UI 选择的内核配置指令被写入一些自动生成的头文件和最终的
.config文件中,使用CONFIG_FOO={y|m}语法,或者CONFIG_FOO只是被注释掉(意味着“完全不构建 FOO”)。 -
接下来,Kbuild 中每个组件的 Makefile(通过内核顶层 Makefile 调用)通常会像这样指定一个 FOO 指令:
obj-$(CONFIG_FOO) += FOO.oFOO 组件可以是任何东西——核心内核功能、设备驱动程序、文件系统、调试指令等。回想一下,
CONFIG_FOO的值可以是y、m或不存在;这将相应地使构建过程要么将组件 FOO 构建到内核中(当其值为y时),要么作为模块构建(当其值为m时)。如果被注释掉,它根本不会被构建,简单明了。实际上,上述 Makefile 指令在构建时将展开为以下三种之一,用于给定的内核组件 FOO:obj-y += FOO.o # 将 FOO 功能构建到内核映像中 obj-m += FOO.o # 将 FOO 功能作为一个独立的内核模块构建(一个 foo.ko 文件) <if CONFIG_FOO is null> # 不构建 FOO 功能
要看到实际的例子,可以查看内核源码树根目录中的 Kbuild 文件(更多关于 Kconfig 文件的细节见“理解 Kconfig* 文件”部分):
$ cat Kconfig
…
# Kbuild for top-level directory of the kernel
…
# Ordinary directory descending
# ---------------------------------------------------------------------------
obj-y += init/
obj-y += usr/
obj-y += arch/$(SRCARCH)/
obj-y += $(ARCH_CORE)
obj-y += kernel/
[ … ]
obj-$(CONFIG_BLOCK) += block/
obj-$(CONFIG_IO_URING) += io_uring/
obj-$(CONFIG_RUST) += rust/
obj-y += $(ARCH_LIB)
[ … ]
obj-y += virt/
obj-y += $(ARCH_DRIVERS)
有趣的是!我们可以直观地看到顶层 Makefile 将如何递归到其他目录中,大多数被设置为 obj-y;实际上,这意味着将其构建到内核中(在少数情况下,它是参数化的,取决于用户如何选择该选项,它可以是 obj-y 或 obj-m)。
好了,现在我们继续前进;关键是要获取一个有效的 .config 文件。我们该如何做到这一点呢?我们可以通过迭代的方式完成这个过程。我们从一个“默认”配置开始——这是下一节的主题——然后逐步修改为自定义配置。
获得默认配置
那么,如何决定最初的内核配置呢?有几种常见的方法可以选择:
- 不指定任何内容;Kconfig 系统将会加载一个默认的内核配置(因为所有的内核配置都有一个默认值)。
- 使用现有发行版的内核配置。
- 基于当前加载在内存中的内核模块构建自定义配置。
第一种方法的优点是简单。内核会处理细节,提供一个默认配置。缺点是默认配置可能非常大(在为基于 x86_64 的桌面或服务器类型系统构建 Linux 时通常是这种情况);默认情况下开启了大量选项,以防万一你需要它们,这会导致构建时间很长,并且内核映像大小非常大。当然,通常情况下,您需要手动配置内核以达到所需的设置。
这引出了一个问题:默认内核配置存储在哪里?Kconfig 系统使用一个优先级列表回退方案来检索默认配置,如果没有指定配置的话。优先级列表及其顺序如下(第一个具有最高优先级):
.config/lib/modules/$(uname -r)/.config/etc/kernel-config/boot/config-$(uname -r)ARCH_DEFCONFIG(如果定义了)arch/${ARCH}/defconfig
从列表中可以看到,Kconfig 系统首先检查内核源码树根目录中是否存在 .config 文件;如果找到,它将从那里获取所有配置值。如果不存在,它接着会查看路径 /lib/modules/$(uname -r)/.config。如果找到,该文件中的值将用作默认值。如果找不到,它会继续检查前面优先级列表中的下一个,依此类推……你可以在图 2.8 中看到这一点。
关于内核的 Kconfig 和 Kbuild 基础设施的更详细的说明,建议你参考以下优秀的文档:
- 探索 Linux 内核:Kconfig/kbuild 的秘密,Cao Jin,opensource.com,2018年10月
- 幻灯片演示:深入 Kbuild,Cao Jin,Fujitsu,2018年8月
这里展示了一张图表(受 Cao Jin 文章的启发),试图传达内核的 Kconfig/Kbuild 系统。这张图表传达了比现在覆盖的内容更多的信息;不用担心,我们将会详细讲解。
好的,现在让我们来看看如何准确地获得一个可用的内核配置吧!
获得内核配置的良好起点
这引出了一个非常重要的问题:虽然将内核配置当作学习练习进行尝试是可以的,但对于生产系统来说,关键是要基于一个经过验证——已知、测试过并且工作正常——的内核配置来定制您的配置。
为帮助您理解选择有效内核配置起点的细微差别,我们将介绍三种获得典型内核配置起点的方法:
- 首先,一种简单(但次优)的方法,即直接模拟现有发行版的内核配置。
- 接下来,是一种更优化的方法,即基于现有系统的内存中内核模块来配置内核。这就是
localmodconfig方法。 - 最后,简要介绍一下典型嵌入式Linux项目的配置方法。
让我们详细探讨一下这些方法。在配置您在前两步中下载并解压的内核时,现在不要做任何事情;先阅读接下来的部分,然后在“使用 localmodconfig 方法入门”一节中,我们将实际开始操作。
使用发行版配置作为起点的内核配置
这种方法的典型目标系统是 x86_64 桌面或服务器 Linux 系统。让我们将内核配置为所有默认值:
$ make mrproper
CLEAN scripts/basic
CLEAN scripts/kconfig
CLEAN include/config include/generated .config
为了确保我们从一个干净的环境开始,我们首先运行 make mrproper;请小心,它会清理几乎所有东西,包括现有的 .config 文件。
接下来,我们执行 make defconfig 步骤,正如 make help 命令输出所示(可以尝试一下!见图2.10),它会生成一个新的配置:
$ make defconfig
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
[ … ]
HOSTLD scripts/kconfig/conf
*** Default configuration is based on 'x86_64_defconfig'
#
# configuration written to .config
#
首先会构建 mconf 实用程序(位于 scripts/kconfig 下),然后生成配置。生成的配置文件如下:
$ ls -l .config
-rw-rw-r-- 1 c2kp c2kp 136416 Apr 29 08:12 .config
这样就完成了:我们现在在 .config 中保存了一个“全默认”的内核配置!
如果在 arch/${ARCH}/configs 下没有针对您的架构的 defconfig 文件,该怎么办?那么,至少在 x86_64 上,您可以简单地复制现有发行版的默认内核配置:
cp /boot/config-$(uname -r) ${LKP_KSRC}/.config
在这里,我们简单地将现有 Linux 发行版(在此例中,是我们的 Ubuntu 22.04 LTS 客户机 VM)的配置文件复制到内核源码树根目录下的 .config 文件中,从而使发行版配置成为起点,之后可以进一步编辑。正如前面提到的,这种快速方法的缺点是配置通常较大,从而导致内核映像体积较大。
另外,供您参考,一旦内核配置以任何方式生成(例如通过 make defconfig),每个内核配置 FOO 都会显示为 include/config 目录下的一个空文件。
通过 localmodconfig 方法优化内核配置
这种方法的典型目标系统是 (通常为 x86_64) 桌面或服务器 Linux 系统。相比之前的方法,localmodconfig 方法更为优化——在需要基于现有运行系统生成内核配置时,通常比桌面或服务器 Linux 系统上的默认配置要紧凑得多。
在此方法中,我们通过将 lsmod 的输出重定向到一个临时文件,并将该文件提供给构建过程,来为 Kconfig 系统提供当前系统上运行的内核模块的快照。具体操作如下:
lsmod > /tmp/lsmod.now
cd ${LKP_KSRC}
make LSMOD=/tmp/lsmod.now localmodconfig
lsmod 工具简单列出了当前驻留在系统内核内存中的所有内核模块。我们将在第4章《编写您的第一个内核模块——第1部分》中进一步探讨这一点。
我们将 lsmod 的输出保存到一个临时文件中,然后通过 LSMOD 环境变量将该文件传递给 Makefile 的 localmodconfig 目标。此目标的任务是以只包含基本功能以及由这些内核模块提供的功能的方式配置内核,并排除其余功能,从而有效地为我们提供一个当前内核的合理复制品(或代表 lsmod 输出的任何内核)。在接下来的“使用 localmodconfig 方法入门”一节中,我们将使用这种技术来配置我们的 6.1 内核。我们还在图2.8中将此方法显示为步骤1(圆圈中的1)。
典型嵌入式 Linux 系统的内核配置
这种方法的典型目标系统通常是小型嵌入式 Linux 系统。目标是在嵌入式 Linux 项目中使用经过验证——已知、测试并且工作正常——的内核配置作为起点。那么,我们该如何实现这一目标呢?
在继续讨论之前,我需要提到的是:这里的初步讨论将展示配置嵌入式 Linux 的较旧方法(针对 AArch32 或 ARM-32 架构);随后我们将看到适用于现代平台的“正确”且现代的方法。
有趣的是,至少对于 AArch32 架构,内核代码库本身包含了各种知名硬件平台的已知、测试并且工作正常的内核配置文件。
假设我们的目标是基于 ARM-32 架构的系统,我们只需选择与我们的嵌入式目标板匹配(或最接近)的那个配置文件即可。这些内核配置文件位于内核源码树的 arch/<arch>/configs/ 目录下。配置文件的格式为 <platform-name>_defconfig。
我们可以快速浏览一下;以下截图展示了在 v6.1.25 Linux 内核代码库中,针对 ARM-32 的特定板级内核代码位于 arch/arm/mach-<foo> 下,平台配置文件位于 arch/arm/configs 下:
哇,真不少!arch/arm/mach-<foo> 目录代表了 Linux 已移植到的硬件平台(通常由芯片供应商完成的板卡或机器);与特定板卡相关的代码位于这些目录中。
同样,这些平台的默认工作内核配置文件也是由它们贡献的,位于 arch/arm/configs 文件夹下,文件名格式为 <foo>_defconfig,如图 2.9 的下半部分所示。
因此,例如,如果您需要为某个硬件平台(比如 NXP 的 i.MX 7 SoC)配置 Linux 内核,请不要从一个 x86_64 内核配置文件开始作为默认配置。这是行不通的。即使您设法做到了,内核也不会干净地构建/工作。选择适当的内核配置文件:在这个例子中,也许 imx_v6_v7_defconfig 文件是一个不错的起点。您可以将这个文件复制到内核源码树的 .config 文件中,然后继续根据项目的具体需求进行微调。
另一个例子是树莓派(Raspberry Pi, www.raspberrypi.org/),它是一个非常受欢迎的爱好者和生产平台。在其内核源码树中,作为基础使用的内核配置文件是arch/arm/configs/bcm2835_defconfig。这个文件名反映了树莓派板卡使用的是基于博通 2835 的 SoC。您可以在此处找到有关树莓派内核编译的详细信息:www.raspberrypi.org/documentati…。不过,请稍等,我们将在第 3 章《从源码构建 6.x Linux 内核——第 2 部分》中的树莓派内核构建部分至少涵盖其中的一些内容。
现代方法——使用设备树 (Device Tree)
要注意!正如我们所见,对于 AArch32 架构,你会在 arch/arm/configs 下找到平台特定的配置文件,以及在 arch/arm/mach-<foo>(其中 foo 是平台名称)下的板级特定内核代码。然而,这种将板级特定的配置和内核源码文件保存在 Linux OS 代码库中的做法,实际上被认为对操作系统来说是错误的做法!Linus 已经明确表示,在 Linux 旧版本中犯下的“错误”——特别是在 ARM-32 是主流架构时的错误——绝不能在其他架构中重演。那么该如何处理呢?对于现代(32 位和 64 位)ARM 和 PPC 架构,答案是使用现代的设备树(Device Tree, DT)方法。
基本上,设备树包含了所有平台硬件拓扑的详细信息。它实际上是板卡或平台的布局描述;它不是代码,而是类似于 VHDL 的硬件平台描述。BSP(板级支持包)相关的代码和驱动仍然需要编写,但现在有了一种整洁的方式来在启动时通过解析由引导加载程序传递的 DTB(设备树二进制文件)来被内核“发现”或枚举。DTB 是通过在平台的 DT 源文件上调用设备树编译器 (DTC) 生成的,作为构建过程的一部分。
如今,你会发现,大多数嵌入式项目(至少对于 ARM 和 PPC 来说)都能很好地利用设备树。它还通过允许使用基本相同的内核并在设备树中构建特定于平台/型号的调整,帮助了 OEM 和 ODM/供应商。想想那些主要硬件大同小异、但有一些细微硬件差异的 Android 手机型号;一个内核通常就足够了!这大大减轻了维护的负担。对于感兴趣的人来说,设备树的源文件——即 .dts 文件,可以在 arch/<arch>/boot/dts 下找到。
对于 AArch64 (ARM-64) 来说,似乎已经很好地吸取了尽可能将板卡特定内容置于内核代码库之外的教训。对比 AArch32,AArch64 的配置和 DTS 文件夹(arch/arm64/configs/ 中只有一个文件,即 defconfig)干净、组织良好且不杂乱。即使是 DTS 文件(在 arch/arm64/boot/dts/ 下查看)也比 AArch32 组织得更好:
6.1.25 $ ls arch/arm64/configs/
defconfig
6.1.25 $ ls arch/arm64/boot/dts/
actions/ amazon/ apm/ bitmain/ exynos/ intel/ marvell/ nuvoton/ realtek/ socionext/ tesla/ xilinx/
allwinner/ amd/ apple/ broadcom/ freescale/ lg/ mediatek/ nvidia/ renesas/ sprd/ ti/ altera/
amlogic/ arm/ cavium/ hisilicon/ Makefile microchip/ qcom/ rockchip/ synaptics/ toshiba/
那么,在现代嵌入式项目和设备树的背景下,如何进行内核/BSP 工作呢?有几种方法可供选择:
- 由内部的 BSP 或平台团队进行。
- 由供应商(通常是你合作的芯片供应商)提供 BSP “包”,以及参考硬件。
- 外包给你合作的外部公司或顾问。目前市场上有多家公司提供此类服务,其中包括西门子(前身为 Mentor Graphics)、Timesys 和 WindRiver。
- 如今,项目通常通过 Yocto 或 Buildroot 等复杂的构建器软件构建和集成,供应商贡献 BSP 层,构建团队将其集成到产品中。
设计方面:虽然有些偏离主题,但我认为这很重要:在项目(尤其是嵌入式项目)中,团队经常表现出直接使用供应商 SDK API 来执行设备特定工作的倾向,既用于应用程序也用于驱动程序。乍一看,这似乎没问题;但当你意识到需求变化、设备本身变化时,这种紧密耦合的软件将成为一个巨大的负担,因为它很容易崩溃!你让应用程序(和驱动程序)几乎没有分离地绑定到设备硬件上。
解决方案当然是使用松散耦合的架构,使用类似 HAL(硬件抽象层)的方法来让应用程序无缝地与设备接口。这也允许更改设备特定代码而不影响更高层(应用程序)。这种设计方法在理论上看似显而易见,实际上很难确保实施;请始终牢记这一点。事实是,Linux 的设备模型鼓励这种松散耦合的方法。进一步阅读部分提供了有关这些设计方法的一些良好链接,位于“通用在线和书籍资源……”部分(在此处:github.com/PacktPublis…)。
好了,这就结束了有关设置内核配置起点的三种方法的讨论。
查看所有可用的配置选项
事实上,就内核配置而言,我们只触及了表面。Kconfig 系统本身内置了许多生成内核配置的技术!这些通过配置目标来实现。你可以在内核源码根目录中运行 make help 来查看它们;它们列在“Configuration targets”标题下:
让我们尝试一下其他几种方法,首先是 oldconfig:
$ make mrproper
[ … ]
$ make oldconfig
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
HOSTCC scripts/kconfig/confdata.o
[ … ]
HOSTLD scripts/kconfig/conf
#
# using defaults found in /boot/config-5.19.0-41-generic
#
[ … ]
* Restart config...
[ … ]
*
Control Group support (CGROUPS) [Y/?] y
Favor dynamic modification latency reduction by default (CGROUP_FAVOR_DYNMODS) [N/y/?] (NEW) << 在这里等待输入 >>
[ … ]
当然,这种方法是可行的,但你可能需要按多次 Enter 键以接受每个新检测到的内核配置的默认值……(或者你可以明确指定一个值;我们将在接下来的部分中看到更多关于这些新内核配置的内容)。虽然这是很正常的,但有点麻烦。还有一种更简单的方法:使用 olddefconfig 目标;正如它的帮助行所说——“与 oldconfig 相同,但将新符号设置为其默认值而不提示”:
$ make olddefconfig
#
# using defaults found in /boot/config-5.19.0-41-generic
#
.config:10301:warning: symbol value 'm' invalid for ANDROID_BINDER_IPC
.config:10302:warning: symbol value 'm' invalid for ANDROID_BINDERFS
#
# configuration written to .config
#
Done.
查看和设置新的内核配置
让我们再做一个简单的实验:清理环境,然后将发行版的内核配置文件复制进来。如果你想保留现有的 .config,可以先备份一下。
$ make mrproper
$ cp /boot/config-5.19.0-41-generic .config
cp: overwrite '.config'? y
$
现在 .config 文件中包含了发行版的默认配置。思考一下:我们目前正在运行 5.19.0-41-generic 发行版内核,但打算构建一个新的 6.1.25 内核。因此,新内核必然会有一些新的内核配置项。在这种情况下,当你尝试配置内核时,Kconfig 系统会向你询问:它会在控制台窗口中显示每一个新的配置选项以及你可以设置的可用值,默认值显示在方括号中。你需要为遇到的新的配置选项选择值。这会以一系列问题的形式呈现,你需要在命令行中回答。
内核提供了两种有趣的机制来查看所有新的内核配置:
listnewconfig– 列出新的选项helpnewconfig– 列出新的选项及帮助文本
运行第一个命令仅列出每个新的内核配置变量:
$ make listnewconfig
[ … ]
CONFIG_CGROUP_FAVOR_DYNMODS=n
CONFIG_XEN_PV_MSR_SAFE=y
CONFIG_PM_USERSPACE_AUTOSLEEP=n
[ … ]
CONFIG_TEST_DYNAMIC_DEBUG=n
有很多新的配置——我得到了 108 个新的配置——所以我在这里截断了输出。
我们可以看到所有的新配置,不过这个输出对理解它们的具体含义帮助不大。运行 helpnewconfig 目标可以解决这个问题——你现在可以看到每个新内核配置的“帮助”信息(来自该配置选项的 Kconfig 文件):
$ make helpnewconfig
[ … ]
CONFIG_CGROUP_FAVOR_DYNMODS:
This option enables the favordynmods mount option by default, which reduces the latencies of dynamic cgroup modifications such as task migrations and controller on/offs at the cost of making hot path operations such as forks and exits more expensive.
Say N if unsure.
Symbol: CGROUP_FAVOR_DYNMODS [=n]
Type : bool
Defined at init/Kconfig:959
Prompt: Favor dynamic modification latency reduction by default
[ … ]
CONFIG_TEST_DYNAMIC_DEBUG:
This module registers a tracer callback to count enabled pr_debugs in a do_debugging function, then alters their enablements, calls the function, and compares counts.
If unsure, say N.
Symbol: TEST_DYNAMIC_DEBUG [=n]
[ … ]
$
暂时不用担心理解 Kconfig 语法;我们将在“自定义内核菜单、Kconfig 以及添加自定义菜单项”部分中详细讲解。
LMC_KEEP 环境变量
此外,你是否注意到在图 2.10 中,localmodconfig 和 localyesconfig 目标可以选择性地包含一个名为 LMC_KEEP(LMC 是 LocalModConfig 的缩写)的环境变量?
它的含义很简单:将 LMC_KEEP 设置为一些用冒号分隔的值后,Kconfig 系统会保留指定路径的原始配置。例如,它可能看起来像这样:"drivers/usb
/gpu"。实际上,这意味着“保持这些模块启用”。
这是在 5.8 内核中引入的一个特性(提交记录在这里:github.com/torvalds/li…)。因此,为了使用它,你可以像这样运行配置命令:
make LSMOD=/tmp/mylsmod \
LMC_KEEP="drivers/usb:drivers/gpu:fs" \
localmodconfig
通过 streamline_config.pl 脚本进行优化配置
有趣的是,内核提供了许多辅助脚本,可以在 scripts/ 目录中执行有用的管理、调试和其他任务。与我们正在讨论的内容相关的一个很好的例子是 scripts/kconfig/streamline_config.pl 这个 Perl 脚本。它特别适用于这样的情况:你的发行版内核启用了太多的模块或内核功能,而你只想保留那些你现在正在使用的——即当前加载的模块所提供的功能,就像 localmodconfig 一样。运行这个脚本,加载你想要的所有模块,并将其输出最终保存到 .config 文件中。然后运行 make oldconfig,配置就准备好了!
顺便说一句,以下是这个脚本的原作者 Steven Rostedt 对它功能的描述(github.com/torvalds/li…):
# Here's what I did with my Debian distribution.
#
# cd /usr/src/linux-2.6.10
# cp /boot/config-2.6.10-1-686-smp .config
# ~/bin/streamline_config > config_strip
# mv .config config_sav
# mv config_strip .config
# make oldconfig
如果你愿意,可以试试看。
使用 localmodconfig 方法开始配置
现在(终于!)让我们动手使用 localmodconfig 技术为我们的 6.1.25 LTS 内核创建一个合理大小的基础内核配置。正如前面提到的,这种基于现有内核模块的方法是一个很好的选择,适用于在基于 x86 系统上进行内核配置的起点,并且将其调整为与当前主机一致。
请记住:此时进行的内核配置适用于典型的 x86_64 桌面/服务器系统,作为一种学习方法。此方法仅提供一个起点,但即便如此,也可能并不适合实际项目。对于实际项目,你必须仔细检查并调整内核配置的各个方面;审查所需支持的硬件和软件是关键。同样,对于嵌入式目标,方法有所不同(正如我们在“典型嵌入式 Linux 系统的内核配置”部分中讨论的那样)。
在继续之前,建议清理源代码树,特别是如果你之前运行了我们进行的实验。请注意:这个命令会清除所有内容,包括 .config 文件:
make mrproper
如前所述,首先获取当前加载的内核模块的快照,然后通过指定 localmodconfig 目标让构建系统对其进行操作,如下所示:
lsmod > /tmp/lsmod.now
cd ${LKP_KSRC}
make LSMOD=/tmp/lsmod.now localmodconfig
当你运行 make [...] localmodconfig 命令时,很可能(实际上是非常可能)你当前正在配置的内核(版本 6.1.25)与当前正在构建机器上运行的内核(对于我来说,主机内核是 $(uname -r) = 5.19.0-41-generic)之间存在配置选项的差异。在这种情况下,如“查看和设置新内核配置”部分中所述,Kconfig 系统会针对每一个新配置向你提问;按 Enter 键接受默认值。
现在让我们使用 localmodconfig 命令。
提示符后缀将显示 (NEW),这实际上告诉你这是一个新的内核配置选项,并希望你对如何配置它给出答案。
输入以下命令(如果尚未完成):
$ uname -r
5.19.0-41-generic
$ lsmod > /tmp/lsmod.now
$ make LSMOD=/tmp/lsmod.now localmodconfig
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
[ ... ]
using config: '/boot/config-5.19.0-41-generic'
System keyring enabled but keys "debian/canonical-certs.pem" not found. Resetting keys to default value.
*
* Restart config...
*
* Control Group support
*
Control Group support (CGROUPS) [Y/?] y
Favor dynamic modification latency reduction by default (CGROUP_FAVOR_DYNMODS) [N/y/?] (NEW) ↵
Memory controller (MEMCG) [Y/n/?] y
[ … ]
Userspace opportunistic sleep (PM_USERSPACE_AUTOSLEEP) [N/y/?] (NEW) ↵
[ … ]
Multi-Gen LRU (LRU_GEN) [N/y/?] (NEW) ↵
[ … ]
Rados block device (RBD) (BLK_DEV_RBD) [N/m/y/?] n
Userspace block driver (Experimental) (BLK_DEV_UBLK) [N/m/y/?] (NEW) ↵
[ … ]
Pine64 PinePhone Keyboard (KEYBOARD_PINEPHONE) [N/m/y/?] (NEW) ↵
[ … ]
Intel Meteor Lake pinctrl and GPIO driver (PINCTRL_METEORLAKE) [N/m/y/?] (NEW) ↵
[ … ]
Test DYNAMIC_DEBUG (TEST_DYNAMIC_DEBUG) [N/m/y/?] (NEW) ↵
[ … ]
#
# configuration written to .config
#
$ ls -l .config
-rw-rw-r-- 1 c2kp c2kp 170136 Apr 29 09:56 .config
在按下许多次 Enter 键(↵)后——我们在上面的输出块中突出显示了这一点,显示了遇到的许多新选项中的一部分——提问终于结束,Kconfig 系统将生成的配置写入当前工作目录中的 .config 文件。请注意,我们截断了前面的输出,因为完整重现内容过于冗长且不必要。
前面的步骤通过 localmodconfig 方法生成了 .config 文件。在结束本节之前,还有一些其他要点需要注意:
- 要确保完全干净的状态,请在内核源代码树的根目录中运行
make mrproper或make distclean,这在你想从头开始重新构建内核时非常有用;放心,总有一天会需要这么做!请注意,这样做也会删除内核配置文件。如果需要,请在开始之前保留备份。 - 本章中所有与内核配置相关的步骤和截图均在 x86_64 Ubuntu 22.04 LTS 客户虚拟机上进行,我们最终将使用它作为主机来构建全新的 6.1 LTS Linux 内核。菜单项的精确名称、存在与否及其内容,以及菜单系统的外观和感觉(UI),都可能会根据 (a) 架构 (CPU) 和 (b) 内核版本而有所不同。
- 正如之前提到的,在生产系统或项目中,平台或 BSP 团队,或者你合作的嵌入式 Linux BSP 供应商公司,将提供一个良好的已知、工作和测试过的内核配置文件。通过将其复制到内核源码树根目录下的
.config文件中,作为起点。此外,像 Yocto 或 Buildroot 这样的构建器软件也可能会被使用。 - 随着你对内核构建经验的积累,你会意识到,首次正确设置内核配置的努力较大;当然,第一次构建所需的时间也较长。不过,一旦正确完成,此过程通常会变得简单得多——这将是一个可以重复运行的配方。
现在,让我们学习如何使用一个有用且直观的 UI 来微调我们的内核配置。
通过 make menuconfig UI 调整内核配置
很好,现在我们已经通过 localmodconfig Makefile 目标生成了一个初始的内核配置文件 .config,正如前面部分详细展示的那样,这是一个不错的起点。通常情况下,我们会进一步微调内核配置。其中一种方法——事实上也是推荐的方法——是通过 menuconfig Makefile 目标来进行调整。此目标会让 Kbuild 系统生成一个相当复杂的基于 C 的可执行程序(scripts/kconfig/mconf),它会向终端用户展示一个整洁的基于菜单的用户界面(UI)。这是图 2.8 中的第 2 步。在下面的输出块中,当我们(在内核源代码树的根目录中)第一次调用该命令时,Kbuild 系统会构建 mconf 可执行文件并调用它:
$ make menuconfig
UPD scripts/kconfig/.mconf-cfg
HOSTCC scripts/kconfig/mconf.o
HOSTCC scripts/kconfig/lxdialog/checklist.o
[…]
HOSTLD scripts/kconfig/mconf
当然,图片胜过千言万语,以下是 menuconfig UI 在我的虚拟机上显示的样子:
顺便说一下,你不需要在图形界面模式下运行虚拟机来使用这种方法;即使在使用 SSH 登录到主机的终端窗口中,这种 UI 方法也能编辑内核配置——这是这种 UI 方法的另一个优点!
正如有经验的开发人员,或者确实是任何曾经充分使用过计算机的人都知道的那样,事情总是会出错。比如,以下场景——在新安装的 Ubuntu 系统上首次运行 make menuconfig:
$ make menuconfig
UPD scripts/kconfig/.mconf-cfg
HOSTCC scripts/kconfig/mconf.o
YACC scripts/kconfig/zconf.tab.c
/bin/sh: 1: bison: not found
scripts/Makefile.lib:196: recipe for target 'scripts/kconfig/zconf.tab.c' failed
make[1]: *** [scripts/kconfig/zconf.tab.c] Error 127
Makefile:539: recipe for target 'menuconfig' failed
make: *** [menuconfig] Error 2
$
别急,不要慌。仔细阅读错误信息。YACC [...] 后面的那一行给出了线索:/bin/sh: 1: bison: not found。啊哈!所以,你需要通过以下命令安装 bison:
sudo apt install bison
现在,一切应该正常了。嗯,差不多;同样是在新安装的 Ubuntu 客户端上,make menuconfig 随后提示没有安装 flex。所以,我们也安装了它(你猜对了:通过 sudo apt install flex)。此外,特别是在 Ubuntu 上,你还需要安装 libncurses5-dev 软件包。在 Fedora 上,则需要执行 sudo dnf install ncurses-devel。
如果你已经阅读并遵循了在线章节《内核工作区设置》,你应该已经安装了所有这些必备软件包。如果没有,请现在参考该章节并安装所有必需的软件包。记住,正如你所播种的那样...
快速提示:运行 <book_src>/ch1/pkg_install4ubuntu_lkp.sh Bash 脚本将在 Ubuntu 系统上安装所有必需的软件包。
继续说,Kconfig+Kbuild 开源框架通过其 UI 为用户提供了线索。请查看图 2.11;你会经常看到菜单前缀有符号(如 [ ], <>, --, (), 等等);这些符号及其含义如下:
-
[.]:内核中的功能,布尔选项。它要么开启,要么关闭;显示的.将被*或空格替代:[*]:开启,功能编译并内置到内核镜像中 (y)[ ]:关闭,完全不编译 (n)
-
<.>:一个功能可能有三种状态。这称为三态;显示的.将被*,M, 或空格替代:<*>:开启,功能编译并内置到内核镜像中 (y)<M>:模块,功能编译并作为内核模块(一个 LKM)构建 (m)< >:关闭,完全不编译 (n)
-
{.}:此配置选项存在依赖性;因此,它必须作为模块 (m) 或编译到内核镜像 (y) 中。 -
-*-:依赖项要求此项目编译 (y)。 -
(...):提示:需要输入一个字母数字。按 Enter 键在此选项上,会出现一个提示框。 -
<Menu name> --->:一个子菜单。按 Enter 键在此项目上,导航到子菜单。
同样,经验性的做法是关键。让我们通过一些 make menuconfig UI 的实验来看看它是如何工作的。这正是我们将在下一节中学习的内容。
使用 make menuconfig UI 的示例
为了熟悉通过便捷的 menuconfig 目标使用 Kbuild 菜单系统,我们可以尝试开启一个非常有趣的内核配置选项。它的名字是 “Kernel .config support”,这个选项允许你在运行内核时查看内核配置的内容!这个功能在开发和测试过程中非常有用,但出于安全原因,它通常在生产环境中是关闭的。
几个需要解决的问题是:
-
Q:这个选项在哪里?
- A: 它位于主菜单中的 "General Setup" 子菜单下(我们很快就会看到)。
-
Q:默认情况下它设置为什么值?
- A: 它默认设置为
<M>,意味着它将作为一个内核模块来构建。
- A: 它默认设置为
作为一个学习实验,我们将其设置为 [*] 或 y,将其编译到内核中,从而使其始终启用。好了,让我们开始吧!
启动内核配置 UI:
make menuconfig
你应该会看到一个类似于图 2.11 的终端界面。通常第一个项目是一个标签为 "General Setup --->" 的子菜单;在它上面按 Enter 键,这将带你进入 "General Setup" 子菜单,其中显示了许多选项;使用向下箭头导航到名为 "Kernel .config support" 的选项:
我们可以在前面的截图中看到,我们正在 x86 平台上配置 6.1.25 内核,当前高亮显示的菜单项是“Kernel .config support”。从其 <M> 前缀可以看出,这是一个三态菜单项,最初(默认情况下)设置为 <M>,即“模块”。
保持这个选项(“Kernel .config support”)处于高亮状态,使用右箭头键导航到底部工具栏上的 < Help > 按钮,并在 < Help > 按钮上按 Enter 键。或者,在一个选项上直接按 ? 键!屏幕现在应该看起来像这样:
帮助屏幕提供了很多信息。事实上,许多内核配置的帮助屏幕都非常详细且有帮助。然而,不幸的是,有些帮助屏幕的内容并不充分。
接下来,在 < Exit > 按钮上按 Enter 键,以便返回到前一个屏幕。
通过按空格键更改该值;这样做会使当前菜单项的值在 <*>(始终开启)、< >(关闭)和 <M>(模块)之间切换。将其设置为 <*>,表示“始终开启”。
接下来,尽管此时该功能已经开启,但实际上查看内核配置的能力是通过 procfs 下的一个伪文件提供的;紧接在该项下方的就是相关选项:
[ ] Enable access to .config through /proc/config.gz
你可以看到它默认是关闭的([ ]);通过导航到该选项并按空格键将其开启。现在它显示为 [*]:
好了,我们暂时完成了;按右箭头键或 Tab 键,导航到 < Exit > 按钮,并在其上按 Enter 键;你将返回到主菜单屏幕。再次按 < Exit >;此时,UI 会询问你是否要保存此配置。选择 < Yes >(在 Yes 按钮上按 Enter 键):
现在,新的内核配置已经保存在 .config 文件中。让我们快速验证一下。我希望你注意到了我们修改的内核配置的确切名称——也就是内核源码中看到的宏定义名称:
CONFIG_IKCONFIG对应 Kernel .config support 选项。CONFIG_IKCONFIG_PROC对应 Enable access to .config through /proc/config.gz 选项。
我们怎么知道的?它们显示在帮助屏幕的左上角!再看看图 2.13。
完成了。当然,实际效果要等我们编译并从这个内核启动后才能看到。现在,启用这个功能到底能实现什么呢?启用后,可以随时通过两种方式查看当前正在运行的内核的配置设置:
- 运行
scripts/extract-ikconfig脚本。 - 直接读取
/proc/config.gz伪文件的内容。当然,这个文件是经过 gzip 压缩的;首先解压缩,然后读取。zcat /proc/config.gz就可以解决问题!
作为进一步的学习练习,为什么不进一步修改默认的内核配置(针对 x86-64 架构的 6.1.25 Linux 内核)中的另外几个项目呢?目前,不用太担心每个内核配置选项的具体含义;这只是为了练习 Kconfig 系统。因此,运行 make menuconfig,并按照下面的格式在其中进行更改。
格式:
- 我们正在处理的内核配置:
- 它的含义:
- 导航路径:
- 菜单项名称(和内核配置宏 CONFIG_FOO 在括号内):
- 默认值:
- 将其更改为:
好,以下是你可以尝试的内容;让我们从以下内容开始:
本地版本:
- 含义: 要附加到内核版本的字符串。以
uname -r为例,实际上,它是内核版本命名法中的 “z” 或 EXTRAVERSION 组件。 - 导航路径: General Setup。
- 菜单项: Local version – append to kernel release(CONFIG_LOCALVERSION);在这里按一次 Enter,你会看到一个提示框。
- 默认值: NULL。
- 更改为: 任何你喜欢的内容;在本地版本前加上一个连字符是个好习惯,例如
-lkp-kernel。
接下来:
计时器频率:
- 含义: 硬件计时器中断触发的频率。你将在第 10 章 “CPU 调度器 – 第 1 部分” 中了解这个可调参数的详细信息。
- 导航路径: Processor type and features | Timer frequency (250 HZ) --->。一直向下滚动,直到找到第二个菜单项。
- 菜单项: Timer frequency(CONFIG_HZ)。
- 默认值: 250 HZ。
- 更改为: 300 HZ。
在你处理这些内核配置时,查看每个内核配置的帮助屏幕。太好了;完成后,保存并退出 UI。
在配置文件中验证内核配置
那么,新的内核配置保存在哪里?这点很重要,所以我们再重复一下:内核配置被写入到内核源代码树根目录中的一个简单的 ASCII 文本文件中,名为 .config。也就是说,它保存在 ${LKP_KSRC}/.config 中。
如前所述,每一个内核配置选项都与一个形式为 CONFIG_<FOO> 的配置变量相关联,其中 <FOO> 当然是用适当的名称替代的。在内部,这些配置变量变成了构建系统甚至内核源代码使用的宏。
因此,为了验证我们刚刚修改的内核配置是否生效,让我们使用 grep 适当地检查内核配置文件:
$ grep -E "CONFIG_IKCONFIG|CONFIG_LOCALVERSION|CONFIG_HZ_300" .config
CONFIG_LOCALVERSION="-lkp-kernel"
# CONFIG_LOCALVERSION_AUTO is not set
CONFIG_IKCONFIG=y
CONFIG_IKCONFIG_PROC=y
CONFIG_HZ_300=y
$
啊哈!配置文件现在反映了我们确实修改了相关的内核配置;配置的值也显示出来了。
注意:最好不要手动编辑 .config 文件。 其中有许多相互依赖关系你可能并不清楚;始终使用 Kbuild 菜单系统(我们建议使用 make menuconfig)来编辑它。
不过,有一种非交互的方式可以通过脚本来进行编辑。我们稍后会学习这一点。不过,使用 make menuconfig UI 仍然是最佳方式。
到目前为止,我希望你已经根据刚才看到的值修改了内核配置。
在我们迄今为止与 Kconfig/Kbuild 系统的快速探险中,很多事情已经在幕后发生了。下一节将探讨一些剩下的要点:关于 Kconfig/Kbuild 的更多内容,在菜单系统中进行搜索,清楚地可视化原始和修改后的内核配置文件之间的差异,使用脚本编辑配置,安全问题以及解决这些问题的提示;还有很多东西要学习!
内核配置 – 更深入的探索
通过 make menuconfig UI 或其他方法在内核源代码树的根目录中创建或编辑 .config 文件,并不是 Kconfig 系统处理配置的最终步骤。接下来,它会内部调用一个隐藏的目标,称为 syncconfig,这个目标之前曾被误称为 silentoldconfig。syncconfig 目标会让 Kconfig 生成一些头文件,这些头文件在进一步的内核构建过程中被使用。
这些文件包括 include/config 目录下的一些元头文件,以及 include/generated/autoconf.h 头文件,后者将内核配置存储为 C 宏,从而使内核的 Makefile 和内核代码能够基于某个内核功能是否可用做出决策。
现在我们已经覆盖了足够的内容,可以再看看图 2.8,这是一个高层次的图示(受曹晋的文章启发),旨在传达内核的 Kconfig/Kbuild 系统。这个图在 Kconfig 部分只展示了常见的 make menuconfig UI;请注意,还有其他几种 UI 方法存在,比如 make config 和 make {x|g|n}config,这些都没有在这里展示。
在 menuconfig UI 中搜索
继续前进,当你在运行 make menuconfig 时,想要寻找某个特定的内核配置选项,但发现难以找到它怎么办?不用担心:menuconfig UI 系统具有“搜索配置参数”功能。就像著名的 vi 编辑器一样(是的,[g]vi[m] 仍然是我们最喜欢的文本编辑器!),按下 /(斜杠)键,弹出搜索对话框,然后输入你的搜索词,可以加上或不加 CONFIG_ 前缀,然后选择 < Ok > 按钮进行搜索。
以下两张截图展示了搜索对话框和结果对话框。作为示例,我们搜索了术语 “vbox”:
图 2.17 中针对前述搜索的结果对话框非常有趣。它揭示了关于配置选项的几个关键信息:
- 配置指令:只需在它显示的 Symbol 前缀上添加
CONFIG_。 - 配置的类型:如布尔型(Boolean)、三态(tristate)、字母数字型(alphanumeric)等。
- 提示字符串:显示在菜单系统中的提示文字。
- 位置:它在菜单系统中的位置,这点非常重要,以便你可以找到它。
- 内部依赖项(如果有):
Depends on:列出该配置选项的依赖项。 - 定义位置:显示了该内核配置在 Kconfig 文件中的路径和行号(
Defined at <path/to/foo.Kconfig*:n>),即此配置选项定义的具体位置。 - 自动选择的配置项:如果此配置项被选中,它会自动选择的其他配置项(
Selects:)。
以下是结果对话框的部分截图:
所有驱动菜单显示和选择的信息都存在于一个由 Kbuild 系统使用的 ASCII 文本文件中——这个文件通常被命名为 Kconfig。实际上,有多个这样的文件。它们的具体名称和位置显示在 "Defined at ..." 行中。
查看配置差异
当 .config 内核配置文件准备被写入时,Kconfig 系统会检查它是否已经存在,如果存在,它会将其备份为 .config.old。知道这一点后,我们总是可以通过对比这两个文件来查看我们刚刚做出的更改。然而,使用通常的 diff 实用程序来做这件事会使差异变得很难理解。内核提供了一种更好的方式,即一个专门用于此目的的控制台脚本。内核源代码树中的 scripts/diffconfig 脚本对这方面很有用。可以通过传递 --help 参数来查看使用说明。
让我们试试看:
$ scripts/diffconfig .config.old .config
HZ 250 -> 300
HZ_250 y -> n
HZ_300 n -> y
LOCALVERSION "" -> "-lkp-kernel"
$
如果你按照前面部分的描述修改了内核配置,那么通过内核的 diffconfig 脚本你应该会看到如上所示的输出。它清楚地显示了我们更改了哪些内核配置选项以及如何更改的。事实上,你甚至不需要传递 .config* 参数;它默认会使用这些参数。
使用内核的 config 脚本查看/编辑内核配置
有时,需要直接编辑或查询内核配置,以检查或修改特定的内核配置选项。我们已经学习了如何通过强大的 make menuconfig UI 来实现这一点。这里我们将学习到另一种可能更简单、更重要的是非交互式且因此可以脚本化的方式——通过内核源代码中的一个 Bash 脚本:scripts/config。
运行它而不带任何参数将显示一个有用的帮助屏幕,建议你查看它。以下是一个关于如何使用它的示例。
查看当前内核配置的能力非常有用,所以让我们确保这些内核配置已经打开。仅为了这个示例,我们先显式禁用相关的内核配置选项,然后再启用它们:
$ scripts/config --disable IKCONFIG --disable IKCONFIG_PROC
$ grep IKCONFIG .config
# CONFIG_IKCONFIG is not set
# CONFIG_IKCONFIG_PROC is not set
$ scripts/config --enable IKCONFIG --enable IKCONFIG_PROC
$ grep IKCONFIG .config
CONFIG_IKCONFIG=y
CONFIG_IKCONFIG_PROC=y
瞧,完成了。
不过要小心:这个脚本可以修改 .config,但不能保证你所要求的操作实际上是正确的。内核配置的有效性只有在你下次构建时才会被检查。当有疑问时,先通过 Kconfig* 文件或运行 make menuconfig 检查所有依赖关系,然后再使用 scripts/config,并测试构建以确保一切正常。
配置内核以增强安全性
在结束之前,有必要快速提一下非常重要的内容:内核安全。尽管用户空间的安全加固技术已经有了长足的发展,内核空间的安全加固技术却还在追赶中。仔细配置内核的配置选项确实在确定给定Linux内核的安全姿态方面发挥着关键作用;问题是,有太多的选项和意见,往往很难判断在安全方面什么是好的做法,什么不是。
Alexander Popov 编写了一个非常有用的 Python 脚本,名为 kconfig-hardened-check。运行它可以检查和比较给定的内核配置,通过常规的配置文件,与从各种 Linux 内核安全项目中获得的一组预先确定的加固偏好进行比较:
- 内核自我保护项目 (KSPP; 链接: kernsec.org/wiki/index.…)
- 最后一个公开的 grsecurity 补丁
- CLIP OS
- 安全锁定 LSM
- Linux 内核维护者的直接反馈
你可以从其 GitHub 仓库 github.com/a13xp0p0v/k… 克隆 kconfig-hardened-check 项目并尝试使用!顺便提一下,我的《Linux Kernel Debugging》书中更详细地介绍了如何使用这个脚本。下面是其帮助屏幕的截图,可以帮助你入门:
快速实用的提示:使用 kconfig-hardened-check 脚本,可以轻松生成一个注重安全的内核配置文件(以下是为 AArch64 生成的示例):
kconfig-hardened-check -g ARM64 > my_kconfig_hardened
(输出文件称为配置片段)。那么,实际情况是,如果你已经有了一个适用于你的产品的内核配置文件,可以合并这两个文件吗?当然可以!内核提供了一个脚本来完成这个任务:scripts/kconfig/merge_config.sh。运行它时,传入原始的(可能不安全的)内核配置文件的路径,以及刚生成的安全内核配置片段的路径;结果是两个文件的合并(merge_config.sh 脚本有额外的参数可以进一步控制合并过程,建议查看一下其用法)。
一个示例可以在这里找到:github.com/a13xp0p0v/k…。
另外,你可能会发现新的 GCC 插件(CONFIG_GCC_PLUGINS)提供了一些很酷的架构特定安全功能。例如,本地/堆变量的自动初始化、启动时的熵生成等。然而,这些功能通常不会默认出现在菜单中。通常它们位于:General architecture-dependent options | GCC plugins,因为默认情况下并未安装相关支持。在 x86 平台上,尝试安装 gcc-<ver#>-plugin-dev 包,其中 ver# 是 GCC 的版本号,然后重新配置内核。
杂项提示——内核配置
以下是一些关于内核配置的剩余提示:
-
当为使用 VirtualBox 的虚拟机构建 x86 内核时(正如我们这里所做的那样),在配置内核时,你可能会发现将
CONFIG_ISO9660_FS=y设置为有用;它随后允许 VirtualBox 让虚拟机挂载 Guest Additions 虚拟 CD,并安装(非常有用的)Guest Additions。这通常能改善虚拟机的性能,并提供更好的图形、USB 功能、剪贴板和文件共享等功能。 -
在构建自定义内核时,有时我们希望编写/构建 eBPF 程序(这里未涵盖的高级主题)或类似的东西。为了做到这一点,需要一些内核中的头文件。你可以通过设置内核配置
CONFIG_IKHEADERS=y(或设置为 m;从 5.2 版本开始)来显式确保这一点。这会在/sys/kernel/kheaders.tar.xz中生成一个文件,该文件可以解压到其他地方以提供头文件。 -
进一步讲到 eBPF,现代内核可以生成一些称为 BPF 类型格式 (BTF) 元数据的调试信息。这可以通过选择内核配置
CONFIG_DEBUG_INFO_BTF=y来启用。这还需要安装pahole工具。有关 BTF 元数据的更多信息,可以在官方内核文档中找到:BTF 文档。 -
当启用此选项时,另一个内核配置
CONFIG_MODULE_ALLOW_BTF_MISMATCH在构建内核模块时变得相关。我们将在接下来的两章中深入讨论这个话题。如果启用了CONFIG_DEBUG_INFO_BTF,最好将后者设置为Yes,否则如果在加载时 BTF 元数据不匹配,模块可能无法加载。 -
接下来,理论上,内核构建不应生成错误甚至警告。为确保这一点,可以将
CONFIG_WERROR=y设置为将警告视为错误。在现在熟悉的make menuconfig界面中,它位于General Setup | Compile the kernel with warnings as errors下,默认情况下通常是关闭的。 -
这里有一个有趣的脚本:
scripts/get_feat.pl;其帮助屏幕显示了如何利用它来列出机器或给定架构的内核功能支持矩阵。例如,要查看 AArch64 的内核功能支持矩阵,可以这样做:scripts/get_feat.pl --arch arm64 ls -
另一个非官方的“数据库”,列出了所有可用的内核配置及其支持的内核版本,可在 Linux Kernel Driver database (LKDDb) 项目站点找到:LKDDb。
-
内核启动配置:在启动时,你总是可以通过强大的内核命令行参数来覆盖某些内核功能。它们在 官方文档 中有详细记录。这很有帮助,但有时我们需要以键值对的形式传递更多参数,基本上是
key=value的形式,扩展内核命令行。这可以通过填充一个名为启动配置的小内核配置文件来完成。此启动配置功能取决于内核配置的BOOT_CONFIG是否为y。它位于General Setup菜单下,通常默认开启。该功能有两种使用方式:通过将启动配置附加到 initrd 或 initramfs 映像(我们将在下一章中讨论 initrd),或者将启动配置嵌入到内核本身中。对于后者,你需要创建启动配置文件,并在内核配置中传递指令
CONFIG_BOOT_CONFIG_EMBED_FILE="x/y/z",然后重新构建内核。请注意,内核命令行参数将优先于启动配置参数。在启动时,如果启用了并使用了启动配置参数,它们可以通过/proc/bootconfig查看。有关启动配置的详细信息,请参阅官方内核文档:启动配置文档。 -
你肯定会遇到许多其他有用的内核配置设置和脚本,包括那些用于强化内核的;保持敏锐的眼光。
好了!你现在已经完成了 Linux 内核构建的前三个步骤——相当不容易。当然,我们将在下一章完成内核构建过程的剩余四个步骤。我们将在本章的最后一节结束前学习另一项有用的技能——如何自定义内核 UI 菜单。
自定义内核菜单、Kconfig 和添加自定义菜单项
假设你开发了一个设备驱动程序、一个实验性的新模块化调度类、一个自定义的 debugfs(调试文件系统)回调或其他一些很酷的内核功能。总有一天你会遇到这种情况。那么,如何让团队中的其他人——或者你的客户或社区——知道这个出色的新内核功能的存在呢?你通常会设置一个新的内核配置(宏),并允许人们选择它是作为内置模块还是作为内核模块,从而构建和使用它。作为其中的一部分,你需要定义新的内核配置并在内核配置菜单中的适当位置插入一个新的菜单项。
为了做到这一点,首先了解一下各种 Kconfig* 文件及其所在位置是很有用的。让我们来了解一下。
理解 Kconfig* 文件
Kconfig* 文件包含由内核的配置和构建系统——Kconfig/Kbuild——解释的元数据,这些元数据允许它构建并(有条件地)显示菜单,当你运行 menuconfig UI 时可以接受选择等等。
例如,位于内核源代码树根目录的 Kconfig 文件用于填充 menuconfig UI 的初始屏幕。可以查看一下它的内容;它通过引用内核源代码树中不同文件夹中的各种其他 Kconfig 文件来工作。内核源代码树中有很多 Kconfig* 文件(在 6.1.25 版本中超过 1700 个)!每个文件通常定义一个菜单项,这让我们意识到构建系统的复杂性。
举个真实的例子,让我们查找定义以下菜单项的 Kconfig 条目:Google Devices | Google Virtual NIC (gVNIC) support。Google Cloud 使用一个虚拟网络接口卡(NIC),称为 Google Virtual NIC。很可能他们的基于 Linux 的云服务器会使用它。它在 menuconfig UI 中的位置如下:
-> Device Drivers
-> Network device support
-> Ethernet driver support
-> Google Devices
以下是显示这些菜单项的截图:
我们如何知道这些菜单项是由哪个 Kconfig 文件定义的呢?相关配置项的帮助屏幕会告诉我们答案!所以,当你在相关菜单项上时,选择 <Help> 按钮并按下 Enter 键;此时,帮助屏幕会显示(除其他信息外):
Defined at drivers/net/ethernet/google/Kconfig:5
这就是描述该菜单的 Kconfig 文件!我们来看一下它的内容:
$ cat drivers/net/ethernet/google/Kconfig
#
# Google network device configuration
#
config NET_VENDOR_GOOGLE
bool "Google Devices"
default y
help
If you have a network (Ethernet) device belonging to this class, say Y.
[ … ]
这是一个简单的 Kconfig 条目;注意 Kconfig 语言中的关键字:config、bool、default 和 help。我们已经反向突出显示了它们。你可以看到,这个设备默认是启用的。我们稍后会详细介绍语法。
下表总结了 Kconfig* 文件和它们在 Kbuild UI 中对应的子菜单:
| 菜单 | 对应的 Kconfig 文件位置 |
|---|---|
主菜单,menuconfig UI 的初始屏幕 | Kconfig |
| 一般设置 + 启用可加载模块支持 | init/Kconfig |
处理器类型和特性 + 总线选项 + 二进制仿真(这个菜单标题通常与架构相关;在这里,它是针对 x86[_64] 的;通常,Kconfig 文件位于:arch/<arch>/Kconfig) | arch/<arch>/Kconfig* |
| 电源管理 | kernel/power/Kconfig* |
| 固件驱动程序 | drivers/firmware/Kconfig* |
| 虚拟化 | arch/<arch>/kvm/Kconfig* |
| 通用架构相关选项 | arch/Kconfig* |
| 启用块层 + IO 调度器 | block/Kconfig* |
| 可执行文件格式 | fs/Kconfig.binfmt |
| 内存管理选项 | mm/Kconfig* |
| 网络支持 | net/Kconfig, net/*/Kconfig* |
| 设备驱动程序 | drivers/Kconfig, drivers/*/Kconfig* |
| 文件系统 | fs/Kconfig, fs/*/Kconfig* |
| 安全选项 | security/Kconfig, security/*/Kconfig* |
| 加密 API | crypto/Kconfig, crypto/*/Kconfig* |
| 库例程 | lib/Kconfig, lib/*/Kconfig* |
| 内核黑客(意味着内核调试) | lib/Kconfig.debug, lib/Kconfig.* |
通常,单个 Kconfig 文件驱动单个菜单,尽管也可能有多个。现在,让我们继续实际添加一个菜单项。
在“General Setup”菜单中创建一个新菜单项
作为一个简单的示例,我们将在“General Setup”菜单中添加一个布尔类型的虚拟配置选项。我们希望配置项的名称为 CONFIG_LKP_OPTION1。如前表所示,相关的 Kconfig 文件是 init/Kconfig,因为它是定义“General Setup”菜单的元文件。
让我们开始操作(假设你现在位于内核源码树的根目录):
-
可选步骤:为了安全起见,始终备份你要编辑的 Kconfig 文件:
cp init/Kconfig init/Kconfig.orig -
编辑
init/Kconfig文件:vi init/Kconfig -
在文件中选择一个适当的位置进行插入。这里我们选择在
LOCALVERSION_AUTO和BUILD_SALT之间插入我们的自定义菜单项。以下截图显示了我们新添加的条目(在vim编辑器中编辑init/Kconfig文件):config LKP_OPTION1 bool "Enable LKP custom option" default n help This is a custom configuration option added for demonstration purposes.
添加完成后,保存并退出编辑器。
说明
config LKP_OPTION1:定义了一个名为LKP_OPTION1的配置项。bool "Enable LKP custom option":这是一个布尔类型的选项,并且在菜单中显示的名称为“Enable LKP custom option”。default n:默认情况下,此选项被设置为n(即未启用)。help:提供了关于此选项的帮助信息。
供参考,我已经将前面实验的内容作为一个补丁提供给我们这本书的 GitHub 源码树中的原始 6.1.25 init/Kconfig 文件。可以在以下路径找到补丁文件:ch2/Kconfig.patch。
新的项目以 config 关键字开始,后跟你新配置变量 CONFIG_LKP_OPTION1 的 FOO 部分。目前,只需阅读我们在 Kconfig 文件中针对该条目所做的声明。关于 Kconfig 语言/语法的更多详细信息,请参见随后的“关于 Kconfig 语言的一些细节”部分。
保存文件并退出编辑器。
重新配置内核:运行 make menuconfig。然后导航到 General Setup | Test case for LKP 2e book/Ch 2: creating … 下的这个新的菜单项。将该功能开启。请注意,在图 2.21 中,它被突出显示,并且默认情况下是关闭的,这正是我们通过 default n 行指定的。
make menuconfig
[...]
这是相关的输出:
现在,通过按空格键将其打开,然后保存并退出菜单系统。
在此期间,尝试按 < Help > 按钮。你应该会看到我们在 init/Kconfig 文件中提供的帮助文本。
检查我们的功能是否已被选择:
$ grep "LKP_OPTION1" .config
CONFIG_LKP_OPTION1=y
$ grep "LKP_OPTION1" include/generated/autoconf.h
$
我们发现它确实已经在 .config 文件中设置为打开(y),但还没有出现在内核的内部自动生成的头文件中。这将在我们构建内核时发生。
现在让我们通过有用的非交互式 config 脚本方法来检查它。我们在“使用内核的 config 脚本查看/编辑内核配置”部分中介绍了这一点。
$ scripts/config -s LKP_OPTION1
y
啊,正如预期的那样,它是开启的(-s 选项与 --state 相同)。接下来,我们通过 -d 选项禁用它,查询它(-s),然后通过 -e 选项重新启用它,再次查询它(只是为了学习):
$ scripts/config -d LKP_OPTION1 ; scripts/config -s LKP_OPTION1
n
$ scripts/config -e LKP_OPTION1 ; scripts/config -s LKP_OPTION1
y
构建内核。别担心,构建内核的详细步骤将在下一章中介绍。你可以现在跳过这一部分,或者先阅读第 3 章《从源代码构建 6.x Linux 内核 – 第 2 部分》,然后再回到这里。
make -j4
此外,在最近的内核版本中,构建步骤完成后,每个已启用的内核配置选项(无论是 y 还是 m)都会在 include/config 目录下显示为一个空文件,我们的新配置也是如此:
$ ls -l include/config/LKP_*
-rw-r--r-- 1 c2kp c2kp 0 Apr 29 11:56 include/config/LKP_OPTION1
完成后,重新检查 autoconf.h 头文件中是否存在我们的新配置选项:
$ grep LKP_OPTION1 include/generated/* 2>/dev/null
include/generated/autoconf.h:#define CONFIG_LKP_OPTION1 1
include/generated/rustc_cfg:--cfg=CONFIG_LKP_OPTION1
include/generated/rustc_cfg:--cfg=CONFIG_LKP_OPTION1="y"
它成功了(从 6.0 版本开始,甚至 Rust 也知道它了!)。然而,在实际项目或产品中,通常需要进一步的步骤才能利用我们新的内核配置。我们通常需要在使用此配置选项的代码相关的 Makefile 中设置配置条目。
这里有一个简单的示例。假设我们在一个名为 lkp_options.c 的 C 源文件中编写了一些内核代码。现在,我们需要确保它在内核编译时被编译并构建到内核映像中。怎么做?一种方法是在内核的顶层(或其自身)Makefile 中添加以下行,以确保它在构建时被编译到内核中;将其添加到相关 Makefile 的末尾:
obj-${CONFIG_LKP_OPTION1} += lkp_option1.o
不要太担心这种看似奇怪的内核 Makefile 语法。接下来的几章会对其进行详细讲解。另外,我们在“如何工作 Kconfig+Kbuild 系统的简要概述”部分也讨论了这种语法。
此外,你应该意识到,相同的配置也可以作为正常的 C 宏在内核代码中使用。例如,我们可以在树内核(或模块)C 代码中这样做:
#ifdef CONFIG_LKP_OPTION1
do_our_thing();
#endif
同时,非常值得注意的是,Linux 内核社区已经制定并严格遵守一些严格的编码风格指南。在这种情况下,指南指出,尽量避免使用条件编译;如果需要使用 Kconfig 符号作为条件,请这样做:
if (IS_ENABLED(CONFIG_LKP_OPTION1))
do_our_thing();
Linux 内核编码风格指南可以在这里找到。我建议你经常参考它们,并且当然要遵循它们!
Kconfig 语言的细节
我们目前对 Kconfig 语言的使用(如图 2.20 所示)只是冰山一角。实际上,Kconfig 系统使用 Kconfig 语言(或语法)通过简单的 ASCII 文本指令来表达和创建菜单。该语言包括菜单条目、属性、依赖关系、可见性约束、帮助文本等内容。
内核在此处记录了 Kconfig 语言的构造和语法:Kconfig 语言文档。请参考该文档以获取完整的详细信息。
以下表格简要介绍了一些常见的 Kconfig 构造:
| 构造 | 含义 |
|---|---|
config <FOO> | 指定菜单条目名称,格式为 CONFIG_FOO,此处在 config 关键字后使用 FOO 部分。 |
| 菜单属性 | |
bool ["<description>"] | 指定配置选项为布尔类型;其值在 .config 中将为 y(编译进内核镜像)或不存在(显示为被注释掉的条目)。 |
tristate ["<description>"] | 指定配置选项为三态类型;其值在 .config 中将为 y、m(编译为内核模块)或不存在(显示为被注释掉的条目)。 |
int ["<description>"] | 指定配置选项为整数值。 |
range x-y | 对整数进行范围约束,有效范围为 x 到 y。 |
default <value> | 指定默认值;根据需要使用 y、m、n 或其他值。 |
prompt "<description>" [if <expr>] | 一个描述性句子的输入提示(可以是条件性的);一个菜单条目最多只能有一个提示。 |
depends on "expr" | 定义菜单项的依赖关系;可以使用类似 `depends on FOO1 && FOO2 && (FOO3 |
select <config> [if "expr"] | 定义反向依赖关系。 |
help "my awesome help text … bleh bleh bleh " | 当选择 < Help > 按钮时显示的文本。 |
表 2.5:Kconfig 的一些构造
为了帮助理解语法,以下是来自 lib/Kconfig.debug 的一些示例(该文件描述了 UI 中 Kernel Hacking 子菜单的菜单项——实际指的是内核调试)。你也可以在线浏览它们:Kconfig.debug 源代码。
我们将从一个简单且易于理解的示例(CONFIG_DEBUG_INFO 选项)开始。
config DEBUG_INFO
bool
help
A kernel debug info option other than "None" has been selected
in the "Debug information" choice below, indicating that debug
information will be generated for build targets.
接下来,我们来看一下 CONFIG_FRAME_WARN 选项。注意它的范围和条件默认值语法,如下所示:
config FRAME_WARN
int "Warn for stack frames larger than"
range 0 8192
default 0 if KMSAN
default 2048 if GCC_PLUGIN_LATENT_ENTROPY
default 2048 if PARISC
default 1536 if (!64BIT && XTENSA)
default 1280 if KASAN && !64BIT
default 1024 if !64BIT
default 2048 if 64BIT
help
Tell the compiler to warn at build time for stack frames larger than this.
Setting this too low will cause a lot of warnings.
Setting it to 0 disables the warning.
CONFIG_FRAME_WARN 选项定义了一个整数值,用于在编译时警告超过某个堆栈帧大小的情况。它有一个范围约束(0 到 8192),并根据不同的条件设置默认值。例如,如果 KMSAN 被启用,默认值将是 0;如果启用了 GCC_PLUGIN_LATENT_ENTROPY,默认值将是 2048,依此类推。
接着,CONFIG_HAVE_DEBUG_STACKOVERFLOW 选项是一个简单的布尔值;它要么打开要么关闭(内核要么具备检测内核空间堆栈溢出的能力,要么不具备)。而 CONFIG_DEBUG_STACKOVERFLOW 选项也是一个布尔值。注意它如何依赖于两个其他选项,这两个选项由布尔 AND(&&)操作符分隔:
config HAVE_DEBUG_STACKOVERFLOW
bool
config DEBUG_STACKOVERFLOW
bool "Check for stack overflows"
depends on DEBUG_KERNEL && HAVE_DEBUG_STACKOVERFLOW
help
Say Y here if you want to check for overflows of kernel, IRQ
and exception stacks (if your architecture uses them). This
option will show detailed messages if free stack space drops
below a certain limit. [...]
另一个有用的点是:在配置内核时(通过常用的 make menuconfig UI),点击 < Help > 不仅会显示一些(通常有用的)帮助文本,还会显示各种配置选项的当前运行时值。你也可以通过搜索配置选项(如前所述,使用斜杠 / 键)来查看这些信息。例如,键入 / 并搜索名为 KASAN 的内核配置项;这是我在搜索时看到的内容。
如果你不知道,KASAN 是内核地址消毒器(Kernel Address SANitizer)——这是一项出色的基于编译器的技术,能够帮助捕捉内存损坏缺陷。我在《Linux Kernel Debugging》一书中对其进行了深入介绍。
仔细看看“Depends on: ”那一行;它显示了依赖项及其当前的值。需要注意的是,除非满足依赖条件,否则该菜单项甚至不会在 UI 中显示。
好了!这就完成了我们对 Kconfig 文件的介绍、创建或编辑内核配置中的自定义菜单项、一些 Kconfig 语言语法的介绍,以及本章的内容。
总结
在本章中,你首先了解了 Linux 内核的版本命名法(记住,Linux 内核发布是基于时间而非功能的!)、各种类型的 Linux 内核(如 -next 树、-rc/mainline 树、稳定版、LTS、SLTS、发行版、自定义嵌入式内核)以及基本的内核开发工作流程。然后,你学习了如何获取 Linux 内核源码树并将压缩的内核源码树解压到磁盘上。在此过程中,你还快速了解了内核源码树的整体布局,使其结构更加清晰。
之后,你学习了如何进行内核配置——这是内核构建过程中关键的一步!此外,你还学习了如何自定义内核菜单、向其中添加自己的条目,以及有关 Kconfig/Kbuild 系统及其使用的 Kconfig 文件的一些知识。
掌握如何获取和配置 Linux 内核是一项非常有用的技能。我们才刚刚开始这段漫长而激动人心的旅程。你会发现,随着对内核内部结构、驱动程序和目标系统硬件的更多了解,你为项目目的微调内核的能力也会不断提升。
我们已经完成了一半的自定义内核构建任务;建议你消化本章内容,动手尝试本章中的步骤,完成问题/练习,并浏览“进一步阅读”部分。然后,在下一章中,我们将实际构建 6.1.25 内核并进行验证!