保持内核代码的可抢占安全 【ChatGPT】

发布时间 2023-12-10 12:00:28作者: 摩斯电码

在可抢占内核下的适当锁定:保持内核代码的可抢占安全

作者 Robert Love rml@tech9.net

介绍

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

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

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

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

struct this_needs_locking tux[NR_CPUS];
tux[smp_processor_id()] = some_value;
/* 任务在此处被抢占... */
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()              减少抢占计数器
preempt_disable()             增加抢占计数器
preempt_enable_no_resched()   减少计数器,但不立即抢占
preempt_check_resched()       如果需要,重新调度
preempt_count()               返回抢占计数器的值

这些函数是可嵌套的。换句话说,在代码路径中可以多次调用preempt_disable,直到第n次调用preempt_enable之前,抢占不会被重新启用。如果未启用抢占,则preempt语句不起作用。

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

但请记住,“中断禁用”是一种基本不安全的禁用抢占的方式——如果抢占计数为0,任何cond_resched()或cond_resched_lock()都可能触发重新调度。一个简单的printk()可能会触发重新调度。因此,只有在你知道受影响的代码路径不执行任何这样的操作时,才使用这种隐式禁用抢占的属性。最好的策略是只在你编写的小型原子代码中使用这种属性,该代码不调用任何复杂的函数。

示例:

cpucache_t *cc; /* 这是每个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)。

另一个问题是正确使用local_irq_disable和local_irq_save。它们可以用于防止抢占,但是在退出时,如果可能启用抢占,应该进行检查以确定是否需要抢占。如果它们是从spin_lock和read/write lock宏中调用的,那么就会做正确的事情。它们也可以在受spin-lock保护的区域内调用,但是如果它们在这个上下文之外被调用,应该进行抢占的检查。请注意,来自中断上下文或底半部/任务队列的调用也受到抢占锁的保护,因此可以使用不检查抢占的版本。