关于Linux Kernel Neon使用的一些总结

前些天在项目中遇到一个和neon使用有关的问题,做一下总结。

问题描述:Melis3.0方案跑在cortex-a7平台上,在用-mfpu=neon-vfpv4 --mfloat-abi=hard编译固件时,会概率性有系统死机或者数据不一致的情况出现。

问题分析:一开始别人跟我讲这个问题并没特别在意, 认为是个别模块的错误使用导致的,修复模块本身的bug就行了,可是后面另一个模块又出现类似的问题,我逐渐开始怀疑和系统相关了。

问题规律: 问题多发生在读写内存前后,发现内存访问结束后,读回来和预期不一致的现象,用调试器查看,发现确实内存数据被改写,而且更诡异的,似乎这个问题可以和某些外设中断有关,中断来的越频繁,问题发生的概率越大。

问题根源:前面说到,方案是使能hard float的情况下编译的,所以GCC会在何时的时候将某些运算编译成neon指令去加速计算过程,根据arm eabi, 一个完整的链接单元必须保证各个模块用一致的浮点选项编译,否则可能会链接不过,最终造成的结果是固件中neon指令可能会出现在任何函数,我们知道,系统运行时是分上下文的,典型的就是任务上下文和中断上下文, 在melis里面,我有在任务切换到时候对neon单元进行上下文保存,但是却疏忽了中断上下文执行对应操作。而在中断中,中断处理函数会调用哪些函数,这些函数会不会使用neon完全是随机的,所以有可能会破坏被中断的NEON现场。

下面是网络找到一些说明:

找出问题的原因并没花太多时间,只是后来联想到,这个问题实际上和系统关联性不大,或者说,任何并发多任务OS上都不可避免要遇到这个问题,那在这些系统上,比如Linux上是如何利用NEON的呢?

Linux简单粗暴的做法不禁让我大跌眼镜,在Linux里面,你是不能使用 --mfloat-abi=hard选项编译的,不但如此,还必须明确的告诉编译器,不能隐式的产生neon指令,方法是加入“-mfloat-abi=softfp”编译选项,指示编译器按照软浮点编译,这样就完全杜绝了编译器意外产生。

关于这一点,Linux内核有文档说明:/Documentation/arm/kernel_mode_neon.txt

至于为什么要用软浮点编译,下图也有说明,大概意思是说,即便只用-mfpu-neon选项,当优化等级调高的时候,编译器背后也会生成一些neon指令,为了杜绝这种情况,保证编译器在任何情况下都不自动产生neon指令,必须使用softfloat编译。

既然内核使用浮点编译选项, 那是如何使用neon的的?

原来,Linux是在层层保护的情况下使用neon的,要关抢占,防止任务调度出去,保存之前neon现场,并在使用完neon之后恢复, 不仅如此,使用neon的代码也不能靠编译器生成,所以当然也不能用c语言实现(因为前面说过,c代码都会按照软浮点编译), 最终只能通过汇编显示的调用neon指令实现。

具体点说,除了上面的编译要求必须满足外,使用neon指令的加速逻辑必须被 kernel_neon_begin / kernel_neon_end调用包裹。

kernel_neon_begin();

//operations that use neon

...................................

kernel_neon_end();

kernel_neon_begin里面干的就是启用neon单元,保存现场,关闭抢占的动作的执行地

其中,get_cpu执行的就是关闭抢占的动作

,从代码可见,kernel_neon_begin不能在中断上下文操作,这是为了避免在中断入口处保存 neon现场。

否则,试想如果一个中断打断了被 kernel_neon_begin/kernel_neon_end pair包裹的代码(关内核抢占并不能关闭中断).

而中断处理函数又可以再次调用这个pair, 这势必会破坏上一个被包裹的现场。所以Linux内核使用了最狠的策略,不允许在中断上下文使用neon,为的就是节省现场保存的那一点点performance.

以linux-4.15.10版本内核举例,说明上述过程,之所以用这个版本是因为我的qemu环境是基于这个版本搭建的。

内核是透过CONFIG_KERNEL_MODE_NEON配置项控制NEON打开和关闭的, 默认情况下,此选项是关闭的, 我们验证的话,需要打开这个选项。

仅仅打开上面的选项 还不够,默认情况下即便编译到了vfpmodule.c,有了kernel_neon_begin/end接口,但没有地方调用,还需要把应用的地方编译进去。

在arch/arm/crypto目录下,有很多加解密算法代码,包括DES/3DES, AES,HMAC和sha128/256/512等等,这种计算密度特别大的逻辑一般用加速单元实现比较何时,Linux也是这么做的, 只不过默认情况没有编译进NEON加速的加解密实现,我们需要打开它,方式也是menuconfig,把他们都选上,重新编译内核。

以sha512的实现为例, 看它的主要实现文件是用什么选项编译的。

先看一下内核其他部分是用什么选项编译的,打开linux-4.15.10/init/.initramfs.o.cmd

我们看到即便我们启用了CONFIG_KERNEL_MODE_NEON选项,initramfs.c仍然是按照软浮点编译的。

那sha512的实现是否也是如此呢?我们再打开linux-4.15.10/arch/arm/crypto/sha512-neon-glue.c ,里面有被 

kernel_neon_begin/end包括的代码段,确定是sha512的实现无疑了。

这个文件的编译选项会不会比环境有变化呢?

NO,  -mfpu和-msoft-float没有任何变化,仍然是软浮点编译的。

还没完,回到上图,我们看到被kernel_neon_begin/end包裹的代码实现有一个奇怪的函数参数sha512_block_data_order_neon, 加解密算法一般用一个通用的流程来对数据进行预处理,然后通过具体算法相关的回调实现差异化的流程,凭感觉这个函数应该有文章。

果然sha512_base_do_update只是一个普通的c代码实现,并且还是用软件浮点编译的,那文章一定坐在它的回调函数sha512_block_data_order_neon里面

目录下有一个sha512-core.S文件,打开后sha512_block_data_order_neon赫然在列,而且全部是用NEON加速指令手写汇编实现,这也辅证了前文说的,NEON指令必须在kernel_neon_begin/end包裹并手写汇编实现。

最后在看一下sha512-core.S是用什么选项汇编的;

果然,最终还是用软浮点。

所以总结来说,Linux只能用软浮点编译,即便你打开了NEON配置项,NEON指令必须显示手写汇编实现,而且必须要倍kernel_neon_begin/end包裹。

Linux全环境grep "-mfloat-abi=hard"没有任何发现,侧面说明了,内核不能以硬浮点选项编译,这和Melis3.0 是完全不同的。

最后遗留的问题: 内核态可以加保护使用neon, 随时可以被抢占的用户态怎么办?如果用户态多于一个线程使用neon寄存器,而内核态又不协助保存执行环境,势必会产生两段属于不同线程的neon代码交替执行的局面,该作何处理?

话说回来,Melis3.0全环境用hard float编译,所以必须保证任务初始调度点,主动调度点,和中断抢占点的neon环境保存。

附:如何检测一个编译目标文件是否使用了neon加速

关于FLAG的证明:

结束