函數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>" #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中):
#define DEFAULT_MESSAGE_LOGLEVEL 4
|
核心可把訊息列印到當前控制台上,可以指定控制台為字元模式的終端或印表機等。預設情況下,“控制台”就是當前的虛擬終端。
為了更好地控制不同層級的資訊顯示在控制台上,核心設定了控制台的記錄層級console_loglevel。printk記錄層級的作用是列印一定層級的訊息,與之類似,控制台只顯示一定層級的訊息。
當記錄層級小於console_loglevel時,訊息才能顯示出來。控制台相應的記錄層級定義如下:
#define MINIMUM_CONSOLE_LOGLEVEL 1 #define DEFAULT_CONSOLE_LOGLEVEL 7
int console_printk[4] = { DEFAULT_CONSOLE_LOGLEVEL,
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選項重新啟動它。
註:#ps -e 查看所有進程PID,然後KILL。
通過讀寫/proc/sys/kernel/printk檔案可讀取和修改控制台的記錄層級。查看這個檔案的方法如下:
#cat /proc/sys/kernel/printk 6 4 1 7 |
上面顯示的4個資料分別對應控制台記錄層級、預設的訊息記錄層級、最低的控制台記錄層級和預設的控制台記錄層級。
可用下面的命令設定當前記錄層級:
# echo 8 > /proc/sys/kernel/printk
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中):
在核心編譯時間,編譯器根據設定檔的設定,產生如下的宏定義:
#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;
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; static unsigned long con_start; static unsigned long log_end; static unsigned long logged_chars; |
任何地方的核心調用都可以調用函數printk列印調試、安全、提示和錯誤訊息。函數printk嘗試得到控制台訊號量(console_sem),如果得到,就將資訊輸出到環形緩衝區__log_buf中,然後函數release_console_sem()在釋放訊號量之前把環形緩衝區中的訊息送到控制台,調用控制台驅動程式顯示列印的資訊。如果沒得到訊號量,就只將資訊輸出到環形緩衝區後返回。函數printk的調用層次1所示。
函數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())
zap_locks();
local_irq_save(flags); //儲存本地中斷標識 lockdep_off(); spin_lock(&logbuf_lock); printk_cpu = smp_processor_id();
printed_len = vscnprintf(printk_buf, sizeof(printk_buf), fmt, args);
for (p = printk_buf; *p; p++) { if (log_level_unknown) {
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)) {
console_locked = 1; printk_cpu = UINT_MAX; spin_unlock(&logbuf_lock);
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 {
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; 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); //調用驅動程式的寫操作函數 } } |