linux内核printk调试
1? printk及控制臺的日志級別
函數(shù)printk的使用方法和printf相似,用于內(nèi)核打印消息。printk根據(jù)日志級別(loglevel)對消息進行分類。
日志級別用宏定義,日志級別宏展開為一個字符串,在編譯時由預(yù)處理器將它和消息文本拼接成一個字符串,因此printk?函數(shù)中日志級別宏和格式字符串間不能有逗號。
下面是兩個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); |
printk的日志級別定義如下(在linux26/includelinux/kernel.h中):
| #defineKERN_EMERG"<0>"/*緊急事件消息,系統(tǒng)崩潰之前提示,表示系統(tǒng)不可用*/ #defineKERN_ALERT"<1>"/*報告消息,表示必須立即采取措施*/ #defineKERN_CRIT"<2>"/*臨界條件,通常涉及嚴重的硬件或軟件操作失敗*/ #defineKERN_ERR"<3>"/*錯誤條件,驅(qū)動程序常用KERN_ERR來報告硬件的錯誤*/ #defineKERN_WARNING"<4>"/*警告條件,對可能出現(xiàn)問題的情況進行警告*/ #defineKERN_NOTICE"<5>"/*正常但又重要的條件,用于提醒。常用于與安全相關(guān)的消息*/ #defineKERN_INFO"<6>"/*提示信息,如驅(qū)動程序啟動時,打印硬件信息*/ #defineKERN_DEBUG"<7>"/*調(diào)試級別的消息*/ ? extern int console_printk[]; ? #define console_loglevel? (console_printk[0]) #define default_message_loglevel (console_printk[1]) #define minimum_console_loglevel (console_printk[2]) #define default_console_loglevel (console_printk[3]) |
日志級別的范圍是0~7,沒有指定日志級別的printk語句默認采用的級別是DEFAULT_ MESSAGE_LOGLEVEL,其定義列出如下(在linux26/kernel/printk.c中):
| /*沒有定義日志級別的printk使用下面的默認級別*/ #define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING?警告條件*/ |
內(nèi)核可把消息打印到當(dāng)前控制臺上,可以指定控制臺為字符模式的終端或打印機等。默認情況下,“控制臺”就是當(dāng)前的虛擬終端。
為了更好地控制不同級別的信息顯示在控制臺上,內(nèi)核設(shè)置了控制臺的日志級別console_loglevel。printk日志級別的作用是打印一定級別的消息,與之類似,控制臺只顯示一定級別的消息。
當(dāng)日志級別小于console_loglevel時,消息才能顯示出來。控制臺相應(yīng)的日志級別定義如下:
| /*?顯示比這個級別更重發(fā)的消息*/ #define MINIMUM_CONSOLE_LOGLEVEL ?1 /*可以使用的最小日志級別*/ #define DEFAULT_CONSOLE_LOGLEVEL? 7 /*比KERN_DEBUG?更重要的消息都被打印*/ ? int console_printk[4] = { DEFAULT_CONSOLE_LOGLEVEL,/*控制臺日志級別,優(yōu)先級高于該值的消息將在控制臺顯示*/ /*默認消息日志級別,printk沒定義優(yōu)先級時,打印這個優(yōu)先級以上的消息*/ DEFAULT_MESSAGE_LOGLEVEL, /*最小控制臺日志級別,控制臺日志級別可被設(shè)置的最小值(最高優(yōu)先級)*/ MINIMUM_CONSOLE_LOGLEVEL, DEFAULT_CONSOLE_LOGLEVEL,/*?默認的控制臺日志級別*/ }; |
如果系統(tǒng)運行了klogd和syslogd,則無論console_loglevel為何值,內(nèi)核消息都將追加到/var/log/messages中。如果klogd沒有運行,消息不會傳遞到用戶空間,只能查看/proc/kmsg。
變量console_loglevel的初始值是DEFAULT_CONSOLE_LOGLEVEL,可以通過sys_syslog系統(tǒng)調(diào)用進行修 改。調(diào)用klogd時可以指定-c開關(guān)選項來修改這個變量。如果要修改它的當(dāng)前值,必須先殺掉klogd,再加-c選項重新啟動它。
通過讀寫/proc/sys/kernel/printk文件可讀取和修改控制臺的日志級別。查看這個文件的方法如下:
| #cat /proc/sys/kernel/printk 6???4??1???7 |
上面顯示的4個數(shù)據(jù)分別對應(yīng)控制臺日志級別、默認的消息日志級別、最低的控制臺日志級別和默認的控制臺日志級別。
可用下面的命令設(shè)置當(dāng)前日志級別:
| # echo 8 > /proc/sys/kernel/printk |
2? printk打印消息機制
在內(nèi)核中,函數(shù)printk將消息打印到環(huán)形緩沖區(qū)__log_buf中,并將消息傳給控制臺進行顯示。控制臺驅(qū)動程序根據(jù)控制臺的日志級別顯示日志消息。
應(yīng)用程序通過系統(tǒng)調(diào)用sys_syslog管理環(huán)形緩沖區(qū)__log_buf,它可以讀取數(shù)據(jù)、清除緩沖區(qū)、設(shè)置日志級別、開/關(guān)控制臺等。
當(dāng)系統(tǒng)調(diào)用sys_syslog從環(huán)形緩沖區(qū)__log_buf讀取數(shù)據(jù)時,如果緩沖區(qū)沒有數(shù)據(jù),系統(tǒng)調(diào)用sys_syslog所在進程將被加入到 等待隊列l(wèi)og_wait中進行等待。當(dāng)printk將數(shù)據(jù)打印到緩沖區(qū)后,將喚醒系統(tǒng)調(diào)用sys_syslog所在進程從緩沖區(qū)中讀取數(shù)據(jù)。等待隊列?log_wait定義如下:
| DECLARE_WAIT_QUEUE_HEAD(log_wait);//等待隊列l(wèi)og_wait |
環(huán)形緩沖區(qū)__log_buf在使用之前就是已定義好的全局變量,緩沖區(qū)的長度為1 << CONFIG_LOG_ BUF_SHIFT。變量CONFIG_LOG_BUF_SHIFT在內(nèi)核編譯時由配置文件定義,對于i386平臺,其值定義如下(在?linux26/arch/i386/defconfig中):
| CONFIG_LOG_BUF_SHIFT=18 |
在內(nèi)核編譯時,編譯器根據(jù)配置文件的設(shè)置,產(chǎn)生如下的宏定義:
| #define CONFIG_LOG_BUF_SHIFT??18 |
環(huán)形緩沖區(qū)__log_buf定義如下(在linux26/kernel/printk.c中):
| #define __LOG_BUF_LEN(1 << CONFIG_LOG_BUF_SHIFT) //定義環(huán)形緩沖區(qū)的長度,i386平臺為 static char __log_buf[__LOG_BUF_LEN]; //printk的環(huán)形緩沖區(qū) static char *log_buf = __log_buf; static int log_buf_len = __LOG_BUF_LEN; /*互斥鎖logbuf_lock保護log_buf、log_start、log_end、con_start和logged_chars */ static DEFINE_SPINLOCK(logbuf_lock); |
通過宏定義LOG_BUF,緩沖區(qū)__log_buf具備了環(huán)形緩沖區(qū)的操作行為。宏定義LOG_BUF得到緩沖區(qū)指定位置序號的字符,位置序號超過緩沖區(qū)長度時,通過與長度掩碼LOG_BUF_MASK進行邏輯與操作,位置序號循環(huán)回到環(huán)形緩沖區(qū)中的位置。
宏定義LOG_BUF及位置序號掩碼LOG_BUF_MASK的定義列出如下:
| #define LOG_BUF_MASK (log_buf_len-1) #define LOG_BUF(idx) ?(log_buf[(idx) & LOG_BUF_MASK]) |
為了指明環(huán)形緩沖區(qū)__log_buf字符讀取位置,定義了下面的位置變量:
| static unsigned long log_start;/*系統(tǒng)調(diào)用syslog讀取的下一個字符*/ static unsigned long con_start;/*送到控制臺的下一個字符*/ static unsigned long log_end;/*最近已寫字符序號加1 */ static unsigned long logged_chars; /*自從上一次read+clear?操作以來產(chǎn)生的字符數(shù)*/ |
任何地方的內(nèi)核調(diào)用都可以調(diào)用函數(shù)printk打印調(diào)試、安全、提示和錯誤消息。函數(shù)printk嘗試得到控制臺信號量(console_sem),如果得到,就將信息輸出到環(huán)形緩沖區(qū)__log_buf中,然后函數(shù)release_console_sem()在釋放信號 量之前把環(huán)形緩沖區(qū)中的消息送到控制臺,調(diào)用控制臺驅(qū)動程序顯示打印的信息。如果沒得到信號量,就只將信息輸出到環(huán)形緩沖區(qū)后返回。函數(shù)printk的調(diào) 用層次如圖1所示。
圖1 函數(shù)printk的調(diào)用層次圖
函數(shù)printk列出如下(在linux26/kernel/printk.c中):
| asmlinkage int printk(const char *fmt, ...) { va_list args; int r; ???? va_start(args, fmt); r = vprintk(fmt, args); va_end(args); ? return r; } ? asmlinkage int vprintk(const char *fmt, va_list args) { unsigned long flags; int printed_len; char *p; static char printk_buf[1024]; static int log_level_unknown = 1; ? preempt_disable(); //關(guān)閉內(nèi)核搶占 if (unlikely(oops_in_progress) && printk_cpu == smp_processor_id()) /*如果在printk運行時,這個CPU發(fā)生崩潰, 確信不能死鎖,10秒1次初始化鎖logbuf_lock和console_sem,留時間 給控制臺打印完全的oops信息*/ zap_locks(); ? local_irq_save(flags);??//存儲本地中斷標(biāo)識 lockdep_off(); spin_lock(&logbuf_lock); printk_cpu = smp_processor_id(); ? /*將輸出信息發(fā)送到臨時緩沖區(qū)printk_buf */ printed_len = vscnprintf(printk_buf, sizeof(printk_buf), fmt, args); ? /*拷貝printk_buf數(shù)據(jù)到循環(huán)緩沖區(qū),如果調(diào)用者沒提供合適的日志級別,插入默認值*/ for (p = printk_buf; *p; p++) { if (log_level_unknown) { ?/* log_level_unknown signals the start of a new line */ if (printk_time) { int loglev_char; char tbuf[50], *tp; unsigned tlen; unsigned long long t; unsigned long nanosec_rem; ? /*在時間輸出之前強制輸出日志級別*/ if (p[0] == '<' && p[1] >='0' && p[1] <= '7' && p[2] == '>') { loglev_char = p[1]; //獲取日志級別字符 p += 3; printed_len -= 3; } else { loglev_char = default_message_loglevel + '0'; } t = printk_clock();//返回當(dāng)前時鐘,以ns為單位 nanosec_rem = do_div(t, 1000000000); tlen = sprintf(tbuf, "<%c>[%5lu.%06lu] ", loglev_char, (unsigned long)t, nanosec_rem/1000);//寫入格式化后的日志級別和時間 ? for (tp = tbuf; tp < tbuf + tlen; tp++) emit_log_char(*tp); ?//將日志級別和時間字符輸出到循環(huán)緩沖區(qū) printed_len += tlen; } else { if (p[0] != '<' || p[1] < '0' || ???p[1] > '7' || p[2] != '>') { emit_log_char('<'); emit_log_char(default_message_loglevel + '0'); ?//輸出字符到循環(huán)緩沖區(qū) emit_log_char('>'); printed_len += 3; } } log_level_unknown = 0; if (!*p) break; } emit_log_char(*p);//將其他printk_buf數(shù)據(jù)輸出到循環(huán)緩沖區(qū) if (*p == '/n') log_level_unknown = 1; } ? if (!down_trylock(&console_sem)) { /*擁有控制臺驅(qū)動程序,降低spinlock并讓release_console_sem()打印字符?*/ console_locked = 1; printk_cpu = UINT_MAX; spin_unlock(&logbuf_lock); ? /*如果CPU準(zhǔn)備好,控制臺就輸出字符。函數(shù)cpu_online檢測CPU是否在線, 函數(shù)have_callable_console()檢測是否 有注冊的控制臺啟動時就可以使用*/ if (cpu_online(smp_processor_id()) || have_callable_console()) { console_may_schedule = 0; release_console_sem(); } else { /*釋放鎖避免刷新緩沖區(qū)*/ console_locked = 0; up(&console_sem); } lockdep_on(); local_irq_restore(flags); //恢復(fù)本地中斷標(biāo)識 } else { /*如果其他進程擁有這個驅(qū)動程序,本線程降低spinlock, 允許信號量持有者運行并調(diào)用控制臺驅(qū)動程序輸出字符*/ printk_cpu = UINT_MAX; spin_unlock(&logbuf_lock); lockdep_on(); local_irq_restore(flags); //恢復(fù)本地中斷標(biāo)識 } ? preempt_enable();??//開啟搶占機制 return printed_len; } |
函數(shù)release_console_sem()給控制臺系統(tǒng)開鎖,釋放控制臺系統(tǒng)及驅(qū)動程序調(diào)用者持有的信號量。持有信號量時,表示printk?已在緩沖區(qū)存有數(shù)據(jù)。函數(shù)release_console_sem()在釋放信號量之前將這些數(shù)據(jù)送給控制臺顯示。如果后臺進程klogd在等待環(huán)形緩沖 區(qū)裝上數(shù)據(jù),它喚醒klogd進程。
函數(shù)release_console_sem列出如下(在linux26/kernel/printk.c中):
| void release_console_sem(void) { unsigned long flags; unsigned long _con_start, _log_end; unsigned long wake_klogd = 0; ? for ( ; ; ) { spin_lock_irqsave(&logbuf_lock, flags); wake_klogd |= log_start - log_end; if (con_start == log_end) break;/*?沒有需要打印的數(shù)據(jù)*/ _con_start = con_start; _log_end = log_end; con_start = log_end;/* Flush */ spin_unlock_irqrestore(&logbuf_lock, flags); ?????????//調(diào)用控制臺driver的write函數(shù)寫入到控制臺 call_console_drivers(_con_start, _log_end); } console_locked = 0; console_may_schedule = 0; up(&console_sem); spin_unlock_irqrestore(&logbuf_lock, flags); if (wake_klogd && !oops_in_progress && waitqueue_active(&log_wait)) wake_up_interruptible(&log_wait);//喚醒在等待隊列上的進程 } |
函數(shù)_call_console_drivers將緩沖區(qū)中從start到end - 1的數(shù)據(jù)輸出到控制臺進行顯示。在輸出數(shù)據(jù)到控制臺之前,它檢查消息的日志級別。只有日志級別小于控制臺日志級別console_loglevel的消 息,才能交給控制臺驅(qū)動程序進行顯示。
函數(shù)_call_console_drivers列出如下:
| static void _call_console_drivers(unsigned long start, unsigned long end, int msg_log_level) { //日志級別小于控制臺日志級別的消息才能輸出到控制臺 if ((msg_log_level < console_loglevel || ignore_loglevel) && console_drivers && start != end) { if ((start & LOG_BUF_MASK) > (end & LOG_BUF_MASK)) { /*?調(diào)用控制臺驅(qū)動程序的寫操作函數(shù)?*/ __call_console_drivers(start & LOG_BUF_MASK,?log_buf_len); __call_console_drivers(0, end & LOG_BUF_MASK); } else { __call_console_drivers(start, end); } } } |
函數(shù)__call_console_drivers調(diào)用控制臺驅(qū)動程序的寫操作函數(shù)顯示消息。其列出如下:
| static void __call_console_drivers(unsigned long start, unsigned long end) { struct console *con; ? for (con = console_drivers; con; con = con->next) { if ((con->flags & CON_ENABLED) && con->write && (cpu_online(smp_processor_id()) || (con->flags & CON_ANYTIME))) con->write(con, &LOG_BUF(start), end - start); //調(diào)用驅(qū)動程序的寫操作函數(shù) } } |
3? sys_syslog系統(tǒng)調(diào)用
系統(tǒng)調(diào)用sys_syslog根據(jù)參數(shù)type的命令執(zhí)行相應(yīng)的操作。參數(shù)type定義的命令列出如下:
0 --?關(guān)閉日志,當(dāng)前沒實現(xiàn)。
1 --?打開日志,當(dāng)前沒實現(xiàn)。
2 --?從環(huán)形緩沖區(qū)讀取日志消息。
3 --?讀取保留在環(huán)形緩沖區(qū)的所有消息。
4 --?讀取并清除保留在環(huán)形緩沖區(qū)的所有消息。
5 --?清除環(huán)形緩沖區(qū)。
6 --?關(guān)閉printk到控制臺的打印。
7 --?開啟printk到控制臺的打印。
8 --?設(shè)置打印到控制臺的消息的日志級別。
9 --?返回日志緩沖區(qū)中沒讀取的字符數(shù)。
10 --?返回日志緩沖區(qū)的大小。
sys_syslog函數(shù)列出如下(在linux26/kernel/printk.c中):
| asmlinkage long sys_syslog(int type, char __user * buf, int len) { return do_syslog(type, buf, len); } ? int do_syslog(int type, char __user *buf, int len) { unsigned long i, j, limit, count; int do_clear = 0; char c; int error = 0; ? error = security_syslog(type);??//檢查是否調(diào)用這個函數(shù)的權(quán)限 if (error) return error; ? switch (type) { case 0:/*?關(guān)閉日志?*/ break; case 1:/*?打開日志*/ break; case 2:/*讀取日志信息*/ error = -EINVAL; if (!buf || len < 0) goto out; error = 0; if (!len) goto out; if (!access_ok(VERIFY_WRITE, buf, len)) { //驗證是否有寫的權(quán)限 error = -EFAULT; goto out; } ???????????//當(dāng)log_start - log_end為0時,表示環(huán)形緩沖區(qū)無數(shù)據(jù)可讀,把當(dāng)前進程放入 ??????????????????等待隊列l(wèi)og_wait error = wait_event_interruptible(log_wait,?(log_start - log_end)); if (error) goto out; i = 0; spin_lock_irq(&logbuf_lock); while (!error && (log_start != log_end) && i < len) { c = LOG_BUF(log_start); //從環(huán)形緩沖區(qū)得到讀取位置log_start log_start++; spin_unlock_irq(&logbuf_lock); error = __put_user(c,buf); //將c地址的字符傳遞到用戶空間的buf中 buf++; i++; cond_resched();??//條件調(diào)度,讓其他進程有運行時間 spin_lock_irq(&logbuf_lock); } spin_unlock_irq(&logbuf_lock); if (!error) error = i; break; case 4:/*?讀/清除上一次內(nèi)核消息*/ do_clear = 1; /* FALL THRU */ case 3:/*讀取上一次內(nèi)核消息*/ error = -EINVAL; if (!buf || len < 0) goto out; error = 0; if (!len)??//讀取長度為0 goto out; if (!access_ok(VERIFY_WRITE, buf, len)) { //驗證有寫權(quán)限 error = -EFAULT; goto out; } count = len; if (count > log_buf_len) count = log_buf_len; spin_lock_irq(&logbuf_lock); if (count > logged_chars) // logged_chars是上次讀/清除以來產(chǎn)生的日志字符數(shù) count = logged_chars; if (do_clear) logged_chars = 0; limit = log_end; /* __put_user()?可以睡眠,當(dāng)__put_user睡眠時,printk()可能覆蓋寫正在 拷貝到用戶空間的消息,因此,這些消息被反方向拷貝,將buf覆蓋部分的數(shù)據(jù)重寫到buf的起始位置*/ for (i = 0; i < count && !error; i++) { //讀取count個字符 j = limit-1-i; if (j + log_buf_len < log_end) break; c = LOG_BUF(j); //從環(huán)形緩沖區(qū)得到讀取位置j spin_unlock_irq(&logbuf_lock); ??//將c位置的字符傳遞到用戶空間的buf中,如果發(fā)生錯誤,將發(fā)生錯誤的c位置給error error = __put_user(c,&buf[count-1-i]);? cond_resched(); spin_lock_irq(&logbuf_lock); } spin_unlock_irq(&logbuf_lock); ? if (error) break; error = i; if (i != count) { //表示__put_user沒有拷貝完成 int offset = count-error; /*?拷貝期間緩沖區(qū)溢出,糾正用戶空間緩沖區(qū)*/ for (i = 0; i < error; i++) { if (__get_user(c,&buf[i+offset]) || ????__put_user(c,&buf[i])) { //將覆蓋部分的數(shù)據(jù) ??????????????????????????????????????????????????????????????重寫到buf的起始位置 error = -EFAULT; break; } cond_resched(); } } break; case 5:/*?清除環(huán)形緩沖區(qū)*/ logged_chars = 0; break; case 6:/*關(guān)閉向控制臺輸出消息*/ console_loglevel = minimum_console_loglevel; break; case 7:/*開啟向控制臺輸出消息*/ console_loglevel = default_console_loglevel; break; case 8:/*?設(shè)置打印到控制臺的日志級別*/ error = -EINVAL; if (len < 1 || len > 8) goto out; if (len < minimum_console_loglevel) len = minimum_console_loglevel; console_loglevel = len; error = 0; break; case 9:/*?得到日志消息所占緩沖區(qū)的大小*/ error = log_end - log_start; break; case 10:/*返回環(huán)形緩沖區(qū)的大小*/ error = log_buf_len; break; default: error = -EINVAL; break; } out: return error; } |
?
為了更好的管理日志,你可通過修改/etc/syslog.conf來滿足你的需求。man syslog.conf獲得更多關(guān)于syslog.conf的信息。當(dāng)你在編寫內(nèi)核模塊時,應(yīng)該注意一個問題,你不能讓那些消息困擾你,因此定義一些宏來?開關(guān)printk消息是很有必要的,畢竟printk是用來調(diào)試,或者顯示設(shè)備、模塊的一些狀態(tài)。另外內(nèi)核同時也提供了一些接口,?在<linux/kernel.h>中定義,他們是一些內(nèi)聯(lián)函數(shù)。int printk_ratelimit(void)。在打印一條可能被重復(fù)的信息之前,應(yīng)調(diào)用該函數(shù)。如果它返回一個非零值,則可以繼續(xù)打印消息,否則跳過。?printk_ratelimit通過跟蹤發(fā)送到控制臺的消息量,開避免消息的重復(fù)輸出。可以通過修改/proc/sys/kernel /printk_ratelimit來設(shè)置重新打開消息應(yīng)該等待的秒數(shù)。
總結(jié)
以上是生活随笔為你收集整理的linux内核printk调试的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 详解网络摄像机中的IR-CUT
- 下一篇: linux mount挂载设备(u盘,光