轉載自:https://blog.csdn.net/yiqiaoxihui/article/details/81133950?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param
在技術面前,多問為什么總是好的,知其然不如知其所以然。
1.前言
本文盡量以設計者的角度去介紹中斷。
本文着重介紹Linux內核中中斷處理的始末流程,因此對一些基本的概念應該有所了解。
2.硬件支持
我們知道,CPU有一個INTR引腳,用於接收中斷請求信號。
而中斷控制器用於提供中斷向量,即第幾號中斷。
3.內核需要做哪些工作?
3.1需要一張表
首先,中斷可能來源於外部設備,而外部設備多種多樣,也可能來源於CPU,無論如何我們需要區分到底是具體哪種中斷,哪個設備產生的。因此,我們首先需要一個表,表中每項表示一種中斷,內容可以是指向具體中斷服務程序的函數指針,這是最簡單的中斷向量表,這樣,當中斷發生時,將中斷向量作為中斷向量表的下表,就可以直接找到中斷服務程序。如圖1所示。
圖1-簡要中斷向量表標題
但是操作系統考慮的更多,比如中斷優先級、中斷類型標識等。於是就產生了如下圖所示中斷向量表項。
中斷向量表項結構
3.2對於用於外設的通用中斷,需要一個隊列
但是這樣還不行,一方面中斷向量有硬件中斷控制器產生,因此中斷向量數目受限於硬件,無法彈性增長;另一方面,作為通用操作系統,可能存在許多不同的外部設備,也會操作中斷向量不夠用。因此,解決辦法就是共用中斷向量。系統為每個中斷向量設置一個隊列,根據中斷源所使用的中斷向量,將其掛入到相應的隊列中去。其中隊列的隊列頭是irq_desc_t結構體數組。
typedef struct { unsigned int status; /* IRQ status */ hw_irq_controller *handler; struct irqaction *action; /* IRQ action list */ unsigned int depth; /* nested irq disables */ spinlock_t lock; } ____cacheline_aligned irq_desc_t; extern irq_desc_t irq_desc [NR_IRQS];
其中,通過其結構體內部的結構體 irqaction 形成一個隊列。irqaction結構體可以理解為掛在當前中斷向量隊列中的特定的設備中斷服務程序,如此,同一中斷向量隊列,就能夠區分是哪個中斷服務程序屬於哪個設備了。
struct irqaction { void (*handler)(int, void *, struct pt_regs *); //設備具體中斷服務程序 unsigned long flags; unsigned long mask; const char *name; void *dev_id; //設備的id表示 struct irqaction *next; //隊列中下一個此結構體 };
最終形成的中斷服務結構圖如下所示。
中斷服務結構圖
4.中斷向量表的設置
首先,中斷向量表內容從0~0x20均為CPU內部產生的中斷,包括除0、頁面錯等。從0x20開始均為用於外部設備的通用中斷(包括中斷請求隊列),但是0x80系統調用除外。這些表項的內容都是在中斷向量表初始化的時候進行設置。
4.1內部中斷的向量表項設置
對於0~19個內部中斷設置由下面函數設置。
void __init trap_init(void) { #ifdef CONFIG_EISA if (isa_readl(0x0FFFD9) == 'E'+('I'<<8)+('S'<<16)+('A'<<24)) EISA_bus = 1; #endif set_trap_gate(0,÷_error); set_trap_gate(1,&debug); set_intr_gate(2,&nmi); set_system_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_trap_gate(8,&double_fault); 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_trap_gate(14,&page_fault); set_trap_gate(15,&spurious_interrupt_bug); set_trap_gate(16,&coprocessor_error); set_trap_gate(17,&alignment_check); set_trap_gate(18,&machine_check); set_trap_gate(19,&simd_coprocessor_error); set_system_gate(SYSCALL_VECTOR,&system_call); /* * default LDT is a single-entry callgate to lcall7 for iBCS * and a callgate to lcall27 for Solaris/x86 binaries */ set_call_gate(&default_ldt[0],lcall7); set_call_gate(&default_ldt[4],lcall27); /* * Should be a barrier for any external CPU state. */ cpu_init(); #ifdef CONFIG_X86_VISWS_APIC superio_init(); lithium_init(); cobalt_init(); #endif }
4.2通用中斷向量表項設置
中斷類型為中斷門的中斷向量表設置具體函數如下。
void set_intr_gate(unsigned int n, void *addr) { //設置中斷向量表項 //n表示第幾項 //addr表示中斷服務的入口程序地址 _set_gate(idt_table+n,14,0,addr); }
事實上,上面函數參數 void *addr即為上面中斷服務結構圖中的一系列IRQ0x00_interrupt(),這類函數也是中斷服務程序入口函數,即進入中斷最先執行的一小段程序,這段程序非常重要,讀者可以先猜測一下其功能。
5.通用中斷,中斷請求隊列初始化
之前提到,用於外設的通用中斷,多個中斷源可以共用一個中斷向量,因此就有了上文中斷服務結構圖中的中斷請求隊列,為了清晰,這里再貼出其結構圖。每一個中斷請求隊列可以想象成一個中斷通道,里面容納了一系列具體設備的中斷服務程序。
通用中斷請求隊列圖
在4.2中我們僅僅是設置了中斷向量表項的內容,但是對於通用中斷來說,那時僅僅設置了一系列IRQ0x00_interrupt()通用中斷入口函數,並沒有將各個設備的具體中斷服務程序通過結構體irqaction掛到相應的中斷請求隊列中。
所以,真正的中斷服務要到具體設備初始化程序將中斷服務程序通過request_irq()向系統“登記”,掛入某個中斷請求隊列以后才會發生,下面的函數根據函數參數設置一個irqaction結構體,並根據下標irq掛入相應的irq_desc[irq]所表示的中斷請求隊列中。
int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs *), unsigned long irqflags, const char * devname, void *dev_id) { int retval; struct irqaction * action; #if 1 /* * Sanity-check: shared interrupts should REALLY pass in * a real dev-ID, otherwise we'll have trouble later trying * to figure out which interrupt is which (messes up the * interrupt freeing logic etc). */ if (irqflags & SA_SHIRQ) { if (!dev_id) printk("Bad boy: %s (at 0x%x) called us without a dev_id!\n", devname, (&irq)[-1]); } #endif if (irq >= NR_IRQS) return -EINVAL; if (!handler) return -EINVAL; action = (struct irqaction *) kmalloc(sizeof(struct irqaction), GFP_KERNEL); if (!action) return -ENOMEM; //irqaction具體參數的設置 action->handler = handler; action->flags = irqflags; action->mask = 0; action->name = devname; action->next = NULL; action->dev_id = dev_id; retval = setup_irq(irq, action); //根據irq,將其掛入某個中斷請求隊列中 if (retval) kfree(action); return retval; }
6.中斷服務程序的響應
上述工作已經完成中斷機制所需要的所有環境,對於0x20以上的通用中斷,外部設備也已經將中斷服務程序掛入到相應的中斷請求隊列中。
我們現在假設外部程序產生了一次中斷請求,該請求通過中斷控制器到達了CPU的中斷請求引線INTR,並且中斷開着,所以當CPU執行完當前指令后就來響應此才中斷請求。
在介紹前,我們首先需要明白中斷的執行意味着什么?
一般外設產生的中斷屬於突發狀況,也就是需要離開正在執行的程序,轉向執行中斷服務。那么這就面臨着幾個問題:
- 當前程序的運行級別和中斷程序的運行級別是否相同?這非常重要,會引起堆棧的切換
- 如何保存當前程序的執行狀態,以便中斷服務結束后能恢復執行?
帶着這兩個問題,我們接着往下看。
6.1獲取中斷向量,執行准備工作
CPU從中斷控制器取得中斷向量后,就從中斷向量表查找該中斷向量所指的那一項,還記得通用中斷請求隊列圖 里面的IRQ0x0x_interrupt()函數嗎?它是通用中斷向量表項中的一部分,即該通用中斷通道的總服務程序入口函數。
另外,當中斷在用戶空間發生,當前運行級別為3,而中斷服務程序屬於內核,其運行級別在中斷向量表項中用DPL標識為0,因此,需要切換堆棧:即從用戶空間堆棧切換成當前進程的系統空間堆棧。正在運行的堆棧指針存放在寄存器TR所指向的TSS中(這不是重點),因此CPU從TSS中取出系統堆棧指針,完成用戶堆棧到系統堆棧的切換。完成堆棧切換后,會將EFLAGS的內容及中斷返回地址壓入系統堆棧。注意這些壓棧操作是由中斷指令INT本身發出的,這是還未進入中斷通道的總服務程序入口函數IRQ0x0x_interrupt()。
而至於切換堆棧,本人暫時沒弄清楚由哪些代碼完成,還請不吝賜教!
相反,如果中斷發生在系統空間,那就無需切換堆棧了,兩者差別如下圖所示。
中斷發生在用戶空間或系統空間的區別
6.2執行中斷總入口函數IRQ0x0YY_interrupt()
由於此過程非常關鍵,在此單獨敘述。
以IRQ0x03_interrupt()為例,將函數具體內容列出。
__asm__ ( \ "\n" \ "IRQ0x03_interrupt: \n\t" \ "pushl $0x03 - 256 \n\t" \ //中斷向量號-256,壓棧 "jmp common_interrupt"); //跳轉 ---------------------------------------------------- #define BUILD_COMMON_IRQ() \ asmlinkage void call_do_IRQ(void); \ __asm__( \ "\n" __ALIGN_STR"\n" \ "common_interrupt:\n\t" \ SAVE_ALL \ //保存現場,中斷前夕所有寄存器內容壓棧 "pushl $ret_from_intr\n\t" \ //將一個函數地址壓棧 SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \ "jmp "SYMBOL_NAME_STR(do_IRQ)); //跳轉到do_IRQ函數
首先執行IRQ0x03_interrupt()
然后,又跳轉到common_interrupt
最后又跳轉到do_IRQ函數。
其中,保存現場的SAVE_ALL操作:
#define SAVE_ALL \ cld; \ pushl %es; \ pushl %ds; \ pushl %eax; \ pushl %ebp; \ pushl %edi; \ pushl %esi; \ pushl %edx; \ pushl %ecx; \ pushl %ebx; \ movl $(__KERNEL_DS),%edx; \ movl %edx,%ds; \ movl %edx,%es;
因此,在跳轉到do_IRQ時,系統堆棧應該如下圖所示,結合上面代碼,可以說非常清晰了。
圖6.2 進入中斷服務程序時,系統堆棧示意圖
堆棧中所保存的這些內容,用於將來恢復進入中斷前的程序的執行。
6.3 do_IRQ函數
接下來就是執行do_IRQ函數,即要執行具體的中斷服務函數了,那么需要具備哪些條件呢?
- 需要獲取中斷調用號,即上面的0x03,注意不是中斷向量,為什么?
- 好像沒了。。。。
那么中斷調用號從哪獲取呢?先看一下do_IRQ函數原型。
unsigned int do_IRQ(struct pt_regs regs); struct pt_regs { long ebx; long ecx; long edx; long esi; long edi; long ebp; long eax; int xds; int xes; long orig_eax; long eip; int xcs; long eflags; long esp; int xss; };
請關注其參數 regs,然后再對照圖6.2系統堆棧,你會發現什么?
如何沒有想到,再提醒一下函數調用在棧中的構造過程是什么?
沒錯,當前所構造出系統堆棧的內容,其實是做了do_IRQ函數的參數,而在調用do_IRQ前,最后壓入的ret_from_intr則是do_IRQ的返回地址。
這樣中斷服務程序do_IRQ所需的中斷調用號有了,返回地址也設置妥了,接下來真的就要進入do_IRQ了。
結合前面所述,再看中斷請求隊列,可以想象一下do_IRQ函數具體做了什么?
- 根據中斷請求號,作為數組隊列頭irq_desc的下標,獲取相應的中斷請求隊列
- 然后,依次處理隊列中具體設備的中斷服務程序
限於篇幅,這里不再貼出代碼,具體可以查看arch/i386/kernel/irq.c中的代碼。
在函數的末尾,可能會執行軟中斷服務程序 do_softirq(),關於軟中斷的由來請參考其他資料。在函數執行完,就會按照之前精心設置的返回地址ret_from_intr進行返回了。
7.中斷返回
do_IRQ()函數通過返回地址ret_from_intr會到達entry.S中標號ret_from_intr處:
ENTRY(ret_from_intr) GET_CURRENT(%ebx) movl EFLAGS(%esp),%eax # mix EFLAGS and CS movb CS(%esp),%al testl $(VM_MASK | 3),%eax # return to VM86 mode or non-supervisor? jne ret_with_reschedule jmp restore_all
上面的操作主要是檢查中斷前夕,cpu運行於用戶空間還是系統空間,若發生於用戶空間,轉移到ret_with_reschedule,然后最終還是會到達restore_all處。
ret_with_reschedule: cmpl $0,need_resched(%ebx) jne reschedule cmpl $0,sigpending(%ebx) jne signal_return restore_all: RESTORE_ALL ALIGN signal_return: sti # we can get here from an interrupt handler testl $(VM_MASK),EFLAGS(%esp) movl %esp,%eax jne v86_signal_return xorl %edx,%edx call SYMBOL_NAME(do_signal) jmp restore_all reschedule: call SYMBOL_NAME(schedule) # test jmp ret_from_sys_call
首先,在ret_with_reschedule中判斷是否需要進行一次進程調度,需要這轉移到reschedule處,接着會轉移到ret_from_sys_call,但是從ret_from_sys_call最終還是會到達restore_all處。
而restore_all操作如下:
#define RESTORE_ALL \ popl %ebx; \ popl %ecx; \ popl %edx; \ popl %esi; \ popl %edi; \ popl %ebp; \ popl %eax; \ 1: popl %ds; \ 2: popl %es; \ addl $4,%esp; \ 3: iret;
這與之前的SAVE_ALL遙相呼應
#define SAVE_ALL \ cld; \ pushl %es; \ pushl %ds; \ pushl %eax; \ pushl %ebp; \ pushl %edi; \ pushl %esi; \ pushl %edx; \ pushl %ecx; \ pushl %ebx; \ movl $(__KERNEL_DS),%edx; \ movl %edx,%ds; \ movl %edx,%es;
這樣,當到達RESTORE_ALL的iret時,iret使CPU從中斷返回,和進入中斷時對應,如果是從系統態返回到用戶態就會將堆棧切換到用戶堆棧。
結束
這就是Linux內核2.4.0版本的中斷機制的內容,當然這里省略了軟中斷的內容,需要的讀者可以參考其他資料。
技術是為了解決問題的,技術的門檻往往在於不了解技術本身,一旦清楚其過程,也就沒有了門檻,但是這對於設計一項技術還遠遠不夠,這就要求我們在了解技術的過程中,多問為什么,知其然不如知其所以然。
參考資料: