Linux单用户模式console问题分析
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:
你会看到ARM64下面使用串口为首选console,而X86是tty0。printk: console [tty0] enabled
通过代码分析,其主要的差异在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), 并判断是否标记为首选;