最常用的調(diào)試技術(shù)是監(jiān)視, 在應(yīng)用程序編程當(dāng)中是通過在合適的地方調(diào)用 printf 來實現(xiàn). 在你調(diào)試內(nèi)核代碼時, 你可以通過 printk 來達(dá)到這個目的.
我們在前面幾章中使用 printk 函數(shù), 簡單地假設(shè)它如同 printf 一樣使用. 現(xiàn)在到時候介紹一些不同的地方了.
一個不同是 printk 允許你根據(jù)消息的嚴(yán)重程度對其分類, 通過附加不同的記錄級別或者優(yōu)先級在消息上. 你常常用一個宏定義來指示記錄級別. 例如, KERN_INFO, 我們之前曾在一些打印語句的前面看到過, 是消息記錄級別的一種可能值. 記錄宏定義擴(kuò)展成一個字串, 在編譯時與消息文本連接在一起; 這就是為什么下面的在優(yōu)先級和格式串之間沒有逗號的原因. 這里有 2 個 printk 命令的例子, 一個調(diào)試消息, 一個緊急消息:
printk(KERN_DEBUG "Here I am: %s:%i\n", __FILE__, __LINE__);
printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);
有 8 種可能的記錄字串, 在頭文件 <linux/kernel.h> 里定義; 我們按照嚴(yán)重性遞減的順序列出它們:
KERN_EMERG
用于緊急消息, 常常是那些崩潰前的消息.
KERN_ALERT
需要立刻動作的情形.
KERN_CRIT
嚴(yán)重情況, 常常與嚴(yán)重的硬件或者軟件失效有關(guān).
KERN_ERR
用來報告錯誤情況; 設(shè)備驅(qū)動常常使用 KERN_ERR 來報告硬件故障.
KERN_WARNING
有問題的情況的警告, 這些情況自己不會引起系統(tǒng)的嚴(yán)重問題.
KERN_NOTICE
正常情況, 但是仍然值得注意. 在這個級別一些安全相關(guān)的情況會報告.
KERN_INFO
信息型消息. 在這個級別, 很多驅(qū)動在啟動時打印它們發(fā)現(xiàn)的硬件的信息.
KERN_DEBUG
用作調(diào)試消息.
每個字串( 在宏定義擴(kuò)展里 )代表一個在角括號中的整數(shù). 整數(shù)的范圍從 0 到 7, 越小的數(shù)表示越大的優(yōu)先級.
一條沒有指定優(yōu)先級的 printk 語句缺省是 DEFAULT_MESSAGE_LOGLEVEL, 在 kernel/printk.c 里指定作為一個整數(shù). 在 2.6.10 內(nèi)核中, DEFAULT_MESSAGE_LOGLEVEL 是 KERN_WARNING, 但是在過去已知是改變的.
基于記錄級別, 內(nèi)核可能打印消息到當(dāng)前控制臺, 可能是一個文本模式終端, 串口, 或者是一臺并口打印機(jī). 如果優(yōu)先級小于整型值 console_loglevel, 消息被遞交給控制臺, 一次一行( 除非提供一個新行結(jié)尾, 否則什么都不發(fā)送 ). 如果 klogd 和 syslogd 都在系統(tǒng)中運行, 內(nèi)核消息被追加到 /var/log/messages (或者另外根據(jù)你的 syslogd 配置處理), 獨立于 console_loglevel. 如果 klogd 沒有運行, 你只有讀 /proc/kmsg ( 用 dmsg 命令最易做到 )將消息取到用戶空間. 當(dāng)使用 klogd 時, 你應(yīng)當(dāng)記住, 它不會保存連續(xù)的同樣的行; 它只保留第一個這樣的行, 隨后是, 它收到的重復(fù)行數(shù).
變量 console_loglevel 初始化成 DEFAULT_CONSOLE_LOGLEVEL, 并且可通過 sys_syslog 系統(tǒng)調(diào)用修改. 一種修改它的方法是在調(diào)用 klogd 時指定 -c 開關(guān), 在 klogd 的 manpage 里有指定. 注意要改變當(dāng)前值, 你必須先殺掉 klogd, 接著使用 -c 選項重啟它. 另外, 你可寫一個程序來改變控制臺記錄級別. 你會發(fā)現(xiàn)這樣一個程序的版本在由 O' Reilly 提供的 FTP 站點上的 miscprogs/setlevel.c. 新的級別指定未一個整數(shù), 在 1 和 8 之前, 包含 1 和 8. 如果它設(shè)為 1, 只有 0 級消息( KERN_EMERG )到達(dá)控制臺; 如果它設(shè)為 8, 所有消息, 包括調(diào)試消息, 都顯示.
也可以通過文本文件 /proc/sys/kernel/printk 讀寫控制臺記錄級別. 這個文件有 4 個整型值: 當(dāng)前記錄級別, 適用沒有明確記錄級別的消息的缺省級別, 允許的最小記錄級別, 以及啟動時缺省記錄級別. 寫一個單個值到這個文件就改變當(dāng)前記錄級別成這個值; 因此, 例如, 你可以使所有內(nèi)核消息出現(xiàn)在控制臺, 通過簡單地輸入:
# echo 8 > /proc/sys/kernel/printk
現(xiàn)在應(yīng)當(dāng)清楚了為什么 hello.c 例子使用 KERN_ALERT 標(biāo)志; 它們是要確保消息會出現(xiàn)在控制臺上.
Linux 在控制臺記錄策略上允許一些靈活性, 它允許你發(fā)送消息到一個指定的虛擬控制臺(如果你的控制臺使用的是文本屏幕). 缺省地, 這個"控制臺"是當(dāng)前虛擬終端. 為了選擇一個不同地虛擬終端來接收消息, 你可對任何控制臺設(shè)備調(diào)用 ioctl(TIOCLINUX). 下面的程序, setconsole, 可以用來選擇哪個控制臺接收內(nèi)核消息; 它必須由超級用戶運行, 可以從 misc-progs 目錄得到.
下面是全部程序. 應(yīng)當(dāng)使用一個參數(shù)來指定用以接收消息的控制臺的編號.
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, 來實現(xiàn)特定于 linux 的功能. 為使用 TIOCLINUX, 你傳遞它一個指向字節(jié)數(shù)組的指針作為參數(shù). 數(shù)組的第一個字節(jié)是一個數(shù), 指定需要的子命令, 下面的字節(jié)是特對于子命令的. 在 setconsole 里, 使用子命令 11, 下一個字節(jié)(存于 bytes[1])指定虛擬控制臺. TIOCLINUX 的完整描述在內(nèi)核源碼的 drivers/char/tty_io.c 里.
printk 函數(shù)將消息寫入一個 __LOG_BUF_LEN 字節(jié)長的環(huán)形緩存, 長度值從 4 KB 到 1 MB, 由配置內(nèi)核時選擇. 這個函數(shù)接著喚醒任何在等待消息的進(jìn)程, 就是說, 任何在系統(tǒng)調(diào)用中睡眠或者在讀取 /proc/kmsg 的進(jìn)程. 這 2 個日志引擎的接口幾乎是等同的, 但是注意, 從 /proc/kmsg 中讀取是從日志緩存中消費數(shù)據(jù), 然而 syslog 系統(tǒng)調(diào)用能夠選擇地在返回日志數(shù)據(jù)地同時保留它給其他進(jìn)程. 通常, 讀取 /proc 文件容易些并且是 klogd 的缺省做法. dmesg 命令可用來查看緩存的內(nèi)容, 不會沖掉它; 實際上, 這個命令將緩存區(qū)的整個內(nèi)容返回給 stdout, 不管它是否已經(jīng)被讀過.
在停止 klogd 后, 如果你偶爾手工讀取內(nèi)核消息, 你會發(fā)現(xiàn) /proc 看起來象一個 FIFO, 讀者阻塞在里面, 等待更多數(shù)據(jù). 顯然, 你無法以這種方式讀消息, 如果 klogd 或者其他進(jìn)程已經(jīng)在讀同樣的數(shù)據(jù), 因為你要競爭它.
如果環(huán)形緩存填滿, printk 繞回并在緩存的開頭增加新數(shù)據(jù), 覆蓋掉最老的數(shù)據(jù). 因此, 這個記錄過程會丟失最老的數(shù)據(jù). 這個問題相比于使用這樣一個環(huán)形緩存的優(yōu)點是可以忽略的. 例如, 環(huán)形緩存允許系統(tǒng)即便沒有一個日志進(jìn)程也可運行, 在沒有人讀它的時候可以通過覆蓋舊數(shù)據(jù)浪費最少的內(nèi)存. Linux 對于消息的解決方法的另一個特性是, printk 可以從任何地方調(diào)用, 甚至從一個中斷處理里面, 沒有限制能打印多少數(shù)據(jù). 唯一的缺點是可能丟失一些數(shù)據(jù).
如果 klogd 進(jìn)程在運行, 它獲取內(nèi)核消息并分發(fā)給 syslogd, syslogd 接著檢查 /etc/syslog.conf 來找出如何處理它們. syslogd 根據(jù)一個設(shè)施和一個優(yōu)先級來區(qū)分消息; 這個設(shè)施和優(yōu)先級的允許值在 <sys/syslog.h> 中定義. 內(nèi)核消息由 LOG_KERN 設(shè)施來記錄, 在一個對應(yīng)于 printk 使用的優(yōu)先級上(例如, LOG_ERR 用于 KERN_ERR 消息). 如果 klogd 沒有運行, 數(shù)據(jù)保留在環(huán)形緩存中直到有人讀它或者緩存被覆蓋.
如果你要避免你的系統(tǒng)被來自你的驅(qū)動的監(jiān)視消息擊垮, 你或者給 klogd 指定一個 -f (文件) 選項來指示它保存消息到一個特定的文件, 或者定制 /etc/syslog.conf 來適應(yīng)你的要求. 但是另外一種可能性是采用粗暴的方式: 殺掉 klogd 和詳細(xì)地打印消息在一個沒有用到的虛擬終端上,[13] 或者從一個沒有用到的 xterm 上發(fā)出命令 cat /proc/kmsg.
在驅(qū)動開發(fā)的早期, printk 非常有助于調(diào)試和測試新代碼. 當(dāng)你正式發(fā)行驅(qū)動時, 換句話說, 你應(yīng)當(dāng)去掉, 或者至少關(guān)閉, 這些打印語句. 不幸的是, 你很可能會發(fā)現(xiàn), 就在你認(rèn)為你不再需要這些消息并去掉它們時, 你要在驅(qū)動中實現(xiàn)一個新特性(或者有人發(fā)現(xiàn)了一個 bug), 你想要至少再打開一個消息. 有幾個方法來解決這 2 個問題, 全局性地打開或關(guān)閉你地調(diào)試消息和打開或關(guān)閉單個消息.
這里我們展示一種編碼 printk 調(diào)用的方法, 你可以單獨或全局地打開或關(guān)閉它們; 這個技術(shù)依靠定義一個宏, 在你想使用它時就轉(zhuǎn)變成一個 printk (或者 printf)調(diào)用.
每個 printk 語句可以打開或關(guān)閉, 通過去除或添加單個字符到宏定義的名子.
所有消息可以馬上關(guān)閉, 通過在編譯前改變 CFLAGS 變量的值.
同一個 print 語句可以在內(nèi)核代碼和用戶級代碼中使用, 因此對于格外的消息, 驅(qū)動和測試程序能以同樣的方式被管理.
下面的代碼片斷實現(xiàn)了這些特性, 直接來自頭文件 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 是否定義, 和以何種方式顯示消息適合代碼運行的環(huán)境: 當(dāng)它在內(nèi)核中就使用內(nèi)核調(diào)用 printk, 在用戶空間運行就使用 libc 調(diào)用 fprintf 到標(biāo)準(zhǔn)錯誤輸出. PDEBUGG 符號, 換句話說, 什么不作; 他可用來輕易地"注釋" print 語句, 而不用完全去掉它們.
為進(jìn)一步簡化過程, 添加下面的行到你的 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)
本節(jié)中出現(xiàn)的宏定義依賴 gcc 對 ANSI C 預(yù)處理器的擴(kuò)展, 支持帶可變個數(shù)參數(shù)的宏定義. 這個 gcc 依賴不應(yīng)該是個問題, 因為無論如何內(nèi)核固有的非常依賴于 gcc 特性. 另外, makefile 依賴 GNU 版本的 make; 再一次, 內(nèi)核也依賴 GNU make, 所以這個依賴不是問題.
如果你熟悉 C 預(yù)處理器, 你可以擴(kuò)展給定的定義來實現(xiàn)一個"調(diào)試級別"的概念, 定義不同的級別, 安排一個整數(shù)(或者位掩碼)值給每個級別, 以便決定它應(yīng)當(dāng)多么詳細(xì).
但是每個驅(qū)動有它自己的特性和監(jiān)視需求. 好的編程技巧是在靈活性和效率之間選擇最好的平衡, 我們無法告訴你什么是最好的. 記住, 預(yù)處理器條件(連同代碼中的常數(shù)表達(dá)式)在編譯時執(zhí)行, 因此你必須重新編譯來打開或改變消息. 一個可能的選擇是使用 C 條件句, 它在運行時執(zhí)行, 因而, 能允許你在出現(xiàn)執(zhí)行時打開或改變消息機(jī)制. 這是一個好的特性, 但是它在每次代碼執(zhí)行時需要額外的處理, 這樣即便消息給關(guān)閉了也會影響效率. 有時這個效率損失無法接受.
本節(jié)出現(xiàn)的宏定義已經(jīng)證明在多種情況下是有用的, 唯一的缺點是要求在任何對它的消息改變后重新編譯.
如果你不小心, 你會發(fā)現(xiàn)自己用 printk 產(chǎn)生了上千條消息, 壓倒了控制臺并且, 可能地, 使系統(tǒng)日志文件溢出. 當(dāng)使用一個慢速控制臺設(shè)備(例如, 一個串口), 過量的消息速率也能拖慢系統(tǒng)或者只是使它不反應(yīng)了. 非常難于著手于系統(tǒng)出錯的地方, 當(dāng)控制臺不停地輸出數(shù)據(jù). 因此, 你應(yīng)當(dāng)非常注意你打印什么, 特別在驅(qū)動的產(chǎn)品版本以及特別在初始化完成后. 通常, 產(chǎn)品代碼在正常操作時不應(yīng)當(dāng)打印任何東西; 打印的輸出應(yīng)當(dāng)是指示需要注意的異常情況.
另一方面, 你可能想發(fā)出一個日志消息, 如果你驅(qū)動的設(shè)備停止工作. 但是你應(yīng)當(dāng)小心不要做過了頭. 一個面對失敗永遠(yuǎn)繼續(xù)的傻瓜進(jìn)程能產(chǎn)生每秒上千次的嘗試; 如果你的驅(qū)動每次都打印"my device is broken", 它可能產(chǎn)生大量的輸出, 如果控制臺設(shè)備慢就有可能霸占 CPU -- 沒有中斷用來驅(qū)動控制臺, 就算是一個串口或者一個行打印機(jī).
在很多情況下, 最好的做法是設(shè)置一個標(biāo)志說, "我已經(jīng)抱怨過這個了", 并不打印任何后來的消息只要這個標(biāo)志設(shè)置著. 然而, 有幾個理由偶爾發(fā)出一個"設(shè)備還是壞的"的提示. 內(nèi)核已經(jīng)提供了一個函數(shù)幫助這個情況:
int printk_ratelimit(void);
這個函數(shù)應(yīng)當(dāng)在你認(rèn)為打印一個可能會常常重復(fù)的消息之前調(diào)用. 如果這個函數(shù)返回非零值, 繼續(xù)打印你的消息, 否則跳過它. 這樣, 典型的調(diào)用如這樣:
if (printk_ratelimit())
printk(KERN_NOTICE "The printer is still on fire\n");
printk_ratelimit 通過跟蹤多少消息發(fā)向控制臺而工作. 當(dāng)輸出級別超過一個限度, printk_ratelimit 開始返回 0 并使消息被扔掉.
printk_ratelimit 的行為可以通過修改 /proc/sys/kern/printk_ratelimit( 在重新使能消息前等待的秒數(shù) ) 和 /proc/sys/kernel/printk_ratelimit_burst(限速前可接收的消息數(shù))來定制.
偶爾地, 當(dāng)從一個驅(qū)動打印消息, 你會想打印與感興趣的硬件相關(guān)聯(lián)的設(shè)備號. 打印主次編號不是特別難, 但是, 為一致性考慮, 內(nèi)核提供了一些實用的宏定義( 在 <linux/kdev_t.h> 中定義)用于這個目的:
int print_dev_t(char *buffer, dev_t dev);
char *format_dev_t(char *buffer, dev_t dev);
兩個宏定義都將設(shè)備號編碼進(jìn)給定的緩沖區(qū); 唯一的區(qū)別是 print_dev_t 返回打印的字符數(shù), 而 format_dev_t 返回緩存區(qū); 因此, 它可以直接用作 printk 調(diào)用的參數(shù), 但是必須記住 printk 只有提供一個結(jié)尾的新行才會刷行. 緩沖區(qū)應(yīng)當(dāng)足夠大以存放一個設(shè)備號; 如果 64 位編號在以后的內(nèi)核發(fā)行中明顯可能, 這個緩沖區(qū)應(yīng)當(dāng)可能至少是 20 字節(jié)長.
[13] * 例如, 使用 setlevel 8; setconsole 10 來配置終端 10 來顯示消息.
更多建議: