心中的内核 —— 在阅读内核代码之前先理解内核

[TOC]
介绍
这并非一部教你编写内核代码的指南,而是一场探寻Linux内核设计思维的思想之旅。
在系统编程的世界里,人们常常迷失于符号定义、头文件结构与实现细节的丛林中。然而剥离代码表象,内核实则是一个井然有序的响应式系统——它受上下文环境所规约,以分离原则为基石,从内存管理到任务调度的每一处设计,都浸透着精准的设计意图。
本系列内容专为那些渴望在内核源码之外构建内核运行心智模型的探索者而作。无论你是初次叩响Linux内核内部机制的大门,还是带着全新思考重返这片领域,我们聚焦的核心始终是行为逻辑,而非语法细节。
每一篇文章起初都是独立成章的思考札记,而当它们汇聚在一起,便勾勒出一幅概念图谱——这幅图谱不关乎函数调用的细枝末节,而是内核如何响应外部请求、执行机制约束、实现模块隔离与提供系统服务的底层逻辑。 内核掌管着系统运行的方方面面,让我们一同洞悉它的运行之道。
01 内核不是进程,而是系统
Linux 内核既不是进程、守护进程(daemon),也不是应用程序。它是一个具有特权的、驻留在内存中的环境,构成了操作系统的基础。与用户程序不同,它不被调度,没有进程标识符(PID),也不像传统任务那样启动或停止。相反,它始终存在——在引导时加载到内存中——并管理硬件和软件之间的所有交互。
一旦被引导加载程序加载,内核就会在 start_kernel() 中开始执行,在那里它初始化内存管理、设备接口和核心子系统。在这个一次性设置之后,内核不会作为独立任务继续运行。相反,它成为一个响应式执行层,仅在需要时被调用——由用户进程、硬件事件或其自己的内部线程调用。
内核代码在三种主要上下文中执行:(1)通过用户进程发起的系统调用(system call),(2)通过硬件触发的中断处理程序(interrupt handlers),以及(3)在完全在内核空间中运行的长期存在的内核线程(kernel threads)中。这些由内核自身创建和管理的线程处理后台任务,例如内存回收、I/O 调度和同步。尽管它们出现在进程列表中(通常用方括号括起来),但它们不是用户空间的守护进程,也从不执行用户空间代码。
第一个这样的线程是 kthreadd,分配的 PID 为 2。它在 rest_init() 函数的初始化最后阶段创建,负责生成所有其他内核线程。就像 PID 1(init 或 systemd)启动用户空间一样,PID 2 标志着内核线程运行时的开始。
内核线程的数量不是固定的。在系统启动时,系统可能会创建 20-40 个基本线程——每个核心对应一个用于软中断(soft IRQs)、 watchdog、迁移助手和早期工作队列的线程。随着系统变得活跃,会根据需要为 I/O、内存管理、文件系统和设备驱动程序创建额外的线程。在典型的现代 Linux 系统上,可能会同时运行 100-150 个内核线程,并随着工作负载动态扩展。
尽管它们可见,但内核线程不是独立程序。内核本身不是运行的任务——它是一个永远存在的执行环境。它是被进入的,而不是被调度的。它提供结构、控制和特权——使所有任务能够运行,同时作为任务本身保持不可见。
简而言之,内核不是系统内的一个进程——它是系统的核心。始终驻留,始终具有特权,始终处于控制之中。

02 为进程服务:内核的首要职责
在运行时,Linux 内核管理内存、调度任务、处理 I/O、响应中断并实施系统安全策略。这些职责至关重要,但它们本身并非最终目标。
内核存在的意义是为用户进程服务。
其工作是确保每个进程可靠、安全且高效地运行。如果内核未能响应系统调用、分配内存、访问存储或实施隔离机制,则意味着其核心目标的失败。
重要的是,内核不会自主运行。它仅在三种情况下进入执行状态:来自用户空间的系统调用、硬件设备的中断,或计划执行系统任务的内部线程。这些情况中的每一种都是对外部需求的响应,而这些需求通常源自用户进程。内核仅在需要时才被激活。
试想启动一个进程时会发生什么。用户调用 exec,内核必须通过虚拟文件系统解析二进制路径,使用底层文件系统驱动程序加载文件,分配并映射内存,通过安全模块验证访问权限,并将进程调度为可执行状态。这些步骤中的每一个都涉及不同的子系统,没有任何一个子系统能够独立完成任务。为了启动单个进程,所有步骤必须按顺序完成。
即使是一个简单的读取调用也会跨越多个边界。系统调用处理程序会从进程的任务结构(task structure)中验证文件描述符。虚拟文件系统(Virtual File System,VFS)会定位关联的文件对象。根据文件类型的不同,读取请求可能会发往普通文件、管道或套接字。如果内存缓冲区位于未映射的页面上,内存管理器必须先解决缺页(page fault)问题,然后才能复制数据。只有当所有这些操作都成功完成后,内核才会返回用户空间。
相同的模式适用于所有 I/O、网络和进程间通信。用户的每一个操作都会引发一系列内部协调工作。内核的任何一个部分都无法单独交付结果,始终需要整个系统协同工作。
内核线程也不例外。当回收内存或刷新脏缓冲区时,它们并非为自身行动,而是为了保持系统健康,使用户进程能够持续运行。它们的工作直接支持用户空间中正在进行或未来的执行。
这就是 Linux 内核的结构。每个子系统都围绕进程支持进行组织,每项内部服务的存在都是为了响应、支持或保护进程的执行。它不是一个闲置的核心,而是一个响应式、协作式的系统。 内核的重要性并非在于它执行了许多任务,而在于它为其他事物提供服务时执行这些任务。
那个“其他事物”就是用户进程。



03 代码之前的概念图
Linux 内核并非按功能组织,而是由必须在并发、硬件交互和故障条件下成立的规则构成。这些规则定义了执行流程、可安全调用的代码以及允许的操作。它们并非实现细节,而是其设计的基础。
内核在所有处理器和任务上运行,但不会盲目共享执行状态。每次进入内核的调用都与当前线程的身份相关:内存空间、文件描述符、信号状态和特权级别。函数基于此上下文运行,而非全局变量。这种隔离可防止干扰,并支持代码在任务间的安全复用。
进入内核的方式决定了其可以执行的操作。系统调用、陷阱、中断和内部线程以不同的约束条件进入。有些路径可以睡眠,有些则不能;有些会访问用户内存,有些则停留在内核空间。这些差异决定了哪些函数有效、哪些必须预先分配,以及控制如何返回。它们塑造了内核代码的编写方式和调用时机。
内核还必须表现出可预测的行为。快速路径避免阻塞,对时间敏感的代码避免使用锁和分支,抢占和调度受到严格控制。这些需求催生了 per-CPU 变量、无锁数据结构和有界执行时间。确定性是响应能力和故障隔离的必要条件。
arch/ 下特定于体系结构的代码处理底层入口、上下文切换、陷阱和分页。它将硬件行为转换为核心内核的一致接口。这种抽象在不影响对 CPU 特定行为控制的前提下实现了可移植性。
内存根据策略分配。请求在原子性、对齐方式、设备可见性和可回收性方面各不相同。子系统有不同的需求,内核通过分层分配器和受保护的 API 来响应。这些路径由正确性而非便利性决定。
内核被设计为可恢复。如果驱动程序行为异常或用户输入无效,它不能崩溃或损坏状态。故障被隔离,缓冲区被验证,转换受到保护。鲁棒性是核心设计考量。
用户空间通过定义的接口进入,但这些接口并不定义内核的结构。重要的是控制如何流动、触及哪些资源以及必须保留哪些保证。
不能将内核视为实用程序的集合。其结构的存在是为了维护安全性、隔离性、确定性和可恢复性。这是它的编写方式,也必须是理解它的方式。

04 作为分层系统的内核:虚拟、映射、隔离、控制
Linux 内核并未呈现单一、统一的系统视图,而是公开了许多受控视图——每个视图都与任务绑定,由上下文塑造,并受策略约束。这些视图并非动态组装而成,而是通过虚拟、映射、抽象、隔离和控制等层次构建而来。
这种结构的存在是为了在并发、抢占和硬件故障情况下使行为可预测。每个层都有定义好的作用域,没有任何一层是单独运行的。内核避免使用全局状态,而是依赖映射、间接和抽象,从而使访问是有意为之的,执行是受限的。
执行始于硬件边界。特定于体系结构的代码处理陷阱、故障和中断,定义了 CPU 如何响应系统调用或页面故障而进入内核。从一开始,内核就将执行与当前任务和调度上下文绑定。
任务并非自主运行,它们被排队、分配给 CPU,并根据需要被抢占。调度器执行策略和公平性。定时器、RCU(读-复制-更新)和延迟工作限制了并发性和时间安排。
抽象定义了内核公开功能的方式。系统调用作用于实现标准接口的内核对象。VFS(虚拟文件系统)抽象文件系统,块层抽象设备,网络栈抽象协议。像 file_operations 和 netdev_ops 这样的接口定义了行为,而不暴露实现。
调度遵循接口表。文件、套接字和设备不暴露内部结构。read() 或 ioctl() 等操作通过函数指针路由。行为是动态选择的,支持替换和模块化重用。
访问通过映射解析。文件描述符变为 file struct,虚拟地址变为物理页面,路径变为 dentry 和 inode。这些转换是任务范围的且经过验证,没有任何内容是直接访问的。
间接性强制分离。内核通过引用——函数表、 per - 任务指针、页表——而非直接访问来路由行为和访问。即使用户空间内存也被视为请求,通过 copy_from_user() 等助手函数解析。间接性确保所有访问都经过中介且具有上下文意识。
每个任务携带自己的上下文:内存映射、文件表、凭据、命名空间。这些结构定义了它能看到什么和做什么。Cgroups 限制使用,LSM(Linux 安全模块)执行策略。默认情况下不相信任何输入,每个转换都经过验证。

05 单体形式,协同行为:真正的内核模型
Linux 内核在结构上是单体的。其核心子系统——调度、内存管理、文件系统、网络和驱动程序——被编译成单个二进制文件。它们共享一个地址空间,以特权模式运行,并直接相互调用。在结构上没有隔离来分隔组件。但在运行时,内核行为由所有子系统都必须遵循的系统级约束所塑造。
执行上下文决定了内何在任何时刻可以执行的操作。代码在进程、内核线程、中断或软中断(softirq)上下文中运行。进程和内核线程上下文允许睡眠、阻塞、用户内存访问和页面故障(page faults)处理。中断和软中断上下文则不允许。这些路径对时间敏感,并且不能阻塞或进行调度,因为这样做会延迟其他任务。页面故障处理是不允许的,因为解决故障可能涉及 I/O、内存分配或回收,所有这些都需要睡眠。这些约束是全局适用的,并影响每个内核决策。
子系统通过这些共享规则进行交互。调度器避免抢占原子路径。分配器在阻塞前检查上下文和标志。文件系统通过从非阻塞状态到阻塞状态的有序转换来执行 I/O。网络栈从中断上下文开始,经过软中断和工作队列。设备驱动程序延迟无法安全就地完成的工作。这不是惯例,而是设计。子系统将工作通过有效阶段进行处理,而不是一次性处理所有事情。
同步机制也反映了相同的原则。自旋锁(spinlocks)用于原子路径。互斥锁(mutexes)仅在允许睡眠的地方使用。RCU(读-复制-更新)使读取者能够在不锁定的情况下继续操作。顺序锁(Seqlocks)允许对更新进行快速重试。这些原语是基于上下文和访问模式选择的,而不是开发人员的偏好。用法通过宏、断言和在内核中一致执行的规则来验证。
内存访问遵循相同的模型。访问用户内存需要进程上下文。故障处理只能在允许睡眠的地方进行,因为解决故障可能涉及磁盘 I/O 或内存回收。分配行为取决于标志和上下文。同一个函数可能会阻塞、立即返回或失败,具体取决于它在哪里运行。内存管理要考虑可见性、局部性和上下文。
延迟执行将这些层连接起来。从中断开始的工作被传递到软中断,然后到工作队列,最后到内核线程。每个步骤的设计都满足下一步的约束。这种分阶段模型支持 I/O、网络、定时器和驱动程序。
内核被构建为一个二进制文件,但它作为一个协同系统运行。子系统不能独立运行。它们遵循由上下文、时间和并发性定义的共享模型。它在形式上是单体的,但在执行上是模块化和有原则的。

06 内核对象揭示设计——函数仅执行设计
使用 Linux 内核的工作通常始于跟踪。调用一个系统调用,路径会在调用栈和分支中展开。数据流动,锁被获取,结构被更新。跟踪显示了内核在特定路径上的行为,但这只是表面现象。它揭示的是执行过程,而非结构。
要将内核理解为一个系统,关注点必须从过程转移到对象——从函数的作用转移到其背后持续存在的事物。
内核通过一组长期存在、相互关联的对象来运行,这些对象既代表状态,又代表控制。执行以 task_struct 为锚点,它将调度、内存映射、凭据、打开的文件和命名空间联系在一起。该结构链接到用于地址空间的 mm_struct、用于身份和特权的 cred,以及用于持久文件元数据的 inode。当进程通信时,msg_queue 对象管理流和阻塞。当数据包移动时,sk_buff 跟踪它们在栈层中的传输。每个对象都扮演着特定的角色,但没有一个是单独行动的。它们共同协调访问、并发和策略。它们的契约——所有权、可见性和生命周期——构成了内核执行背后的稳定基础。
这些对象不会被直接访问。大多数路径从当前活动任务 current 开始。从那里,文件描述符、PID 或 IPC 密钥等句柄通过受引用计数、锁定或 RCU 保护的查找路径解析为内部结构。这种间接性并非优化——它强制实现有效性和隔离。
以 do_msgsnd() 为例。跟踪显示了参数如何被处理、如何找到队列以及如何将消息入队。但队列限制、发送方阻塞和唤醒行为全部由 msg_queue 对象定义。函数执行一系列操作,而对象定义了该序列必须遵守的契约。
上下文进一步影响访问。在进程上下文中,允许阻塞和分配;在中断上下文中,则不允许。在两种上下文中使用的对象必须反映这一点——某些字段必须是原子的,其他字段则是延迟的,还有一些字段是禁止访问的。这些不是最佳实践,而是结构性保证。
作用域和生命周期是明确的。task_struct 在退出时释放;cred 若被共享则可能继续存在;inode 可以在缓存中持续存在,超出使用时间;net_device 可能在整个系统运行时间内都存在。每个对象都定义了谁可以访问它、它的有效时间以及如何安全地停用它。
函数在这个模型内运行——它们不定义模型。内核不是在调用栈上运行,而是在超越调用栈持续存在的对象上运行,这些对象强制实现责任、约束和连续性。跟踪显示发生了什么,对象解释为什么它有效。对它们了解得越多——它们的角色、生命周期和访问模式——内核就越清晰易懂。

07 无冲突的代码——内核如何在并发风暴中保持安全
Linux 内核为所有进程和线程所共享,每个进程、每个线程、每个 CPU 都运行相同的代码库。然而,系统并未在并发的压力下崩溃,线程之间没有数据冲突,没有泄漏的文件描述符,也没有损坏的状态。
为什么?因为内核是围绕间接性、上下文感知,以及关键的无状态代码而设计的。
大多数内核代码避免使用持久的全局状态,不在函数内部跟踪“谁在调用”。相反,它依赖外部上下文:一个通常通过 current 宏访问的 per - 线程指针,该指针告诉内核调用者的身份、可以访问的内存,以及拥有的文件或凭据。
这使得内核代码在功能上是无状态的。每次调用不依赖全局变量,仅对从调用线程上下文解析的数据进行操作。这就是内核可重入的原因:同一个函数可以在多个 CPU 上为多个线程运行,而不会产生干扰。
以 sys_read() 为例,该函数对每个调用者来说看起来都一样。但在内部,它访问 current->files,使用线程自己的内核模式栈,并写入映射到该进程内存的缓冲区。代码路径是相同的,但每次运行看到的东西不同。
什么发生了变化?
输入、指针、引用。
这就是关键。逻辑保持共享,但数据是私有的。内核不会为每个线程重写函数,它只是遵循作用于活动任务的正确指针。
对于管道、缓存或套接字等共享数据,内核应用细粒度锁:自旋锁(spinlocks)、互斥锁(mutexes)和 per - CPU 结构,以最小化争用。在极高热度的路径中,它使用 RCU(读 - 复制 - 更新),这是一种无锁同步策略,允许读取者在更新并行发生时并发访问数据。RCU 是现代内核中可扩展读取性能的基石。
这种设计很强大,但它依赖于正确性。
如果内核跟随了错误的指针,一切都会崩溃。一个使用后释放(use - after - free)漏洞可能会留下对已重新用途内存的引用。缓冲区溢出可能会损坏相邻结构,改变线程所看到的内容,甚至劫持其身份。
这些漏洞会颠覆模型,违反上下文是私有的、隔离的和可信赖的保证。
但当设计得以维持时——当内存受到保护且指针有效时——内核会非常健壮。它能同时安全且高效地处理数千个线程。
内核并不避免并发,而是为并发而构建。
它的代码是通用的,但执行始终是特定的——由间接性驱动,由隔离保护,并且结构上避免假设。
这就是一个内核能够为所有进程和线程服务的方式——永远不会忘记谁是谁。

08 间接的力量——一个内核如何为所有进程服务
如果内核被映射到每个进程中,它如何避免混淆?为什么一个线程的系统调用不会干扰另一个线程的内存或状态?并且,单一的内核镜像如何在不复制自身的情况下为所有用户和CPU服务?
答案是间接性。
间接性意味着不直接访问数据,而是通过一个根据上下文不同而解析结果不同的引用进行访问。内核不指向固定的全局结构,而是使用一个通常称为current的 per - 线程引用,来定位与正在运行任务相关的数据。这就是共享的内核区分数千个隔离进程的方式。
内核空间在代码上是共享的,而非在上下文上。
每个进程都映射相同的高地址范围,其中包含内核代码、只读数据、全局符号、设备映射和动态加载的模块。这些区域由共享的物理页面支持,在所有进程中高效且一致。
但当进程通过系统调用、页面故障或中断进入内核时,它会带来自己的执行上下文。这就是间接性变得至关重要的地方。
内核不使用全局变量来保存每个进程的状态。相反,每个CPU或线程维护一个指向当前正在运行任务的指针,该指针通常从寄存器或内核栈派生,并通过current宏暴露。因此,当内核代码访问current->files时,它跟随的是指向该进程文件描述符表的指针,该指针在运行时动态解析。
这种重定向持续发生。每个系统调用、每个调度决策、每次对内存映射、凭据和信号处理程序的访问都使用间接性来确保正确性,即使相同的代码在所有线程和进程中运行。
这同样适用于内核栈。每个线程都有自己的内核模式栈,在线程创建时分配。当CPU切换到内核模式时,它会切换到与活动线程关联的栈。没有两个线程共享此空间。局部变量、保存的寄存器和返回地址保持隔离,即使对于瞬态执行状态也能保持安全性。
间接性是使内核具有可扩展性和安全性的原因。代码是统一的且始终被映射,但上下文(内核看到和修改的内容)严格绑定到当前正在运行的线程。没有这种模型,内核要么需要为每个进程复制自身,要么就得接受关键数据的不安全共享。

09 内核的设备模型:硬件如何成为/dev
磁盘驱动器不知道/dev/sda是什么,网卡也不知道eth0是什么。
而内核也不指望它们知道。
相反,内核维护着一个结构化模型——一个抽象的层级体系——弥合了物理硬件和用户空间可见的逻辑接口之间的差距。从总线和中断到文件描述符和套接字API,这个模型定义了设备如何被发现、命名和使用。
这一切都始于总线。PCIe、USB、I²C——这些是设备用来自我宣告的通道。内核的总线子系统(drivers/pci/、drivers/usb/等)扫描每条总线,探测连接的设备。如果设备用可识别的厂商和类别响应,内核就会创建一个相应的内部对象(struct pci_dev、usb_device或i2c_client)并注册它。
但仅有设备没有驱动程序是无用的。一旦被发现,内核就会将设备与驱动程序(知道如何操作它的代码)匹配。块设备驱动程序可能会为PCIe NVMe SSD注册一个gendisk,USB驱动程序可能会为串行适配器暴露一个tty接口,网络驱动程序会注册一个net_device并准备好数据包交换队列。这些驱动程序不需要知道哪个应用程序会使用该设备,只需要知道如何使其正常工作。
在驱动程序之上,内核将设备组织成类:块设备、字符设备、网络设备。这里的抽象更加明确。驱动程序绑定到这些类,内核则暴露统一的接口(/dev/sda、/dev/ttyUSB0、eth0),而不管底层总线或设备细节如何。对于块设备,内核管理请求队列;对于字符设备,它通过file_operations路由系统调用;对于网络接口,它与IP栈、套接字层和流量控制集成。
这些接口就是用户空间所看到的。/dev中的文件、/sys/class/net/中的名称、open()返回的文件描述符。应用程序不在乎存储设备是通过SATA、NVMe还是USB大容量存储连接的,这正是关键所在。内核将物理层面的东西抽象为稳定、可导航和统一的东西。
然而,在这种简单性之下是精心的编排。DMA映射、IOMMU转换、中断路由——这些确保数据高效且安全地移动。对/dev/sda的写入会变成一系列内存操作、排队的请求、DMA传输,最终是设备I/O。发送到eth0的数据包会变成一个sk_buff,传递给驱动程序,为DMA映射,并在网络上传输——所有这些转换都不会暴露给应用程序。
内核的设备模型使这一切成为可能。它将设备的本质与工作方式分离,将连接方式与使用方式分离。设备被发现、命名、匹配、抽象——然后才作为可用的东西出现在用户空间。
这就是/dev/sda存在的原因。
不是因为硬件创建了它,而是因为内核创建了它。

10 内核如何看待内存:不是映射,而是责任
我们学习内存时,通常从图表入手:虚拟与物理、用户空间与内核空间、低内存与高内存。这些图表很有帮助,它们为我们提供了一幅地图——内存的布局、地址空间中各部分的位置,以及系统的宏观架构。
但这种视角仍然是静态的。
它没有展示系统运行时内存的行为,没有展示页面如何分配、回收或移动,没有揭示内存如何在子系统之间共享或为硬件锁定,也没有解释为何某些内存永远不能交换,或为何一个分配器与另一个并存。这些视图描述了内存的形态,却未说明内存的意义,以及内核如何有目的地使用它。
内核不将内存视为平坦空间来管理,而是将其视为责任。它根据每个子系统的工作方式,响应其需求。内存不是以通用块的形式分配的,而是根据手头任务赋予相应的形式、结构和规则。
这就是内核称它们为子系统的原因。每个子系统本身就是一个系统。调度器移动线程并管理上下文,网络栈缓冲数据包并处理流控制,文件系统管理元数据、缓存和日志记录,驱动程序分配硬件可见的缓冲区,甚至内存管理器也会跟踪自身——区域、使用情况和回收策略。每个子系统请求内存时,不仅关注大小,还关注意图——如何使用、生命周期长短以及必须遵循的约束。
内核会倾听,并通过专注、轻量级的接口回应。kmalloc返回快速、对齐的内存供内核内部使用,Slab缓存为可重用的结构化固定大小对象提供服务,vmalloc从分散的物理页面创建虚拟连续的缓冲区,DMA API确保硬件访问的物理安全性,mmap为用户进程提供灵活、受保护的内存视图,并通过陷阱延迟填充和保护。这些不仅仅是API,更是代码与系统行为之间的契约。
每个请求都流经相同的核心分配器,但带有不同的标志、约束和假设。调用可以阻塞吗?内存需要固定吗?它是可移动的还是可回收的?是短期还是长期存在的?内核跟踪此上下文并相应地分配——无声、高效、持续地进行。
从外部看,内存似乎很简单:一个指针、一个段、一个页面。但在内核内部,内存不是平坦的——它是分层的、有形状的,并由需求决定。每个子系统不仅仅需要内存,还需要一个适合其功能的工作空间。内核不仅仅是分配,更是理解。
当我们理解每个意图时,就会明白这些接口为何存在。我们不再被多样性淹没,而是专注于需要完成的任务,并相信内核已经知道如何去做。
这就是它维持系统运行的方式。

11 内存不是一个地方,而是一个系统
从系统启动的那一刻起,内核就通过结构化方式管理内存。固件表定义可用区域,内核将其注册为物理段,分类到区域(zone)中,并为每个页面映射元数据。vmemmap区域为不连续的物理内存提供页面描述符的线性视图。分配器基础设施在用户空间启动前完成初始化。
每个物理页面由一个struct page表示。这些描述符被所有内存子系统使用——包括匿名内存、文件映射内存、slab、vmalloc、页面缓存和回收系统。它们始终被引用,任何分配、映射或回收操作都离不开它们。
每个进程被分配一个由mm_struct跟踪的虚拟地址空间,其中的区域由定义了边界和标志的vm_area_struct描述。这些区域在发生缺页(page fault)时延迟填充。内核遍历页表,安装中间层级,检查权限,并根据需要分配物理内存。匿名缺页分配新页面,文件映射缺页实例化folio并填充页面缓存。相同的匿名页面可通过KSM合并,大页面可通过THP提升。
回收是异步且分代的。内核扫描LRU列表或评估MGLRU代际。在压力下触发直接回收,Cgroups隔离内存域,收缩器(shrinker)释放子系统特定的缓存。回收的页面被逐出、交换或回写,DAMON可观察访问模式以优化策略。
ZSWAP和ZRAM提供压缩交换功能,页面在到达磁盘前在RAM中压缩。通过延迟分配和回收减少内存压力,页面迁移和NUMA平衡根据访问局部性重新定位页面,迁移由缺页或后台扫描触发。内存热插拔(memory hotplug)在运行时更新区域边界,ZONE_DEVICE支持CPU无法直接寻址的内存。
内核通过专用内部接口分配内存:页面分配器返回大块内存,slab分配器服务小对象,vmalloc提供虚拟连续区域,vmap将物理页面列表映射到连续虚拟范围,get_user_pages为内核或设备访问固定用户内存,ioremap创建内核可访问的设备内存映射。
内核不被动观察内存使用,而是在分配时强制边界,通过元数据跟踪所有权,并通过定义的规则恢复可用性。缺页解决访问问题,保护机制引发陷阱,回收解决不平衡,交换、压缩和迁移响应系统状态。没有轮询,没有被动监控。
内核将内存作为所有权和重用的分层系统进行管理——从启动到关闭,跨越架构、配置和工作负载。
一切都经过它,没有它则无物运行。


12 内核始终存在——你知道它在哪里吗?
大多数开发者在用户空间中度过时间。内存被管理,崩溃被遏制,隔离得到保证。但在这一切之下是内核——已映射、存在且对软件与硬件之间的每一次交互都至关重要。
内核不会出现在进程列表中,无法被终止,不能像用户进程一样向它发送信号或跟踪它。然而,每次从文件读取、发送数据包或分配内存时,都在进入内核。内核始终存在,但隐藏在特权边界和硬件保护之后。
其内存空间是每个进程地址映射的一部分,但只有内核可以访问它。这种分离至关重要:如果用户空间可以自由写入内核空间,一个简单的错误就可能导致系统崩溃,或者更糟。用户空间和内核空间之间的界限不仅仅是技术上的区别,更是使安全、稳定计算成为可能的一道墙。
但内核的内存空间不是单个块,而是有意构造的。有用于代码、静态数据、动态分配、设备映射、模块加载、每个CPU变量等的区域。每个区域都有自己的规则:有些是只读的,有些是无缓存的,有些直接映射到物理RAM,有些为了灵活性或安全性而虚拟构建。
当一切正常时,不需要考虑这些。但当出现故障时——当在内核地址中遇到页面错误、驱动程序行为异常或系统无明显原因崩溃时——理解内存映射就变得至关重要。这能让你知道一个地址是在已加载的模块、slab缓存还是设备寄存器中。这是停止猜测并开始诊断的方法。
内核内存不仅仅是内核所在的地方,更是控制所在的地方。它管理设备访问、为系统管理内存、跟踪任务和线程并处理中断。该空间的每个角落都有意义。即使轻微误用,也可能触发未定义行为、不稳定或需要数天才能显现的微妙数据损坏。
不必每天记忆地址或深入研究页表转储,但需要在内心中建立内核如何布局内存、存在哪些区域以及如何使用这些区域的模型。这种理解会影响底层代码的编写方式、分析故障的方式,以及设计不仅功能正常而且可靠的系统的方式。
是的,内核始终存在。但直到了解它在哪里以及如何存在,才只看到了系统的一半。

13 不只是代码执行:内核实际执行的内容
Linux 内核不仅仅是执行代码,它还控制代码被允许做什么、何时做以及由谁来做。这种控制并非是建议性的或外部分层的,而是在权限与动作交汇的时刻直接执行。
当发出系统调用时,内核会根据目标资源的权限检查进程的凭据(其 UID、GID、补充组和能力)。这些检查决定是否允许访问。没有它们,用户之间以及用户空间和系统之间就没有界限。
但仅有权限是不够的。内核还定义了进程可以看到什么。通过使用命名空间(namespaces),它重新映射每个进程对 PID、挂载点和网络接口的视图。这种隔离使进程甚至无法知道其他资源的存在。没有它,权限检查就失去了意义。
即使进程能够看到某个资源,也不意味着它能够控制该资源。
特权执行确保影响系统状态的操作(如配置网络设备或加载模块)仅允许从正确的特权级别执行。用户模式和内核模式之间的转换在运行时会被严格验证。
为了避免“全有或全无”的 root 访问,Linux 应用了能力(capabilities)机制。内核并非通过 UID 0 授予完全控制权,而是按进程强制实施范围狭窄的权利(例如用于网络的 CAP_NET_ADMIN 或用于调试的 CAP_SYS_PTRACE)。在敏感操作期间会直接检查这些能力。
并非所有动作在所有上下文中都有效。系统调用内的代码可能会阻塞或分配内存,而中断内的代码则不能。内核持续跟踪此执行上下文,并执行安全操作。没有外部逻辑能够可靠地做出此决定。
策略执行增加了另一个维度。seccomp 过滤系统调用,SELinux 和 AppArmor 等 LSM(Linux 安全模块)应用强制访问规则,Cgroups 控制 CPU、内存和设备使用。这些控制直接集成到系统调用路径、调度器和资源记账中。
在虚拟化环境中,内核(或其支持的虚拟机管理程序)仲裁客户操作系统的访问。特权指令和硬件 I/O 会被捕获和模拟。没有这一点,客户机可能会危及主机。虚拟化执行保护的是整个系统,而不仅仅是进程。
这些机制并非独立运作。能力与 LSM 协同工作,命名空间限制可见性,而 Cgroups 限制使用。即使权限允许,上下文也会阻止不安全的动作;即使合法,策略规则也会拒绝危险行为。
它们共同阻止未授权访问、意外披露、特权升级、不稳定执行和跨虚拟机妥协。内核集成并执行这些机制,并非因为它优雅,而是因为它是唯一具有全面可见性和权威性的层。每个动作都流经它,控制必须存在于那里。

14 boot结束之处:内核开始之处
在 Linux 中,系统从硬件级设置过渡到与架构无关的内核核心,存在一个精确的时刻,这个时刻由位于 /init/main.c 中的 start_kernel() 函数定义。
执行甚至更早从引导加载程序(bootloader)开始。加电后,系统固件初始化处理器和内存控制器,然后加载 GRUB 或 U - Boot 等引导加载程序。引导加载程序将内核镜像和可选的 initrd 放入内存,准备引导参数,并跳转到内核特定于架构的入口点。
这会触发特定于架构的设置阶段,该阶段由汇编语言和早期阶段的 C 代码编写。在 x86 上,这包括 head.S 和 head64.c 等文件。CPU 进入 64 位长模式执行,构建临时页表,清除 .bss 段,并设置初始栈。系统在单个核心上运行,中断禁用,没有调度器,也没有动态内存,其唯一目的是为 C 代码准备一个安全的环境。
最终,控制权到达 C 级入口点(如 x86_64_start_kernel()),然后调用 start_kernel()。这标志着特定于平台的设置结束,与架构无关的内核接管。
start_kernel() 的早期部分在所谓的“早期 C”中运行:这是一个最小且受约束的上下文,内核后来依赖的许多核心子系统尚未可用。没有内存分配器,没有抢占,没有并发。代码不能让步或阻塞,完全依赖静态分配的内存。
为解决这些循环依赖,内核使用分阶段初始化。轻量级的早期实现(如早期 RCU、早期 printk 和 memblock 分配器)替代完整系统,使相关功能在早期启动期间能安全运行。然后,随着 start_kernel() 的推进,完整的基础设施按严格顺序上线:分配器、调度器、定时器、中断、每个 CPU 区域、RCU 和工作队列。内核变得能够进行异步、并发执行。
一旦稳定,它会调用 initcalls(初始化调用):这是一系列结构化的函数,用于启动更高层的子系统,如块 I/O、文件系统、设备驱动程序和网络。
最后,内核调用 rest_init()。在这里,它创建第一个内核线程:kthreadd,以及启动用户空间的线程(通常是 /sbin/init 或备用 shell)。
此时,内核完全激活。它调度任务,处理 I/O 和中断,管理内存,并在 CPU 之间协调子系统。
start_kernel() 不仅仅是一个函数,它是 Linux 自举的方式。它通过分层、静态设计和精心排序解决依赖关系,在裸机和运行系统之间架起桥梁。在此之后,内核不再对机器做出反应,而是按自己的规则运行。

15 从vmlinuz到eBPF:Linux内核内部实际运行的内容
Linux内核不仅仅是一个静态的二进制文件。它是一个动态的、可扩展的系统,不同类型的代码会在不同时间、出于不同目的进入并在内核空间中执行。
它始于vmlinuz,即引导时加载的核心内核镜像。其中包含了 核心组件:调度器、内存管理器、中断处理程序和系统调用调度逻辑。它始终处于映射状态,始终存在,但并不像进程那样被调度。相反,它是响应式的——当陷阱、系统调用或中断发生时进入,并尽快退出。它不等待或循环,只是做出响应。
编译到这个镜像中的内容(以及被排除在外或做成模块化的内容)由内核配置决定。通过.config,你可以决定哪些功能始终存在,哪些可以按需加载,哪些完全不包含在内。这从内核运行的第一刻起就塑造了它的占用空间、功能和行为。
接下来是内核模块(.ko文件),它们在运行时扩展内核。这些模块包括设备驱动程序、文件系统、网络栈等。它们不是静态镜像的一部分,而是根据需要加载。一旦插入,它们的运行方式就像内置的内核代码一样,可以完全访问内部API和内存。它们可以在不重启的情况下卸载或更新,使系统具有灵活性和模块化特性。
然后是eBPF,它采用了不同的方法。eBPF程序用受限的C或Rust编写,编译成安全的字节码,由内核验证,并在运行时注入。它们附着在特定的钩子上(系统调用、跟踪点、网络接口),仅在被触发时执行。它们不存在于内核镜像或模块中,而是在严格约束下在内核空间中运行。启用后,eBPF提供了一种安全高效的方式来观察和扩展内核行为,通过bpftrace等工具被广泛使用,无需修改代码或重启即可跟踪运行中的系统。
热补丁是另一种运行时代码形式,用于应用于已在运行的内核以修复错误或漏洞。热补丁会在内存中替换特定函数,重定向执行而无需重启。只要系统运行,它就保持活动状态。不过,除非重新应用或包含在较新的内核镜像中,否则它不会在重启后保留。
在这一切开始之前,还有initramfs。尽管它在用户空间中运行,但会在内核引导后立即执行,并在其控制下运行。它准备系统(加载模块、挂载文件系统),然后移交给真正的init。它不是内核代码,但定义了内核早期使用的内容。
在内核中运行的不只是编译进去的内容。它还包括驻留的、可加载的、可注入的或可替换的代码,所有这些代码要么源自内核源代码,要么遵循其规则。内核不仅仅是一个二进制文件,它是一个活的系统,由配置塑造,并通过设计进行扩展。

16 无状态CPU,有状态内核:执行如何被协调
在机器层面,CPU从根本上是无状态的。它一条接一条地执行指令,使用寄存器和内存的内容,而不感知任务、所有权或历史。在任何周期中,它只处理呈现给它的内容,不了解之前或之后的情况。CPU不跟踪正在执行哪个任务或它属于哪里;结构和连续性完全由内核维护。
这种设计是有意为之的。保持CPU无状态可保留速度、简洁性和通用性。CPU通过其格式化的指令、指令指针(IP)、栈指针(SP)和通用寄存器来暴露其功能。IP决定下一条指令从哪里获取;SP控制临时数据的推入和弹出。它们共同定义了代码和数据的流动。CPU本身不管理上下文或保留连续性;它精确地遵循这些流动,没有记忆。
在Linux中,内核是真正有状态的实体。它记录并管理每个执行路径的完整上下文,包括CPU状态、内存映射和调度信息。每个执行路径(无论是用户空间进程、进程内的线程还是内核线程)都由一个task_struct表示,其中包含精确暂停和稍后恢复执行所需的一切。
每个任务都有一个私有的内核栈。对于用户程序,这个栈与用户空间栈是分开的。当任务进入内核模式(通过系统调用、页面错误或中断)时,CPU切换到私有内核栈,允许内核安全地保存寄存器并管理临时数据。没有它,特权操作将不可靠。
上下文切换是内核用于在执行路径之间移动的机制。它将当前任务的完整CPU状态保存到其task_struct中,恢复另一个任务的状态,并让CPU从那里继续执行。CPU对此并不知晓,它只是从新状态继续执行。这使得许多独立任务可以共享单个核心,在同一时间只有一条指令运行的情况下创造出并行执行的假象。
在人类时间尺度上,上下文切换发生得如此之快,以至于多个程序似乎在同时进行。但在CPU层面,每个周期都严格专用于一个任务。内核对任务状态和调度决策的精心管理为本身无状态的CPU带来了结构。
CPU不记忆,它执行。内核记住一切。它推进每个执行路径,在无数次切换中保留身份和连续性,在幕后协调系统。
CPU带来执行的精确性;内核带来时间上的连续性——它们共同将简单的执行转化为协调的系统行为。

17 内核构建的内容——逐层构建
Linux内核是分层构建的,但并非作为随意的软件抽象。从上下文切换到内存隔离再到中断处理,每一层的存在都是为了直接解决CPU硬件的限制。内核的结构不是为了掩盖底层机器,而是为了完善仅靠硬件无法提供的功能。
Linux内核在CPU设计的约束下运行。其核心机制(任务切换、抢占、中断处理和内存保护)不是可选功能,而是对CPU所暴露和省略内容的必要响应。
现代CPU提供执行单元、寄存器、特权级别、指令指针以及用于中断和虚拟内存转换的硬件机制,但它们不跟踪任务、执行公平性、保留执行历史或管理并发。CPU只执行当前加载的内容,仅此而已。
上下文切换的存在是因为CPU不会在任务之间保留状态。切换时,内核会将完整的寄存器集(包括栈指针、指令指针和标志)显式保存到即将离开的任务的task_struct中,然后恢复下一个任务的状态。CPU只执行指令,不知道切换已经发生。
每个任务的内核栈是必需的,因为CPU在从用户模式切换到内核模式时不会分配或隔离栈内存。内核为每个任务分配一个私有的内核栈,并确保所有特权操作都在那里进行,这保证了一致性和内存安全性。
抢占和调度完全在内核中实现。CPU不衡量时间片或对执行进行优先级排序。内核注入定时器中断,评估调度决策,并使用重新调度标志来控制任务何时让步或继续。公平性和策略是内核级别的构造。
中断处理反映了更多的约束。CPU引发中断时不考虑当前任务状态。内核在硬中断(hardirq)上下文中处理这一点,在该上下文中不允许睡眠或阻塞。如果需要进一步的工作,它会将执行推迟到软中断(softirq)、任务小项(tasklet)或工作队列(workqueue)上下文,这些上下文在更宽松的环境中运行。
内存保护使用CPU的MMU,但内核设置页表并在上下文更改时切换它们。CPU执行访问权限,但内核定义内存映射、分配空间并执行所有权边界。
每个内核层都对应着CPU不做的事情:它不跟踪上下文、隔离栈、管理时间或安全处理嵌套执行。内核相应地构建每个机制。
Linux内核的架构不是从CPU抽象出来的,而是由它定义的。内核一层一层地填补硬件留下的空白。

18 内核执行路径:在哪里运行,以及为何重要
Linux内核代码在不同的上下文中运行,每个上下文都有其独特的规则和约束。这些路径定义了内核在给定时刻可以执行的操作(例如是否可以睡眠、阻塞、抢占或访问用户内存),并支配着系统对用户调用、硬件事件和内部活动的响应方式。
当用户进程发出系统调用时,CPU切换到内核模式,但仍处于该进程的上下文中。内核使用该进程的task_struct执行,此时具有更高的特权。在这种模式下,进程可以睡眠、阻塞、分配内存并处理页面错误。大多数同步系统服务都在该上下文中运行。
内核线程也在进程上下文中执行,但不与任何用户空间任务绑定。它们由内核创建,用于内存回收、I/O调度或线程创建等后台任务,生命周期较长,像普通任务一样被调度。它们可以完全访问内核服务,并可根据需要睡眠或阻塞。
当硬件触发中断时,会进入中断上下文。相应的处理程序会立即异步执行,且不在任何进程上下文中。它不能睡眠或阻塞,只能执行有限栈空间下的原子操作。此路径针对低延迟进行了优化,设计为快速退出。任何大量工作必须被推迟。
内核提供了几种延迟执行路径来处理此类工作。软中断(SoftIRQs)是由子系统或中断处理程序调用的静态注册处理程序。它们在原子上下文中运行,不能睡眠。如果未立即处理,它们会由每个CPU的ksoftirqd线程在进程上下文中执行,但仍受软中断约束。
任务小项(Tasklets)构建在软中断之上,提供了更简单的序列化API。它们不可抢占,并绑定到特定CPU,确保不会在同一核心上并发运行。与软中断一样,它们不能阻塞或睡眠。
当延迟工作需要睡眠能力时,内核使用工作队列(Workqueues)。这些工作由kworker线程在完整的进程上下文中执行,因此适用于起源于中断上下文但需要阻塞操作灵活性的任务。工作队列在驱动程序实现中被广泛使用。
定时器(Timers)允许内核代码在延迟后调度执行。当定时器到期时,其回调函数在软中断上下文中调用,必须简短且非阻塞。
RCU回调将内存回收推迟到所有读取者完成之后。宽限期过后,根据系统配置,回调函数要么在软中断上下文中运行,要么通过专用线程运行。
每条路径的存在都有其原因。内核强制实施这些边界以维持正确性、隔离性和响应性。选择正确的上下文并非可有可无,这对编写安全且功能正常的内核代码至关重要。

19 追踪执行的模板
理解内核并非始于分类,而是始于流程。每次进入内核都会启动一条既定的执行路径——由上下文塑造,通过接口路由,并受到底层分层职责的约束。
执行始于一个触发条件:系统调用、硬件中断、处理器异常或已调度任务。每种触发条件都有其自身约束。在进程上下文中,允许睡眠和阻塞;在中断上下文中,严格禁止睡眠和阻塞;软中断(SoftIRQ)和任务小项(Tasklet)在原子上下文中运行,灵活性有限;工作队列(Workqueue)和内核线程在线程上下文中执行,但无法访问用户内存。
一旦执行跨越入口边界,控制权便传递给子系统。这并非通过直接调用实现,而是通过已注册的接口:系统调用表、文件操作向量、协议钩子和驱动程序回调。间接性并非偶然——它定义了谁拥有控制权,以及当前适用的规则。
每个子系统对其数据和逻辑拥有独占所有权。内存管理通过mm_struct、vm_area_struct和struct page跟踪映射和分配;VFS(虚拟文件系统)抽象文件操作,而具体文件系统管理inode、dentry和file;网络通过sk_buff、套接字状态和按命名空间的路由表处理流量。这些结构是特定领域的,对它们的控制意味着责任。
执行可能跨层移动——文件系统可能分配内存,网络处理程序可能将工作入队,驱动程序可能启动DMA(直接内存访问)——但转换绝非临时决定。所有移动都经过中介:共享状态在锁保护下访问,通过引用跟踪,并由上下文决定条件。通过命名空间、控制组(cgroup)、能力(capability)和安全模块维持隔离。虚拟化和容器依赖这些相同机制,而非例外。
每条路径都遵循其开始时的相同规则完成。系统调用返回用户空间,中断处理程序退出到被抢占的任务,内核线程让步,延迟工作要么重新调度要么结束。清理遵循所有权原则:释放内存、减少引用、释放锁。无物脱离结构。
追踪任何执行时:识别入口、确定上下文、定位调度点、观察所属子系统、跟踪访问的数据,并理解路径如何结束。这同样适用于read系统调用、页面错误、数据包接收或定时器到期。执行不仅是运行的内容,还包括在哪里、何时以及在谁的控制下运行。
这不是作为代码的内核,而是作为流程的内核——在每次转换时都是结构化、受规范且明确的。呈现的不是描述,而是一个模板:一种可复用的方式,用于在任何条件下追踪跨任何子系统的任何执行路径。即使组件不同,问题始终相同。这种一致性使内核可追踪,且此模型可靠。


20 中断不是干扰,而是设计
“中断”一词意味着打断、干扰,是意外之事。但在内核以及其底层架构中,中断并非如此。它不是混乱,不是冲突,而是系统主张行动权的方式,无论当前运行的是什么。它是结构化的、可预期的,受设计约束。
在内核处理中断之前,它早已做出决策:中断将去往何处、如何处理,以及谁不负责处理它。
每条中断线都注册有处理程序。内核设置向量表、初始化本地和I/O APIC、分配优先级、屏蔽或取消屏蔽中断线,并将每个源路由到逻辑CPU。这些不是反应,而是声明。系统预先构建了时间、设备和其他CPU可能进行干预的确切路径。
当中断发生时(无论是定时器滴答、网络数据包还是来自另一个核心的关闭请求),CPU切换到内核模式。它保存当前执行状态并开始执行处理程序。但此处理程序不属于它所中断的任务。该任务可能在用户空间,可能在系统调用中途,可能处于空闲状态,这都无关紧要。中断跨越该边界,却不成为其一部分。
内核精确处理这种区别。处理程序在中断上下文中运行,使用当前任务的内核栈,但从不声明任务的身份。它不修改任务状态,不改变其调度状态,不留下任何痕迹。
这就是中断不能睡眠的原因。不仅因为它必须快速,还因为它不能成为任务的一部分。睡眠意味着此执行可以被暂停并在调度规则下恢复,就好像它是一个线程。但它不是,它根本不是线程的一部分,而是系统从外部进行的干预。
当需要更多工作时,内核会移交任务。它委托给软中断或将函数入队到工作队列。这些路径可以安全调度、阻塞和拥有线程。中断路径则不然,它的定义是无上下文、无所有权、无延续。
当处理程序完成时,内核决定接下来发生什么。如果需要重新调度任务,它会切换;如果不需要,被中断的任务会恢复。无论哪种方式,栈都是完整的,边界得到尊重。被中断的内容和响应的内容之间没有泄漏。
这就是内核获得控制权的方式。不仅是它可以被中断,还在于它可以响应而不被纠缠。中断不属于它所抢占的逻辑,它的存在是为了确保系统对不能等待用户代码注意到的事件保持响应。
因此,“中断”这个名字可能听起来像是一种干扰,但设计讲述了不同的故事。
它不是流程的干扰,
而是流程之外的路径——结构化、精确且有边界。

21 执行是逻辑的,位置是物理的
当进程在Linux中运行时,它似乎会精确地从上次停止的位置继续。寄存器被恢复,栈有效,内存布局符合预期。中断处理程序在已知的CPU上下文中执行,系统调用完成时不会中断。从运行代码的角度来看,环境是一致的。
在这种稳定性之下,系统始终在变化。为了平衡负载、改善内存局部性或响应硬件事件,内核会在CPU和内存节点之间移动实体。任务、内存页面和中断处理程序的这种移动称为迁移。这是系统操作的一个持续部分,并且发生时不会中断执行逻辑。
使其成为可能的是内核将执行与位置严格分离。执行状态通过上下文切换得以保留。任务可能在不同的CPU上运行,但在恢复之前,其程序计数器、栈和虚拟内存会被完全恢复。进程不会察觉到这种变化。
内存迁移遵循相同的原则。页面可以在NUMA节点之间移动或在压缩期间重新排列。内核更新页表、使过时的TLB条目失效,并维持一致的虚拟地址视图。只要映射保持不变,底层页面可以自由移动。
中断也会迁移。设备IRQ会在CPU之间重新路由以分配负载。然而,处理程序仍然在正确的上下文中运行,使用有效的每个CPU结构。传递路径发生变化,但处理程序的执行环境不变。
关联性对执行可能发生的位置施加了约束,但不会将其固定在适当的位置。任务的CPU关联性定义了其符合条件的位置,调度器会尊重该边界。在这些限制内,可以自由进行迁移。这同样适用于IRQ关联性、内存策略和每个CPU的基础设施。
这之所以有效,是因为每个子系统都是协调的且具有状态意识。迁移线程在CPU之间安全地执行任务移动。内存子系统跟踪反向映射并更新引用。调度器管理运行队列和抢占以保持执行的一致性。每个部分都确保自身的正确性,因此即使系统重新定位,逻辑模型也能保持稳定。
这种分离使系统能够扩展和适应而不会失去完整性。代码继续运行,不知道其下方正在进行的物理重组。内核移动它必须移动的东西,并保留它不能保留的东西。
执行是逻辑的,迁移是物理的。它们之间的边界不是概念上的,而是由允许进程、页面或处理程序移动而不破坏运行内容的机制来维持的。

22 不仅仅是一段代码:每个内核路径内部的过程
内核代码中的函数不仅仅是返回结果,它必须在严格规则下运行——受系统上下文、安全策略、共享结构和并发控制的约束。它不仅要执行逻辑,还要安全、一致且与系统的其余部分协调地执行。这是程序性的,而不仅仅是功能性的。
每条内核路径都涉及一系列维度:意图、上下文、执行、对象、状态和同步。这些不是孤立的阶段或层,而是系统行为的相互依存的方面,通常分布在多个函数中。
意图是指函数预期要做的事情,通常反映在其签名中——名称、参数和返回类型。参数定义调用者提供什么以及数据如何进入系统。在内核代码中,它们必须高效且安全地传递。有些是复制的,有些是通过引用传递的。用户指针需要验证,大型结构作为指针传递以减少开销。每种形式都反映了意图、成本和正确性。
上下文指的是函数执行的位置。进程上下文允许阻塞、页面错误和带可睡眠标志的分配,中断上下文则不允许。在中断上下文中运行的代码绝不能睡眠,并且必须快速完成。函数必须明确考虑这一点,违反上下文规则可能导致死锁、崩溃或未定义行为。
执行验证操作是否被允许。权限、能力、命名空间和安全模块都参与其中。这些检查嵌入在执行路径中,而不是在其外部。执行是核心操作——读取、写入、映射、调度等,它仅在验证后开始。
对象指的是涉及的共享结构:task_struct、file、inode、mm_struct、socket。这些结构可能被并发修改,安全访问需要引用计数、锁或RCU。状态包括持久更改:文件位置、计数器、时间戳、I/O统计信息,这些在并发情况下和中断后必须保持一致。
同步确保正确性。锁、屏障和原子操作防止竞争条件,没有同步,对对象或状态的更新都不安全。
vfs_write反映了这些维度。它的意图是用户写入请求,其上下文是具有出错权限的进程模式,执行检查模式和安全钩子,执行委托给文件系统,对象包括file和inode,状态在位置和记账中更新,同步确保正确的顺序和互斥。
没有单个函数能完整表达所有维度,但每条内核路径都经过它们。一些函数验证,另一些更新,有些只起保护作用。了解这些维度至关重要,它们定义了内核如何在并发、共享的特权系统中保持控制。

23 内核如何自我通信——内部通信工具
在 Linux 内核内部,代码在不同的上下文中运行:用户发起的系统调用、硬件中断、延迟处理程序和内部内核线程。每个上下文都在特定的约束下运行——有些可以阻塞或睡眠,另一些则必须快速执行且不被中断。尽管存在这些差异,内核仍通过一组专为安全高效通信设计的内部工具,实现了跨上下文的数据交换和协调。
共享内存结构提供了基础。缓冲区、队列和状态字段等对象可在多个执行路径中被并发访问。根据上下文的阻塞能力,使用自旋锁、原子操作或 RCU 等无锁技术来维持同步。这些机制在不影响并发性或响应性的前提下,确保了一致性。
为支持阻塞操作,内核使用等待队列(wait queues)。无法立即进行的系统调用可能会阻塞并在等待队列上睡眠。另一个上下文(通常是中断处理程序或内核线程)可在条件变化后唤醒该进程。这将请求与其解决解耦,而不会浪费 CPU 周期。
中断处理程序面临最严格的限制:它们不能阻塞、分配内存或访问用户空间。其作用是确认硬件并标记延迟工作。如果需要额外处理,内核会调度软中断(softirq)或任务小项(tasklet),它们会在中断后不久运行,但仍处于非阻塞约束范围内。
对于需要更多时间或灵活性的操作,内核使用工作队列(workqueues)。这些工具将任务委托给内核管理的线程,使其能够独立运行并完全访问内核服务。工作队列广泛用于延迟 I/O、内存回收和异步设备处理。
为实现快速无锁信号传递,内核使用原子标志和计数器。更新单个位或计数器可向另一个上下文通知就绪、进展或完成状态。这些在网络和存储等性能关键路径中尤为常见。
内核线程是由内核创建的持久性、调度器管理的任务。与中断或软中断不同,它们可以阻塞、睡眠并使用任何内核 API。它们处理后台工作、延迟清理和必须独立于用户活动运行的周期性任务。
回调(Callbacks)完善了协调模型。子系统注册函数指针,以便在满足特定条件时调用。这些处理程序在遇到条件的上下文中执行,实现了子系统间响应迅速、解耦的行为。
这些工具(共享内存、等待队列、工作队列、原子变量、内核线程和回调)共同构成了 Linux 内核的通信架构。每个工具都针对其执行上下文量身定制,实现了整个系统精确、可靠的协调。

24 内核模块仅通过导出符号相互认知
内核模块是独立编译的内核功能单元,设计为可在运行时加载。它们提供了一种灵活的方式来扩展内核(通常用于设备驱动程序、文件系统、密码学例程或协议实现),而无需完全重建或重启。一旦插入,模块就成为运行内核的一部分,在特权空间中运行,可完全访问内核环境。
尽管具有这种级别的访问权限,但内核模块在设计上是隔离的。除非内核函数或变量已被显式导出,否则模块不能引用或调用它们。内核不提供发现、延迟绑定或符号查找功能。所有交互必须通过通过EXPORT_SYMBOL或EXPORT_SYMBOL_GPL声明的预定义接口进行。
这种边界是有意设定的。内核不保证稳定的内部ABI,并且除了导出的接口外不提供任何支持。任何未显式导出的符号都被视为内部符号,可能在版本之间更改或删除。导出接口是模块与内核其余部分之间唯一受支持的边界。
加载模块时,其未定义的符号会与仅包含内核选择公开的标识符的全局符号表进行匹配。如果缺少必需的符号,模块将无法加载。解析仅在加载时进行一次,不能动态调整。
模块不能访问彼此的内部符号,除非这些符号被显式导出。模块是一起开发、在相同配置中编译,还是放在相关的源目录中,都没有区别。没有EXPORT_SYMBOL,符号就是不可见的。从模块的角度来看,未导出的内容就不存在。
模块分层和堆叠很常见,但必须严格显式。一个模块可能依赖另一个模块来注册回调、提供处理程序表或公开实用函数,但只能通过导出符号和已建立的注册点实现。运行时不会发现或连接任何内容。
这不是技术限制,而是有意的架构决策。内核强制实施这种分离,以保持内部灵活性、版本独立性和系统完整性。像task_struct、cred和mm_struct等关键结构是内核行为的核心,除非通过安全接口有意公开,否则永远不能直接访问。
每个模块都被视为外部代码,无论它与内核功能的集成多么紧密。它必须声明许可证,预先解决依赖关系,并将其访问限制为内核明确选择公开的接口。
内核模块仅通过导出符号相互认知,没有例外。这不是惯例,而是强制的设计:精确、有意,且对Linux内核的长期稳定性至关重要。

25 搭建组件之间的桥梁
现代系统由独立运行的部件构成。内存以页为单位组织,磁盘存储块,网卡传输数据包,CPU在寄存器中逐次执行指令。每个组件都按自身规则运行,对其他组件毫无所知。
用户空间看不到这些细节。进程打开文件、发送数据、分配内存并执行逻辑,却无需了解这些请求是如何实现的。这种假象之所以成立,是因为内核能看到底层的每个组件,并懂得如何在它们之间进行转换。
内核并非通过扁平化来统一系统,而是维持各组件的分离,通过精心控制的结构将一个域映射到另一个域。它跟踪边界并解决可能导致系统无法使用的不匹配问题。
文件描述符不是文件,而是对struct file的引用,该结构指向inode,inode又映射到块范围,通过块层解析为磁盘I/O。当请求读取时,内核沿此路径确定数据的物理位置,将I/O操作入队,并将结果复制到用户内存(若该内存已映射、可写且符合策略允许)。
用户空间中的指针实为虚拟地址。内核通过进程的页表将其映射,页表指向物理内存。这些页面可能是匿名的、基于文件的,或当前已换出。每个页面都在内核空间中被跟踪,其元数据描述了引用计数、状态、标志和访问约束。这不仅是地址转换,更是一组保证,可在负载下维持隔离性、安全性和公平性。
网络数据包以DMA缓冲区的形式到达NIC,内核将其提取到sk_buff结构中,解析协议头,对流量分类,并将它们入队以传递给匹配的套接字。该套接字属于某个进程,进程看到的是流。但该流之所以存在,是因为内核强制实现了一致性,处理了重新排序、超时、确认和流量控制。每次发送或接收调用都依赖这种底层结构。
每个子系统都不了解其他子系统。内存子系统知道页,却不知道文件;文件系统知道inode和块,却不知道进程;网络栈理解数据包和协议,却不知道程序或流。它们在架构上是独立的。只有内核能看到所有子系统,并维持映射,使它们以一致、可控和可组合的方式交互。
内核不消除限制,而是理解限制。它尊重每个组件的自然边界,并在它们之间搭建桥梁——虚拟与物理、逻辑与程序、共享与隔离的桥梁。用户空间依赖的每一个抽象(每一个系统调用、每一个文件、每一个套接字、每一个映射页)之所以存在,只是因为内核在不兼容的域之间进行中介,并将系统维系在一起。

26 libc之外:用户空间与内核的真实通信方式
当大多数人思考用户空间如何与Linux内核通信时,他们会想到libc。这是有道理的——libc提供了open、read、write和malloc等常见函数,它带来了便利性、可移植性和一致的API。但libc只是一个层,并非唯一的接口。
在其之下是一个更广泛且经过精心设计的系统。libc封装了系统调用,但这些系统调用可以直接调用,无需借助库。更重要的是,并非所有的内核交互都完全依赖系统调用。
Linux内核通过多种接口展示自身。像/proc和/sys这样的虚拟文件系统提供了对内部状态和配置的结构化访问;ioctl支持特定于设备的控制路径,这些路径不适合标准的读/写模型;mmap允许用户空间和内核空间之间直接内存映射,以实现高效I/O;ptrace等工具为调试提供了底层进程控制;Netlink套接字在用户空间和内核子系统之间实现了结构化的异步通信;而eBPF则引入了一个可编程运行时,可在预定义的钩子处安全地将逻辑注入内核。
这些接口的存在是有原因的。内核并非强制使用单一路径,而是支持一系列交互方式,每种方式都适合不同的目的。脚本可能从/proc读取数据,对性能要求严格的服务可能依赖mmap或io_uring,跟踪工具可能附加eBPF程序来实时观察内核行为。
在内部,这些路径汇聚到共享逻辑。无论是处理系统调用、文件读取还是Netlink消息,内核都使用通用的调度表、内部抽象和子系统来处理请求。表面可能不同,但基础始终是统一的。
这种灵活性并非偶然,它反映了塑造内核开发的原则:不破坏用户空间、保持兼容性、为可扩展性而设计。即使内部发生演变,接口也保持稳定;机制优于固定策略;模块化使子系统能够独立发展,同时与整体保持一致。
这正是Linux同时具备稳定性和适应性的原因。旧工具继续有效,新工具获得发展空间。多种接口并存并非碎片化,而是有意的设计。libc仍然是最常见的内核入口路径,但它只是众多路径之一,所有路径都被设计得安全、有目的性且精确。
这不是偶然,而是设计。

27 CPU不移动数据——但没有CPU,什么都无法移动
每次I/O操作都是CPU、内核和硬件之间的协同工作。CPU不直接传输数据,它发起数据交换、准备内存并等待完成。
这始于内存映射I/O(MMIO),设备的控制寄存器会作为物理地址暴露出来。当CPU向这些地址写入时,就是在发出命令:配置、启动、停止或请求状态。这些命令会被设备的控制器接收,控制器负责将高层指令转换为底层硬件操作。无论是USB主机控制器、SATA主机总线适配器(HBA)还是NVMe引擎,控制器都会解释这些写入操作,并在设备端管理相关操作。
MMIO事务通过系统总线(通常是PCI Express)传输。总线就像连接CPU、内存和设备的硬件通道,负责在端点之间路由命令和传输数据。
发出这些命令、准备内存和设置传输的逻辑由设备驱动程序处理,驱动程序是内核的一个组件,充当操作系统和设备之间的软件接口。驱动程序会为控制器设置DMA地址,并在内核中注册中断处理程序。
一旦命令发出,控制器就会接管。内核已经代表设备分配并映射了内存缓冲区。CPU告诉控制器这些缓冲区的位置,然后退到一旁。
此时,直接内存访问(DMA)成为主要角色。控制器使用其DMA引擎在设备本地内存和系统内存之间直接传输数据,完全绕过CPU。这使得高吞吐量设备能够高效运行,而不会因每个字节的传输而占用CPU周期。
传输完成后,控制器不会将数据推回CPU,而是引发中断——通过中断控制器路由的硬件信号,促使CPU暂停、切换上下文并调用内核级处理程序。处理程序检查状态,将缓冲区标记为已完成,并可能在中断上下文之外调度后续处理。然后,CPU恢复之前的任务。
每个部分都扮演着不同的角色:CPU发起操作,控制器执行操作,总线负责连接,DMA移动数据,中断提供通知,驱动程序进行协调,而内核将所有部分结合在一起——确保安全性、管理内存并维持协调。
从read()这样的系统调用到网络数据包的到达,这种流程始终在表面之下持续发生。
CPU不搬运数据,但没有它系统就无法工作。

28 时间与精度:内核眼中的CPU执行
运行在2.4GHz频率下的现代英特尔x86-64 CPU,每秒可完成24亿个时钟周期。在这样的速度下,单个操作发生得太快,难以直观理解其意义。为了更好地理解CPU的内部活动,我们可以对时间进行缩放:将一个CPU周期视为人类的一天。
按此比例,1个CPU秒将代表24亿天,约合657.5万年。2.4GHz下的1个CPU秒 ≈ 657.5万年
这种时间缩放有助于阐明每个CPU周期的独特性。即使是最小的操作(如一次简单的加法、寄存器移动或内存查找),都是纳秒级的独立且经过深思熟虑的步骤。尽管每秒会发生数十亿次操作,但每个周期在纳秒级别上都是有意为之、结构化且独立的。
在此比例下,即使是单个系统调用或中断也会成为重大事件。系统调用涉及用户空间与内核之间的协同转换。保存上下文、切换页表、执行系统调用逻辑以及恢复执行状态,通常需要100到200个周期。中断也遵循严格的流程。当硬件中断发生时,CPU会保存关键寄存器,并通过快速硬中断处理程序将控制权转移给内核,通常会将进一步的工作推迟到软中断处理。即使在高负载下,这些转换也能可靠地发生。
系统通过快速上下文切换处理数千个任务。通过仅用数百个周期保存和恢复执行上下文,CPU和内核维持了许多操作同时进行的假象。每个上下文切换、进程调度和中断处理操作,都在CPU内部时间尺度的精度内精心编排。
内核管理CPU执行和系统活动,但仅通过CPU驱动的指令与设备通信。所有内核操作最终都是为用户空间服务,确保内核之外的进程能够可靠执行。
随着操作从CPU寄存器延伸到缓存再到主内存,延迟会增加。然而,内核和CPU在所有层级上都保持精确同步。每条指令、内存访问、上下文切换和中断都符合严格的结构,确保即使在纳秒级分辨率下,执行也能保持可预测、连贯和可靠。
内核设计不仅关乎功能,还关乎与处理器内部时间尺度的协调。在这个尺度下,每个周期都代表着有意义的动作,每个操作都为每秒数十亿次执行中的系统稳定性和意图做出贡献。

29 内核在虚拟化中的角色:理解KVM
基于内核的虚拟机(KVM,Kernel-based Virtual Machine)使Linux内核能够向用户空间应用程序提供硬件辅助的虚拟化功能。它本身并不是一个完整的虚拟机管理程序,而是一个暴露现代CPU内置虚拟化功能的内核模块。当与QEMU等用户空间虚拟机监视器结合使用时,KVM便构成了一个完整、高效且模块化的虚拟化平台。
QEMU在用户空间运行,负责定义虚拟硬件、分配内存和模拟设备。要启动虚拟机,QEMU会通过/dev/kvm接口与内核通信,请求创建虚拟机及其虚拟CPU(vCPU)。每个vCPU都由一个内核线程支持,并像任何其他任务一样由Linux内核调度。
在支持如Intel VT-x或AMD-V等虚拟化扩展的处理器上,客户机代码在不同于主机的特殊CPU模式下执行。这种模式区别对于客户机而言称为非root模式,对于主机而言称为root模式。这些模式是处理器虚拟化功能集的一部分,与传统的特权级别(如0环或3环)完全独立。例如,客户机内核在0环中运行,但处于非root模式,而主机内核则在0环(ring 0)的root模式下运行。
当虚拟CPU执行客户机代码时,它在非root模式下运行,允许大多数指令直接在硬件上执行。这实现了高性能且开销极小。然而,某些操作(如访问I/O端口、修改控制寄存器或执行特权系统指令)在此模式下是不允许的。当CPU遇到此类指令或预定义条件时,会执行VM退出(VMEXIT),将控制权从非root模式转换为root模式,把执行交还给内核。
KVM会检查退出原因并相应地处理。如果退出涉及CPU内部状态或内存管理,可能在内核中解决;对于与I/O或设备相关的操作,该事件会转发给QEMU,由其执行所需的模拟。然后,QEMU使用KVM接口更新客户机的虚拟CPU状态,KVM从中断点恢复客户机的执行。
为了隔离客户机内存,KVM使用如扩展页表(EPT,Extended Page Tables)等硬件辅助技术,将客户机物理地址转换为主机物理地址。这些映射由内核管理,并在发生更改时同步,确保安全且一致的内存访问。
当虚拟机关闭时,KVM会释放所有相关资源并重置CPU的虚拟化状态。在整个生命周期中,KVM管理执行转换、隔离边界和底层CPU控制,而QEMU处理更高级别的编排。它们共同提供了一个高性能、安全且深度集成到Linux内核中的虚拟化解决方案。

30 两个世界,一个CPU:虚拟化中的root操作和非root操作
现代英特尔处理器的虚拟化依赖于通过虚拟机扩展(VMX,Virtual Machine Extensions)建立的执行环境的严格划分。这种分离定义了两个操作世界:VMX root模式和VMX非root模式,从而在CPU的严格控制下实现安全、高效的虚拟化。
当KVM模块设置了CR4.VMXE并执行VMXON时,VMX操作开始,将CPU转换为root模式并启用VMX指令。在客户机运行之前,KVM为每个虚拟CPU(vCPU)分配并配置一个虚拟机控制结构(VMCS,Virtual Machine Control Structure),其中包含客户机和主机处理器状态以及执行和控制字段。
客户机执行以VMLAUNCH开始或以VMRESUME恢复。CPU从VMCS加载客户机状态并进入VMX非root模式。在这种模式下,客户机操作系统直接在硬件上运行,同时与主机隔离,即使在 0 环(ring 0)执行也是如此。
大多数客户机指令在执行时无需虚拟机管理程序干预,除非被VMCS控制所禁止。特权指令、I/O端口访问、控制寄存器修改或外部中断会触发VM退出。在VM退出期间,CPU将客户机状态保存到 VMCS 中,恢复主机状态,记录退出原因,并将控制权交还给在root模式下运行的KVM。
KVM读取退出原因并处理该事件。CPU状态更改、特权操作或中断处理直接在内核中处理,而设备访问或用户驱动的事件则转发给QEMU等用户空间监视器。如果vCPU线程被抢占,KVM会保存客户机上下文并让步给Linux调度器。
在vCPU之间切换时,KVM使用VMPTRLD加载新vCPU的专用VMCS。每个vCPU维护自己的VMCS,切换涉及更新活动VMCS指针以确保客户机之间的隔离。Linux调度器将vCPU视为普通线程,允许在vCPU之间或客户机与主机进程之间进行公平调度。
重新调度后,当再次选择vCPU时,KVM会根据需要更新VMCS,并使用VMRESUME恢复客户机执行。
VMCS在转换过程中维护处理器状态,包括通用寄存器、控制寄存器、指令指针、标志和执行控制。对VMCS字段的精心管理可最大限度地减少开销、保持隔离并确保客户机行为正确。
客户机和主机之间的内存一致性通过INVEPT和INVVPID等指令维护,从而能够选择性地使地址映射和TLB条目失效,而无需完全刷新处理器。
当客户机终止时,KVM发出VMXOFF,结束VMX操作并将CPU恢复为正常的主机执行。
通过root模式和非root模式之间的结构化划分、每个vCPU的专用VMCS结构以及KVM转换的协调,现代处理器提供了安全高效的硬件虚拟化。

31 内核与VirtIO:无需模拟的网络驱动程序
当虚拟机发送或接收数据包时,Linux内核不会模拟物理网卡,而是直接使用虚拟机监视器(VMM)暴露的虚拟设备,并通过VirtIO驱动程序提供的半虚拟化接口工作。就网络而言,该驱动程序是virtio_net,它完全在内核空间中运行,无需设备模拟即可处理数据包流。
VirtIO定义了客户机与主机之间的共享内存传输机制,它用基于内存映射环和事件信号的精简协议取代了模拟硬件的开销。客户机看到的是标准网络接口,主机则直接移动数据,双方通过各自的内核进行协调。
在客户机内部,virtio_net注册一个虚拟以太网接口。应用程序进行诸如send()和recv()之类的套接字调用,而意识不到不涉及任何物理网卡。在底层,驱动程序分配数据包缓冲区并将它们组织成称为virtqueue的环形结构,每个结构包含一个描述符表、一个用于出站缓冲区的可用环和一个用于已完成缓冲区的已用环。
这项工作完全由客户机内核管理,它用描述符填充可用环,处理后从已用环回收描述符,并处理信号(在数据包准备好时通知主机,在数据到达时响应中断)。
主机内核也扮演着同样积极的角色。当虚拟机在QEMU等虚拟机监视器下启动时,VMM会配置VirtIO网络设备,并通过/dev/vhost-net向主机注册客户机的内存布局和virtqueue地址。从那里开始,vhost_net模块接管,它作为内核线程运行,完全绕过用户空间以实现高性能网络。
对于出站流量,vhost_net直接从客户机的TX virtqueue读取数据,并将数据包转发到TAP设备(主机上的虚拟第2层接口)。对于入站流量,TAP接口接收以太网帧,vhost_net将它们写入客户机的RX缓冲区,更新环状态,并引发eventfd。KVM的irqfd机制将其转换为传递给客户机的虚拟中断。
TAP设备连接到Linux网桥,该网桥充当虚拟交换机,在虚拟机和物理网卡之间路由流量。主机内核管理此流程,确保所有组件之间可靠高效地传递数据包。
这一切都不涉及硬件模拟,数据包永远不会通过模拟设备。相反,两个内核都在共享协议中执行各自的角色。VirtIO不模仿硬件,而是实现直接协作。
这不是模拟,而是真实的内核到内核的协调。

32 一切仍由操作系统掌控
现代系统涵盖诸多层面:语言、运行时、解释器、容器、模型、协议。但每一层最终都会将控制权传递给同一个核心层。
内核仍在管理着这条路径。
执行始于结构化逻辑:创建进程、映射内存、调度指令、将输入输出作为流打开、连接套接字、驱动程序移动数据、处理中断、激活硬件。每次转换都由内核验证、隔离和调节。
即便如今——当代码在托管运行时中运行、由事件触发、在容器内或跨虚拟机执行时——控制流依然似曾相识。每次内存访问、I/O操作和任务切换都通过内核暴露的接口进行。边界依然存在,契约依然有效。
这背后的结构并非新事物,它由冯·诺依曼架构定义:共享内存中的代码和数据,指令按顺序提取和执行,状态一次更新一个操作。这仍是通用机器的模型,内核完全在其中运行,协调所有更高层。
但并非所有变化都是渐进的,有些变化可能是结构性的:
- 当系统不再基于冯·诺依曼模型运行时(即没有取指-解码-执行循环、没有共享指令/数据内存、没有程序计数器),结构就会改变。
- 当内存不再按字节寻址时(即访问变为基于图、内容寻址或与逻辑物理共存),接口就会中断。
- 当执行不再通过系统调用,且用户空间与内核空间的边界消失时,控制模型就不再适用。
- 当不再需要内核来抽象设备、隔离进程或调度计算时,它就不再是系统的核心。
- 当逻辑不再是编写、编译或符号解释的,而是通过学习、涌现或无需离散指令的模拟来实现时,软件本身就已发生转变。
在那之前,结构依然稳固。
系统仍一次运行一条指令,仍将代码存储为数据,仍会陷入内核,仍在等待权限,仍只在被询问时回应。
而当它必须真正运行时,内核仍会响应。

33 对齐即理解
本节的标题的含义是说,Linux的文档、代码实现和运行时行为都是保持一致的,即三者“对齐”,对齐了就容易通过任意一部分去了解其他部分,不存在理解负担。这里的“对齐”不是指技术上的字节对齐。 —— 张小方注
没有任何单一来源能完整描述内核是什么或其行为方式。
文档概述预期,代码定义已实现的内容,运行时行为展示系统在真实条件下的实际表现。每一层都至关重要,孤立来看都不完整。
文档反映设计意图——系统旨在做什么、受哪些约束、在何种上下文中运行。它可能描述接口、锁规则、内存语义或策略边界,但本质上是不完整的。文档分部分开发,清晰度和覆盖范围参差不齐,通常滞后于代码,且子系统之间存在差异。
代码定义机制,实现控制流、数据结构、状态转换和执行边界。其行为精确,但目的未必总是明确。约束可能隐含,命名可能反映早期设计,决策背后的原因往往缺失。代码展示内核做了什么,但未必解释为何这样做。
执行揭示实际行为,展示哪些路径处于活跃状态、哪些锁存在竞争,以及在真实时序和并发条件下哪些假设成立。行为由配置、硬件拓扑和工作负载塑造,反映实际发生的情况——而非预期或理论上允许的情况。
当这些层达成对齐时,理解才会浮现。
对齐并非指完全一致,而是指将文档、代码和行为结合审视——行为可追溯至其实现,实现在上下文中被理解。这使得清晰推理成为可能:结构如何填充、为何选择特定路径、观察到的行为是否有效。
这并非调试练习,而是学习方法。文档、代码和运行时之间的每处差异都揭示了有意义的内容。缺口可能反映历史,不匹配可能反映演进,未明说的假设可能反映性能、可移植性或遗留约束。
差异是可预期的。文档可能过时,代码可能执行不再被描述的约束,运行时可能倾向于曾被视为例外的路径。这些未必是错误,而是持续变化的系统的副作用。
内核并非静态的。代码渐进演化,文档独立维护,行为通过集成和使用呈现。理解取决于比较这些视角并解析它们所揭示的内容。
这个过程并非线性。它始于阅读,但通过跟踪、观察和关联才变得真实。一次提交解释变更,一次跟踪显示其影响,一个结构揭示其生命周期。这些共同形成可测试和推理的模型。
文档提供意图,代码定义机制,执行揭示真相。
只有通过它们的对齐,理解才会成形。
这就是方法。

34 如果内核不是由 Linus 创建和维护,会怎样?
本节是一首歌颂Linux内核贡献者的打油诗,小方翻译时,雅致谈不上,只能尽量表达其原意。 ——译者:张小方
到 20 世纪 90 年代初,相关理念已初具雏形。
GPL(通用公共许可证)为软件自由奠定了法律框架。
UNIX 展现了可组合系统的强大力量。
自由软件工具正广泛传播。
但理念无法自行构成一个系统,
更无法形成一个持久的系统。
接下来发生的不只是一个项目,
而是一系列清晰、务实且悄然激进的决策。
Linus 将内核按 GPL 发布,
此举并非为了发表声明,而是为了让所有人的贡献都能安全纳入。
他选择 C 语言,
并非出于怀旧,而是因为它能提供精确性、可预测性以及对每个字节的控制。
他构建了具有模块化边界的单体内核,
在性能与灵活性、简洁性与可扩展性之间取得平衡。
当内核发展超出原有工具的支持范围时,
他没有等待解决方案,
而是自己编写了一个。
Git 不仅仅是工具,更是大规模信任的基础设施。
最重要的是,他创建了一种变革需肩负责任的流程:
子系统有所有者,
所有权意味着审查,
审查意味着信任。
代码并非通过共识在 kernel 中流动,
而是通过信任链流转:
从贡献者到维护者,从维护者到集成树,
再通过拉取请求到达 Linus。
每一步的把关依据并非头衔,而是赢得并维持的信任。
这种信任成为了结构,
而这种结构得以存续。
这并非由某个基金会设计,
也不是从委员会中产生,
更不是为速度而优化。
它由约束塑造,
源于在不丧失可靠性的前提下管理复杂性的需求。
这正是它得以扩展的原因,
得以吸纳新架构、驱动程序、文件系统、调度器,
得以引入成千上万的贡献者,
同时不损害系统的完整性。
不难想象另一种起源:
由企业发起,
由委员会驱动的标准,
为某产品线构建的内核,仅维护至下一个产品发布。
或许它会附带保密协议而非邮件列表,
或许补丁需通过审批链而非公开审查,
或许开发速度会更快——直到某一天无法继续。
它可能更早被采用,
可能发布时功能更多,
但不会像现在这样持久。
内核的持久力不仅仅源于清晰的抽象或巧妙的代码,
更是关于如何接受变革、如何分担责任、如何赢得并维护信任等决策的结果。
这些决策早早做出,
始终如一地执行,
历经时间考验。
没有任何部分是必然的,
它的存续并非与生俱来,
而是通过纪律、结构和信任得以维护。
它仍在运行,
不是因为一成不变,
而是因为它被设计为可谨慎变革,
因为信任从未被视为事后考虑。
而这,同样是一个决策。

35 配置并非定制,而是内核的身份标识
Linux内核既非为嵌入式系统而打造,也不是专为服务器而设计。它的构建目标是成为目标所需的样子,而它了解自身角色的唯一途径便是通过配置。
对于内核而言,.config并非一组偏好设置,而是一项结构性决策。它定义了内核被允许知晓什么、必须忽略什么,以及将包含自身的哪些部分。这并非定制层,而是身份的声明。
这种区别之所以有效,是因为内核在保持稳固的部分和可变的部分之间划清了界限。
保持稳固的是设计:系统调用接口、调度器、内存管理器、设备模型和内部进程架构。这些是不变的,无论运行在嵌入式环境还是云规模环境,无论是最小化还是功能丰富的场景,它们都定义了内核的结构。
硬件随目标而变化。嵌入式系统可能使用SPI连接的传感器和最小化的MMU;服务器可能依赖PCIe设备、NUMA内存和虚拟化扩展。但内核并非通过为每个平台重写逻辑来适应,而是通过框架保持一致性。
框架架起了逻辑与物理的桥梁,它们定义设备的功能,而非连接方式。网络栈不关心NIC是在PCIe上还是USB上,输入子系统不区分I2C上的触摸屏和PS/2上的键盘。重要的是每个驱动程序都符合共享框架。
这些框架是保持稳固的一部分,它们为不同的物理配置和统一的内核行为提供了共同基础。由于接口不变,逻辑也不会改变,只有其背后的硬件会改变,甚至这也只是因为目标需要。
可变的是内核周围系统的接口:ACPI或设备树、PCI或SPI、NUMA或flatmem、抢占式或实时、服务器级或精简版。内核不会在运行时扩展自身以匹配环境,而是在构建时就被塑造以履行分配的角色。
该角色不是被发现的,而是被声明的。架构、可用驱动程序和启用的子系统在内核运行前就已选定。内核不是通过反应来适应,而是通过接受定义来适应。
这就是为什么配置并非表面功夫,它不是对已存在的内核进行调优,而是告诉内核它被允许成为哪个版本的自身。一旦构建完成,该身份就固定了。
无论最终是在小型ARM板还是虚拟机监控器主机中,内核都按配置的方式运行。这不是因为它的猜测,而是因为它被指示如此。
配置不是定制,而是承诺。

36 内存生命周期与塑造它的角色
在Linux内核中,内存通过一系列构成生命周期的职责进行管理。这些角色(请求者、分配器、访问者、所有者、释放器等)并非显式声明,而是通过函数、结构和约定来体现。
这个过程始于内核子系统(如文件系统、网络栈或驱动程序)使用kmalloc、vmalloc或alloc_pages请求内存,这是请求者的角色。
分配器(主要在mm/中实现,由slab、slub、buddy或vmalloc支持)选择一个内存区域,更新struct page或struct folio中的元数据,并返回一个指针。它授予访问权,但不跟踪内存的使用方式或时间。
访问者使用该内存存储内核结构、缓冲区或状态,这需要精确操作。诸如越界访问或释放后使用等错误并非源于分配,而是源于误用。
所有权通过引用计数或RCU跟踪。像sk_buff、inode和net_device这样的结构管理自身的生命周期,当引用数降至零时,该内存就有资格释放。
释放器使用kfree、vfree或__free_pages释放内存,将其返回给分配器。分配器可能将其合并到空闲列表或应用中毒模式,但不验证正确性,这仍是所有者的责任。
误用由KASAN、page_owner和kmemleak等跟踪器捕获,它们暴露释放后使用、跟踪分配位置并报告泄漏。这些工具支持开发,但不属于核心内存路径。
诸如ftrace、perf和eBPF等观察者监控时间、频率和分配行为,它们提供洞察而不影响逻辑。
访问控制由SELinux、AppArmor和cgroups执行,它们在分配前评估凭证并应用策略,其角色是执行约束,而非直接管理内存。
在压力下,内核调用回收器(kswapd、收缩器和OOM逻辑)从缓存和匿名页面回收内存,这些操作独立于原始分配器运行。
页面也可能被迁移器移动以进行压缩、大分配或NUMA平衡,这些操作在保留数据的同时更新映射。
在硬件边界,内存管理单元(MMU)维护页表并执行权限,错误在硬件中隔离无效访问。
尽管此逻辑的大部分位于mm/中,但没有单个模块管理整个生命周期,每项职责由内核的不同部分处理。内存安全不是通过中央监督来维护的,而是通过一致的边界和协调来维护的。
内核中的内存流经函数和结构,而非声明。这些角色未被命名但真实存在,通过设计执行并通过纪律维护。


37 中断如何在不变中演变
Linux 始终分两个阶段处理中断:立即运行的快速上半部分,以及完成剩余工作的延迟下半部分。尽管这一模型从最早的内核起就保持一致,但其背后的机制已发生显著演变。
在 Linux 1.x 和 2.0 中,下半部分实现为全局静态处理程序列表。即使在多处理器系统上,一次也只能运行一个,这种序列化设计随着对称多处理的普及成为可扩展性瓶颈。
为解决这一问题,Linux 2.3 引入软中断(softirqs),这是一种在中断上下文中处理延迟工作的 per-CPU 机制。软中断对网络、定时器和 RCU 仍至关重要,但其不能睡眠或被抢占,在负载下可能导致延迟。为简化使用,任务小项(tasklets)作为更高层接口被添加,管理序列化并抽象软中断细节,但保留了相同的原子约束。随着工作负载变得更复杂,这种灵活性的缺乏开始受限。
Linux 2.5 引入工作队列(workqueues),允许延迟工作完全在线程上下文中运行。工作队列处理程序可以睡眠、阻塞,并像任何其他内核线程一样与调度器交互。如今,工作队列是通用延迟处理的标准机制,已取代任务小项和自定义线程逻辑的大部分用途。
一个重大转变是线程化中断处理程序的引入,最初在 PREEMPT_RT 补丁集中开发,并在 Linux 2.6.30 中合并到主线。在这种模型中,上半部分仅确认中断,唤醒专用内核线程处理其余部分。这些处理程序可以睡眠、按优先级调度,并且完全可抢占,非常适合实时或对延迟敏感的工作负载。现在许多驱动程序默认使用线程化 IRQ。
从 Linux 2.6 到 4.x,内核通过 blk-mq、RPS 和 RFS 等功能改进中断处理,同时合并关键的 PREEMPT_RT 增强功能。这些更改提高了可扩展性和响应性,而不改变快速上半部分执行和延迟下半部分工作的核心模型。
Linux 6.x 继续这一进程。NAPI(网络轮询机制)现在可以在专用线程而非软中断上下文中运行,这改善了调度控制和 CPU 隔离,尤其在有严格性能或延迟约束的系统中。
曾经是软中断便捷抽象的任务小项现在已弃用,其序列化、原子执行不再与内核向灵活的基于线程的基础设施的转变一致。仍依赖它们的子系统正在迁移到工作队列或线程化 IRQ。
在此过程中,中断模型始终不变:处理紧急事务,延迟其余部分。改变的是延迟处理的实现方式——从序列化、不可抢占的路径转向并发、可调度的执行。接口保持不变,只是内核更擅长实现它。

38 并发之外的同步机制
大多数关于同步的解释始于线程并终于锁,但仅线程安全并不能反映内核的设计承受能力。并发只是内核必须防范的多个维度之一。内核中同步的目的不是保护代码不被多个线程执行,而是维护不相互等待的执行上下文之间共享状态的完整性。
在内核空间中,同一个函数可能同时在多个CPU上执行。重要的不是代码是否被重入,而是它触及的数据是否受到保护。临界区由数据而非代码定义。同步确保共享内存结构(任务列表、文件描述符、套接字缓冲区)即使在被可抢占线程、中断处理程序或延迟下半部分访问时也能保持一致。被序列化的不是调用路径,而是数据的边界。
系统安全不仅依赖于线程间的原子性,还包括对抢占的控制、中断的排除、核心间的可见性、对象生命周期的执行以及访问的限制。竞争、崩溃或安全漏洞可能并非源于并行执行本身,而是源于不安全的访问——通过中断更新、过早释放或对共享状态的未检查修改。
内核提供自旋锁、互斥锁和顺序锁用于互斥,使用RCU在延迟回收时实现无锁读取,在必须维护局部性的区域禁用抢占和CPU迁移,应用per-CPU变量完全消除竞争。为确保内存安全,它将引用计数与kref等生命周期感知对象结合,并使用RCU确保读取者从不在内存被释放后观察到它。在可见性必须跟随初始化的无锁路径中,它使用内存屏障强制执行顺序。这些机制不是避免竞争的通用解决方案,而是在特定上下文中消除特定形式不安全的目标工具。
内核不将同步视为优化,而是视为基础。任何可从多个上下文访问的数据必须考虑每一种可能的交互——SMP并发、中断抢占、软中断干扰、NMI进入和异步拆卸。它必须在抢占下安全,在CPU迁移中正确,通过每个引用有效,在释放后不可用。它必须在使用前准备好,并在整个可见期受到保护。
确保内核安全的不是它并行运行,而是它保持对共享内容的控制。内核中的同步是对该控制的执行,它保护数据而非函数,维护状态而非流程,确保共享结构即使通过多个执行路径到达也能保持一致。

39 这从来不是关于炒作,而是关于硬件
Linux 的成功并非源于追逐潮流,而是源于对真实硬件变化的持续适应。在过去十年中,CPU 获得了更多核心和新的调度模型,内存变得分层且持久化,存储从旋转磁盘转向基于队列的快速闪存,网络速度已快到可与本地内存访问相媲美。内核在每个阶段都在演进——不是通过营销,而是通过人们在真实系统上运行它、遇到真实问题并修复它们。
内核在两个方向上运作:朝向用户空间和朝向硬件。它避免破坏用户空间,因为这样做从根本上就是错误的,而不仅仅是冒险的。基于早期内核版本构建的程序应该继续工作,这种稳定性被描述为一种责任而非仅仅是一项政策。当向后兼容变得不切实际时——例如放弃 386 支持——它会以谨慎和明确的理由进行。
内核在必要时也接受变革。它早期采用了 64 位支持,现在同时支持 GCC 和 Clang,并在内存安全重要的领域采用 Rust。这些变革是由解决问题驱动的,而非追求新奇。
同样重要的是 Linux 刻意回避的领域。它并非为每种设备或具有极端约束的深度嵌入式系统而设计。这种局限性被描述为一个明确的决定,而非弱点。Linux 提供了一个完整的内核,包含调度、内存管理、驱动程序和用户空间接口。它不能也不应该适用于不需要这些功能的硬件——更小的内核在那里更有意义。
作者强调了这种务实的范围:Linux 并非试图做所有事情,而是旨在在有意义的地方做正确的事情。它之所以持续成长,是因为它被积极使用,而使用会产生错误报告,这些报告导致修复,从而为每个人改进内核。从单板计算机到云基础设施,Linux 通过一次解决一个真实问题而取得成功。
内存分层、安全虚拟机隔离、异步 I/O 和可编程网络等功能并非为了追随潮流而添加——它们是因为系统在真实工作负载下需要可靠性而添加的。内核之所以保持可信赖,是因为它专注于硬件,成长不是为了可见性而是为了实用性,改进是因为人们运行它、测试它并依赖它。
这种方法并非过时——它是稳定的、有目的性的,而且仍然正是复杂系统所需要的。
40 从意图到 I/O:内核如何看待文件、磁盘和设备
当一个进程调用 read() 时,它请求的是数据,而非磁盘。随之而来的是跨内核层的结构化交接,每一层都有明确的职责。内核对文件的感知方式与用户不同——通过结构、委派和执行。从意图到 I/O 的路径穿越三个核心组件:虚拟文件系统(VFS)、块 I/O 子系统和设备驱动程序。
VFS 是解释用户空间请求的第一层。它将路径名还原为内部对象:dentry、inode 和文件描述符。它无需知道挂载了哪个文件系统;其角色是"提供统一的接口并将操作分派到正确的实现"。通过抽象和间接性,VFS 将逻辑访问与物理布局分离。
文件系统驱动程序(ext4、xfs 等)将文件偏移量转换为设备上的逻辑块地址。它遍历元数据结构,解析映射,并确定请求的数据驻留在何处。它不与硬件交互。相反,它构造一个 bio——一个描述 I/O 操作的结构,包含内存页面、偏移量和方向。驱动程序通过 submit_bio() 提交它。
至此,文件上下文已消失。bio 层不跟踪用户状态或系统调用来源。它专注于移动数据:“哪些页面、哪些扇区、哪个方向”。它可能通过多队列(blk-mq)框架合并、拆分或调度 bio,将它们转换为设备特定的请求。
在栈的末端是设备驱动程序。 它不关心是什么触发了请求或数据代表什么。它的工作是将请求转换为硬件命令:设置 DMA、发出操作并处理完成。“它在不了解文件、路径或进程的情况下执行 I/O。”
I/O 栈是可扩展的,因为每一层都恪守其角色。VFS 解释结构。Bio 描述 I/O。驱动程序执行。“没有一层承担另一层的职责。”
设备映射器(Device Mapper) 正是因此设计而存在。它可以无缝地嵌入 bio 和驱动程序之间,提供用于加密、镜像或配置的虚拟块设备——而不触及 VFS 或文件系统逻辑。它处理 bio 并传递它们,符合块接口规范。文件系统在上方不变地运行。驱动程序在下方毫不知情。“系统保持连贯,因为边界得以维持。”
Linux I/O 栈之所以持久,是因为它的层是受纪律约束的。读操作从 VFS 开始,变成 bio,并在驱动程序中完成。设备映射器或循环设备等可选层可以拦截或转换请求而不会破坏模型。eBPF 等工具可以在不干扰的情况下观察路径。
“内核看到的不是文件。它看到的是一个任务:解析、传输、完成。“每一层都恰好做它应该做的事——不多不少。这就是它运作的原因。
41 心中的内核——效率至上而非历史遗留原因:为什么内核仍用 C 语言开发
系统语言的目的不是服务于程序员。而是让内核能够独立运行——直接在硬件上。没有运行时。没有依赖。只有代码、机器以及它们之间的接口。
内核使用 C 语言并非因为历史原因。而是因为没有其他语言能在软件与硬件交汇之处提供同等级别的效率、控制和结构清晰度。
C 是一种组件语言。源文件定义函数、结构体和指针。头文件仅声明需要对外可见的内容。编译结果形成 ABI——一个稳定的二进制契约,允许每个模块在不同构建和架构之间可预测地运行。
内核不是在运行时定义的;它是在构建时塑造的。通过 C 预处理器,.config 设置使用条件宏激活或排除功能。这些决策控制编译路径,将内核裁剪到特定硬件,并完全消除未使用的逻辑。看似通用的行为是通过基于模式的代码复用(宏和内联函数)实现的——而非通过模板或反射。
C 在内核设计中扮演三个角色。首先,它直接表达与硬件数据手册对齐的控制逻辑——寄存器、内存映射 I/O 和控制流。其次,它实现紧凑、可预测的算法来管理 CPU 时间、内存和线程。第三,它通过引用和组合来构建系统。嵌套结构体、内嵌回调和函数指针实现了模块化框架,无需继承。
C 中的结构体在内存中精确布局。每个字段根据 ABI 强制的对齐规则放置。填充仅在必要时引入,总大小不仅反映数据还反映布局。这使得每次访问都是可预测的,每个偏移量都是有意义的,结构体适合二进制级通信。
C 中的指针不仅仅代表地址。函数指针绑定到代码。结构体指针指向状态。void * 引用原始内存——它不是用来转换任何东西的。它不转换;它解释。它允许对扁平内存进行有结构的访问,其中结构由程序员基于已知类型施加——而非通过改变类型本身。内存是扁平的,void * 是在该空间中表示数据的最自然方式。
C 中的可见性是刻意的。静态符号保持内部。外部声明仅定义共享的内容。在内核中,符号导出通过 EXPORT_SYMBOL 显式控制,它定义了模块在运行时可以链接的内容。没有隐式可见性——每个边界都是有意为之的。
C 提供对内存、执行和结构的直接访问,而不对机器之上的任何东西做假设。这就是它仍然是内核语言的原因——不是因为它古老,而是因为它仍然是描述系统如何独立运行的最高效和最诚实的方式。
42 活着的内核:Linux 内核架构的实用概述
当 Linux 启动时,在固件和引导加载程序完成后,压缩的内核镜像被加载到内存中,同时加载的还有 initramfs——一个最小的临时根文件系统,用于在真正的根文件系统接管之前帮助完成早期设置,例如加载驱动程序。
Linux 是一个具有模块化能力的单体内核。 核心组件被编译成单个二进制文件,但可选功能(如设备驱动程序)可以在运行时动态加载或卸载。
用户空间与内核空间
内核在用户空间和内核空间之间实行严格分离。用户空间运行有限特权的应用程序,而内核空间以完全特权运行,允许它管理内存、调度进程、控制 I/O 设备。在 x86 上,Ring 3 用于用户应用程序(最低特权),Ring 0 用于内核。转换通过系统调用发生。
七大核心子系统
1. 进程调度器 — 使用抢占式、基于优先级的调度管理多任务。默认的完全公平调度器(CFS)使用红黑树高效选择下一个要运行的任务。它还支持实时策略(SCHED_FIFO、SCHED_RR、SCHED_DEADLINE)、CPU 亲和性和负载均衡。
2. 内存管理 — 每个进程获得自己的虚拟地址空间。MMU 和页表将虚拟地址转换为物理地址。该子系统处理页面分配、交换、共享内存、写时复制和内存映射文件。
3. 虚拟文件系统(VFS) — 一个抽象层,为所有支持的文件系统提供统一接口,无论后端是 ext4、Btrfs、XFS、NFS 还是 tmpfs。它管理 inode、dentry、文件描述符和挂载点,将用户接口与存储后端解耦。
4. 设备驱动程序 — 为存储、输入、网络、图形和声音设备抽象硬件细节。它们可以静态编译进内核,也可以作为可加载内核模块在运行时动态加载。
5. 网络栈 — 实现完整的 TCP/IP 栈,支持路由、套接字通信、通过 iptables/nftables 实现防火墙、NAT,以及包括 IPv4、IPv6、ARP 和 ICMP 在内的协议。高级功能包括流量整形、桥接、隧道和虚拟网络接口。
6. 系统调用接口 — 作为用户空间应用程序访问特权内核服务的受控网关。示例包括 read()、write()、fork()、execve()、mmap() 和 brk()。
7. IPC 与命名空间 — 提供管道/FIFO、信号量、互斥锁、消息队列和共享内存。命名空间将全局资源分区为隔离视图,而 cgroups 限制资源使用。它们共同构成了进程隔离和容器化的基础。
可加载内核模块
LKM 让单体内核获得灵活性——独立组件在运行时加载或卸载,用于驱动程序、文件系统和网络协议。modprobe、insmod 和 rmmod 等工具无需重启即可管理它们。
系统调用机制(逐步解析)
- 应用程序通过 libc 调用库函数(例如 read())
- 特殊 CPU 指令(syscall、int 0x80 或 svc)切换到内核模式
- 内核读取系统调用号(在 x86_64 上从 rax 获取)并通过系统调用表分派
- 结果放入返回寄存器,sysret 切换回用户模式
内存保护与进程隔离
MMU 通过带权限标志的每进程页表强制执行分离。未授权访问触发页面错误或段错误,内核通过终止违规进程来响应。内核内存对用户模式代码不可访问。这保证了安全性、稳定性和多任务安全。
内核被描述为一个通用、模块化和安全的操作系统核心,协调调度、内存管理、I/O 和 IPC,同时在各种硬件平台上维护系统完整性和性能。
43 Linux 内核漫步:理解其子系统
Linux 内核并非一个单体的庞然大物,而是一个结构良好、高度模块化的系统,设计用于跨设备、架构和用例工作。
arch/ — 包含特定于架构的代码,适用于 x86、ARM 和 RISC-V 等平台,包括引导逻辑和低层内存管理。
init/ — 在特定于架构的设置之后,真正的初始化在此发生,挂载根文件系统并启动第一个用户空间进程。
kernel/ — 被描述为中枢神经系统,处理进程调度、信号、计时和系统调用。
ipc/ — 实现 System V IPC 机制:消息队列、信号量和共享内存,用于进程间通信。
mm/ — 管理内存管理,包括虚拟/物理内存、分页、交换、NUMA 和透明大页。
fs/ — 包含 VFS 层和文件系统实现,如 ext4、Btrfs、XFS 和 NFS。
net/ — 包含完整的网络协议栈,从以太网到 TCP/IP,再到 VPN 和隧道协议。
drivers/ — 被称为 Linux 传奇硬件支持的核心,涵盖显卡、USB、声卡、网络适配器等。
lib/ — 共享工具,包括数据结构、字符串函数和压缩算法。
include/ — 头文件,包含将内核连接在一起的声明、常量和宏。
security/ — 实现基于 LSM 的模块,如 SELinux 和 AppArmor。
block/ — 管理低层存储 I/O,用于硬盘、SSD 和闪存。
io_uring/ — 现代异步 I/O 接口,使用共享环形缓冲区最小化上下文切换和系统调用。
virt/ — 虚拟化基础设施,支持 KVM、客户/主机通信和半虚拟化。
crypto/ — 模块化加密 API,包含 AES、SHA 和 RSA 等算法。
certs/ — X.509 证书,用于验证签名内核模块,支持安全启动。
sound/ — ALSA 驱动的音频支持,包含驱动程序和数字混音逻辑。
tools/ — 用户空间工具,用于调试和性能调优,包括 perf 和 bpf。
scripts/ — 构建时工具和实用程序,用于配置、代码生成和编译。
rust/ — 新兴的基于 Rust 的内核代码,在不牺牲性能的情况下提供内存安全。
内核是一个活的、不断进化的有机体,具有清晰的边界、深思熟虑的抽象和专门构建的子系统——鼓励读者一次探索一个子系统。
44 共享代码,分离状态:我在内核内存管理中的第一课
我最近开始探索 Linux 内核的工作方式——已经遇到了一些改变我对操作系统理解的观念。
最令人惊讶的教训之一是关于内核本身如何被映射到内存中。内核不像用户程序那样作为单独的进程运行。然而,它存在于每个进程的地址空间中。它的代码和只读数据在所有进程中以固定的虚拟地址映射。这不是复制——这是一个共享的、只读的映射,节省内存并保持系统高效。
但真正让我印象深刻的是内核如何将共享逻辑与可变状态分离。虽然内核代码在整个系统中是相同的,但每个进程都有自己的内核栈——一个仅在该进程进入内核模式时使用的私有内存区域。每任务结构如 task_struct、内存映射和调度元数据都随之而来。这些都是分开管理的,以维护进程隔离。
这种设计允许内核保持集中和轻量级,同时仍然为每个进程提供安全和私有的上下文来运行。内存模型被描述为优雅的:一个共享的内核,但个性化的执行环境。
学习这些引发了关于并发的问题。同一个内核函数可能同时在多个 CPU 上运行——处理不同的系统调用、服务中断或处理后台任务。虽然我才刚刚开始,但我已经能看到为什么线程安全和可重入性在内核开发中如此重要。共享逻辑和每进程状态意味着内核需要精确协调访问。
我现在的重点是理解这些内存基础:代码如何映射,栈如何隔离,内核如何保持一切既高效又安全。即使在这个早期阶段,这种视角已经改变了我对系统设计的思考方式。
这只是开始,但感觉已经用新的眼光看待 Linux 了。鼓励那些走类似道路进入系统或内核内部的人从内存开始。那是所有事情开始变得有意义的地方。
45 进程如何工作——从内核的视角
虽然在探索内核开发和虚拟化时,作者退后一步来理解一些基本的东西:Linux 中的进程到底是什么?从外部看,我们将进程视为运行的程序,但从 Linux 内核的角度看,进程是一个丰富、复杂的数据结构——不仅仅是一个执行单元,而是内核在整个生命周期中管理的完整对象。
核心:task_struct
核心是 task_struct——内核对进程或线程的内部表示,定义在 include/linux/sched.h 中。它包含内核管理执行所需的一切:
struct task_struct {
pid_t pid;
long state;
struct mm_struct *mm;
struct files_struct *files;
struct thread_struct thread;
...
};
它很大(通常 9-12 KB),因为它存储了 CPU 状态和调度信息、内存布局(mm_struct)、打开的文件、信号、凭据、命名空间和 cgroup 数据,以及审计、跟踪和安全字段。
创建:fork()、clone() 和 execve()
fork()创建调用进程的几乎相同的副本。clone()更灵活——它允许你共享内存、文件或信号处理程序(这就是线程的创建方式)。execve()用新的程序镜像替换当前进程内存空间。
总结:fork() 复制,execve() 替换。
内存和执行上下文
每个 task_struct 指向一个 mm_struct,它定义了进程的内存布局——栈、堆、代码和映射区域。thread_struct 存储 CPU 寄存器,以便进程可以被暂停和恢复。调度器使用这个来高效地在进程之间切换。每个进程有一个内核栈和一个用户栈。
系统调用:跨越边界
当用户进程进行系统调用(如 read() 或 open())时,它从用户模式转换到内核模式。内核切换 CPU 以使用内核栈,查找系统调用处理程序,在内核上下文中运行,然后返回用户空间。这种交互是严格控制和安全的,这使得 Linux 具有鲁棒性。
进程的生与死
当进程结束时,它调用 exit(),清理资源,变成僵尸进程直到其父进程调用 wait()。如果没有父进程存活,init(PID 1)将回收它。
线程?它们也只是任务
在 Linux 中,线程也是 task_struct 条目。唯一的区别是它们共享什么。如果两个任务共享同一个 mm_struct,它们就在同一个内存空间中运行——这就是使它们成为线程的原因。
理解 Linux 内核如何看待和管理进程是令人大开眼界的。它是模块化的、高效的,为大规模灵活性而构建。一切都从 task_struct 开始。
46 如何进入内核:系统调用、陷阱和中断
Linux 内核不是进程、线程或守护进程,而是一个特权的、驻留在内存中的环境,始终存在但从未被调度。内核不像程序那样运行——它是被进入的。
进入内核意味着 CPU 从用户模式(Ring 3)转换到内核模式(Ring 0)。这是一个硬件控制的转换,处理器使用专用的内核栈以提升的特权从内核的内存空间执行指令。没有启动新进程;在用户空间中运行的同一线程继续在内核内部执行。内核是被线程执行的,在需要时。
三个明确定义的场景导致这种进入:系统调用、硬件中断和 CPU 异常/陷阱。
1. 系统调用:来自用户空间的有意进入
当用户空间程序需要特权操作(如打开文件)时,它调用库函数如 read(),该函数发出特殊指令——在 x86_64 上是 syscall,在 ARM64 上是 svc #0。这些不是普通的跳转——它们是硬件管理的特权转换。
此时,CPU 保存用户模式寄存器状态,切换到线程的内核模式栈,禁用某些用户可见的标志,并跳转到存储在型号特定寄存器(如 MSR_LSTAR)中的预定义内核入口点(如 entry_SYSCALL_64)。
同一线程现在在自己的内核栈上执行内核代码。内核处理请求(如运行 sys_read()),然后执行受控返回到 Ring 3。线程本身没有改变。没有发生上下文切换。
2. 硬件中断:来自设备的异步进入
硬件设备(网卡、磁盘控制器、定时器)可以随时使用中断请求(IRQ)向 CPU 发出信号。当 IRQ 被触发时,CPU 暂停当前运行的线程,使用中断描述符表切换到 Ring 0,并跳转到设备特定的内核处理程序。
与系统调用不同,中断处理程序在中断上下文中执行。它们不关联任何用户进程,不能睡眠,并且必须快速返回。额外的处理被延迟到内核线程(如 kworker/*),通过软中断或工作队列。
3. 异常和故障:非计划的转换
当进程访问无效内存、除以零或触发页面错误时,会发生故障或陷阱。这些不是故意的——它们是用户空间中不正确或不完整执行的结果。
CPU 使用故障特定的入口陷入内核,进入 Ring 0,并跳转到相应的故障处理程序(如 page_fault())。内核可能解决问题(加载内存页面)或终止进程(通过 SIGSEGV)。同一线程仍在运行——它只是穿越到了内核,以便系统可以响应故障。
关于内核映射的说明
内核被永久映射到内存中——特别是每个用户进程的虚拟地址空间中。在 64 位系统上,低地址范围用于用户内存,而高地址范围(如 x86_64 上的 0xffff800000000000)保留给内核。
虽然这种映射存在于每个进程中,但该区域被标记为特权。在 Ring 3 中运行时无法访问。用户模式访问触发页面错误。只有在 Ring 0 中——通过系统调用、中断或陷阱——才能使用内核内存。
这就是为什么我们说内核始终存在:它存在于每个进程的内存空间中,但在被显式进入之前保持不可访问。
没有被调度的执行
在所有三种进入场景中,内核都是作为事件的结果被进入的。CPU 使用与当前线程或 CPU 关联的内核栈,从已映射的内存执行内核代码。没有全局的"内核线程"接管。
这解释了为什么内核没有 PID,不出现在 ps 中,在空闲时似乎不活动。然而它始终被映射,始终具有特权,始终准备好接管控制。
Linux 内核不是进程表的参与者。通过系统调用、中断和陷阱,它被短暂而精确地进入,响应需求而执行。它不会自己运行——它是有目的地被进入的,然后退出。它不是进程——它是系统。
47 现代 Intel 64 位 CPU 内部有什么?
从系统层面看现代 x86_64 CPU,通过 Linux 内部、虚拟化和系统调用/硬件交互的学习,可以将 CPU 组织为八个主要组件区域。
1. 执行单元
每个核心包含一个 ALU(整数算术和逻辑)和一个 FPU(浮点运算),处理从系统调用逻辑到实时数学的核心计算任务。
2. SIMD 单元
SSE、AVX 和 AVX2 扩展在多个数据上同时执行单条指令,适用于图形、科学计算和机器学习加速。
3. 寄存器
在 64 位模式下,寄存器集包括通用寄存器(RAX-R15)、RIP(指令指针)、RSP(栈指针)、控制寄存器(CR0、CR3、CR4)用于分页/保护,以及 MSR 用于配置 SYSENTER、SYSCALL 和 VMX 行为。这些携带系统调用参数、返回值、指针和系统状态。
4. 内存管理
MMU 将虚拟内存转换为物理内存。通过 CR3 访问的页表定义映射,TLB 缓存转换以加速,支持大页(2MB/1GB),EPT 允许虚拟机中的嵌套分页。这是 fork、exec 和 VM 内存隔离的基础。
5. 缓存层次结构
三级缓存最小化内存延迟:L1(约 32KB,最快,每核心)、L2(约 256KB-1MB,每核心)和 L3(8MB-30MB+,共享)。预取器预测访问模式以提高吞吐量。
6. 中断控制
本地 APIC 处理每核心中断,而 IOAPIC 路由来自设备的硬件 IRQ,实现实时调度、核心间通信和设备事件处理。
7. 定时器与计数器
TSC 提供高分辨率计时,HPET 实现精确调度,PMU 硬件事件计数器支持性能分析。内核使用这些进行任务调度和性能测量。
8. 特权与模式控制
Ring 0(内核)与 Ring 3(用户)模式,通过 SYSCALL/SYSENTER 快速进入,以及 VMX root/non-root 用于主机与客户虚拟化。这些转换及其导致的陷阱(VMEXIT)使虚拟机监控程序成为可能。
指令集扩展
SSE/AVX/AVX-512 用于并行化,AES-NI 用于加密加速,SGX 用于安全飞地,Intel VT-x/VMX 用于硬件虚拟化。
48 select() 和 poll() 如何为 epoll() 铺平道路
早期 UNIX 网络
在早期 UNIX 网络中,处理多个客户端连接通常意味着为每个连接创建一个新线程或进程。它能工作但缺乏效率。一个更好的方法出现了:与其在每个连接上阻塞,不如等待其中任何一个就绪。这个概念催生了 I/O 多路复用和 select() 系统调用。
select() — 革命性的飞跃
select() 是开创性的。它使单线程程序能够同时监视多个套接字。你提供一组文件描述符,它报告哪些已准备好读取或写入。服务器现在可以处理并发客户端而无需生成多个线程——这是那个时代的重大进步。
select() 的局限性
随着 Web 的扩展,select() 暴露出显著的弱点。它带有硬编码的文件描述符限制,通常为 1024。每次调用都需要将整个描述符集复制到内核中并线性扫描它们。随着连接数增加,性能下降。
poll() — 向前一步
poll() 消除了文件描述符上限并提供了更灵活的 API。然而,其核心机制仍然是线性扫描器。完整的描述符列表仍然需要在每次调用时重建。对于轻量级应用程序来说足够了;对于大规模系统来说证明是不够的。
epoll() — 一切都改变了
Linux 引入了 epoll(),专为规模化而设计。对事件的兴趣只需注册一次,内核维护对哪些描述符就绪的感知。内核不再重复扫描列表,而是仅在发生变化时通知应用程序。这种内部状态管理使 epoll() 大幅提高效率。它还引入了边沿触发通知,使应用程序能够进一步减少开销。
对现代系统的影响
这种模型解锁了现代事件驱动服务器如 NGINX。NGINX 不再为每个连接生成线程,而是采用单个 epoll 驱动的事件循环监视数千个套接字。这种架构转变是 NGINX 主导高性能 Web 托管的关键原因。
Node.js 等更高层次的平台遵循相同的哲学。虽然开发者从不在 JavaScript 中直接编写 epoll(),但 Node.js 构建在 libuv 之上,这是一个跨平台事件库,在 Linux 上使用 epoll(),在 macOS 上使用 kqueue,在 Windows 上使用 IOCP。这让开发者能够以简洁的单线程风格编写异步、非阻塞的应用程序,而系统处理繁重的工作。
从 select() 到 poll() 到 epoll() 的演进不仅仅是 API 的进化——它构成了互联网如何扩展的基础。API、消息系统、流媒体平台和实时应用程序都依赖于这一演进。关键洞察:不是更多的线程,而是更智能的事件——只在必要时响应。
这种转变悄然改变了互联网的运作方式,一切都始于一个简单的问题:“我可以从这个套接字读取了吗?”
49 epoll() 之后:io_uring 如何重新定义 Linux I/O
文章追溯了 Linux I/O 机制从 select()/poll() 经过 epoll() 到 io_uring 的演进。
epoll 时代
当 epoll 出现时,它取代了需要在每次调用时重建文件描述符集并线性扫描的旧方法。NGINX 和 Node.js 在其事件驱动模型上蓬勃发展,使用单线程事件循环处理数千个连接。
epoll 的局限性
随着系统扩展,epoll 显示出弱点。它仍然使用两步流程——等待就绪,然后执行 I/O——意味着每个操作至少需要两个系统调用,一个检查,一个行动。
io_uring 登场
io_uring 于 2019 年在 Linux 5.1 中由 Jens Axboe 引入,采用了根本不同的方法。它不仅仅发出就绪信号,而是让你提前提交整个操作。请求进入共享内存环,内核负责执行。完成事件出现在单独的环中供应用程序消费。
关键洞察: 当你的应用程序看到完成事件时,数据已经到位了。对于读取,内核已经填充了缓冲区——不需要后续系统调用。这消除了额外的往返,减少了上下文切换,并提供了对结果的即时访问。
基于完成的 I/O 模型
这种方法避免了不必要的开销,消除了来回的系统调用,并利用用户空间和内核空间之间的共享内存在负载下实现重大性能提升。名字反映了这一点:通过用户空间环形缓冲区进行 I/O。
思维转变
文章将其描述为从问"这个准备好了吗?“转变为说"在你可以的时候做这个,然后告诉我。”
采用状态
io_uring 在性能关键系统中正在获得关注。NGINX 提供实验性支持,MySQL 正在探索其好处,更新的项目将其作为核心组件采用。
epoll 仍然是经过实战检验和可靠的,但 io_uring 代表了从 select→poll→epoll→io_uring 演进的自然下一步。它不是替代品,而是向前的飞跃,减少摩擦,削减开销,释放新的性能潜力。
50 多任务与虚拟化——真正的区别是什么?
多任务:一个操作系统,多个进程
在典型的多任务系统中,主机操作系统拥有硬件并调度多个进程(应用程序、守护进程、服务)。每个进程获得自己的内存空间,但它们都信任同一个内核。操作系统使用上下文切换来暂停和恢复进程——保存 CPU 寄存器、程序计数器和内存映射。这被描述为分时共享,在共享单个监管者的同时提供并行的假象。
虚拟化:一个主机,多个操作系统
在虚拟化中,主机操作系统(或虚拟机监控程序)运行多个客户操作系统。每个客户操作系统管理自己的进程、文件系统、驱动程序和内核,每个都相信它拥有整台机器。但虚拟机监控程序保持控制。
现代 CPU(Intel VT-x、AMD-V)通过以下方式提供帮助:
- 隔离特权 CPU 状态(控制寄存器、中断表等)
- 允许对 CPUID 或 HLT 等指令进行陷阱和退出机制
- 为每个客户维护虚拟 CPU 上下文(VMCS)
- 确保一个客户不能干扰另一个客户或主机
这不仅仅是多任务处理进程——而是多任务处理整个操作系统,每个操作系统都有自己拥有裸机硬件的假象。
关键区别
多任务:
- 多个进程
- 一个操作系统内核(监管者)
- 软件隔离
虚拟化:
- 多个操作系统内核
- 一个监管者(虚拟机监控程序)监督所有客户操作系统,每个客户操作系统是自己进程的监管者
- 硬件和虚拟机监控程序隔离
虚拟机监控程序是将虚拟化与多任务区分开来的东西。 它不仅仅是共享时间——它创建安全的虚拟硬件环境。
快速术语表
- 虚拟机监控程序 — 运行和管理虚拟机的软件,将每个客户操作系统与主机和其他客户隔离。
- CPUID — 报告 CPU 功能的指令;虚拟机监控程序捕获它以控制客户看到的内容。
- HLT(停机指令) — 停止 CPU 直到下一个中断;在 VM 中,它被捕获以便主机可以调度其他工作。
- VT-x(Intel 虚拟化技术) — Intel 对虚拟化的硬件支持;实现高效的客户/主机切换和隔离。
- VMCS(虚拟机控制结构) — CPU 在虚拟化转换期间用于存储客户和主机状态的内存结构。
- CR3(控制寄存器 3) — 持有页表的基地址,用于虚拟到物理内存转换。切换 CR3 切换进程或操作系统内存空间。
- GDT(全局描述符表) — 在 x86 保护模式下定义内存段和访问权限。每个操作系统通常设置自己的 GDT。
- IDT(中断描述符表) — 将硬件和软件中断映射到其处理程序函数。客户操作系统设置自己的 IDT。
- VMX(虚拟机扩展) — Intel 的硬件虚拟化功能,引入新的 CPU 模式(root 和 non-root)和指令(VMXON、VMLAUNCH)以安全运行客户操作系统。
- VMEXIT — 从客户(non-root)模式返回到主机(root)模式的转换,由特定指令或事件(CPUID、HLT、I/O)触发。虚拟机监控程序处理事件并决定下一步。
51 内核始终存在——即使在它没有运行的时候
核心问题
Linux 内核不是进程、线程或调度实体。如果内核缺少这些属性,它如何能始终存在于系统中?
答案涉及内存结构、硬件保护,以及 CPU 将内核视为每个进程下方的特权基底。
内核始终被映射
内核在启动时被永久加载到物理内存中,并映射到每个用户进程的虚拟地址空间。在 x86_64 上,用户空间占据 0x0 到 0x00007fffffffffff,而内核空间从 0xffff800000000000 及以上开始——在 48 位寻址中每个区域跨越 128 TiB。
每个进程共享相同的内核映射。这不是复制,而是由内核 MMU 插入的全局一致的虚拟区域。然而,当 CPU 在 Ring 3 中运行时,此范围不可访问。
硬件保护:Ring 0 或故障
内核内存页面在页表中携带仅监管者标志。如果用户空间尝试读取或跳转到内核内存,CPU 通过用户/监管者位和 CR0 的写保护位触发页面错误。
安全访问内核内存的唯一方法是进入 Ring 0——通过 syscall、int 或 SVC 等机制。这种方法产生快速转换(系统调用时不切换页表),一致的每线程内核栈访问,以及统一的执行基底。
每线程内核栈
每个线程获得自己的内核模式栈,在创建时分配。当 CPU 进入内核时,它切换到这个栈,该栈不与用户空间共享,由溢出保护的保护页守护,用于控制流和局部变量。没有专门的"内核线程"等待系统调用——栈和代码只是准备好并被映射。
不需要调度——然而内核调度
内核不被调度,但它编写调度。当 nanosleep()、poll() 或 read() 等系统调用导致阻塞时,内核将线程置于睡眠状态,决定唤醒时间,并选择下一个可运行实体。它的代码在导致转换的任何东西中执行(用户线程系统调用或中断处理程序),然后返回用户空间或让步给调度器。
始终存在,从未运行
内核的性质是三重的:始终作为每个地址空间的一部分映射到内存中,始终通过 Ring 0 门控保护,始终准备好栈和处理程序。然而它从未"传统地运行”——它不会出现在 top 或 ps 中,不能接收信号,不会循环或调度自己。
它不在你的程序旁边运行;它从下方使能它们。
存在而非执行
内核没有自己的生命周期,而是定义了其他一切的生命周期。它仅在硬件控制的时刻激活:边界穿越、设备中断和故障。有时它处理系统调用,响应 IRQ,或让步给内核线程——安静地醒来,完成工作,然后再次让步。
内核不是你看到的东西。它是使其他一切可见的东西。
52 仍然流经它的一切
现代系统跨越许多层——语言、运行时、解释器、容器、模型、协议——但每一层最终都将控制权传递给同一个核心层:内核仍然管理着那条路径。
执行开始时是结构化的逻辑:创建进程,映射内存,调度指令,将 I/O 打开为流,连接套接字,驱动程序移动数据,处理中断,硬件被激活。每次转换都由内核验证、隔离和调解。
即使代码在托管运行时内运行,由事件触发,在容器中,跨虚拟机——控制流仍然熟悉。每个内存访问、I/O 操作和任务切换都通过内核接口进行。边界仍然存在。契约仍然有效。
这根植于冯·诺依曼架构——代码和数据在共享内存中,指令被顺序获取和执行,状态一次更新一个操作。这仍然是通用机器的模型,内核完全在其中运作,协调所有更高的层。
什么会构成结构性变化
当系统不再运行在冯·诺依曼模型上时——没有取指-译码-执行循环,没有共享的指令/数据内存,没有程序计数器——结构就改变了。
当内存不再以字节为单位寻址时——当访问变成基于图的、内容寻址的或物理上与逻辑共存时——接口就打破了。
当执行不再通过系统调用,用户/内核空间边界消解时——控制模型不再适用。
当内核不再需要抽象设备、隔离进程或调度计算时——它不再是中心。
当逻辑不再被编写、编译或符号解释,行为被学习、涌现或没有离散指令地模拟时——软件本身已经转变。
在那之前,结构保持不变。系统仍然一次运行一条指令,将代码存储为数据,陷入内核,等待许可,只在被询问时回答。而当它必须真正运行时,内核仍然回答。
53 我为什么持续书写内核
内核是一个无法用单一解释捕获的宇宙。
每次你认为理解了它,就会发现另一层——另一个子系统,另一个约束,另一个设计决策回响穿越数十年的代码。这不是因为内核是混乱的。恰恰相反。它是因为内核是一致的,以至于它的简单性具有欺骗性。
你学习调度器,然后发现调度器不能脱离内存管理来理解。你学习内存管理,然后发现它依赖于中断处理。你学习中断处理,然后发现它与同步原语交织在一起。每一个都会引向另一个,不是因为糟糕的设计,而是因为这就是系统的工作方式。
内核不是一个你可以逐章阅读然后宣称完成的项目。它是一个活的文档,每次硬件变化、每次新工作负载出现、每次有人在代码中发现更好的方式时都在增长。
我持续书写它,因为每次我写一个解释,我都会学到一些新东西。不是关于代码本身——而是关于代码背后的推理。为什么选择这个数据结构而不是另一个?为什么这个锁在这里而不是那里?为什么这个接口以这种方式暴露?
这些问题没有在手册页中回答。它们没有在注释中回答。它们在代码的行为中回答——在它如何响应负载、如何处理故障、如何在变化中保持正确。
我持续书写它,因为有人需要。系统编程的世界充满了假设你已经知道一切的文档。但你不是天生就知道的。你是通过阅读、犯错、再阅读来学习的。
如果这些文章帮助哪怕一个人少走一条弯路,少犯一个错误,少感到一点迷失——那就值得了。
内核是一个宇宙,但它是你可以进入的宇宙。一次一个概念。
54 成为内核开发者需要什么——以及有多少人?
Linux 内核是世界上最大、最活跃的开源项目之一。它由来自全球各地的数千名贡献者维护,跨越数百个子系统。
角色与技能层级
内核社区有明确的层级结构:
首次贡献者 — 从修复文档错误、编码风格问题或小 bug 开始。内核有严格的代码审查流程,每个补丁都必须通过维护者的审查。
定期贡献者 — 随着经验积累,贡献者开始处理更复杂的功能和修复。他们学会了内核的编码规范、提交消息格式和审查文化。
子系统维护者 — 负责特定子系统(如网络、文件系统、驱动程序)的代码审查、合并和维护。他们拥有对特定代码区域的最终决定权。
顶级维护者 — 如 Linus Torvalds,负责整个内核树的最终合并决策。
贡献趋势
内核每年接收数万个补丁。顶级贡献组织包括 Intel、Red Hat、SUSE、Google、AMD 和 ARM。贡献不仅来自公司,也来自独立开发者和学术机构。
源代码树的规模
Linux 内核源代码树现在超过 4000 万行代码,包括:
- 数千个驱动程序
- 数十个文件系统
- 完整的网络协议栈
- 多架构支持(x86、ARM、RISC-V 等)
- 虚拟化、安全、实时等子系统
子系统补丁分布
驱动程序子系统接收最多的补丁,其次是架构特定代码、网络栈和文件系统。这反映了内核的主要工作——支持不断增长的硬件生态系统。
内核开发的独特之处
内核开发与用户空间开发有显著不同:
- 没有稳定的内部 API——任何内部函数都可能在版本之间改变
- 必须考虑并发、中断上下文和内存限制
- 代码必须在多种架构和配置上工作
- 提交过程严格,需要经过多轮审查
成为内核开发者不需要天才,但需要耐心、细致和对系统编程的热情。内核社区欢迎新贡献者,并提供了详细的文档和指南来帮助入门。