抽絲剝繭 Linux 浮點運算的原理

抽絲剝繭 Linux 浮點運算的原理

編者按:本文來自華辰連科技術團隊,分享了他們在將浮點運算放到核心態時的探索。

最近我們有一個需求,需要把使用者態的浮點數運算全部放到核心態執行,以提高執行速度,移植的過程中發現問題沒有這麼簡單,然後我們抽絲剝繭,揭開 Linux 對浮點處理的原理。

此文章的程式碼基於 x86 64 位 CPU,Linux 4。14 核心。

一、 Linux 核心新增浮點運算出現的問題

我們以一個簡單的浮點運算例子來說明:

#include

test_float。c

obj-m := test_float。oKDIR := /lib/modules/$(shell uname -r)/build​all:make -C $(KDIR) M=$(PWD) modules

Makefile

這個核心模組就是計算了兩個浮點數除的結果,然後將結果打印出來 。但是我們執行

make

編譯的時候發現報錯:

抽絲剝繭 Linux 浮點運算的原理

提示 SSE 暫存器返回的報錯資訊為 “SSE disabled”。我們執行

make V=1

檢視關鍵的編譯資訊:

抽絲剝繭 Linux 浮點運算的原理

我們發現在

gcc

的引數中有

-mno-sse -mno-mmx -mno-sse2

選項,原來

gcc

預設的編譯選項禁用了 sse、mmx、sse2 等浮點運算指令。

二、透過新增 gcc 編譯引數和 kernel_fpu_begin/kernel_fpu_end 來解決問題

為了讓核心支援浮點運算,我們在

Makefile

中新增支援 sse 等選項,原始碼中新增

kernel_fpu_begin

/

kernel_fpu_end

函式,修改後的原始碼如下所示:

#include

test_float。c

obj-m := test_float。oKDIR := /lib/modules/$(shell uname -r)/build​FPU_CFLAGS += -mhard-floatFPU_CFLAGS += -msse -msse2CFLAGS_test_float。o += $(FPU_CFLAGS)​all:make -C $(KDIR) M=$(PWD) modules

Makefile

此時執行

make

,發現編譯正確通過了:

然後

insmod test_float。ko

,觀察

dmesg

的輸出:

從上面的例子,結合核心原始碼中

arch/x86/Makefile

中的

KBUILD_CFLAGS

,可以看到編譯核心及核心模組時,

gcc

選項繼承 Linux 中的規則,指定了

-mno-sse -mno-mmx -mno-sse2

,也就是禁用了 FPU 。所以,要想核心模組支援浮點運算,編譯選項需要顯式的指定

-msse -msse2

三、 Linux 核心態對浮點運算處理方式的分析

從上面可以看到,我們為了實現一個核心模組的浮點運算,添加了編譯引數

-mhard-float和-msse -msse2

,對於編譯引數來說,

-mhard-float

是告訴編譯器直接生成浮點運算的指令,而

-msse -msse2

則是告訴編譯器可以使用 sse/sse2 指令集來編譯程式碼。

kernel_fpu_begin

kernel_fpu_end

也是必須的,因為 Linux 核心為了提高系統的執行速率,在任務上下文切換時,只會儲存/恢復普通暫存器的值,並不包括 FPU 浮點暫存器的值,而呼叫

kernel_fpu_begin

主要作用是關掉系統搶佔,浮點計算結束後呼叫

kernel_fpu_end

開啟系統搶佔,這使得程式碼不會被中斷,從而安全的進行浮點運算,並且要求這之間的程式碼不能有休眠或排程操作,另外不得有巢狀的情況出現(將會覆蓋原始儲存的狀態,然後執行

kernel_fpu_end()

最終將恢復錯誤的 FPU 狀態)。

void kernel_fpu_begin(void){ preempt_disable; __kernel_fpu_begin;}

四、三角函式在 Linux 核心態的實現

由於核心態不支援浮點運算,所以像三角函式之類浮點運算都沒有實現,如果需要,可以將使用者態 glibc 中相關的三角函式的實現移植到核心態。

五、 Linux 使用者態對浮點運算處理方式的分析

為什麼使用者態浮點運算就不需要指定編譯選項以及顯式呼叫

kernel_fpu_begin

kernel_fpu_end

函式呢?我們在使用者態下寫一個簡單的帶浮點運算的例子:

#include

user_float。c

我們分別使用下面四條編譯指令檢視編譯出來的彙編:

gcc -S user_float。c

gcc -S user_float。c -msoft-float

gcc -S user_float。c -mhard-float

gcc -S user_float。c -msoft-float -mno-sse -mno-mmx -mno-sse2

前三條命令編譯成功。依次檢視編譯生成的彙編程式碼,發現生成的彙編程式碼是完全一樣的,都是用到了 sse 指令中的 mmx 暫存器,也就是使用到了 FPU。

第四條命令編譯失敗 ,提示

error: SSE register return with SSE disabled

。從上面的現象中我們可以得出結論,系統預設使用

gcc

編譯使用者態程式時,

gcc

預設使用 FPU,也就是使用硬浮點來編譯。

經過查閱各種文件和分析程式碼,x86 CPU 提供如下特性:CPU 提供的 TS 暫存器的第三個位是任務已切換標誌(Task Switched bit),CPU 在每次任務切換時會設定這個位。而且 TS 的這個位被設定時,當程序使用 FPU 指令時 CPU 會產生一個 DNA(Device Not Availabel)異常。Linux 使用此特性,當用戶態應用程式進行浮點運算時(SSE 等指令),觸發 DNA 異常,同時使用 FPU 專用暫存器和指令來執行浮點數功能,此時

TS_USEDFPU

標誌為 1,表示使用者態程序使用了 FPU。

void fpu__restore(struct fpu *fpu){ fpu__initialize(fpu); /* Avoid __kernel_fpu_begin right after fpregs_activate */ 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 暫存器的內容。

六、 結論

Linux 中當任務切換時,預設不儲存浮點器暫存器。

如果需要核心態支援浮點運算,需要增加支援浮點的編譯選項和使用

kernel_fpu_begin

kernel_fpu_end

函式手動處理上下文。

使用者態預設支援浮點運算,但是需要核心來輔助。