中斷處理程序
除了上一章外,迄今為止,我們在內核中所做的每件事都是作為對一個進程請求的回應,要麼通過處理特殊的文件,發送 ioctl,要麼發出系統調用。但是內核的工作並不僅僅是回應進程請求。另一個每個字節都很重要的工作是和連接到機器的硬件對話。
在CPU和計算機的其他設備之間有兩種交互作用。第一種是當CPU對硬件發布命令時,另一種是當硬件要告訴CPU什麼事情時。第二種,被稱為中斷,實現起來是很困難的,因為它不得不處理什麼時間硬件是方便的而不是CPU。典型的硬件設備只有很少的內存,如果當信息可見的時候你不讀取的話它就會消失。
在 Linux 下,硬件中斷被稱為 IRQs [Interrupt Requests (這是Linux起源的Intel 架構上的標准術語。 )的縮寫]。有兩種 IRQs,短的和長的。一個短的 IRQ 預期占用 非常短的一段時間,在那期間,機器的剩余部分被阻塞,沒有其他的中斷將被處理。長的 IRQ 占用的時間長些,在那期間其他中斷有可能發生(但不能是來自同一設備)。只要是可能的,聲明一個長中斷是較好的。
當 CPU 接收到一個中斷,它停止它正在做的任何事情(除非它正在處理一個更重要的中斷,在那種情況下,它將處理完那個中斷後才來處理現在的這個),在堆棧中保存某些參數並調用中斷處理程序。這意味著在中斷處理程序自身中有些東西是不能允許的,因為系統處於一種未知的狀態。解決的辦法是中斷處理程序馬上做完需要做的,通常是從硬件裡面讀什麼或向硬件發送什麼然後安排處理稍後的新信息(這被稱為‘bottom half’)並返回。然後內核保證只要可能就調用bottom half --當這在運行,內核模塊中允許做的所有事情將被允許。
實現這個辦法是當接收到相關的IRQ(在 Intel 平台下有16個)時去調用 request_irq 以使中斷處理程序被調用。 這個函數接收IRQ 號,函數名,標志, /proc/interrupts 中的名字及一個傳送給中斷處理程序的參數作為其參數。標志可以包括 SA_SHIRQ 以指明你願意和其他的中斷處理程序分享那個IRQ(通常因為幾個硬件設備在同一IRQ)以及 SA_INTERRUPT 以指明這是一個快速中斷。這個函數只在那個IRQ上沒有處理程序的情況下成功,或者你願意兩者共享。
然後從中斷處理程序中我們和硬件通信,聯合tq_immediate使用 queue_task_irq 和 mark_bh(BH_IMMEDIATE) 調度 bottom half。我們在 2.0 版中不使用標准的queue_task 的原因是中斷有可能在其他人的 queue_task(queue_task_irq 從這被一個全局鎖保護 -- 在 2.2 版中沒有queue_task_irq 而 queue_task 被一個鎖保護。 )中發生。我們需要 mark_bh 是因為Linux 的早期版本只能有32個 bottom half,而現在它們中的一個(BH_IMMEDIATE) 用於還沒有得到bottom half入口的驅動程序的bottom half連接表。
Intel 架構鍵盤
警告: 這章剩下的內容都特別指定為完全的 基於Intel 架構。如果你不是在這個平台下運行,它沒有用。甚至不要試圖去編譯這裡的代碼。
在為這章寫范例代碼的時候我有一個問題。一方面,對於一個有用的范例,它應該運行於每個人的計算機上且有意味深長的結果。另一方面,內核已經包含了所有的通用設備的驅動程序,並且那些設備驅動程序不能和我將要寫的共存。我發現的結果是寫一些鍵盤中斷的東西並且先關閉通常的鍵盤的中斷句柄。因為在內核源文件(明確的, drivers/char/keyboard.c)中它被定義為靜態符號,所以沒有辦法恢復它。如果你重視你的文件系統,在 insmod 這些代碼前,在另一個終端上sleep 120 ; reboot 。
這個代碼將自己綁定為 IRQ 1,這是Intel 架構下的鍵盤控制器的IRQ(中斷請求)。然後當它收到鍵盤中斷時它就讀鍵盤的狀態( 那就是inb(0x64)的目的)和掃描代碼,該代碼即是鍵盤的返回值。然後,內核一認為它是可行的它就運行給出鍵所使用的代碼(掃描代碼的前7位)和它是被按下(第8位為0)還是被釋放(第8位為1)的got_char函數。
范例 intrpt.c
/* intrpt.c - 中斷句柄 */ /* Copyright (C) 1998 by Ori Pomerantz */ /* 必要頭文件 */ /* 標准頭文件 */ #include /* 內核工作 */ #include /* 明確指定是模塊 */ /* 處理 CONFIG_MODVERSIONS */ #if CONFIG_MODVERSIONS==1 #define MODVERSIONS #include #endif #include #include /* 我們想中斷 */ #include #include /* 在 2.2.3 版/usr/include/linux/version.h 中包含這個宏, * 但 2.0.35 版不包含-因此在這加入以被需要 */ #ifndef KERNEL_VERSION #define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c)) #endif /* Bottom Half - 一旦內核模塊認為它做任何事都是安全的時候這將被內核調用。 */ static void got_char(void *scancode) { printk("Scan Code %x %s.\n", (int) *((char *) scancode) & 0x7F, *((char *) scancode) & 0x80 ? "Released" : "Pressed"); } /* 這個函數為鍵盤中斷服務。它讀取來自鍵盤的相關信息然後安排當內核認為bottom half安全的時候讓它運行 */ void irq_handler(int irq, void *dev_id, struct pt_regs *regs) { /* 這些變量是靜態的,因為它們需要對 bottom half 可見(通過指針)。 */ static unsigned char scancode; static struct tq_struct task = {NULL, 0, got_char, &scancode}; unsigned char status; /* Read keyboard status */ status = inb(0x64); scancode = inb(0x60); /* 安排 bottom half 運行 */ #if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0) queue_task(&task, &tq_immediate); #else queue_task_irq(&task, &tq_immediate); #endif mark_bh(IMMEDIATE_BH); } /* 初始化模塊--登記 IRQ 句柄 */ int init_module() { /* 既然鍵盤的句柄不能和我們的共存,在我們做事情前我們不得不關閉它(釋放它的 IRQ)。 * 因為我們不知道它在哪兒,所以以後沒有辦法恢復它--因此當我們做完時計算機將被重新啟動。 */ free_irq(1, NULL); /* 請求 IRQ 1,鍵盤的 IRQ,指向我們的 irq_handler。 */ return request_irq( 1, /* PC上的鍵盤的 IRQ 號 */ irq_handler, /* 我們的句柄 */ SA_SHIRQ, /* SA_SHIRQ 意味著我們將另一個句柄用於這個 IRQ。 * * SA_INTERRUPT 能使句柄為一個快速中斷。 */ "test_keyboard_irq_handler", NULL); } /* 清除 */ void cleanup_module() { /* 它在這兒只是為了完全。它是完全不相關的,因為我們沒有辦法恢復通常的鍵盤中斷因此計算機完全沒用 * 了,需要被重新啟動。 */ free_irq(1, NULL); }