14.7printk 和 early_printk console驱动
在 Linux 内核中,printk()是最常用的调试手段。printk()的打印消息会放入一个环形缓冲区(RingBuffer),而/proc/kmsg 文件用于描述这个环形缓冲区。通过 dmesg 命令或 klogd 可以读取该环形缓冲区。如果用户空间的 klogd 守护进程在运行,klogd将获取内核消息并分发给 syslogd,syslogd 接着检查/etc/syslog.conf 来找出如何处理它们。
内核 printk 信息支持 8 个级别,优先级从高到低(数值越高,级别越低,消息越不重要)分别是:KERN_EMERG(数值为0)、KERNEL_ALERT、KERN_CRIT、KERN_ERR、KERN_WARNING、KERN_NOTICE、KERN_INFO、KERN_DEBUG(数值为7)。当调用 printk()函数时指定的优先级小于指定的控制台优先级 console_loglevel 时,调试消息显示在控制台终端。默认的的 console_loglevel 值是 DEFAULT_CONSOLE_LOGLEVEL,用户可以使用系统调用 sys_syslog 或 klogd -c 来修改 console_loglevel 值,也可以直接 echo 值到/proc/sys/kernel/printk。/proc/sys/kernel/printk文件包含四个整数值,第一个表示系统当前的优先级,第二个表示系统默认的优先级。
在 Linux 中,用于 printk 输出的是内核 console,用 console 结构体来描述,如代码清单 14.19 所示。
代码清单 14.19 用于 printk 的 console 结构体
include/linux/console.h
struct console {
char name[16];
void (*write)(struct console *, const char *, unsigned);
int (*read)(struct console *, char *, unsigned);
struct tty_driver *(*device)(struct console *, int *);
void (*unblank)(void);
int (*setup)(struct console *, char *);
short flags;
short index;
int cflag;
void *data;
struct console *next;
};
分析:
其中,较关键的是 write()和 setup()成员函数,write()用于将打印消息写入 console,setup()用于设置 console 的特性,如波特率、停止位等。
printk()函数经过重重调用,经过_ _call_console_drivers()函数,最终调用 console 的 write()成员函数将控制台消息打印出去,如代码清单 14.20 所示。
代码清单 14.20 printk()最终调用到 console 的 write()成员函数
kernel/printk.c
/*
* Call the console drivers on a range of log_buf
*/
static void __call_console_drivers(unsigned long start, unsigned long end)
{
struct console *con;
for (con = console_drivers; con; con = con->next) {
if ((con->flags & CON_ENABLED) && con->write &&
(cpu_online(smp_processor_id()) ||
(con->flags & CON_ANYTIME)))
con->write(con, &LOG_BUF(start), end - start);
}
}
内核提供如下 API 用于注册和注销 console:
include/linux/console.h
kernel/printk.c
void register_console(struct console *console);
int unregister_console(struct console *console);
在内核 init/main.c 文件中的 start_kernel()函数中,会调用 console_init()函数,该函数会调用位于内核存放 console 初始化函数的代码段,调用其中的每一个初始化 console 的函数,如代码清单 14.21。
代码清单 14.21 console_init()函数
drivers/char/tty_io.c
/*
* Initialize the console device. This is called *early*, so
* we can't necessarily depend on lots of kernel help here.
* Just do some early initializations, and do the complex setup
* later.
*/
void __init console_init(void)
{
initcall_t *call;
/* Setup the default TTY line discipline. */
(void) tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY);
/*
* set up the console device so that later boot sequences can
* inform about problems etc..
*/
call = __con_initcall_start;
while (call < __con_initcall_end) {
(*call)();
call++;
}
}
对于任何一个初始化 console 的函数,只需要通过 console_initcall()进行包装,即可把它放入.con_initcall.init 段(起始地址为_ _con_initcall_start),如最常用的 8250 对应的 console 结构体以及初始化代码如清单 14.22。
代码清单 14.22 8250 的 console 及 console_initcall
drivers/serial/8250.c
static struct console serial8250_console = {
.name = "ttyS",
.write = serial8250_console_write,
.device = uart_console_device,
.setup = serial8250_console_setup, // 设置
.flags = CON_PRINTBUFFER,
.index = -1,
.data = &serial8250_reg,
};
static int __init serial8250_console_init(void)
{
serial8250_isa_init_ports();
register_console(&serial8250_console);
return 0;
}
console_initcall(serial8250_console_init);
console_initcall()是一个宏,定义于 include/linux/init.h 文件。
#define console_initcall(fn) \
static initcall_t __initcall_##fn \
__attribute_used__ __attribute__((__section__(".con_initcall.init")))=fn
展开为:
#define console_initcall(serial8250_console_init) \
static initcall_t __initcall_serial8250_console_init \
__attribute_used__ __attribute__((__section__(".con_initcall.init")))=serial8250_console_init
__section(.con_initcall.init),是一个链接阶段的指示,表明将指定的函数放入.con_initcall.init 段。
console_init()是由 init/main.c 文件中的 start_kernel()函数调用的,在 console_init ()被调用前,还执行了一系列的操作。为了在 console_init()被调用前就能使用 printk(),可以使用内核的“early printk”支持,该选项位于内核配置菜单“Linux Kernel Configuration”下的“ Kernel hacking”菜单之下。
对于 early printk 的 console 的注册通过解析内核的 early_param 完成,例如对于 8250,定义“earlycon”一个内核参数,当解析这个内核参数时,相应地被 early_param ()绑定的函数 setup_early_serial8250_console()被调用,此函数将注册一个用于 early printk 的 console。
代码清单 14.23 8250 的 early printk console
static struct console early_serial8250_console __initdata = {
.name = "uart",
.write = early_serial8250_write,
.flags = CON_PRINTBUFFER | CON_BOOT,
.index = -1,
};
int _ _init setup_early_serial8250_console(char *cmdline)
{
char *options;
int err;
options = strstr(cmdline, "uart8250,");
if (!options) {
options = strstr(cmdline, "uart,");
if (!options)
return 0;
}
options = strchr(cmdline, ',') + 1;
err = early_serial8250_setup(options);
if(err < 0)
return err;
register_console(&early_serial8250_console);
return 0;
}
early_param("earlycon", setup_early_serial8250_console);//注册一个用于 early printk 的 console
例如,在 Linux 启动的 command line 中设置如下参数,将使能 8250 作为 early printk 的 console。
earlycon=uart8250,mmio,0xff5e0000,115200n8
earlycon=uart8250,io,0x3f8,9600n8
总结:
代码清单 14.22 第 7 行的 flags 和代码清单 14.23 第 4 行的 flags 的区别,后者多出一个CON_BOOT 属性。
所有的具有CON_BOOT 属性的console 都会在内核初始化至late initcall 阶段的时候被注销,注销函数是disable_boot_consoles(),其定义如代码清单14.24。
代码清单 14.24 disable_boot_consoles()函数
static int _ _init disable_boot_consoles(void)
{
if (console_drivers != NULL) {
if (console_drivers->flags & CON_BOOT) {
printk(KERN_INFO "turn off boot console %s%d\n",
console_drivers->name, console_drivers->index);
return unregister_console(console_drivers);
}
}
return 0;
}
late_initcall(disable_boot_consoles);
disable_boot_consoles()被 late_initcall()修饰,因此被放入到.initcall7.init 段中。
补充知识:
内核的 initcall 分成 8 级,对应的段分别为.initcall0.init、.initcall1.init、
.initcall2.init、.initcall3.init、.initcall4.init、.initcall5.init、.initcall6.init、.initcall7.init,分别通过
pure_initcall(fn)、core_initcall(fn) 、postcore_initcall(fn) 、arch_initcall(fn)、subsys_initcall(fn)、fs_initcall(fn)、device_initcall(fn)、late_initcall(fn)可将指定的函数放入对应的段。对于 pure_initcall(),指
定的 initcall 不依赖于任何其他部分,因此,其指定函数只能 built-in(编入镜像),不能在模块中。对于 1~7
级,还存在对应的 sync 版本,分别通过 core_initcall_sync(fn)、postcore_initcall_sync(fn)、arch_
initcall_sync(fn)、subsys_initcall_sync(fn)、fs_initcall_sync(fn)、device_initcall_sync(fn)、late_initcall_sync(fn)修饰。