本文以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来实现打印。