earlyprintk技术分析

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。
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值