我在 asm/i387.h 中使用 kernel_fpu_begin
和 kernel_fpu_end
函数保护 Linux 内核模块内部一些简单浮点运算的 FPU 寄存器状态。
我很好奇在 kernel_fpu_end
函数之前两次调用 kernel_fpu_begin
函数的行为,反之亦然。例如:
#include <asm/i387.h>
double foo(unsigned num){
kernel_fpu_begin();
double x = 3.14;
x += num;
kernel_fpu_end();
return x;
}
...
kernel_fpu_begin();
double y = 1.23;
unsigned z = 42;
y -= foo(z);
kernel_fpu_end();
在foo
函数中,我调用了kernel_fpu_begin
和kernel_fpu_end
;但是 kernel_fpu_begin
在调用 foo
之前已经被调用了。这会导致未定义的行为吗?
此外,我是否应该在 foo
函数中调用 kernel_fpu_end
?我在 kernel_fpu_end
调用后返回了一个 double,这意味着访问浮点寄存器是不安全的,对吗?
我最初的猜测是不要在 foo
函数中使用 kernel_fpu_begin
和 kernel_fpu_end
调用;但是,如果 foo
将 double 强制转换为 unsigned 会怎样——程序员不知道要使用 kernel_fpu_begin
和 kernel_fpu_end
在 foo
之外?
最佳答案
简短回答:不,嵌套 kernel_fpu_begin()
是不正确的调用,这将导致用户空间 FPU 状态被破坏。
中等答案:这行不通,因为 kernel_fpu_begin()
使用当前线程的 struct task_struct
保存 FPU 状态( task_struct
有一个体系结构相关的成员 thread
,在 x86 上,thread.fpu
保存线程的 FPU 状态),然后做第二个 kernel_fpu_begin()
将覆盖原来保存的状态。然后做 kernel_fpu_end()
最终会恢复错误的 FPU 状态。
长答案:正如您在 <asm/i387.h>
中看到的实际实现一样,细节有点棘手。在较旧的内核中(如您查看的 3.2 源代码),FPU 处理始终是“惰性”的——内核希望避免在真正需要它之前重新加载 FPU 的开销,因为线程可能会运行并再次被调度无需实际使用 FPU 或需要其 FPU 状态。所以kernel_fpu_end()
只是设置了 TS 标志,这会导致 FPU 的下一次访问陷入陷阱并导致 FPU 状态重新加载。希望我们实际上没有足够多的时间使用 FPU,从而使整体成本更低。
但是,如果您查看较新的内核(我相信是 3.7 或更新版本),您会发现所有这些实际上都有第二条代码路径——“eager”FPU。这是因为较新的 CPU 具有“优化的”XSAVEOPT 指令,并且较新的用户空间更频繁地使用 FPU(对于 memcpy 中的 SSE 等)。 XSAVEOPT/XRSTOR 的成本更低,惰性优化实际上避免 FPU 重新加载的机会也更少,因此在新 CPU 上使用新内核,kernel_fpu_end()
继续并恢复 FPU 状态。 (
但是在“惰性”和“急切”FPU 模式下,task_struct
中仍然只有一个槽。保存 FPU 状态,所以嵌套 kernel_fpu_begin()
最终会破坏用户空间的 FPU 状态。
关于在 kernel_fpu_end 之前调用 kernel_fpu_begin 两次,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15934615/