NO IMAGE

原文地址 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位,其結構如圖一:

Linux 中斷詳解 - 海飛絲 - 風十二的部落格

  中斷進入過程如圖二所示。

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

Linux 中斷詳解 - 海飛絲 - 風十二的部落格

二.異常處理機制:

  Intel公司保留0-31號中斷向量用來處理異常事件:當產生一個異常時,處理機就會自動把控制轉移到相應的處理程式的入口,異常的處理程式由作業系統提供,中斷向量和異常事件對應如表一:

中斷向量號異常事件Linux的處理程式
0除法錯誤Divide_error
1除錯異常Debug
2NMI中斷Nmi
3單位元組,int 3Int3
4溢位Overflow
5邊界監測中斷Bounds
6無效操作碼Invalid_op
7裝置不可用Device_not_available
8雙重故障Double_fault
9協處理器段溢位Coprocessor_segment_overrun
10無效TSSIncalid_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主要完成這樣一些任務:

    1. 中斷優先順序排隊管理,
    2. 接受外部中斷請求
    3. 向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級鏈示意圖

Linux 中斷詳解 - 海飛絲 - 風十二的部落格

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個空著未用。

  這些中斷服務程式入口的設定將在下面有詳細說明。

一.相關資料結構

  1. 中斷描述符表idt: 也就是中斷向量表,相當如一個陣列,儲存著各中斷服務例程的入口。(詳細描述參見圖一、中斷描述符格式)
  2. 與硬中斷相關資料結構:

與硬中斷相關資料結構主要有三個:

一:定義在/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;

};

三者關係如下:

Linux 中斷詳解 - 海飛絲 - 風十二的部落格

圖四、與硬中斷相關的幾個資料結構各關係

各結構成員詳述如下:

    1. 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不是共享的,因此這個成員就為空。
    1. 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中的其它資訊。
  1. 與Bottom_half相關的資料結構:

圖五、底半處理資料結構示意圖

Linux 中斷詳解 - 海飛絲 - 風十二的部落格 

  • 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中斷向量用來作為系統呼叫的總入口點;而其他中斷向量,則用來處理外部裝置中斷;這三者的處理過程都是不一樣的。

    1. 異常的處理全過程

      對這0-31號中斷向量,保留用來處理異常事件;作業系統提供相應的異常的處理程式,並在初 始化時把處理程式的入口等級在對應的中斷向量表項中。當產生一個異常時,處理機就會自動把控制轉移到相應的處理程式的入口,執行相應的處理程式,進行相應 的處理後,返回原中斷處。當然,在前面已經提到,此版本(2.2.5)的Linux只提供了0-17號中斷向量的處理程式。

    2. 中斷的處理全過程

        對於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系統可看作有一下幾個部分組成:

    1. 程序管理機制部分;
    2. 記憶體管理機制部分;
    3. 檔案系統部分;
    4. 硬體驅動部分;
    5. 系統呼叫部分等;

  以功能為中心、各個擊破,就是指從這五個功能入手,通過原始碼分析,找出Linux是怎樣實現這些功能的。

  在這五個功能部件中,系統呼叫是使用者程式或操作呼叫核心所提供的功能的介面;也是分析 Linux核心原始碼幾個很好的入口點之一。對於那些在dos或 Uinx、Linux下有過C程式設計經驗的高手尤其如此。又由於系統呼叫相對其它功能而言,較為簡單,所以,我就以它為例,希望通過對系統呼叫的分析,能使 讀者體會到這一方法。

  與系統呼叫相關的內容主要有:系統呼叫總控程式,系統呼叫向量表sys_call_table,以及各系統呼叫服務程式。下面將對此一一介紹:

    1. 保護模式下的初始化過程中,設定並初始化idt,共256個入口,服務程式均為ignore_int, 該服務程式僅列印“Unknown interruptn”。(原始碼參見/Arch/i386/KERNEL/head.S檔案;相關內容可參見 保護模式下的初始化 部分)
    2. 在系統初始化完成後執行的第一個核心程式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、系統呼叫過程

 ∥頤侵潰低車饔檬怯沒С絛蚧蠆僮韉饔煤誦乃峁┑墓δ艿慕湧冢凰韻低車粲玫墓嘆褪譴佑沒С絛虻較低襯諍耍緩笥只氐接沒С絛虻墓蹋輝贚inux中,此過程大體過程可描述如下:

  系統呼叫過程示意圖:

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語句就相當 於直接呼叫對應的系統呼叫服務程式。
  • 引數傳遞的實現:在Linux中所有系統呼叫服務例程都使用了asmlinkage標誌。此標誌是一個定義在/include/linux/linkage.h 中的一個巨集:

    #if defined __i386__ && (__GNUC__ > 2 || __GNUC_MINOR__ > 7)

    #define asmlinkage CPP_ASMLINKAGE__attribute__((regparm(0)))

    #else

    #define asmlinkage CPP_ASMLINKAGE

    #endif

    其中涉及到了gcc的一些約定,總之,這個標誌它可以告訴編譯器該函式不需要從暫存器中獲得任何引數,而是從堆疊中取得引數;即引數在堆疊中傳遞,而不是直接通過暫存器;

    堆疊引數如下:

    EBX = 0x00

ECX = 0x04

EDX = 0x08

ESI = 0x0C

EDI = 0x10

EBP = 0x14

EAX = 0x18

DS = 0x1C

ES = 0x20

ORIG_EAX = 0x24

EIP = 0x28

CS = 0x2C

EFLAGS = 0x30

  在進入系統呼叫總控程式前,使用者按照以上的對應順序將引數放到對應暫存器中,在系統呼叫 總控程式一開始就將這些暫存器壓入堆疊;在退出總控程式前又按如上順序堆疊;使用者程式則可以直接從暫存器中復得被服務程式加工過了的引數。而對於系統呼叫 服務程式而言,引數就可以直接從總控程式壓入的堆疊中復得;對引數的修改一可以直接在堆疊中進行;其實,這就是asmlinkage標誌的作用。所以在進 入和退出系統呼叫總控程式時,“保護現場”和“恢復現場”的內容並不一定會相同。

  • 特殊的服務程式:在此版本(2.2.5)的linux核心中,有好幾個系統呼叫的服務程式都是定義在/usr/src/linux/kernel/sys.c 中的同一個函式:

asmlinkage int sys_ni_syscall(void)

{

return -ENOSYS;

}

此函式除了返回錯誤號之外,什麼都沒幹。那他有什麼作用呢?歸結起來有如下三種可能:

1.處理邊界錯誤,0號系統呼叫就是用的此特殊的服務程式;

2.用來替換舊的已淘汰了的系統呼叫,如: Nr 17, Nr 31, Nr 32, Nr 35, Nr 44, Nr 53, Nr 56, Nr58, Nr 98;

3. 用於將要擴充套件的系統呼叫,如: Nr 137, Nr 188, Nr 189;

III、系統呼叫總控程式(system_call)

系統呼叫總控程式(system_call)可參見arch/i386/kernel/entry.S其執行流程如下圖:

Linux 中斷詳解 - 海飛絲 - 風十二的部落格

IV、例項:增加一個系統呼叫

由以上的分析可知,增加系統呼叫由於下兩種方法:

i.編一個新的服務例程,將它的入口地址加入到sys_call_table的某一項,只要該項的原服務例程是sys_ni_syscall,並且是sys_ni_syscall的作用屬於第三種的項,也即Nr 137, Nr 188, Nr 189。

ii.直接增加:

  1. 編一個新的服務例程;
  2. 在sys_call_table中新增一個新項, 並把的新增加的服務例程的入口地址加到sys_call_table表中的新項中;
  3. 把增加的 sys_call_table 表項所對應的向量, 在include/asm-386/unistd.h 中進行必要申明,以供使用者程序和其他系統程序查詢或呼叫。
  4. 由於在標準的c語言庫中沒有新系統呼叫的承接段,所以,在測試程式中,除了要#include ,還要申明如下 _syscall1(int,additionSysCall,int, num)。

下面將對第ii種情況列舉一個我曾經實現過了的一個增加系統呼叫的例項:

1.)在kernel/sys.c中增加新的系統服務例程如下:

asmlinkage int sys_addtotal(int numdata)

{

int i=0,enddata=0;

while(i<=numdata)

enddata =i ;

return enddata;

}

  該函式有一個 int 型入口引數 numdata , 並返回從 0 到 numdata 的累加值; 當然也可以把系統服務例程放在一個自己定義的檔案或其他檔案中,只是要在相應檔案中作必要的說明;

2.)把 asmlinkage int sys_addtotal( int) 的入口地址加到sys_call_table表中:

arch/i386/kernel/entry.S 中的最後幾行原始碼修改前為:

… …

.long SYMBOL_NAME(sys_sendfile)

.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */

.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */

.long SYMBOL_NAME(sys_vfork) /* 190 */

.rept NR_syscalls-190

.long SYMBOL_NAME(sys_ni_syscall)

.endr

修改後為: … …

.long SYMBOL_NAME(sys_sendfile)

.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */

.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */

.long SYMBOL_NAME(sys_vfork) /* 190 */

/* add by I */

.long SYMBOL_NAME(sys_addtotal)

.rept NR_syscalls-191

.long SYMBOL_NAME(sys_ni_syscall)

.endr

3.) 把增加的 sys_call_table 表項所對應的向量,在include/asm-386/unistd.h 中進行必要申明,以供使用者程序和其他系統程序查詢或呼叫:

增加後的部分 /usr/src/linux/include/asm-386/unistd.h 檔案如下:

… …

#define __NR_sendfile 187

#define __NR_getpmsg 188

#define __NR_putpmsg 189

#define __NR_vfork 190

/* add by I */

#define __NR_addtotal 191

4.測試程式(test.c)如下:

#include

#include

_syscall1(int,addtotal,int, num)

main()

{

int i,j;

do

printf(“Please input a numbern”);

while(scanf(“%d”,&i)==EOF);

if((j=addtotal(i))==-1)

printf(“Error occurred in syscall-addtotal();n”);

printf(“Total from 0 to %d is %d n”,i,j);

}

  對修改後的新的核心進行編譯,並引導它作為新的作業系統,執行幾個程式後可以發現一切正常;在新的系統下對測試程式進行編譯(*注:由於原核心並未提供此係統呼叫,所以只有在編譯後的新核心下,此測試程式才能可能被編譯通過),執行情況如下:

$gcc o test test.c

$./test

Please input a number

36

Total from 0 to 36 is 666

綜述

  可見,修改成功。

  由於作業系統核心原始碼的特殊性:體系龐大,結構複雜,程式碼冗長,程式碼間聯絡錯綜複雜。所以要把核心原始碼分析清楚,也是一個很艱難,很需要毅力的事。尤其需要交流和講究方法;只有方法正確,才能事半功倍。

  在上面的論述中,一共列舉了兩個核心分析的入口、和三種分析原始碼的方法:以程式流程為線索,一線串珠;以資料結構為基點,觸類旁通;以功能為中心,各個擊破。三種方法各有特點,適合於分析不同部分的程式碼:

以程式流程為線索,適合於分析系統的初始化過程:系統引導、真實模式下的初始化、保護模式下的初始化三個部分,和分析應用程式的執行流程:從程式的裝載,到執行,一直到程式的退出。而流程圖則是這種分析方法最合適的表達工具。以資料結構為基點、觸類旁通,這種方法是分析作業系統原始碼最常用的和最主要的方法。對分析程序管理,裝置管理,記憶體管理等等都是很有效的。以功能為中心、各個擊破,是把整個系統分成幾個相對獨立的功能模組,然後分別對各個功能進行分析。這樣帶來的一個好處就是,每次只以一 個功能為中心,涉及到其他部分的內容,可以看作是其它功能提供的服務,而無需急著追究這種服務的實現細節;這樣,在很大程度上減輕了分析的複雜度。

  三種方法,各有其長,只要合理的綜合運用這些方法,相信對減輕分析的複雜度還是有所幫組的。