可抢占内核下的正确加锁:保持内核代码的抢占安全¶
- 作者:
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 之前不会重新启用抢占。如果未启用抢占,则抢占语句定义为空。
请注意,如果您持有任何锁或禁用了中断,则无需显式防止抢占,因为在这些情况下,抢占会被隐式禁用。
但请记住,“中断已禁用”是一种从根本上不安全的禁用抢占的方式 - 如果 preempt count 为 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 和 read/write lock 宏调用的,则会完成正确的事情。它们也可以在 spin-lock 保护区域内调用,但是,如果它们在此上下文之外调用,则应测试抢占。请注意,来自中断上下文或 bottom half/tasklet 的调用也受到抢占锁的保护,因此可以使用不检查抢占的版本。