X86中斷/異常與APIC

異常(exception)是由軟件或硬件產生的,分爲同步異常異步異常同步異常即CPU執行指令期間同步產生的異常,好比常見的除零錯誤、訪問不在RAM中的內存 、MMU 發現當前虛擬地址沒有對應的物理地址,因而觸發一個異常,系統調用等。異步異常即平時所說的中斷(interrupt),外部硬件硬件給 CPU 發送的一種信號,好比說你按下了鍵盤的某一個按鍵,鍵盤控制器因而向 CPU 發送一箇中斷,通知CPU處理。html

外部硬件中斷又分爲可屏蔽不可屏蔽中斷;可屏蔽中斷是能夠用如下兩個x86_64 -sti和cli指令阻止的中斷。Linux內核中源代碼以下:git

static inline void native_irq_disable(void)
{
	asm volatile("cli": : :"memory");
}

static inline void native_irq_enable(void)
{
	asm volatile("sti": : :"memory");
}

sticli經過修改中斷寄存中的IF標誌位來達到目的, sti指令設置IF標誌,cli指令清除該標誌。github

1 異常向量(vector)

不管是中斷仍是異常,會每一箇中斷或異常分配一個數來標識,稱爲vector number,在X86體系中中斷向量範圍爲0-255,最多表示256個異常或中斷,以下所示,用一個8位的無符號整數來表示,前32個vector爲處理器保留用做異常處理,32 - 255被指定爲用戶定義的中斷,而且不禁處理器保留。這些vector一般分配給外部I / O設備,以使這些設備可以向處理器發送中斷。編程

前面說到vector 32 - 255被指定爲用戶定義的中斷,一般分配給外部I/O設備,CPU是如何接受和處理中斷的呢?2.2節內容來源於https://github.com/GiantVM/doc/tree/master/interrupt_and_ioapi

2 高級可編程中斷控制器(APIC)

在x86中,當外設向CPU發出中斷,中斷不會直接發送到CPU,在舊機器中有一個PIC(可編程中斷控制器),它是一個芯片(如8259),負責順序處理來自對各設備的多箇中斷請求,在如今的新機器中有一個高級可編程中斷控制器(APIC),APIC由Local APIC和I/O APIC兩部分構成,通常來講,全部 LAPIC 都鏈接到一個 I/O APIC 上,造成一個一對多的結構(不排除有多 IOAPIC 的架構):緩存

有兩種工做模式:架構

  1. 8259A 模式: 禁用 LAPIC,APIC 直連 CPU
  2. 標準模式: 啓用 LAPIC,全部的外部中斷經過 IOAPIC 接收後轉發給對應的 LAPIC

爲何設備中斷要通過APIC再與CPU相連,而不直接與CPU相連?緣由有二:1)存在大量的外部設備,但CPU的中斷引腳等資源是頗有限的,知足不了全部的直連需求;2)若是設備中斷與CPU直接相連,鏈接關係隨硬件固化,這樣在MP系統中,中斷負載均衡等需求就沒法實現了。負載均衡

2.1 Local APIC(LAPIC)

Local APIC是一種負責接收/發送中斷的芯片,集成在 CPU 內部,每一個 CPU 有一個屬於本身的 LAPIC。它們經過 APIC ID 進行區分。異步

每一個 LAPIC 都有本身的一系列寄存器、一個內部時鐘(TSC)、一個熱傳感器、一個本地定時設備(APIC-timer)和 兩條 IRQ 線 LINT0 和 LINT1。性能

寄存器

其中經常使用的寄存器包括:

  • ICR(Interrupt Command Register) 用於發送 IPI
  • IRR(Interrupt Request Register) 當前 LAPIC 接收到的中斷請求
  • ISR(In-Service Register) 當前 LAPIC 送入 CPU 中 (CPU 正在處理) 的中斷請求
  • TPR(Task Priority Register) 當前 CPU 處理中斷所需的優先級
  • PPR(Processor Priority Register) 當前 CPU 處理中斷所需的優先級,只讀,由 TPR 決定

IRR與ISR兩個寄存器,在處理一個vector的同時,緩存一個相同的vector,vector經過2個256-bit的寄存器標識,256個bit表明256個可能的vector,置1表示上報了相應的vector請求處理或者正在處理中。

優先級

中斷向量的vector的高4位(bit4-7)爲Interrupt-Priority class,每一個 class 包含 16 箇中斷向量。0-15 號中斷向量的 class 爲 0,但其不合法,這些中斷永遠不會提交。在 Intel 64 和 IA-32 架構中,0-31 號中斷向量被保留,所以 class 0-1 不可用。中斷向量的 bit0-3 決定了同 class 下的優先級,越大在 class 內的優先級就越高,因爲vector 0-31是CPU保留,因此可用中斷優先級範圍爲2-15。

PPR 決定了 CPU 接受的中斷。只有 Interrupt-Priority class 大於 Processor-Priority Class 的中斷纔會被送到 CPU 中(注意, NMI / SMI / INIT / ExtINT / SIPI 不受該限制)。Processor-Priority Sub-Class 不影響中斷的送達,只是用來湊數而已。

Local APIC的TPR和PPR用於設置task優先級和CPU優先級,這兩個寄存器的值控制着CPU處理該中斷行爲,當I/O APIC轉發的中斷vector優先級小於Local APIC TPR設置的值時,此中斷不會打斷該CPU上運行的task,當I/O APIC轉發的中斷vector優先級小於Local APIC PPR值時,該CPU不處理該中斷,操做系統經過動態設置local APIC TPR和PPR,來實現操做系統的實時性需求和負載均衡。

中斷類型

LAPIC 主要處理如下中斷:

  • APIC Timer 產生的中斷(APIC timer generated interrupts)
  • Performance Monitoring Counter 在 overflow 時產生的中斷(Performance monitoring counter interrupts)
  • 溫度傳感器產生的中斷(Thermal Sensor interrupts)
  • LAPIC 內部錯誤時產生的中斷(APIC internal error interrupts)
  • 本地直連 IO 設備 (Locally connected I/O devices) 經過 LINT0 和 LINT1 引腳發來的中斷
  • 其餘 CPU (甚至是本身,稱爲 self-interrupt)發來的 IPI(Inter-processor interrupts)
  • IOAPIC 發來的中斷

其中前 5 種中斷被稱爲本地中斷,LAPIC 在收到後會設置好 LVT(Local Vector Table)的相關寄存器,經過 interrupt delivery protocol 送達 CPU。

LVT 其實是一片連續的地址空間,每 32-bit 一項,做爲各個本地中斷源的 APIC register :

register 被劃分紅多個部分:

  • bit 0-7: Vector,即CPU收到的中斷向量號,其中0-15號被視爲非法,會產生一個Illegal Vector錯誤(即ESR的bit 6,詳下)
  • bit 8-10: Delivery Mode,有如下幾種取值:
    • 000 (Fixed):按Vector的值向CPU發送相應的中斷向量號
    • 010 (SMI):向CPU發送一個SMI,此模式下Vector必須爲0
    • 100 (NMI):向CPU發送一個NMI,此時Vector會被忽略
    • 101 (INIT):向CPU發送一個 INIT,此模式下Vector必須爲0
    • 111 (ExtINT):令CPU按照響應外部8259A的方式響應中斷,這將會引發一個INTA週期,CPU在該週期向外部控制器索取Vector。APIC只支持一個ExtINT中斷源,整個系統中應當只有一個CPU的其中一個LVT表項配置爲ExtINT模式
  • bit 12: Delivery Status(只讀),取0表示空閒,取1表示CPU還沒有接受該中斷(還沒有EOI)
  • bit 13: Interrupt Input Pin Polarity,取0表示active high,取1表示active low
  • bit 14: Remote IRR Flag(只讀),若當前接受的中斷爲fixed mode且是level triggered的,則該位爲1表示CPU已經接受中斷(已將中斷加入IRR),但還沒有進行EOI。CPU執行EOI後,該位就恢復到0
  • bit 15: Trigger Mode,取0表示edge triggered,取1表示level triggered(具體使用時尚有許多注意點,詳見手冊10.5.1節)
  • bit 16: 爲Mask,取0表示容許接受中斷,取1表示禁止,reset後初始值爲1
  • bit 17/17-18: Timer Mode,只有LVT Timer Register有,用於切換APIC Timer的三種模式

最後兩種中斷經過寫 ICR 來發送。當對 ICR 進行寫入時,將產生 interrupt message 並經過 system bus(Pentium 4 / Intel Xeon) 或 APIC bus(Pentium / P6 family) 送達目標 LAPIC 。

當有多個 APIC 向經過 system bus / APIC bus 發送 message 時,須要進行仲裁。每一個 LAPIC 會被分配一個仲裁優先級(範圍爲 0-15),優先級最高的拿到 bus,從而可以發送消息。在消息發送完成後,剛剛發送消息的 LAPIC 的仲裁優先級會被設置爲 0,其餘的 LAPIC 會加 1。

中斷髮送流程

舉個例子:當一個 CPU 想要向其餘 CPU 發送中斷時,就在本身的 ICR(interrupt command ragister) 中存放對應的中斷向量和目標 LAPIC ID 標識。而後由 system bus(Pentium 4 / Intel Xeon) 或 APIC bus(Pentium / P6 family) 直接傳遞到目標 LAPIC。

中斷接收流程

一個 LAPIC 在收到一個 interrupt message 後,執行如下流程:

  1. 判斷本身是否屬於消息指定的 destination ,若是不是,拋棄該消息
  2. 若是中斷的 Delivery Mode 爲 NMI / SMI / INIT / ExtINT / SIPI ,則直接將中斷髮送給 CPU
  3. 若是不是以上的 Mode ,則設置中斷消息在 IRR 中對應的 bit。若是 IRR 中 bit 已被設置(沒有 open slot),則拒絕該請求,而後給 sender 發送一個 retry 的消息
  4. 對於 IRR 中的中斷,LAPIC 每次會根據中斷的優先級和當前 CPU 的優先級 PPR 選出一個發送給 CPU,會清空該中斷在 IRR 中對應的 bit,並設置該中斷在 ISR 中對應的 bit
  5. CPU 在收到 LAPIC 發來的中斷後,經過中斷 / 異常處理機制進行處理。處理完畢後,向 LAPIC 的 EOI(end-of-interrupt)寄存器進行寫入(NMI / SMI / INIT / ExtINT / SIPI 無需寫入)
  6. LAPIC 清除 ISR 中該中斷對應的 bit(只針對 level-triggered interrupts)
  7. 對於 level-triggered interrupt, EOI 會被髮送給全部的 IOAPIC。能夠經過設置 Spurious Interrupt Vector Register 的 bit12 來避免 EOI 廣播

IRR + ISR 的機制決定了同一個中斷最多能夠 pending 兩次,第一次已被送到 CPU 中進行處理,而第二次處於 IRR 中等待送到 CPU 中。

2.2 IO APIC

IOAPIC (I/O Advanced Programmable Interrupt Controller) 屬於 Intel 芯片組的一部分,也就是說一般位於南橋.

像 PIC 同樣,鏈接各個設備,負責接收外部 IO 設備 (Externally connected I/O devices) 發來的中斷,典型的 IOAPIC 有 24 個 input 管腳(INTIN0~INTIN23),沒有優先級之分。

I/O APIC提供多處理器中斷管理,用於CPU核之間分配外部中斷,在某個管腳收到中斷後,按必定規則將外部中斷處理成中斷消息發送到Local APIC。

寄存器

和 LAPIC 同樣,IOAPIC 的寄存器一樣是經過映射一片物理地址空間實現的:

  • IOREGSEL(I/O REGISTER SELECT REGISTER): 選擇要讀寫的寄存器
  • IOWIN(I/O WINDOW REGISTER): 讀寫 IOREGSEL 選中的寄存器
  • IOAPICVER(IOAPIC VERSION REGISTER): IOAPIC 的硬件版本
  • IOAPICARB(IOAPIC ARBITRATION REGISTER): IOAPIC 在總線上的仲裁優先級
  • IOAPICID(IOAPIC IDENTIFICATION REGISTER): IOAPIC 的 ID,在仲裁時將做爲 ID 加載到 IOAPICARB 中
  • IOREDTBL(I/O REDIRECTION TABLE REGISTERS): 有 0-23 共 24 個,對應 24 個引腳,每一個長 64bit。當該引腳收到中斷信號時,將根據該寄存器產生中斷消息送給相應的 LAPIC

2.3 擴展

xAPIC(extended APIC)

取消了 APIC bus,LAPIC 與 IOAPIC 直接經過 system bus 通訊。寄存器經過內存映射到物理地址來進行讀寫。

在 APIC 規範中 APIC ID 只有 4bit ,所以最多隻能支持 15 個 CPU。 xAPIC 擴展到 8bit ,支持 255 個。

x2APIC

x2APIC 將 APIC ID 擴展到 32bit ,佔 APIC ID Register 的32位,所以支持 \(2^{32}-1\)個 CPU。

寄存器被改成只讀,只會在開機時由硬件設置一次,其末8位被做爲 xAPIC 模式下的 APIC ID 。

新增了 Self IPI Register ,向該寄存器寫入 Interrupt Vector 可實現發送一個 Edge Triggered + Fixed Interrupt 的 Self IPI 。

2.4 MSI(Message Signaled Interrupt)

PCI Specification 2.2 引入,設備經過向某個 MMIO 地址寫入 system-specified message 可實現向 CPU 發送中斷的效果。

寫入的數據僅能用來決定發送給哪一個 CPU,而不能攜帶更多的信息。

具體的實現方式爲設備經過 PCI write command 向 Message Address Register 指示的地址寫入 Message Data Register 中內容來向 LAPIC 發送中斷。

Message Address Register

Message Address Register 的格式以下:

Destination ID 字段存放了中斷要發往 LAPIC ID。該 ID 也會記錄在 I/O APIC Redirection Table 中每一個表項的 bit56-63 。Redirection hint indication 指定了 MSI 是否直接送達 CPU。 Destination mode 指定了 Destination ID 字段存放的是邏輯仍是物理 APIC ID 。

Message Data Register

Message Data Register 的格式以下:

Vector 指定了中斷向量號, Delivery Mode 定義同傳統中斷,表示中斷類型。Trigger Mode 爲觸發模式,0 爲邊緣觸發,1 爲水平觸發。 Level 指定了水平觸發中斷時處於的電位(邊緣觸發無須設置該字段)。

優勢

容許設備分配 1/2/4/8/16/32 箇中斷。

傳統中斷基於的引腳 (pin) 每每被多個設備所共享。中斷觸發後,OS 須要調用對應的中斷處理例程來肯定產生中斷的設備,耗時較長。而 MSI 中斷只屬於一個特定的設備,不存在該問題。

傳統中斷一般是設備寫完數據 (DMA) 後,給 CPU 一箇中斷請求,通知 CPU 進行處理。可是可能因爲某些緣由(優化?),PCI bridge 或 Memory controller 可能會延遲寫數據操做,致使 CPU 在收到中斷時,數據還未到達內存。爲了解決這個問題,interrupt handlers 必須從經過輪詢來確保寫操做已經完成,具體操做是訪問一個寄存器,只有數據到達內存後,寄存器纔會返回值(PCI 事務保證),這樣致使性能很差。而 MSI 的中斷本質上也是寫內存,這樣就保證了寫內存後發中斷這樣的流程是串行的,於是避免了輪詢的問題。

傳統中斷先發送到 IOAPIC 後再轉發給對應的 LAPIC ,路徑較長。MSI 能讓設備直接將中斷送達 LAPIC 。

缺點

沒法保證 Interrupt Latency,MSG 可能會被 Host/Loading Cache 這樣就可能會出現 Latency,另外當 Loading 重的時候也可能會出現比較大的 Latency。

2.5 MSI-X

PCI 3.0 引入。最多容許設備分配 2048 箇中斷,給每一箇中斷都分配一個不一樣的目標地址和 data word,比 MSI 粒度更細(須要 LAPIC 的支持)。

3 中斷/異常處理

異常/中斷的發生和捕捉,是在硬件層面完成的,異常的處理還須要軟件來完成。在計算機的內存裏,會保存一個表,這個表叫做中斷描述符表(Interrupt Descriptor Table或IDT),每一個異常的處理程序的地址入口做爲一項保存在該表裏,稱爲gates

CPU使用特殊寄存器IDTR來保存中斷描述符表的位置,可使用lidt指令將IDT的基地址保存到IDTRIDTR是一個48bit的寄存器,存放了 IDT 的起始地址和長度。IDTR寄存器結構以下:

當異常產生和捕捉後,CPU會拿到表示該異常的異常向量(vector),接下來會先保存當前程序的執行現場,保存到程序堆棧裏面,而後從 IDTR 拿到IDT表的 base address,加上向量號 * IDT entry size,便可以定位到對應的表項(IDT gate)。

下面來看IDT具體內容。

3.1 IDT

32 bit IDT

32bit處理與64bit相似就不細說,直接看64Bit

64 bit IDT

在64位x86下IDT用16字節描述。

IDT圖包含以下字段:
0-15 bits - 從segment select的偏移,處理器使用該段選擇器做爲中斷處理程序入口點的基址;

16-31 bits - segment select的基地址,包含中斷處理程序的入口點;

IST - x86_64提供切換到新堆棧以進行中斷處理的功能。

32位與64位對比,能夠發現 byte 4-7 的 bit 0-4 由 reserved 變成了 IST(Interrupt Stack Table),而 offset 在 64 位下須要擴展爲 64 bit,所以 byte 8-11 將保存 offset 的 bit 32-63 。

IST 是 64 位引入的新的棧切換機制。在收到中斷 / 異常時,若是中斷對應的 IDT 表項中 IST 字段非 0,則硬件會自動切換到對應的中斷棧(中斷棧的指針存放在 TSS 中,被加載到 rsp)。IST 最多有 7 項,它們指向的中斷棧的大小均可以不一樣。目前實現的棧有:

  • DOUBLEFAULT_STACK:專門用於 Double Fault Exception ,由於 double fault 時不該該再用原來的中斷棧。大小爲 EXCEPTION_STKSZ
  • NMI_STACK:專門用於不可屏蔽中斷,由於 NMI 可能在任意時刻到來,若是此時正在切換棧則會引發混亂。大小爲 EXCEPTION_STKSZ
  • DEBUG_STACK:專門用於 debug 中斷,由於 debug 中斷可能在任意時刻到來。大小爲 DEBUG_STKSZ
  • MCE_STACK:專門用於 Machine Check Exception ,由於 MCE 中斷可能在任意時刻到來。大小爲 EXCEPTION_STKSZ

Type - IDT條目類型:GATE_INTERRUPTGATE_TRAPGATE_CALLGATE_TASK

DPL - 描述符的權限級別0最高

P - Segment Present標誌

Segment Present GDT或LDT代碼段選擇子

48-63 bits - 處理程序基址的第二部分

64-95 bits - 處理程序基址的第三部分

96-127 bits - 由CPU保留

3.2 中斷/異常處理流程

當CPU收到一箇中斷/異常後,CPU 執行如下流程:

  1. 根據向量號在 IDT 中找到對應的表項,即找到中斷描述符。CPU將vector乘以16來找到IDT中的條目(32位系統是乘以8)。
  2. 進行特權級檢查。根據中斷描述符表來檢查特權等級。
  3. 切換堆棧。
  • 若是要以較低的數字特權級別執行處理程序過程,則會發生堆棧切換。從當前執行任務的TSS得到處理程序要使用的堆棧的段選擇器和堆棧指針,加載 tss.esp0 到 esp 中, tss.ss0 到 ss 中,從而切換到內核棧。

  • 若是要以與被中斷過程相同的特權級別執行處理程序,則不須要切換堆棧。

  1. 壓棧

在 32 位下,會根據有沒有特權級切換決定是否壓 ss 和 sp:

  • 若是發生了堆棧切換,堆棧切換後,處理器將原來的EFLAGS、SS、CS、EIP寄存器依次壓入新堆棧中。若是異常致使保存錯誤代碼(error code),則將其壓入EIP值以後的新堆棧中。
  • 若沒有特權級的切換,無需進行棧切換,則在原堆棧上進行操做,處理器將EFLAGS,CS和EIP寄存器的當前狀態保存在當前堆棧中。一樣若是異常致使保存錯誤代碼,則將其推入EIP值以後的當前堆棧中。

在 64 位下不管如何都會壓。這樣一來,保證了全部中斷和異常的棧幀(stackframe)都是同樣大的。在 iret 時也沒必要進行區分,都彈出相同數量的寄存器值。

error code 用於向 handler 傳遞相關信息(並非全部異常都有error code )。好比對於 page fault handler 來講,產生 page fault的緣由有幾個,須要讓handler區別處理,page fault error code 定義以下:

  1. 執行handler

注意的是,爲了防止中斷重入,interrupt gate 在執行時會清掉 eflags 寄存器的 IF bit,而 trap gate 不會這樣作。

  1. 返回原來上下文

要從異常或中斷處理程序過程返回,處理程序必須使用IRET(或IRETD)指令。

IRET指令與RET指令類似,不一樣之處在於它將已保存的標誌恢復到EFLAGS寄存器中。 僅當CPL爲0時,才恢復EFLAGS寄存器的IOPL字段。僅當CPL小於或等於IOPL時,才更改IF標誌。 請參閱英特爾®64和IA 32架構的第3章「指令集參考,A-L」軟件開發人員手冊,第2A卷,介紹了IRET指令執行的完整操做。

若是在調用處理程序過程時發生了堆棧切換,則IRET指令將在返回時切換回被中斷過程的堆棧。

idt

3.3 異常處理示例

系統調用

參考

英特爾® 64 位和 IA-32 架構軟件開發人員手冊第 3 卷 :系統編程指南

相關文章
相關標籤/搜索