Linux单用户模式console问题分析

问题背景

  • Kernel:v5.4.131
  • ACPI有SPCR表

最近遇到一个问题: 单用户模式, arm64平台, 显示器进不了console,只显示部分log或只有光标在闪烁, 而接的调试串口可以正常进console;
MIPS和X86没有这个问题,可以正常在显示器进console

原因分析

单用户模式下,首先看下ARM64和X86下dmesg关于console的差异:

  • ARM64:
    printk: console [ttyAMA0] enabled
  • X86:
    printk: console [tty0] enabled
    你会看到ARM64下面使用串口为首选console,而X86是tty0。

通过代码分析,其主要的差异在acpi_table_parse()函数调用这:
在ARM64平台:
arch/arm64/kernel/acpi.c

if (acpi_disabled) {
	if (earlycon_acpi_spcr_enable)
		early_init_dt_scan_chosen_stdout();
} else {
	acpi_parse_spcr(earlycon_acpi_spcr_enable, true);
	if (IS_ENABLED(CONFIG_ACPI_BGRT))
		acpi_table_parse(ACPI_SIG_BGRT, acpi_parse_bgrt);
}

X86平台:
arch/x86/kernel/acpi/boot.c

/* Do not enable ACPI SPCR console by default */
	acpi_parse_spcr(earlycon_acpi_spcr_enable, false);

以上可以看出2个平台处理SPCR时,传递的enable_console参数值不一样,所以ARM平台会根据SPCR的配置去添加首选console到console_cmdline,而X86的不会,这就是问题的关键,而且一般的X86平台都没有SPCR表

console整个处理流程

通过代码分析, console整个处理流程如下:

ACPI table(SPCR)         \
add_preferred_console()  -| --> __add_preferred_console() -> console_cmdline[]
cmdline(console=xxx)	 /                                         |
                                                                   |
                                                                   |
    vt      \                                                      |
amba-pl011  -| --> register_console()  < — — — — — — — — — — — — — -
  tty等驱动  /

主要分为2大块: console_cmdline数组添加、console注册register_console, 下面就分别进行讨论

console_cmdline添加

console_cmdline[]数组的内容是通过__add_preferred_console()函数添加的,
主要下面几个来源:

  • SPCR
  • cmdline"console=xxx"参数
  • 驱动中使用add_preferred_console()

下面,我们依次看下 SPCR相关处理流程、cmdline处理流程及__add_preferred_console()处理函数

SPCR处理流程

在支持ACPI的情况下,基本上所有平台都会走以下流程来处理SPCR表:

setup_arch()
	-> acpi_boot_table_init()  
		-> acpi_parse_spcr() [drivers/acpi/spcr.c]
			-> add_preferred_console() [kernel/printk/printk.c]
				-> __add_preferred_console()

acpi_parse_spcr() 函数会去解析SPCR表, 然后根据表里的内容,去添加配置首选的控制台console。
而其中的2个参数enable_earlycon,enable_console分别控制是否配置earlycon, 和添加首选console

int __init acpi_parse_spcr(bool enable_earlycon, bool enable_console)
{
    ... ...

    pr_info("console: %s\n", opts);

	if (enable_earlycon)
		setup_earlycon(opts);

	if (enable_console)
		err = add_preferred_console(uart, 0, opts + strlen(uart) + 1);
	else
		err = 0;

    ... ...
}

继续向下分析代码,add_preferred_console()函数会最终调用__add_preferred_console 来添加首选console:

int add_preferred_console(char *name, int idx, char *options)
{
	return __add_preferred_console(name, idx, options, NULL);
}

cmdline处理流程

使用grub传递”console=ttyx”参数, 和处理SPCR后面的步骤比较类似,最后也是调用到__add_preferred_console()函数:

/*
 * Set up a console.  Called via do_early_param() in init/main.c
 * for each "console=" parameter in the boot command line.
 */
static int __init console_setup(char *str)
{
	char buf[sizeof(console_cmdline[0].name) + 4]; /* 4 for "ttyS" */
	char *s, *options, *brl_options = NULL;
	int idx;

	... ...

	__add_preferred_console(buf, idx, options, brl_options);
	console_set_on_cmdline = 1;
	return 1;
}
__setup("console=", console_setup);

__add_preferred_console函数

static int __add_preferred_console(char *name, int idx, char *options,
				   char *brl_options)
{
	struct console_cmdline *c;
	int i;

	/*
	 *	See if this tty is not yet registered, and
	 *	if we have a slot free.
	 */
	for (i = 0, c = console_cmdline;
	     i < MAX_CMDLINECONSOLES && c->name[0];
	     i++, c++) {
		if (strcmp(c->name, name) == 0 && c->index == idx) {
			if (!brl_options)
				preferred_console = i;
			return 0;
		}
	}
	if (i == MAX_CMDLINECONSOLES)
		return -E2BIG;
	if (!brl_options)
		preferred_console = i;
	strlcpy(c->name, name, sizeof(c->name));
	c->options = options;
	braille_set_options(c, brl_options);

	c->index = idx;
	return 0;
}

首先会去匹配console_cmdline里面有没有相同的,有的话就首选这个并返回;
没有的话就追加到console_cmdline里面,并标记为preferred

console注册

虚拟终端vt_console部分:

drivers/tty/vt/vt.c

console_initcall(con_init); [drivers/tty/vt/vt.c]
	-> con_init()
		-> register_console() [kernel/printk/printk.c]
static struct console vt_console_driver = {
	.name		= "tty",
	.write		= vt_console_print,
	.device		= vt_console_device,
	.unblank	= unblank_screen,
	.flags		= CON_PRINTBUFFER,
	.index		= -1,
};

注意vt_console默认的index-1
内核init的时候默认就会去初始化虚拟终端,并注册console

串口终端部分:

这里举例使用的是amba-pl011驱动
drivers/tty/serial/amba-pl011.c

pl011_probe() [drivers/tty/serial/amba-pl011.c]
	-> pl011_register_port()
		-> uart_add_one_port() [drivers/tty/serial/serial_core.c]
			-> uart_configure_port()
				-> register_console() [kernel/printk/printk.c]

驱动与ACPI或是DTS中的节点匹配后就会调用probe函数-pl011_probe(),才会去注册console

earlycon部分:

setup_earlycon() [drivers/tty/serial/earlycon.c]
	-> register_earlycon()
		-> register_console() [kernel/printk/printk.c]

register_console函数

源码路径:kernel/printk/printk.c
register_console()主要的一些处理流程:

void register_console(struct console *newcon)
{
	int i;
	unsigned long flags;
	struct console *bcon = NULL;
	struct console_cmdline *c;
	static bool has_preferred;

	... ...

	if (console_drivers && console_drivers->flags & CON_BOOT)
		bcon = console_drivers;

	if (!has_preferred || bcon || !console_drivers)
		has_preferred = preferred_console >= 0;

	/*
	 *	See if we want to use this console driver. If we
	 *	didn't select a console we take the first one
	 *	that registers here.
	 */
	if (!has_preferred) {
		if (newcon->index < 0)
			newcon->index = 0;
		if (newcon->setup == NULL ||
		    newcon->setup(newcon, NULL) == 0) {
			newcon->flags |= CON_ENABLED;
			if (newcon->device) {
				newcon->flags |= CON_CONSDEV;
				has_preferred = true;
			}
		}
	}

	/*
	 *	See if this console matches one we selected on
	 *	the command line.
	 */
	for (i = 0, c = console_cmdline;
	     i < MAX_CMDLINECONSOLES && c->name[0];
	     i++, c++) {
		if (!newcon->match ||
		    newcon->match(newcon, c->name, c->index, c->options) != 0) {
			/* default matching */
			BUILD_BUG_ON(sizeof(c->name) != sizeof(newcon->name));
			if (strcmp(c->name, newcon->name) != 0)
				continue;
			if (newcon->index >= 0 &&
			    newcon->index != c->index)
				continue;
			if (newcon->index < 0)
				newcon->index = c->index;

			if (_braille_register_console(newcon, c))
				return;

			if (newcon->setup &&
			    newcon->setup(newcon, c->options) != 0)
				break;
		}

		newcon->flags |= CON_ENABLED;
		if (i == preferred_console) {
			newcon->flags |= CON_CONSDEV;
			has_preferred = true;
		}
		break;
	}

	if (!(newcon->flags & CON_ENABLED))
		return;

	... ...

	pr_info("%sconsole [%s%d] enabled\n",
		(newcon->flags & CON_BOOT) ? "boot" : "" ,
		newcon->name, newcon->index);
	if (bcon &&
	    ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV) &&
	    !keep_bootcon) {
		/* We need to iterate through all boot consoles, to make
		 * sure we print everything out, before we unregister them.
		 */
		for_each_console(bcon)
			if (bcon->flags & CON_BOOT)
				unregister_console(bcon);
	}
}

在没有首选console的情况下, 会把第一个注册过来的console作为首选preferred,并enable置位(CON_ENABLED), 接下来会去匹配console_cmdline中的console, 如果匹配到了就会enable置位(CON_ENABLED), 并判断是否标记为首选;