歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

Linux2.6對新型CPU的支持

文章分析了在 Linux 2.6 中引入的對 Intel CPU 快速系統調用指令 SYSENTER/SYSEXIT 支持的實現。Linux 驅動及內核開發者通過了解快速系統調用指令的機制,可以在自己的代碼中通過利用這一機制,提高系統性能,並避開由快速系統調用方式帶來的一些局限(如系統調用中嵌套系統調用)。  

前言

在 Linux 2.4 內核中,用戶態 Ring3 代碼請求內核態 Ring0 代碼完成某些功能是通過系統調用完成的,而系統調用的是通過軟中斷指令(int 0x80)實現的。在 x86 保護模式中,處理 INT 中斷指令時,CPU 首先從中斷描述表 IDT 取出對應的門描述符,判斷門描述符的種類,然後檢查門描述符的級別 DPL 和 INT 指令調用者的級別 CPL,當 CPL<=DPL 也就是說 INT 調用者級別高於描述符指定級別時,才能成功調用,最後再根據描述符的內容,進行壓棧、跳轉、權限級別提升。內核代碼執行完畢之後,調用 IRET 指令返回,IRET 指令恢復用戶棧,並跳轉會低級別的代碼。

其實,在發生系統調用,由 Ring3 進入 Ring0 的這個過程浪費了不少的 CPU 周期,例如,系統調用必然需要由 Ring3 進入 Ring0(由內核調用 INT 指令的方式除外,這多半屬於 Hacker 的內核模塊所為),權限提升之前和之後的級別是固定的,CPL 肯定是 3,而 INT 80 的 DPL 肯定也是 3,這樣 CPU 檢查門描述符的 DPL 和調用者的 CPL 就是完全沒必要。正是由於如此,Intel x86 CPU 從 PII 300(Family 6,Model 3,Stepping 3)之後,開始支持新的系統調用指令 sysenter/sysexit。sysenter 指令用於由 Ring3 進入 Ring0,SYSEXIT 指令用於由 Ring0 返回 Ring3。由於沒有特權級別檢查的處理,也沒有壓棧的操作,所以執行速度比 INT n/IRET 快了不少。

不同系統調用方式的性能比較:

下面是一些來自互聯網的有關sysenter/sysexit 指令和 INT n/IRET 指令在 Intel Pentium CPU 上的性能對比:

1:系統調用性能測試測試硬件:

Intel? Pentium? III CPU, 450 MHz Processor Family: 6 Model: 7 Stepping: 2

用戶模式花費的時間 核心模式花費的時間

基於 sysenter/sysexit 指令的系統調用 9.833 microseconds 6.833 microseconds

基於中斷 INT n 指令的系統調用     17.500 microseconds 7.000 microseconds

2:各種 CPU 上 INT 0x80 和 SYSENTER 執行速度的比較

CPU           Int0x80 sysenter

Athlon XP 1600+     277   169

800MHz mode 1 athlon  279   170

2.8GHz p4 northwood ht 1152  442

上述數據為對 100000 次 getppid() 系統調用所花費的 CPU 時鐘周期取的平均值

自這種技術推出之後,人們一直在考慮在 Linux 中加入對這種指令的支持,在 Kernel.org 的郵件列表中,主題為 "Intel P6 vs P7 system call performance" 的大量郵件討論了采用這種指令的必要性,郵件中列舉的理由主要是 Intel 在 Pentium 4 的設計上存在問題,造成 Pentium 4 使用中斷方式執行的系統調用比 Pentium 3 以及 AMD Athlon 所耗費的 CPU 時鐘周期多上 5~10 倍。因此,在 Pentium 4 平台上,通過 sysenter/sysexit 指令來執行系統調用已經是刻不容緩的需求。

sysenter/sysexit 系統調用的機制:

在 Intel 的軟件開發者手冊第二、三卷(Vol.2B,Vol.3)中,4.8.7 節是關於 sysenter/sysexit 指令的詳細描述。手冊中說明,sysenter 指令可用於特權級 3 的用戶代碼調用特權級 0 的系統內核代碼,而 SYSEXIT 指令則用於特權級 0 的系統代碼返回用戶空間中。sysenter 指令可以在 3,2,1 這三個特權級別調用(Linux 中只用到了特權級 3),而 SYSEXIT 指令只能從特權級 0 調用。

執行 sysenter 指令的系統必須滿足兩個條件:1.目標 Ring 0 代碼段必須是平坦模式(Flat Mode)的 4GB 的可讀可執行的非一致代碼段。2.目標 RING0 堆棧段必須是平坦模式(Flat Mode)的 4GB 的可讀可寫向上擴展的棧段。

在 Intel 的手冊中,還提到了 sysenter/sysexit 和 int n/iret 指令的一個區別,那就是 sysenter/sysexit 指令並不成對,sysenter 指令並不會把 SYSEXIT 所需的返回地址壓棧,sysexit 返回的地址並不一定是 sysenter 指令的下一個指令地址。調用 sysenter/sysexit 指令地址的跳轉是通過設置一組特殊寄存器實現的。這些寄存器包括:

SYSENTER_CS_MSR - 用於指定要執行的 Ring 0 代碼的代碼段選擇符,由它還能得出目標 Ring 0 所用堆棧段的段選擇符;

SYSENTER_EIP_MSR - 用於指定要執行的 Ring 0 代碼的起始地址;

SYSENTER_ESP_MSR-用於指定要執行的Ring 0代碼所使用的棧指針

這些寄存器可以通過 wrmsr 指令來設置,執行 wrmsr 指令時,通過寄存器 edx、eax 指定設置的值,edx 指定值的高 32 位,eax 指定值的低 32 位,在設置上述寄存器時,edx 都是 0,通過寄存器 ecx 指定填充的 MSR 寄存器,sysenter_CS_MSR、sysenter_ESP_MSR、sysenter_EIP_MSR 寄存器分別對應 0x174、0x175、0x176,需要注意的是,wrmsr 指令只能在 Ring 0 執行。

這裡還要介紹一個特性,就是 Ring0、Ring3 的代碼段描述符和堆棧段描述符在全局描述符表 GDT 中是順序排列的,這樣只需知道 SYSENTER_CS_MSR 中指定的 Ring0 的代碼段描述符,就可以推算出 Ring0 的堆棧段描述符以及 Ring3 的代碼段描述符和堆棧段描述符。

在 Ring3 的代碼調用了 sysenter 指令之後,CPU 會做出如下的操作:

1.將 SYSENTER_CS_MSR 的值裝載到 cs 寄存器

2.將 SYSENTER_EIP_MSR 的值裝載到 eip 寄存器

3.將 SYSENTER_CS_MSR 的值加 8(Ring0 的堆棧段描述符)裝載到 ss 寄存器。

4.將 SYSENTER_ESP_MSR 的值裝載到 esp 寄存器

5.將特權級切換到 Ring0

6.如果 EFLAGS 寄存器的 VM 標志被置位,則清除該標志

7.開始執行指定的 Ring0 代碼

在 Ring0 代碼執行完畢,調用 SYSEXIT 指令退回 Ring3 時,CPU 會做出如下操作:

1.將 SYSENTER_CS_MSR 的值加 16(Ring3 的代碼段描述符)裝載到 cs 寄存器

2.將寄存器 edx 的值裝載到 eip 寄存器

3.將 SYSENTER_CS_MSR 的值加 24(Ring3 的堆棧段描述符)裝載到 ss 寄存器

4.將寄存器 ecx 的值裝載到 esp 寄存器

5.將特權級切換到 Ring3

6.繼續執行 Ring3 的代碼

由此可知,在調用 SYSENTER 進入 Ring0 之前,一定需要通過 wrmsr 指令設置好 Ring0 代碼的相關信息,在調用 SYSEXIT 之前,還要保證寄存器edx、ecx 的正確性。

如何得知 CPU 是否支持 sysenter/sysexit 指令

根據 Intel 的 CPU 手冊,我們可以通過 CPUID 指令來查看 CPU 是否支持 sysenter/sysexit 指令,做法是將 EAX 寄存器賦值 1,調用 CPUID 指令,寄存器 edx 中第 11 位(這一位名稱為 SEP)就表示是否支持。在調用 CPUID 指令之後,還需要查看 CPU 的 Family、Model、Stepping 屬性來確認,因為據稱 Pentium Pro 處理器會報告 SEP 但是卻不支持 sysenter/sysexit 指令。只有 Family 大於等於 6,Model 大於等於 3,Stepping 大於等於 3 的時候,才能確認 CPU 支持 sysenter/sysexit 指令。

Linux 對 sysenter/sysexit 系統調用方式的支持

在 2.4 內核中,直到最近的發布的 2.4.26-rc2 版本,沒有加入對 sysenter/sysexit 指令的支持。而對 sysenter/sysexit 指令的支持最早是2002 年,由 Linus Torvalds 編寫並首次加入 2.5 版內核中的,經過多方測試和多次 patch,最終正式加入到了 2.6 版本的內核中。

http://kerneltrap.org/node/view/531/1996

http://lwn.net/Articles/18414/

具體談到系統調用的完成,不能孤立的看內核的代碼,我們知道,系統調用多被封裝成庫函數提供給應用程序調用,應用程序調用庫函數後,由 glibc 庫負責進入內核調用系統調用函數。在 2.4 內核加上老版的 glibc 的情況下,庫函數所做的就是通過 int 指令來完成系統調用,而內核提供的系統調用接口很簡單,只要在 IDT 中提供 INT 0x80 的入口,庫就可以完成中斷調用。

在 2.6 內核中,內核代碼同時包含了對 int 0x80 中斷方式和 sysenter 指令方式調用的支持,因此內核會給用戶空間提供一段入口代碼,內核啟動時根據 CPU 類型,決定這段代碼采取哪種系統調用方式。對於 glibc 來說,無需考慮系統調用方式,直接調用這段入口代碼,即可完成系統調用。這樣做還可以盡量減少對 glibc 的改動,在 glibc 的源碼中,只需將 "int $0x80" 指令替換成 "call 入口地址" 即可。

下面,以 2.6.0 的內核代碼配合支持 SYSENTER 調用方式的 glibc2.3.3 為例,分析一下系統調用的具體實現。

內核在啟動時做的准備

前面說到的這段入口代碼,根據調用方式分為兩個文件,支持 sysenter 指令的代碼包含在文件 arch/i386/kernel/vsyscall-sysenter.S 中,支持int中斷的代碼包含在arch/i386/kernel/vsyscall-int80.S中,入口名都是__kernel_vsyscall,這兩個文件編譯出的二進制代碼由arch/i386/kernel/vsyscall.S所包含,並導出起始地址和結束地址。

2.6內核在啟動的時候,調用了新增的函數sysenter_setup(參見arch/i386/kernel/sysenter.c),在這個函數中,內核將虛擬內存空間的頂端一個固定地址頁面(從0xffffe000開始到0xffffeffff的4k大小)映射到一個空閒的物理內存頁面。然後通過之前執行CPUID的指令得到的數據,檢測CPU是否支持sysenter/sysexit指令。如果CPU不支持,那麼將采用INT調用方式的入口代碼拷貝到這個頁面中,然後返回。相反,如果CPU支持SYSETER/SYSEXIT指令,則將采用SYSENTER調用方式的入口代碼拷貝到這個頁面中。使用宏on_each_cpu在每個CPU上執行enable_sep_cpu這個函數。

在enable_sep_cpu函數中,內核將當前CPU的TSS結構中的ss1設置為當前內核使用的代碼段,esp1設置為該TSS結構中保留的一個256字節大小的堆棧。在X86中,TSS結構中ss1和esp1本來是用於保存Ring 1進程的堆棧段和堆棧指針的。由於內核在啟動時,並不能預知調用sysenter指令進入Ring 0後esp的確切值,而應用程序又無權調用wrmsr指令動態設置,所以此時就借用esp1指向一個固定的緩沖區來填充這個MSR寄存器,由於Ring 1根本沒被啟用,所以並不會對系統造成任何影響。在下面的文章中會介紹進入Ring 0之後,內核如何修復ESP來指向正確的Ring 0堆棧。關於TSS結構更細節的應用可參考代碼include/asm-i386/processor.h)。

然後,內核通過wrmsr(msr,val1,val2)宏調用wrmsr指令對當前CPU設置MSR寄存器,可以看出調用宏的第三個參數即edx都被設置為0。其中SYSENTER_CS_MSR的值被設置為當前內核用的所在代碼段;SYSENTER_ESP_MSR被設置為esp1,即指向當前CPU的TSS結構中的堆棧;SYSENTER_EIP_MSR則被設置為內核中處理sysenter指令的接口函數sysenter_entry(參見arch/i386/kernel/entry.S)。這樣,sysenter指令的准備工作就完成了。

通過內核在啟動時進行這樣的設置,在每個進程的進程空間中,都能訪問到內核所映射的這個代碼頁面,當然這個頁面對於應用程序來說是只讀的。我們通過新版的ldd工具查看任意一個可執行程序,可以看到下面的結果:

  [root@test]# file dynamic
  dynamic: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5,   dynamically linked (uses shared libs), not stripped
  [root@test]# ldd dynamic
    linux-gate.so.1 => (0xffffe000)
    libc.so.6 => /lib/tls/libc.so.6 (0x4002c000)
    /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

這個所謂的"linux-gate.so.1"的內容就是內核映射的代碼,系統中其實並不存在這樣一個鏈接庫文件,它的名字是由ldd自己起的,而在老版本的ldd中,雖然能夠檢測到這段代碼,但是由於沒有命名而且在系統中找不到對應鏈接庫文件,所以會有一些顯示上的問題。有關這個問題的背景,可以參考下面這個網址:http://sources.redhat.com/ml/libc-alpha/2003-09/msg00263.html。

Copyright © Linux教程網 All Rights Reserved