一:引言
在Intel的文檔中,把中斷分為兩種.一種是異常,也叫同步同斷.一種稱之為中斷,也叫異常中斷.
同步中斷指的是由CPU控制單元產生,之所以稱之為同步,是只有一條指令執行完畢后才會發出中斷.例如除法運算中,除數為零的時候,就會產生一個異常
非同步中斷是由外部設備按照CPU的時鐘隨機產生的.例如,網卡檢測到一個數據到來就會產生一個中斷.
二:x86的中斷處理過程
由於中斷是開著的,所以當執行完一條指令后,cs和eip這對寄存器中已經包含了下一條將要執行的指令的邏輯地址.在處理那條指令之前,控制單元會檢查在運行前一條指令時是否發生了一個中斷或異常.如果發生了一個中斷和異常,那麼控制單元執行下列操作:
1. 確定與中斷或異常關聯的向量i(0≤ i ≤255)
2. 讀由idtr寄存器指向的IDT表中的第i項.
3. 從gdtr寄存器獲得GDT的基地址,並在GDT中查找,以讀取IDT表項中的選擇符標識的段描述符.這個描述符指定中斷或異常處理程序所在的段的基地址.
4. 確信中斷是由授權的(中斷)發生源發出的.將當前特權級CPL(存放在cs寄存器的低兩位)與段描述符(存放在GDT中)的描述符特權級DPL比較.如果CPL小於DPL,就產生一個「通常保護」異常,中斷處理程序的特權級不能低於引起中斷的程序的特權.對於編程異常,則做進一步的安全檢查:比較CPL與處於IDT中的門描述符的DPL,如果DPL小於CPL,就產生一個「通常保護」異常,這
一個檢查可以避免用戶應用程序訪問特殊的陷阱門和中斷門.
5. 檢查是否發生了特權級的變化,也就是說,CPL是否不同於所選擇的段描述符的DPL.如果是,控制單元必須開始使用與新的特權級相關的棧,通過執行以下步驟來保證這一點:
A. 讀tr寄存器,以訪問運行進程的TSS段.
B. 用與新特權級相關的棧段和棧指針的正確值裝載ss和esp寄存器.這些值可以在TSS中找到.
C. 在新的棧中保存ss和esp以前的值,這些值定義了與舊特權級相關的棧的邏輯地址.
6. 如果故障已發生,用引起異常的指令地址裝載cs和eip寄存器,從而是的這條指令能再次被執行.
7. 在棧中保存eflag、cs和eip的內容.
8. 如果異常產生了一個硬體出錯碼,則將它保存在棧中.
9. 裝載cs和eip寄存器,其值分別是IDT表中第i項門描述符的段選擇符和偏移量欄位.這些值給出了中斷或者異常處理程序的第一條指令的邏輯地址.
控制單元所執行的一步就是跳轉到中斷或異常處理程序.換句話說,處理完中斷信號后,控制單元所執行的指令就是被選中處理程序的第一條指令.
上面的處理過程的描述摘自<<深入理解linux內核>>,其中有幾點值得注意的地方:
1:通過門后,只能提高運行級別.就像上面所述的 「當前特權級CPL(存放在cs寄存器的低兩位)與段描述符(存放在GDT中)的描述符特權級DPL比較.如果CPL小於DPL,就產生一個「通常保護」異常」.在中斷處理中,通常把IDT中的相應段選擇符設為__KERNEL_CS.即最高的運行級別
2:上面C所述:「在新的棧中保存ss和esp以前的值,這些值定義了與舊特權級相關的棧的邏輯地址」,那ss,esp以前的值是如何找到的呢?應該是從TSS中.在中斷髮生的時候,如果檢測到運行級別發生了改了,將寄存器SS,ESP中的值保存進TSS的相應級別位置.再載入新的SS,ESP的值,然後從TSS中取出舊的SS,ESP值,再壓棧.
3:堆棧的改變,如下圖所示:
從上圖中我們可以看到,硬體自動保存的硬體環境是非常少,要在中斷後恢復到以前的環境,還需要保存更多的寄存器值,這是由操作系統完成的.這我們在以後的代碼分析中可以看到
中斷和異常被處理完畢后,相應的處理程序必須產生一條iret指令,把控制權轉交給被中斷的進程,這將迫使控制單元:
1. 用保存在棧中的值裝載cs、eip和eflag寄存器.如果一個硬體出錯碼曾被壓入棧中,並且在eip內容的上面,那麼,執行iret指令前必須先彈出這個硬體出錯碼.
2. 檢查處理程序的CPL是否等於cs中的低兩位的值.如果是,iret終止返回;否則,轉入下一步.
3. 從棧中轉載ss和esp寄存器,因此,返回到與舊特權級相關的棧.
4. 檢查ds、es、fs及gs段寄存器的內容,如果其中一個寄存器包含的選擇符是一個段描述符,並且其DPL值小於CPL,那麼,清相關的段寄存器.控制單元這麼做是為了禁止用戶態的程序利用內核以前所用的段寄存器.如果不清除這些寄存器的話,惡意的用戶程序就會利用他們來訪問內核地址空間.
注意到4:舉例說明一下.如果通過系統調用進入內核態.然後將DS,ES的值賦為__KERNEL_DS(在2.4的內核里),處理完后(調用iret后),恢復CS,EIP的值,此時CS的CPL是3.DS,ES被設為了__KERNEL_DS,所以其DPL是0,所以要將DS,ES中的值清除.在2.6內核中,發生中斷或異常后,將DS,ES的值設為了__USER_DS,避免了上述的清除過程,提高了效率.
三:重要的數據結構
在深入源代碼之前,先把所用到的數據結構分析如下:
Irq_desc[]定義如下:
extern irq_desc_t irq_desc [NR_IRQS]
typedef struct irq_desc {
unsigned int status; /* IRQ的狀態;IRQ 是否被禁止了,有關IRQ 的設備當前是否正被自動檢測*/
hw_irq_controller *handler;/*指向一個中斷控制器的指針*/
c *action; /* 掛在IRQ上的中斷處理程序 */
unsigned int depth; /* 為0:該IRQ被啟用,如果為一個正數,表示被禁用 */
unsigned int irq_count; /* 該IRQ發生的中斷的次數 */
unsigned int irqs_unhandled; /*該IRQ線上沒有被處理的IRQ總數*/
spinlock_t lock;
} ____cacheline_aligned irq_desc_t;
Hw_irq_controller定義如下:
struct hw_interrupt_type {
const char * typename; /*中斷控制器的名字*/
unsigned int (*startup)(unsigned int irq); /*允許從IRQ線產生中斷*/
void (*shutdown)(unsigned int irq); /*禁止從IRQ線產生中斷*/
void (*enable)(unsigned int irq); /*enable與disable函數在8259A中與上述的startup shutdown函數相同*/
void (*disable)(unsigned int irq);
void (*ack)(unsigned int irq); /*在IRQ線上產生一個應答*/
void (*end)(unsigned int irq); /*在IRQ處理程序終止時被調用*/
void (*set_affinity)(unsigned int irq, cpumask_t dest); /*在SMP系統中,設置IRQ處理的親和力*/
}
typedef struct hw_interrupt_type hw_irq_controller;
struct irqaction定義如下:
struct irqaction {
//中斷處理常式
irqreturn_t (*handler)(int, void *, struct pt_regs *);
//flags:
//SA_INTERRUPT:中斷嵌套
//SA_SAMPLE_RANDOM:這個中斷源於物理隨機性
//SA_SHIRQ:中斷線共享
unsigned long flags;
//在x86平台無用
cpumask_t mask;
//產生中斷的硬體名字
const char *name;
//設備ID,一般由廠商指定
void *dev_id;
//下一個irqaction.共享的時候,通常一根中斷線對應很多硬體設備的中斷處理常式
struct irqaction *next;
}
可以用下圖來表示上述數據結構的關係:
四:idt在保護模式下的初始化
有關實模式下的初始化,以後再做專題分析.詳情請關注本站更新.
在init/main.c中:
asmlinkage void __init start_kernel(void)
{
……
//設定系統規定的異常/中斷
trap_init();
//設置外部IRQ中斷
init_IRQ();
……
}
在start_kernel中,調用trap_init()來設置系統規定的異常與中斷,調用init_IRQ()來設置外部中斷.
void __init trap_init(void)
{
……
set_trap_gate(0,÷_error);
set_intr_gate(1,&debug);
set_intr_gate(2,&nmi);
set_system_intr_gate(3, &int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_intr_gate(14,&page_fault);
set_trap_gate(15,&spurious_interrupt_bug);
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
#ifdef CONFIG_X86_MCE
set_trap_gate(18,&machine_check);
#endif
set_trap_gate(19,&simd_coprocessor_error);
set_system_gate(SYSCALL_VECTOR,&system_call); //系統調用
……
}
如上所示,設置了0~19的中斷/異常處理程序,這些都是intel所規定的,除些之後設置了系統調用入口(用戶空間的 int SYSCALL_VECTOR )
那, set_trap_gate()/set_intr_gate()/set_system_gata()都有一些什麼樣的區別呢?繼續看代碼:
void set_intr_gate(unsigned int n, void *addr)
{
_set_gate(idt_table n,14,0,addr,__KERNEL_CS);
}
static inline void set_system_intr_gate(unsigned int n, void *addr)
{
_set_gate(idt_table n, 14, 3, addr, __KERNEL_CS);
}
void __init set_trap_gate(unsigned int n, void *addr)
{
_set_gate(idt_table n,15,0,addr,__KERNEL_CS);
}
void __init set_system_gate(unsigned int n, void *addr)
{
_set_gate(idt_table n,15,3,addr,__KERNEL_CS);
}
都是通過統一的介面_set_gate().在i386中,這段代碼是用嵌入式彙編完成的,如下所示:
#define _set_gate(gate_addr,type,dpl,addr,seg)
do {
int __d0, __d1;
__asm__ __volatile__ ("movw %%dx,%%axnt"
"movw %4,%%dxnt"
"movl %
[火星人 ] linux中斷處理之初始化已經有601次圍觀