early_console是Kernel初始化初期建立起来用于串行输出的设备,源码在earlycon.c中实现。
Kernel中 early_param 通常作为模块初始化入口元素的定义,在Kernel初始化时执行解析和模块初始化。Linux Kernel中众多模块或驱动子系统均通过这种方式定义其初始化入口。在Kernel初始化汇编代码执行完跳转到start_kernel之后,setup_arch调用parse_early_param,进而在其中执行 early_param 的解析,具体如下:
start_kernel->setup_arch->parse_early_param->parse_early_options->do_early_param
实际上在parse_early_options中调用parse_args,并且将do_early_param作为参数传入,进而继续调用parse_one且传入参数do_early_param,parse_one将参数解析成功后执行do_early_param:
/* 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;
}
在vmlinux.lds.h(include/asm-generic)中可以看到对于.init.setup段的定义:
#define INIT_SETUP(initsetup_align) \
. = ALIGN(initsetup_align); \
__setup_start = .; \
KEEP(*(.init.setup)) \
__setup_end = .;
其对__setup_start和__setup_end地址也做了划定,那么此处的do_early_param函数就是对.init.setup段内定义的参数进行遍历并调用器setup_func成员进行初始化。
而earlycon的early_param的定义如下:
/* early_param wrapper for setup_earlycon() */
static int __init param_setup_earlycon(char *buf)
{
int err;
/* Just 'earlycon' is a valid param for devicetree and ACPI SPCR. */
if (!buf || !buf[0]) {
if (IS_ENABLED(CONFIG_ACPI_SPCR_TABLE)) {
earlycon_acpi_spcr_enable = 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;
}
early_param("earlycon", param_setup_earlycon);
这里通过early_param来定义,需要先分析一下early_param:
struct obs_kernel_param {
const char *str;
int (*setup_func)(char *);
int early;
};
/*
* Only for really core code. See moduleparam.h for the normal way.
*
* Force the alignment so the compiler doesn't space elements of the
* obs_kernel_param "array" too far apart in .init.setup.
*/
#define __setup_param(str, unique_id, fn, early) \
static const char __setup_str_##unique_id[] __initconst \
__aligned(1) = str; \
static struct obs_kernel_param __setup_##unique_id \
__used __section(.init.setup) \
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_##unique_id, fn, early }
#define __setup(str, fn) \
__setup_param(str, fn, fn, 0)
/*
* NOTE: fn is as per module_param, not __setup!
* Emits warning if fn returns non-zero.
*/
#define early_param(str, fn) \
__setup_param(str, fn, fn, 1)
就是一系列的宏,展开如下:
early_param("earlycon", param_setup_earlycon);
__setup_param("earlycon", param_setup_earlycon, param_setup_earlycon, 1);
static const char __setup_str_param_setup_earlycon[] __initconst __aligned(1) = "earlycon";
static struct obs_kernel_param __setup_param_setup_earlycon
__used_section(.init.setup) __attribute__((aligned((sizeof(long)))))
= {
.str = __setup_str_param_setup_earlycon,
.setup_func = param_setup_earlycon,
.early = 1
};
通过展开可以看出其实际作用是定义了一个类型为struct obs_kernel_param的静态结构并赋予初值,并且使用了编译器选项通知编译器在编译阶段将变量放置在.init.setup段,在上面提到的do_early_param解析过程中解析,并执行setup_func。
如上遍历执行p->setup_func(val)时,如果遍历到__setup_param_setup_earlycon元素,则p实际上是指针,指向&__setup_param_setup_earlycon, 即该结构的地址,执行其setup_func函数,即param_setup_earlycon。
通过上面的分析可以看到,在start_kernel调用setup_arch函数,进而执行启动参数(boot_command_line)解析,如果存在参数“earlycon”,则 param_setup_earlycon 函数被执行,正式进入early_console初始化过程,该函数其实是一个包裹函数,其中进行两种解析过程,第一种是early_init_dt_scan_chosen_stdout函数中进行的根据device-tree解析进行,另一种则通过 setup_earlycon 函数。
这里提到了Kernel启动参数boot_command_line,有读者对这部分感兴趣的话可以阅读另一篇博文《Kernel启动参数boot_command_line》。下面这篇文章也可以参考:https://blog.csdn.net/tiantao2012/article/details/54923232
执行过程如下:
early_param: param_setup_earlycon->setup_earlycon->register_earlycon->earlycon_map; earlycon_init; register_console
/**
* setup_earlycon - match and register earlycon console
* @buf: earlycon param string
*
* Registers the earlycon console matching the earlycon specified
* in the param string @buf. Acceptable param strings are of the form
* ,io|mmio|mmio32|mmio32be,,
* ,0x,
* ,
*
*
* Only for the third form does the earlycon setup() method receive the
* string in the 'options' parameter; all other forms set
* the parameter to NULL.
*
* Returns 0 if an attempt to register the earlycon was made,
* otherwise negative error code
*/
int __init setup_earlycon(char *buf)
{
const struct earlycon_id **p_match;
if (!buf || !buf[0])
return -EINVAL;
if (early_con.flags & CON_ENABLED)
return -EALREADY;
for (p_match = __earlycon_table; p_match < __earlycon_table_end;
p_match++) {
const struct earlycon_id *match = *p_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设备为early_console_dev,定义在earlycon.c中:
static struct console early_con = {
.name = "uart", /* fixed up at earlycon registration */
.flags = CON_PRINTBUFFER | CON_BOOT,
.index = 0,
};
static struct earlycon_device early_console_dev = {
.con = &early_con,
};
register_earlycon函数中通过parse_options函数进行命令行参数解析,获取到console的各项配置参数,如port地址信息等,通过 earlycon_map; earlycon_init;函数将获取到的参数实际进行赋值,完成初始化,再通过 register_console注册。
参考Documentation/kernel-parameters.txt和Documentation/serial-console.txt中对console和earlycon的说明。
也可以参考如下文档:Linux Serial Console
有一个具体配置的例子可以作为参考,以具体实例解析了函数执行过程:grub参数console=
函数定义如下:
#ifdef CONFIG_SERIAL_EARLYCON
int __init early_init_dt_scan_chosen_stdout(void)
{
int offset;
const char *p, *q, *options = NULL;
int l;
const struct earlycon_id **p_match;
const void *fdt = initial_boot_params;
offset = fdt_path_offset(fdt, "/chosen");
if (offset < 0)
offset = fdt_path_offset(fdt, "/chosen@0");
if (offset < 0)
return -ENOENT;
p = fdt_getprop(fdt, offset, "stdout-path", &l);
if (!p)
p = fdt_getprop(fdt, offset, "linux,stdout-path", &l);
if (!p || !l)
return -ENOENT;
q = strchrnul(p, ':');
if (*q != '\0')
options = q + 1;
l = q - p;
/* Get the node specified by stdout-path */
offset = fdt_path_offset_namelen(fdt, p, l);
if (offset < 0) {
pr_warn("earlycon: stdout-path %.*s not found\n", l, p);
return 0;
}
for (p_match = __earlycon_table; p_match < __earlycon_table_end;
p_match++) {
const struct earlycon_id *match = *p_match;
if (!match->compatible[0])
continue;
if (fdt_node_check_compatible(fdt, offset, match->compatible))
continue;
if (of_setup_earlycon(match, offset, options) == 0)
return 0;
}
return -ENODEV;
}
#endif
该函数对device-tree中的chosen节点进行解析,获取到stdout-path内容后对__earlycon_table段元素定义进行遍历检查和匹配,从而调用of_setup_earlycon函数进行earlycon的初始化:
#ifdef CONFIG_OF_EARLY_FLATTREE
int __init of_setup_earlycon(const struct earlycon_id *match,
unsigned long node,
const char *options)
{
int err;
struct uart_port *port = &early_console_dev.port;
const __be32 *val;
bool big_endian;
u64 addr;
spin_lock_init(&port->lock);
port->iotype = UPIO_MEM;
addr = of_flat_dt_translate_address(node);
if (addr == OF_BAD_ADDR) {
pr_warn("[%s] bad address\n", match->name);
return -ENXIO;
}
port->mapbase = addr;
val = of_get_flat_dt_prop(node, "reg-offset", NULL);
if (val)
port->mapbase += be32_to_cpu(*val);
port->membase = earlycon_map(port->mapbase, SZ_4K);
val = of_get_flat_dt_prop(node, "reg-shift", NULL);
if (val)
port->regshift = be32_to_cpu(*val);
big_endian = of_get_flat_dt_prop(node, "big-endian", NULL) != NULL ||
(IS_ENABLED(CONFIG_CPU_BIG_ENDIAN) &&
of_get_flat_dt_prop(node, "native-endian", NULL) != NULL);
val = of_get_flat_dt_prop(node, "reg-io-width", NULL);
if (val) {
switch (be32_to_cpu(*val)) {
case 1:
port->iotype = UPIO_MEM;
break;
case 2:
port->iotype = UPIO_MEM16;
break;
case 4:
port->iotype = (big_endian) ? UPIO_MEM32BE : UPIO_MEM32;
break;
default:
pr_warn("[%s] unsupported reg-io-width\n", match->name);
return -EINVAL;
}
}
val = of_get_flat_dt_prop(node, "current-speed", NULL);
if (val)
early_console_dev.baud = be32_to_cpu(*val);
val = of_get_flat_dt_prop(node, "clock-frequency", NULL);
if (val)
port->uartclk = be32_to_cpu(*val);
if (options) {
early_console_dev.baud = simple_strtoul(options, NULL, 0);
strlcpy(early_console_dev.options, options,
sizeof(early_console_dev.options));
}
earlycon_init(&early_console_dev, match->name);
err = match->setup(&early_console_dev, options);
if (err < 0)
return err;
if (!early_console_dev.con->write)
return -ENODEV;
register_console(early_console_dev.con);
return 0;
}
#endif /* CONFIG_OF_EARLY_FLATTREE */
可以看到与前面提到通过 setup_earlycon 函数进行初始化过程不同,当param_setup_earlycon参数buf中并未传入console参数,则通过这种方式进行初始化,当然是解析dts来获取对应的配置参数,完成类似前面提到的一系列初始化过程,最后注册console信息到console_drivers全局链表中。
可以通过EARLYCON_DECLARE宏来进行earlycon定义,比如著名的uart8250,其定义在8250_early.c(drivers/tty/serial/8250):
int __init early_serial8250_setup(struct earlycon_device *device,
const char *options)
{
if (!(device->port.membase || device->port.iobase))
return -ENODEV;
if (!device->baud) {
struct uart_port *port = &device->port;
unsigned int ier;
/* assume the device was initialized, only mask interrupts */
ier = serial8250_early_in(port, UART_IER);
serial8250_early_out(port, UART_IER, ier & UART_IER_UUE);
} else
init_port(device);
device->con->write = early_serial8250_write;
return 0;
}
EARLYCON_DECLARE(uart8250, early_serial8250_setup);
EARLYCON_DECLARE(uart, early_serial8250_setup);
OF_EARLYCON_DECLARE(ns16550, "ns16550", early_serial8250_setup);
OF_EARLYCON_DECLARE(ns16550a, "ns16550a", early_serial8250_setup);
OF_EARLYCON_DECLARE(uart, "nvidia,tegra20-uart", early_serial8250_setup);
OF_EARLYCON_DECLARE(uart, "snps,dw-apb-uart", early_serial8250_setup);
继续分析EARLYCON_DECLARE,可以看到其定义如下:
/*
* Console helpers.
*/
struct earlycon_device {
struct console *con;
struct uart_port port;
char options[16]; /* e.g., 115200n8 */
unsigned int baud;
};
struct earlycon_id {
char name[15];
char name_term; /* In case compiler didn't '\0' term name */
char compatible[128];
int (*setup)(struct earlycon_device *, const char *options);
};
extern const struct earlycon_id *__earlycon_table[];
extern const struct earlycon_id *__earlycon_table_end[];
#if defined(CONFIG_SERIAL_EARLYCON) && !defined(MODULE)
#define EARLYCON_USED_OR_UNUSED __used
#else
#define EARLYCON_USED_OR_UNUSED __maybe_unused
#endif
#define _OF_EARLYCON_DECLARE(_name, compat, fn, unique_id) \
static const struct earlycon_id unique_id \
EARLYCON_USED_OR_UNUSED __initconst \
= { .name = __stringify(_name), \
.compatible = compat, \
.setup = fn }; \
static const struct earlycon_id EARLYCON_USED_OR_UNUSED \
__section(__earlycon_table) \
* const __PASTE(__p, unique_id) = &unique_id
#define OF_EARLYCON_DECLARE(_name, compat, fn) \
_OF_EARLYCON_DECLARE(_name, compat, fn, \
__UNIQUE_ID(__earlycon_##_name))
#define EARLYCON_DECLARE(_name, fn) OF_EARLYCON_DECLARE(_name, "", fn)
这里看到了__UNIQUE_ID宏定义,有三个不同的版本,gcc版本定义如下:
/* 这里用到了__PASTE宏,同样贴出来,其实就是简单的粘连 */
/* Indirect macros required for expanded argument pasting, eg. __LINE__. */
#define ___PASTE(a,b) a##b
#define __PASTE(a,b) ___PASTE(a,b)
#define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__)
为了保证ID的唯一性,GCC预定义了__COUNTER__宏,编译过程中对其递增后展开,那么我们对uart8250定义展开如下:
EARLYCON_DECLARE(uart8250, early_serial8250_setup);
OF_EARLYCON_DECLARE(uart8250, "", early_serial8250_setup);
_OF_EARLYCON_DECLARE(uart8250, "", early_serial8250_setup, \
__PASTE(__PASTE(__UNIQUE_ID_, __earlycon_uart8250), __COUNTER__));
/* 此处我们假设 __COUNTER__ 是第二次出现,其展开为1 */
_OF_EARLYCON_DECLARE(uart8250, "", early_serial8250_setup, __UNIQUE_ID___earlycon_uart82501)
static const struct earlycon_id __UNIQUE_ID___earlycon_uart82501
EARLYCON_USED_OR_UNUSED __initconst
= {
.name = __stringify(uart8250),
.compatible = "",
.setup = early_serial8250_setup
};
static const struct earlycon_id
EARLYCON_USED_OR_UNUSED __section(__earlycon_table)
* const __punique_id = &__UNIQUE_ID___earlycon_uart82501;
如上最后的展开结果看到在__earlycon_table段定义了struct earlycon_id类型的指针,指向前面静态定义的结构,其初始化函数为early_serial8250_setup。那么问题又来了,该table中定义的元素何时用到并执行其初始化函数完成console的初始化过程呢?
首先来看__eatlycon_table定义,同样在vmlinux.lds.h文件中:
#ifdef CONFIG_SERIAL_EARLYCON
#define EARLYCON_TABLE() . = ALIGN(8); \
__earlycon_table = .; \
KEEP(*(__earlycon_table)) \
__earlycon_table_end = .;
#else
#define EARLYCON_TABLE()
#endif
对于定义在该section中earlycon元素的解析和初始化发生在setup_earlycon函数中:
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;
}
/**
* setup_earlycon - match and register earlycon console
* @buf: earlycon param string
*
* Registers the earlycon console matching the earlycon specified
* in the param string @buf. Acceptable param strings are of the form
* ,io|mmio|mmio32|mmio32be,,
* ,0x,
* ,
*
*
* Only for the third form does the earlycon setup() method receive the
* string in the 'options' parameter; all other forms set
* the parameter to NULL.
*
* Returns 0 if an attempt to register the earlycon was made,
* otherwise negative error code
*/
int __init setup_earlycon(char *buf)
{
const struct earlycon_id **p_match;
if (!buf || !buf[0])
return -EINVAL;
if (early_con.flags & CON_ENABLED)
return -EALREADY;
for (p_match = __earlycon_table; p_match < __earlycon_table_end;
p_match++) {
const struct earlycon_id *match = *p_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;
}
其实对于buf中的带解析参数,与所有在该section中的earlycon_id元素进行name的对比,匹配到之后调用register_earlycon函数进行具体的初始化和注册操作:在该函数内执行了parse_options进行参数解析,进一步调用earlycon_map进行端口地址映射,然后在earlycon_init进一步进行初始化,最终经过对应的setup函数配置过程之后调用register_console将对应的console注册到console_drivers全局链表中。
earlycon的两种定义方式可以参考如下来自stackoverflow的回答:
https://stackoverflow.com/questions/42967091/how-to-use-early-printk-functionality-for-arm64
Two options,
Using DTS entry: Use "stdout-path" option in chosen node to specify the serial/uart driver to use
uart3: serial@e0126000 { compatible = "actions,s900-uart", "actions,owl-uart"; reg = <0x0 0xe0126000 0x0 0x2000>; interrupts =
; status = "okay"; }; aliases { serial3 = &uart3; }; chosen { stdout-path = "serial3:115200n8"; }; Again the example is for Actions SoC from here. With this entry in place, one need to give bootargs as "earlycon" either from u-boot or using the same chosen node "bootargs" option.
- Another way is to explicitly specify the driver name using "earlycon" as bootargs like earlyprink. earlycon=owl-uart,e0126000 will ask the kernel to use the earylconsole definition in UART driver and uses the base address for Virtual and physical address mapping.
这里有一篇博文也非常值得参考《[console] earlycon实现流程》