Linux初始化之early_console

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》。

2. 通過setup_earlycon初始化earlycon

執行過程如下:

early_paramparam_setup_earlycon->setup_earlycon->register_earlycon->earlycon_mapearlycon_initregister_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
 *	   <name>,io|mmio|mmio32|mmio32be,<addr>,<options>
 *	   <name>,0x<addr>,<options>
 *	   <name>,<options>
 *	   <name>
 *
 *	Only for the third form does the earlycon setup() method receive the
 *	<options> 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_mapearlycon_init;函數將獲取到的參數實際進行賦值,完成初始化,再通過 register_console註冊。

3. 通過命令行參數定義earlycon

參考Documentation/kernel-parameters.txtDocumentation/serial-console.txt中對console和earlycon的說明。

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
 *	   <name>,io|mmio|mmio32|mmio32be,<addr>,<options>
 *	   <name>,0x<addr>,<options>
 *	   <name>,<options>
 *	   <name>
 *
 *	Only for the third form does the earlycon setup() method receive the
 *	<options> 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全局鏈表中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章