轉自:http://blog.csdn.net/tiangwan2011/article/details/7891818
原文地址 http://www.yesky.com/20010813/192117.shtml
方法之三:以數據結構為基點,觸類旁通
結構化程序設計思想認為:程序 =數據結構 +算法。數據結構體現了整個系統的構架,所以數據結構通常都是代碼分析的很好的着手點,對Linux內核分析尤其如此。比如,把進程控制塊結構分析清楚 了,就對進程有了基本的把握;再比如,把頁目錄結構和頁表結構弄懂了,兩級虛存映射和內存管理也就掌握得差不多了。為了體現循序漸進的思想,在這我就以 Linux對中斷機制的處理來介紹這種方法。
首先,必須指出的是:在此處,中斷指廣義的中斷概義,它指所有通過idt進行的控制轉移的機制和處理;它覆蓋以下幾個常用的概義:中斷、異常、可屏蔽中斷、不可屏蔽中斷、硬中斷、軟中斷 … … …
I、硬件提供的中斷機制和約定
一.中斷向量尋址:
硬件提供可供256個服務程序中斷進入的入口,即中斷向量;
中斷向量在保護模式下的實現機制是中斷描述符表idt,idt的位置由idtr確定,idtr是個48位的寄存器,高32位是idt的基址,低16位為idt的界限(通常為2k=256*8);
idt中包含256個中斷描述符,對應256個中斷向量;每個中斷描述符8位,其結構如圖一:

中斷進入過程如圖二所示。
當中斷是由低特權級轉到高特權級(即當前特權級CPL>DPL)時,將進行堆棧的轉移;內層堆棧的選擇由當前tss的相應字段確定,而且內層堆棧將依次被壓入如下數據:外層SS,外層ESP,EFLAGS,外層CS,外層EIP; 中斷返回過程為一逆過程;

二.異常處理機制:
Intel公司保留0-31號中斷向量用來處理異常事件:當產生一個異常時,處理機就會自動把控制轉移到相應的處理程序的入口,異常的處理程序由操作系統提供,中斷向量和異常事件對應如表一:
| 中斷向量號 | 異常事件 | Linux的處理程序 |
| 0 | 除法錯誤 | Divide_error |
| 1 | 調試異常 | Debug |
| 2 | NMI中斷 | Nmi |
| 3 | 單字節,int 3 | Int3 |
| 4 | 溢出 | Overflow |
| 5 | 邊界監測中斷 | Bounds |
| 6 | 無效操作碼 | Invalid_op |
| 7 | 設備不可用 | Device_not_available |
| 8 | 雙重故障 | Double_fault |
| 9 | 協處理器段溢出 | Coprocessor_segment_overrun |
| 10 | 無效TSS | Incalid_tss |
| 11 | 缺段中斷 | Segment_not_present |
| 12 | 堆棧異常 | Stack_segment |
| 13 | 一般保護異常 | General_protection |
| 14 | 頁異常 | Page_fault |
| 15 | Spurious_interrupt_bug | |
| 16 | 協處理器出錯 | Coprocessor_error |
| 17 | 對齊檢查中斷 | Alignment_check |
三.可編程中斷控制器8259A:
為更好的處理外部設備,x86微機提供了兩片可編程中斷控制器,用來輔助cpu接受外部的中斷信號;對於中斷,cpu只提供兩個外接引線:NMI和INTR;
NMI只能通過端口操作來屏蔽,它通常用於:電源掉電和物理存儲器奇偶驗錯;
INTR可通過直接設置中斷屏蔽位來屏蔽,它可用來接受外部中斷信號,但只有一個引線,不夠用;所以它通過外接兩片級鏈了的8259A,以接受更多的外部中斷信號。8259A主要完成這樣一些任務:
- 中斷優先級排隊管理,
- 接受外部中斷請求
- 向cpu提供中斷類型號
外部設備產生的中斷信號在IRQ(中斷請求)管腳上首先由中斷控制器處理。中斷控制器可 以響應多個中斷輸入,它的輸出連接到 CPU 的 INT 管腳,信號可通過INT 管腳,通知處理器產生了中斷。如果 CPU 這時可以處理中斷,CPU 會通過 INTA(中斷確認)管腳上的信號通知中斷控制器已接受中斷,這時,中斷控制器可將一個 8 位數據放置在數據總線上,這一 8 位數據也稱為中斷向量號,CPU 依據中斷向量號和中斷描述符表(IDT)中的信息自動調用相應的中斷服務程序。圖三中,兩個中斷控制器級聯了起來,從屬中斷控制器的輸出連接到了主中斷控 制器的第 3 個中斷信號輸入,這樣,該系統可處理的外部中斷數量最多可達 15 個,圖的右邊是 i386 PC 中各中斷輸入管腳的一般分配。可通過對8259A的初始化,使這15個外接引腳對應256個中斷向量的任何15個連續的向量;由於intel公司保留0- 31號中斷向量用來處理異常事件(而默認情況下,IBM bios把硬中斷設在0x08-0x0f),所以,硬中斷必須設在31以后,linux則在實模式下初始化時把其設在0x20-0x2F,對此下面還將具 體說明。
圖三、i386 PC 可編程中斷控制器8259A級鏈示意圖

II、Linux的中斷處理
硬件中斷機制提供了256個入口,即idt中包含的256個中斷描述符(對應256個中斷向量)。
而0-31號中斷向量被intel公司保留用來處理異常事件,不能另作它用。對這 0-31號中斷向量,操作系統只需提供異常的處理程序,當產生一個異常時,處理機就會自動把控制轉移到相應的處理程序的入口,運行相應的處理程序;而事實 上,對於這32個處理異常的中斷向量,此版本(2.2.5)的 Linux只提供了0-17號中斷向量的處理程序,其對應處理程序參見表一、中斷向量和異常事件對應表;也就是說,17-31號中斷向量是空着未用的。
既然0-31號中斷向量已被保留,那么,就是剩下32-255共224個中斷向量可用。 這224個中斷向量又是怎么分配的呢?在此版本(2.2.5)的Linux中,除了0x80 (SYSCALL_VECTOR)用作系統調用總入口之外,其他都用在外部硬件中斷源上,其中包括可編程中斷控制器8259A的15個irq;事實上,當 沒有定義CONFIG_X86_IO_APIC時,其他223(除0x80外)個中斷向量,只利用了從32號開始的15個,其它208個空着未用。
這些中斷服務程序入口的設置將在下面有詳細說明。
一.相關數據結構
- 中斷描述符表idt: 也就是中斷向量表,相當如一個數組,保存着各中斷服務例程的入口。(詳細描述參見圖一、中斷描述符格式)
- 與硬中斷相關數據結構:
與硬中斷相關數據結構主要有三個:
一:定義在/arch/i386/kernel/irq.h中的
struct hw_interrupt_type {
const char * typename;
void (*startup)(unsigned int irq);
void (*shutdown)(unsigned int irq);
void (*handle)(unsigned int irq, struct pt_regs * regs);
void (*enable)(unsigned int irq);
void (*disable)(unsigned int irq);
};
二:定義在/arch/i386/kernel/irq.h中的
typedef struct {
unsigned int status; /* IRQ status - IRQ_INPROGRESS, IRQ_DISABLED */
struct hw_interrupt_type *handler; /* handle/enable/disable functions */
struct irqaction *action; /* IRQ action list */
unsigned int depth; /* Disable depth for nested irq disables */
} irq_desc_t;
三:定義在include/linux/ interrupt.h中的
struct irqaction {
void (*handler)(int, void *, struct pt_regs *);
unsigned long flags;
unsigned long mask;
const char *name;
void *dev_id;
struct irqaction *next;
};
三者關系如下:

圖四、與硬中斷相關的幾個數據結構各關系
各結構成員詳述如下:
- struct irqaction結構,它包含了內核接收到特定IRQ之后應該采取的操作,其成員如下:
- handler:是一指向某個函數的指針。該函數就是所在結構對相應中斷的處理函數。
- flags:取值只有SA_INTERRUPT(中斷可嵌套),SA_SAMPLE_RANDOM(這個中斷是源於物理隨機性的),和SA_SHIRQ(這個IRQ和其它struct irqaction共享)。
- mask:在x86或者體系結構無關的代碼中不會使用(除非將其設置為0);只有在SPARC64的移植版本中要跟蹤有關軟盤的信息時才會使用它。
- name:產生中斷的硬件設備的名字。因為不止一個硬件可以共享一個IRQ。
- dev_id:標識硬件類型的一個唯一的ID。Linux支持的所有硬件設備的每一種類型,都有一個由制造廠商定義的在此成員中記錄的設備ID。
- next:如果IRQ是共享的,那么這就是指向隊列中下一個struct irqaction結構的指針。通常情況下,IRQ不是共享的,因此這個成員就為空。
- struct hw_interrupt_type結構,它是一個抽象的中斷控制器。這包含一系列的指向函數的指針,這些函數處理控制器特有的操作:
- typename:控制器的名字。
- startup:允許從給定的控制器的IRQ所產生的事件。
- shutdown:禁止從給定的控制器的IRQ所產生的事件。
- handle:根據提供給該函數的IRQ,處理唯一的中斷。
- enable和disable:這兩個函數基本上和startup和shutdown相同;
- 另外一個數據結構是irq_desc_t,它具有如下成員:
- status:一個整數。代表IRQ的狀態:IRQ是否被禁止了,有關IRQ的設備當前是否正被自動檢測,等等。
- handler:指向hw_interrupt_type的指針。
- action:指向irqaction結構組成的隊列的頭。正常情況下每個IRQ只有一個操作,因此鏈接列表的正常長度是1(或者0)。但是,如果IRQ被兩個或者多個設備所共享,那么這個隊列中就有多個操作。
- depth:irq_desc_t的當前用戶的個數。主要是用來保證在中斷處理過程中IRQ不會被禁止。
- irq_desc是irq_desc_t 類型的數組。對於每一個IRQ都有一個數組入口,即數組把每一個IRQ映射到和它相關的處理程序和irq_desc_t中的其它信息。
- 與Bottom_half相關的數據結構:
圖五、底半處理數據結構示意圖
- bh_mask_count:計數器。對每個enable/disable請求嵌套對進行計數。這些請求通過調用enable_bh和 disable_bh實現。每個禁止請求都增加計數器;每個使能請求都減小計數器。當計數器達到0時,所有未完成的禁止語句都已經被使能語句所匹配了,因 此下半部分最終被重新使能。(定義在kernel/softirq.c中)
- bh_mask和bh_active:它們共同決定下半部分是否運行。它們兩個都有32位,而每一個下半部分都占用一位。當一個上半部 分(或者一些其它代碼)決定其下半部分需要運行時,就通過設置bh_active中的一位來標記下半部分。不管是否做這樣的標記,下半部分都可以通過清空 bh_mask中的相關位來使之失效。因此,對bh_mask和bh_active進行位AND運算就能夠表明應該運行哪一個下半部分。特別是如果位與運 算的結果是0,就沒有下半部分需要運行。
- bh_base:是一組簡單的指向下半部分處理函數的指針。
bh_base代表的指針數組中可包含 32 個不同的底半處理程序。bh_mask 和 bh_active 的數據位分別代表對應的底半處理過程是否安裝和激活。如果 bh_mask 的第 N 位為 1,則說明 bh_base 數組的第 N 個元素包含某個底半處理過程的地址;如果 bh_active 的第 N 位為 1,則說明必須由調度程序在適當的時候調用第 N 個底半處理過程。
二. 向量的設置和相關數據的初始化:
- 在實模式下的初始化過程中,通過對中斷控制器8259A-1,9259A-2重新編程,把硬中斷設到0x20-0x2F。即把IRQ0& #0;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中。
|


