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

作者:

Robert Love <rml@tech9.net>

简介

可抢占内核带来了新的加锁问题。这些问题与 SMP 下的问题相同:并发和可重入性。幸运的是,Linux 可抢占内核模型利用了现有的 SMP 加锁机制。因此,内核仅在极少数额外情况下才需要显式的附加加锁。

本文档面向所有内核黑客。在内核中开发代码需要对这些情况进行保护。

规则 #1:Per-CPU 数据结构需要显式保护

会出现两个相似的问题。以下是一个代码片段示例

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

首先,由于数据是 per-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 时才会重新启用抢占。如果未启用抢占,则 preempt 语句不执行任何操作。

请注意,如果你持有任何锁或者禁用了中断,则不需要显式防止抢占,因为在这些情况下抢占是隐式禁用的。

但请记住,“禁用中断(irqs disabled)”是一种根本上不安全的禁用抢占方式 —— 如果抢占计数为 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 中,禁用中断现在仅是 per-CPU 的(例如本地的)。

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