early_printk技术分析
前言:最近调试stm32f7的uClinux的时候,卡在Satrting kernel …这个地方,这是U-boot打印的最后一串字符串。所以为了看打印,我就看了early_print这部分相关的一些代码。做一下总结。
关于内核下的printk技术,我也了解了一下,调用printk的时候,如果struct console console还没有初始化,要打印的字符串是会暂时存放到缓冲区里面(具体缓冲区是什么形式的,暂时我还没有太深入的研究)。当console初始化完成之后,再调用printk的时候,就会一股脑的把之前缓存的缓冲区里面的数据打印出来。
而early_printk函数,可以不借助console相关的接口,而是可以通过简单的直接往对应的串口TX对应的寄存器中写入据就可以,提前打印一些信息(备注:主要是用于调试)。
所以,今天我就以stm32f7位例子,演示如何实现early_printk技术。首先要明确一个概念,内核中有函数early_print和early_printk。其中early_print,只是尝试提前打印信息,需要借助printk,而early_printk则是注册了一个简单的console,供printk调用。用示意图展示如下所示:
early_print
\
printk
\
early_printk(技术)
首先看一下early_print函数
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);
}
可以看出early_print会直接调用printk函数完成打印。
接下来就着重分析early_printk技术,有一个很重要的文件arch/arm/kernel/early_printk.c。在这个文件中,首先分析宏early_param(“earlyprintk”, setup_early_printk)
early_param("earlyprintk", setup_early_printk)
__setup_param("earlyprintk", setup_early_printk, setup_early_printk, 1)
最终展开如下所示:
static const char __setup_str_setup_early_printk[] __initconst \
__aligned(1) = "earlyprintk";
static struct obs_kernel_param __setup_setup_early_printk \
__used __section(.init.setup) \
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_setup_early_printk, setup_early_printk, 1 }
去除一些编译器有关段属性相关的概念,简单来说,就是使用宏early_param(“earlyprintk”, setup_early_printk) ,定义了一个结构体
static struct obs_kernel_param __setup_setup_early_printk {
.str = "earlyprintk",
.setup_func = setup_early_printk,
.early = 1,
};
接下来就是看什么时候会调用setup_early_printk函数,这时候需要分析函数do_early_param。具体的执行流程如下所示:
start_kernel()
...
setup_arch()//从命令行或者设备树中获取bootargs参数。
...
parse_early_param()//开始分析 bootargs参数,这里面会判断如果命令行中有earlyprintk关键字,并且对应的存在struct obs_kernel_param 结构体与之匹配,那么回调用对应结构体的setup_func函数
strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);//复制一份命令行参数
parse_early_options(tmp_cmdline);//开始分析
parse_args("early options", cmdline, NULL, 0, 0, 0, NULL, do_early_param);
static int __init do_early_param(char *param, char *val, const char *unused, void *arg)//回调do_early_param函数
具体的匹配和初始化是在do_early_param函数中实现的,这个函数的注释如下
extern const struct obs_kernel_param __setup_start[], __setup_end[];
static int __init do_early_param(char *param, char *val,
const char *unused, void *arg)
{
const struct obs_kernel_param *p;
//以下的for循环会从obs_kernel_param结构体列表中遍历,有两种情况会执行该结构体的setup_func函数:
//1.如果成员early=1并且bootargs中存在该结构体的str字符串;
//2.如果该结构体的str为earlycon,并且bootargs中有console关键字;
for (p = __setup_start; p < __setup_end; p++) {
if ((p->early && parameq(param, p->str)) ||
(strcmp(param, "console") == 0 &&
strcmp(p->str, "earlycon") == 0)
) {
//如果符合以上中的任意一个,就会执行对应的setup_func函数
if (p->setup_func(val) != 0)
pr_warn("Malformed early option '%s'\n", param);
}
}
/* We accept everything at this stage. */
return 0;
}
现在联想到最开始,early_printk.c文件中,使用宏定义的static struct obs_kernel_param __setup_setup_early_printk。可以判断,就是在这里调用了对应的setup_func函数。继续分析setup_func函数。
//这个early_console_dev只有write接口,没有setup接口,所以就只是实现简单的写串口操作
static struct console early_console_dev = {
.name = "earlycon",
.write = early_console_write,
.flags = CON_PRINTBUFFER | CON_BOOT,
.index = -1,
};
static int __init setup_early_printk(char *buf)
{
early_console = &early_console_dev;
//调用register_console函数,注册了一个console,
//提前说下,在串口没有被真正初始化之前,printk使用的是early_console_dev这个console
register_console(&early_console_dev);
return 0;
}
重点在于函数register_console,我选择关键的执行到的部分进行分析
void register_console(struct console *newcon)
{
...
if (preferred_console < 0) {
if (newcon->index < 0)
newcon->index = 0;//设置early_console_dev->index=0
if (newcon->setup == NULL ||
newcon->setup(newcon, NULL) == 0) {
newcon->flags |= CON_ENABLED;//设置early_console_dev->flags使能
...
}
}
...
if ((newcon->flags & CON_CONSDEV) || console_drivers == NULL) {
newcon->next = console_drivers;
console_drivers = newcon;//将early_console_dev添加到链表中,并设置console_drivers = early_console_dev
...
}
...
if (newcon->flags & CON_PRINTBUFFER) {
...
exclusive_console = newcon;//设置exclusive_console=early_console_dev!!!很关键
}
}
通过上述代码的分析,可知,执行setup_early_printk()函数的结果是,console_drivers=exclusive_console=early_console_dev;
接下来就分析printk函数了,看下printk函数是如何使用early_console_dev这个console的:
DEFINE_PER_CPU(printk_func_t, printk_func) = vprintk_default;
printk(const char *fmt, ...)
vprintk_func = this_cpu_read(printk_func);//这句话等价vprintk_func=vprintk_default;我把这段分析放在文章最后。
r = vprintk_func(fmt, args);//实际调用的是函数vprintk_default(fmt, args)
vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, 0, fmt, args)
...
void console_unlock(void)
.../在函数call_console_drivers调用了early_console_dev的write接口函数
static void call_console_drivers(int level, const char *ext_text, \
size_t ext_len, const char *text, size_t len) {
...
for_each_console(con) {
if (exclusive_console && con != exclusive_console)
continue;//之前已经设置exclusive_console=early_console_dev;所以只会匹配early_console_dev设备
if (!(con->flags & CON_ENABLED))
continue;//在register_console中使能过了
if (!con->write)
continue;//early_console_dev存在write接口函数
if (!cpu_online(smp_processor_id()) &&
!(con->flags & CON_ANYTIME))
continue;
if (con->flags & CON_EXTENDED)
con->write(con, ext_text, ext_len);
else
con->write(con, text, len);//调用该接口函数
}
}
...//
所以,在执行完parse_early_param函数之后,如果使能了earlyprintk,那么就可以通过printk看到串口输出了。接下来就是实现early_console_dev的write接口函数,该函数调用了汇编函数printch,这个汇编函数在文件arch/arm/kernel/debug.S中定义,具体代码如下:
ENTRY(printascii)
addruart_current r3, r1, r2
b 2f
1: waituart r2, r3
senduart r1, r3
busyuart r2, r3
teq r1, #'\n'
moveq r1, #'\r'
beq 1b
2: teq r0, #0
ldrneb r1, [r0], #1
teqne r1, #0
bne 1b
ret lr
ENDPROC(printascii)
ENTRY(printch)
addruart_current r3, r1, r2
mov r1, r0
mov r0, #0
b 1b
ENDPROC(printch)
根据上述代码,关键在于实现三个宏:waituart、senduart、busyuart。为了支持stm32f7,我写了三个宏
//备注CONFIG_DEBUG_STM32F7_UART_PHYS通过menuconfig进行配置
.macro addruart, rp, rv, tmp
ldr \rp, =CONFIG_DEBUG_STM32F7_UART_PHYS
.endm
.macro waituart,rd,rx
.endm
#define STM32_UART_TDR 0x28
.macro senduart,rd,rx
strb \rd, [\rx, #STM32_UART_TDR]
.endm
#define STM32_UART_ISR 0x1c
#define STM32_UART_TC_FLAG 0x1 << 6
.macro busyuart,rd,rx
3201: ldrb \rd, [\rx, #STM32_UART_ISR]
and \rd, \rd, #STM32_UART_TC_FLAG
teq \rd, #0x0
beq 3201b
.endm
代码实现层面,主要就是上述几行代码,为了可以编译该文件,还需要进行一些Kconfig文件的修改,在这里就不赘述了,根据原始的Kconfig,修改下就可以。
分析vprintk_func = this_cpu_read(printk_func)这行代码
DEFINE_PER_CPU(printk_func_t, printk_func) = vprintk_default;
DEFINE_PER_CPU(printk_func_t, printk_func) ;
DEFINE_PER_CPU_SECTION(printk_func_t, printk_func, "")
__PCPU_ATTRS("") PER_CPU_DEF_ATTRIBUTES __typeof__(printk_func_t) printk_func
__percpu __attribute__((section(".data" ""))) __typeof__(printk_func_t) printk_func
//备注,__percpu也是段属性相关的宏,未展开,简单来说就是定义了一个printk_func_t函数指针类型的变量printk_func;并给printk_func赋值vprintk_default;
vprintk_func = this_cpu_read(printk_func)
this_cpu_read(printk_func)
__pcpu_size_call_return(this_cpu_read_, printk_func)
({ \
typeof(printk_func) pscr_ret__;//定义了一个printk_func同类型的变量pscr_ret__ \
__verify_pcpu_ptr(&(printk_func)); //做一些检查 \
switch(sizeof(printk_func)) {//因为是函数指针,32bit,sizeof(printk_func)=4 \
case 1: pscr_ret__ = this_cpu_read_1(printk_func); break; \
case 2: pscr_ret__ = this_cpu_read_2(printk_func); break; \
case 4: pscr_ret__ = this_cpu_read_4(printk_func); break; \
case 8: pscr_ret__ = this_cpu_read_8(printk_func); break; \
default: \
__bad_size_call_parameter(); break; \
} \
pscr_ret__; \
})
/继续分析
this_cpu_read_4(printk_func)
this_cpu_generic_read(printk_func)
({ \
typeof(printk_func) __ret; \
preempt_disable(); \
__ret = *this_cpu_ptr(&(printk_func)); \
preempt_enable(); \
__ret; \
})
//继续分析
this_cpu_ptr(&(printk_func))
raw_cpu_ptr(&(printk_func))
per_cpu_ptr(&(printk_func), 0)
({ (void)(&(printk_func)); VERIFY_PERCPU_PTR(&(printk_func)); })//返回的就是变量printk_func(一个函数指针)自己的地址,也就是vprintk_default的地址&vprintk_default。