二. 向量的設置和相關數據的初始化:
在實模式下的初始化過程中,通過對中斷控制器8259A-1,9259A-2重新編程,把硬中斷設到0x20-0x2F。即把IRQ0�;IRQ15分別與0x20-0x2F號中斷向量對應起來;當對應的IRQ發生了時,處理機就會通過相應的中斷向量,把控制轉到對應的中斷服務例程。(源碼在Arch/i386/boot/setup.S文件中;相關內容可參見 實模式下的初始化 部分)
在保護模式下的初始化過程中,設置並初始化idt,共256個入口,服務程序均為ignore_int, 該服務程序僅打印“Unknown interruptn”。(源碼參見Arch/i386/KERNEL/head.S文件;相關內容可參見 保護模式下的初始化 部分)
在系統初始化完成後運行的第一個內核程序asmlinkage void __init start_kernel(void) (源碼在文件init/main.c中) 中,通過調用void __init trap_init(void)函數,把各自陷和中斷服務程序的入口地址設置到 idt 表中,即將表一中對應的處理程序入口設置到相應的中斷向量表項中;在此版本(2.2.5)的Linux只設置0-17號中斷向量。(trap_init(void)函數定義在arch/i386/kernel/traps.c 中; 相關內容可參見 詳解系統調用 部分)
在同一個函數void __init trap_init(void)中,通過調用函數set_system_gate(SYSCALL_VECTOR,&system_call); 把系統調用總控程序的入口掛在中斷0x80上。其中SYSCALL_VECTOR是定義在 linux/arch/i386/kernel/irq.h中的一個常量0x80; 而 system_call 即為中斷總控程序的入口地址;中斷總控程序用匯編語言定義在arch/i386/kernel/entry.S中。(相關內容可參見 詳解系統調用 部分)
在系統初始化完成後運行的第一個內核程序asmlinkage void __init start_kernel(void) (源碼在文件init/main.c中) 中,通過調用void init_IRQ(void)函數,把地址標號interrupt[i](i從1-223)設置到 idt 表中的的32-255號中斷向量(0x80除外),外部硬件IRQ的觸發,將通過這些地址標號最終進入到各自相應的處理程序。(init_IRQ(void)函數定義在arch/i386/kernel/IRQ.c 中;)
interrupt[i](i從1-223),是在arch/i386/kernel/IRQ.c文件中,通過一系列嵌套的類似如BUILD_16_IRQS(0x0)的宏,定義的一系列地址標號;(這些定義interrupt[i]的宏,全部定義在文件arch/i386/kernel/IRQ.c和arch/i386/kernel/IRQ.H中。這些嵌套的宏的使用,原理很簡單,但很煩,限於篇幅,在此省略)
各以interrupt[i]為入口的代碼,在進行一些簡單的處理後,最後都會調用函數asmlinkage void do_IRQ(struct pt_regs regs),do_IRQ函數調用static void do_8259A_IRQ(unsigned int irq, struct pt_regs * regs) 而do_8259A_IRQ在進行必要的處理後,將調用已與此IRQ建立聯系irqaction中的處理函數,以進行相應的中斷處理。最後處理機將跳轉到ret_from_intr進行必要處理後,整個中斷處理結束返回。(相關源碼都在文件arch/i386/kernel/IRQ.c和arch/i386/kernel/IRQ.H中。Irqaction結構參見上面的數據結構說明)
三. Bottom_half處理機制
在此版本(2.2.5)的Linux中,中斷處理程序從概念上被分為上半部分(top half)和下半部分(bottom half);在中斷發生時上半部分的處理過程立即執行,但是下半部分(如果有的話)卻推遲執行。內核把上半部分和下半部分作為獨立的函數來處理,上半部分決定其相關的下半部分是否需要執行。必須立即執行的部分必須位於上半部分,而可以推遲的部分可能屬於下半部分。
那麼為什麼這樣劃分成兩個部分呢?
一個原因是要把中斷的總延遲時間最小化。Linux內核定義了兩種類型的中斷,快速的和慢速的,這兩者之間的一個區別是慢速中斷自身還可以被中斷,而快速中斷則不能。因此,當處理快速中斷時,如果有其它中斷到達;不管是快速中斷還是慢速中斷,它們都必須等待。為了盡可能快地處理這些其它的中斷,內核就需要盡可能地將處理延遲到下半部分執行。
另外一個原因是,當內核執行上半部分時,正在服務的這個特殊IRQ將會被可編程中斷控制器禁止,於是,連接在同一個IRQ上的其它設備就只有等到該該中斷處理被處理完畢後果才能發出IRQ請求。而采用Bottom_half機制後,不需要立即處理的部分就可以放在下半部分處理,從而,加快了處理機對外部設備的中斷請求的響應速度。
還有一個原因就是,處理程序的下半部分還可以包含一些並非每次中斷都必須處理的操作;對這些操作,內核可以在一系列設備中斷之後集中處理一次就可以了。即在這種情況下,每次都執行並非必要的操作完全是一種浪費,而采用Bottom_half機制後,可以稍稍延遲並在後來只執行一次就行了。
由此可見,沒有必要每次中斷都調用下半部分;只有bh_mask 和 bh_active的對應位的與為1時,才必須執行下半部分(do_botoom_half)。所以,如果在上半部分中(也可能在其他地方)決定必須執行對應的半部分,那麼可以通過設置bh_active的對應位,來指明下半部分必須執行。當然,如果bh_active的對應位被置位,也不一定會馬上執行下半部分,因為還必須具備另外兩個條件:首先是bh_mask的相應位也必須被置位,另外,就是處理的時機,如果下半部分已經標記過需要執行了,現在又再次標記,那麼內核就簡單地保持這個標記;當情況允許的時候,內核就對它進行處理。如果在內核有機會運行其下半部分之前給定的設備就已經發生了100次中斷,那麼內核的上半部分就運行100次,下半部分運行1次。
bh_base數組的索引是靜態定義的,定時器底半處理過程的地址保存在第 0 個元素中,控制台底半處理過程的地址保存在第 1 個元素中,等等。當 bh_mask 和 bh_active 表明第 N 個底半處理過程已被安裝且處於活動狀態,則調度程序會調用第 N 個底半處理過程,該底半處理過程最終會處理與之相關的任務隊列中的各個任務。因為調度程序從第 0 個元素開始依次檢查每個底半處理過程,因此,第 0 個底半處理過程具有最高的優先級,第 31 個底半處理過程的優先級最低。
內核中的某些底半處理過程是和特定設備相關的,而其他一些則更一般一些。表二列出了內核中通用的底半處理過程。
表二、Linux 中通用的底半處理過程
TIMER_BH(定時器) 在每次系統的周期性定時器中斷中,該底半處理過程被標記為活動狀態,並用來驅動內核的定時器隊列機制。 CONSOLE_BH(控制台) 該處理過程用來處理控制台消息。 TQUEUE_BH(TTY 消息隊列) 該處理過程用來處理 tty 消息。 NET_BH(網絡) 用於一般網絡處理,作為網絡層的一部分 IMMEDIATE_BH(立即) 這是一個一般性處理過程,許多設備驅動程序利用該過程對自己要在隨後處理的任務進行排隊。
當某個設備驅動程序,或內核的其他部分需要將任務排隊進行處理時,它將任務添加到適當的系統隊列中(例如,添加到系統的定時器隊列中),然後通知內核,表明需要進行底半處理。為了通知內核,只需將 bh_active 的相應數據位置為 1。例如,如果驅動程序在 immediate 隊列中將某任務排隊,並希望運行 IMMEDIATE 底半處理過程來處理排隊任務,則只需將 bh_active 的第 8 位置為 1。在每個系統調用結束並返回調用進程之前,調度程序要檢驗 bh_active 中的每個位,如果有任何一位為 1,則相應的底半處理過程被調用。每個底半處理過程被調用時,bh_active 中的相應為被清除。bh_active 中的置位只是暫時的,在兩次調用調度程序之間 bh_active 的值才有意義,如果 bh_active 中沒有置位,則不需要調用任何底半處理過程。
四.中斷處理全過程
由前面的分析可知,對於0-31號中斷向量,被保留用來處理異常事件;0x80中斷向量用來作為系統調用的總入口點;而其他中斷向量,則用來處理外部設備中斷;這三者的處理過程都是不一樣的。
異常的處理全過程
對這0-31號中斷向量,保留用來處理異常事件;操作系統提供相應的異常的處理程序,並在初始化時把處理程序的入口等級在對應的中斷向量表項中。當產生一個異常時,處理機就會自動把控制轉移到相應的處理程序的入口,運行相應的處理程序,進行相應的處理後,返回原中斷處。當然,在前面已經提到,此版本(2.2.5)的Linux只提供了0-17號中斷向量的處理程序。
中斷的處理全過程
對於0-31號和0x80之外的中斷向量,主要用來處理外部設備中斷;在系統完成初始化後,其中斷處理過程如下:
當外部設備需要處理機進行中斷服務時,它就會通過中斷控制器要求處理機進行中斷服務。如果 CPU 這時可以處理中斷,CPU將根據中斷控制器提供的中斷向量號和中斷描述符表(IDT)中的登記的地址信息,自動跳轉到相應的interrupt[i]地址;在進行一些簡單的但必要的處理後,最後都會調用函數do_IRQ , do_IRQ函數調用 do_8259A_IRQ 而do_8259A_IRQ在進行必要的處理後,將調用已與此IRQ建立聯系irqaction中的處理函數,以進行相應的中斷處理。最後處理機將跳轉到ret_from_intr進行必要處理後,整個中斷處理結束返回。
從數據結構入手,應該說是分析操作系統源碼最常用的和最主要的方法。因為操作系統的幾大功能部件,如進程管理,設備管理,內存管理等等,都可以通過對其相應的數據結構的分析來弄懂其實現機制。很好的掌握這種方法,對分析Linux內核大有裨益。
方法之四:以功能為中心,各個擊破
從功能上看,整個Linux系統可看作有一下幾個部分組成:
進程管理機制部分;
內存管理機制部分;
文件系統部分;
硬件驅動部分;
系統調用部分等;
以功能為中心、各個擊破,就是指從這五個功能入手,通過源碼分析,找出Linux是怎樣實現這些功能的。
在這五個功能部件中,系統調用是用戶程序或操作調用核心所提供的功能的接口;也是分析Linux內核源碼幾個很好的入口點之一。對於那些在dos或Uinx、Linux下有過C編程經驗的高手尤其如此。又由於系統調用相對其它功能而言,較為簡單,所以,我就以它為例,希望通過對系統調用的分析,能使讀者體會到這一方法。
與系統調用相關的內容主要有:系統調用總控程序,系統調用向量表sys_call_table,以及各系統調用服務程序。下面將對此一一介紹:
保護模式下的初始化過程中,設置並初始化idt,共256個入口,服務程序均為ignore_int, 該服務程序僅打印“Unknown interruptn”。(源碼參見/Arch/i386/KERNEL/head.S文件;相關內容可參見 保護模式下的初始化 部分)
在系統初始化完成後運行的第一個內核程序start_kernel中,通過調用 trap_init函數,把各自陷和中斷服務程序的入口地址設置到 idt 表中;同時,此函數還通過調用函數set_system_gate 把系統調用總控程序的入口地址掛在中斷0x80上。其中:
start_kernel的原型為void __init start_kernel(void) ,其源碼在文件 init/main.c中;
trap_init函數的原型為void __init trap_init(void),定義在arch/i386/kernel/traps.c 中
函數set_system_gate同樣定義在arch/i386/kernel/traps.c 中,調用原型為set_system_gate(SYSCALL_VECTOR,&system_call);
其中,SYSCALL_VECTOR是定義在 linux/arch/i386/kernel/irq.h中的一個常量0x80;
而 system_call 即為系統調用總控程序的入口地址;中斷總控程序用匯編語言定義在arch/i386/kernel/entry.S中。
(其它相關內容可參見 中斷和中斷處理 部分)
系統調用向量表sys_call_table, 是一個含有NR_syscalls=256個單元的數組。它的每個單元存放著一個系統調用服務程序的入口地址。該數組定義在/arch/i386/kernel/entry.S中;而NR_syscalls則是一個等於256的宏,定義在include/linux/sys.h中。
各系統調用服務程序則分別定義在各個模塊的相應文件中;例如asmlinkage int sys_time(int * tloc)就定義在kerneltime.c中;另外,在kernelsys.c中也有不少服務程序
II、系統調用過程
∥頤侵道,系統調用是用戶程序或操作調用核心所提供的δ艿慕湧冢凰以系統掉用的過程就是從用戶程序到系統內核,然後又回到用戶程序的過程;在Linux中,此過程大體過程可描述如下:
系統調用過程示意圖:
整個系統調用進入過程客表示如下:
用戶程序 系統調用總控程序(system_call) 各個服務程序
可見,系統調用的進入課分為“用戶程序 系統調用總控程序”和“系統調用總控程序各個服務程序”兩部分;下邊將分別對這兩個部分進行詳細說明:
“用戶程序 系統調用總控程序”的實現:在前面已經說過,Linux的系統調用使用第0x80號中斷向量項作為總的入口,也即,系統調用總控程序的入口地址system_call就掛在中斷0x80上。也就是說,只要用戶程序執行0x80中斷 ( int 0x80 ),就可實現“用戶程序 系統調用總控程序”的進入;事實上,在Linux中,也是這麼做的。只是0x80中斷的執行語句int 0x80 被封裝在標准C庫中,用戶程序只需用標准系統調用函數就可以了,而不需要在用戶程序中直接寫0x80中斷的執行語句int 0x80。至於中斷的進入的詳細過程可參見前面的“中斷和中斷處理”部分。
“系統調用總控程序 各個服務程序” 的實現:在系統調用總控程序中通過語句“call * SYMBOL_NAME(sys_call_table)(,%eax,4)”來調用各個服務程序(SYMBOL_NAME是定義在/include/linux/linkage.h中的宏:#define SYMBOL_NAME_LABEL(X) X),可以忽略)。當系統調用總控程序執行到此語句時,eax中的內容即是相應系統調用的編號,此編號即為相應服務程序在系統調用向量表sys_call_table中的編號(關於系統調用的編號說明在/linux/include/asm/unistd.h中)。又因為系統調用向量表sys_call_table每項占4個字節,所以由%eax 乘上4形成偏移地址,而sys_call_table則為基址;基址加上偏移所指向的內容就是相應系統調用服務程序的入口地址。所以此call語句就相當於直接調用對應的系統調用服務程序。