Linux终端控制台console和earlycon分析

early_console是Kernel初始化初期建立起来用于串行输出的设备,源码在earlycon.c中实现。

1. 初始化入口

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

2. 通过setup_earlycon初始化earlycon

执行过程如下:

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注册。

3. 通过命令行参数定义earlycon

参考Documentation/kernel-parameters.txt和Documentation/serial-console.txt中对console和earlycon的说明。

也可以参考如下文档:Linux Serial Console

有一个具体配置的例子可以作为参考,以具体实例解析了函数执行过程:grub参数console=

4. 通过early_init_dt_scan_chosen_stdout初始化earlycon

函数定义如下:

#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全局链表中。

5. 另一种定义方式——EARLYCON_DECLARE系列宏

可以通过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实现流程》 

你可能感兴趣的:(Linux终端控制台console和earlycon分析)