異常(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"); }
sti
和cli
經過修改中斷寄存中的IF
標誌位來達到目的, sti
指令設置IF標誌,cli
指令清除該標誌。github
不管是中斷仍是異常,會每一箇中斷或異常分配一個數來標識,稱爲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
在x86中,當外設向CPU發出中斷,中斷不會直接發送到CPU,在舊機器中有一個PIC(可編程中斷控制器),它是一個芯片(如8259),負責順序處理來自對各設備的多箇中斷請求,在如今的新機器中有一個高級可編程中斷控制器(APIC),APIC由Local APIC和I/O APIC兩部分構成,通常來講,全部 LAPIC 都鏈接到一個 I/O APIC 上,造成一個一對多的結構(不排除有多 IOAPIC 的架構):緩存
有兩種工做模式:架構
爲何設備中斷要通過APIC再與CPU相連,而不直接與CPU相連?緣由有二:1)存在大量的外部設備,但CPU的中斷引腳等資源是頗有限的,知足不了全部的直連需求;2)若是設備中斷與CPU直接相連,鏈接關係隨硬件固化,這樣在MP系統中,中斷負載均衡等需求就沒法實現了。負載均衡
Local APIC是一種負責接收/發送中斷的芯片,集成在 CPU 內部,每一個 CPU 有一個屬於本身的 LAPIC。它們經過 APIC ID 進行區分。異步
每一個 LAPIC 都有本身的一系列寄存器、一個內部時鐘(TSC)、一個熱傳感器、一個本地定時設備(APIC-timer)和 兩條 IRQ 線 LINT0 和 LINT1。性能
其中經常使用的寄存器包括:
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 主要處理如下中斷:
其中前 5 種中斷被稱爲本地中斷,LAPIC 在收到後會設置好 LVT(Local Vector Table)的相關寄存器,經過 interrupt delivery protocol 送達 CPU。
LVT 其實是一片連續的地址空間,每 32-bit 一項,做爲各個本地中斷源的 APIC register :
register 被劃分紅多個部分:
最後兩種中斷經過寫 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 後,執行如下流程:
IRR + ISR 的機制決定了同一個中斷最多能夠 pending 兩次,第一次已被送到 CPU 中進行處理,而第二次處於 IRR 中等待送到 CPU 中。
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 的寄存器一樣是經過映射一片物理地址空間實現的:
取消了 APIC bus,LAPIC 與 IOAPIC 直接經過 system bus 通訊。寄存器經過內存映射到物理地址來進行讀寫。
在 APIC 規範中 APIC ID 只有 4bit ,所以最多隻能支持 15 個 CPU。 xAPIC 擴展到 8bit ,支持 255 個。
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 。
PCI Specification 2.2 引入,設備經過向某個 MMIO 地址寫入 system-specified message 可實現向 CPU 發送中斷的效果。
寫入的數據僅能用來決定發送給哪一個 CPU,而不能攜帶更多的信息。
具體的實現方式爲設備經過 PCI write command 向 Message Address Register 指示的地址寫入 Message Data Register 中內容來向 LAPIC 發送中斷。
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 的格式以下:
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。
PCI 3.0 引入。最多容許設備分配 2048 箇中斷,給每一箇中斷都分配一個不一樣的目標地址和 data word,比 MSI 粒度更細(須要 LAPIC 的支持)。
異常/中斷的發生和捕捉,是在硬件層面完成的,異常的處理還須要軟件來完成。在計算機的內存裏,會保存一個表,這個表叫做中斷描述符表(Interrupt Descriptor Table或IDT),每一個異常的處理程序的地址入口做爲一項保存在該表裏,稱爲gates。
CPU使用特殊寄存器IDTR來保存中斷描述符表的位置,可使用lidt
指令將IDT的基地址保存到IDTR,IDTR是一個48bit的寄存器,存放了 IDT 的起始地址和長度。IDTR寄存器結構以下:
當異常產生和捕捉後,CPU會拿到表示該異常的異常向量(vector),接下來會先保存當前程序的執行現場,保存到程序堆棧裏面,而後從 IDTR 拿到IDT表的 base address,加上向量號 * IDT entry size,便可以定位到對應的表項(IDT gate)。
下面來看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 項,它們指向的中斷棧的大小均可以不一樣。目前實現的棧有:
Type
- IDT條目類型:GATE_INTERRUPT,GATE_TRAP、GATE_CALL、GATE_TASK
DPL
- 描述符的權限級別0最高
P
- Segment Present標誌
Segment Present
GDT或LDT代碼段選擇子
48-63 bits
- 處理程序基址的第二部分
64-95 bits
- 處理程序基址的第三部分
96-127 bits
- 由CPU保留
當CPU收到一箇中斷/異常後,CPU 執行如下流程:
若是要以較低的數字特權級別執行處理程序過程,則會發生堆棧切換。從當前執行任務的TSS得到處理程序要使用的堆棧的段選擇器和堆棧指針,加載 tss.esp0 到 esp 中, tss.ss0 到 ss 中,從而切換到內核棧。
若是要以與被中斷過程相同的特權級別執行處理程序,則不須要切換堆棧。
在 32 位下,會根據有沒有特權級切換決定是否壓 ss 和 sp:
在 64 位下不管如何都會壓。這樣一來,保證了全部中斷和異常的棧幀(stackframe)都是同樣大的。在 iret 時也沒必要進行區分,都彈出相同數量的寄存器值。
error code 用於向 handler 傳遞相關信息(並非全部異常都有error code )。好比對於 page fault handler 來講,產生 page fault的緣由有幾個,須要讓handler區別處理,page fault error code 定義以下:
注意的是,爲了防止中斷重入,interrupt gate 在執行時會清掉 eflags 寄存器的 IF bit,而 trap gate 不會這樣作。
要從異常或中斷處理程序過程返回,處理程序必須使用IRET(或IRETD)指令。
IRET指令與RET指令類似,不一樣之處在於它將已保存的標誌恢復到EFLAGS寄存器中。 僅當CPL爲0時,才恢復EFLAGS寄存器的IOPL字段。僅當CPL小於或等於IOPL時,才更改IF標誌。 請參閱英特爾®64和IA 32架構的第3章「指令集參考,A-L」軟件開發人員手冊,第2A卷,介紹了IRET指令執行的完整操做。
若是在調用處理程序過程時發生了堆棧切換,則IRET指令將在返回時切換回被中斷過程的堆棧。