[console] early printk实现流程

时间:2021-07-19 20:46:46

本文以ARM为例

一、功能说明

printk的log输出是由console实现(会在其他文章中说明)。由于在kernel刚启动的过程中,还没有为串口等设备等注册console(在device probe阶段实现),此时无法通过正常的console来输出log。
为此,linux提供了early console机制,用于实现为设备注册console之前的早期log的输出,对应console也称为boot console,简称bcon。这个console在kernel启动的早期阶段就会被注册,主要通过输出设备(比如串口设备)的简单的write方法直接进行数据打印。而这个write方法也就是平台实现。
注意,这时候作为输出的串口设备是基于bootloader中已经初始化完成的。
early console机制有两种实现方式,早期的early_printk实现和后面的earlycon实现。在这里主要说明early_printk的实现方式.
early_printk与earlycon相比较为落后,其差异可以参考《earlycon实现流程》文章,建议使用earlycon的实现方式来做early console功能

二、需要打开的宏,如何使能

1、需要打开的宏

CONFIG_DEBUG_LL

ENTRY(printch)定义在arch/arm/debug.S中,需要用这个宏来打开。

CONFIG_EARLY_PRINTK

setup_early_printk的定义。解析cmdline中的early_printk参数并安装boot console。
early_printk输出函数的定义。

2、如何使能

在cmdline中添加“earlyprintk”字符串,如下:

"console=ttySAC0,115200n8 root=/dev/mmcblk0p1 rw rootwait ignore_loglevel earlyprintk"

三、如何使用

1、printascii、printk、early_print、early_printk的区别。

2、在setup.c中加对应例子并打印

比如:

printascii("early_printk success")
early_printk("early_printk success");
printk("printk success")
early_print("early_print success")

(1)printascii(不建议使用)
(2)early_printk
(3)printk(通过early_console(bcon)来进行输出的)
(4)early_print(不建议使用)
内部包含了printascii和printk
加一条early_print测试一下会不会打印两次

四、early console代码流程

由于early_printk和printk都是基于early console的基础上实现。所以先说明early console的流程。

1、解析cmdline中的“early_printk参数”

arch/arm/kernel/early_printk.c

early_param("earlyprintk", setup_early_printk);

early_param和__setup相似,在cmdline中解析到”earlyprintk”字符串时,调用setup_early_printk。
__setup与early_param不同的是,early_param 宏注册的内核选项必须要在其他内核选项之前被处理。在函数start_kernel中,parse_early_param处理early_param定义的参数,parse_args处理__setup定义的参数,这里不详细说明,只需要知道当从cmdline中查找到对应字符串时,对应的setup函数会被调用。

2、setup_early_printk

arch/arm/kernel/early_printk.c

static int __init setup_early_printk(char *buf)
{
    early_console = &early_console_dev;
    register_console(&early_console_dev);
    return 0;
}

设置了early_console的实现。
调用register_console向kernel注册一个console(具体细节在console篇中说明)。

3、early_console_dev实现

arch/arm/kernel/early_printk.c

static struct console early_console_dev = {
    .name =        "earlycon",
    .write =    early_console_write,
    .flags =    CON_PRINTBUFFER | CON_BOOT,
    .index =    -1,
};

其中write方法用于实现console输出的入口,也early console的核心。
CON_PRINTBUFFER标识,表示注册这个console的时候,需要把printk的buf中的log通过这个console进行输出。
CON_BOOT标识,表示这是一个boot console(bcon)。当启动过程了注册其他非boot console的时候,需要先卸载掉这个console。

4、early_console_write也就是要实现printk和early_printk的核心

arch/arm/kernel/early_printk.c

static void early_console_write(struct console *con, const char *s, unsigned n)
{
    early_write(s, n);
}
static void early_write(const char *s, unsigned n)
{
    while (n-- > 0) {
        if (*s == '\n')
            printch('\r');
        printch(*s);
        s++;
    }
}

调用到printch函数中。

5、printch实现

arch/arm/kernel/debug.S

ENTRY(printch)
        addruart_current r3, r1, r2
        mov    r1, r0
        mov    r0, #0
        b    1b
ENDPROC(printch)

addruart_current宏定义如下:

        .macro    addruart_current, rx, tmp1, tmp2
        addruart    \tmp1, \tmp2, \rx
        mrc        p15, 0, \rx, c1, c0
        tst        \rx, #1
        moveq        \rx, \tmp1
        movne        \rx, \tmp2
        .endm

调用到addruart,也就是真正做写数据动作的位置,这也是平台上要实现的东西,也是说移植的核心就是实现这个函数。
以s5pv210为例
arch/arm/include/debug/s5pv210.S

.macro addruart, rp, rv, tmp
        ldr    \rp, =S5PV210_PA_UART
        ldr    \rv, =S3C_VA_UART
#if CONFIG_DEBUG_S3C_UART != 0
        add    \rp, \rp, #(0x400 * CONFIG_DEBUG_S3C_UART)
        add    \rv, \rv, #(0x400 * CONFIG_DEBUG_S3C_UART)
#endif
    .endm

注意在这里,只是基于bootloader中对于uart已经初始化完成并且可以正常使用的基础上,直接往uart的tx寄存器中写入数据,从而实现串口输出的目的。

至此,有两种方法通过early_console来输出log。
(1)直接调用early_console->write。
early_console->write(early_console, buf, n);
early_printk函数就是通过这种方法实现。
(2)通过标准printk接口调用到console的write函数。
printk函数就是通过这种方法实现。
下面会详细说明。

五、early_printk软件流程

kernel/printk/printk.c

#ifdef CONFIG_EARLY_PRINTK
struct console *early_console;
asmlinkage __visible void early_printk(const char *fmt, ...)
{
    va_list ap;
    char buf[512];
    int n;
    if (!early_console)
        return;
    va_start(ap, fmt);
    n = vscnprintf(buf, sizeof(buf), fmt, ap);
    va_end(ap);
    early_console->write(early_console, buf, n);
}
#endif

直接通过early_console->write来进行console的输出。也就是上述四的内容。

六、printk软件流程(当early_console作为console)

当四中的register_console(&early_console_dev);完成之后,console子系统中的console_drivers就存在了early_console_dev这个console(具体参考“console”的文章)。
经过printk的标准调用之后

printk->vprintk->console_unlock->call_console_drivers

在call_console_drivers调用如下:

     for_each_console(con) {
        con->write(con, text, len);
    }
#define for_each_console(con) \
        for (con = console_drivers; con != NULL; con = con->next)

early_console_dev作为当前console_drivers一个con,其write函数也会被调用。
early_console_dev->write(con, text, len);
也就是上述第四节的内容,可以实现输出的目的。

七、Q&A

1、为什么就算early printk功能没有打开,只要串口console被注册,启动早期的log也会被打印出来?
printk buffer的存在。
串口console中包含CON_PRINTBUFFER标识,可以打印出printk buffer中的log。具体可以看register_console的实现。

八、backup

1、printascii软件流程(废弃)

arch/arm/kernel/debug.S
ENTRY(printascii)
addruart_current r3, r1, r2
ENDPROC(printascii)
同样也是通过addruart_current来实现。
具体参考《四、5实现》

2、 early_print软件流程(同样不推荐使用)

arch/arm/kernel/setup.c

void __init early_print(const char *str, ...)
{
    extern void printascii(const char *);
    char buf[256];
    va_list ap;
    va_start(ap, str);
    vsnprintf(buf, sizeof(buf), str, ap);
    va_end(ap);
#ifdef CONFIG_DEBUG_LL
    printascii(buf);
#endif
    printk("%s", buf);
}

通过printascii和printk来实现打印。