使用 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 上运行,而理论上 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 的保护。