方法之三:以數據結構為基點,觸類旁通
結構化程序設計思想認為:程序 = 數據結構 + 算法。數據結構體現了整個系統的構架,所以數據結構通常都是代碼分析的很好的著手點,對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 個底半處理過程。