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