Linux 内核黑客不可靠指南

作者:

Rusty Russell

简介

欢迎,亲爱的读者,来到 Rusty 极其不可靠的 Linux 内核黑客指南。本文档描述了内核代码的常用例程和一般要求:其目标是为有经验的 C 程序员提供 Linux 内核开发的入门读物。我避开了实现细节:那是代码的任务,而且我忽略了大片有用的例程。

在阅读本文之前,请理解我从未想过要写这篇文档,因为我严重资历不足,但我一直想读到它,而这是唯一的办法。我希望它能成长为一个包含最佳实践、常见起点和随机信息的汇编。

角色

在任何时候,系统中的每个 CPU 都可以处于:

  • 不与任何进程关联,正在处理硬件中断;

  • 不与任何进程关联,正在处理软中断(softirq)或 tasklet;

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

  • 在用户空间运行一个进程。

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

我们将看到用户上下文可以屏蔽中断的几种方法,从而变得真正不可抢占。

用户上下文

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

注意

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

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

警告

请注意,如果你禁用了抢占或软中断(见下文),in_interrupt() 将返回误报(false positive)。

硬件中断 (Hard IRQs)

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

你可以通过 in_hardirq() 返回 true 来判断你正处于硬件中断中。

警告

请注意,如果中断被禁用(见下文),这将返回误报。

软件中断上下文:Softirqs 和 Tasklets

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

许多真实的中断处理工作都是在这里完成的。在向 SMP 过渡的早期,只有“底半部”(BHs),它们没有利用多 CPU 的优势。在我们不再使用火柴棒和鼻涕做的发条计算机后不久,我们就放弃了这个限制,转向了“softirqs”。

include/linux/interrupt.h 列出了不同的 softirqs。一个非常重要的 softirq 是定时器 softirq (include/linux/timer.h):你可以注册让它在给定的时间长度后为你调用函数。

Softirqs 处理起来往往很痛苦,因为同一个 softirq 会在多个 CPU 上同时运行。出于这个原因,tasklets (include/linux/interrupt.h) 被更频繁地使用:它们是可动态注册的(意味着你可以拥有任意数量),而且它们还保证任何 tasklet 在任何时候都只能在一个 CPU 上运行,尽管不同的 tasklet 可以同时运行。

警告

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

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

警告

请注意,如果持有了 底半部锁 (bottom half lock),这将返回误报。

一些基本规则

无内存保护

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

无浮点运算或 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.h, include/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 系统调用的座右铭是“提供机制而非策略”。

死锁处方

你不能调用任何可能睡眠的例程,除非:

  • 你处于用户上下文中。

  • 你不拥有任何自旋锁(spinlocks)。

  • 你开启了中断(实际上,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 第 6 版源码在其 printf 函数顶部有一行注释:“Printf 不应该用于闲聊”。你应该遵循这个建议。

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

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

[会睡眠]

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

[可能睡眠:见下文]

这些例程用于动态请求指针对齐的内存块,就像用户空间中的 malloc 和 free 所做的那样,但 kmalloc() 需要一个额外的标志参数。重要值包括:

GFP_KERNEL

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

GFP_ATOMIC

不睡眠。不如 GFP_KERNEL 可靠,但可以从中断上下文中调用。你 **真的** 应该有一个良好的内存不足(out-of-memory)错误处理策略。

GFP_DMA

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

如果你看到“在无效上下文调用睡眠函数”的警告消息,那么可能是你在中断上下文中调用了睡眠分配函数而没有使用 GFP_ATOMIC。你真的应该修复它。赶紧行动,别磨蹭。

如果你正在分配至少 PAGE_SIZE (asm/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(),它接受一个指向给定类型的指针,并返回转换后的值。另一个变体是“原地 (in-situ)”系列,如 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 上禁用软中断,并恢复它们。它们是可重入的;如果之前软中断被禁用了,在调用这对函数后它们仍将被禁用。它们防止 softirqs 和 tasklets 在当前 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()

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

等待队列 include/linux/wait.h

[会睡眠]

等待队列用于在某个条件为真时等待有人唤醒你。必须小心使用它们以确保没有竞争条件。你声明一个 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)。

注意这些函数比普通算术运算慢,因此不应不必要地使用。

第二类原子操作是对 unsigned long 的原子位操作,定义在 include/linux/bitops.h 中。这些操作通常接受一个指向位模式的指针和一个位数: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() 导出的符号只能由 MODULE_LICENSE() 指定了 GPLv2 兼容许可的模块看到。它意味着该函数被认为是内部实现问题,而不是真正的接口。然而,一些维护者和开发者在添加任何新的 API 或功能时,可能会要求使用 EXPORT_SYMBOL_GPL()

EXPORT_SYMBOL_NS()

定义于 include/linux/export.h

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

EXPORT_SYMBOL_NS_GPL()

定义于 include/linux/export.h

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

例程和惯例

双向链表 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 info 页的“C 扩展”部分 —— 是的,真的是 info 页,man 页只是 info 中内容的简短摘要)。

  • 内联函数 (Inline functions)

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

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

  • typeof

  • 零长度数组

  • 宏可变参数

  • 对 void 指针进行算术运算

  • 非恒定初始化器

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

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

  • __builtin_constant_p()

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

C++

在内核中使用 C++ 通常是个坏主意,因为内核不提供必要的运行时环境,而且包含文件也没有为此进行测试。虽然仍有可能,但不推荐。如果你真的想这样做,至少忘掉异常(exceptions)吧。

#if

通常认为在头文件(或 .c 文件的顶部)使用宏来抽象功能,比在整个源代码中使用 '#if' 预处理器语句更整洁。

将你的代码放入内核

为了让你的东西能够正式被接纳,或者仅仅是为了制作一个整洁的补丁,还有一些行政工作要做:

  • 弄清楚你一直在修改的代码的所有者是谁。查看源文件顶部、MAINTAINERS 文件内部,最后是在 CREDITS 文件中。你应该与这些人协调,以确保你没有在做重复劳动,或者尝试一些已经被拒绝过的东西。

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

  • 通常你希望为你的内核 hack 提供一个配置选项。编辑相应目录下的 Kconfig。Config 语言通过剪切和粘贴使用很简单,在 Kconfig 语言 中有完整的文档。

    在对选项的描述中,确保兼顾专家用户和对你的功能一无所知的用户。在此处提及不兼容性和问题。描述的结尾 **一定要** 加上“if in doubt, say N”(或者偶尔是 'Y');这是给那些根本不知道你在说什么的人看的。

  • 编辑 Makefile:CONFIG 变量在这里导出,所以你通常只需添加一行 “obj-$(CONFIG_xxx) += xxx.o”。语法记录在 Linux 内核 Makefiles 中。

  • 如果你认为自己所做的值得注意(通常超出了单个文件),请将你自己放入 CREDITS(无论如何,你的名字都应该在源文件的顶部)。出现在 MAINTAINERS 意味着你希望在子系统发生更改时被征求意见,并获知错误信息;它意味着对代码某些部分的长期承诺,而不仅仅是路过。

  • 最后,别忘了阅读 提交补丁:将代码提交到内核的基础指南

内核咒语 (Cantrips)

浏览源码时的一些收藏。欢迎添加更多内容到此列表。

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 检查并补充了 Configure 部分。Telsa Gwynne 教我 DocBook。