使用 RCU 保护动态 NMI 处理程序¶
虽然 RCU 通常用于保护主要用于读取的数据结构,但也可以使用 RCU 来提供动态非屏蔽中断处理程序,以及动态 irq 处理程序。本文档描述了如何做到这一点,大致借鉴了 Zwane Mwaikambo 在旧版本“arch/x86/kernel/traps.c”中的 NMI-timer 工作。
相关的代码片段如下所示,每个代码片段后都有简要说明
static int dummy_nmi_callback(struct pt_regs *regs, int cpu)
{
return 0;
}
dummy_nmi_callback() 函数是一个“虚拟” NMI 处理程序,它什么也不做,但返回零,从而表示它什么也没做,允许 NMI 处理程序采取默认的机器特定操作
static nmi_callback_t nmi_callback = dummy_nmi_callback;
nmi_callback 变量是一个指向当前 NMI 处理程序的全局函数指针
void do_nmi(struct pt_regs * regs, long error_code)
{
int cpu;
nmi_enter();
cpu = smp_processor_id();
++nmi_count(cpu);
if (!rcu_dereference_sched(nmi_callback)(regs, cpu))
default_do_nmi(regs);
nmi_exit();
}
do_nmi() 函数处理每个 NMI。它首先以与硬件 irq 相同的方式禁用抢占,然后增加每个 CPU 的 NMI 计数。然后,它调用存储在 nmi_callback 函数指针中的 NMI 处理程序。如果此处理程序返回零,则 do_nmi() 调用 default_do_nmi() 函数来处理机器特定的 NMI。最后,恢复抢占。
理论上,不需要 rcu_dereference_sched()
,因为此代码仅在 i386 上运行,理论上不需要 rcu_dereference_sched()
。但是,实际上它是一个很好的文档辅助工具,特别是对于任何试图在 Alpha 或具有激进优化编译器的系统上做类似事情的人。
- 快速测验
考虑到指针引用的代码是只读的,为什么 Alpha 上可能需要
rcu_dereference_sched()
?
回到关于 NMI 和 RCU 的讨论
void set_nmi_callback(nmi_callback_t callback)
{
rcu_assign_pointer(nmi_callback, callback);
}
set_nmi_callback() 函数注册一个 NMI 处理程序。请注意,回调要使用的任何数据都必须在调用 set_nmi_callback() -之前- 初始化。对于不排序写入的架构,rcu_assign_pointer()
确保 NMI 处理程序看到初始化的值
void unset_nmi_callback(void)
{
rcu_assign_pointer(nmi_callback, dummy_nmi_callback);
}
此函数注销 NMI 处理程序,恢复原始的 dummy_nmi_handler()。但是,很可能有些其他 CPU 上当前正在执行 NMI 处理程序。因此,在旧 NMI 处理程序在所有其他 CPU 上完成执行之前,我们无法释放它使用的任何数据结构。
实现此目的的一种方法是通过 synchronize_rcu()
,可能如下所示
unset_nmi_callback();
synchronize_rcu();
kfree(my_nmi_data);
这是可行的,因为(从 v4.20 开始)synchronize_rcu()
会阻塞,直到所有 CPU 完成它们正在执行的任何禁用抢占的代码段。由于 NMI 处理程序禁用抢占,因此保证 synchronize_rcu()
不会返回,直到所有正在进行的 NMI 处理程序退出。因此,在 synchronize_rcu()
返回后立即释放处理程序的数据是安全的。
重要提示:为此目的生效,所讨论的架构必须分别在 NMI 进入和退出时调用 nmi_enter() 和 nmi_exit()。
- 快速测验的答案
考虑到指针引用的代码是只读的,为什么 Alpha 上可能需要
rcu_dereference_sched()
?set_nmi_callback() 的调用者可能已经初始化了一些将由新的 NMI 处理程序使用的数据。在这种情况下,将需要
rcu_dereference_sched()
,因为否则在设置新处理程序后立即收到 NMI 的 CPU 可能会看到指向新 NMI 处理程序的指针,但却看到处理程序数据的旧的预初始化版本。当使用具有激进指针值推测优化的编译器时,其他 CPU 上也可能发生同样的悲惨故事。(但是请不要这样做!)
更重要的是,
rcu_dereference_sched()
向阅读代码的人清楚地表明该指针正在受到 RCU-sched 的保护。