arm64 earlycon分析

ARM64,在kernel未建立console之前,使用earlycon,实现打印。在bootargs中,要加入如下选项:

earlycon=pl011,0x70000000

pl011表示针对pl011这个串口设备,0x70000000是串口的起始地址。

使用 amba-pl011串口,在 amba-pl011.c中,

有如下宏定义:

EARLYCON_DECLARE(pl011, pl011_early_console_setup)

展开之后,

static const struct earlycon_id _earlycon_pl011  \
__used __section(__earlycon_table) \
= {.name = "pl011", setup=pl011_early_console_setup };

定义了一个earlycon_id结构的变量_earlycon_pl011。该变量中, setup函数指针指向pl011_early_console_setup函数。

struct earlycon_id {
    char name[16];
    int (*setup)(struct earlycon_device *, const char *options);
} __aligned(32);

在 earlycon.c(drivers/tty/serial)中,有如下宏定义:

early_param("earlycon", param_setup_earlycon);

展开如下,定义一个变量,和一个结构体变量。结构体变量,放在了.init.setup段中。

static const char __setup_str_param_setup_earlycon \
        __initconst __aligned(1) = "earlycon";  \
static struct obs_kernel_param __setup_param_setup_earlycon  \
    __used __setion(.init.setup)       \
    __attrubite__((aligned(sizeof(long))))  \
    = { "earlycon", param_setup_earlycon, 1 };

obs_kernel_param原型:

struct obs_kernel_param {
    const char *str;
    int (*setup_func)(char *);
    int early;
}

在 init/main.c中, start_kernel->setup_arch->parse_early_param,通过cmdline传递的参数,进行early初始化。

/* Arch code calls this early on, or if not, just before other parsing. */
void __init parse_early_param(void)
{
    static int done __initdata;
    static char tmp_cmdline[COMMAND_LINE_SIZE] __initdata;
    if (done)
        return;
    /* All fall through to do_early_param. */
    strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
    parse_early_options(tmp_cmdline);
    done = 1;
}

通过parse_early_options函数,分析cmdline,也就是bootargs。

void __init parse_early_options(char *cmdline)
{
    parse_args("early options", cmdline, NULL, 0, 0, 0, NULL, do_early_param);
}

调用parse_args,从cmdline中,分析early options。关键是do_early_param函数。参数param是cmdline中的参数变量以及参数值。

/* Check for early params. */
static int __init do_early_param(char *param, char *val,
                 const char *unused, void *arg)
{
    const struct obs_kernel_param *p;
    for (p = __setup_start; p < __setup_end; p++) {
        if ((p->early && parameq(param, p->str)) ||
            (strcmp(param, "console") == 0 &&
             strcmp(p->str, "earlycon") == 0)
        ) {
            if (p->setup_func(val) != 0)
                pr_warn("Malformed early option '%s'\n", param);
        }
    }
    /* We accept everything at this stage. */
    return 0;
}

这里的__setup_start,是链接脚本中的变量,定义如下,该变量是段.init.setup的起始地址,__setup_end是.init.setup段的结束地址。

.= ALIGN(16); __setup_start = .; *(.init.setup) __setup_end = .;

do_early_param函数的for循环中,从.init.setup段中,依次将obs_kernel_param结构体变量取出来,如果变量中的early为1,并且变量中的str,和函数的参数一致,那么调用结构体中的setup_func函数。

在之前,__setup_param_setup_earlycon变量,是定义在.init.setup。如下图所示。

因为cmdline中,传递了earlycon参数,匹配__setup_param_setup_earlycon中的earlycon,因此执行param_setup_earlycon函数。

earlycon.c(drivers/tty/serial),有该函数,该函数调用setup_earlycon函数。

/* early_param wrapper for setup_earlycon() */
static int __init param_setup_earlycon(char *buf)
{
    int err;
    /*
     * Just 'earlycon' is a valid param for devicetree earlycons;
     * don't generate a warning from parse_early_params() in that case
     */
    if (!buf || !buf[0]) {
        if (IS_ENABLED(CONFIG_ACPI_SPCR_TABLE)) {
            earlycon_init_is_deferred = true;
            return 0;
        } else if (!buf) {
            return early_init_dt_scan_chosen_stdout();
        }
    }
    err = setup_earlycon(buf);
    if (err == -ENOENT || err == -EALREADY)
        return 0;
    return err;
}

对于setup_earlycon函数,参数buf是cmdline的参数值。在这里是earlycon=pl011,0x70000000

int __init setup_earlycon(char *buf)
{
    const struct earlycon_id *match;
    if (!buf || !buf[0])
        return -EINVAL;
    if (early_con.flags & CON_ENABLED)
        return -EALREADY;
    for (match = __earlycon_table; match->name[0]; match++) {
        size_t len = strlen(match->name);
        if (strncmp(buf, match->name, len))
            continue;
        if (buf[len]) {
            if (buf[len] != ',')
                continue;
            buf += len + 1;
        } else
            buf = NULL;
        return register_earlycon(buf, match);
    }
    return -ENOENT;
}

遍历__earlycon_table开始的earlycon_id类型的变量。

对于_earlycon_table,是定义在链接脚本中,保存__early_table段的起始地址。

__early_table = .; *(__early_table) *(__earlycon_table_end) . = ALIGN(8)

在之前,有定义_earlycon_pl011变量,并且,放在了__early_table段中。

cmdline传的参数是 earlycon=pl011,0x70000000
参数
值为pl011,0x70000000逗号之前的pl011和_earlycon_pl011变量中的pl011匹配,因此执行register_earlycon函数。

该函数的2个参数,buf,是0x70000000match_earlycon_pl011变量的指针。

static int __init register_earlycon(char *buf, const struct earlycon_id *match)
{
    int err;
    struct uart_port *port = &early_console_dev.port;
    /* On parsing error, pass the options buf to the setup function */
    if (buf && !parse_options(&early_console_dev, buf))
        buf = NULL;
    spin_lock_init(&port->lock);
    port->uartclk = BASE_BAUD * 16;
    if (port->mapbase)
        port->membase = earlycon_map(port->mapbase, 64);
    earlycon_init(&early_console_dev, match->name);
    err = match->setup(&early_console_dev, buf);
    if (err < 0)
        return err;
    if (!early_console_dev.con->write)
        return -ENODEV;
    register_console(early_console_dev.con);
    return 0;
}

最终,调用match->setup函数,建立earlycon,其实就是调用pl011_early_console_setup。

/*
* On non-ACPI systems, earlycon is enabled by specifying
* "earlycon=pl011,<address>" on the kernel command line.
*
* On ACPI ARM64 systems, an "early" console is enabled via the SPCR table,
* by specifying only "earlycon" on the command line.  Because it requires
* SPCR, the console starts after ACPI is parsed, which is later than a
* traditional early console.
*
* To get the traditional early console that starts before ACPI is parsed,
* specify the full "earlycon=pl011,<address>" option.
*/
static int __init pl011_early_console_setup(struct earlycon_device *device, const char *opt)
{
    if (!device->port.membase)
        return -ENODEV;
    device->con->write = pl011_early_write;
    return 0;
}

其实就是将设置write函数指针为pl011_early_write。这样,在kernel未建立console之前,使用printk打印的信息,最终是调用pl011_early_write函数输出了。

过程中遇到的问题:

1.  在kernel未建立console之前,printk打印的信息是怎么输出?

对于ARM64,通过earlycon机制输出。

2.  在kernel未初始化earlycon之前,printk打印的信息是怎么输出?

在未初始化earlycon之前,printk打印的信息,其实是没有打印出来的,打印信息保存在内部的缓冲区,等待earlycon建立好后,缓冲区的信息才被打印出来。

此条目发表在linux分类目录,贴了, , 标签。将固定链接加入收藏夹。

发表评论

电子邮件地址不会被公开。