X86中斷/異常與APIC

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

外部硬體中斷又分為可屏蔽不可屏蔽中斷;可屏蔽中斷是可以用以下兩個x86_64 -sti和cli指令阻止的中斷。Linux內核中源程式碼如下:

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指令清除該標誌。

1 異常向量(vector)

不論是中斷還是異常,會每個中斷或異常分配一個數來標識,稱為vector number,在X86體系中中斷向量範圍為0-255,最多表示256個異常或中斷,如下所示,用一個8位的無符號整數來表示,前32個vector為處理器保留用作異常處理,32 – 255被指定為用戶定義的中斷,並且不由處理器保留。這些vector通常分配給外部I / O設備,以使這些設備能夠向處理器發送中斷。

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

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 卷 :系統編程指南