1. 程式人生 > >驅動除錯(一)-printk

驅動除錯(一)-printk

目錄


title: 驅動除錯(一)-printk
date: 2019/1/9 19:35:14
toc: true
---

驅動除錯(一)-printk

引入

uboot的啟動引數中定義了我們核心啟動時的資訊輸出

bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0

如果去除console=ttySAC0,則核心複製後沒有資訊輸出,可以看下lcd,已經有顯示了

#OpenJTAG> set bootargs noinitrd root=/dev/mtdblock3 init=/linuxrc

Starting kernel ...
Uncompressing Linux.............................................. done, booting the kernel.

也可以設定為tty1,直接在LCD上輸出,這個需要有lcd驅動程式了(廢話 哈哈),這裡我試了tty0和tty1 和tty2,tty3都是在lcd顯示

set  bootargs noinitrd root=/dev/mtdblock3 init=/linuxrc console=tty1

其實也可以使用多個終端輸出,比如這樣

set  bootargs noinitrd root=/dev/mtdblock3 init=/linuxrc console=tty1 console=ttySAC0

注意 這裡設定引數後不要使用save 儲存到flash,直接退回到menu,輸入b啟動即可

那麼核心的printk

是怎麼根據console=xxx找到輸出的硬體裝置的?

框架

入口console_setup

搜尋console=,在有以下程式碼__setup("console=", console_setup);,這個巨集是用來處理啟動引數的

檔案在kernel\printk.c

/*
 * Set up a list of consoles.  Called from init/main.c
 */
static int __init console_setup(char *str)
{  
    char name[sizeof(console_cmdline[0].name)];
    char *s, *options;
    int idx;

    
    /*
     * Decode str into name, index, options.
     */
    
    // 先複製8位元組到name
    if (str[0] >= '0' && str[0] <= '9') {
        strcpy(name, "ttyS");
        strncpy(name + 4, str, sizeof(name) - 5);
    } else {
        strncpy(name, str, sizeof(name) - 1);
    }
    name[sizeof(name) - 1] = 0;
    
    
    // 判斷是否有"," 也就是是不是有選項位元組
    if ((options = strchr(str, ',')) != NULL)
        *(options++) = 0;
#ifdef __sparc__
    if (!strcmp(str, "ttya"))
        strcpy(name, "ttyS0");
    if (!strcmp(str, "ttyb"))
        strcpy(name, "ttyS1");
#endif
    
    //從name中 找數字
    for (s = name; *s; s++)
        if ((*s >= '0' && *s <= '9') || *s == ',')
            break;
    idx = simple_strtoul(s, NULL, 10);
    *s = 0;

    // 這裡就會新增控制檯了,也就是記錄下來,還沒有找到硬體
    add_preferred_console(name, idx, options);
    return 1;
}
__setup("console=", console_setup);

add_preferred_console

這裡是將命令列引數解析後存入全絕的結構體變數console_cmdline,這裡只是存起來,並沒有去解析

add_preferred_console
{
    struct console_cmdline *c;
    
    // 這裡有個全域性變數 console_cmdline,儲存所有的終端,這裡支援8個
    //#define MAX_CMDLINECONSOLES 8
    //static struct console_cmdline console_cmdline[MAX_CMDLINECONSOLES];
    
    //step1 判斷是否存在了已經
    for(i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++)
        ...
    
    //指向最後一個命令列的引數console_cmdline
    selected_console = i;   

    //step2 存入這個全域性的陣列 包括name,序號,選項
    c = &console_cmdline[i];
    memcpy(c->name, name, sizeof(c->name));
    c->name[sizeof(c->name) - 1] = 0;
    c->options = options;
    c->index = idx;
    
}

register_console

繼續搜尋這個全域性變數,可以看到註冊函式,匹配命令列的name和註冊的驅動後加入到連結串列中

selected_console 在add_preferred_console 處理命令引數的時候 指向最後一個命令列的引數console_cmdline
register_console   
    // 如果沒有註冊過console,preferred_console 指向selected_console 也就是最後一個命令列引數的console
    if (preferred_console < 0 || bootconsole || !console_drivers)
        preferred_console = selected_console;
   // 如果沒有註冊過console,會先來一個初始化這第一個來註冊的console
    if (preferred_console < 0)
        console->setup(console, NULL)
        console->index = 0; //沒有註冊時,強制賦值0
    for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0];i++)
        ...
        // 1. 比較命令列的名字與註冊的驅動的名字,如果name匹配
        // 2. 執行帶有option的 console->setup(console, console_cmdline[i].options)
        // 3. 如果成功,設定標誌
            console->flags |= CON_ENABLED;
            console->index = console_cmdline[i].index;
        //4. 選擇一個作為preferred_console,如果匹配到最後一個命令列,preferred_console就等於這個selected_console=最後一個命令列
            if (i == selected_console) {
            console->flags |= CON_CONSDEV;
            preferred_console = selected_console;
       // 5.加入到連結串列 console_drivers ,註冊的console本身也包含了一個連結串列指向
            if ((console->flags & CON_CONSDEV) || console_drivers == NULL) {
            console->next = console_drivers;
            console_drivers = console;
            if (console->next)
                console->next->flags &= ~CON_CONSDEV;
            } else {
                console->next = console_drivers->next;
                console_drivers->next = console;
            }    

這裡的連結串列結構應該是如下這樣的:

mark

s3c24xx_serial_initconsole

搜尋這個註冊函式的呼叫,發現s3c24xx_serial_initconsole使用了這個註冊函式,可以看到s3c24xx_serial_console的name正是"ttySAC"

s3c24xx_serial_initconsole
    // 驅動相關,先看看是不是有硬體驅動
    struct platform_device *dev = s3c24xx_uart_devs[0];
    //註冊console
    register_console(&s3c24xx_serial_console);

static struct console s3c24xx_serial_console =
{
    .name       = S3C24XX_SERIAL_NAME,
    .device     = uart_console_device,
    .flags      = CON_PRINTBUFFER,
    .index      = -1,
    .write      = s3c24xx_serial_console_write,
    .setup      = s3c24xx_serial_console_setup
};

#define S3C24XX_SERIAL_NAME "ttySAC"
#define S3C24XX_SERIAL_MAJOR    204
#define S3C24XX_SERIAL_MINOR    64

write

可以在這個結構體裡面發現write函式操作了實際的硬體,也就是writeprintk是實際的寫硬體函式

s3c24xx_serial_console_write
    >uart_console_write(cons_uart, s, count, s3c24xx_serial_console_putchar);
        >wr_regb(cons_uart, S3C2410_UTXH, ch);

printk

asmlinkage int printk(const char *fmt, ...)
{
    va_start(args, fmt);
    r = vprintk(fmt, args);
    va_end(args);
}

vprintk

這個函式最後會查詢console_drivers這個連結串列來進行列印處理,通過msg_level判斷是否輸出到硬體

vprintk(const char *fmt, va_list args)
{
    // 解析資料到一個buf
    /* Emit the output into the temporary buffer */
    printed_len = vscnprintf(printk_buf, sizeof(printk_buf), fmt, args);

    //對buf 進行特殊處理printk_buf,填充到 log_buf
    for (p = printk_buf; *p; p++)
    {
        //如果沒有 形如 <數字> 的開頭,自動補上 <DEFAULT_CONSOLE_LOGLEVEL>也就是<4>,提取這個lev
        //如果有,同樣提取這個lev
        emit_log_char(c)
    }

    if (cpu_online(smp_processor_id()) || have_callable_console()) {
                                            //have_callable_console 遍歷 console_drivers
                                            // > for (con = console_drivers; con; con = con->next)
                                            // 這個連結串列就是register_console 中註冊的了
        console_may_schedule = 0;
        // 列印輸出
        release_console_sem();
        {
            ....
        }
    }
}

release_console_sem

先將資料輸出到LOG_BUF,實際的輸出到硬體會去判斷一個列印級別,不論是否到達列印級別,都可以使用dmesg顯示這個log_buf[]

release_console_sem()
{
    // 靜態全域性變數
    _con_start = con_start;
    _log_end = log_end;
    call_console_drivers(_con_start, _log_end);
    {
        // 提取列印等級
        msg_level = LOG_BUF(cur_index + 1) - '0';
        _call_console_drivers(start_print, cur_index, msg_level);
        {
            // lev < 設定的log lev,則列印
            if ((msg_log_level < console_loglevel || ignore_loglevel) && console_drivers && start != end)
            {
                //遍歷console驅動連結串列,判斷是否有write函式,如果有,執行write函式
                __call_console_drivers(start, end);
                {
                    for (con = console_drivers; con; con = con->next)
                    {
                        if ((con->flags & CON_ENABLED) && con->write...)
                            con->write(con, &LOG_BUF(start), end - start);
                    }
                }
            }
                        
        }
    }
}

列印級別

我們在裡面使用的是_call_console_drivers中判斷if msg_log_level < console_loglevel,也就是說預設的級別就是console_loglevel,也就是預設小於<7>才打印

#define console_loglevel (console_printk[0]) ==7

可以使用cat /proc/sys/kernel/printk檢視是不是這個,這個值就是陣列console_printk[4]

# cat /proc/sys/kernel/printk
7       4       1       7
  • 第一個引數 7表示小於7優先順序訊息才會被輸出到控制檯
  • 第二個引數4 表示預設的printk訊息優先級別,即printk(“hell world”);優先順序為4, 由於4<7,故可以被列印到控制檯。
  • 第三個引數1 表示可接收的最高優先順序,當printk disable控制檯輸出時,設定第一個引數為1,但是,從核心等級來看,還有優先順序0,這個是printk最高階優先順序,一般用於核心嚴重訊息列印。比如記憶體錯誤或者 watchdog reset.也可以設定第一個和第三個引數為0
  • 第四個引數7 預設控制檯優先順序,即第一個引數的預設優先順序。

具體相關的定義在這裡,可以使用include\linux\kernel.h檢視

int console_printk[4] = {
    //=7
    DEFAULT_CONSOLE_LOGLEVEL,   /* console_loglevel */
    //=4
    DEFAULT_MESSAGE_LOGLEVEL,   /* default_message_loglevel */
    //=1
    MINIMUM_CONSOLE_LOGLEVEL,   /* minimum_console_loglevel */
    //=7
    DEFAULT_CONSOLE_LOGLEVEL,   /* default_console_loglevel */
};

/* printk's without a loglevel use this.. */
#define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING */
/* We show everything that is MORE important than this.. */
#define MINIMUM_CONSOLE_LOGLEVEL 1 /* Minimum loglevel we let people use */
#define DEFAULT_CONSOLE_LOGLEVEL 7 /* anything MORE serious than KERN_DEBUG */


#define    KERN_EMERG     "<0>"        // 系統崩潰
#define    KERN_ALERT     "<1>"      //必須緊急處理
#define    KERN_CRIT     "<2>"       // 臨界條件,嚴重的硬軟體錯誤
#define    KERN_ERR       "<3>"       // 報告錯誤
#define    KERN_WARNING   "<4>"       //警告
#define    KERN_NOTICE    "<5>"      //普通但還是須注意
#define    KERN_INFO      "<6>"      // 資訊
#define    KERN_DEBUG     "<7>"     // 除錯資訊

使用printk

格式可以加上列印級別,形式如下:

printk(KERN_EMERG "abc")  ===  printk( "<0>abc");

修改列印級別

  1. 臨時修改/proc/sys/kernel/printk,重啟後失效,下述命令關閉列印,也就是設定小於DEFAULT_CONSOLE_LOGLEVEL=1才打印

    echo "1 4 1 7" > /proc/sys/kernel/printk
  2. 修改初始化的陣列或者是那個判斷的函式

    int console_printk[4] = {
        1,//DEFAULT_CONSOLE_LOGLEVEL,   /* console_loglevel */
        DEFAULT_MESSAGE_LOGLEVEL,   /* default_message_loglevel */
        MINIMUM_CONSOLE_LOGLEVEL,   /* minimum_console_loglevel */
        DEFAULT_CONSOLE_LOGLEVEL,   /* default_console_loglevel */
    };

    或者修改 核心原始碼

    static void _call_console_drivers(unsigned long start,
                    unsigned long end, int msg_log_level)
    {
        //if ((msg_log_level < console_loglevel || ignore_loglevel) &&
        if ((msg_log_level < 1 || ignore_loglevel) &&
                console_drivers && start != end) {
            if ((start & LOG_BUF_MASK) > (end & LOG_BUF_MASK)) {
                /* wrapped write */
                __call_console_drivers(start & LOG_BUF_MASK,
                            log_buf_len);
                __call_console_drivers(0, end & LOG_BUF_MASK);
            } else {
                __call_console_drivers(start, end);
            }
        }
    }
  3. 設定uboot傳遞的引數,我們可以看到級別定義如下,搜尋文字console_loglevel,可以找到一些函式

    #define console_loglevel (console_printk[0])
    //init\main.c
    static int __init debug_kernel(char *str)
    {
        if (*str)
            return 0;
        console_loglevel = 10;
        return 1;
    }
    static int __init quiet_kernel(char *str)
    {
        if (*str)
            return 0;
        console_loglevel = 4;
        return 1;
    }
    __setup("debug", debug_kernel);
    __setup("quiet", quiet_kernel);
    static int __init loglevel(char *str)
    {
        get_option(&str, &console_loglevel);
        return 1;
    }
    __setup("loglevel=", loglevel);

    也就是說可以用這些引數傳遞列印級別

    loglevel=0 console=ttySA0,115200
    debug # 使用級別10
    quiet # 使用級別4
    
    
    set bootargs noinitrd root=/dev/mtdblock3 init=/linuxrc loglevel=0 console=ttySAC0
    boot
    # cat /proc/sys/kernel/printk
    0       4       1       7
    
    set bootargs noinitrd root=/dev/mtdblock3 init=/linuxrc debug console=ttySAC0
    boot
    # cat /proc/sys/kernel/printk
    10      4       1       7
    
    set bootargs noinitrd root=/dev/mtdblock3 init=/linuxrc quiet console=ttySAC0
    boot
    # cat /proc/sys/kernel/printk
    4       4       1       7

使用dmesg列印所有日誌

使用這個命令可以列印那些被遮蔽的緩衝,可以儲存到文本里面去看

# dmesg 
....▒...................................... done, booting the kernel.
Linux version 2.6.22.6 ([email protected]) (gcc version 3.4.5) #3 Wed Jan 9 15:33:52 CST 2019
CPU: ARM920T [41129200] revision 0 (ARMv4T), cr=c0007177

測試

修改級別為0,都可以實現如下效果,不列印核心啟動資訊

Starting kernel ...

Uncompressing Linux...................................................................................................................... done, booting the kernel.
init started: BusyBox v1.7.0 (2018-11-13 23:35:45 CST)
starting pid 766, tty '': '/etc/init.d/rcS'

Please press Enter to activate this console.
starting pid 771, tty '/dev/console': 'bin/sh'

也可以# cat /proc/sys/kernel/printk 來檢視,除了那個修改函式中的if判斷的,都可以打印出來如下效果

# cat /proc/sys/kernel/printk
1       4       1       7

修改if判斷的依然是7 4 1 7,因為這個檔案本質上就是這個陣列顯示 哈哈

小結

  1. 核心解析uboot傳遞的命令列引數,來尋找實際的硬體來輸出資訊

    console_setup
        >add_preferred_console
  2. 註冊實際的硬體驅動,加入到console_drivers連結串列

    s3c24xx_serial_initconsole
        >register_console
            >比較命令列的name 與 硬體驅動的name ,如果匹配,加入到 console_drivers連結串列中
  3. 使用printk( lev "...")來輸出資訊,如果沒有指定lev,以預設的lev=4輸出,具體是在console_drivers中尋找驅動找到他的write函式輸出

    printk
        >vprintk
            >release_console_sem
                >判斷lev 列印
  4. 使用dmesg可以列印所有日誌,包括被遮蔽的

附錄(3.4核心的分析)

這個程式碼講的比較具體,先不去仔細分析了

http://blog.chinaunix.net/uid-27717694-id-3495612.html

console驅動:
一、基本概念
終端是一種字元型裝置,通常使用tty簡稱各種型別的終端。linux的終端型別:
/dev/ttySn,序列口終端
 
/dev/pty,偽終端
 
/dev/tty,當前程序的控制終端,可以是介紹的其它任何一種終端
 
/dev/ttyn,tty1~tty6是虛擬終端,tty0當前虛擬終端的別名。
 
/dev/console,控制檯終端(顯示器)
 
二、uboot傳引數的處理
linux啟動時uboot傳遞進console=ttyS2,115200n8的引數
核心中用__setup()巨集宣告引數處理的方法:__setup("console=", console_setup);  
1.console_cmdline結構體
struct console_cmdline  
{  
    char name[8];    //驅動名   
    int  index;      //次裝置號   
    char *options;   //選項   
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE   
    char    *brl_options;     
#endif   
};  
 
2.核心呼叫console_setup()函式處理uboot傳進的console引數
static int __init console_setup(char *str)  
{  
    char buf[sizeof(console_cmdline[0].name) + 4]; //分配驅動名+index的緩衝區,分配12個位元組  
    char *s, *options, *brl_options = NULL;  
    int idx;  
   
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE   
    if (!memcmp(str, "brl,", 4)) {  
        brl_options = "";  
        str += 4;  
    } else if (!memcmp(str, "brl=", 4)) {  
        brl_options = str + 4;  
        str = strchr(brl_options, ',');  
        if (!str) {  
            printk(KERN_ERR "need port name after brl=\n");  
            return 1;  
        }  
        *(str++) = 0;  
    }  
#endif   
 
    if (str[0] >= '0' && str[0] <= '9') { //第一個引數屬於[0,9]   
        strcpy(buf, "ttyS");    //則將其驅動名設為ttyS   
        strncpy(buf + 4, str, sizeof(buf) - 5);//將次裝置號放其後面   
    } else {  
        strncpy(buf, str, sizeof(buf) - 1); //否則直接將驅動名+裝置號拷貝到buf中
    }  
    buf[sizeof(buf) - 1] = 0;  
    if ((options = strchr(str, ',')) != NULL) //獲取options,即“115200n8”   
        *(options++) = 0;  
         
#ifdef __sparc__   
    if (!strcmp(str, "ttya"))  
        strcpy(buf, "ttyS0");  
    if (!strcmp(str, "ttyb"))  
        strcpy(buf, "ttyS1");  
#endif  
  
    for (s = buf; *s; s++)  
        if ((*s >= '0' && *s <= '9') || *s == ',')//移動指標s到次裝置號處  
            break;  
    idx = simple_strtoul(s, NULL, 10); //獲取次裝置號,字串轉換成unsigend long long型資料,s表示字串的開始,NULL表示字串的結束,10表示進位制 
                                                                        //這裡返回的是次裝置號=2
    *s = 0;  
   
    __add_preferred_console(buf, idx, options, brl_options);  
    console_set_on_cmdline = 1;  
    return 1;  
}  
 
3.__add_preferred_console()函式
//整體的作用是根據uboot傳遞的引數設定全域性console_cmdline陣列
//該陣列及全域性selected_console,在register_console中會使用到
static int __add_preferred_console(char *name, int idx, char *options,char *brl_options)  
{  
    struct console_cmdline *c;  
    int i;  
    for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++)//可以最多8個console   
        if (strcmp(console_cmdline[i].name, name) == 0 && console_cmdline[i].index == idx) {  
            //比較已註冊的console_cmdline陣列中的項的名字及次裝置號,若console_cmdline已經存在   
                if (!brl_options)  
                    selected_console = i;//設定全域性selected_console索引號   
                return 0;//則返回   
        }  
      
    if (i == MAX_CMDLINECONSOLES)//判斷console_cmdline陣列是否滿了   
        return -E2BIG;  
    if (!brl_options)  
        selected_console = i; //設定全域性selected_console索引號   
     
    c = &console_cmdline[i];//獲取全域性console_cmdline陣列的第i項地址   
    strlcpy(c->name, name, sizeof(c->name));  //填充全域性console_cmdline的驅動名“ttyS2”   
    c->options = options;    //填充配置選項115200n8   
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE   
    c->brl_options = brl_options;  
#endif   
    c->index = idx;  //填充索引號2,即次裝置號   
    return 0;  
}  
 
三、在console初始化之前能使用printk,使用核心提供的early printk支援。
//在呼叫console_init之前呼叫printk也能打印出資訊,這是為什麼呢?在start_kernel函式中很早就呼叫了 parse_early_param函式,
//該函式會呼叫到連結指令碼中.init.setup段的函式。其中就有 setup_early_serial8250_console函式。
//該函式通過 register_console(&early_serial8250_console);
//註冊了一個比較簡單的串列埠裝置。可以用來列印核心啟 動早期的資訊。
 
//對於early printk的console註冊往往通過核心的early_param完成。
early_param(“earlycon”,setup_early_serial8250_console);
//定義一個earlycon的核心引數,核心解析這個引數時呼叫setup_early_serial8250_console()函式
 
1.setup_early_serial8250_console()函式
//earlycon = uart8250,mmio,0xff5e0000,115200n8
int __init setup_early_serial8250_console(char *cmdline)
{
    char *options;
    int err;
 
    options = strstr(cmdline, "uart8250,");//找到“uart8250,”字串,返回此字串的起始位置
    if (!options) {
        options = strstr(cmdline, "uart,");
        if (!options)
            return 0;
    }
 
    options = strchr(cmdline, ',') + 1;//options指標指向第一個逗號後邊的字串地址
    err = early_serial8250_setup(options);//進行配置
    if (err < 0)
        return err;
     
    /*
    static struct console early_serial8250_console __initdata = {
        .name   = "uart",
        .write  = early_serial8250_write,
        .flags  = CON_PRINTBUFFER | CON_BOOT,//所用具有CON_BOOT屬性的console都會在核心初始化到late initcall階段被登出,相互消他們的函式是
        .index  = -1,
    };
    */
    //註冊一個早期的console,到真正的console_init時,此console會被登出,因為設定了CON_BOOT標誌
    register_console(&early_serial8250_console);
 
    return 0;
}
 
static int __init early_serial8250_setup(char *options)
{
    struct early_serial8250_device *device = &early_device;
    int err;
 
    if (device->port.membase || device->port.iobase)//early_device裝置的埠地址若配置過則返回
        return 0;
 
    err = parse_options(device, options);//解析引數並配置early_device裝置對應的uart_port結構
    if (err < 0)
        return err;
 
    init_port(device);//early_device裝置對應的初始化uart_port結構
    return 0;
}
 
static int __init parse_options(struct early_serial8250_device *device,char *options)
{
    struct uart_port *port = &device->port;//找到early_device裝置對應的uart_port結構
    int mmio, mmio32, length;
 
    if (!options)
        return -ENODEV;
 
    port->uartclk = BASE_BAUD * 16;//串列埠時鐘
 
    mmio = !strncmp(options, "mmio,", 5);//查詢"mmio,"字串,找到mmio=1
    mmio32 = !strncmp(options, "mmio32,", 7);//mmio32=0
    if (mmio || mmio32) {
        port->iotype = (mmio ? UPIO_MEM : UPIO_MEM32);//串列埠型別設為UPIO_MEM=2
        port->mapbase = simple_strtoul(options + (mmio ? 5 : 7),&options, 0);//獲得串列埠的配置暫存器基礎地址(實體地址),這裡是得到0xff5e0000
        if (mmio32)
            port->regshift = 2;
#ifdef CONFIG_FIX_EARLYCON_MEM
        set_fixmap_nocache(FIX_EARLYCON_MEM_BASE,port->mapbase & PAGE_MASK);
        port->membase =(void __iomem *)__fix_to_virt(FIX_EARLYCON_MEM_BASE);
        port->membase += port->mapbase & ~PAGE_MASK;
#else
        port->membase = ioremap_nocache(port->mapbase, 64);//對映到記憶體的配置暫存器基礎地址
        if (!port->membase) {
            printk(KERN_ERR "%s: Couldn't ioremap 0x%llx\n",    __func__,(unsigned long long) port->mapbase);
            return -ENOMEM;
        }
#endif
    } else if (!strncmp(options, "io,", 3)) {
        port->iotype = UPIO_PORT;
        port->iobase = simple_strtoul(options + 3, &options, 0);
        mmio = 0;
    } else
        return -EINVAL;
 
    options = strchr(options, ',');//指標移到“115200n8”字串處
    if (options) {//存在
        options++;
        device->baud = simple_strtoul(options, NULL, 0);//取得波特率115200
        length = min(strcspn(options, " "), sizeof(device->options));
        strncpy(device->options, options, length);//將字串115200n8拷貝到裝置的device->options欄位中
    } else {
        device->baud = probe_baud(port);
        snprintf(device->options, sizeof(device->options), "%u",device->baud);
    }
 
    if (mmio || mmio32)
        printk(KERN_INFO "Early serial console at MMIO%s 0x%llx (options '%s')\n",mmio32 ? "32" : "",(unsigned long long)port->mapbase,device->options);
    else
        printk(KERN_INFO
              "Early serial console at I/O port 0x%lx (options '%s')\n",port->iobase,device->options);
 
    return 0;
}
 
static void __init init_port(struct early_serial8250_device *device)
{
    struct uart_port *port = &device->port;
    unsigned int divisor;
    unsigned char c;
 
    serial_out(port, UART_LCR, 0x3);    /* 8n1 */
    serial_out(port, UART_IER, 0);      /* no interrupt */
    serial_out(port, UART_FCR, 0);      /* no fifo */
    serial_out(port, UART_MCR, 0x3);    /* DTR + RTS */
 
    divisor = port->uartclk / (16 * device->baud);//根據波特率設定分頻
    c = serial_in(port, UART_LCR);
    serial_out(port, UART_LCR, c | UART_LCR_DLAB);
    serial_out(port, UART_DLL, divisor & 0xff);
    serial_out(port, UART_DLM, (divisor >> 8) & 0xff);
    serial_out(port, UART_LCR, c & ~UART_LCR_DLAB);
}
 
void register_console(struct console *newcon)
{
    int i;
    unsigned long flags;
    struct console *bcon = NULL;
    /*
    現在是註冊一個early console,即
    static struct console early_serial8250_console __initdata = {
        .name   = "uart",
        .write  = early_serial8250_write,
        .flags  = CON_PRINTBUFFER | CON_BOOT,//所用具有CON_BOOT屬性的console都會在核心初始化到late initcall階段被登出,相互消他們的函式是
        .index  = -1,
    };
    */
    if (console_drivers && newcon->flags & CON_BOOT) {//註冊的是否是引導控制檯。early console的CON_BOOT置位,表示只是一個引導控制檯,以後會被登出
        for_each_console(bcon) {////遍歷全域性console_drivers陣列   
            if (!(bcon->flags & CON_BOOT)) {//判斷是否已經有引導控制檯了,有了的話就直接退出
                printk(KERN_INFO "Too late to register bootconsole %s%d\n",newcon->name, newcon->index);
                return;
            }
        }
    }
     
    if (console_drivers && console_drivers->flags & CON_BOOT)//如果註冊的是引導控制檯  
        bcon = console_drivers;//讓bcon指向全域性console_drivers   
 
    if (preferred_console < 0 || bcon || !console_drivers)
        preferred_console = selected_console;//設定preferred_console為uboot命令選擇的selected_console(即索引)   
 
    if (newcon->early_setup)//early console沒有初始化early_setup欄位,以下這個函式不執行
        newcon->early_setup();//呼叫serial8250_console_early_setup()
 
 
    if (preferred_console < 0) {
        if (newcon->index < 0)
            newcon->index = 0;
        if (newcon->setup == NULL ||newcon->setup(newcon, NULL) == 0) {
            newcon->flags |= CON_ENABLED;
            if (newcon->device) {
                newcon->flags |= CON_CONSDEV;
                preferred_console = 0;
            }
        }
    }
 
     //傳給核心引數:
     //Kernel command line: console=ttyS2,115200n8 rw root=/dev/ram0 initrd=0xc2000000,20M mem=128M ip=192.168.1.220::192.168.1.1:255.255.255.0::eth0:off
     //所以這裡將根據傳參console=ttyS2,115200來配置作為console的ttyS2串列埠
    for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0];i++) {//遍歷全域性console_cmdline找到匹配的 
        if (strcmp(console_cmdline[i].name, newcon->name) != 0)//比較終端名稱“ttyS”
            continue;
        if (newcon->index >= 0 &&newcon->index != console_cmdline[i].index)//console_cmdline[i].index=2。//比較次裝置號  
            continue;
        if (newcon->index < 0)
            newcon->index = console_cmdline[i].index;//將終端號賦值給serial8250_console->index
             
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE//沒有定義,下邊不執行
        if (console_cmdline[i].brl_options) {
            newcon->flags |= CON_BRL;
            braille_register_console(newcon,console_cmdline[i].index,console_cmdline[i].options,console_cmdline[i].brl_options);
            return;
        }
#endif
        //console_cmdline[i].options = "115200n8",對於early console而言setup欄位未被初始化,故下邊的函式不執行
        if (newcon->setup &&newcon->setup(newcon, console_cmdline[i].options) != 0)//呼叫serial8250_console_setup()對終端進行配置
            break;
        newcon->flags |= CON_ENABLED; //設定標誌為CON_ENABLE(這個在printk呼叫中使用到) 
        newcon->index = console_cmdline[i].index;//設定索引號   
        if (i == selected_console) { //索引號和uboot指定的console的一樣 
            newcon->flags |= CON_CONSDEV;//設定標誌CON_CONSDEV(全域性console_drivers連結串列中靠前) 
            preferred_console = selected_console;
        }
        break;
    }//for迴圈作用大致是檢視註冊的console是否是uboot知道的引導console,是則設定相關標誌和preferred_console
 
    if (!(newcon->flags & CON_ENABLED))
        return;
 
    if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV))//防止重複列印   
        newcon->flags &= ~CON_PRINTBUFFER;
 
    acquire_console_sem();
    if ((newcon->flags & CON_CONSDEV) || console_drivers == NULL) {//如果是preferred控制檯
        newcon->next = console_drivers;
        console_drivers = newcon;//新增進全域性console_drivers連結串列前面位置(printk中會遍歷該表呼叫合適的console的write方法列印資訊)
        if (newcon->next)
            newcon->next->flags &= ~CON_CONSDEV;
    } else {//如果不是preferred控制檯 
        newcon->next = console_drivers->next;
        console_drivers->next = newcon; //新增進全域性console_drivers連結串列後面位置
    }
     
    //主冊console主要是刷選preferred_console放置在全域性console_drivers連結串列前面,剩下的console放置連結串列靠後的位置,並設定相應的flags,
    //console_drivers最終會在printk函式的層層呼叫中遍歷到,並呼叫console的write方法將資訊打印出來
 
    if (newcon->flags & CON_PRINTBUFFER) {
        spin_lock_irqsave(&logbuf_lock, flags);
        con_start = log_start;
        spin_unlock_irqrestore(&logbuf_lock, flags);
    }
    release_console_sem();
 
    if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV)) {
        printk(KERN_INFO "console [%s%d] enabled, bootconsole disabled\n",newcon->name, newcon->index);
        for_each_console(bcon)
            if (bcon->flags & CON_BOOT)
                unregister_console(bcon);
    } else {//呼叫這裡
        printk(KERN_INFO "%sconsole [%s%d] enabled\n",(newcon->flags & CON_BOOT) ? "boot" : "" ,newcon->name, newcon->index);
    }
}
 
四、在未對console進行初始化之前,核心使用early console進行列印。之後核心進行真正的console初始化
//console_init()在start_kernel()中呼叫,用來對控制檯初始化,這個函式執行完成後,串列埠可以看到核心用printk()函式列印的資訊
void __init console_init(void)
{
 initcall_t *call;
 
 /* Setup the default TTY line discipline. */
 //此函式呼叫tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY)
 //#define N_TTY 0
 /*struct tty_ldisc_ops tty_ldisc_N_TTY = {
         .magic           = TTY_LDISC_MAGIC,
         .name            = "n_tty",
         .open            = n_tty_open,
         .close           = n_tty_close,
         .flush_buffer    = n_tty_flush_buffer,
         .chars_in_buffer = n_tty_chars_in_buffer,
         .read            = n_tty_read,
         .write           = n_tty_write,
         .ioctl           = n_tty_ioctl,
         .set_termios     = n_tty_set_termios,
         .poll            = n_tty_poll,
         .receive_buf     = n_tty_receive_buf,
         .write_wakeup    = n_tty_write_wakeup
    };
    核心定義一個tty_ldiscs陣列,然後根據陣列下標來存放對應的線路規程的操作集,而這裡的陣列下標表示的就是具體的協議,在標頭檔案中已經通過巨集定義好了。例如N_TTY 0。 
 
    所以可以發現:ldisc[0] 存放的是N_TTY對應的線路規程操作集
    ldisc[1]存放的是N_SLIP對應的線路規程操作集
    ldisc[2]存放的就是N_MOUSE對應的線路規程操作集
    依次類推。此處就是ldisc[N_TTY] = tty_ldisc_N_TTY。
 
 int tty_register_ldisc(int disc, struct tty_ldisc_ops *new_ldisc)
    {
         unsigned long flags;
         int ret = 0;
         if (disc < N_TTY || disc >= NR_LDISCS)
                 return -EINVAL;
         spin_lock_irqsave(&tty_ldisc_lock, flags);
         tty_ldiscs[disc] = new_ldisc;//tty_ldiscs[0]存放的是N_TTY對應的線路規程操作集
         new_ldisc->num = disc;//0
         new_ldisc->refcount = 0;
         spin_unlock_irqrestore(&tty_ldisc_lock, flags);
         return ret;
     }
*/
 tty_ldisc_begin();//這段程式碼前面是註冊了第0個(邏輯上1)線路規程
 
  //依次呼叫從__con_initcall_start到__con_initcall_end之間的函式指標
  //會呼叫兩個函式就是con_init()和serial8250_console_init()
 call = __con_initcall_start;
 while (call < __con_initcall_end) {
  (*call)();
  call++;
 }
}
 
static int __init serial8250_console_init(void)
{
    if (nr_uarts > UART_NR)//串列埠數量不能大於3個
        nr_uarts = UART_NR;
 
    serial8250_isa_init_ports();//對三個串列埠的uart_8250_port結構靜態常量serial8250_ports結構進行初始化,主要是將up->port.ops = &serial8250_pops
    /*
    static struct console serial8250_console = {
        .name       = "ttyS",
        .write      = serial8250_console_write,//寫方法
        .device     = uart_console_device,//tty驅動
        .setup      = serial8250_console_setup,//設定串列埠波特率,也就是設定串列埠。很重要,裡面涉及到平臺特性,波特率相關。
        .early_setup    = serial8250_console_early_setup,
        .flags      = CON_PRINTBUFFER | CON_ANYTIME,
        .index      = -1,
        .data       = &serial8250_reg,
    };
    */
    register_console(&serial8250_console);//在這裡註冊serial8250_console真正的console終端
    return 0;
}
console_initcall(serial8250_console_init);
/*
serial8250_console_init()函式會比serial8250_probe()先呼叫,所以呼叫register_console的時候,port還沒有初始化,所以當
register_console呼叫serial8250_console_setup()設定buad,parity bits的時候,
serial8250_console_setup()會檢測port->iobase和port->membase是否是有效值,如果不是就返回,
放棄初始化console,所以實際上,console不是在serial8250_console_init()裡邊初始化,
如果要在serial8250_console_init初始化,需要將port靜態初始化.
 
當serial8250_probe()呼叫uart_add_one_port->uart_configure_port:
if (port->cons && !(port->cons->flags & CON_ENABLED)){
    printk("%s retister console\n", __FUNCTION__);
    register_console(port->cons);
}
該函式會檢查console有沒有初始化,如果沒有初始化,則呼叫register_console來初始化.
所以console放在這裡初始化也是比較好一些,可以將console_initcall(serial8250_console_init) comment.
*/
 
//對三個串列埠的uart_8250_port結構靜態常量serial8250_ports結構進行初始化,主要是將up->port.ops = &serial8250_pops
static void __init serial8250_isa_init_ports(void)
{
    struct uart_8250_port *up;
    static int first = 1;
    int i, irqflag = 0;
 
    if (!first)//靜態變數,serial8250_console_init()第一次進入這個函式,之後serial8250_init()再進入這個函式就會直接返回
        return;
    first = 0;
     
    //對三個串列埠的uart_8250_port結構serial8250_ports結構體進行初始化
    for (i = 0; i < nr_uarts; i++) {
        struct uart_8250_port *up = &serial8250_ports[i];
 
        up->port.line = i;//0代表串列埠0,1代表串列埠1
        spin_lock_init(&up->port.lock);
 
        init_timer(&up->timer);//初始化定時器
        up->timer.function = serial8250_timeout;//初始化定時器的超時函式
 
        //ALPHA_KLUDGE_MCR needs to be killed.
        up->mcr_mask = ~ALPHA_KLUDGE_MCR;
        up->mcr_force = ALPHA_KLUDGE_MCR;
         
        //初始化uart_8250_port指向的uart_port欄位port的操作
        up->port.ops = &serial8250_pops;
        /*
        static struct uart_ops serial8250_pops = {
            .tx_empty   = serial8250_tx_empty,
            .set_mctrl  = serial8250_set_mctrl,
            .get_mctrl  = serial8250_get_mctrl,
            .stop_tx    = serial8250_stop_tx,
            .start_tx   = serial8250_start_tx,
            .stop_rx    = serial8250_stop_rx,
            .enable_ms  = serial8250_enable_ms,
            .break_ctl  = serial8250_break_ctl,
            .startup    = serial8250_startup,
            .shutdown   = serial8250_shutdown,
            .set_termios    = serial8250_set_termios,
            .set_ldisc  = serial8250_set_ldisc,
            .pm     = serial8250_pm,
            .type       = serial8250_type,
            .release_port   = serial8250_release_port,
            .request_port   = serial8250_request_port,
            .config_port    = serial8250_config_port,
            .verify_port    = serial8250_verify_port,
        #ifdef CONFIG_CONSOLE_POLL
            .poll_get_char = serial8250_get_poll_char,
            .poll_put_char = serial8250_put_poll_char,
        #endif
        };
        */
    }
 
    if (share_irqs)//中斷是否共享(這裡設定成不共享)
        irqflag = IRQF_SHARED;
     
    //條件不滿足,不會進來初始化
    for (i = 0, up = serial8250_ports;i < ARRAY_SIZE(old_serial_port) && i < nr_uarts;i++, up++) {
/*  up->port.iobase   = old_serial_port[i].port;
        up->port.irq      = irq_canonicalize(old_serial_port[i].irq);
        up->port.irqflags = old_serial_port[i].irqflags;
        up->port.uartclk  = old_serial_port[i].baud_base * 16;
        up->port.flags    = old_serial_port[i].flags;
        up->port.hub6     = old_serial_port[i].hub6;
        up->port.membase  = old_serial_port[i].iomem_base;
        up->port.iotype   = old_serial_port[i].io_type;
        up->port.regshift = old_serial_port[i].iomem_reg_shift;
        set_io_from_upio(&up->port);
        up->port.irqflags |= irqflag;
        if (serial8250_isa_config != NULL)
            serial8250_isa_config(i, &up->port, &up->capabilities);
*/
    }
}
 
//下邊再次呼叫register_console()註冊serial8250_console真正的console終端
void register_console(struct console *newcon)
{
    int i;
    unsigned long flags;
    struct console *bcon = NULL;
    /*
    現在是註冊一個serial8250_console,即
    static struct console serial8250_console = {
        .name       = "ttyS",
        .write      = serial8250_console_write,//寫方法
        .device     = uart_console_device,//tty驅動
        .setup      = serial8250_console_setup,//設定串列埠波特率,也就是設定串列埠。很重要,裡面涉及到平臺特性,波特率相關。
        .early_setup    = serial8250_console_early_setup,
        .flags      = CON_PRINTBUFFER | CON_ANYTIME,
        .index      = -1,
        .data       = &serial8250_reg,
    };
    */
    if (console_drivers && newcon->flags & CON_BOOT) {//註冊的是serial8250_console,CON_BOOT沒有置位,不是引導控制檯。下邊不會進去遍歷
        for_each_console(bcon) {////遍歷全域性console_drivers陣列   
            if (!(bcon->flags & CON_BOOT)) {//判斷是否已經有引導控制檯了,有了的話就直接退出
                printk(KERN_INFO "Too late to register bootconsole %s%d\n",newcon->name, newcon->index);
                return;
            }
        }
    }
     
    if (console_drivers && console_drivers->flags & CON_BOOT)//如果註冊的是引導控制檯,serial8250_console不是引導控制檯
        bcon = console_drivers;//這裡不執行 
 
    if (preferred_console < 0 || bcon || !console_drivers)
        preferred_console = selected_console;//設定preferred_console為uboot命令選擇的selected_console(即在Uboot傳入的引數“console=ttyS2,115200n8”在console_cmdline[]陣列中的索引)   
                                                                                 //這裡preferred_console =0
    if (newcon->early_setup)//serial8250_console初始化early_setup欄位
        newcon->early_setup();//呼叫serial8250_console_early_setup()
 
 
    if (preferred_console < 0) {//由於preferred_console =0,不會進入下邊
        if (newcon->index < 0)
            newcon->index = 0;
        if (newcon->setup == NULL ||newcon->setup(newcon, NULL) == 0) {
            newcon->flags |= CON_ENABLED;
            if (newcon->device) {
                newcon->flags |= CON_CONSDEV;
                preferred_console = 0;
            }
        }
    }
 
     //傳給核心引數:
     //Kernel command line: console=ttyS2,115200n8 rw root=/dev/ram0 initrd=0xc2000000,20M mem=128M ip=192.168.1.220::192.168.1.1:255.255.255.0::eth0:off
     //所以這裡將根據傳參console=ttyS2,115200來配置作為console的ttyS2串列埠
    for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0];i++) {//遍歷全域性console_cmdline找到匹配的,i=0就是匹配的“ttyS2”
        if (strcmp(console_cmdline[i].name, newcon->name) != 0)//比較終端名稱“ttyS”
            continue;
        if (newcon->index >= 0 &&newcon->index != console_cmdline[i].index)//console_cmdline[i].index=2。//比較次裝置號  
            continue;
        if (newcon->index < 0)
            newcon->index = console_cmdline[i].index;//將終端號賦值給serial8250_console->index,這裡是2
             
        //console_cmdline[i].options = "115200n8",對於serial8250_console而言setup欄位已初始化
        if (newcon->setup && newcon->setup(newcon, console_cmdline[i].options) != 0)//呼叫serial8250_console_setup()對終端進行配置,呼叫不成功
            break;
        //在這裡註冊serial8250_console時,呼叫serial8250_console_setup()由於port->iobase和port->membase不是有效值,
        //故返回錯誤,這樣下邊的操作不會執行,直接break跳出,從flag1出跳出函式。即在這裡serial8250_console沒有註冊成功
        //由於核心在下邊的操作隊串列埠進行初始化時,還會呼叫register_console()來註冊serial8250_console,在那時註冊就會成功
         
        newcon->flags |= CON_ENABLED; //設定標誌為CON_ENABLE,表示console使能(這個在printk呼叫中使用到) 
        newcon->index = console_cmdline[i].index;//設定索引號   
        if (i == selected_console) { //索引號和uboot指定的console的一樣 
            newcon->flags |= CON_CONSDEV;//設定標誌CON_CONSDEV(全域性console_drivers連結串列中靠前) 
            preferred_console = selected_console;
        }
        break;
    }//for迴圈作用大致是檢視註冊的console是否是uboot知道的引導console,是則設定相關標誌和preferred_console
 
  //flag1:
    if (!(newcon->flags & CON_ENABLED))//若前邊沒有設定CON_ENABLED標誌,就退出
        return;
 
    if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV))//防止重複列印   
        newcon->flags &= ~CON_PRINTBUFFER;
 
    acquire_console_sem();
    if ((newcon->flags & CON_CONSDEV) || console_drivers == NULL) {//如果是preferred控制檯
        newcon->next = console_drivers;
        console_drivers = newcon;//新增進全域性console_drivers連結串列前面位置(printk中會遍歷該表呼叫合適的console的write方法列印資訊)
        if (newcon->next)
            newcon->next->flags &= ~CON_CONSDEV;
    } else {//如果不是preferred控制檯 
        newcon->next = console_drivers->next;
        console_drivers->next = newcon; //新增進全域性console_drivers連結串列後面位置
    }
     
    //主冊console主要是刷選preferred_console放置在全域性console_drivers連結串列前面,剩下的console放置連結串列靠後的位置,並設定相應的flags,
    //console_drivers最終會在printk函式的層層呼叫中遍歷到,並呼叫console的write方法將資訊打印出來
 
    if (newcon->flags & CON_PRINTBUFFER) {
        spin_lock_irqsave(&logbuf_lock, flags);
        con_start = log_start;
        spin_unlock_irqrestore(&logbuf_lock, flags);
    }
    release_console_sem();
 
    if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV)) {
        printk(KERN_INFO "console [%s%d] enabled, bootconsole disabled\n",newcon->name, newcon->index);
        for_each_console(bcon)
            if (bcon->flags & CON_BOOT)
                unregister_console(bcon);
    } else {//呼叫這裡
        printk(KERN_INFO "%sconsole [%s%d] enabled\n",(newcon->flags & CON_BOOT) ? "boot" : "" ,newcon->name, newcon->index);
    }
}
 
//serial8250_console_early_setup()-->serial8250_find_port_for_earlycon()
int serial8250_find_port_for_earlycon(void)
{
    struct early_serial8250_device *device = &early_device;//early console初始化時對early_device結構的初始化
    struct uart_port *port = &device->port;
    int line;
    int ret;
 
    if (!device->port.membase && !device->port.iobase)//early_device結構初始化時已經配置好
        return -ENODEV;
    //early console註冊時不會呼叫此函式。
    //當真正的console初始化時,會呼叫此函式。
    //真正的console初始化時,會查詢early console註冊時用的是哪一個串列埠號,從serial8250_ports[]中根據uart_port->mapbase地址來比對
    line = serial8250_find_port(port);//根據uart_port結構找到串列埠號,比對沒有找到串列埠號,line返回負值
    if (line < 0)
        return -ENODEV;//從這裡返回,下邊的不再執行
     
    //若找到early console用的串列埠號,更新當初傳入核心引數使用的console_cmdline[i],名稱改成ttyS。。。。
    ret = update_console_cmdline("uart", 8250, "ttyS", line, device->options);
    if (ret < 0)
        ret = update_console_cmdline("uart", 0,"ttyS", line, device->options);
 
    return ret;
}
 
static int __init serial8250_console_setup(struct console *co, char *options)
{
    struct uart_port *port;
    int baud = 9600;
    int bits = 8;
    int parity = 'n';
    int flow = 'n';
 
    if (co->index >= nr_uarts)//console的索引,這裡是2,即ttyS2
        co->index = 0;
    port = &serial8250_ports[co->index].port;//找到對應的ttyS2的uart_port結構
     
    //由於console_init在註冊serial8250_console時呼叫的register_console()函式呼叫serial8250_console_setup()
    //進入這個函式時,由於ttyS2的uart_port結構沒有初始化,port->iobase 和port->membase值都未設定,所以直接從下邊返回
    //當進行串列埠初始化時,還會回來註冊serial8250_console,再呼叫到這裡,由於設定了ttyS2的uart_port結構,所以下邊的配置就會成功
    if (!port->iobase && !port->membase)//第一次註冊時,由於未設定,從這裡直接返回
        return -ENODEV;
 
    if (options)//如果options不為空,就將options裡的數值寫給baud, &parity, &bits, &flow
        uart_parse_options(options, &baud, &parity, &bits, &flow);
    //沒有配置options,則使用預設值,否則使用傳下來的的引數options裡的串列埠配置
    return uart_set_options(port, co, baud, parity, bits, flow);
}
 
五、通過四知道,在對console註冊時,沒有成功,由於串列埠還沒有配置。當對串列埠配置時再對console註冊就能成功。
serial8250_console就能註冊到核心全域性變數console_drivers中。這樣終端列印時就通過註冊的serial8250_console就能將資訊列印到終端上。
  
//核心的列印函式
asmlinkage int printk(const char *fmt, ...)
{
    va_list args;   //可變引數連結串列
    int r;
 
#ifdef CONFIG_KGDB_KDB
    if (unlikely(kdb_trap_printk)) {
        va_start(args, fmt);
        r = vkdb_printf(fmt, args);
        va_end(args);
        return r;
    }
#endif
    va_start(args, fmt);    //獲取第一個可變引數
    r = vprintk(fmt, args); //呼叫vprintk函式
    va_end(args);   //釋放可變引數連結串列指標
 
    return r;
}
 
//vprintk函式
asmlinkage int vprintk(const char *fmt, va_list args)
{
    int printed_len = 0;
    int current_log_level = default_message_loglevel;
    unsigned long flags;
    int this_cpu;
    char *p;
 
    boot_delay_msec();
    printk_delay();
    preempt_disable();
    raw_local_irq_save(flags);
    this_cpu = smp_processor_id();
    if (unlikely(printk_cpu == this_cpu)) {
        if (!oops_in_progress) {
            recursion_bug = 1;
            goto out_restore_irqs;
        }
        zap_locks();
    }
 
    lockdep_off();
    spin_lock(&logbuf_lock);
    printk_cpu = this_cpu;
 
    if (recursion_bug) {
        recursion_bug = 0;
        strcpy(printk_buf, recursion_bug_msg);
        printed_len = strlen(recursion_bug_msg);
    }
    printed_len += vscnprintf(printk_buf + printed_len,sizeof(printk_buf) - printed_len, fmt, args);
    p = printk_buf;
    if (p[0] == '<') {//處理列印級別欄位
        unsigned char c = p[1];
        if (c && p[2] == '>') {
            switch (c) {
            case '0' ... '7': /* loglevel */
                current_log_level = c - '0';
            case 'd': /* KERN_DEFAULT */
                if (!new_text_line) {
                    emit_log_char('\n');
                    new_text_line = 1;
                }
            case 'c': /* KERN_CONT */
                p += 3;
                break;
            }
        }
    }
    for ( ; *p; p++) {
        if (new_text_line) {
            /* Always output the token */
            emit_log_char('<');
            emit_log_char(current_log_level + '0');
            emit_log_char('>');
            printed_len += 3;
            new_text_line = 0;
 
            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);
                nanosec_rem = do_div(t, 1000000000);
                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;
            }
 
            if (!*p)
                break;
        }
 
        emit_log_char(*p);
        if (*p == '\n')
            new_text_line = 1;
    }
    if (acquire_console_semaphore_for_printk(this_cpu))
        release_console_sem();
 
    lockdep_on();
out_restore_irqs:
    raw_local_irq_restore(flags);
 
    preempt_enable();
    return printed_len;
}
 
//接著呼叫release_console_sem函式 
void release_console_sem(void)
{
    unsigned long flags;
    unsigned _con_start, _log_end;
    unsigned wake_klogd = 0;
 
    if (console_suspended) {
        up(&console_sem);
        return;
    }
 
    console_may_schedule = 0;
 
    for ( ; ; ) {
        spin_lock_irqsave(&logbuf_lock, flags);
        wake_klogd |= log_start - log_end;
        if (con_start == log_end)
            break;          /* Nothing to print */
        _con_start = con_start;
        _log_end = log_end;
        con_start = log_end;        /* Flush */
        spin_unlock(&logbuf_lock);
        stop_critical_timings();    /* don't trace print latency */
        call_console_drivers(_con_start, _log_end);
        start_critical_timings();
        local_irq_restore(flags);
    }
    console_locked = 0;
    up(&console_sem);
    spin_unlock_irqrestore(&logbuf_lock, flags);
    if (wake_klogd)
        wake_up_klogd();
}
EXPORT_SYMBOL(release_console_sem);
 
//呼叫call_console_drivers函式
static void call_console_drivers(unsigned start, unsigned end)
{
    unsigned cur_index, start_print;
    static int msg_level = -1;
 
    BUG_ON(((int)(start - end)) > 0);
 
    cur_index = start;
    start_print = start;
    while (cur_index != end) {
        if (msg_level < 0 && ((end - cur_index) > 2) &&LOG_BUF(cur_index + 0) == '<' &&LOG_BUF(cur_index + 1) >= '0' &&LOG_BUF(cur_index + 1) <= '7' &&LOG_BUF(cur_index + 2) == '>') {
            msg_level = LOG_BUF(cur_index + 1) - '0';
            cur_index += 3;
            start_print = cur_index;
        }
        while (cur_index != end) {
            char c = LOG_BUF(cur_index);
 
            cur_index++;
            if (c == '\n') {
                if (msg_level < 0) {
                    msg_level = default_message_loglevel;
                }
                _call_console_drivers(start_print, cur_index, msg_level);
                msg_level = -1;
                start_print = cur_index;
                break;
            }
        }
    }
    _call_console_drivers(start_print, end, msg_level);
}_call_console_drivers函式
 
//呼叫console的寫方法
static void __call_console_drivers(unsigned start, unsigned end)  
{  
    struct console *con;  
   
    for_each_console(con) {//遍歷console_drivers陣列 #define for_each_console(con) for (con = console_drivers; con != NULL; 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);   //呼叫console的寫方法   
    }  
}  
 
//由於已經註冊的終端是serial8250_console,這個終端的寫方法是呼叫serial8250_console_write()函式--->uart_console_write()--->serial8250_console_putchar()
//--->serial_out()最終列印在串列埠2終端上
/*
    static struct console serial8250_console = {
        .name       = "ttyS",
        .write      = serial8250_console_write,//寫方法
        .device     = uart_console_device,//tty驅動
        .setup      = serial8250_console_setup,//設定串列埠波特率,也就是設定串列埠。很重要,裡面涉及到平臺特性,波特率相關。
        .early_setup    = serial8250_console_early_setup,
        .flags      = CON_PRINTBUFFER | CON_ANYTIME,
        .index      = -1,
        .data       = &serial8250_reg,
    };
    */
console_drivers連結串列在register_console中會設定