通过设置early_printk选项,可以在内核串口设备初始化之前,看到打印输出,对于调试内核早期的启动问题非常有帮助。如下可在grub命令行中,手动加载启动内核,增加内核参数:
grub>
grub> linux \(hd0,msdos2)/vmlinuz earlyprintk=ttyS0,115200
grub>
grub> boot
grub>
对于x86平台,内核函数setup_early_printk处理earlyprintk选项,解析earlyprintk=之后的参数。主要是串口初始化early_serial_init,和注册函数early_console_register。
static int __init setup_early_printk(char *buf)
{
int keep;
if (early_console) return 0;
keep = (strstr(buf, "keep") != NULL);
while (*buf != '\0') {
if (!strncmp(buf, "serial", 6)) {
buf += 6;
early_serial_init(buf);
early_console_register(&early_serial_console, keep);
if (!strncmp(buf, ",ttyS", 5))
buf += 5;
}
if (!strncmp(buf, "ttyS", 4)) {
early_serial_init(buf + 4);
early_console_register(&early_serial_console, keep);
}
...
early_param("earlyprintk", setup_early_printk);
如下串口初始化函数,可通过命令行earlyprintk指定串口的基地址如: earlyprintk=serial,0x3f8,115200,也可以指定串口编号由内核选择串口寄存器的基地址:earlyprintk=serial,ttyS0,115200。如果是后者,内核仅支持两个串口,即ttyS0和ttyS1,基地址分别为0x3f8和0x2f8。如果指定其它的ttySx,内核修改为ttyS0。
如果不指定波特率,内核默认使用9600。另外,earlyprintk也可以不指定serial选项。
#define DEFAULT_BAUD 9600
static __init void early_serial_init(char *s)
{
unsigned divisor;
unsigned long baud = DEFAULT_BAUD;
char *e;
if (*s == ',') ++s;
if (*s) {
unsigned port;
if (!strncmp(s, "0x", 2)) {
early_serial_base = simple_strtoul(s, &e, 16);
} else {
static const int __initconst bases[] = { 0x3f8, 0x2f8 };
if (!strncmp(s, "ttyS", 4))
s += 4;
port = simple_strtoul(s, &e, 10);
if (port > 1 || s == e)
port = 0;
early_serial_base = bases[port];
}
s += strcspn(s, ",");
if (*s == ',')
s++;
}
if (*s) {
baud = simple_strtoull(s, &e, 0);
if (baud == 0 || s == e)
baud = DEFAULT_BAUD;
}
...
/* Set up the HW */
early_serial_hw_init(divisor);
以下初始化串口硬件寄存器,可见使用8个数据位,1个停止位,无奇偶校验位。
static __init void early_serial_hw_init(unsigned divisor)
{
unsigned char c;
serial_out(early_serial_base, LCR, 0x3); /* 8n1 */
serial_out(early_serial_base, IER, 0); /* no interrupt */
serial_out(early_serial_base, FCR, 0); /* no fifo */
serial_out(early_serial_base, MCR, 0x3); /* DTR + RTS */
c = serial_in(early_serial_base, LCR);
serial_out(early_serial_base, LCR, c | DLAB);
serial_out(early_serial_base, DLL, divisor & 0xff);
serial_out(early_serial_base, DLH, (divisor >> 8) & 0xff);
serial_out(early_serial_base, LCR, c & ~DLAB);
如果在earlyprintk中指定keep选项,如: earlyprintk=ttyS0,115200 keep,在注册终端时,如果有CON_BOOT标志,在系统串口驱动程序正常运行时,将自动注销此console口;否则,如果设置了keep选项,将清除CON_BOOT标记,保留此串口。
static void early_console_register(struct console *con, int keep_early)
{
if (con->index != -1) {
printk(KERN_CRIT "ERROR: earlyprintk= %s already used\n",
con->name);
return;
}
early_console = con;
if (keep_early)
early_console->flags &= ~CON_BOOT;
else
early_console->flags |= CON_BOOT;
register_console(early_console);
在内核启动函数start_kernel中,earlyprintk由函数parse_early_param进行处理,所以,如果内核代码在之前发生panic等错误,earlyprintk也不能打印任何信息。
asmlinkage __visible void __init start_kernel(void)
{
set_task_stack_end_magic(&init_task);
smp_setup_processor_id();
...
pr_notice("%s", linux_banner);
early_security_init();
setup_arch(&command_line);
...
build_all_zonelists(NULL);
page_alloc_init();
pr_notice("Kernel command line: %s\n", boot_command_line);
/* parameters may set static keys */
jump_label_init();
parse_early_param();
内核版本 5.10