內核調試

1.調試技術
內核編程帶來了它自己的,獨特的調試挑戰。內核代碼不能簡單地在調試器中執行,也不能被簡單地跟蹤,因爲它是一組不與特定進程相關的功能。內核代碼的錯誤非常難重現並且可能導致整個系統崩潰,因此破壞很多用來發現它們的證據。
本章將介紹在如此惱人的情況下你可以用來監視內核代碼和跟蹤錯誤的技術。
1.1.內核中的調試支持
在第二章中,我們建議你編譯和安裝你自己的內核,而不是運行你所使用的發行版中的原始內核。運行你自己的內核
的最有力理由是內核開發者已經在內核中構建了很多調試特性。這些特性會創建額外的輸出並使系統運行變慢,因此它們在經銷商發行的內核中沒有被激活。但是,
作爲一個內核開發者,你有不同的優先權並樂意地接受(最小的)內核調試支持帶來的額外開銷。
這裏,我們列出應該在用於開發的內核中激活的配置選項。除非特別指明,否則,無論你使用的是你喜歡的任何內核配置工具,所有的這些選項都可以在“kernel hacking”菜單下找到。注意有些選項並不被所有的體系結構支持。
CONFIG_DEBUG_KERNEL
該選項只是使其它的調試選項可用;它必須被打開,但是,它自己不打開任何特性。
CONFIG_DEBUG_SLAB

個選項打開對許多種類型的內核內存分配函數的檢查;激活這些檢查,就可能發現許多內存越界和未初始化錯誤。每一個分配的內存字節在傳遞給調用者之前設置爲
0xa5,釋放之後設置爲0x6b。如果你曾經看到這些“毒物”中的任意一個重複出現在你的驅動程序的輸出中(或者經常出現在一個oops列表中),你就
能確切地知道該去找尋什麼樣的錯誤。當調試選項被激活的時候,內核在分配內存對象之前和之後都在其中放置特殊的監視值;如果這些值曾經被修改,內核就知道
有內存分配已經越界,它就會不客氣地提出警告。許多其它不顯眼的錯誤檢查也被激活。
CONFIG_DEBUG_PAGEALLOC
頁面被釋放時是整個的從內核地址空間中移除的。該選項顯著地降低了速度,但它也能迅速指出特定類型的內存崩潰錯誤。
CONFIG_DEBUG_SPINLOCK
激活該選項,內核捕捉對未初始化自旋鎖的操作和各種其它錯誤(比如解鎖一個鎖兩次)。
CONFIG_DEBUG_SPINLOCK_SLEEP
該選項能執行對持有自旋鎖的時候試圖睡眠的檢查。事實上,如果你調用一個可能潛在地睡眠的函數,它會提出警告,即使調用的函數不可能睡眠。
CONFIG_INIT_DEBUG
有__init(或__initdata)標記的項在系統初始化或模塊加載時被丟棄。該選項使你能對在初始化完成之後試圖訪問初始化時的內存的代碼進行檢查。
CONFIG_DEBUG_INFO
該選項使得內核編譯時包含完整的調試信息。如果你想使用gdb來調試內核,你就需要這些信息。如果你計劃使用gdb,你可能也想激活CONFIG_FRAME_POINTER選項。
CONFIG_MAGIC_SYSRQ
激活“神奇的系統請求鍵”。我們將在本章隨後的“系統掛起”一節中看到這些鍵。
CONFIG_DEBUG_STACKOVERFLOW
CONFIG_DEBUG_STACK_USAGE
這些選項可以幫助追蹤內核的棧溢出。棧溢出的確定跡象是一個沒有任何合理的回溯線索。第一個選項給內核添加明確的溢出檢查;第二個讓內核監視棧使用並通過神奇的系統請求鍵提供一些可用的數據。
CONFIG_KALLSYMS
該選項(位於“General setup/Standard features”)使得內核符號信息編譯到內核中;它默認是激活的。符號信息用來調試上下文;沒有它,oops列表僅能讓你以十六進制的方式回溯內核,這種方法沒有什麼用處。
CONFIG_IKCONFIG
CONFIG_IKCONFIG_PROC
這些選項(在“General setup”菜單下)使完整的內核配置狀態編譯進內核並通過/proc可用。大多數的內核開發者瞭解他們使用的配置,所以不需要這個選項(它使內核變得更大)。如果你嘗試調試其它人編譯的內核中的問題,它會非常有幫助。
CONFIG_ACPI_DEBUG
位於“Power management/ACPI”中。該選項打開詳細的ACPI(Advanced Configuration and Power Interface)調試信息,當你懷疑出現ACPI相關的問題時將非常有用。
CONFIG_DEBUG_DRIVER
位於“Device drivers”。打開驅動程序核心的調試信息,跟蹤底層支持代碼產生的問題時很有用。我們將在第14章學習驅動程序核心(driver core)。
CONFIG_SCSI_CONSTANTS
該選項位於“Device drivers/SCSI device support”。它可以建立詳細的SCSI錯誤信息。如果你編寫一個SCSI驅動程序,你可能想激活這個選項。
CONFIG_INPUT_EVBUG
該選項(位於“Device drivers/Input device support”)打開對輸入事件的詳細記錄。如果你在爲一個輸入設備編寫驅動程序,這個選項可能非常有幫助。這個選項有潛在的安全問題,因爲它記錄你輸入的所有信息,包括你的密碼。
CONFIG_PROFILING
該選項在“Profiling support”中。Profiling通常用於系統性能調節,但是它對跟蹤一些內核掛起及相關的錯誤也非常有用。
我們將在我們學習跟蹤內核問題的許多方法時重新遇到上面的其中一些選項。但首先,我們來看看經典的調試技術:print語句。
1.2.打印調試信息
最常用的調試技術是監視,在應用程序中通過在適當的地方調用printf語句來完成。當你調試內核代碼的時候,你可以使用printk實現相同的目標。
1.2.1.printk
我們在前面的章節中使用printk函數時簡單地假設它像pirntf一樣工作。現在是時候來介紹一些不同之處了。

個不同之處就是printk可以讓你根據信息的不同記錄級別或優先級來嚴格地分類要記錄的信息。通常用一個宏來表示記錄級別。例如,我們已經在一些前面的
print語句中看到過的KERN_INFO,是一個可能信息的記錄級別。表示記錄級別的宏擴展爲一個字符串,在編譯時連結到信息文本中;這就是爲什麼下
面的兩個例子中在優先級與格式化字符串之間沒有逗號的原因。這裏是prink命令的兩個例子,一條調試信息和個臨界情況信息:
printk(KERN_DEBUG "Here I am: %s:%i\n", __FILE__, __LINE__);
printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);
有8種可能的記錄級別字符串,它們定義在頭文件中;我們按照以重要性遞減的順序來列出它們:
KERN_EMERG
用於緊急情況信息,通常是那些在系統崩潰之前的信息。
KERN_ALERT
一個必須被立即處理的錯誤。
KERN_CRIT
臨界情況,常常與嚴重的硬件或軟件失敗相關。
KERN_ERR
用於報告錯誤情況;設備驅動程序常常使用KERN_ERR報告一個硬件問題。
KERN_WARNING
對一個有問題的情況提出警告,這些情況並不對系統造成嚴重的問題。
KERN_NOTICE
一個普通的,不過也有可能需要注意到的情況。許多安全相關的情況都是在這個級別被報告的。
KERN_INFO
提供信息的消息。很多的驅動程序在這個級別打印它們在啓動時找到的硬件的信息。
KERN_DEBUG
用於調試信息。
每一個字符串(宏擴展)表示一個在尖括號中的整數。整數範圍從0到7,更小的值表示更高的優先級。

有指定優先級的prink語句的默認是DEFAULT_MESSAGE_LOGLEVEL,在kernel/printk.c中作爲一個整型指定。在
2.6.10內核中,DEFAULT_MESSAGE_LOGLEVEL是KERN_WARNING,它已經與過去的不一樣了。
以記錄級別爲基
礎,內核可能向當前控制檯打印信息,它可能是一個文本模式的終端,一個串口,或一個並行打印機。如果優先級比整型變量console_loglevel的
小,信息將會每次向控制檯傳遞一行(只有遇到換行符的時候信息纔會被傳遞)。如果klogd和syslogd都在系統中運行,內核信息追加在/var
/log/messages文件(或根據syslogd的配置來處理)中,與console_loglevel無關。如果klogd不在運行,除非你讀取
/proc/kmsg文件(通常最簡單的方法是使用dmesg方法),信息將不會到達用戶空間。如果使用klogd,你要記住它不保存相同的連續的行;它
僅保存第一行,隨後跟着它收到的重複的行的數目。
console_loglevel變量被初始化爲
DEFAULT_CONSOLE_LOGLEVEL並且它可以通過sys_syslog系統調用來修改。一種方法是在調用klogd的時候指定-c開關,
就像klogd的幫助頁說明的一樣。注意要改變當前值,你必須首先殺死klogd然後重新使用-c選項啓動它。另外的選擇是寫一個程序區改變控制檯的記錄
級別。你可以在O'Reilly的FTP站點提供的源代碼文件中的找到一個這樣的程序,它在misc-progs/setlevel.c文件中。新的級別
以一個1和8之間的整型值指定,包含1和8。如果它設置爲1,那麼僅有0級別(KERN_EMERG)的信息能到達控制檯;如果設置爲8,那麼所有的信
息,包括調試信息,都在控制檯顯示。
也可以通過修改/proc/sys/kernel/printk文本文件來改變控制檯的記錄級別。這個文件包
含四個整型值:當前記錄級別,缺乏明確記錄級別的信息的默認級別,允許的最小級別,和系統引導時的默認級別。向文件中寫入一個值就將當前的記錄級別改變爲
該值;如此,你可以使所有的內核信息出現在控制檯上,例如,通過輸入:
#echo 8 > /proc/sys/kernel/printk
現在應該非常清楚爲什麼hello.c樣例代碼使用KERN_ALERT標誌了;它可以保證信息顯示在控制檯上。
1.2.2.重定向控制檯信息
Linux在控制檯的日誌記錄策略允許你把信息發送到一個指定的虛擬控制檯(如果你的控制檯是一個文本屏
幕)來提供一定的靈活性。默認情況下,“控制檯”就是當前的虛擬終端。你可以在任何控制檯設備上使用ioctl(TIOCLINUX)來選擇一個不同的虛
擬終端來接收信息。下面的setconsole程序可以用來選擇哪一個控制檯接收內核信息;它在misc-progs目錄下並且它必須由根用戶來運行。
下面是整個的程序代碼。你應該在調用它的時候使用一個參數來指定接收信息的控制檯數目。
int main(int argc, char **argv)
{
        /* 11 is the TIOCLINUX cmd number */
        char bytes[2] = {11, 0};
        if (argc == 2)
                bytes[1] = atoi(argv[1]); /* the chosen console */
        else {
                fprintf(stderr, "%s: need a single arg\n", argv[0]);
                exit(1);
        }
        /* use stdin */
        if (ioctl(STDIN_FILENO, TIOCLINUX, bytes) 
1.2.3.信息是怎樣被記錄的
printk函數把信息寫入到一個__LOG_BUF_LEN字節長的循環緩衝區中:一個在配置內核的
時候選擇的介於4KB到1MB之間的值。函數接着喚醒等待信息的所有進程,即那些在syslog系統調用或讀取/proc/kmsg時睡眠的進程。這兩個
記錄引擎幾乎是相同的,但是請注意從/proc/kmsg讀取信息時會消耗記錄緩衝區中的數據,然而syslog系統調用能隨意地返回記錄數據並把它留給
其它進程。一般來說,讀取/proc文件更簡單並且是klogd的默認行爲。dmesg命令可以用來查看緩衝區的內容而不沖洗(清除)它的內容;事實上,
該命令將緩衝區的所有內容返回到stdout(標準輸出)中,不論它是否已經被讀取過。
如果你碰巧手工讀取內核信息,停止klogd後,你會發現/proc文件就像一個FIFO,因爲讀者阻塞,等待更多的數據。很明顯,如果klogd或其它的進程已經讀取了相同的數據,你就不能使用這種方法,因爲會發生競爭。

果循環緩衝區被填滿,prink繞回並且在緩衝區開始的地方開始添加新數據,覆蓋最老的數據。因此,記錄進程失去最老的數據。與使用循環緩衝區的優勢相比
這個問題可以忽略不計。例如,循環緩衝區允許系統在沒有記錄進程的情況下運行,雖然因爲覆蓋舊的數據浪費了少量的內存。Linux記錄信息的方法的另一個
特點是prink函數可以從任何地方調用,甚至從一箇中斷處理程序,沒有打印多少數據的限制。唯一的缺點就是可能丟失一些數據。
如果klogd進
程正在運行,它取回內核信息併發送它們到syslogd,由syslogd來檢查/etc/syslog.conf文件以決定怎樣處理它們。
syslogd區分根據設施和優先級來區分數據;設施和優先級的允許值都定義在中。內核信息以LOG_KERN
設施在一個printk中使用的相應的優先級被記錄(比如,LOG_ERR對應KERN_ERR信息)。如果klogd沒有運行,數據存在於循環緩衝區中
只到被讀取或緩衝區溢出。
如果你想避免你的驅動程序的監視信息拉跨你的系統記錄,你能爲klogd指定-f(文件)選項以指示klogd在一個指
定的文件中保存信息,或者定製/etc/syslog.conf文件來適應你的需要。還有一種可能就是採用暴力的方法:殺死klogd並在一個沒有使用的
虛擬終端打印詳細的信息 ,或在一個沒有使用的xterm上使用cat /proc/kmsg命令。
1.2.4.打開和關閉信息
在驅動程序開發的初始階段,prink能很好地幫助你調試和測試你的新代碼。但是,當你正式地發佈你的驅動
程序的時候,你必須移除,或者至少禁止這樣的打印語句。不辛的是,你可能會發現一旦你認爲你不再需要這些信息並移除它們,你在驅動程序中實現一個新功能
(或者有人發現一個bug),你又想重新打開至少其中的一個信息。有很多方法來解決這兩個問題,通過全局地允許或禁止你的調試信息和打開或關閉單個的信
息。
下面展示一種編寫printk調用的方法,你可以單個地或全局地打開或關閉它們;該技術取決於定義一個分解printk(或printf)調用的宏,這個宏的功能爲:
? 每一個打印語句都能通過移除或添加單個的子母到宏的名字中來允許或禁止。
? 所有的信息都可以通過在編譯之前改變CFLAGS變量的值來禁止。
? 相同的打印語句能在內核和用戶級的代碼中使用,這樣驅動程序和測試程序就能以相同的方式來管理額外的信息。
下面的代碼段實現了這些功能,它直接來源於頭文件scull.h:
#undef PDEBUG /* undef it, just in case */
#ifdef SCULL_DEBUG
#       ifdef __KERNEL__
        /* This one if debugging is on, and kernel space */
#               define PDEBUG(fmt, args...)\
                printk( KERN_DEBUG "scull: " fmt, ## args)
#       else
        /* This one for user space */
#               define PDEBUG(fmt, args...)\
                fprintf(stderr, fmt, ## args)
#       endif
#else
#       define PDEBUG(fmt, args...) /* not debugging: nothing */
#endif
#undef PDEBUGG
#define PDEBUGG(fmt, args...) /* nothing: it's a placeholder */
PDEBUG
符號被定義或不被定義,取決於SCULL_DEBUG是否定義,顯示信息的方式與代碼運行的環境相關:當它在內核中時它使用內核調用printk,在用戶
空間時使用libc調用fprintf輸出到標準錯誤。PDEBUGG符號沒有什麼意義;它用來“註釋”打印語句而不完全地移除它們。
爲了使你將來的處理過程簡單一些,在你的makefile文件中添加以下的行:
# Comment/uncomment the following line to
# disable/enable debugging
DEBUG = y
# Add your debugging flag (or not) to CFLAGS
ifeq ($(DEBUG), y)
# "-O" is needed to expand inlines
        DEBFLAGS = -O -g -DSCULL_DEBUG
else
        DEBFLAGS = -O2
endif
CFLAGS += $(DEBFLAGS)
本節出現的宏依賴於ANSI C預處理器的gcc擴展,該擴展支持可變數目參數的宏。這種gcc依賴並不是一個難題,因爲內核也嚴重地依賴於gcc的特性。此外,makefile依賴於GNU的make;再一次,內核已經依賴於GNU make,所以該依賴也不是問題。
如果你熟悉C預處理器,你可以擴展已經給出的定義來實現“調試級別”的概念,定義不同的級別並指派一個整型值(或爲掩碼)來決定每一個級別的信息有多麼詳細。

是每個驅動程序有它自己的特性和監視需求。好的編程藝術就是在靈活性和效率之間作出最好的選擇,但我們也不能告訴你什麼是最好的選擇。記住有條件的預處理
代碼(代碼中的常量表達式也一樣)是在編譯時執行的,因此你必須通過重新編譯來打開或關閉信息。一個可能的選擇是使用在運行時執行的C條件語句,這樣,就
允許你在運行時關閉和打開信息。這是一個很好的方法,但是每次代碼執行時都需要額外的處理,即使信息被禁用時也會影響性能。
本節展示的宏被證明在許多情況下都非常有用,唯一的缺陷就是它的信息的任何改變都需要重新編譯該模塊。
1.2.5.速率限制
如果你不小心,你會發現你的printk語句產生了成千上萬的信息,塞滿控制檯或可能溢出系統的日誌文件。當使用
的是慢速的控制檯設備(比如,一個串行端口)時,過分的信息速率會拖慢系統速度或使系統變得反應遲鈍。當控制檯被不中斷的數據擁塞時,你很難處理並發現系
統出了什麼問題。因此,你必須非常小心你要打印什麼樣的信息,特別是在驅動程序的發佈版本和一旦初始化完成後。一般來說,產品的代碼中決不該在普通的操作
中打印任何信息;打印出的信息應該指示一個值得注意的異常情況。
另一方面,你想在你驅動的設備停止工作之後發出一個記錄信息。但你必須注意不要做
過度的事情。一個愚蠢的永遠運行的進程面對錯誤時每秒能產生成千上萬的重試操作;如果你的驅動程序每次都打印“設備壞掉”的信息,它能產生大量的輸出,如
果控制檯設備很慢,它有可能過多地佔用CPU——沒有中斷可以用來控制終端,即使它是一個串口或行式打印機。
很多情況下,最好的方法是設置一個標誌來說明,“我已經輸出過這個提示信息了”,一旦標誌被設置,就不再輸出任何進一步的信息。在有些情況下,也有偶爾發出“設備仍有錯誤”的提示的理由。內核提供了能在這些情況下有幫助的函數:
int printk_ratelimit(void);
這個函數應該在你考慮打印一個經常重複的信息之前調用。如果函數返回一個非0值,繼續打印你的信息,否則跳過它(打印信息的語句)。因此,典型的調用像這樣:
if (printk_ratelimit())
        printk(KERN_NOTICE "The printer is still on fire\n");
printk_ratelimit通過跟蹤有多少信息被髮送到控制檯來完成工作。如果輸出的信息超過一個閥值,printk_ratelimit開始返回0值以使得信息被丟棄。
可以通過修改/proc/sys/kernel/prink_ratelimit(重新打開信息等待的妙數)和/proc/sys/kernel/printk_ratelimit_burst(速率限制之前接受的信息數)來定製。
1.2.6.打印設備號
有時侯,當從一個驅動程序中打印信息時,你想打印與硬件結合的設備號以引起注意。打印主次設備號並不是非常難,但是,爲了一致性,內核提供了一對工具宏(在中定義)來達成這個目的:
int print_dev_t(char *buffer, dev_t dev);
char *format_dev_t(char *buffer, dev_t dev);

個宏都把設備號編碼到給出的buffer中;唯一的區別是print_dev_t返回的是被打印的字符數目,而format_dev_t返回
buffer;因此,它可以直接作爲printk調用的參數,雖然必須記住printk在遇到換行符之前不會輸出。緩衝區必須足夠大以能保存一個設備
號;64位的設備號在將來的內核中是明顯可能的,緩衝區至少需要20字節長。
1.3.通過查詢調試
前面的一節描述了printk怎樣工作及如何使用它。我們沒有談到的是它的缺點。
大量使用printk能
使系統顯著地變慢,即使你降低console_loglevel來避免裝載控制檯設備,因爲syslogd保持同步它的輸出文件;因此,每一行的輸出都導
致一個磁盤操作。從syslogd的視角來看這是正確的實現。爲了防止萬一系統在打印消息之後就崩潰,它嘗試把所有的信息都寫到磁盤上;當然,你不想因爲
調試信息的原因拖慢系統的速度。這個問題可以通過給/etc/syslogd.conf文件中出現的記錄文件加一個連字符前綴來解決
。修改配置文件的問題是在你完成調試之後你的修改還保存在配置文件中,即使是普通的系統操作你也想把信息儘可能快地寫到磁盤上。這樣的持久改變的另一種選
擇是運行一個其它程序而不是klogd(比如cat /proc/kmsg,像前邊建議的一樣),但是這樣可能不能爲普通的系統操作提供一個合適的環境。
更多情況下,獲得相應信息的最好方法當你需要信息的時候向系統查詢,而不是持續地產生數據。事實上,每個Unix系統都提供了許多工具來獲得系統信息:ps,netstat,vmstat,等等。
驅動開發者有一些可以用來查詢系統信息的技術:在/proc文件系統中創建一個文件,使用ioctl驅動程序方法,和通過sysfs導出屬性。使用sysfs需要很多驅動程序模式的背景知識。它在第14章討論。
1.3.1.使用/proc文件系統
/proc文件系統是一個特殊的,軟件創建的文件系統,內核使用它來向外界導出信息。每一個
/proc下的文件都依賴於一個當文件被讀取的時候匆忙產生文件內容的內核函數。我們已經看到過一些這樣的文件在運作;例如/proc/modules,
總是返回當前加載的模塊列表。
/proc在Linux系統中使用非常多。現在的Linux發佈版裏的許多工具,比如ps,top,和
uptime,就是從/proc中獲取它們需要的信息。一些設備驅動程序也通過/proc導出信息,當然你也可以這樣做。/proc文件系統是動態的,所
以你的模塊可以在任何時候添加和移除條目。
完整功能的/proc條目非常的複雜;其中,它們被寫入和讀取。但是大多數時候,/proc條目都是隻讀文件。本節只關注簡單的只讀情形。那些對實現更復雜的/proc條目感興趣的人可以通過學習這裏來打基礎;然後可以向內核源代碼查閱完整的信息。

是在開始之前,我們並不鼓勵你在/proc下添加文件。/proc文件系統被內核開發者認爲有點混亂,它已經超越了它最初的目的(提供系統中運行的進程的
信息)。我們建議你在新編寫的代碼中通過sysfs來提供信息。使用sysfs需要理解Linux設備模式,但是我們要等到第14章才能接觸到它。其實,
在/proc下創建文件是很簡單的,並且它們非常適合用於調試,因此我們在這裏學習它。
1.3.1.1.在/proc中實現文件
使用/proc的所有模塊都必須包含文件以定義適當的函數。
創建一個只讀的/proc文件,你的驅動程序必須實現一個當文件被讀取時產生數據的函數。當一些進程讀取這個文件時(使用read系統調用),該請求藉助於到達你的模塊。我們首先來看一下這個函數,在本節的後面再來了解註冊接口。
當進程從你的/proc文件中讀取數據時,內核分配一頁內存(比如,PAGE_SIZE字節)來保存驅動程序寫入的被返回用戶空間的數據。這個緩衝區被傳遞到你的函數,該函數是一個名爲read_proc的方法。
int (*read_proc)(char *page, char **start, off_t offset, int count,
               int *eof, void *data);
page
指針是你寫入數據的緩衝區;函數使用start來表明有趣的信息被寫在page的何處(後面有更詳細的討論);offset和count與read方法的
意義是一樣的。eof參數指向一個整型,驅動程序必須設置它來表明沒有更多的數據返回,data是驅動程序特定的數據指針,你可以用它來作爲內部簿記。
這個函數應該返回實際寫入page緩衝區的字節數目,就象read方法針對其它文件那樣。另外的輸出值是*eof和*start。eof是一個簡單的標誌,但是start值的使用稍微更復雜一些;它的目的是幫助實現大/proc文件(大於一頁)。
start
參數多少有些非傳統的使用。它的目的是表明哪裏(頁面內)可以找到返回給用戶的數據。當你的proc_read方法被調用時,*start是NULL值。
如果你保持它爲NULL,內核假設數據被寫入到頁的offset爲0的地方;換句話來說,它假設一個簡單的proc_read版本,把整個虛擬文件的內容
放置在頁中而不管offset參數。如果你設置*start爲一個非NULL值,內核假設*start指向的數據已經把offset計算在內並準備直接返
回給用戶。一般說來,簡單的proc_read方法忽略start,返回少量的數據。更復雜一些的方法設置*start爲page並只在請求的數據的位移
的開始處寫入數據。
start也解決了/proc文件的另一個長久的主要問題。有的時候內核數據結構的ASCII表示被連續的read調用改變,
因此讀取數據的進程必須找到該調用和下一個調用之間的不一致性。如果*start設置爲一個小的整型值,調用者使用它來增加
filp->f_pos,而不管返回的數據的數量。例如,如果你的read_proc函數返回一個大型的結構數組,並在第一次調用時返回5個這樣的
數據結構,*start應該被設置爲5。下一個調用在位移之上提供相同的值;驅動程序就知道從數組中的第六個數據結構返回值。這是一個被公認的它的作者
“hack”,你可以在fs/proc/generic.c中看到它。
注意有一個更好的方法來實現大/proc文件;它被稱爲seq_file,我們不久就會討論它。首先,是時候給出一個例子了。這是一個簡單的(有些醜陋的)scull設備的read_proc實現:
int scull_read_procmem(char *buf, char **start, off_t offset,
                        int count, int *eof, void *data)
{
        int i, j, len = 0;
        int limit = count - 80; /* Don't print more than this */
        
        for (i = 0; i data;
                if (down_interruptible(&d->sem))
                        return -ERESTARTSYS;
                len += sprintf(buf+len, "\nDevice %i: qset %i, q %i,
                     sz %li\n", i, d->qset, d->quantum, d->size);
               /* dump only the last item */
                if (qs->data && !qs->next) 
                        for (j = 0; j qset; j++) {
                                if (qs->data[j])
                                        len += sprintf(buf + len,
                                        "       %4i: %8p\n",
                                        j, qs->data[j]);
                        }
                        up(&scull_devices.sem);
        }
        *eof = 1;
        return len;
}
這是相當典型的read_proc實現。它假設從不需要創建多於一頁的數據,因此忽略start和offset的值。爲了以防萬一,你必須小心不要緩衝區越界。
1.3.1.2.舊的接口
如果你通讀內核代碼,你會遇到用舊的接口實現/proc文件的代碼:
int (*get_info)(char *page, char **start, off_t offset, int count);
所有的參數都與read_proc中的參數有相同的意義和作用,但是它少了eof和data參數。這個接口仍然被支持,不過將來會被消除;新的代碼應該改爲使用read_proc接口。
1.3.1.3.創建你的/proc文件
一旦你定義了一個read_proc函數,必須把它與/proc層次結構下的一個條目相連接。可以通過調用create_proc_read_entry來完成:
struct proc_dir_entry *create_proc_read_entry(const char *name,
                     mode_t mode, struct proc_dir_entry *base,
                     read_proc_t *read_proc, void *data);

中,name是要創建的文件名,mode是文件的保護掩碼(傳遞給它0值時使用系統的默認值)。base表明文件將在哪個目錄下被創建(如果base爲
NULL,文件將在/proc根目錄下創建),read_proc是實現文件的read_proc函數,data被內核忽略(但它傳遞給
read_proc)。下面是scull中使得/proc函數對/proc/scullmem可用的調用:
create_proc_read_entry("scullmem", 0 /* default mode */,
                  NULL /* parent dir */, scull_read_procmem,
                  NULL /* client data */);
其中,我們在/proc下直接創建了一個名爲scullmem的文件,使用默認的全局可讀的保護法則。

錄條目指針能在/proc下創建完整的目錄層次。但是要注意,將一個條目放在以條目名字的一部分命名的目錄下更爲簡單——只要目錄本身已經存在。例如,一
個(常常被忽略)常用的協定是與設備驅動程序相關的條目應該放在driver/子目錄下;scull應該簡單地把它的條目放在該目錄下並把它的proc文
件的名字命名爲driver/scullmem。
/proc中的條目當然應該在模塊被卸載的模塊時候被移除。remove_proc_entry函數可以用來撤消create_proc_read_entry已經完成的功能:
remove_proc_entry("scullmem", NULL /* parent dir */
刪除條目失敗會導致時間的浪費,或者如果你的模塊已經被卸載,內核就可能崩潰。
如果你使用/proc文件,你必須記住它實現上的一些缺點——現在不鼓勵使用它你就不必覺得奇怪了。

重要的問題出現在移除/proc條目的時候。可能發生的情況是移除的時候文件正在被使用,因爲/proc條目沒有相應的擁有者,因此使用它們不會作用於模
塊的引用計數。比如,這個問題可以通過在移除模塊之前運行sleep
100
1.3.1.4.seq_file接口
就像我們上面提到的一樣,在/proc文件下實現一個大的文件有點笨拙。久而久之,/proc方
法因爲輸出的信息變得越來越大,和實現上的諸多問題而變得臭名昭著。seq_file接口作爲清除/proc代碼並讓內核開發人員的生活變得簡單的方法被
添加。該接口提供了一些簡單的函數集來實現大容量的內核虛擬文件。
seq_file接口假設你創建一個必須返回用戶空間的需要訪問一個系列中的單
個條目的虛擬文件。使用seq_file,你必須創建一個可以在順序中建立位置,向前移動並輸出系列中的一個條目的簡單“迭代器”對象。聽起來挺複雜,但
事實上,過程相當簡單。我們將一步步學習scull驅動程序中創建/proc文件的方法來演示它是怎麼做的。
第一步,不可避免地,是包含文件。然後你必須創建四個迭代器的方法,它們是start,next,stop和show。
start方法總是第一個被調用。該函數的原型爲:
void *start(struct seq_file *sfile, loff_t *pos);
sfile
參數幾乎總是可以被忽略。pos是一個表明從哪個位置開始讀取的整型值。對位置的解釋完全由實現決定;它不應該是一個文件中的字節位置。因爲
seq_file典型的實現是遍歷一個系列中感興趣的條目,位置通常被解釋爲一個指向系列中的下一個條目的光標。scull驅動程序把每個設備解釋爲系列
中的一個條目,所以pos只是一個scull_devices數組中的簡單索引。因此,在scull中使用的start方法爲:
static void *scull_seq_start(struct seq_file *s, loff_t *pos)
{
        if (*pos >= scull_nr_devs)
                return NULL; /* No more to read */
        return scull_devices + *pos;
}
如果返回值不爲NULL,它是一個只能被迭代器實現使用的私有值。
next函數應該把迭代器移動到下一個位置,如果系列中沒有餘下的條目就返回NULL。該方法的原型爲:
void *next(struct seq_file *sfile, void *v, loff_t *pos);
其中,v是一個從上一個start或next調用返回的迭代器,pos是文件中的當前位置。next應該遞增被pos指向的值;根據你的迭代器的工作情況,你可能(雖然可能不會)想遞增pos不止1個位置。下面是scull的實現方法:
static void *scull_seq_next(struct seq_file *s, void *v, loff_t *pos)
{
        (*pos)++;
        if (*pos >= scull_nr_devs)
                return NULL;
        return scull_devices + *pos;
}
當內核完成迭代器相關的工作後,它調用stop來清除:
void stop(struct seq_file *sfile, void *v);
scull實現中沒有清除工作,所以它的stop方法是空的。

得注意的是,seq_file代碼在設計上是在調用start和stop之間不可睡眠或執行其它的非原子操作任務的。有一天你肯定會看到在調用start
之後立即調用stop的情況。因此,你的start方法獲得一個信號量或自旋鎖是安全的。因爲你的其它的seq_file方法是原子的,整個系列的調用都
是原子的。(如果你還不能理解這一段的內容,在你讀了下一章之後再回來讀它)。
在這些調用中,內核調用show方法來實際輸出一些用戶空間感興趣的信息。該方法的原型是:
int show(struct seq_file *sfile, void *v);
該方法輸出系列中由迭代器v標識的條目。但是,它不能使用printk;有用於seq_file輸出的特殊函數集:
int seq_printf(struct seq_file *sfile, const char *fmt, ...);
這是printf函數的seq_file實現;它使用常用的格式化字符串和附加的參數值。而且你必須給它傳送傳遞給show函數的seq_file結構。如果seq_printf返回一個非0值,就意味着緩衝區已滿,並且輸出被忽略。但是大多數的實現忽略返回值。
int seq_putc(struct seq_file *sfile, char c);
int seq_puts(struct seq_file *sfile, const char *s);
它們和用戶空間的putc與puts函數是等價的。
int seq_escape(struct seq_file *m, const char *s, const char *esc);
該函數等價於seq_puts函數,除了在s中的字符如果也在esc中的話就以八進制的格式打印這些字符。esc的常用值是“ \t\n\\”,以防止嵌入的空白字符擾亂輸出並使shell腳本的疑惑。
int seq_path(struct seq_file *sfile, struct vfsmount *m,
             struct dentry *dentry, char *esc);
該函數可用於輸出與給定的目錄項相應的文件名。它在設備驅動程序中不太可能有用;我們只是爲了完整纔在這裏包含它。
回到我們的例子;scull中使用的show方法爲:
static int scull_seq_show(struct seq_file *s, void *v)
{
        struct scull_dev *dev = (struct scull_dev *) v;
        struct scull_qset *d;
        int i;
        
        if (down_interruptible(&dev->sem))
                return -ERESTARTSYS;
        seq_printf(s, "\nDevice %i: qset %i, q %i, sz %li\n",
                (int) (dev - scull_devices), dev->qset,
                dev->quantum, dev-size);
        for (d = dev->data; d; d = d->next) { /* scan the list */
                seq_printf(s, "item at %p, qset at %p\n",
                             d, d->data);
                /* dump only the last item */
                if (d->data && !d->next)
                        for (i = 0; i qset; i++) {
                                if (d->data)
                                        seq_printf(s, "%4i: %8p\n",
                                                i, d->data);
                        }
        }
        up(&dev->sem);
        return 0;
}
其中,我們最後來解釋我們的“迭代器”值,它是一個簡單的指向scull_dev的結構。
現在我們有了迭代器操作的全集,scull必須將它們打包並連接到/proc中的文件。第一步是填充一個seq_operations結構:
static struct seq_operations scull_seq_ops = {
        .start = scull_seq_start,
        .next = scull_seq_next,
        .stop = scull_seq_stop,
        .show = scull_seq_show
};

建好上面的結構後,我們必須創建一個內核可以理解的文件實現。我們沒有使用前邊描述的read_proc方法;使用seq_file時,最好從更底層次上
來連接/proc。這意味着創建一個file_operations結構(是的,它和字符設備驅動程序使用的結構是相同的)來實現內核處理讀和尋址文件必
須的所有操作。幸運的是,這個工作很簡單。第一步是創建一個open方法來連接文件到seq_file操作:
static int scull_proc_open(struct inode *inode, struct file *file)
{
        return seq_open(file, &scull_seq_ops);
}
seq_open調用使用我們上面定義的順序操作來連接file結構。open是我們必須自己實現的文件方法,因此現在我們可以建立我們的file_operations結構了:
static struct file_operations scull_proc_ops = {
        .owner = THIS_MODULE,
        .open = scull_proc_open,
        .read = seq_read,
        .llseek = seq_lseek,
        .release = seq_release
};
這裏我們指定了我們自己的open方法,但是使用的其它函數不便,seq_read,seq_lseek,和seq_release。
最後的一步是在/proc中創建實際的文件:
entry = create_proc_entry("scullseq", 0, NULL);
if (entry)
        entry->proc_fops = &scull_proc_ops;
我們使用底層的create_proc_entry,而不是使用create_proc_read_entry,create_proc_entry的原型如下:
struct proc_dir_entry *create_proc_entry(const char *name,
                                    mode_t mode,
                                    struct proc_dir_entry *parent);
參數和create_proc_read_entry中相應的參數相同:文件名字,文件的保護,和它的父目錄。
使用上面的代碼,scull就有了一個與前面的/proc條目非常相似的新的條目。但是它更出衆,因爲它不管輸出有多大都能工作,它正確地處理尋址,並且它從整體上來說更容易閱讀和維護。我們建議你在實現一個輸出包含不止小數目的行的文件中使用seq_file。
1.3.2.ioctl方法
ioctl是一個作用於文件描述符的系統調用,我們在第一章中描述過怎麼使用它;它接收一個表明要執行的命
令的數字和(可選地)另一個參數,通常是一個指針。作爲使用/proc文件系統的另外一種選擇,你可以實現一些爲調試定製的ioctl命令。這些命令可以
從驅動程序拷貝相關的數據結構到用戶空間,你就可以在用戶空間來檢查它們。
ioctl的這種獲取信息的使用方法比使用/proc稍微困難一些,因爲你需要另一個程序來調用ioctl並顯示結果。這個程序被編寫,編譯,並與你要測試的模塊保持同步。從另一方面來說,這時的驅動方代碼將比需要實現/proc文件的驅動代碼更加簡單。
ioctl是獲取信息的最好方式已經有好長一段時間了,因爲它比讀取/proc更加快速。如果在數據寫到屏幕之前必須對它進行處理,以二進制形式獲取數據比讀取一個文本文件更加高效。此外,ioctl不需要把小於一頁的數據分段。
ioctl
的另一個有趣的優點是即使調試被禁用後獲取信息的命令也能留在驅動程序中。與/proc文件不同的是,/proc文件是對查看該目錄的所有人都可見的(好
多人可能會驚奇“這些奇怪的文件是做什麼用的”),未公開的ioctl命令可能不容易被人注意到。此外,當有驅動程序有怪異的情況發生時,它仍然在那裏。
它們唯一的缺點是模塊可能輕微的更大一些。
1.4.通過監視調試
有的時候一些小問題可以通過監視用戶空間的應用程序來跟蹤。監視程序對創建一個可靠性的(驅動程序正確工作)驅動程序也有幫助。例如,我們在查看read實現怎樣應答對不同數量數據的請求後,能對我們的設備驅動程序更加有把握。
有很多監視用戶空間程序工作的不同方法。你可以使用一個調試器單步執行它的函數,添加打印語句,或在strace下面運行該程序。這裏我們只討論最後一種技術,它的真實目的是檢查內核代碼,所以非常有趣。
strace
命令是一個強大的工具,它顯示用戶空間程序的所有系統調用。不僅顯示這些調用,它還以符號化的形式顯示調用的參數和它們的返回值。如果系統調用失敗,錯誤
的符號值(比如,ENOMEM)和相應的字符串(內存不足)都被顯示。strace有很多命令行選項;最有用的選項是-t選項,它顯示每個系統調用開始執
行時的時間,-T顯示調用花費的時間,-e限制被跟蹤的調用的類型,-o重定向輸出到一個文件。默認的,strace打印在stderr打印跟蹤信息。
strace自己從內核接收信息。這就意味着一個程序可以被跟蹤,不論它是否在編譯的時候選擇調試支持(gcc的-g選項),不管調試信息是不是被移除。你還能掛接你的跟蹤到一個運行的進程,就像調試器連接到運行的進程並控制進程執行一樣。
追蹤信息常常用來支持發送到用戶程序開發者的bug報告,但是它對內核程序員的作用也是不可估量的。我們可以通過對系統調用的反應來了解驅動程序是怎麼執行的;strace允許我們檢查每一個調用的數據輸出和輸入的一致性。
例如,下面的屏幕轉儲顯示的是運行命令strace ls /dev > /dev/scull0的最後(包括大多數)幾行:
open("/dev",
O_RDONLY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY) = 3
fstat64(3, {st_mode=S_IFDIR|0755, st_size=24576, ...}) = 0
fcntl64(3, F_SETFD, FD_CLOEXEC) = 0
getdents64(3, /* 141 entries */, 4096) = 4088
[...]
getdents64(3, /* 0 entries */, 4096) = 0
close(3) = 0
[...]
fstat64(1,
{st_mode=S_IFCHR|0664, st_rdev=makedev(254, 0), ...}) = 0
write(1,
"MAKEDEV\nadmmidi0\nadmmidi1\nadmmid"..., 4096) = 4000
write(1, "b\nptywc\nptywd\nptywe\nptywf\nptyx0\n"..., 96) = 96
write(1,
"b\nptyxc\nptyxd\nptyxe\nptyxf\nptyy0\n"..., 4096) = 3904
write(1,
"s17\nvcs18\nvcs19\nvcs2\nvcs20\nvcs21"..., 192) = 192
write(1,
"\nvcs47\nvcs48\nvcs49\nvcs5\nvcs50\nvc"..., 673) = 673
close(1) = 0
exit_group(0) = ?

明顯,ls完成目標目錄的查詢後,第一個write調用嘗試寫4KB的數據。奇怪的是(對於ls),僅有4000字節的數據被寫出,然後操作重試。但是,
我們知道scull中的write實現每次寫單個量子,因此我們已經預期到不完全的寫操作。幾個步驟之後,所有的任務完成,程序成功退出。
另一個例子,我們來讀取scull設備(使用wc命令):
[...]
open("/dev/scull0", O_RDONLY|O_LARGEFILE) = 3
fstat64(3,
{st_mode=S_IFCHR|0664, st_rdev=makedev(254, 0), ...}) = 0
read(3,
"MAKEDEV\nadmmidi0\nadmmidi1\nadmmid"..., 16384) = 4000
read(3,
"b\nptywc\nptywd\nptywe\nptywf\nptyx0\n"..., 16384) = 4000
read(3,
"s17\nvcs18\nvcs19\nvcs2\nvcs20\nvcs21"..., 16384) = 865
read(3, "", 16384) = 0
fstat64(1,
{st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
write(1, "8865 /dev/scull0\n", 17) = 17
close(3) = 0
exit_group(0) = ?

如所期望的那樣,read每次能獲取4000字節的數據,但是總的數據量與前面例子中寫入到設備中的數據量是相同的。注意到這個例子中是怎樣組織重試的,
你會發現非常有意思,它和前面的例子是相對的。wc對快速讀取作了優化,因此,它繞過了標準庫,每一個系統調用都試圖讀取更多的數據。你可以從read的
trace輸出行中看到wc試圖一次讀取16KB的數據。
Linux專家能在strace的輸出信息中找到很多有用的信息。如果所有的符號讓你分心,你可以使用efile標誌來限制僅跟蹤文件方法(open,read等等)。
就個人來說,我們發現strace能準確地描述系統調用的運行錯誤。通常應用程序中的perror調用或是演示程序不能提供足夠的信息以供調試,並且能準確地發現哪一個參數引發了錯誤能有很大的幫助。
1.5.調試系統錯誤
即使你使用了所有的監視和調試技術,有時bugs仍然存在於你的驅動程序中,並在驅動程序被執行時引發系統錯誤。如果這種情況發生,收集儘可能多的信息用於解決問題將非常重要。

意“錯誤”並不意味這“panic”。Linux代碼已經足夠健壯,能優美地對錯誤作出迴應:一個錯誤通常導致當前進程被破壞,系統繼續運行。如果錯誤發
生在進程上下文之外或系統中一些重要部分被損壞時,系統就會panic。但是問題是由驅動程序錯誤引起的,它通常僅僅導致不辛使用驅動程序的進程突然死
亡。唯一的不可恢復的損壞是當分配給進程上下文的內存丟失時;舉例來說,驅動程序通過kmalloc分配的動態鏈表可能丟失。但是,因爲內核在進程死亡時
對任何打開的設備調用close操作,你的驅動程序可以釋放open方法分配的內存。
雖然oops通常不會讓你的系統整個崩潰,你可能發現發生
oops後你需要重啓計算機。一個有bug的驅動程序會使硬件處於不可用的狀態,使內核資源處於不一致的狀態,或者更糟的情況是在任意位置破壞內核內存。
通常情況下,你可以卸載有bug的驅動程序並在oops發生後重試。但是如果你看到任何關於系統整體運轉不良好的信息,你最好的選擇常常是馬上重啓系統。
我們提到當內核代碼運行失常時,終端會打印出相關的信息。下一節我們解釋怎樣解碼和使用這些信息。雖然對於新手來說,它們看起來非常難解,不過處理器轉儲包含許多有趣的信息,不用額外的測試就足夠精確地定位一個程序bug。
1.5.1.oops信息
許多bug是由於解析NULL指針或使用其它錯誤的指針值引起。通常這些bug的輸出結果是oops信息。

乎任何處理器使用的地址都是虛擬地址,它們通過一個複雜的頁表結構(也有這樣的例外情況,就是物理地址使用它自己的內存管理子系統)來映射到物理地址。當
一個無效(非法)的指針被解析時,頁機制映射指針到物理地址時就會失敗,處理器向操作系統通知一個頁錯誤。如果這個地址是不正確的,內核就不能“調入”遺
失的頁面;如果處理器在特權模式下,一旦有這種情況發生,內核就會產生oops。
oops顯示錯誤發生時刻的處理器狀態,包括CPU寄存器的內容和其它一些表面上看起來不可理解的信息。這些信息是由錯誤處理代碼中的printk語句產生的並以前面“printk”節描述的那樣被派發的。
讓我們來看一個這樣的信息。下面的例子是在一臺運行2.6版本內核的機器上解析一個NULL指針的結果。這裏最相關的信息是指令指針(EIP),即錯誤指令的地址:
Unable to handle kernel NULL pointer dereference
at virtual address 00000000
printing eip:
d083a064
Oops: 0002 [#1]
SMP
CPU: 0
EIP: 0060:[] Not tainted
EFLAGS: 00010246 (2.6.6)
EIP is at faulty_write+0x4/0x10 [faulty]
eax: 00000000 ebx: 00000000 ecx: 00000000 edx: 00000000
esi: cf8b2460 edi: cf8b2480 ebp: 00000005 esp: c31c5f74
ds: 007b es: 007b ss: 0068
Process bash (pid: 2086, threadinfo=c31c4000 task=cfa0a6c0)
Stack: c0150558 cf8b2460 080e9408 00000005
cf8b2480 00000000 cf8b2460 cf8b2460
fffffff7 080e9408 c31c4000 c0150682 cf8b2460
080e9408 00000005 cf8b2480
00000000 00000001 00000005 c0103f8f 00000001
080e9408 00000005 00000005
Call Trace:
[] vfs_write+0xb8/0x130
[] sys_write+0x42/0x70
[] syscall_call+0x7/0xb
Code: 89 15 00 00 00 00 c3 90 8d 74 26 00 83 ec
0c b8 00 a6 83 d0
該信息是由於向一個faulty模塊擁有的設備寫入的時候產生的,一個爲了演示錯誤而故意編寫的模塊。faulty.c中的write方法的實現很普通:
ssize_t faulty_write (struct file *filp, const char __user *buf,
                    size_t count, loff_t *pos)
{
        /* make a simple fault by dereferencing a NULL pointer */
        *(int *)0 = 0;
        return 0;
}
從代碼中可以看出,我們這裏所做的就是解析一個NULL指針。因爲0從來都不是一個有效的指針值,所以產生一個錯誤,內核進入錯誤處理代碼並輸出前面顯示的信息。然後調用該函數的進程就被殺死。
faulty模塊的read實現有不同的錯誤條件:
ssize_t faulty_read(struct file *filp, char __user *buf,
                   size_t count, loff_t *pos)
{
        int ret;
        char stack_buf[4];
        /* Let's try a buffer overflow */
        memset(stack_buf, oxff, 20);
        if (count > 4)
                count = 4; */ copy 4 bytes to the user */
        ret = copy_to_user(buf, stack_buf, count);
        if (!ret)
                return count;
        return ret;
}
該方法拷貝一個字符串到局部變量中;不辛的是,這個字符串比目的數組要長。該緩衝區溢出的結果是在函數返回的時候引發oops。return指令把指令指針指向毫無意義的地方,因此這類錯誤非常難於跟蹤,不過你可以得到下面的一些信息:
EIP: 0010:[]
Unable to handle kernel paging request at virtual
address ffffffff
printing eip:
ffffffff
Oops: 0000 [#5]
SMP
CPU: 0
EIP: 0060:[] Not tainted
EFLAGS: 00010296 (2.6.6)
EIP is at 0xffffffff
eax: 0000000c ebx: ffffffff ecx: 00000000 edx: bfffda7c
esi: cf434f00 edi: ffffffff ebp: 00002000 esp: c27fff78
ds: 007b es: 007b ss: 0068
Process head (pid: 2331, threadinfo=c27fe000 task=c3226150)
Stack: ffffffff bfffda70 00002000 cf434f20 00000001
00000286 cf434f00 fffffff7
bfffda70 c27fe000 c0150612 cf434f00 bfffda70
00002000 cf434f20 00000000
00000003 00002000 c0103f8f 00000003 bfffda70
00002000 00002000 bfffda70
Call Trace:
[] sys_read+0x42/0x70
[] syscall_call+0x7/0xb
Code: Bad EIP value.
在上面的例子中,我們僅能看到一部分調用棧(vfs_read和faulty_read丟失了),並且內核發出“錯誤的EIP值”的警告。這個警告與開始處列出的地址(ffffffff)都是內核棧崩潰的提示。
當你遇到一個oops,通常情況下你要作的第一件事就是查看錯誤發生的地方,通常它(錯誤位置)和調用棧是分開列出的。在上面提供的第一個oops中相應的行爲:
EIP is at faulty_write+0x4/0x10 [faulty]
其中我們可以看到我們位於faulty模塊(在方括號中列出)的faulty_write函數中。十六進制數字表示指令指針在函數的4字節偏移處,它有10(十六進制)字節長。這通常已經足夠指出問題的所在了。

果你需要更多的信息,調用棧可以提供怎麼找到代碼崩潰的地方的信息。調用棧是以十六進制的形式打印的;通過少許的工作,你就能通過棧列表決定局部變量的值
和函數的參數。有經驗的內核開發者能通過特定的模式來獲得重要信息;比如,如果我們來觀察faulty_read的oops中的棧列表:
Stack: ffffffff bfffda70 00002000 cf434f20 00000001
00000286 cf434f00 fffffff7
bfffda70 c27fe000 c0150612 cf434f00 bfffda70
00002000 cf434f20 00000000
00000003 00002000 c0103f8f 00000003 bfffda70
00002000 00002000 bfffda70

棧頂部的ffffffff是產生錯誤的字符串的一部分。在x86體系結構中,用戶空間棧的默認地址底於0xc0000000;因此,緊跟在它後邊的值
0xbfffda70可能是一個用戶空間的棧地址;實際上,它是傳遞到read系統調用的緩衝區的地址,每次都隨內核調用鏈被複制並傳遞。在x86結構上
(又一次,默認的),內核空間從0xc0000000開始,因此高於該地址的值幾乎應該是內核空間地址,以此類推。
最後,當我們查看oops列表的時候,應該時刻小心本章一開始提到的“slab毒物”。比如,如果你得到的oops中存在0xa5a5a5a5這樣的地址,你就幾乎可以肯定在其它地方忘記初始化動態內存。
請注意,只有你的內核在編譯的時候激活CONFIG_KALLSYMS選項,你才能看到符號化的調用棧(就像上面顯示的一樣)。否則,你只能看到單純的十六進制列表,在你使用其它的方法解碼它之前基本上沒什麼用處。
1.5.2.系統掛起
雖然大多數內核代碼中的bug都以oops信息的方式結束,有的時候它們可能完全地掛起系統。如果系統掛起,沒有
信息被打印。比如,如果代碼進入一個死循環
,內核就停止調度,系統不對任何事件作出反應,包括神奇的Ctrl-Alt-Del組合。你有兩種選擇可以用來處理系統掛起——事先阻止它們或在事後來調
試它們。
你可以通過在關鍵點插入schedule調用來阻止死循環。schedule調用(正如你所想的一樣)通過請求調度器來允許其它進程從當前進程獲取CPU時間。如果進程因爲你的驅動程序的bug在內核空間循環,schedule調用能讓你在跟蹤發生的情況之後殺死進程。

當然應該意識到,任何對schedule的調用都可能產生對你的驅動程序的可重入調用,因爲它允許其它進程運行。假設你已經在你的驅動程序中使用了適當的
鎖機制,這種重入情況一般不是什麼大問題。但是,你必須確定,不在你的驅動程序持有一個自旋鎖的任何時刻調用schedule。
如果你的驅動程序真的使系統掛起,而且你不知道在什麼地方插入schedule調用,最好的方法就是加入一些打印信息並把它們寫到控制檯(如果需要,你得改變console_loglevel值)。
有的時候系統可能顯得已經掛起,但是實際上並沒有。這有可能發生,比如,如果鍵盤由於一些奇怪的原因被持續鎖住。這些錯誤掛起可以通過查看你專門爲這一目的運行的程序的輸出來發現。時鐘或系統負載計量器是極好的狀態監視器;只要它們保持更新,調度器就在運行。

理很多掛起事件的一個不可或缺的工具是“神奇的系統請求鍵”,它在許多體系結構上都可以使用。神奇的系統請求鍵通過PC鍵盤上的Alt鍵和系統請求鍵的組
合來產生一個請求,或者在其它平臺(查看Documentation/sysrq.txt來獲得詳細的信息)上使用其它的特定鍵,它還可以在串口控制檯上
使用。第三個鍵必須和這兩個鍵同時按下,它完成其中的一系列的有用操作:
r
關閉鍵盤的原始模式;在崩潰程序(比如X服務器)使你的鍵盤處於奇怪的狀態時使用。
k
請求“secure attention key”(SAK)函數。SAK殺死所有在當前控制檯運行的進程,留給你一個乾淨的終端。
s
執行所有磁盤的緊急同步操作。
u
卸載。嘗試以只讀的方式重新掛載所有的磁盤。該操作通常在s之後馬上調用,它在系統處於嚴重問題時可以節省很多文件系統檢查時間。
b
引導。快速重啓系統。確定首先同步並重新掛載磁盤。
p
打印處理器的寄存器信息。
t
打印當前的任務鏈表。
m
打印內存信息。

在其它神奇的系統請求函數;查看內核源代碼Documentation目錄下的sysrq.txt文件可以獲得完整列表。注意神奇的系統請求鍵必須在內核
配置的時候明確地被激活,很多發行版因爲明顯的安全原因都沒有激活它。但是一個用於開發驅動程序的內核,激活神奇的系統請求鍵是值得的。神奇的系統請求鍵
可以在運行的時候通過下面的命令來禁用:
echo 0 > /proc/sys/kernel/sysrq
如果非特權用戶可以接觸到你的鍵盤,你應該考慮禁用它,以防止無意或有意的系統破壞。一些以前的內核版本的系統請求鍵默認是被禁用的,因此你必須在運行的時候向相同的/proc/sys文件寫入1來激活它。

統請求操作非常有用,因此它們可以被不能接觸控制檯管理員使用。/proc/sysrq_trigger文件是一個只寫的接入點,在那你能通過寫入相應的
命令字符來觸發指定的系統請求操作;然後你就能從內核日誌中收集任何的輸出數據。系統請求的這一接入點總是可用,即使你在控制檯禁用系統請求。

果你遇到一個“活動的掛起”,即你的驅動程序處於一個循環中但是系統作爲一個整體仍然在運行,有許多技術值得我們來了解。一般情況下,系統請求p函數能直
接找到錯誤的例程。如果失敗,你可以使用內核的審計函數。編譯內核的時候激活審計選項,並在命令行啓動時提供profile=2參數。使用
readprofile工具重置審計計數器,然後讓驅動程序進入循環執行。一段時間後,再次使用readprofile工具來查看內核在各個部分花費的時
間。Documentation/basic_profiling.txt文件中有你開始使用審計工具所需的所有信息。
追捕系統掛起的一個值得使
用的預防措施是以只讀的方式掛載所有的磁盤(或者卸載它們)。如果磁盤是隻讀的或是沒被掛載的,就沒有破壞文件系統或使它處於不一致狀態的風險。另外的一
個可能的方法是網絡文件系統,使用一臺通過NFS掛載它所有文件系統的電腦。“NFS-Root”特性必須在內核中打開,並且在系統啓動時必須給它傳遞特
殊的參數。這樣,你甚至不用使用神奇的系統請求鍵就可以避免文件系統崩潰,因爲文件系統的一致性是由NFS服務器來管理的,它不可能被你的設備驅動程序影
響。
1.6.調試器和相關的工具
調試模塊的最後一種方法是使用一個調試器來單步執行代碼,監視變量的值和機器的寄存器。這是一件耗時的工作,應該儘可能地避免。雖然如此,通過調試器來完成對代碼的細粒度調試的價值是不可估量的。
在內核上使用一個交互性的調試器是一種挑戰。內核在自己的地址空間代表系統的所有進程執行。因此,許多用戶空間調試器提供的很多公用功能,例如斷點和單步執行,在內核中很難獲得。本節我們討論許多調試內核的方法;它們各有優缺點。
1.6.1.使用gdb
gdb對檢查系統內部非常有用。在這個級別上高效率地使用gdb要求一些使用gdb命令的信心,對目標平臺的彙編代碼的一定了解,和配對源代碼與被優化過的彙編代碼的能力。
調試器必須把內核看成一個應用程序方式來調用。除了指定ELF內核境象的文件名外,你必須在命令行提供一個core文件名。對於一個運行的內核,這個core文件是內核的核心境象,/proc/kcore。典型的gdb調用像下面這樣:
gdb /usr/src/linux/vmlinux /proc/kcore
第一個參數是沒有壓縮過的可執行的ELF內核名,不是zImage或bzImage或其它任何爲特殊啓動環境編譯的內核。
gdb
命令行的第二個參數是core文件的名字。像其它/proc中的文件一樣,/proc/kcore是讀取的時候產生的。當read系統調用在/proc文
件系統中執行時,它映射到一個數據產生函數而不是一個數據獲取函數;我們已經在前面的“使用/proc文件系統”一節中使用過這一特性。kcore以一個
core文件的格式來表示“可執行的”的內核;它是一個很大的文件,因爲它代表整個與物理內存相應的內核地址空間。在gdb中,你能通過標準的gdb命令
來查看內核變量的值。比如,p jiffies打印從系統啓動到當前時間的時鐘滴答數。
當你在gdb中打印數據時,內核仍然在運行,並且不同的數
據條目在不同的時間有不一樣的值;但是,gdb通過緩存已經讀取的數據來優化對core文件的訪問。如果你嘗試再次讀取jiffies變量,你會得到與前
面相同的值。對於常規core文件來說,緩存值可以避免額外的磁盤訪問,它是正確的選擇,但是但使用“動態”core境象的時候很不方便。解決方法是在你
需要更新gdb緩存的任何時刻發出core-file
/proc/kcore命令;調試器使用新的core文件並丟棄所有的舊信息。但是你並不總是需要在讀取新數據的時候都使用core-file命
令;gdb每次讀取幾千字節的一塊數據而且僅緩存它已經被它引用過的塊。
許多標準gdb提供的功能在調試內核是不能使用。比如,gdb不能修改內核數據;在處理內存境象之前,它被期望在自己的控制下運行一個被調試的程序。也不能設置斷點或觀察點,或是單步執行內核函數。
注意,爲了讓gdb能使用符號信息,你必須在編譯內核的時候打開CONFIG_DEBUG_INFO選項集。這樣的做的結果是磁盤上更大的內核境象,但是,沒有那些信息,獲取內核變量的信息幾乎是不可能的。

果調試信息可以使用,你就能瞭解許多內核內部正在進行什麼工作的信息。gdb能打印出結構,跟隨指針,等等。但是檢查模塊非常困難。因爲模塊不是傳遞到
gdb的vmlinux境象的一部分,調試器對它一無所知。辛運的是,在2.6.7內核中,你可以傳遞給gdb檢查可加載模塊所需要的信息。
Linux的可加載模塊是ELF格式的可執行境象;它們被劃分爲很多段。一個典型的模塊可以包含一打或更多的段,但是與調試期間相應的段只有典型的下面三個:
.text
該段包含模塊的可執行代碼。調試器要能跟蹤或設置斷點就必須指定這一段的位置。(這些操作都與在/proc/kcore上運行調試器無關,但是當使用kgdb是它們很有用,kgdb在後面討論)。
.bss
.data
這兩個段包含模塊的變量。編譯的時候沒有被初始化的變量保存在.bss中,而那些被初始化的變量保存在.data段。

讓gdb能在可加載模塊上工作,需要給調試器提供被加載模塊的各個段的位置。這些信息可以在/sys/module下的sysfs中找到。例如,加載
scull模塊後,/sys/module/scull/sections目錄包含以各個段命名的文件,比如.text文件;每個文件的內容就是各個段的
基地址。
現在是我們執行gdb命令來告訴它我們模塊的信息的時候了。我們需要的命令是add-symbol-file;該命令需要的參數是模塊目
標文件的名字,.text的基地址,和一些可選的描述其它段的位置的參數。通過查看sysfs中的模塊的各個段的相關數據後,我們就可以構建命令:
(gdb)add-symbol-file …/scull.ko 0xd0832000 \
        -s .bss 0xd0837100 \
        -s .data 0xd0836be0
我們在樣例(gdbline)中包含一個簡單的腳本,它可以爲指定的模塊創建該命令。
現在我們可以使用gdb來檢查我們的可加載模塊了。下面是一個scull調試過程中的一個例子:
(gdb)add-symbol-file scull.ko 0xd0832000 \
        -s .bss 0xd0837100 \
        -s .data 0xd0836be0
add symbol table from file "scull.ko" at
            .text_addr = 0xd0832000
            .bss_addr = 0xd0837100
            .data_addr = 0xd0836be0
(y or n) y
Reading symbols from scull.ko...done.
(gdb) p scull_devices[0]
$1 = {data = 0xcfd66c50,
      quantum = 4000,
      qset = 1000,
      size = 20881,
      access_key = 0,
...}
其中我們可以看出第一個scull設備現在持有20,881字節的數據。如果我們願意,我們可以跟隨數據鏈,或查看模塊中其它的我們感興趣的任何信息。
另一個值得學習的有用的訣竅是:
(gdb)print *(address)
在address中填入一個十六進制的地址;輸出是一個文件和與該地址對應的代碼的行號。這一技術可能有用,例如,找出一個函數指針的確確指向。
我們仍然不能執行像設置斷點或修改數據的典型調試任務;要執行這些操作,我們必須使用像kdb(下一個討論)或kgdb(馬上就會學習到)這樣的工具。
1.6.2.kdb內核調試器
很多的讀者可能會奇怪爲什麼內核中沒有集成更高級的調試特性。答案很簡單,因爲Linus不信任交互性的調試器。他當心調試器會引入不良的修正,它們修補症狀而不是尋找到問題的真正根源。因此,沒有內置的調試器。

是其它的內核開發者,偶爾會使用交互型的調試工具。其中的一個是kdb,它是編譯到內核中的調試器,可以從oss.sgi.com獲得一個非官方的補丁
包。使用kdb,你必須獲取補丁包(確定獲取的是與你的內核版本相匹配的補丁包),應用它,並重新編譯和重新安裝內核。注意,在編寫本書時,kdb僅僅能
在IA-32(x86)系統上使用(雖然針對IA-64版本的補丁包在正式的內核源代碼包中存在了一段時間,但不久就被移除了)。
一旦你運行的是一個能使用kdb的內核,有很多方法進入調試器。在控制檯按下Pause(或Bread)鍵啓動調試器。當內核oops發生或到達斷點時,kdb也會啓動。不管怎樣,你將看到像下面這樣的信息:
Entering kdb (0xc0347b80) on processor 0 due to Keyboard Entry
[0]kdb>
注意在kdb運行的時候,內核的所有部分都停止運行。當你調用kdb時,不應該在系統上運行其它的任何東西;特別是,你不能有網絡連接打開——除非你在調試一個網絡設備驅動程序。如果你將使用kdb,以單用戶模式啓動系統通常是一個好注意。
我們使用scull的調試過程作爲例子。假設驅動程序已經加載,我們就能像下面一樣告訴kdb在scull_read中設置一個斷點:
[0]kdb> bp scull_read
        Instruction(i) BP #0 at 0xcd087c5dc (scull_read)
        is enabled globally adjust 1
[0]kdb> go
bp命令告訴kdb在下一次內核進入scull_read時停止執行。然後你鍵入go來繼續執行。在給其中的一個scull設備添加數據之後,我們就能在另一個終端的shell下執行cat來讀取它的內容,輸出如下:
Instruction(i) breakpoint #0 at 0xd087c5dc (adjusted)
0xd087c5dc scull_read: int3
Entering kdb (current=0xcf09f890, pid 1575) on
processor 0 due to
Breakpoint @ 0xd087c5dc
[0]kdb>
現在我們處於scull_read函數的起始處。想了解我們是怎樣到達該位置的,我們可以獲得堆棧痕跡:
[0]kdb> bt
     ESP         EIP         Function (args)
0xcdbddf74 0xd087c5dc   [scull]scull_read
0xcdbddf78 0xc0150718   vfs_read+0xb8
0xcdbddfa4 0xc01509c2   sys_read+0x42
0xcdbddfc4 0xc0103fcf    syscall_call+0x7
[0]kdb>
kdb試着打印調用棧的每一個函數的參數。但是它被編譯器使用的優化訣竅給搞混了。因此,它沒有能打印出scull_read的參數。
是查看數據的時候了。mds命令用來操縱數據;我們可以使用下面的命令來查詢scull_devices指針的值:
[0]kdb> mds scull_devices 1
0xd0880de8 cf36ac00     ....
其中我們請求一個從scull_devices位置開始處的一個(4字節)字的數據;結果告訴我們我們的設備數組的地址爲0xd0880de8;第一個設備結構的地址爲0xcf36ac00。爲了查看這個設備結構,我們必須使用這個地址:
[0]kdb> mds cf36ac00
0xcf36ac00 ce137dbc   ....
0xcf36ac04 00000fa0   ....
0xcf36ac08 000003e8   ....
0xcf36ac0c 0000009b   ....
0xcf36ac10 00000000   ....
0xcf36ac14 00000001   ....
0xcf36ac18 00000000   ....
0xcf36ac1c 00000001   ....

面的8行輸出與scull_dev結構中的各個數據項的起始地址相對應。因此,我們看到第一個設備的內存是在地址0xce137dbc分配的,量子大小是
4000(十六進制的fa0),量子集的大小是1000(十六進制的3e8),現在有155(十六進制的9b)字節存儲在設備中。
kdb可以修改數據的值。假設我們想削減一些設備中的數據:
[0]kdb> mm cf26ac0c 0x50
0xcf26ac0c = 0x50
緊接着的cat命令將比之前的命令返回更少的數據。
kdb有很多其它的功能,包括單步執行(按指令,而不是按C源代碼的行數),設置數據訪問的斷點,反彙編代碼,遍歷鏈表,訪問寄存器數據,等。使用kdb補丁包後,可以在內核源代碼樹的Documentation/kdb目錄下找到一套完整的手冊頁。
1.6.3.kgdb補丁
我們迄今爲止看到的交互型調試方法(在/proc/kcore上使用gdb和kdb)都缺乏用戶空間應用程序開發者所熟悉的那種環境。難道就沒有更好的供內核使用的能支持像修改變量值,設置斷點等等特性的調試器了嗎?

所描述的一樣,解決方法確實存在。在編寫本書時,有兩個獨立的補丁在流通使用,它們允許gdb在調試內核使用它的全部功能。令人迷惑的是,兩個補丁都叫做
kgdb。它們通過把運行測試內核的系統與運行調試器的系統分開來完成工作;它們都是通過一個串口線連接兩個系統的。因此,開發人員可以在他或她的穩定版
的桌面系統上運行gdb,而在一個運行在作爲犧牲的測試盒裏的內核上操作。以這種模式建立gdb需要在開始時花費一些時間,但是這些投資在一個非常困難的
bug顯現的時候就能迅速得到回報。
這些補丁處於很強的交互狀態,甚至有些特性是合併在一起的,因此我們避免談論它們的更多信息,除了在哪裏獲得它們和它們的基本特性外。感興趣的讀者我們支持你瞭解事件的當前狀態。

一個kgdb補丁現在可以在-mm內核樹下找到——加入2.6主內核樹必須經歷的路徑。這一版本的補丁支持
x86,SuperH,ia64,x86_64,SPARC,和32位的PPC體系結構。除了通過普通模式下的串口操作外,該版本的kgdb能通過局域網
來交互。只要激活以太網模式並在啓動時提供kgdboe參數設置來設置可以發出調試命令的IP地址。Documentation/i386/kgdb目錄
下的文檔包含怎樣設置的參數的內容 。
你可以有另外選擇,使用在
http://kgdb.sf.net/
找到的補丁。這一版本的調試器不支持網絡交互模式(不過聽說已經在開發當中),但是它有調試可加載模塊的內置支持。它支持x86,x86,x86_64,PowerPC,和S/390體系結構。
1.6.4.用戶模式的Linux端口
用戶模式Linux(UML)是一個非常有趣的概念。它在它自己的arch/um子目錄下結構化
一個獨立的Linux內核端口。但是它不能在新型的硬件上運行;它運行在一個通過Linux系統調用接口實現的虛擬機上。因此,UML允許Linux內核
作爲一個獨立的,用戶模式進程在Linux系統上運行。
有一個作爲用戶空間進程運行的內核好處。因爲它運行在一個被約束的,虛擬處理器上,一個有
bug的內核就不能破壞“真正的”系統。不同的硬件和軟件配置就可以簡單地在相同的盒子下測試。可能對於內核開發者最重要的是,用戶模式的內核能非常簡單
地被gdb或其它的調試器操縱。
不過別忘了,它僅僅只是另一個進程。UML有潛力來加速內核的開發。
但是,從驅動程序編寫者的角度來看,UML有一個很大的不足之處:用戶模式的內核不能訪問宿主系統的硬件。因此,雖然它能對調試本書中的大多數樣例代碼有用,但UML還不能對處理真正硬件設備的驅動程序的調試有幫助。
查看
http://user-mode-linux.sf.net/
以獲得UML的更多信息。
1.6.5.Linux Trace Toolkit
Linux Trace Toolkit(LTT)是一個內核補丁和一系列允許跟蹤內核事件的工具。跟蹤信息包含計時信息並能對特定時期內核發生了什麼創建一個合理完整的描述。因此,它不僅用來調試,還用來追蹤性能問題。
LTT和它的廣泛文檔可以在
http://www.opersys.com/LTT
上找到。
4.6.6.Dynamic Probes
Dynamic
Probes(或者Dprobes)是一個IBM發行的(遵循GPL)針對IA-32體系結構的調試工具。它允許在幾乎系統中的所有地方放置“探測點”,
不管是用戶空間還是內核空間。探測點包含一些代碼(是由特殊的,面向棧的語言編寫的),這些代碼在控制到達給定的點時執行。這些代碼能向用戶空間報告信
息,改變寄存器,或者做一些其它的事情。DProbes的有用特性就是一旦該功能被編譯進內核,就能在運行中的系統中的任何地方插入探測點而不需要改變內
核代碼或重啓。DProbes能與LTT一起工作,在任何地方插入新的跟蹤事件。
DPorbes工具可以從IBM的開源網站:
http://oss.software.ibm.com
下載。
發佈了32 篇原創文章 · 獲贊 23 · 訪問量 43萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章