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段中。
s
tatic 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,是0x70000000,match是_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函数输出了。
过程中遇到的问题:
- 在kernel未建立console之前,printk打印的信息是怎么输出?
对于ARM64,通过earlycon机制输出。
- 在kernel未初始化earlycon之前,printk打印的信息是怎么输出?
在未初始化earlycon之前,printk打印的信息,其实是没有打印出来的,打印信息保存在内部的缓冲区,等待earlycon建立好后,缓冲区的信息才被打印出来。