FPU context corruption导致的错误

1. FPU是什么?

FPU是Floating point unit的缩写,代表的是浮点运算的部件。

一个进程在运行过程中通常需要使用CPU通用寄存器、控制寄存器、PC、interrupt flags等CPU资源,同时也需要stack等内存资源,这就是所谓的process context。除此之外,还有一个非常重要的资源,FPU context。如果一个进程使用了浮点运算,在进程切换过程中正确地保存/恢复其FPU context是一个非常重要的工作。Linux kernel要确保进程的FPU context不能损坏,否则就会产生数据错误,并且这一类的所谓silent错误debug起来非常困难。

2. 遇到的问题

我们的系统需要从kernel 3.x升级到4.4。把必要的patch打到新kernel上后,一切都看起来很正常,做了大量的测试,也都跑的很好。需要跑我们的关键应用程序了,这时,问题出现了。这个应用是一个多线程程序,进行了大量的浮点运算去生成SHA1。生成的SHA1会存入磁盘,也会被读回内存进行在线的比较。在这一过程中,发现很多的数据比较错误,这个failure阻止了进一步的测试流程。

3. debug过程

这个应用在kernel升级过程中并没有做任何改动,所以这个failure肯定与新kernel有关。但,到底是因为API的变化导致的还是kernel本身引起的?通过逐层剥离,这个关键应用的开发人员写了一个简单的独立的测试程序。这个测试程序很纯粹,就是创建多个线程,每个线程都是在进行SHA1计算。这就排除了其它系统调用或模块可能导致的干扰。

这样的简单程序仍然会失败。这里面值得怀疑的只有进程/线程切换了。虽然我们怀疑是FPU context corruption的问题,但并不知道具体是在哪里发生的corruption。在研究代码的同时,通过在不同平台上测试这个程序,我们发现在一个VMware虚拟机上,它一直没有失败!仔细对比这个VM与其它系统,终于发现在这个VM上的不同之处:

x86/fpu: Using 'lazy' FPU context switches

其它系统都是使用 'eager' 模式。在文件arch/x86/kernel/fpu/init.c中,FPU的初始化代码会判断当前CPU是否支持eager模式,如果支持,则设置为eager。通过设eagerfpu=disable,把FPU mode设为lazy,我们发现这个hack解决了所有平台上的问题。

那么FPU eager和lazy mode到底有什么区别呢?

  • lazy:对于早期的CPU,FPU context的save/restore是一件非常耗时的动作 -- 要把多个128/256bit的寄存器写到内存或者从内存取回。如果每次进程(在内核态所有的线程也都是进程形式存在)切换都去做这一工作,则会很耗时。但是,另外一个事实是,很多进程根本就没有进行过浮点运算,也就没有使用到FPU。对这种进程也去执行FPU save/restore显然是一种浪费。所以,一个变通的方法是,直到进程执行了第一个浮点指令时,才去执行FPU的restore操作,这就是所谓的lazy,跟Linux kernel里其它的lazy概念是一致的。同样,只有一个进程曾经执行过浮点指令,才需要在进程切换save它的FPU context。lazy FPU依赖于kernel及CPU的exception处理机制,即Device Not Available trap,参见其handler,do_device_not_available()。
  • eager: 现代CPU为FPU context切换进行了优化,所以save/restore的开销不再是一个问题。因此,新kernel的做法就是在每次进程切换时都执行FPU context switch。但是,新kernel还是做了一些优化:线程被换出时,只有它使用过浮点计算,它的FPU register才会save下来;对于被换入的线程,只有它上一次使用过浮点寄存器,其FPU context才会被restore。eager与lazy 模式最大的区别就是eager没有使用exception机制,减少了一次context switch。另外,eager模式中,在判断线程是否使用过FPU寄存器时使用了很技巧性的方法(依赖于fpstate_active和fpregs_active变量):所有的用户线程都会设fpstate_active;在restore时,会调用fpu_want_lazy_restore来决定是否preload新线程的FPU。正是这些eager模式的变化引起了测试结果的变化。

不管是eager还是lazy模式,我们都会发现,FPU context的save/restore都是发生在进程切换时。如果进程在kernel态时,比如在一个系统调用中执行了浮点运算就会使用浮点寄存器,进程从kernel态返回用户态时可能根本没有意识到自己的FPU context已经发生改变,这就破坏了进程的FPU context。所以,无论如何,如果kernel函数里使用了浮点运算,都应该进行保护,保护的方法就是调用kernel_fpu_begin/kernel_fpu_end。恰恰是这一点的疏忽,导致了eager模式在新kernel里不能正确地工作。通过仔细地分析,最终发现是kernel里的一个driver使用了-msse/-msse2编译选项,GCC在此情况下会尽可能地使用浮点寄存器进行数学运算,查看汇编代码可以发现,即使非浮点运算也会用到浮点寄存器如xmm, ymm等。去掉这个编译选项,把引起问题的浮点运算转换成定点运算,所有这一切问题都消失了,即使eager模式下也能很好地工作了。

参考资料:https://tthtlc.wordpress.com/2016/12/17/understanding-fpu-usage-in-linux-kernel/


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值