最常用的除錯技術是monitoring, 在應用程序編程當中是通過在合適的地方調用printf 來實現. 在你調試內核代碼時, 你可以通過printk 來達到這個目的.
我們在前面幾章中使用printk 函數, 簡單地假設它如同printf 一樣使用. 現在到時候介紹一些不同的地方了.
一個不同是printk 允許你根據 message的嚴重程度對其分類, 通過附加不同的loglevel或者優先級在message上. 你常常用一個macro定義來指示loglevel. 例如, KERN_INFO, 我們之前曾在一些print statements的前綴看到過, 是其中一種可能的message loglevel. loglevel macro定義擴展成一個字串, 在編譯時與message文字連接在一起; 這就是為什麼下面的在優先級和格式串之間沒有逗號的原因. 這裡有2 個printk 命令的例子, 一個除錯message, 一個critical message:
每個字串( 在macro定義擴展裡)代表一個在角括號中的整數. 整數的範圍從0 到7, 越小的數表示越大的優先級.
一條沒有指定優先級的printk 語句預設值是DEFAULT_MESSAGE_LOGLEVEL, 在kernel/printk.c 裡被指定為一個整數. 在2.6.10 內核中, DEFAULT_MESSAGE_LOGLEVEL 是KERN_WARNING, 但是在過去已知是改變的.
基於loglevel, 內核可能print message到當前console, 可能是一個text-mode ternimal, 序列埠, 或者是一台並列埠印表機. 如果優先級小於整型值console_loglevel, message被遞交給console, 一次一行( 提供一個新行結尾時,才會發送). 如果klogd 和syslogd 都在系統中運行, 內核消息被追加到/var/log/messages (或者另外根據你的syslogd 配置處理), 獨立於console_loglevel之外. 如果klogd沒有運行, 你只有讀/proc/kmsg ( 用dmsg 命令最易做到)將消息取到user space. 當使用klogd 時, 你應當記住, 它不會保存連續的同樣的行; 它只保留第一個這樣的行, 隨後是, 它收到的重複行數.
變數console_loglevel 初始化成DEFAULT_CONSOLE_LOGLEVEL, 並且可通過sys_syslog system call修改. 一種修改它的方法是在調用klogd 時指定-c 開關, 在klogd 的manpage 裡有指定. 注意要改變當前值, 你必須先殺掉klogd , 接著使用-c 選項重啟它. 另外, 你可寫一個program來改變console loglevel. 你會在由O' Reilly 提供的FTP 站點上的miscprogs/setlevel.c發現這樣一個program的版本. 新的級別指定未一個整數, 在1 和8 之前, 包含1 和8. 如果它設為1, 只有0 級消息( KERN_EMERG )到達console; 如果它設為8, 所有消息, 包括調試消息, 都顯示.
也可以通過文本文件/proc/sys/kernel/printk 讀寫console loglevel. 這個文件有4 個整數值: 當前loglevel,沒有明確loglevel的message的預設級別, 最小允許的loglevel, 以及啟動時預設loglevel. 寫一個值到這個文件就改變當前loglevel成這個值; 因此, 例如, 你可以使所有內核message出現在console, 通過簡單地輸入:
一個不同是printk 允許你根據 message的嚴重程度對其分類, 通過附加不同的loglevel或者優先級在message上. 你常常用一個macro定義來指示loglevel. 例如, KERN_INFO, 我們之前曾在一些print statements的前綴看到過, 是其中一種可能的message loglevel. loglevel macro定義擴展成一個字串, 在編譯時與message文字連接在一起; 這就是為什麼下面的在優先級和格式串之間沒有逗號的原因. 這裡有2 個printk 命令的例子, 一個除錯message, 一個critical message:
printk(KERN_DEBUG "Here I am: %s:%i\n", __FILE__, __LINE__); printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);有8 種可能的loglevel字串, 在頭文件<linux/kernel.h> 裡定義; 我們按照嚴重性遞減的順序列出它們:
- KERN_EMERG
- 用於緊急message, 常常是那些crash前的message.
- KERN_ALERT
- 需要立刻動作的情形.
- KERN_CRIT
- 嚴重情況, 常常與嚴重的硬體或者軟體失效有關.
- KERN_ERR
- 用來報告錯誤情況; device driver常常使用KERN_ERR 來報告硬體故障.
- KERN_WARNING
- 有問題的情況的警告, 這些情況自己不會引起系統的嚴重問題.
- KERN_NOTICE
- 正常但是仍然值得注的情況 . 在這個級別會報告一些安全相關的情況.
- KERN_INFO
- 信息型message. 在這個級別, 很多driver會print在啟動時發現的硬體的信息.
- KERN_DEBUG
- 用作debug的message.
一條沒有指定優先級的printk 語句預設值是DEFAULT_MESSAGE_LOGLEVEL, 在kernel/printk.c 裡被指定為一個整數. 在2.6.10 內核中, DEFAULT_MESSAGE_LOGLEVEL 是KERN_WARNING, 但是在過去已知是改變的.
基於loglevel, 內核可能print message到當前console, 可能是一個text-mode ternimal, 序列埠, 或者是一台並列埠印表機. 如果優先級小於整型值console_loglevel, message被遞交給console, 一次一行( 提供一個新行結尾時,才會發送). 如果klogd 和syslogd 都在系統中運行, 內核消息被追加到/var/log/messages (或者另外根據你的syslogd 配置處理), 獨立於console_loglevel之外. 如果klogd沒有運行, 你只有讀/proc/kmsg ( 用dmsg 命令最易做到)將消息取到user space. 當使用klogd 時, 你應當記住, 它不會保存連續的同樣的行; 它只保留第一個這樣的行, 隨後是, 它收到的重複行數.
變數console_loglevel 初始化成DEFAULT_CONSOLE_LOGLEVEL, 並且可通過sys_syslog system call修改. 一種修改它的方法是在調用klogd 時指定-c 開關, 在klogd 的manpage 裡有指定. 注意要改變當前值, 你必須先殺掉klogd , 接著使用-c 選項重啟它. 另外, 你可寫一個program來改變console loglevel. 你會在由O' Reilly 提供的FTP 站點上的miscprogs/setlevel.c發現這樣一個program的版本. 新的級別指定未一個整數, 在1 和8 之前, 包含1 和8. 如果它設為1, 只有0 級消息( KERN_EMERG )到達console; 如果它設為8, 所有消息, 包括調試消息, 都顯示.
也可以通過文本文件/proc/sys/kernel/printk 讀寫console loglevel. 這個文件有4 個整數值: 當前loglevel,沒有明確loglevel的message的預設級別, 最小允許的loglevel, 以及啟動時預設loglevel. 寫一個值到這個文件就改變當前loglevel成這個值; 因此, 例如, 你可以使所有內核message出現在console, 通過簡單地輸入:
# echo 8 > /proc/sys/kernel/printk現在應當清楚了為什麼hello.c 例子使用KERN_ALERT 標誌; 它們是要確保message會出現在console上.
Linux 在console記錄策略上允許一些靈活性, 它允許你發送消息到一個指定的虛擬console(如果你的conslole使用的是text screen). 預設, 這個"console"是當前虛擬終端. 為了選擇一個不同地虛擬終端來接收消息, 你可對任何console設備調用ioctl(TIOCLINUX). 下面的程序, setconsole, 可以用來選擇哪個控制台接收kernel message; 它必須由superuser運行, 可以從misc-progs 目錄得到.
下面是全部程序. 應當使用一個參數來指定用以接收message的console的編號.
下面是全部程序. 應當使用一個參數來指定用以接收message的console的編號.
int main(int argc, char **argv)
{
char bytes[2] = {11,0}; /* 11 is the TIOCLINUX cmd number */
if (argc==2) bytes[1] = atoi(argv[1]); /* the chosen console */
else {
fprintf(stderr, "%s: need a single arg\n",argv[0]); exit(1); } if (ioctl(STDIN_FILENO, TIOCLINUX, bytes)<0) { /* use stdin */
fprintf(stderr,"%s: ioctl(stdin, TIOCLINUX): %s\n",
argv[0], strerror(errno));
exit(1);
}
exit(0);
}
setconsole 使用特殊的ioctl 命令TIOCLINUX, 來實現特定於linux 的功能. 為使用TIOCLINUX, 你傳遞它一個指向byte array的pointer作為參數. 數組的第byte字節是一個數, 指定需要的子命令,下面的字節是特對於子命令的. 在setconsole 裡, 使用子命令11, 下一個byte(存於bytes[1])指定虛擬console. TIOCLINUX 的完整描述在內核源碼的drivers/char/tty_io .c 裡.
printk 函數將message寫入一個__LOG_BUF_LEN byte長的circule buffer, 長度值從4 KB 到1 MB, 由配置kernel時選擇. 這個函數接著喚醒任何在等待message的process, 就是說, 任何在syslog system call中sleeping或者在讀取/proc/kmsg 的process. 這2 個logging engine的interface幾乎是等同的, 但是注意, 從/proc/kmsg 中讀取是從log buffer中消耗掉data, 然而syslog system call能夠選擇地將data再還回去log buffer,同時保留它給其他process. 通常, 讀取/proc 文件容易些並且是klogd 的預設做法. dmesg 命令可用來查看buffer的內容, 不會沖掉它; 實際上, 這個命令將緩存區的整個內容返回給stdout, 不管它是否已經被讀過.
在停止klogd 後, 如果你偶爾人工讀取kernel message, 你會發現/proc 看起來像一個FIFO, 讀者阻塞在裡面, 等待更多數據. 顯然, 你無法以這種方式讀messgae, 如果klogd 或者其他進程已經在讀同樣的數據, 因為你要競爭它.
如果circule buffer填滿, printk 繞回並在buffer的開頭增加新數據, 覆蓋掉最老的數據. 因此, 這個記錄過程會丟失最老的數據. 這個問題相比於使用這樣一個circule buffer的優點是可以忽略的. 例如, circule bffer允許系統即便沒有一個logging process也可運行, 在沒有人讀它的時候可以通過覆蓋舊數據浪費最少的memory. Linux 對於message的解決方法的另一個特性是, printk 可以從任何地方調用, 甚至從一個中斷處理裡面, 沒有限制能print多少數據. 唯一的缺點是可能丟失一些數據.
如果klogd process在運行, 它獲取kernel message並分發給syslogd, syslogd 接著檢查/etc/syslog.conf 來找出如何處理它們. syslogd 根據一個facility和一個priority來區分message; 這個facility和priority的允許值在<sys/syslog.h> 中定義. kernel message由LOG_KERN facility來記錄, 在一個對應於printk 使用的priority上(例如, LOG_ERR 用於KERN_ERR message). 如果klogd 沒有運行, 數據保留在circular buffer中直到有人讀它或者buffer被覆蓋.
如果你要避免你的系統被來自你的driver的監視message擊垮,你或者給klogd指定一個-f (文件)選項來指示它保存message息到一個特定的文件,或者定制/etc/syslog.conf來適應你的要求.但是另外一種可能性是採用粗暴的方式:殺掉klogd和詳細地print message在一個沒有用到的虛擬終端上, [ 13 ]或者從一個沒有用到的xterm上發出命令cat /proc/kmsg.
在停止klogd 後, 如果你偶爾人工讀取kernel message, 你會發現/proc 看起來像一個FIFO, 讀者阻塞在裡面, 等待更多數據. 顯然, 你無法以這種方式讀messgae, 如果klogd 或者其他進程已經在讀同樣的數據, 因為你要競爭它.
如果circule buffer填滿, printk 繞回並在buffer的開頭增加新數據, 覆蓋掉最老的數據. 因此, 這個記錄過程會丟失最老的數據. 這個問題相比於使用這樣一個circule buffer的優點是可以忽略的. 例如, circule bffer允許系統即便沒有一個logging process也可運行, 在沒有人讀它的時候可以通過覆蓋舊數據浪費最少的memory. Linux 對於message的解決方法的另一個特性是, printk 可以從任何地方調用, 甚至從一個中斷處理裡面, 沒有限制能print多少數據. 唯一的缺點是可能丟失一些數據.
如果klogd process在運行, 它獲取kernel message並分發給syslogd, syslogd 接著檢查/etc/syslog.conf 來找出如何處理它們. syslogd 根據一個facility和一個priority來區分message; 這個facility和priority的允許值在<sys/syslog.h> 中定義. kernel message由LOG_KERN facility來記錄, 在一個對應於printk 使用的priority上(例如, LOG_ERR 用於KERN_ERR message). 如果klogd 沒有運行, 數據保留在circular buffer中直到有人讀它或者buffer被覆蓋.
如果你要避免你的系統被來自你的driver的監視message擊垮,你或者給klogd指定一個-f (文件)選項來指示它保存message息到一個特定的文件,或者定制/etc/syslog.conf來適應你的要求.但是另外一種可能性是採用粗暴的方式:殺掉klogd和詳細地print message在一個沒有用到的虛擬終端上, [ 13 ]或者從一個沒有用到的xterm上發出命令cat /proc/kmsg.
在driver開發的早期, printk 非常有助於調試和測試新代碼. 當你正式發行drver時, 換句話說, 你應當去掉, 或者至少關閉, 這些print語句. 不幸的是, 你很可能會發現,就在你認為你不再需要這些message並去掉它們時, 你要在driver中實現一個新特性(或者有人發現了一個bug), 你想要至少再打開一個消message. 有幾個方法來解決這2個問題, 全局性地打開或關閉你地調試message和打開或關閉單個message.
這裡我們展示一種編碼printk 調用的方法, 你可以單獨或全局地打開或關閉它們; 這個技術依靠定義一個macro, 在你想使用它時就轉變成一個printk (或者printf)調用.
下面的代碼片斷實現了這些特性, 直接來自頭文件scull.h:
為進一步簡化過程, 添加下面的行到你的makfile 裡:
如果你熟悉C 預處理器, 你可以擴展給定的定義來實現一個"debug level"的概念, 定義不同的級別, 安排一個整數(或者bit mask)值給每個級別, 以便決定它應當多麼詳細.
但是每個driver有它自己的特性和監視需求. 好的編程技巧是在靈活性和效率之間選擇最好的平衡, 我們無法告訴你什麼是最好的. 記住, 預處理器條件(連同代碼中的常數表達式)在編譯時執行, 因此你必須重新編譯來打開或改變message. 一個可能的選擇是使用C 條件句, 它在運行時執行, 因而, 能允許你在出現執行時打開或改變message機制. 這是一個好的特性, 但是它在每次代碼執行時需要額外的處理, 這樣即便message給關閉了也會影響效率. 有時這個效率損失無法接受.
本節出現的macro定義已經證明在多種情況下是有用的, 唯一的缺點是要求在任何對它的message改變後重新編譯.
這裡我們展示一種編碼printk 調用的方法, 你可以單獨或全局地打開或關閉它們; 這個技術依靠定義一個macro, 在你想使用它時就轉變成一個printk (或者printf)調用.
- 每個printk 語句可以打開或關閉, 通過去除或添加單個字符到macro定義的名字.
- 所有消息可以馬上關閉, 通過在編譯前改變CFLAGS 變量的值.
- 同一個print 語句可以在kernel代碼和user level代碼中使用, 因此對於格外的message,driver和測試程序能以同樣的方式被管理.
#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 符號, 換句話說, 什麼不作; 他可用來輕易地"註釋" print 語句, 而不用完全去掉它們.
為進一步簡化過程, 添加下面的行到你的makfile 裡:
# Comment/uncomment the following line to disable/enable debugging DEBUG = y # Add your debugging flag (or not) to CFLAGS ifeq ($(DEBUG),y) DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" is needed to expand inlines else DEBFLAGS = -O2 endif CFLAGS += $(DEBFLAGS)本節中出現的macro定義依賴gcc 對ANSI C 預處理器的擴展, 支持帶可變數量的參數的macro定義. 這個gcc 依賴不應該是個問題, 因為無論如何kernel固有的非常依賴於gcc 特性. 另外, makefile 依賴GNU 版本的make; 再一次, kernel也依賴GNU make, 所以這個依賴不是問題.
如果你熟悉C 預處理器, 你可以擴展給定的定義來實現一個"debug level"的概念, 定義不同的級別, 安排一個整數(或者bit mask)值給每個級別, 以便決定它應當多麼詳細.
但是每個driver有它自己的特性和監視需求. 好的編程技巧是在靈活性和效率之間選擇最好的平衡, 我們無法告訴你什麼是最好的. 記住, 預處理器條件(連同代碼中的常數表達式)在編譯時執行, 因此你必須重新編譯來打開或改變message. 一個可能的選擇是使用C 條件句, 它在運行時執行, 因而, 能允許你在出現執行時打開或改變message機制. 這是一個好的特性, 但是它在每次代碼執行時需要額外的處理, 這樣即便message給關閉了也會影響效率. 有時這個效率損失無法接受.
本節出現的macro定義已經證明在多種情況下是有用的, 唯一的缺點是要求在任何對它的message改變後重新編譯.
如果你不小心, 你會發現自己用printk 產生了上千條message, 壓倒了控制台並且, 可能地, 使系統日誌文件溢出. 當使用一個慢速控制台設備(例如, 一個serial port), 過量的message速率也能拖慢系統或者只是使它不反應了. 非常難於著手於系統出錯的地方, 當控制台不停地輸出數據. 因此, 你應當非常注意你print什麼, 特別在driver的產品版本以及特別在初始化完成後. 通常, 產品代碼在正常操作時不應當print任何東西; print的輸出應當是指示需要注意的異常情況.
另一方面, 你可能想發出一個log message, 如果driver的device停止工作. 但是你應當小心不要做過了頭. 一個面對失敗永遠繼續的傻瓜process能產生每秒上千次的嘗試; 如果你的driver每次都打印"my device is broken", 它可能產生大量的輸出, 如果控制台設備慢就有可能霸占CPU -- 沒有中斷用來驅動控制台, 就算是一個serial port或者一個line printer.
在很多情況下, 最好的做法是設置一個flag說, "我已經抱怨過這個了", 並不print任何後來的message只要這個flag設置著. 然而, 有幾個理由偶爾發出一個"device還是壞的"的提示. kernel已經提供了一個函數幫助這個情況:
printk_ratelimit 的行為可以通過修改/proc/sys/kern/printk_ratelimit( 在重新使能message前等待的秒數) 和/proc/sys/kernel/printk_ratelimit_burst(限速前可接收的message數)來定制.
另一方面, 你可能想發出一個log message, 如果driver的device停止工作. 但是你應當小心不要做過了頭. 一個面對失敗永遠繼續的傻瓜process能產生每秒上千次的嘗試; 如果你的driver每次都打印"my device is broken", 它可能產生大量的輸出, 如果控制台設備慢就有可能霸占CPU -- 沒有中斷用來驅動控制台, 就算是一個serial port或者一個line printer.
在很多情況下, 最好的做法是設置一個flag說, "我已經抱怨過這個了", 並不print任何後來的message只要這個flag設置著. 然而, 有幾個理由偶爾發出一個"device還是壞的"的提示. kernel已經提供了一個函數幫助這個情況:
int printk_ratelimit(void);這個函數應當在你認為print一個可能會常常重複的message之前調用. 如果這個函數返回非零值, 繼續print你的message, 否則跳過它. 這樣, 典型的調用如這樣:
if (printk_ratelimit())
printk(KERN_NOTICE "The printer is still on fire\n");
printk_ratelimit 通過跟踪多少message發向控制台而工作. 當輸出級別超過一個限度, printk_ratelimit 開始返回0 並使message被扔掉.printk_ratelimit 的行為可以通過修改/proc/sys/kern/printk_ratelimit( 在重新使能message前等待的秒數) 和/proc/sys/kernel/printk_ratelimit_burst(限速前可接收的message數)來定制.
偶爾地, 當從一個driver print message, 你會想print與感興趣的硬件相關聯的device number. print major number不是特別難, 但是, 為一致性考慮, kernel提供了一些實用的macro定義( 在<linux/kdev_t.h> 中定義)用於這個目的:
int print_dev_t(char *buffer, dev_t dev); char *format_dev_t(char *buffer, dev_t dev);兩個macro定義都將device number編進給定的緩衝區; 唯一的區別是print_dev_t 返回print的charcters數, 而format_dev_t 返回buffer; 因此, 它可以直接用作printk 調用的參數, 但是必須記住printk只有提供一個結尾的新行才會刷行. buffer應當足夠大以存放一個device number; 如果64 位編號在以後的kernel發行中明顯可能, 這個buffer應當可能至少是20 bytes長.