2020-03-18-Linux內核17-硬件如何處理中斷和異常

layout title subtitle date author header-img catalog tags
post
Linux內核17-硬件如何處理中斷和異常
基於X86架構的中斷和異常的硬件工作原理
2020-03-18
Tupelo Shen
img/post-bg-unix-linux.jpg
true
Linux
Linux內核
中斷
異常

在上一篇文章中,我們已經瞭解了中斷和異常的一些概念,對於中斷和異常也有了大概的理解。那麼,系統中硬件到底是如何處理中斷和異常的呢?本文我們就以常見的X86架構爲例,看看中斷和異常的硬件工作原理。

1 高級可編程中斷控制器-APIC

之前,我們主要考慮的單處理器系統,如果是多處理器系統,主PIC控制器的INTR管腳是如何接到CPU上的?我們接下來討論這個話題。

我們知道,多核處理系統的價值在於 並行處理。所以,如何把中斷分配到每一個CPU上就至關重要了。基於這個原因,Intel從奔騰III開始,引入一個新的高級可編程中斷控制器(I/O-APIC)。這個控制器是8259A中斷控制器的加強版。爲了兼容舊版本的操作系統,有些主板包含這兩種芯片。x86架構中,每個處理器包含自己的APIC,每個APIC具有32位的寄存器,內部時鐘,內部定時器以及2個額外的IRQ線,LINT0和LINT1,用作APIC的中斷。所有私有的APIC都連接到I/O-APIC,組成一個多APIC系統。

圖4-1展示了一個多APIC系統的原理圖。I/O-APIC通過APIC總線和各個APIC連接在一起。I/O-APIC相等於一箇中繼的角色。

圖4-1 多APIC系統

I/O-APIC由24條中斷線,中斷重定向表,可編程寄存器和一個通過APIC總線收發數據的消息單元組成。與8259A中斷控制器不同,管腳編號不再具有優先級:重定向表中的每一項都可以被獨立設置中斷向量和優先級,目的處理器以及處理器如何處理該中斷。也就是說,中斷重定向表就是外部IRQ到私有APIC的映射關係。

中斷請求被分配到CPU上的方式有兩種:

  1. 靜態分配

    按照重定向表中的定義把IRQ請求分配到相應的私有APIC高級可編程中斷控制器上。中斷可以指定給單個CPU,或者一組CPU,或者所有的CPU(相當於廣播)。

  2. 動態分配

    IRQ請求被髮送給正在運行低優先級進程的處理器的私有APIC中斷控制器上。通俗地說,就是哪個處理器正在運行低優先級任務,IRQ請求就發送給誰。

    每個私有APIC都有一個可編程任務優先級寄存器,用來保存當前運行任務的優先級。Intel期望每次進程切換的時候,操作系統內核修改這個寄存器。

    如果有多個CPU擁有相同的最低任務優先級,則使用仲裁技術分配中斷請求。根據仲裁,每個CPU被分配一個不同的優先級(0-15,數字越小,優先級越大),這個優先級存儲在私有APIC的任務優先級寄存器中。

    分配策略是,每當分配一箇中斷請求給一個CPU,則它對應的仲裁優先級被自動設爲0,而其它CPU的仲裁優先級則被增加。當優先級寄存器中的值大於15時,則設爲1。因爲具有相同任務優先級的CPU的中斷分配使用循環方式進行。

    動態分配的策略就是負載均衡的一種手段。關於負載均衡的算法以後再研究。

除了CPU與外設之間的中斷,多APIC系統還允許CPU產生CPU之間的中斷。當一個CPU想給另一個CPU發送中斷時,它就會把目標CPU的私有APIC的標識符和中斷號存儲到自己APIC的中斷命令寄存器(ICR)中。然後通過APIC總線發送給目標APIC,該APIC就會給自己的CPU發送一個相應的中斷。

CPU間的中斷(簡稱IPI)是多核系統一個重要組成部分。Linux有效地利用它們,在CPU之間傳遞消息。

目前,大部分的單核系統也都包含一個I/O-APIC芯片,可以使用兩種不同的方式配置它:

  1. 當一個標準的8259A類型的外部PIC使用。私有APIC被禁止,LINT0和LINT1這兩個IRQ請求線被分別配置爲INTR和NMI管腳。

  2. 作爲標準的I/O-APIC使用,只不過只有一個CPU而已。

2 異常

x86架構大約有20種不同的異常。內核必須爲每種異常提供專用的處理函數。對於某些異常,CPU控制單元也會產生硬件錯誤碼,並將其壓入內核態棧,然後再啓動異常處理函數。

下表是異常列表,列出了異常號,名稱,類型等等。更多信息請參考Intel技術手冊。

# 異常 類型 異常處理函數 信號
0 除法錯誤 fault divide_error() SIGFPE
1 Debug trap/fault debug( ) SIGTRAP
2 NMI - nmi( ) -
3 斷點 trap int3( ) SIGTRAP
4 溢出 trap overflow( ) SIGSEGV
5 邊界檢查 fault bounds( ) SIGSEGV
6 非法操作碼 fault invalid_op( ) SIGILL
7 設備不可用 fault device_not_available( ) -
8 串行處理異常錯誤 abort doublefault_fn() -
9 協處理器錯誤 abort coprocessor_segment_overrun( ) SIGFPE
10 非法TSS fault invalid_TSS( ) SIGSEGV
11 段引用錯誤 fault segment_not_present( ) SIGBUS
12 棧段錯誤 fault stack_segment( ) SIGBUS
13 通用保護 fault general_protection( ) SIGSEGV
14 頁錯誤 fault page_fault( ) SIGSEGV
15 Intel保留 - - -
16 浮點錯誤 fault coprocessor_error( ) SIGFPE
17 對齊檢查 fault alignment_check( ) SIGBUS
18 機器檢查 abort machine_check() -
19 SIMD浮點異常 fault simd_coprocessor_error() SIGFPE

Intel保留20-31未來使用。如上表所示,每個異常都有一個專門的處理函數處理,並給造成異常的進程發送一個信號。

3 中斷描述符表

現在,我們已經知道了中斷信號是如何從設備發出,然後經過高級可編程中斷控制器的分配,到達各個指定的CPU中。那麼,剩下的工作就是內核的了,內核使用一箇中斷描述符表(IDT),記錄每個中斷或者異常編號以及相應的處理函數。那麼,收到中斷信號後,將相應的處理函數的地址加載到eip寄存器中執行即可。

IDT表中,每一項對應一箇中斷或者異常,大小8個字節。因而,IDT需要256x8=2048個字節大小的存儲空間。

IDT表的物理地址存儲在CPU寄存器idtr中:包括IDT的基地址和最大長度。在使能中斷之前,必須使用lidt彙編指令初始化IDT表。

IDT表包含三種類型的描述符,使用Type位域表示(40-43位)。下圖分別解釋了這三種描述符各個位的意義。

三種描述符分別爲:

  1. 任務門

    包含中斷髮生時要替換當前進程的新進程的TSS選擇器。

  2. 中斷門

    包含段選擇器和在段中的偏移量。設置了正確的段後,處理器清除IF標誌,禁止可屏蔽中斷。

  3. 陷阱門

    同中斷門類似,只是不會修改IF標誌。

4 中斷和異常的硬件處理

現在,我們來探究以下CPU控制單元是如何處理中斷和異常的。我們假設內核已經完成初始化,CPU工作在保護模式下。

CPU控制單元,在取指令之前,檢查控制單元在執行前一條指令的時候是否有中斷或異常發生。如果發生中斷,控制單元就會做如下處理:

  1. 確定中斷或異常的編號N;

  2. 讀取IDT表中的第N項;(在後面的描述中,假設包含的是中斷門或陷阱門)

  3. 獲取GDT的基地址,遍歷GDT找到IDT表第N項中的段選擇器標識的段描述符。這個描述符指定了包含中斷或異常處理程序的段的基地址。

  4. 確保中斷合法性。

    首先比較cs寄存器中的CPL(當前特權等級)和包含在GDT中的段描述符的DPL(描述符特權等級),如果CPL小於DPL,產生 通用保護 異常,因爲中斷處理程序的特權等級不能比造成中斷的程序的低。對於可編程異常,還會做進一步的安全檢查:比較當前特權等級(CPL)和IDT表中包含的描述符的DPL,如果DPL小於CPL,則產生通用保護的異常。後一項檢查,可以阻止用戶應用程序訪問特定的trap或中斷門。

  5. 檢查特權等級是否發生變化。如果CPL與描述符中的DPL不同,控制單元應該使用新特權等級下的堆棧。

    1. 讀取tr寄存器,訪問運行中的進程的TSS段;
    2. 使用新特權等級對應的堆棧段和堆棧指針加載ss和esp寄存器;(這些值存儲在TSS中)
    3. 在新的堆棧中,保存舊任務的ss和esp寄存器值。(處理完中斷或異常後,還要恢復到舊任務執行)

    其實對於Linux來說,只使用了supervisor和user兩種特權等級。所以中斷應該都是在supervisor特權等級下運行。

  6. 根據造成異常的指令的邏輯地址,加載cs和eip寄存器(異常解決後,程序可以繼續從這兒執行);

  7. 保存eflags、cs和eip到堆棧中;

  8. 如果異常攜帶異常錯誤碼,將其保存在堆棧中;

  9. 根據IDT表中的第N項內容,加載cs和eip寄存器。

至此,CPU控制單元跳轉到中斷或異常處理程序處開始執行。等到中斷或異常處理完成後,把CPU的使用權讓給之前被中斷的進程,使用iret指令,該指令強迫控制單元執行下面步驟:

  1. 加載被中斷進程的cs,eip和eflags寄存器。(如果壓棧過異常錯誤碼,應該在執行iret指令之前彈出)
  2. 檢查CPL是否等於cs寄存器中的CPL,如果相等,則iret指令結束執行;否則,繼續。
  3. 加載舊特權等級的ss和esp寄存器值。
  4. 檢查ds、es、fs和gs寄存器中的值。如果它們之中任何一個的描述符中的DPL小於CPL,則清除相應的段寄存器。這麼做,可以禁止用戶態程序使用先前內核態的段寄存器。如果這些寄存器沒有被清除,惡意用戶態程序就可以利用它們訪問內核地址空間。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章