Linux内核破解不可靠指南¶
- 作者:
Rusty Russell
简介¶
欢迎,各位读者,来到Rusty的Linux内核破解极其不可靠指南。本文档描述了内核代码的常用例程和一般要求:其目标是为经验丰富的C程序员提供Linux内核开发的入门知识。我避免了实现细节:这是代码的作用,并且我忽略了整套有用的例程。
在阅读本文之前,请理解我从未想过要编写本文档,因为我完全不具备资格,但我一直想阅读它,而这是唯一的途径。我希望它能发展成为最佳实践,常用起点和随机信息的纲要。
参与者¶
在任何时候,系统中的每个CPU都可以
未与任何进程关联,正在处理硬件中断;
未与任何进程关联,正在处理软中断或tasklet;
在内核空间中运行,与进程关联(用户上下文);
在用户空间中运行进程。
这些之间存在一种顺序。最下面的两个可以抢占彼此,但除此之外是一个严格的层次结构:每个只能被上面的抢占。例如,当一个softirq在CPU上运行时,没有其他的softirq会抢占它,但是硬件中断可以。但是,系统中的任何其他CPU都可以独立执行。
我们将看到许多用户上下文可以阻止中断的方式,以变得真正不可抢占。
用户上下文¶
用户上下文是指从系统调用或其他陷阱进入的情况:与用户空间一样,你可能会被更重要的任务和中断抢占。你可以通过调用schedule()
来睡眠。
注意
你始终处于模块加载和卸载时的用户上下文中,以及在块设备层上的操作中。
在用户上下文中,current
指针(指示我们当前正在执行的任务)是有效的,并且in_interrupt()
(include/linux/preempt.h
)为false。
警告
请注意,如果你禁用了抢占或软中断(见下文),in_interrupt()
将返回假阳性。
硬件中断(Hard IRQ)¶
定时器滴答、网卡和键盘都是真实的硬件示例,它们会随时产生中断。内核运行中断处理程序,为硬件提供服务。内核保证此处理程序永远不会被重新进入:如果同一中断到达,它将被排队(或丢弃)。因为它禁用了中断,所以此处理程序必须快速:通常它只是确认中断,标记一个“软件中断”以供执行并退出。
你可以判断你是否处于硬件中断中,因为in_hardirq()返回true。
警告
请注意,如果禁用了中断(见下文),这将返回假阳性。
软件中断上下文:Softirq和Tasklet¶
每当系统调用即将返回到用户空间,或者硬件中断处理程序退出时,任何标记为挂起的“软件中断”(通常由硬件中断标记)都会运行(kernel/softirq.c
)。
大部分实际的中断处理工作都在这里完成。在早期过渡到SMP时,只有“bottom halves”(BHs),它们没有利用多个CPU的优势。在我们从用火柴棍和鼻涕制成的发条计算机切换后不久,我们放弃了此限制并切换到“softirqs”。
include/linux/interrupt.h
列出了不同的softirqs。一个非常重要的softirq是定时器softirq(include/linux/timer.h
):你可以注册让它在给定的时间长度内为你调用函数。
Softirqs通常难以处理,因为相同的softirq会同时在多个CPU上运行。因此,更常用的是tasklet(include/linux/interrupt.h
):它们是动态可注册的(意味着你可以拥有任意多个),并且它们还保证任何tasklet一次只会在一个CPU上运行,尽管不同的tasklet可以同时运行。
警告
名称“tasklet”具有误导性:它们与“任务”无关。
你可以使用in_softirq()
宏(include/linux/preempt.h
)判断你是否处于softirq(或tasklet)中。
警告
请注意,如果持有bottom half锁,这将返回假阳性。
一些基本规则¶
- 没有内存保护
如果你破坏了内存,无论是在用户上下文还是中断上下文中,整个机器都会崩溃。你确定你不能在用户空间中完成你想要做的事情吗?
- 没有浮点或MMX
FPU上下文未保存;即使在用户上下文中,FPU状态也可能与当前进程不符:你可能会弄乱某些用户进程的FPU状态。如果你真的想这样做,你必须显式保存/恢复完整的FPU状态(并避免上下文切换)。这通常是个坏主意;首先使用定点算术。
- 严格的堆栈限制
根据配置选项,对于大多数32位体系结构,内核堆栈约为3K到6K:在大多数64位架构上约为14K,并且通常与中断共享,因此你不能全部使用它。避免深度递归和堆栈上的大型局部数组(而是动态分配它们)。
- Linux内核是可移植的
让我们保持这种状态。你的代码应为64位clean且与字节序无关。你还应该尽量减少CPU特定的东西,例如,内联汇编应干净地封装并尽量减少,以简化移植。通常,它应限制在内核树的体系结构相关部分。
ioctl:不编写新的系统调用¶
系统调用通常如下所示
asmlinkage long sys_mycall(int arg)
{
return 0;
}
首先,在大多数情况下,你不想创建新的系统调用。你创建一个字符设备并为其实现适当的ioctl。这比系统调用灵活得多,不必在每个体系结构的include/asm/unistd.h
和arch/kernel/entry.S
文件中输入,并且更有可能被Linus接受。
如果你的所有例程只是读取或写入一些参数,请考虑实现一个sysfs()
接口。
在ioctl内部,你处于进程的用户上下文中。发生错误时,你返回一个取反的errno(参见include/uapi/asm-generic/errno-base.h
,include/uapi/asm-generic/errno.h
和include/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
中
[睡眠]
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
可靠,但可以从中断上下文中调用。你真的应该有一个良好的内存不足错误处理策略。GFP_DMA
分配低于16MB的ISA DMA。如果你不知道那是什么,则不需要它。非常不可靠。
如果你看到从无效上下文中调用的睡眠函数警告消息,那么你可能从没有GFP_ATOMIC
的中断上下文中调用了睡眠分配函数。你真的应该修复它。快跑,别走。
如果你至少分配PAGE_SIZE
(asm/page.h
或asm/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上的软中断,并恢复它们。它们是可重入的;如果之前禁用了软中断,则在调用这对函数之后,它们仍将被禁用。它们阻止softirq和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()
宏,可以很容易地编写没有 #ifdef 的代码,这些代码可以作为模块或构建到内核中运行。
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
¶
[睡眠]
等待队列用于等待某人在某个条件为真时唤醒您。 必须小心使用它们以确保没有竞争条件。 您声明一个 wait_queue_head_t
,然后想要等待该条件的进程声明一个引用自身的 wait_queue_entry_t
,并将其放入队列中。
声明¶
您可以使用 DECLARE_WAIT_QUEUE_HEAD()
宏或在初始化代码中使用 init_waitqueue_head()
例程来声明一个 wait_queue_head_t
。
排队¶
将自己放入等待队列相当复杂,因为您必须在检查条件之前将自己放入队列中。 有一个宏可以做到这一点: wait_event_interruptible()
(include/linux/wait.h
) 第一个参数是等待队列头,第二个参数是一个被评估的表达式; 当该表达式为真时,该宏返回 0,如果收到信号,则返回 -ERESTARTSYS
。 wait_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()
导出的符号只能被具有指定 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++ 通常不是一个好主意,因为内核没有提供必要的运行时环境,并且未针对它测试包含文件。 这仍然是可能的,但不建议这样做。 如果您真的想这样做,至少忘记异常。
#if¶
通常认为在头文件(或 .c 文件的顶部)中使用宏来抽象函数比在整个源代码中使用 `#if` 预处理器语句更干净。
将您的内容放入内核¶
为了使您的内容成形以便正式包含,甚至为了制作一个整洁的补丁,需要完成管理工作
找出您一直在修改的代码的所有者。 查看源文件顶部、
MAINTAINERS
文件内部,以及最后的CREDITS
文件。 您应该与这些人协调以确保您没有重复工作,或者尝试已经被拒绝的事情。确保将您的姓名和电子邮件地址放在您创建或显着修改的任何文件的顶部。 这是人们发现错误或当他们想要进行更改时会首先查看的地方。
通常您希望为您的内核黑客准备一个配置选项。 编辑相应目录中的
Kconfig
。 通过剪切和粘贴可以轻松使用 Config 语言,并且在Documentation/kbuild/kconfig-language.rst
中有完整的文档。在您对该选项的描述中,请确保同时向专家用户和对您的功能一无所知的用户说明。 在此处提及不兼容性和问题。 绝对以“如果怀疑,请说 N”(或者,偶尔,`Y`)结束您的描述; 这是为那些不知道你在说什么的人准备的。
编辑
Makefile
:CONFIG 变量在此处导出,因此您通常只需添加一行“obj-$(CONFIG_xxx) += xxx.o”。 语法记录在Documentation/kbuild/makefiles.rst
中。如果您认为您所做的事情值得注意,通常超出单个文件(无论如何您的姓名应该在源文件的顶部),请将您自己放入
CREDITS
中。MAINTAINERS
意味着您希望在对子系统进行更改时咨询您,并了解错误; 它意味着对代码的某些部分进行比传递性承诺更多的承诺。最后,不要忘记阅读
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 检查和添加到 Configure 部分。 感谢 Telsa Gwynne 教我 DocBook。