现代 Linux 内核开发者应掌握的核心知识

Core Knowledge That Modern Linux Kernel Developer Should Have
语言

Linux 内核是用 C 编程语言编写的,因此 C 语言对于 Linux 内核开发者来说是最重要的语言。最初,内核是用 GNU C 编写的(现在也可以使用 LLVM 构建),它用一些附加的关键字和属性扩展了标准 C。我建议学习一些现代 C 版本,如 C11,并额外学习 GNU 扩展,以便能够有效地阅读内核代码。内核中小的、特定于架构的部分以及一些高度优化的驱动程序部分是用汇编语言编写的。这是第二选择的语言。现在主要有 3 种架构:x86、ARM 和 RISC-V。选择哪种汇编语言取决于您的硬件平台。

您绝对应该关注 Rust,它在 Linux 内核社区中越来越受欢迎,作为 C 语言更安全可靠的替代方案。

Linux 是一个高度可配置的系统,其可配置性基于内核构建系统 KBuild。每个开发者都应该了解 KBuild 和 Make 的基础知识,以便能够成功地扩展/修改内核代码。最后但同样重要的是 shell 脚本。很难想象没有命令行使用情况的内核开发,开发者不可避免地要编写一些 shell 脚本来通过自动化重复性任务来支持他们的工作。

软件环境

Linux 内核开发与 Git 源代码控制系统密不可分。现在无法想象没有 Git 的内核开发工作流程。因此,Git 知识是一项要求。

除非内核开发者在特定的/定制的硬件上运行他们的内核 - 仿真才是开发者最好的朋友。最流行的平台是 Qemu/KVM。一个典型的工作流程是这样的:开发者对内核或驱动程序进行一些更改,构建它,将其复制到虚拟环境中,并在那里进行测试。如果一切正常,那么开发者将在真实的硬件上测试这些更改,但如果出现问题,那么虚拟机下的内核就会崩溃。在这种情况下,只需关闭虚拟机,修复错误并重复开发/调试周期就很容易了。如果我们没有虚拟化,我们将在每次内核崩溃时重启真实的机器,开发时间将增加一个数量级。

与用户空间不同,内核的调试能力有限。实际上,最流行的内核调试方法曾经是(有时仍然是)插入 printk 函数调用,它将其输出存储到内核的循环缓冲区中,并在用户空间中使用 dmesg -kw 命令分析此输出。自内核版本 2.6 以来,引入了一个新的内核内 ftrace 框架。从那时起,它一直在发展,现在它已经非常全面和强大。它提出了许多调试方法和多种输出格式。最流行的功能 - 跟踪整个内核、部分内核或特定模块的内核堆栈跟踪,并在一个特殊文件中显示其输出。这为开发者节省了数小时的调试时间。此外,它在非活动状态下是零开销的。每个现代内核开发者都应该了解 ftrace

在许多情况下,开发者会遇到他的内核模块运行缓慢的情况。这就是 perf 发挥作用的地方。perf 是一对内核内性能分析框架和一个用户空间工具,用于帮助分析内核内性能。用于收集内核运行时信息的最复杂和灵活的工具是 eBPF 框架,它允许在内核内部运行用户程序并将信息传递到用户空间。实际上,通过启用用户定义的内核遥测技术,这个内核框架彻底改变了内核的可观测性。

内核开发的主要领域之一是嵌入式开发。实际上,大多数嵌入式设备,从智能家居的 IoT 到基于 Android 的智能手机,都搭载了 Linux 内核的变种。嵌入式世界中有 2 个主要的构建系统:Buildroot 和 Yocto。前者更简单直接,后者更灵活和复杂。两者都旨在构建高度定制的 Linux 发行版,即 Linux 内核 + 一组用户空间软件,为特定的硬件板量身定制。值得一提的是,嵌入式开发者必须了解并能够创建/更新 dts 文件,描述板上的一组硬件组件。嵌入式世界中的主要引导加载程序是 u-boot,了解它也是一项要求。说到用户空间,最简单和最著名的极简框架之一是 busybox,它只包含最少量的必要实用程序。它的体积非常小,因此方便嵌入式和模拟(Qemu/KVM)开发。

最后但同样重要的是,开发环境。大多数 Linux 内核开发者在终端中使用 vim(或 qemu)文本编辑器,tmux 作为现代且方便的终端复用器,cscope 用于为内核源代码构建交叉引用。

Linux 内核核心概念

Linux 内核开发技术技能分为两类:通用技能和特定领域技能。通用技能是每个内核开发者都应该知道的,而特定领域技能是特定领域(例如:网络、存储、虚拟化、密码学、嵌入式等)的开发者应该知道的。Linux 内核非常庞大,不可能以相同的详细程度了解每个部分。

让我们从通用技能开始

  1. 内核编码风格 - Linux 内核有自己的编码风格,这种风格在一个子系统到另一个子系统之间可能略有不同。定期使用内核源代码树中的特殊脚本 scripts/checkpatch.pl 检查您的代码始终是一个好主意。
  2. 内核编码模式 - 内核有一组推荐使用的编码模式。其中最著名的是在使用 goto 运算符的多步骤资源初始化期间分配/释放资源。
  3. 内核内部数据结构 - 内核中有几个最重要的数据结构在全局范围内使用,每个开发者都必须了解。这些是:单链表和双链表、队列、哈希表、二叉树、红黑树、Maple 树等等。
  4. 同步原语 - 回到 2000 年代初,第一批商品级 SMP CPU 被推出。从那时起,每个内核开发者都必须在编写代码时考虑到多线程。Linux 内核有很多同步原语,每种原语都有不同的用途:原子操作、自旋锁、信号量、互斥锁、RCU(无锁算法类)等等。
  5. 中断处理:上半部和下半部 - Linux 内核有一个独特的中断处理方案:上半部和下半部。上半部旨在尽可能快地处理中断并返回,而下半部是延迟工作,进一步处理上半部传递的结果。例如:上半部尽可能快地将来自网卡的新数据包复制到主内存并返回,等待新的数据包。稍后作为延迟工作运行的下半部,检查接收到的数据包并处理它:填充一些字段,创建适当的数据结构,并将其传递给内核的网络堆栈。每个开发者都必须了解这种中断处理方案并适当地设计他们的中断处理程序。
  6. 延迟工作 - Linux 内核开发中的常见情况是将部分工作推迟到未来的某个时刻。前面提到的中断就是一个很好的例子。内核中有几种针对不同情况的延迟工作机制:任务队列、软中断、任务小片、工作队列等等。
  7. 内存管理 - 内核开发者应该了解 2 个层次的内存管理:最低的本地层由函数 kmalloc/kfree 组成,以及 slab 层,slab 层构建在本地层之上,旨在将不同大小的结构存储在不同的缓存中,以避免内存碎片。
  8. 虚拟文件系统 - 无论下层文件系统的类型(ext3、ext4、zfs、lustrefs、xfs 等...),内核都在其之上维护一个通用接口。值得获得 VFS 的一般知识,因为文件系统交互是内核和用户空间之间最流行的通信方法之一。
  9. 调度器 - 调度器管理操作系统中的所有进程:内核和用户空间。开发者必须了解它的基础知识。
  10. 系统调用接口 - 内核和用户空间之间通信的主要方式是系统调用接口。用户空间中的 libc 库封装了它,并为开发者提供了更全面和方便的函数,但有时需要直接调用系统。因此,用户空间和内核程序员都应该知道如何做到这一点。在极少数情况下,内核开发者可能希望添加新的系统调用或使用 ftrace/strace 跟踪其参数。有用的知识。
  11. /sys /proc 目录 - 内核和用户空间之间交互的第二种最流行的方式是文件系统。特别是,大量的信息和设置包含在 /sys/proc 目录中。值得学习这些结构。
  12. 可加载内核模块 - Linux 内核开发者的主要工作之一是为新的硬件设备开发驱动程序。Linux 驱动程序以可加载内核模块的形式制作,可加载内核模块是从使用特定结构制作的源代码创建的特殊格式二进制文件。这些模块可以在不重启系统的情况下加载/卸载。开发者应该总体上了解内核模块的结构,以及字符设备、块设备和网络设备的附加规则。此外,他们应该了解与用户空间通信的方式:sysfs 属性、MMIO、内核模块参数等等。
  13. Udev - 驱动程序开发者应该了解 Udev 子系统,该子系统实现了支持在设备热插拔时运行用户脚本的基础设施。
  14. 故障注入框架 - 允许通过将错误结果注入到通常总是正确的函数(如内存分配的 malloc)中来测试不寻常的代码路径。
  15. 内核消毒器 - KASAN、KMSAN 等等。动态工具可以捕获一些糟糕的情况,如内存损坏。加载新的内核模块,运行工作负载,并尝试捕获一些微妙的动态错误总是值得的。
  16. 锁定正确性验证器 - 内核/模块代码的部分实现了复杂的锁定方案。这通常会导致死锁和活锁。调试这样的代码很困难。Lockdep 运行时验证器可以捕获这种情况,并节省数小时的调试工作。
  17. Kdump/Kexec - 在某些情况下,几乎不可能调试代码,特别是当它与早期的系统启动时间代码有关时。这就是 Kdump/Kexec 发挥作用的地方。它加载第二个崩溃内核,该内核拦截崩溃的内核并制作其转储以供进一步分析。

当涉及到特定的、领域相关的技能时,唯一的正确答案是:这取决于情况。Linux 内核包含许多专门的框架。仅举一个例子:对于嵌入式开发,值得学习 I2C (SMB)、SPI 和 GPIO 框架。

用户空间工具

内核开发者应该具备一些用户空间知识并使用常用工具,例如

  • bash(或替代 shell)- 构建内核、编写例行操作脚本等。
  • ssh(安全网络 shell)- 用于登录并在目标机器上工作,目标机器可以是远程网络机器、虚拟机或嵌入式设备。
  • tmux(终端复用器)- 支持多终端配置。内核开发的便捷工具。一个窗口可以显示内核构建过程的日志,第二个窗口有一个远程 ssh shell,第三个窗口打开了 vim 编辑器。
  • minicom - 这是与未配备以太网/wifi/蓝牙模块的嵌入式板卡配合使用的主要工具。Linux 内核可以配置为在早期启动期间创建 UART 控制台。反过来,开发者拥有 2 线 UART-TO-USB 适配器,可以将 RX 和 TX 线连接到适当的 GPIO 引脚,并将 USB 适配器插入他们的笔记本电脑,运行 minicom,并获得连接。
  • vim - 大多数内核开发者的首选编辑器。此外,还有许多不包含 GUI 的服务器,而 Vim 是那里的唯一选择。
  • gdb - 可用于调试内核 OOPS 错误。拥有 OOPS 指令地址并加载带有调试符号的未压缩内核镜像 (vmlinux),可以遍历错误的堆栈跟踪来分析代码。
软技能
  • 热情 - 内核开发是一项艰苦而细致的工作。没有激情不可能在其中取得成功。
  • 耐心 - 这种类型的工作需要在各个方面都保持耐心。首先,这是一项艰苦的工作,意味着仔细的代码设计和调试,有时需要数小时的调试才能检测和修复一个小错误。其次,内核不断变化的性质要求您不断更新自己最新的变化,并准备好相应地更新您的代码,特别是已被主线内核接受的代码。第三,与社区合作是一个艰难且有时存在争议的过程。让您的想法被接受并非易事。
  • 毅力 - 内核开发者应该在不断学习和与社区沟通方面保持毅力,如果他们希望他们的代码被主线内核接受。

Roman 是一位拥有 20 多年经验的软件开发者,目前在英特尔担任 Linux 内核开发者, 支持即将推出的 Xeon CPU 系列的最新功能。这是他的 LinkedIn 个人资料。

加载 Disqus 评论