编者注:本文来自华辰联科技术团队,分享他们将浮点运算放入内核态时的探索。最近有一个需求,就是把用户态的浮点运算全部放到内核态来提高运行速度。在移植的过程中,我们发现问题并没有那么简单,于是我们抽丝剥茧,揭开了Linux浮点数处理的原理。本文代码基于x8664位CPU,Linux4.14内核。1.Linux内核加入浮点运算的问题我们用一个简单的浮点运算例子来说明:#include#include#include#include#includestaticnoinlinedoublefloat_divide(doublefloat1,doublefloat2){returnfloat1/float2;}staticint__inittest_float_init(void){双结果,float1=4.9,float492=0.;result=float_divide(float1,float2);printk("result=%d\n",(int)result);return0;}staticvoid__exittest_float_exit(void){;}module_init(test_float_init);module_exit(test_float_exit);MODULE_LICENSE(“GPL”);test_float.cobj-m:=test_float.oKDIR:=/lib/modules/$(shelluname-r)/buildall:make-C$(KDIR)M=$(PWD)modulesMakefile这个内核模块计算两个浮点数相除的结果,然后打印出结果。但是当我们执行make编译的时候,发现报错:SSE寄存器返回的错误信息是“SSEdisabled”。我们执行makeV=1查看关键编译信息:发现gcc的参数中有-mno-sse-mno-mmx-mno-sse2选项。原来gcc的默认编译选项禁用了sse、mmx、sse2等浮点运算指令。2、通过添加gcc编译参数和kernel_fpu_begin/kernel_fpu_end解决问题为了让内核支持浮点运算,我们在Makefile中添加了对sse等选项的支持,并在源码中添加了kernel_fpu_begin/kernel_fpu_end函数。修改后的源码如下:#include#include#include#include#includestaticnoinlinedoublefloat_divide(doublefloat1,doublefloat2){returnfloat1/float2;}staticint__inittest_float_init(void){double结果,float1=4.9,float2=0.49;kernel_fpu_begin();结果=float_divide(float1,float2);kernel_fpu_end();printk("result=%d\n",(int)result);return0;}staticvoid__exittest_float_exit(void){;}module_init(test_float_init);module_exit(test_float_exit);MODULE_LICENSE("GPL");test_float.cobj-m:=test_float.oKDIR:=/lib/modules/$(shelluname-r)/buildFPU_CFLAGS+=-mhard-floatFPU_CFLAGS+=-msse-msse2CFLAGS_test_float.o+=$(FPU_CFLAGS)全部:制作-C$(KDIR)M=$(PWD)modulesMakefile此时执行make,发现编译正确通过:然后insmodtest_float.ko,观察dmesg的输出:从上面的例子,结合kernel中arch/x86/Makefile中的KBUILD_CFLAGS源码,可以看到在编译内核和内核模块时,gcc选项继承了Linux中的规则,指定了-mno-sse-mno-mmx-mno-sse2,即禁用FPU。因此,如果内核模块支持浮点运算,编译选项需要显式指定-msse-msse2。3、Linux内核态浮点运算处理方式分析从上面可以看出,为了实现一个内核模块的浮点运算,我们加入了编译参数-mhard-float和-msse-msse2。对于编译参数,-mhard-float是告诉编译器直接生成浮点指令,-msse-msse2是告诉编译器使用sse/sse2指令集编译代码。kernel_fpu_begin和kernel_fpu_end也是必须的,因为Linux内核为了提高系统的运行速度,只保存/恢复普通寄存器的值,不包括FPU浮点寄存器的值,main函数调用kernel_fpu_begin是关闭系统抢占,浮点计算结束后调用kernel_fpu_end开启系统抢占,这样可以防止代码被打断,从而可以安全的进行浮点计算,代码它们之间不能有睡眠或调度操作,也不能有嵌套(原来保存的状态会被覆盖,再执行kernel_fpu_end()最终会恢复错误的FPU状态)。voidkernel_fpu_begin(void){preempt_disable();__kernel_fpu_begin();}四、Linux内核态下三角函数的实现由于内核态不支持浮点运算,所以没有实现三角函数等浮点运算.如果需要,可以将用户态glibc中相关三角函数的实现移植到内核态。五、Linux用户态浮点运算处理方法分析为什么用户态浮点运算不需要指定编译选项,显式调用kernel_fpu_begin和kernel_fpu_end函数?我们在用户模式下编写一个带有浮点运算的简单示例:#includeintmain(intargc,char**argv){intresult,float1=4.9,float2=0.49;result=float1/浮动2;printf("result=%d\n",result);return0;}user_float.c我们使用以下四个编译指令来查看编译后的程序集:gcc-Suser_float.cgcc-Suser_float.c-msoft-floatgcc-Suser_float.c-mhard-floatgcc-Suser_float.c-msoft-float-mno-sse-mno-mmx-mno-sse2前三个命令编译成功。依次查看编译后的汇编代码,发现生成的汇编代码一模一样,而且都是在sse指令中使用了mmx寄存器,也就是使用了FPU。第四条命令编译失败,提示error:SSEregisterreturnwithSSEdisabled。从以上现象我们可以得出一个结论,当系统默认使用gcc编译用户态程序时,gcc默认使用FPU,即使用硬浮点来编译。通过查阅各种文档和分析代码,x86CPU提供了以下特点:被切换。而当TS的这一位被置位时,CPU会在进程使用FPU指令时产生DNA(DeviceNotAvailable)异常。Linux使用此功能。当用户态应用程序进行浮点运算(SSE等指令)时,会触发DNA异常,同时使用FPU特殊寄存器和指令进行浮点运算。此时TS_USEDFPU标志位为1,表示用户态进程使用了??FPU。voidfpu__restore(structfpu*fpu){fpu__initialize(fpu);/*在fpregs_activate()之后避免__kernel_fpu_begin()*/kernel_fpu_disable();trace_x86_fpu_before_restore(fpu);fpregs_activate(fpu);copy_kernel_to_fpregs(&fpu->state);trace_x86_fpu_after_restore(fpu);kernel_fpu_enable();}EXPORT_SYMBOL_GPL(fpu__restore);假设用户态进程A使用FPU进行浮点运算,用户态进程B被调度执行,那么当进程A被调度出去时,内核设置TS并调用fpu__restore来保存FPU的内容。当进程A恢复执行浮点运算时,会触发DNA异常,相应的异常处理器会恢复之前保存的FPU状态。假设用户态进程A使用FPU进行浮点运算(TS_USEDFPU标志为1),此时内核态进程C调度使用FPU。由于内核只保存普通寄存器的值,不包括FP等寄存器的值,所以内核会主动调用kernel_fpu_begin函数保存寄存器的内容,使用完毕后调用kernel_fpu_end。当用户态进程A恢复执行浮点运算时,会触发DNA异常,相应的异常处理程序会恢复FPU寄存器的内容。6.结论在Linux中,任务切换时,浮点设备寄存器默认是不保存的。如果需要内核态支持浮点运算,需要加入支持浮点的编译选项,并使用kernel_fpu_begin和kernel_fpu_end函数手动处理context。用户态默认支持浮点运算,但需要内核协助。