Linux 内核破解不可靠指南

作者:

Rusty Russell

简介

欢迎,各位读者,来到 Rusty 的 Linux 内核破解极其不可靠指南。本文档描述了内核代码的常见例程和一般要求:其目标是为有经验的 C 程序员提供 Linux 内核开发的入门知识。我避免了实现细节:那是代码的作用,并且我忽略了整套有用的例程。

在您阅读本文之前,请理解我从未想过要编写本文档,因为我严重不符合资格,但我一直想阅读它,而这是唯一的方法。我希望它会发展成为最佳实践、常见起点和随机信息的纲要。

参与者

在任何时候,系统中每个 CPU 都可能是

  • 未与任何进程关联,服务硬件中断;

  • 未与任何进程关联,服务软中断或 tasklet;

  • 在内核空间中运行,与进程关联(用户上下文);

  • 在用户空间中运行进程。

这些之间存在顺序。底部的两个可以互相抢占,但其上是一个严格的层次结构:每个只能被其上的抢占。例如,当软中断在 CPU 上运行时,没有其他软中断会抢占它,但硬件中断可以。但是,系统中的任何其他 CPU 都是独立执行的。

我们将看到许多用户上下文阻止中断、变得真正不可抢占的方法。

用户上下文

当您从系统调用或其他陷阱进入时,就是用户上下文:与用户空间一样,您可能会被更重要的任务和中断抢占。您可以通过调用 schedule() 来休眠。

注意

在模块加载和卸载时,以及在块设备层上的操作时,您始终处于用户上下文中。

在用户上下文中,current 指针(指示我们当前正在执行的任务)有效,并且 in_interrupt()include/linux/preempt.h)为 false。

警告

请注意,如果您禁用了抢占或软中断(见下文),in_interrupt() 将返回一个错误的肯定结果。

硬件中断(硬 IRQ)

定时器滴答声、网卡和键盘是实时硬件的示例,它们随时产生中断。内核运行中断处理程序,为硬件提供服务。内核保证此处理程序永远不会重新进入:如果同一中断到达,它将被排队(或丢弃)。因为它禁用中断,所以此处理程序必须很快:通常它只是确认中断,标记一个“软件中断”以供执行并退出。

您可以知道您处于硬件中断中,因为 in_hardirq() 返回 true。

警告

请注意,如果禁用中断(见下文),这将返回一个错误的肯定结果。

软件中断上下文:软中断和 Tasklet

每当系统调用即将返回到用户空间,或者硬件中断处理程序退出时,都会运行任何标记为挂起的“软件中断”(通常由硬件中断标记)(kernel/softirq.c)。

大部分实际的中断处理工作都是在这里完成的。在过渡到 SMP 的早期,只有“底层半部”(BH),它们没有利用多个 CPU。在我们从用火柴棍和鼻涕制成的绕线式计算机切换过来后不久,我们放弃了这种限制,并切换到“软中断”。

include/linux/interrupt.h 列出了不同的软中断。一个非常重要的软中断是定时器软中断(include/linux/timer.h):您可以注册在给定的时间内让它为您调用函数。

软中断通常很难处理,因为相同的软中断会在多个 CPU 上同时运行。因此,更常用 tasklet(include/linux/interrupt.h):它们是动态可注册的(这意味着您可以拥有任意多个),并且它们还保证任何 tasklet 在任何时候都只在一个 CPU 上运行,尽管不同的 tasklet 可以同时运行。

警告

“tasklet”这个名称具有误导性:它们与“任务”没有任何关系。

您可以使用 in_softirq() 宏(include/linux/preempt.h)来判断您是否处于软中断(或 tasklet)中。

警告

请注意,如果持有 底层半部锁,这将返回一个错误的肯定结果。

一些基本规则

没有内存保护

如果您破坏了内存,无论是在用户上下文还是中断上下文中,整台机器都将崩溃。您确定您不能在用户空间中执行您想要的操作吗?

没有浮点或 MMX

FPU 上下文未保存;即使在用户上下文中,FPU 状态也可能与当前进程不对应:您会弄乱某些用户进程的 FPU 状态。如果您真的想这样做,您必须显式保存/恢复完整的 FPU 状态(并避免上下文切换)。这通常是一个坏主意;首先使用定点算术。

严格的堆栈限制

根据配置选项,大多数 32 位架构的内核堆栈约为 3K 到 6K:在大多数 64 位架构上约为 14K,并且经常与中断共享,因此您不能全部使用它。避免深度递归和堆栈上的巨大局部数组(改为动态分配它们)。

Linux 内核是可移植的

让我们保持这种方式。您的代码应该是 64 位干净的,并且与字节顺序无关。您还应该最大限度地减少 CPU 特定的内容,例如,内联汇编应进行干净的封装并最小化,以方便移植。通常,它应限制为内核树中与体系结构相关的部分。

ioctls:不编写新的系统调用

系统调用通常如下所示

asmlinkage long sys_mycall(int arg)
{
        return 0;
}

首先,在大多数情况下,您不想创建新的系统调用。您创建一个字符设备并为其实现适当的 ioctl。这比系统调用灵活得多,不必在每个架构的 include/asm/unistd.harch/kernel/entry.S 文件中输入,并且更可能被 Linus 接受。

如果您的所有例程只是读取或写入一些参数,请考虑改为实现 sysfs() 接口。

在 ioctl 内部,您处于进程的用户上下文中。当发生错误时,您返回一个取反的 errno(参见 include/uapi/asm-generic/errno-base.hinclude/uapi/asm-generic/errno.hinclude/linux/errno.h),否则返回 0。

在您休眠后,您应该检查是否发生了信号:Unix/Linux 处理信号的方式是暂时以 -ERESTARTSYS 错误退出系统调用。系统调用入口代码将切换回用户上下文,处理信号处理程序,然后重新启动您的系统调用(除非用户禁用了此功能)。因此,您应该准备好处理重新启动,例如,如果您正在操作某些数据结构。

if (signal_pending(current))
        return -ERESTARTSYS;

如果您正在进行较长的计算:首先考虑用户空间。如果您真的想在内核中进行计算,您应该定期检查是否需要放弃 CPU(请记住,每个 CPU 都有协同多任务处理)。习语

cond_resched(); /* Will sleep */

关于接口设计的简短说明:UNIX 系统调用的座右铭是“提供机制而不是策略”。

死锁的处方

您不能调用任何可能会休眠的例程,除非

  • 您处于用户上下文中。

  • 您不拥有任何自旋锁。

  • 您启用了中断(实际上,Andi Kleen 说调度代码会为您启用它们,但这可能不是您想要的)。

请注意,某些函数可能会隐式休眠:常见的函数是用户空间访问函数 (*_user) 和没有 GFP_ATOMIC 的内存分配函数。

您应该始终启用 CONFIG_DEBUG_ATOMIC_SLEEP 来编译您的内核,如果违反这些规则,它会发出警告。如果您确实违反了规则,最终会锁定您的计算机。

真的。

常用例程

printk()

include/linux/printk.h 中定义

printk() 将内核消息发送到控制台、dmesg 和 syslog 守护进程。它对于调试和报告错误很有用,并且可以在中断上下文中使用,但请谨慎使用:如果机器的控制台被 printk 消息淹没,则无法使用。它使用与 ANSI C printf 大部分兼容的格式字符串,并使用 C 字符串连接为其提供第一个“优先级”参数。

printk(KERN_INFO "i = %u\n", i);

请参阅 include/linux/kern_levels.h;获取其他 KERN_ 值;这些值被 syslog 解释为级别。特殊情况:要打印 IP 地址,请使用

__be32 ipaddress;
printk(KERN_INFO "my ip: %pI4\n", &ipaddress);

printk() 内部使用 1K 缓冲区,并且不会捕获溢出。请确保它足够大。

注意

当你开始在你的用户程序中将 printf 拼写为 printk 时,你就知道你是一个真正的内核黑客了:)

注意

另一个附注:最初的 Unix Version 6 源码在其 printf 函数的顶部有一条注释:“Printf 不应用于闲聊”。你应该遵循这个建议。

copy_to_user() / copy_from_user() / get_user() / put_user()

定义在 include/linux/uaccess.h / asm/uaccess.h

[SLEEPS]

put_user()get_user() 用于从用户空间获取和放置单个值(例如 int、char 或 long)。永远不应简单地解引用指向用户空间的指针:应该使用这些例程复制数据。两者都返回 -EFAULT 或 0。

copy_to_user()copy_from_user() 更通用:它们将任意数量的数据复制到用户空间或从用户空间复制。

警告

put_user()get_user() 不同,它们返回未复制的数据量(即 0 仍然表示成功)。

[是的,这个令人反感的接口让我感到畏缩。 争论每年都会出现一次左右。 --RR.]

这些函数可能会隐式休眠。 这绝不应该在用户上下文之外(没有意义)、禁用中断或持有自旋锁的情况下调用。

kmalloc()/kfree()

定义在 include/linux/slab.h

[MAY SLEEP: SEE BELOW]

这些例程用于动态请求指针对齐的内存块,就像用户空间中的 malloc 和 free 一样,但 kmalloc() 采用一个额外的标志字。 重要值

GFP_KERNEL

可能休眠并交换以释放内存。仅允许在用户上下文中使用,但它是分配内存最可靠的方法。

GFP_ATOMIC

不休眠。 不如 GFP_KERNEL 可靠,但可以从中断上下文调用。 你应该**真的**有一个很好的内存不足错误处理策略。

GFP_DMA

分配低于 16MB 的 ISA DMA。如果你不知道那是什么,你就不需要它。 非常不可靠。

如果你看到从无效上下文调用的休眠函数警告消息,那么可能你在没有 GFP_ATOMIC 的情况下从中断上下文中调用了休眠分配函数。 你应该真正修复它。 跑起来,不要走。

如果要分配至少 PAGE_SIZEasm/page.hasm/page_types.h)字节,请考虑使用 __get_free_pages()include/linux/gfp.h)。 它采用一个 order 参数(0 表示页面大小,1 表示双倍页面,2 表示四个页面等)和与上述相同的内存优先级标志字。

如果要分配超过一个页面的字节数,可以使用 vmalloc()。 它将在内核映射中分配虚拟内存。 此块在物理内存中不连续,但 MMU 使其看起来像对你来说是连续的(因此它只会对 CPU 看起来是连续的,而不是对外部设备驱动程序)。 如果你真的需要为某些奇怪的设备提供大的物理连续内存,你就会遇到问题:在 Linux 中它支持不佳,因为在运行的内核中一段时间后,内存碎片化会使其变得困难。最好的方法是通过 alloc_bootmem() 例程在引导过程的早期分配块。

在发明你自己的常用对象缓存之前,请考虑使用 include/linux/slab.h 中的 slab 缓存

current

定义在 include/asm/current.h

这个全局变量(实际上是一个宏)包含一个指向当前任务结构的指针,因此仅在用户上下文中有效。例如,当进程进行系统调用时,这将指向调用进程的任务结构。它在中断上下文中**不是 NULL**。

mdelay()/udelay()

定义在 include/asm/delay.h / include/linux/delay.h

udelay()ndelay() 函数可用于短暂暂停。不要在它们中使用较大的值,因为你可能会溢出 - 辅助函数 mdelay() 在这里很有用,或者考虑使用 msleep()

cpu_to_be32()/be32_to_cpu()/cpu_to_le32()/le32_to_cpu()

定义在 include/asm/byteorder.h

cpu_to_be32() 系列(其中“32”可以替换为 64 或 16,“be”可以替换为“le”)是在内核中进行字节序转换的通用方法:它们返回转换后的值。所有变体都提供反向转换:be32_to_cpu() 等。

这些函数有两种主要变体:指针变体,例如 cpu_to_be32p(),它接受指向给定类型的指针,并返回转换后的值。另一个变体是“就地”系列,例如 cpu_to_be32s(),它转换指针所引用的值,并返回 void。

local_irq_save()/local_irq_restore()

定义在 include/linux/irqflags.h

这些例程禁用本地 CPU 上的硬中断,然后恢复它们。它们是可重入的;将之前的状态保存在它们的一个 unsigned long flags 参数中。如果你知道中断已启用,你可以简单地使用 local_irq_disable()local_irq_enable()

local_bh_disable()/local_bh_enable()

定义在 include/linux/bottom_half.h

这些例程禁用本地 CPU 上的软中断,然后恢复它们。它们是可重入的;如果之前禁用了软中断,则在调用此函数对后,它们仍将被禁用。它们阻止软中断和 tasklet 在当前 CPU 上运行。

smp_processor_id()

定义在 include/linux/smp.h

get_cpu() 函数会禁用抢占(因此您不会突然被移动到另一个 CPU),并返回当前处理器编号,介于 0 和 NR_CPUS 之间。请注意,CPU 编号不一定是连续的。当您完成操作后,需要使用 put_cpu() 再次返回。

如果您知道您不会被另一个任务抢占(例如,您处于中断上下文中,或者已禁用抢占),则可以使用 smp_processor_id()。

__init/__exit/__initdata

定义在 include/linux/init.h

启动后,内核会释放一个特殊的部分;标记为 __init 的函数和标记为 __initdata 的数据结构在启动完成后会被丢弃:类似地,模块在初始化后也会丢弃此内存。__exit 用于声明一个仅在退出时才需要的函数:如果此文件不是作为模块编译的,则该函数将被丢弃。请参阅头文件了解用法。请注意,将标记为 __init 的函数导出到模块,并使用 EXPORT_SYMBOL()EXPORT_SYMBOL_GPL() 是没有意义的,这会造成破坏。

__initcall()/module_init()

定义在 include/linux/init.h / include/linux/module.h

内核的许多部分作为模块(内核的动态加载部分)运行良好。使用 module_init()module_exit() 宏,可以轻松编写无需 #ifdefs 的代码,这些代码既可以作为模块运行,也可以构建到内核中。

module_init() 宏定义了在模块插入时(如果文件是作为模块编译的)或在启动时要调用的函数:如果该文件不是作为模块编译的,则 module_init() 宏等同于 __initcall(),它通过链接器魔术确保该函数在启动时被调用。

该函数可以返回一个负错误号,以导致模块加载失败(不幸的是,如果该模块被编译到内核中,则此操作无效)。此函数在启用了中断的用户上下文中调用,因此它可以休眠。

module_exit()

定义在 include/linux/module.h

此宏定义了在模块移除时要调用的函数(或者在文件编译到内核的情况下永远不会调用)。仅当模块使用计数达到零时才会调用。此函数也可以休眠,但不能失败:在返回之前,必须清理所有内容。

请注意,此宏是可选的:如果不存在,则您的模块将不可移除(除了使用 'rmmod -f')。

try_module_get()/module_put()

定义在 include/linux/module.h

这些操作会操作模块的使用计数,以防止移除(如果另一个模块使用了其导出的符号之一,则模块也不能被移除:请参见下文)。在调用模块代码之前,您应该在该模块上调用 try_module_get():如果它失败,则该模块正在被移除,您应该像它不存在一样操作。否则,您可以安全地进入该模块,并在完成操作后调用 module_put()

大多数可注册的结构都有一个所有者字段,例如 struct file_operations 结构。将此字段设置为宏 THIS_MODULE

等待队列 include/linux/wait.h

[SLEEPS]

当某个条件为真时,等待队列用于等待某人唤醒您。必须小心使用它们,以确保没有竞争条件。您声明一个 wait_queue_head_t,然后想要等待该条件的进程声明一个引用自身的 wait_queue_entry_t,并将其放置在队列中。

声明

您可以使用 DECLARE_WAIT_QUEUE_HEAD() 宏声明 wait_queue_head_t,或者在初始化代码中使用 init_waitqueue_head() 例程。

排队

将自己放入等待队列相当复杂,因为您必须在检查条件之前将自己放入队列中。有一个宏可以做到这一点:wait_event_interruptible() (include/linux/wait.h) 第一个参数是等待队列头,第二个参数是求值的表达式;当此表达式为真时,该宏返回 0,如果收到信号,则返回 -ERESTARTSYSwait_event() 版本会忽略信号。

唤醒排队的任务

调用 wake_up() (include/linux/wait.h),它将唤醒队列中的每个进程。例外情况是,如果其中一个进程设置了 TASK_EXCLUSIVE,在这种情况下,队列的其余部分将不会被唤醒。在同一头文件中还有此基本函数的其他变体可用。

原子操作

某些操作保证在所有平台上都是原子的。第一类操作作用于 atomic_t (include/asm/atomic.h);它包含一个有符号整数(至少 32 位长),您必须使用这些函数来操作或读取 atomic_t 变量。atomic_read()atomic_set() 获取和设置计数器,atomic_add(), atomic_sub(), atomic_inc(), atomic_dec(), 和 atomic_dec_and_test() (如果递减到零则返回 true)。

是的。如果原子变量为零,则返回 true(即 != 0)。

请注意,这些函数比正常的算术运算慢,因此不应不必要地使用它们。

第二类原子操作是在 include/linux/bitops.h 中定义的针对 unsigned long 类型的原子位操作。这些操作通常接受一个指向位模式的指针和一个位号:0 表示最低有效位。set_bit()clear_bit()change_bit() 分别设置、清除和翻转给定的位。test_and_set_bit()test_and_clear_bit()test_and_change_bit() 做同样的事情,但如果该位之前被设置,则返回 true;这些对于原子地设置标志特别有用。

可以调用这些操作,使用大于 BITS_PER_LONG 的位索引。然而,在大端平台上,结果行为会很奇怪,所以最好不要这样做。

符号

在内核内部,正常的链接规则适用(即,除非一个符号使用 static 关键字声明为文件作用域,否则它可以在内核的任何地方使用)。然而,对于模块,会保留一个特殊的导出符号表,它限制了内核的入口点。模块也可以导出符号。

EXPORT_SYMBOL()

定义在 include/linux/export.h

这是导出符号的经典方法:动态加载的模块可以像正常情况一样使用该符号。

EXPORT_SYMBOL_GPL()

定义在 include/linux/export.h

EXPORT_SYMBOL() 类似,不同之处在于 EXPORT_SYMBOL_GPL() 导出的符号只能被具有指定 GPLv2 兼容许可证的 MODULE_LICENSE() 的模块看到。这意味着该函数被认为是内部实现问题,而不是真正的接口。然而,一些维护人员和开发人员在添加任何新的 API 或功能时可能会要求使用 EXPORT_SYMBOL_GPL()。

EXPORT_SYMBOL_NS()

定义在 include/linux/export.h

这是 EXPORT_SYMBOL() 的变体,它允许指定符号命名空间。符号命名空间在 符号命名空间 中有文档说明。

EXPORT_SYMBOL_NS_GPL()

定义在 include/linux/export.h

这是 EXPORT_SYMBOL_GPL() 的变体,它允许指定符号命名空间。符号命名空间在 符号命名空间 中有文档说明。

例程和约定

双向链表 include/linux/list.h

内核头文件中曾经有三组链表例程,但这一组是最终的赢家。如果您没有对单链表的特别迫切的需求,这是一个不错的选择。

特别是,list_for_each_entry() 很有用。

返回约定

对于在用户上下文中调用的代码,通常会违反 C 约定,返回 0 表示成功,返回负的错误号(例如,-EFAULT)表示失败。这起初可能不直观,但在内核中相当普遍。

使用 ERR_PTR() (include/linux/err.h) 将负的错误号编码为指针,并使用 IS_ERR()PTR_ERR() 将其取回:避免了单独的指针参数用于错误号。这很糟糕,但以一种好的方式。

破坏编译

Linus 和其他开发人员有时会在开发内核中更改函数或结构名称;这样做不仅仅是为了让每个人保持警惕:它反映了根本性的变化(例如,不再能够在启用中断的情况下调用,或者进行额外的检查,或者不进行以前捕获的检查)。通常,这会伴随着向相应的内核开发邮件列表发送相当完整的说明;搜索存档。简单地对文件进行全局替换通常会使情况**更糟**。

初始化结构成员

初始化结构的首选方法是使用 ISO C99 定义的指定初始化器,例如

static struct block_device_operations opt_fops = {
        .open               = opt_open,
        .release            = opt_release,
        .ioctl              = opt_ioctl,
        .check_media_change = opt_media_change,
};

这使得易于进行 grep 查找,并明确设置了哪些结构字段。您应该这样做,因为它看起来很酷。

GNU 扩展

Linux 内核中明确允许使用 GNU 扩展。请注意,由于缺乏普遍使用,一些更复杂的扩展没有得到很好的支持,但以下扩展被认为是标准的(有关更多详细信息,请参阅 GCC 信息页面的“C 扩展”部分 - 是的,真的是信息页面,手册页只是信息内容的简短摘要)。

  • 内联函数

  • 语句表达式(即 ({ 和 }) 构造)。

  • 声明函数/变量/类型的属性 (__attribute__)

  • typeof

  • 零长度数组

  • 宏可变参数

  • 对 void 指针进行算术运算

  • 非常量初始化器

  • 汇编器指令(不在 arch/ 和 include/asm/ 之外)

  • 函数名作为字符串 (__func__)。

  • __builtin_constant_p()

在内核中使用 long long 时要小心,gcc 为其生成的代码很糟糕,更糟糕的是:除法和乘法在 i386 上不起作用,因为内核环境中缺少它的 GCC 运行时函数。

C++

在内核中使用 C++ 通常不是一个好主意,因为内核没有提供必要的运行时环境,并且没有针对 C++ 测试包含文件。它仍然是可能的,但不推荐。如果您真的想这样做,至少请忘记异常。

#if

通常认为在头文件(或 .c 文件的顶部)中使用宏来抽象函数,而不是在整个源代码中使用 `#if` 预处理器语句,这样做更简洁。

将您的内容放入内核

为了让您的内容符合官方包含的要求,甚至为了创建一个整洁的补丁,还需要完成一些管理工作

  • 找出您一直在修改的代码的所有者。查看源文件的顶部、MAINTAINERS 文件内部,最后查看 CREDITS 文件。您应该与这些人协调,以确保您没有重复工作,或者尝试已经被拒绝的事情。

    确保将您的姓名和电子邮件地址放在您创建或大幅修改的任何文件的顶部。这是人们在发现错误时或当**他们**想要进行更改时首先会查看的地方。

  • 通常,您需要为您的内核 hack 添加一个配置选项。在相应的目录中编辑 Kconfig。Config 语言通过剪切和粘贴很容易使用,并且在 Documentation/kbuild/kconfig-language.rst 中有完整的文档。

    在您对该选项的描述中,确保您同时面向专家用户和对您的功能一无所知的用户。在此处提及不兼容性和问题。**绝对**以 “如果对此有疑问,请选择 N” (或者偶尔选择 “Y”)结束您的描述;这是为了那些不知道您在说什么的人。

  • 编辑 Makefile:CONFIG 变量在这里导出,所以您通常只需添加一个 “obj-$(CONFIG_xxx) += xxx.o” 行即可。该语法在 Documentation/kbuild/makefiles.rst 中有文档说明。

  • 如果您认为您所做的工作值得注意(通常不仅仅是一个文件),请将您自己添加到 CREDITS 中(您的名字无论如何都应该在源文件的顶部)。MAINTAINERS 表示您希望在对子系统进行更改时得到咨询,并了解错误;这表明对代码的某些部分有超出 passing 的承诺。

  • 最后,别忘了阅读 Documentation/process/submitting-patches.rst

内核小技巧

一些浏览源代码时发现的有趣之处。欢迎添加到此列表中。

arch/x86/include/asm/delay.h:

#define ndelay(n) (__builtin_constant_p(n) ? \
        ((n) > 20000 ? __bad_ndelay() : __const_udelay((n) * 5ul)) : \
        __ndelay(n))

include/linux/fs.h:

/*
 * Kernel pointers have redundant information, so we can use a
 * scheme where we can return either an error code or a dentry
 * pointer with the same return value.
 *
 * This should be a per-architecture thing, to allow different
 * error and pointer decisions.
 */
 #define ERR_PTR(err)    ((void *)((long)(err)))
 #define PTR_ERR(ptr)    ((long)(ptr))
 #define IS_ERR(ptr)     ((unsigned long)(ptr) > (unsigned long)(-1000))

arch/x86/include/asm/uaccess_32.h:

#define copy_to_user(to,from,n)                         \
        (__builtin_constant_p(n) ?                      \
         __constant_copy_to_user((to),(from),(n)) :     \
         __generic_copy_to_user((to),(from),(n)))

arch/sparc/kernel/head.S:

/*
 * Sun people can't spell worth damn. "compatability" indeed.
 * At least we *know* we can't spell, and use a spell-checker.
 */

/* Uh, actually Linus it is I who cannot spell. Too much murky
 * Sparc assembly will do this to ya.
 */
C_LABEL(cputypvar):
        .asciz "compatibility"

/* Tested on SS-5, SS-10. Probably someone at Sun applied a spell-checker. */
        .align 4
C_LABEL(cputypvar_sun4m):
        .asciz "compatible"

arch/sparc/lib/checksum.S:

/* Sun, you just can't beat me, you just can't.  Stop trying,
 * give up.  I'm serious, I am going to kick the living shit
 * out of you, game over, lights out.
 */

感谢

感谢 Andi Kleen 的想法,解答我的问题,修正我的错误,填充内容等等。感谢 Philipp Rumpf 提供了更多的拼写和清晰度修复,以及一些非常棒的非显而易见的观点。感谢 Werner Almesberger 提供了关于 disable_irq() 的精彩总结, Jes Sorensen 和 Andrea Arcangeli 添加了注意事项。感谢 Michael Elizabeth Chastain 检查并补充了“配置”部分。感谢 Telsa Gwynne 教我 DocBook。