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