歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux內核 >> 基於FS4412嵌入式系統移植(8) linux內核調試之printk

基於FS4412嵌入式系統移植(8) linux內核調試之printk

日期:2017/3/1 11:49:46   编辑:Linux內核

以下內容主要摘錄自《Linux安全體系分析與編程》

1、基本原理

(1)在UBOOT裡設置console=ttySAC0或者console=tty1
這裡是設置控制終端,tySAC0 表示串口, tty1 表示lcd
(2)內核用printk打印

內核就會根據命令行參數來找到對應的硬件操作函數,並將信息通過對應的硬件終端打印出來!

2、printk及控制台的日志級別
函數printk的使用方法和printf相似,用於內核打印消息。printk根據日志級別(loglevel)對消息進行分類。
相似的在android中有使用Log函數進行調試信息的打印。
日志級別用宏定義,日志級別宏展開為一個字符串,在編譯時由預處理器將它和消息文本拼接成一個字符串,因此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的日志級別定義如下(在linux2.6和3.14/include/linux/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,其定義列出如下(在linux2.6/kernel/printk.c中,在linux3.14/kernel/printk/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
7   4  1   7

上面顯示的4個數據分別對應控制台日志級別、默認的消息日志級別、最低的控制台日志級別和默認的控制台日志級別。

可用下面的命令設置當前日志級別:

# echo "4 4 1 7" > /proc/sys/kernel/printk
這裡有個問題需要說一下,在一些開發板提供的內核源碼printk的默認打印級別都是未經修改的,例如飛凌的開發板提供的內核,在實際使用時有一些驅動會輸出一些調試信息,在實際使用時這些信息會影響正常的用戶使用,比如飛凌開發板連接網線後串口終端會定時輸出以下內容:

eth0: link down 
eth0: link up, 100Mbps, full-duplex, lpa 0x4DE1
以上信息並不是錯誤,只是在打印調試信息,說明網絡正常連接,但這樣會干擾串口輸入,所以在編譯內核時,修改printk.c中的調試級別如下即可解決這個問題:

int console_printk[4] = {
DEFAULT_MESSAGE_LOGLEVEL,/*控制台日志級別,優先級高於該值的消息將在控制台顯示*/
/*默認消息日志級別,printk沒定義優先級時,打印這個優先級以上的消息*/
DEFAULT_MESSAGE_LOGLEVEL,/*最小控制台日志級別,控制台日志級別可被設置的最小值(最高優先級)*/
MINIMUM_CONSOLE_LOGLEVEL,
DEFAULT_CONSOLE_LOGLEVEL,/* 默認的控制台日志級別*/
};

3、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平台,其值定義如下(在 linux2.6/arch/i386/defconfig中):

CONFIG_LOG_BUF_SHIFT=18

在內核編譯時,編譯器根據配置文件的設置,產生如下的宏定義:
#define CONFIG_LOG_BUF_SHIFT 18

環形緩沖區__log_buf定義如下(在linux2.6/kernel/printk.c中,在linux3.14/kernel/printk/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列出如下(在linux2.6/kernel/printk.c中,在linux3.14/kernel/printk/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列出如下(在linux2.6/kernel/printk.c中,在linux3.14/kernel/printk/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); //調用驅動程序的寫操作函數
        
    }
    
}
4、sys_syslog系統調用

系統調用sys_syslog根據參數type的命令執行相應的操作。參數type定義的命令列出如下:

0 -- 關閉日志,當前沒實現。
1 -- 打開日志,當前沒實現。
2 -- 從環形緩沖區讀取日志消息。
3 -- 讀取保留在環形緩沖區的所有消息。
4 -- 讀取並清除保留在環形緩沖區的所有消息。
5 -- 清除環形緩沖區。
6 -- 關閉printk到控制台的打印。
7 -- 開啟printk到控制台的打印。
8 -- 設置打印到控制台的消息的日志級別。
9 -- 返回日志緩沖區中沒讀取的字符數。
10 -- 返回日志緩沖區的大小。

sys_syslog函數列出如下(在linux2.6/kernel/printk.c中,在linux3.14/kernel/printk/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;
    
}




Copyright © Linux教程網 All Rights Reserved