printk 工作原理

========================================================
v0.1 3.4.2009 by arethe   Email: [email protected]
========================================================
    printk()的參數個數是可變的,linux內核中提供了va_arg機制。該機制主要通過3個宏來實現:
    va_arg(ap, T):獲取ap中的一個參數,該參數的類型是T,然後ap自加sizeof(T),跳過剛獲取的參數。
    va_end(ap):該宏定義爲空。
    va_start(ap, A):通過A獲取參數列表的地址,A是printk的第一個參數(fmt)。一個函數的參數,是按從右到左的順序逐個入棧的,因此通過A的地址加上A的大小(通常爲4Bytes),就可以獲得從第二個參數開始的參數列表的首地址。
    printk()函數首先使用va_start(args, fmt),將第二個參數的地址存入args變量。然後調用vprintk(fmt, args),vprintk完成具體的輸出任務,返回值爲輸出的字符的個數。
    下面我們詳細討論vprintk的實現。
    printk機制中有兩個主要的緩存:一個是printk_buf,一個是log_buf。前者用來存儲將要輸出的字符串,fmt+報錯信息(如果有的話)。後者用來存儲最終要輸出的字符串,是整個printk機制的核心。log_buf是一個環形數組,默認大小爲128K。配合這個緩衝使用的有3個變量:
    static unsigned log_start;      /* Index into log_buf: next char to be read by syslog() */
    static unsigned con_start;      /* Index into log_buf: next char to be sent to consoles */
    static unsigned log_end;        /* Index into log_buf: most-recently-written-char + 1 */
    在用到這個緩衝時,我們再詳細的介紹。
    vprintk首先會調用函數boot_delay_msec(),進行忙等待一段時間。這段時間的大小是由內核啓動參數boot_delay指定的。boot_delay的單位是毫秒。不知道這裏爲什麼要等待。
    然後調用preempt_disable()禁止搶佔機制。接下來是關中斷,獲取當前CPU的編號。
    如果在某個CPU上正在執行printk時,突然崩潰掉,........
    vscnprintf()函數,將輸出的字符串按fmt中的格式編排好,放入printk_buf中,並返回應該輸出的字符的個數。爲了保證完整性,我們還是來談談vscnprintf()函數的實現吧。
    vscnprintf()調用的是vsnprintf(buf,size,fmt,args)。該函數進行主要的格式化操作。其首先判斷size是否小於0,若小於0,則給出一個警告,並返回0。然後對格式化字符串fmt進行遍歷,如果fmt當前的字符不是"%",直接將其考入buf中,若是"%",則後面的處理要複雜一點。相信讀者對printf和printk的使用方法都很熟悉,"%"後面一般會跟一個標誌符,這種標誌符共有5個,'-','+','#','SPACE','0'。標誌符'-'表示後面的字符靠左輸出,比如printk("%-10c",'a'),會先輸出'a',再輸出9個空格。'#'標誌的作用是當後面輸出16進制的數據時,會自動在數據前加上"0x",比如printk("%#x",10),會輸出"0xa"。'+'標誌的作用是在輸出的數字前自動加上一個"+",比如printk("%+d/n",10),輸出結果爲:"+10"。這裏會根據不同的標誌符對一個標誌變量"flags"進行置位。各標誌符對應的bit如下:
    #define ZEROPAD 1               /* pad with zero -- '0'*/
    #define SIGN    2               /* unsigned/signed long --  */
    #define PLUS    4               /* show plus -- '+' */
    #define SPACE   8               /* space if plus -- ' ' */
    #define LEFT    16              /* left justified -- '-' */
    #define SMALL   32              /* Must be 32 == 0x20 */
    #define SPECIAL 64              /* 0x -- '#' */
    在標誌符的後面通常是輸出寬度,這是一個數字,vsnprintf()定義了一個變量來獲取這個值,首先,我們需要判斷緊接着標誌符後面的是不是數字,如果是則通過skip_atoi()將該數字字符串轉化成數字。skip_atoi()的實現很簡潔:
    static int skip_atoi(const char **s)
    {//change the string "s" to digit
            int i=0;
 
            while (isdigit(**s))
                    i = i*10 + *((*s)++) - '0';
            return i;
    }
    對於內核中的這類函數最好能記熟一點,在用的時候就不用再費時間自己實現了。在這裏有一點需要強調的是"%*",至少我以前並不知道"%*"的含義,"*"對應後面的一個參數,這個參數指定輸出的寬度。比如:printk("%*c",10,'a');會先輸出9個空格再輸出字符'a'。你甚至可以給一個負值,表示靠左輸出。獲取的輸出寬度保存在變量field_width中。
    接下來處理的是輸出精度,在格式化輸出浮點數時,我們經常採用".num"的形式來指定小數點後輸出幾位有效數字。在處理的時候,首先判度當前字符是不是'.',如果是,那麼讀入後面緊跟着的數字,保存在變量precision中。這裏也可以使用".*"的形式,將精度放到後面的參數中指定。
    接着是讀取限定符,如果有的話。什麼是限定符呢? 如果我們想輸出一個長整數,會用到"%ld",這裏的"l"便是限定符,用於輔助說明輸出數據的具體類型。printf或printk中用的限定符有:"h,l,L,Z,z,t"。另外"ll"相當於"L"。大家應該能猜到下一步應該做什麼了,沒錯,判斷輸出數據的類型。可能的類型有"c,s,p,n,%,o,X,x,d,i,u",這裏的實現都很簡單,如果大家有興趣,可以自己去看源代碼。
    有一個問題到現在一直沒有說明,就是如何從參數列表中獲得想要的參數。其實在文章剛開始的時候提到了一點,就是va_arg()宏,該宏每次根據指定的類型讀取一個參數,然後將參數列表的開始位置自動向前移一個。這樣,我們在分析"fmt"的格式的同時也就把對應的參數放到了輸出字符串(printk_buf)中合適的位置上。
    分析完"fmt"後,函數vsnprintf()返回應該輸出的字符個數。
    執行流有返回到了函數vprintk()中,我們接着來看。下面是一個for循環,用於將printk_buf中的輸出字符串拷貝到日誌緩存log_buf中。首先需要判度輸出的級別,我們知道在printk中,可以指定8個輸出級別,通過printk("<x>...")來指定。這裏的輸出級別被保存到變量current_log_level中。
    下面用到了一個函數emit_log_char():
    static void emit_log_char(char c)
    {// write c into log. the log buf is ring queue, the defaut is 128K.
            LOG_BUF(log_end) = c;
            log_end++;
            if (log_end - log_start > log_buf_len)
                    log_start = log_end - log_buf_len;
            if (log_end - con_start > log_buf_len)
                    con_start = log_end - log_buf_len;
              if (logged_chars < log_buf_len)
                logged_chars++;
    }
    這個函數的作用是把字符c寫道日誌緩存log_buf中,並更新log_start,log_end,con_start的值。
    接下來有個需要解釋一下的問題,就是printk_time。我們在啓動內核的時候可以通過指定一個內核啓動參數"time",來使所有printk出來的數據前加入當前時間。這個時間是從系統啓動到這個printk時所逝去的時間。
                        if (printk_time) {
                                /* Follow the token with the time */
                                char tbuf[50], *tp;
                                unsigned tlen;
                                unsigned long long t;
                                unsigned long nanosec_rem;
 
                                t = cpu_clock(printk_cpu);//the unit of t is
nanosecond
                                nanosec_rem = do_div(t, 1000000000);//Now, the
unit of t is second, the unit of nanosec_rem is nanosecond.
                                tlen = sprintf(tbuf, "[%5lu.%06lu] ",
                                                (unsigned long) t,
                                                nanosec_rem / 1000);
 
                                for (tp = tbuf; tp < tbuf + tlen; tp++)
                                        emit_log_char(*tp);
                                printed_len += tlen;
                        }
    函數cpu_clock()返回從系統啓動到當前的納秒值。do_div(a,b)是一個宏,它計算a/b,將商放在a中,返回餘數。那麼tbuf中的數據便是"[second.nanosecond]"形式的。有興趣的話,大家可以在內核啓動時加入time參數試一試,看看會有什麼效果。
    在for循環結束後,printk_buf中的數據便按照新的輸出格式(比如在每一行前面加入print_time)copy到了log_buf中。
    下面便是具體的輸出工作了,在輸出之前,需要先獲取信號量console_sem,這裏獲取的方式採用的是down_trylock(&console_sem)。這個函數在沒有獲取信號量時不會睡眠,而是立即返回一個非0值。另外,我們還需要釋放鎖console_locked和logbuf_lock。這些工作在函數acquire_console_semaphore_for_printk(this_cpu)中完成,當該函數成功返回時,會調用函數release_console_sem()進行顯示工作。這個函數會再調用call_console_drivers(_con_start, _log_end)將_con_start,_log_end之間的log_buf中的內容。函數call_console_drivers()首先判斷輸出級別,然後根據輸出內容,按行調用函數_call_console_drivers(start_print, cur_index, msg_level),該函數的作用是處理環形隊列,將正確的內容作爲參數調用函數__call_console_drivers(start, end),該將輸出內容發送給console的驅動。在內核中,所有的console都被鏈入了一個鏈表--console_drivers,在這裏,我們要遍歷這個鏈表,如果某個console允許輸出的話,就調用它的write()方法。
/*
 * Call the console drivers on a range of log_buf
 */
static void __call_console_drivers(unsigned start, unsigned end)
{
        struct console *con;
 
        for (con = console_drivers; con; con = con->next) {//All consoles belongs to a list -- console_drivers.
                if ((con->flags & CON_ENABLED) && con->write &&
                                (cpu_online(smp_processor_id()) ||
                                (con->flags & CON_ANYTIME)))
                        con->write(con, &LOG_BUF(start), end - start);
        }
}
    到此爲止,整個printk的過程就結束了。哦,不!還有一個工作沒有做,就是將輸出內容同時寫入日誌。這個工作是由內核中的守護進程klogd來完成的。在函數release_console_sem(void)的最後,會判斷是否需要喚醒klogd,然後調用wake_up_klogd()來喚醒守護進程。那麼什麼時候需要喚醒klogd呢?只要log_buf中有未輸出的信息,便需喚醒klogd將其寫入日誌文件。
    OK!現在是真的結束了。如果有什麼問題,可以跟我聯繫,共同討論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章