4.2. 用打印調(diào)試

2018-02-24 15:49 更新

4.2.?用打印調(diào)試

最常用的調(diào)試技術(shù)是監(jiān)視, 在應(yīng)用程序編程當(dāng)中是通過(guò)在合適的地方調(diào)用 printf 來(lái)實(shí)現(xiàn). 在你調(diào)試內(nèi)核代碼時(shí), 你可以通過(guò) printk 來(lái)達(dá)到這個(gè)目的.

4.2.1.?printk

我們?cè)谇懊鎺渍轮惺褂?printk 函數(shù), 簡(jiǎn)單地假設(shè)它如同 printf 一樣使用. 現(xiàn)在到時(shí)候介紹一些不同的地方了.

一個(gè)不同是 printk 允許你根據(jù)消息的嚴(yán)重程度對(duì)其分類, 通過(guò)附加不同的記錄級(jí)別或者優(yōu)先級(jí)在消息上. 你常常用一個(gè)宏定義來(lái)指示記錄級(jí)別. 例如, KERN_INFO, 我們之前曾在一些打印語(yǔ)句的前面看到過(guò), 是消息記錄級(jí)別的一種可能值. 記錄宏定義擴(kuò)展成一個(gè)字串, 在編譯時(shí)與消息文本連接在一起; 這就是為什么下面的在優(yōu)先級(jí)和格式串之間沒(méi)有逗號(hào)的原因. 這里有 2 個(gè) printk 命令的例子, 一個(gè)調(diào)試消息, 一個(gè)緊急消息:


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
需要立刻動(dòng)作的情形.

KERN_CRIT
嚴(yán)重情況, 常常與嚴(yán)重的硬件或者軟件失效有關(guān).

KERN_ERR
用來(lái)報(bào)告錯(cuò)誤情況; 設(shè)備驅(qū)動(dòng)常常使用 KERN_ERR 來(lái)報(bào)告硬件故障.

KERN_WARNING
有問(wèn)題的情況的警告, 這些情況自己不會(huì)引起系統(tǒng)的嚴(yán)重問(wèn)題.

KERN_NOTICE
正常情況, 但是仍然值得注意. 在這個(gè)級(jí)別一些安全相關(guān)的情況會(huì)報(bào)告.

KERN_INFO
信息型消息. 在這個(gè)級(jí)別, 很多驅(qū)動(dòng)在啟動(dòng)時(shí)打印它們發(fā)現(xiàn)的硬件的信息.

KERN_DEBUG
用作調(diào)試消息.

每個(gè)字串( 在宏定義擴(kuò)展里 )代表一個(gè)在角括號(hào)中的整數(shù). 整數(shù)的范圍從 0 到 7, 越小的數(shù)表示越大的優(yōu)先級(jí).

一條沒(méi)有指定優(yōu)先級(jí)的 printk 語(yǔ)句缺省是 DEFAULT_MESSAGE_LOGLEVEL, 在 kernel/printk.c 里指定作為一個(gè)整數(shù). 在 2.6.10 內(nèi)核中, DEFAULT_MESSAGE_LOGLEVEL 是 KERN_WARNING, 但是在過(guò)去已知是改變的.

基于記錄級(jí)別, 內(nèi)核可能打印消息到當(dāng)前控制臺(tái), 可能是一個(gè)文本模式終端, 串口, 或者是一臺(tái)并口打印機(jī). 如果優(yōu)先級(jí)小于整型值 console_loglevel, 消息被遞交給控制臺(tái), 一次一行( 除非提供一個(gè)新行結(jié)尾, 否則什么都不發(fā)送 ). 如果 klogd 和 syslogd 都在系統(tǒng)中運(yùn)行, 內(nèi)核消息被追加到 /var/log/messages (或者另外根據(jù)你的 syslogd 配置處理), 獨(dú)立于 console_loglevel. 如果 klogd 沒(méi)有運(yùn)行, 你只有讀 /proc/kmsg ( 用 dmsg 命令最易做到 )將消息取到用戶空間. 當(dāng)使用 klogd 時(shí), 你應(yīng)當(dāng)記住, 它不會(huì)保存連續(xù)的同樣的行; 它只保留第一個(gè)這樣的行, 隨后是, 它收到的重復(fù)行數(shù).

變量 console_loglevel 初始化成 DEFAULT_CONSOLE_LOGLEVEL, 并且可通過(guò) sys_syslog 系統(tǒng)調(diào)用修改. 一種修改它的方法是在調(diào)用 klogd 時(shí)指定 -c 開(kāi)關(guān), 在 klogd 的 manpage 里有指定. 注意要改變當(dāng)前值, 你必須先殺掉 klogd, 接著使用 -c 選項(xiàng)重啟它. 另外, 你可寫一個(gè)程序來(lái)改變控制臺(tái)記錄級(jí)別. 你會(huì)發(fā)現(xiàn)這樣一個(gè)程序的版本在由 O' Reilly 提供的 FTP 站點(diǎn)上的 miscprogs/setlevel.c. 新的級(jí)別指定未一個(gè)整數(shù), 在 1 和 8 之前, 包含 1 和 8. 如果它設(shè)為 1, 只有 0 級(jí)消息( KERN_EMERG )到達(dá)控制臺(tái); 如果它設(shè)為 8, 所有消息, 包括調(diào)試消息, 都顯示.

也可以通過(guò)文本文件 /proc/sys/kernel/printk 讀寫控制臺(tái)記錄級(jí)別. 這個(gè)文件有 4 個(gè)整型值: 當(dāng)前記錄級(jí)別, 適用沒(méi)有明確記錄級(jí)別的消息的缺省級(jí)別, 允許的最小記錄級(jí)別, 以及啟動(dòng)時(shí)缺省記錄級(jí)別. 寫一個(gè)單個(gè)值到這個(gè)文件就改變當(dāng)前記錄級(jí)別成這個(gè)值; 因此, 例如, 你可以使所有內(nèi)核消息出現(xiàn)在控制臺(tái), 通過(guò)簡(jiǎn)單地輸入:

 # echo 8 > /proc/sys/kernel/printk 

現(xiàn)在應(yīng)當(dāng)清楚了為什么 hello.c 例子使用 KERN_ALERT 標(biāo)志; 它們是要確保消息會(huì)出現(xiàn)在控制臺(tái)上.

4.2.2.?重定向控制臺(tái)消息

Linux 在控制臺(tái)記錄策略上允許一些靈活性, 它允許你發(fā)送消息到一個(gè)指定的虛擬控制臺(tái)(如果你的控制臺(tái)使用的是文本屏幕). 缺省地, 這個(gè)"控制臺(tái)"是當(dāng)前虛擬終端. 為了選擇一個(gè)不同地虛擬終端來(lái)接收消息, 你可對(duì)任何控制臺(tái)設(shè)備調(diào)用 ioctl(TIOCLINUX). 下面的程序, setconsole, 可以用來(lái)選擇哪個(gè)控制臺(tái)接收內(nèi)核消息; 它必須由超級(jí)用戶運(yùn)行, 可以從 misc-progs 目錄得到.

下面是全部程序. 應(yīng)當(dāng)使用一個(gè)參數(shù)來(lái)指定用以接收消息的控制臺(tái)的編號(hào).


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, 來(lái)實(shí)現(xiàn)特定于 linux 的功能. 為使用 TIOCLINUX, 你傳遞它一個(gè)指向字節(jié)數(shù)組的指針作為參數(shù). 數(shù)組的第一個(gè)字節(jié)是一個(gè)數(shù), 指定需要的子命令, 下面的字節(jié)是特對(duì)于子命令的. 在 setconsole 里, 使用子命令 11, 下一個(gè)字節(jié)(存于 bytes[1])指定虛擬控制臺(tái). TIOCLINUX 的完整描述在內(nèi)核源碼的 drivers/char/tty_io.c 里.

4.2.3.?消息是如何記錄的

printk 函數(shù)將消息寫入一個(gè) __LOG_BUF_LEN 字節(jié)長(zhǎng)的環(huán)形緩存, 長(zhǎng)度值從 4 KB 到 1 MB, 由配置內(nèi)核時(shí)選擇. 這個(gè)函數(shù)接著喚醒任何在等待消息的進(jìn)程, 就是說(shuō), 任何在系統(tǒng)調(diào)用中睡眠或者在讀取 /proc/kmsg 的進(jìn)程. 這 2 個(gè)日志引擎的接口幾乎是等同的, 但是注意, 從 /proc/kmsg 中讀取是從日志緩存中消費(fèi)數(shù)據(jù), 然而 syslog 系統(tǒng)調(diào)用能夠選擇地在返回日志數(shù)據(jù)地同時(shí)保留它給其他進(jìn)程. 通常, 讀取 /proc 文件容易些并且是 klogd 的缺省做法. dmesg 命令可用來(lái)查看緩存的內(nèi)容, 不會(huì)沖掉它; 實(shí)際上, 這個(gè)命令將緩存區(qū)的整個(gè)內(nèi)容返回給 stdout, 不管它是否已經(jīng)被讀過(guò).

在停止 klogd 后, 如果你偶爾手工讀取內(nèi)核消息, 你會(huì)發(fā)現(xiàn) /proc 看起來(lái)象一個(gè) FIFO, 讀者阻塞在里面, 等待更多數(shù)據(jù). 顯然, 你無(wú)法以這種方式讀消息, 如果 klogd 或者其他進(jìn)程已經(jīng)在讀同樣的數(shù)據(jù), 因?yàn)槟阋?jìng)爭(zhēng)它.

如果環(huán)形緩存填滿, printk 繞回并在緩存的開(kāi)頭增加新數(shù)據(jù), 覆蓋掉最老的數(shù)據(jù). 因此, 這個(gè)記錄過(guò)程會(huì)丟失最老的數(shù)據(jù). 這個(gè)問(wèn)題相比于使用這樣一個(gè)環(huán)形緩存的優(yōu)點(diǎn)是可以忽略的. 例如, 環(huán)形緩存允許系統(tǒng)即便沒(méi)有一個(gè)日志進(jìn)程也可運(yùn)行, 在沒(méi)有人讀它的時(shí)候可以通過(guò)覆蓋舊數(shù)據(jù)浪費(fèi)最少的內(nèi)存. Linux 對(duì)于消息的解決方法的另一個(gè)特性是, printk 可以從任何地方調(diào)用, 甚至從一個(gè)中斷處理里面, 沒(méi)有限制能打印多少數(shù)據(jù). 唯一的缺點(diǎn)是可能丟失一些數(shù)據(jù).

如果 klogd 進(jìn)程在運(yùn)行, 它獲取內(nèi)核消息并分發(fā)給 syslogd, syslogd 接著檢查 /etc/syslog.conf 來(lái)找出如何處理它們. syslogd 根據(jù)一個(gè)設(shè)施和一個(gè)優(yōu)先級(jí)來(lái)區(qū)分消息; 這個(gè)設(shè)施和優(yōu)先級(jí)的允許值在 <sys/syslog.h> 中定義. 內(nèi)核消息由 LOG_KERN 設(shè)施來(lái)記錄, 在一個(gè)對(duì)應(yīng)于 printk 使用的優(yōu)先級(jí)上(例如, LOG_ERR 用于 KERN_ERR 消息). 如果 klogd 沒(méi)有運(yùn)行, 數(shù)據(jù)保留在環(huán)形緩存中直到有人讀它或者緩存被覆蓋.

如果你要避免你的系統(tǒng)被來(lái)自你的驅(qū)動(dòng)的監(jiān)視消息擊垮, 你或者給 klogd 指定一個(gè) -f (文件) 選項(xiàng)來(lái)指示它保存消息到一個(gè)特定的文件, 或者定制 /etc/syslog.conf 來(lái)適應(yīng)你的要求. 但是另外一種可能性是采用粗暴的方式: 殺掉 klogd 和詳細(xì)地打印消息在一個(gè)沒(méi)有用到的虛擬終端上,[13] 或者從一個(gè)沒(méi)有用到的 xterm 上發(fā)出命令 cat /proc/kmsg.

4.2.4.?打開(kāi)和關(guān)閉消息

在驅(qū)動(dòng)開(kāi)發(fā)的早期, printk 非常有助于調(diào)試和測(cè)試新代碼. 當(dāng)你正式發(fā)行驅(qū)動(dòng)時(shí), 換句話說(shuō), 你應(yīng)當(dāng)去掉, 或者至少關(guān)閉, 這些打印語(yǔ)句. 不幸的是, 你很可能會(huì)發(fā)現(xiàn), 就在你認(rèn)為你不再需要這些消息并去掉它們時(shí), 你要在驅(qū)動(dòng)中實(shí)現(xiàn)一個(gè)新特性(或者有人發(fā)現(xiàn)了一個(gè) bug), 你想要至少再打開(kāi)一個(gè)消息. 有幾個(gè)方法來(lái)解決這 2 個(gè)問(wèn)題, 全局性地打開(kāi)或關(guān)閉你地調(diào)試消息和打開(kāi)或關(guān)閉單個(gè)消息.

這里我們展示一種編碼 printk 調(diào)用的方法, 你可以單獨(dú)或全局地打開(kāi)或關(guān)閉它們; 這個(gè)技術(shù)依靠定義一個(gè)宏, 在你想使用它時(shí)就轉(zhuǎn)變成一個(gè) printk (或者 printf)調(diào)用.

  • 每個(gè) printk 語(yǔ)句可以打開(kāi)或關(guān)閉, 通過(guò)去除或添加單個(gè)字符到宏定義的名子.

  • 所有消息可以馬上關(guān)閉, 通過(guò)在編譯前改變 CFLAGS 變量的值.

  • 同一個(gè) print 語(yǔ)句可以在內(nèi)核代碼和用戶級(jí)代碼中使用, 因此對(duì)于格外的消息, 驅(qū)動(dòng)和測(cè)試程序能以同樣的方式被管理.

下面的代碼片斷實(shí)現(xiàn)了這些特性, 直接來(lái)自頭文件 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 */

符號(hào) PDEBUG 定義和去定義, 取決于 SCULL_DEBUG 是否定義, 和以何種方式顯示消息適合代碼運(yùn)行的環(huán)境: 當(dāng)它在內(nèi)核中就使用內(nèi)核調(diào)用 printk, 在用戶空間運(yùn)行就使用 libc 調(diào)用 fprintf 到標(biāo)準(zhǔn)錯(cuò)誤輸出. PDEBUGG 符號(hào), 換句話說(shuō), 什么不作; 他可用來(lái)輕易地"注釋" print 語(yǔ)句, 而不用完全去掉它們.

為進(jìn)一步簡(jiǎn)化過(guò)程, 添加下面的行到你的 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 對(duì) ANSI C 預(yù)處理器的擴(kuò)展, 支持帶可變個(gè)數(shù)參數(shù)的宏定義. 這個(gè) gcc 依賴不應(yīng)該是個(gè)問(wèn)題, 因?yàn)闊o(wú)論如何內(nèi)核固有的非常依賴于 gcc 特性. 另外, makefile 依賴 GNU 版本的 make; 再一次, 內(nèi)核也依賴 GNU make, 所以這個(gè)依賴不是問(wèn)題.

如果你熟悉 C 預(yù)處理器, 你可以擴(kuò)展給定的定義來(lái)實(shí)現(xiàn)一個(gè)"調(diào)試級(jí)別"的概念, 定義不同的級(jí)別, 安排一個(gè)整數(shù)(或者位掩碼)值給每個(gè)級(jí)別, 以便決定它應(yīng)當(dāng)多么詳細(xì).

但是每個(gè)驅(qū)動(dòng)有它自己的特性和監(jiān)視需求. 好的編程技巧是在靈活性和效率之間選擇最好的平衡, 我們無(wú)法告訴你什么是最好的. 記住, 預(yù)處理器條件(連同代碼中的常數(shù)表達(dá)式)在編譯時(shí)執(zhí)行, 因此你必須重新編譯來(lái)打開(kāi)或改變消息. 一個(gè)可能的選擇是使用 C 條件句, 它在運(yùn)行時(shí)執(zhí)行, 因而, 能允許你在出現(xiàn)執(zhí)行時(shí)打開(kāi)或改變消息機(jī)制. 這是一個(gè)好的特性, 但是它在每次代碼執(zhí)行時(shí)需要額外的處理, 這樣即便消息給關(guān)閉了也會(huì)影響效率. 有時(shí)這個(gè)效率損失無(wú)法接受.

本節(jié)出現(xiàn)的宏定義已經(jīng)證明在多種情況下是有用的, 唯一的缺點(diǎn)是要求在任何對(duì)它的消息改變后重新編譯.

4.2.5.?速率限制

如果你不小心, 你會(huì)發(fā)現(xiàn)自己用 printk 產(chǎn)生了上千條消息, 壓倒了控制臺(tái)并且, 可能地, 使系統(tǒng)日志文件溢出. 當(dāng)使用一個(gè)慢速控制臺(tái)設(shè)備(例如, 一個(gè)串口), 過(guò)量的消息速率也能拖慢系統(tǒng)或者只是使它不反應(yīng)了. 非常難于著手于系統(tǒng)出錯(cuò)的地方, 當(dāng)控制臺(tái)不停地輸出數(shù)據(jù). 因此, 你應(yīng)當(dāng)非常注意你打印什么, 特別在驅(qū)動(dòng)的產(chǎn)品版本以及特別在初始化完成后. 通常, 產(chǎn)品代碼在正常操作時(shí)不應(yīng)當(dāng)打印任何東西; 打印的輸出應(yīng)當(dāng)是指示需要注意的異常情況.

另一方面, 你可能想發(fā)出一個(gè)日志消息, 如果你驅(qū)動(dòng)的設(shè)備停止工作. 但是你應(yīng)當(dāng)小心不要做過(guò)了頭. 一個(gè)面對(duì)失敗永遠(yuǎn)繼續(xù)的傻瓜進(jìn)程能產(chǎn)生每秒上千次的嘗試; 如果你的驅(qū)動(dòng)每次都打印"my device is broken", 它可能產(chǎn)生大量的輸出, 如果控制臺(tái)設(shè)備慢就有可能霸占 CPU -- 沒(méi)有中斷用來(lái)驅(qū)動(dòng)控制臺(tái), 就算是一個(gè)串口或者一個(gè)行打印機(jī).

在很多情況下, 最好的做法是設(shè)置一個(gè)標(biāo)志說(shuō), "我已經(jīng)抱怨過(guò)這個(gè)了", 并不打印任何后來(lái)的消息只要這個(gè)標(biāo)志設(shè)置著. 然而, 有幾個(gè)理由偶爾發(fā)出一個(gè)"設(shè)備還是壞的"的提示. 內(nèi)核已經(jīng)提供了一個(gè)函數(shù)幫助這個(gè)情況:


int printk_ratelimit(void); 

這個(gè)函數(shù)應(yīng)當(dāng)在你認(rèn)為打印一個(gè)可能會(huì)常常重復(fù)的消息之前調(diào)用. 如果這個(gè)函數(shù)返回非零值, 繼續(xù)打印你的消息, 否則跳過(guò)它. 這樣, 典型的調(diào)用如這樣:


if (printk_ratelimit())
    printk(KERN_NOTICE "The printer is still on fire\n");

printk_ratelimit 通過(guò)跟蹤多少消息發(fā)向控制臺(tái)而工作. 當(dāng)輸出級(jí)別超過(guò)一個(gè)限度, printk_ratelimit 開(kāi)始返回 0 并使消息被扔掉.

printk_ratelimit 的行為可以通過(guò)修改 /proc/sys/kern/printk_ratelimit( 在重新使能消息前等待的秒數(shù) ) 和 /proc/sys/kernel/printk_ratelimit_burst(限速前可接收的消息數(shù))來(lái)定制.

4.2.6.?打印設(shè)備編號(hào)

偶爾地, 當(dāng)從一個(gè)驅(qū)動(dòng)打印消息, 你會(huì)想打印與感興趣的硬件相關(guān)聯(lián)的設(shè)備號(hào). 打印主次編號(hào)不是特別難, 但是, 為一致性考慮, 內(nèi)核提供了一些實(shí)用的宏定義( 在 <linux/kdev_t.h> 中定義)用于這個(gè)目的:


int print_dev_t(char *buffer, dev_t dev); 
char *format_dev_t(char *buffer, dev_t dev);

兩個(gè)宏定義都將設(shè)備號(hào)編碼進(jìn)給定的緩沖區(qū); 唯一的區(qū)別是 print_dev_t 返回打印的字符數(shù), 而 format_dev_t 返回緩存區(qū); 因此, 它可以直接用作 printk 調(diào)用的參數(shù), 但是必須記住 printk 只有提供一個(gè)結(jié)尾的新行才會(huì)刷行. 緩沖區(qū)應(yīng)當(dāng)足夠大以存放一個(gè)設(shè)備號(hào); 如果 64 位編號(hào)在以后的內(nèi)核發(fā)行中明顯可能, 這個(gè)緩沖區(qū)應(yīng)當(dāng)可能至少是 20 字節(jié)長(zhǎng).

[13] * 例如, 使用 setlevel 8; setconsole 10 來(lái)配置終端 10 來(lái)顯示消息.

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)