對於任何編寫核心代碼的人來說,最吸引他們注意的問題之一就是如何完成調試。由於核心是一個不與某個進程相關的功能集,其代碼不能很輕鬆地放在調試器中執行,而且也不能跟蹤。
本章介紹你可以用來監視核心代碼和跟蹤錯誤的技術。
用列印資訊調試
最一般的調試技術就是監視,就是在應用內部合適的點加上printf調用。當你調試核心代碼的時候,你可以用printk完成這個任務。
Printk
在前些章中,我們簡單假設printk工作起來和printf很類似。現在是介紹一下它們之間不同的時候了。
其中一個不同點就是,printk允許你根據它們的嚴重程度,通過附加不同的“記錄級”來對訊息分類,或賦予訊息優先順序。你可以用宏來指示記錄級。例如,KERN_INFO,我們前面已經看到它被加在列印語句的前面,它就是一種可能的訊息記錄級。記錄級宏展開為一個字串,在編譯時間和訊息文本拼接在一起;這也就是為什麼下面的例子中優先順序和格式字串間沒有逗號。這有兩個printk的例子,一個是調試資訊,一個是關鍵資訊:
(代碼)
在<linux/kernel.h>中定義了8種記錄層級串。沒有指定優先順序的printk語句預設使用DEFAULT_MESSAGE_LOGLEVEL優先順序,它是一個在kernel/printk.c中定義的整數。預設記錄級的具體數值在Linux的開發期間曾變化過若干次,所以我建議你最好總是指定一個合適的記錄級。
根據記錄級,核心將訊息列印到當前文本控制台上:如果優先順序低於console_loglevel這個數值的話,該訊息就顯示在控制台上。如果系統同時運行了klogd和syslogd,無論console_loglevel為何值,核心都將訊息追加到/var/log/messages中。
變數console_loglevel最初初始化為DEFAULT_CONSOLE_LOGLEVEL,但可以通過sys_syslog系統調用修改。如klogd的手冊所示,可以在啟動klogd時指定-c開關來修改這個變數。此外,你還可以寫個程式來改變控制台記錄級。你可以在O’Reilly網站上的源檔案中找到我寫的一個這種功能的程式,miscprogs/setlevel.c。新優先順序是通過一個1到8之間的整數值指定的。
你也許需要在核心失效後降低記錄級(見“調試系統故障”),這是因為失效處理代碼會將console_loglevel提升到15,之後所有的訊息都會出現在控制台上。為看到你的調試資訊,如果你啟動並執行是核心2.0.x話,你需要提升記錄級。核心2.0發行降低了MINIMUM_CONSOLE_LOGLEVEL,而舊版本的klogd預設情況下要列印很多控制訊息。如果你碰巧使用了這箇舊版本的守護進程,除非你提升記錄級,核心2.0會比你預期的列印出更少的訊息。這就是為什麼hello.c中使用了<1>標記,這樣可以保證訊息顯示在控制台上。
從1.3.43一來的核心版本通過允許你向指定虛控制台發送訊息,藉此提供一個靈活的記錄策略。預設情況下,“控制台”是當前虛終端。也可以選擇不同的虛終端接收訊息,你只需向所選的虛終端調用ioctl(TIOCLINUX)。如下程式,setconsole,可以用來選擇哪個虛終端接收核心訊息;它必須以超級使用者身份運行。如果你對ioctl還不有把握,你可以跳過這至下一節,等到讀完第5章“字元裝置驅動程式的擴充操作”的“ioctl”一節後,再回到這裡讀這段代碼。
(代碼)
setconsole使用了用於Linux專用功能的特殊的ioctl命令TIOCLINUX。為了使用TIOCLINUX,你要傳遞給它一個指向位元組數組的指標。數組的第一個位元組是所請求的子命令的編碼,隨後的位元組依命令而不同。在setconsole中使用了子命令11,後一個位元組(存放在bytes[1]中)標別虛擬控制台。TIOCLINUX的完成介紹可以在核心源碼drivers/char/tty_io.c中找到。
訊息是如何記錄的
printk函數將訊息寫到一個長度為LOG_BUF_LEN個位元組的迴圈緩衝區中。然後喚醒任何等待訊息的進程,即那些在調用syslog系統調用或讀取/proc/kmesg過程中睡眠的進程。這兩個訪問記錄引擎的介面是等價的。不過/proc/kmesg檔案更象一個FIFO檔案,從中讀取資料更容易些。一條簡單的cat命令就可以讀取訊息。
如果迴圈緩衝區填滿了,printk就繞到緩衝區的開始處填寫新資料,覆蓋舊資料。於是記錄進程就丟失了最舊的資料。這個問題與利用迴圈緩衝區所獲得的好處相比可以忽略不計。例如,迴圈緩衝區可以使系統在沒有記錄進程的情況下照樣運行,同時又不浪費記憶體。Linux處理訊息的方法的另一個特點是,可以在任何地方調用printk,甚至在中斷處理函數裡也可以調用,而且對資料量的大小沒有限制。這個方法的唯一缺點就是可能丟失某些資料。
如果klogd正在運行,它讀取核心訊息並將它們指派到syslogd,它隨後檢查/etc/syslog.conf找到處理這些資料的方式。syslogd根據一個“設施”和“優先順序”切分訊息;可以使用的值定義在<sys/syslog.h>中。核心訊息根據相應printk中指定的優先順序記錄到LOG_KERN設施中。如果klogd沒有運行,資料將儲存在迴圈緩衝區中直到有進程來讀取資料或資料溢出。
如果你不希望因監視你的驅動程式的訊息而把你的系統記錄搞亂,你給klogd指定-f(檔案)選項或修改/etc/syslog.conf將記錄寫到另一個檔案中。另一種方法是一種強硬方法:殺掉klogd,將訊息列印到不用的虛終端上*,或者在一個不用的xterm上執行cat /proc/kmesg顯示訊息。
使用預先處理方便監視處理
在驅動程式開發早期,printk可以對調試和測試新代碼都非常有協助。然而當你正式發行驅動程式時,你應該去掉,或者至少關閉,這些列印語句。很不幸,你可能很快就發現,隨著你想不再需要那些訊息並去掉它們時,你可能又要加新功能,你又需要這些訊息了。解決這些問題有幾種方法――如何從全域開啟和關閉訊息以及如何開啟和關閉個別訊息。
下面給出了我處理訊息所用的大部分代碼,它有如下一些功能:
可以通過在宏名字加一個字母或去掉一個字母開啟或關閉每一條語句。
通過在編譯前修改 CFLAGS 變數,可以一次關閉所有訊息。
同樣的列印語句既可以用在核心態(驅動程式)也可以用在使用者態(示範或測試程式)。
下面這些直接來自scull.h的代碼片斷實現了這些功能。
(代碼)
符合PDEBUG和PDEBUGG依賴於是否定義了SCULL_DEBUG,它們都和printf調用很類似。
為了進一步方便這個過程,在你的Makefile加上如下幾行。
(代碼)
本節所給出的代碼依賴於gcc對ANSI C先行編譯器的擴充,gcc可以支援帶可變數目參數的宏。這種對gcc的依賴並不是什麼問題,因為核心對gcc特性的依賴更強。此外,Makefile依賴於GNU的gmake;基於同樣的道理,這也不是什麼問題。
如果你很熟悉C先行編譯器,你可以將上面的定義擴充為可以支援“調試級”概念的,可以為每級賦一個整數(或位元影像),說明這一級列印多麼瑣碎的訊息。
但是每一個驅動程式都有它自己的功能和監視需求。好的編程技巧會在靈活性和高效之間找到一個權衡點,這個我就不能說哪個對你最好了。記住,先行編譯器條件(還有代碼中的常量運算式)只到編譯時間運行,你必須重新編譯器來開啟或關閉訊息。另一種方法就是使用C條件陳述式,它在運行時運行,因此可以讓你在程式執行期間開啟或關閉訊息。這個功能很好,但每次代碼執行系統都要進行額外的處理,甚至在訊息關閉後仍然會影響效能。有時這種效能損失是無法接受的。
個人觀點,儘管上面給出的宏迫使你每次要增加或去掉訊息時都要重新編譯,重新載入模組,但我覺得用這些宏已經很好了。
通過查詢調試
上一節談到了printk是如何工作的以及如何使用它。但沒有談及它的缺點。
由於syslogd會一直保持重新整理它的輸出檔案,每列印一行都會引起一次磁碟操作,因此過量使用printk會嚴重降低系統效能。至少從syslogd的角度看是這樣的。它會將所有的資料都一股腦地寫到磁碟上,以防在列印訊息後系統崩潰;然而,你不想因為調試資訊的緣故而降低系統效能。這個問題可以通過在/etc/syslogd.conf中記錄檔案的名字前加一個波折號解決,但有時你不想修改你的設定檔。如果不這樣,你還可以運行一個非klogd的程式(如前面介紹的cat /proc/kmesg),但這樣並不能為正常操作提供一個合適的環境。
與這相比,最好的方法就是在你需要資訊的時候,通過查詢系統獲得相關資訊,而不是持續不斷地產生資料。事實上,每一個Unix系統都提供了很多工具用來獲得系統資訊:ps,netstat,vmstat等等。
有許多技術適合與驅動程式開發人員查詢系統,簡而言之就是,在/proc下建立檔案和使用ioctl驅動程式方法。
使用/proc檔案系統
Linux中的/proc檔案系統與任何裝置都沒有關係――/proc中的檔案都在被讀取時有核心建立的。這些檔案都是普通的文字檔,它們基本上可由普通人理解,也可被工具程式理解。例如,對於大多數Linux的ps實現而言,它都通過讀取/proc檔案系統獲得進程表資訊的。/proc虛擬檔案的創意已由若干現代作業系統使用,且非常成功。
/proc的當前實現可以動態建立i節點,允許使用者模組為方便資訊檢索建立如何進入點。
為了在/proc中建立一個健全的檔案節點(可以read,write,seek等等),你需要定義file_operations結構和inode_operations結構,後者與前者有類似的作用和尺寸。建立這樣一個i節點比起建立整個字元裝置並沒有什麼不同。我們這裡不討論這個問題,如果你感興趣,你可以在源碼樹fs/proc中獲得進一步細節。
與大多數/proc檔案一樣,如果檔案節點僅僅用來讀,建立它們是比較容易的,我將這裡介紹這一技術。很不幸,這一技術只能在Linux 2.0及其後續版本中使用。
這裡是建立一個稱為/proc/scullmem檔案的scull代碼,這個檔案用來擷取scull使用的記憶體資訊。
(代碼)
填寫/proc檔案非常容易。你的函數擷取一個空閑頁面填寫資料;它將資料寫進緩衝區並返回所寫資料的長度。其他事情都由/proc檔案系統處理。唯一的限制就是所寫的資料不能超過PAGE_SIZE個位元組(宏PAGE_SIZE定義在標頭檔<asm/page.h>中;它是與體繫結構相關的,但你至少可以它有4KB大小)。
如果你需要寫多於一個頁面的資料,你必須實現功能健全的檔案。
注意,如果一個正在讀你的/proc檔案的進程發出了若干read調用,每一個都擷取新資料,儘管只有少量資料被讀取,你的驅動程式每次都要重寫整個緩衝區。這些額外的工作會使系統效能下降,而且如果檔案產生的資料與下一次的不同,以後的read調用要重新裝配不相關的部分,這一會造成資料錯位。事實上,由於每個使用C庫的應用程式都大塊地讀取資料,效能並不是什麼問題。然而,由於錯位時有發生,它倒是一個值得考慮的問題。在擷取資料後,庫調用至少要調用1次read――只有當read返回0時才報告檔案尾。如果驅動程式碰巧比前面產生了更多的資料,系統就返回到使用者空間額外的位元組並且與前面的資料區塊是錯位的。我們將在第6章“時間流”的“任務隊列”一節中涉及/proc/jiq*,那時我們還會遇到錯位問題。
cleanup_module中應該使用下面的語句登出/proc節點:
(代碼)
傳遞給函數的參數是包含要撤銷檔案的目錄名和檔案的i節點號。由於i節點號是自動分配的,在編譯時間是無法知道的,必須從資料結構中讀取。
ioctl方法
ioctl,下一章將詳細討論,是一個系統調用,它可以操做在檔案描述符上;它接收一個“命令”號和(可選的)一個參數,通常這是一個指標。
做為替代/proc檔案系統的方法,你可以為調試實現若干ioctl命令。這些命令從驅動程式空間複製相關資料到進程空間,在進程空間裡檢查這些資料。
只有使用ioctl擷取資訊比起/proc來要困難一些,因為你一個程式調用ioctl並顯示結果。必須編寫這樣的程式,還要編譯,保持與你測試的模組間的一致性等。
不過有時候這是最好的擷取資訊的方法,因為它比起讀/proc來要快得多。如果在資料寫到螢幕前必須完成某些處理工作,以二進位擷取資料要比讀取文字檔有效得多。此外,ioctl不限制返回資料的大小。
ioctl方法的一個優點是,當調試關閉後調試命令仍然可以保留在驅動程式中。/proc檔案對任何查看這個目錄的人都是可見的,然而與/proc檔案不同,未公開的ioctl命令通常都不會被注意到。此外,如果驅動程式有什麼異常,它們仍然可以用來調試。唯一的缺點就是模組會稍微大一些。
通過監視調試
有時你遇到的問題並不特別糟,通過在使用者空間運行應用程式來查看驅動程式與系統之間的互動過程可以協助你捕捉到一些小問題,並可以驗證驅動程式確實工作正常。例如,看到scull的read實現如何處理不同資料量的read請求後,我對scull更有信心。
有許多方法監視一個使用者態程式的工作情況。你可以用調試器一步步跟蹤它的函數,插入列印語句,或者用strace運行程式。在實際目的是查看核心代碼時,最後一項技術非常有用。
strace命令是一個功能非常強大的工具,它可以現實程式所調用的所有系統調用。它不僅可以顯示調用,而且還能顯示調用的參數,以符號方式顯示傳回值。當系統調用失敗時,錯誤的符號值(如,ENOMEM)和對應的字串(Out of memory)同時顯示。strace還有許多命令列選項;最常用的是-t,它用來顯示調用發生的時間,-T,顯示調用所花費的時間,以及-o,將輸出重新導向到一個檔案中。預設情況下,strace將所有跟蹤資訊列印到stderr上。
strace從核心接收資訊。這意味著一個程式無論是否按調試方式編譯(用gcc的-g選項)或是被去掉了符號資訊都可以被跟蹤。與調試器可以串連到一個運行進程並控制它類似,你還可以跟蹤一個已經啟動並執行進程。
跟蹤資訊通常用來建置錯誤報告報告給應用開發人員,但是對核心編程人員來說也一樣非常有用。我們可以看到系統調用是如何執行驅動程式代碼的;strace允許我們檢查每一次調用輸入輸出的一致性。
例如,下面的螢幕輸出給出了命令ls /dev > /dev/scull0的最後幾行:
(代碼)
很明顯,在ls完成目標目錄的檢索後首次對write的調用中,它試圖寫4KB。很奇怪,唯寫了4000個位元組,接著重試這一操作。然而,我們知道scull的write實現每次唯寫一個量子,我在這裡看到了部分寫。經過若干步驟之後,所有的東西都清空了,程式正常退出。
另一個例子,讓我們來讀scull裝置:
(代碼)
正如所料,read每次只能讀到4000個位元組,但是資料總量是不變的。注意本例中重試工作是如何組織的,注意它與上面寫跟蹤的對比。wc專門為快速讀資料進行了最佳化,它繞過了標準庫,以便每次用一個系統調用讀取更多的資料。你可以從跟蹤的read行中看到wc每次要讀16KB。
Unix專家可以在strace的輸出中找到很多有用資訊。如果你被這些符號搞得滿頭霧水,我可以只看檔案方法(open,read等等)是如何工作的。
個人認為,跟蹤工具在查明系統調用的執行階段錯誤過程中最有用。通常應用或示範程式中的perror調用不足以用來調試,而且對於查明到底是什麼樣的參數觸發了系統調用的錯誤也很有協助。
調試系統故障
即便你用了所有監視和調試技術,有時候驅動程式中依然有錯誤,當這樣的驅動程式執行會造成系統故障。當這種情況發生時,擷取足夠多的資訊來解決問題是至關重要的。
注意,“故障”不意味著“panic”。Linux代碼非常魯棒,可以很好地響應大部分錯誤:故障通常會導致當前進程的終止,但系統繼續運行。如果在進程上下文之外發生故障,或是組成系統的重要組件發生故障時,系統可能panic。但問題出在驅動程式時,通常只會導致產生故障的進程終止――即那個使用驅動程式的進程。唯一不可恢複的損失就是當進程被終止時,進程上下文分配的記憶體丟失了;例如,由驅動程式通過kmalloc分配的動態鏈表可能丟失。然而,由於核心會對尚是開啟的裝置調用close,你的驅動程式可以釋放任何有open方法分配的資源。
我們已經說過,當核心行為異常時會在控制台上顯示一些有用的資訊。下一節將解釋如何解碼和使用這些訊息。儘管它們對於初學者來說相當晦澀,處理器的給出資料都是些很有意思的資訊,通常無需額外測試就可以查明程式錯誤。
Oops訊息
大部分錯誤都是NULL指標引用或使用其他不正確的指標數值。這些錯誤通常會導致一個oops訊息。
由處理器使用的地址都是“虛”地址,而且通過一個複雜的稱為頁表(見第13章“Mmap和DMA”中的“頁表”一節)的結構映射為物理地址。當引用一個非法指標時,頁面映射機制就不能將地址映射到物理地址,並且處理器向作業系統發出一個“頁面失效”。如果地址確實是非法的,核心就無法從失效地址上“換頁”;如果此時處理在超級使用者太,系統於是就產生一個“oops”。值得注意的是,在版本2.1中核心處理失效的方式有所變化,它可以處理在超級使用者態的非法地址引用了。新實現將在第17章“最近發展”的“處理核心空間失效”中介紹。
oops顯示故障時的處理器狀態,模組CPU寄存器內容,頁描述符表的位置,以及其他似乎不能理解的資訊。這些是由失效處理函數(arch/*/kernel/traps.c)中的printk語句產生的,而且象前面“Printk”一節介紹的那樣進行指派。
讓我們看看這樣一個訊息。這裡給出的是傳統個人電腦(x86平台),運行Linux 2.0或更新版本的oops――版本1.2