X86中斷/異常與APIC
阿新 • • 發佈:2020-11-29
異常(exception)是由軟體或硬體產生的,分為**同步異常**和**非同步異常**。**同步異常**即CPU執行指令期間同步產生的異常,比如常見的除零錯誤、訪問不在RAM中的記憶體 、MMU 發現當前虛擬地址沒有對應的實體地址,於是觸發一個異常,系統呼叫等。**非同步異常**即平時所說的**中斷**(interrupt),外部硬體硬體給 CPU 傳送的一種訊號,比如說你按下了鍵盤的某一個按鍵,鍵盤控制器於是向 CPU 傳送一箇中斷,通知CPU處理。
外部硬體中斷又分為**可遮蔽**和**不可遮蔽中斷**;可遮蔽中斷是可以用以下兩個x86_64 -sti和cli指令阻止的中斷。Linux核心中原始碼如下:
```C
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`指令清除該標誌。
# 1 異常向量(vector)
不論是中斷還是異常,會每個中斷或異常分配一個數來標識,稱為vector number,在X86體系中中斷向量範圍為0-255,最多表示256個異常或中斷,如下所示,用一個8位的無符號整數來表示,前32個vector為處理器保留用作異常處理,32 - 255被指定為使用者定義的中斷,並且不由處理器保留。這些vector通常分配給外部I / O裝置,以使這些裝置能夠向處理器傳送中斷。
![](https://wsg-blogs-pic.oss-cn-beijing.aliyuncs.com/xenomai/vector.png)
前面說到vector 32 - 255被指定為使用者定義的中斷,通常分配給外部I/O裝置,CPU是如何接受和處理中斷的呢?2.2節內容來源於https://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 的架構):
![](https://wsg-blogs-pic.oss-cn-beijing.aliyuncs.com/xenomai/apic-intel.png)
有兩種工作模式:
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。
![](https://wsg-blogs-pic.oss-cn-beijing.aliyuncs.com/xenomai/lapic-intel.png)
### 暫存器
其中常用的暫存器包括:
- 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 :
![](https://wsg-blogs-pic.oss-cn-beijing.aliyuncs.com/xenomai/intel-lvt.png)
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 廣播
![](https://wsg-blogs-pic.oss-cn-beijing.aliyuncs.com/xenomai/lint-h.png)
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 的格式如下:
![](https://wsg-blogs-pic.oss-cn-beijing.aliyuncs.com/xenomai/mar.png)
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 的格式如下:
![](https://wsg-blogs-pic.oss-cn-beijing.aliyuncs.com/xenomai/lmisdr.png)
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的基地址儲存到**IDTR**,**IDTR**是一個48bit的暫存器,存放了 IDT 的起始地址和長度。IDTR暫存器結構如下:
![](https://wsg-blogs-pic.oss-cn-beijing.aliyuncs.com/xenomai/IDTR-and-IDT.png)
當異常產生和捕捉後,CPU會拿到表示該異常的異常向量(vector),接下來會先儲存當前程式的執行現場,儲存到程式堆疊裡面,然後從 IDTR 拿到IDT表的 base address,加上向量號 * IDT entry size,即可以定位到對應的表項(IDT gate)。
下面來看IDT具體內容。
## 3.1 IDT
**32 bit IDT**
![](https://wsg-blogs-pic.oss-cn-beijing.aliyuncs.com/xenomai/32-idt.png)
32bit處理與64bit類似就不細說,直接看64Bit
**64 bit IDT**
在64位x86下IDT用16位元組描述。
![](https://wsg-blogs-pic.oss-cn-beijing.aliyuncs.com/xenomai/idt-64.png)
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_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保留
## 3.2 中斷/異常處理流程
當CPU收到一箇中斷/異常後,CPU 執行以下流程:
1. 根據向量號在 IDT 中找到對應的表項,即找到中斷描述符。CPU將vector乘以16來找到IDT中的條目(32位系統是乘以8)。
2. 進行特權級檢查。根據中斷描述符表來檢查特權等級。
3. 切換堆疊。
- **如果要以較低的數字特權級別執行處理程式過程,則會發生堆疊切換**。從當前執行任務的TSS獲得處理程式要使用的堆疊的段選擇器和堆疊指標,載入 tss.esp0 到 esp 中, tss.ss0 到 ss 中,從而切換到核心棧。
- 如果要以與被中斷過程**相同的特權級別**執行處理程式,則不需要切換堆疊。
4. 壓棧
在 32 位下,會根據有沒有特權級切換決定是否壓 ss 和 sp:
- 如果發生了堆疊切換,堆疊切換後,處理器將原來的EFLAGS、SS、CS、EIP暫存器依次壓入**新堆疊**中。如果異常導致儲存錯誤程式碼(error code),則將其壓入EIP值之後的**新堆疊**中。
- 若沒有特權級的切換,無需進行棧切換,則在原堆疊上進行操作,處理器將EFLAGS,CS和EIP暫存器的當前狀態儲存在**當前堆疊**中。同樣如果異常導致儲存錯誤程式碼,則將其推入EIP值之後的**當前堆疊**中。
![](https://wsg-blogs-pic.oss-cn-beijing.aliyuncs.com/xenomai/image-20200215154425586.png)
在 64 位下無論如何都會壓。這樣一來,保證了所有中斷和異常的棧幀(stackframe)都是一樣大的。在 iret 時也不必進行區分,都彈出相同數量的暫存器值。
error code 用於向 handler 傳遞相關資訊(並不是所有異常都有error code )。比如對於 page fault handler 來說,產生 page fault的原因有幾個,需要讓handler區別處理,page fault error code 定義如下:
5. 執行handler
注意的是,為了防止中斷重入,interrupt gate 在執行時會清掉 eflags 暫存器的 IF bit,而 trap gate 不會這樣做。
6. 返回原來上下文
要從異常或中斷處理程式過程返回,處理程式必須使用IRET(或IRETD)指令。
> IRET指令與RET指令相似,不同之處在於它將已儲存的標誌恢復到EFLAGS暫存器中。 僅當CPL為0時,才恢復EFLAGS暫存器的IOPL欄位。僅當CPL小於或等於IOPL時,才更改IF標誌。 請參閱英特爾®64和IA 32架構的第3章“指令集參考,A-L”軟體開發人員手冊,第2A卷,介紹了IRET指令執行的完整操作。
**如果在呼叫處理程式過程時發生了堆疊切換,則IRET指令將在返回時切換回被中斷過程的堆疊。**
![idt](https://wsg-blogs-pic.oss-cn-beijing.aliyuncs.com/xenomai/idt.png)
## 3.3 異常處理示例
[系統呼叫](https://blog.csdn.net/qq_22654551/article/details/106845729)
# 參考
[英特爾® 64 位和 IA-32 架構軟體開發人員手冊第 3 卷 :系統程式設計指南](https://www.intel.cn/content/www/cn/zh/architecture-and-technology/64-ia-32-architectures-software-developer-system-programming-manual-32538