在可抢占内核下的正确加锁:保持内核代码的抢占安全

作者:

Robert Love <rml@tech9.net>

简介

一个可抢占的内核会产生新的加锁问题。这些问题与 SMP 下的问题相同:并发和可重入。值得庆幸的是,Linux 可抢占内核模型利用了现有的 SMP 加锁机制。因此,内核只需要在极少数其他情况下进行显式的额外加锁。

本文档适用于所有内核黑客。在内核中开发代码需要保护这些情况。

规则 #1:每个 CPU 的数据结构需要显式保护

会出现两个类似的问题。一个示例代码片段

struct this_needs_locking tux[NR_CPUS];
tux[smp_processor_id()] = some_value;
/* task is preempted here... */
something = tux[smp_processor_id()];

首先,由于数据是每个 CPU 的,它可能没有显式的 SMP 加锁,但需要其他加锁。其次,当一个被抢占的任务最终被重新调度时,smp_processor_id 的先前值可能不等于当前值。您必须通过在它们周围禁用抢占来保护这些情况。

您还可以使用 put_cpu() 和 get_cpu(),这将禁用抢占。

规则 #2:CPU 状态必须受到保护。

在抢占下,CPU 的状态必须受到保护。这是与架构相关的,但包括在上下文切换期间未保留的 CPU 结构和状态。例如,在 x86 上,进入和退出 FPU 模式现在是一个必须在禁用抢占时发生的关键部分。想想如果内核正在执行浮点指令然后被抢占会发生什么。请记住,内核不会保存 FPU 状态,用户任务除外。因此,在抢占时,FPU 寄存器将被出售给出价最低的人。因此,必须在这些区域周围禁用抢占。

请注意,一些 FPU 函数已经明确是抢占安全的。例如,kernel_fpu_begin 和 kernel_fpu_end 将禁用和启用抢占。

规则 #3:锁的获取和释放必须由同一个任务执行

在一个任务中获取的锁必须由同一个任务释放。这意味着您不能做一些奇怪的事情,例如获取一个锁然后去玩,而另一个任务释放它。如果您想做这样的事情,请在同一个代码路径中获取和释放任务,并让调用者等待另一个任务的事件。

解决方案

抢占下的数据保护是通过在关键区域的持续时间内禁用抢占来实现的。

preempt_enable()              decrement the preempt counter
preempt_disable()             increment the preempt counter
preempt_enable_no_resched()   decrement, but do not immediately preempt
preempt_check_resched()       if needed, reschedule
preempt_count()               return the preempt counter

这些函数是可嵌套的。换句话说,您可以在代码路径中调用 preempt_disable n 次,并且在第 n 次调用 preempt_enable 之前,抢占不会被重新启用。如果未启用抢占,则抢占语句定义为无。

请注意,如果您持有任何锁或禁用了中断,则无需显式阻止抢占,因为在这些情况下抢占会被隐式禁用。

但请记住,“禁用 irqs”是一种从根本上不安全的禁用抢占的方式 - 如果抢占计数为 0,任何 cond_resched() 或 cond_resched_lock() 都可能触发重新调度。一个简单的 printk() 可能会触发重新调度。因此,仅当您知道受影响的代码路径不执行任何此类操作时,才使用此隐式禁用抢占属性。最佳策略是将此仅用于您编写的小型原子代码,并且该代码不调用任何复杂函数。

示例

cpucache_t *cc; /* this is per-CPU */
preempt_disable();
cc = cc_data(searchp);
if (cc && cc->avail) {
        __free_block(searchp, cc_entry(cc), cc->avail);
        cc->avail = 0;
}
preempt_enable();
return 0;

请注意,抢占语句必须包含关键变量的每个引用。另一个示例

int buf[NR_CPUS];
set_cpu_val(buf);
if (buf[smp_processor_id()] == -1) printf(KERN_INFO "wee!\n");
spin_lock(&buf_lock);
/* ... */

这段代码不是抢占安全的,但是请看我们如何通过简单地将 spin_lock 向上移动两行来轻松修复它。

使用禁用中断来防止抢占

可以使用 local_irq_disable 和 local_irq_save 来防止抢占事件。请注意,这样做时,您必须非常小心,不要引起任何会设置 need_resched 并导致抢占检查的事件。如有疑问,请依赖加锁或显式禁用抢占。

请注意,在 2.5 中,禁用中断现在仅是每个 CPU 的(例如本地的)。

另一个需要注意的问题是 local_irq_disable 和 local_irq_save 的正确用法。这些可以用来防止抢占,但是,在退出时,如果可能启用抢占,则应进行测试以查看是否需要抢占。如果这些是从 spin_lock 和读/写锁宏中调用的,则会执行正确的操作。它们也可以在自旋锁保护的区域内调用,但是,如果它们在此外的上下文中被调用,则应进行抢占测试。请注意,来自中断上下文或底部一半/任务小程序的调用也受到抢占锁的保护,因此可以使用不检查抢占的版本。