Linux中國

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

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

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

此文章的代碼基於 x86 64 位 CPU,Linux 4.14 內核。

一、 Linux 內核添加浮點運算出現的問題

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

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/debugfs.h>
#include <asm/fpu/api.h>
#include <linux/delay.h>

static noinline double float_divide(double float1, double float2)
{
    return float1 / float2;
}

static int __init test_float_init(void)
{
  double result, float1 = 4.9, float2 = 0.49;
​
  result = float_divide(float1, float2);
  printk("result = %dn", (int)result);
​
  return 0;
}
​
static void __exit test_float_exit(void)
{
  ;
}
​
module_init(test_float_init);
module_exit(test_float_exit);
MODULE_LICENSE("GPL");

test_float.c

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

Makefile

這個內核模塊就是計算了兩個浮點數除的結果,然後將結果列印出來 。但是我們執行 make 編譯的時候發現報錯:

提示 SSE 寄存器返回的報錯信息為 「SSE disabled」。我們執行 make V=1 查看關鍵的編譯信息:

我們發現在 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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/debugfs.h>
#include <asm/fpu/api.h>
#include <linux/delay.h>

static noinline double float_divide(double float1, double float2)
{
    return float1 / float2;
}

static int __init test_float_init(void)
{
  double result, float1 = 4.9, float2 = 0.49;
​
  kernel_fpu_begin();
  result = float_divide(float1, float2);
  kernel_fpu_end();
  printk("result = %dn", (int)result);
​
  return 0;
}
​
static void __exit test_float_exit(void)
{
  ;
}
​
module_init(test_float_init);
module_exit(test_float_exit);
MODULE_LICENSE("GPL");

test_float.c

obj-m := test_float.o
KDIR := /lib/modules/$(shell uname -r)/build
​
FPU_CFLAGS += -mhard-float
FPU_CFLAGS += -msse -msse2
CFLAGS_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_beginkernel_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_beginkernel_fpu_end 函數呢?我們在用戶態下寫一個簡單的帶浮點運算的例子:

#include <stdio.h>
​
int main(int argc, char **argv)
{
  int result, float1=4.9, float2=0.49;
​
  result = float1 / float2;
  printf("result = %dn", result);
​
  return 0;
}

user_float.c

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

  1. gcc -S user_float.c
  2. gcc -S user_float.c -msoft-float
  3. gcc -S user_float.c -mhard-float
  4. 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 寄存器的內容。

六、 結論

  1. Linux 中當任務切換時,預設不保存浮點器寄存器。
  2. 如果需要內核態支持浮點運算,需要增加支持浮點的編譯選項和使用 kernel_fpu_beginkernel_fpu_end 函數手動處理上下文。
  3. 用戶態預設支持浮點運算,但是需要內核來輔助。

本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國