一.printk
printk函数主要做两件事情:第一件就是将信息记录到log中,而第二件事就是调用控制台驱动来将信息输出。printk的相关函数定义在linux/printk.h。
1.日志级别
printk需要设置日志级别,用来控制printk打印的这条信息是否在终端上显示的,当printk设置的日志级别高于控制台级别时,printk要打印的信息才会在控制台打印出来。
内核日志一共有8种级别:
#define KERN_EMERG "<0>" /* system is unusable */ #define KERN_ALERT "<1>" /* action must be taken immediately */ #define KERN_CRIT "<2>" /* critical conditions */ #define KERN_ERR "<3>" /* error conditions */ #define KERN_WARNING "<4>" /* warning conditions */ #define KERN_NOTICE "<5>" /* normal but significant condition */ #define KERN_INFO "<6>" /* informational */ #define KERN_DEBUG "<7>" /* debug-level messages */
2.控制台级别
#define MINIMUM_CONSOLE_LOGLEVEL 1 /*可以使用的最小日志级别*/ #define DEFAULT_CONSOLE_LOGLEVEL 7 /*默认的控制台级别*/ #define DEFAULT_MESSAGE_LOGLEVEL 4 /* 默认的日志级别 */ int console_printk[4] = { DEFAULT_CONSOLE_LOGLEVEL,/*控制台日志级别:优先级高于该值的消息将被打印至控制台*/ DEFAULT_MESSAGE_LOGLEVEL,/*缺省的消息日志级别:将用该优先级来打印没有优先级的消息*/ MINIMUM_CONSOLE_LOGLEVEL,/*最低的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级)*/ DEFAULT_CONSOLE_LOGLEVEL,/*缺省的控制台日志级别:控制台日志级别的缺省值*/ }; #define console_loglevel (console_printk[0]) #define default_message_loglevel (console_printk[1]) #define minimum_console_loglevel (console_printk[2]) #define default_console_loglevel (console_printk[3])
使用命令 cat /proc/sys/kernel/printk来查看这四个值
其中的 4 4 1 7,分别对应与:console_loglevel、default_message_loglevel、minimum_console_loglevel、default_console_loglevel
default_message_loglevel是缺省时的消息日志级别,因此当printk未指定优先级时,将以该默认级别输出,也就是DEFAULT_MESSAGE_LOGLEVEL =4, 对应KERN_WARNING。
也就是说printk("hello world\n");就表示printk(KERN_WARNING "hello world\n");
那如果我们将控制台级别设成<4,如:
Echo 3 > /proc/sys/kernel/printk
那么printk("hello world\n");就无法输出到控制台。
3.如何修改控制台级别
Echo "n" > /proc/sys/kernel/printk
Eg: Echo 8 > /proc/sys/kernel/printk
那么此时所有的printk日志级别都会被输出到控制台,如下图所示。
printk ( KERN_EMERG "Hello, EMERG.\n" ) ; printk ( KERN_ALERT "Hello, ALERT.\n" ) ; printk ( KERN_CRIT "Hello, CRIT.\n" ) ; printk ( KERN_ERR "Hello, ERR.\n" ) ; printk ( KERN_WARNING "Hello, WARNING.\n" ) ; printk ( KERN_NOTICE "Hello, NOTICE.\n" ) ; printk ( KERN_INFO "Hello, INFO.\n" ) ; printk ( KERN_DEBUG "Hello, DEBUG.\n" ) ;
除了上面的方法修改console打印级别外,还可以修改bootargs来设定:
Uboot中修改“console=ttyS0,115200”改为“loglevel=7 console=ttyS0,115200”,表示设置内核的console_loglevel 值=7,开机cat /proc/sys/kernel/printk,可以看到控制台级别被设置成了7。
4. printk带时间戳讯息
make menuconfig开启如下:
5. printk实现
位于kernel\printk\printk.c
printk // linux 4.9: kernel/printk/internal.h // linux 5.4: kernel/printk/printk_safe.c vprintk_func vprintk_default(fmt, args); vprintk_emit vprintk_store // 把要打印的信息保存在log_buf中 log_output preempt_disable(); if (console_trylock_spinning()) console_unlock(); preempt_enable(); console_unlock for (;;) { msg = log_from_idx(console_idx); if (suppress_message_printing(msg->level)) { /* 如果消息的级别数值大于console_loglevel, 则不打印此信息 */ } printk_safe_enter_irqsave(flags); call_console_drivers(ext_text, ext_len, text, len); printk_safe_exit_irqrestore(flags); }
6. 命令行参数
/* IMX6ULL */ [root@100ask:~]# cat /proc/cmdline console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw
命令行信息可以来自设备树或者环境变量:
/ { chosen { bootargs = "console=ttymxc1,115200"; }; };
修改环境变量:
/* 进入IMX6ULL的UBOOT */ => print mmcargs mmcargs=setenv bootargs console=${console},${baudrate} root=${mmcroot} => print console console=ttymxc0 => print baudrate baudrate=115200
7. console驱动注册过程
struct console { char name[16]; // name为"ttyXXX",在cmdline中用"console=ttyXXX0"来匹配 // 输出函数 void (*write)(struct console *, const char *, unsigned); int (*read)(struct console *, char *, unsigned); // APP访问/dev/console时通过这个函数来确定是哪个(index)设备 // 举例: // a. cmdline中"console=ttymxc1" // b. 则注册对应的console驱动时:console->index = 1 // c. APP访问/dev/console时调用"console->device"来返回这个index struct tty_driver *(*device)(struct console *co, int *index); void (*unblank)(void); // 设置函数, 可设为NULL int (*setup)(struct console *, char *); // 匹配函数, 可设为NULL int (*match)(struct console *, char *name, int idx, char *options); short flags; // 哪个设备用作console: // a. 可以设置为-1, 表示由cmdline确定 // b. 也可以直接指定 short index; // 常用: CON_PRINTBUFFER int cflag; void *data; struct console *next; };
console驱动注册过程
1. 处理命令行参数
__setup("console=", console_setup);
console=ttymxc0,115200 console=ttyVIRT0
处理u-boot通过dts传给内核的cmdline参数,比如bootparam参数。
对于这两个"console=xxx"就会调用console_setup函数两次,构造得到2个数组项:
struct console_cmdline { char name[16]; /* Name of the driver */ int index; /* Minor dev. to use */ char *options; /* Options for the driver */ #ifdef CONFIG_A11Y_BRAILLE_CONSOLE char *brl_options; /* Options for braille driver */ #endif }; static struct console_cmdline console_cmdline[MAX_CMDLINECONSOLES];
在cmdline中,最后的"console=xxx"就是"selected_console"(被选中的console,对应/dev/console):
2. register_console
uart_add_one_port
uart_configure_port
register_console(port->cons);
3. /dev/console
tty_open tty = tty_open_by_driver(device, inode, filp); driver = tty_lookup_driver(device, filp, &index); case MKDEV(TTYAUX_MAJOR, 1): { struct tty_driver *console_driver = console_device(index); /* 从console_drivers链表头开始寻找 * 如果console->device成功,就返回它对应的tty_driver * 这就是/dev/console对应的tty_driver */ struct tty_driver *console_device(int *index) { struct console *c; struct tty_driver *driver = NULL; console_lock(); for_each_console(c) { if (!c->device) continue; driver = c->device(c, index); if (driver) break; } console_unlock(); return driver; }
内核信息的早期打印
当我们注册了uart_driver、并调用uart_add_one_port后,它里面才注册console,在这之后才能使用printk。
如果想更早地使用printk函数,比如在安装UART驱动之前就使用printk,这时就需要自己去注册console。
更早地、单独地注册console,有两种方法:
early_printk:自己实现write函数,不涉及设备树,简单明了
earlycon:通过设备树传入硬件信息,跟内核中驱动程序匹配
earlycon是新的、推荐的方法,在内核已经有驱动的前提下,通过设备树或cmdline指定寄存器地址即可。
1. early_printk
arch\arm\kernel\early_printk.c,必须实现这几点:
-
配置内核,选择:CONFIG_EARLY_PRINTK
-
内核中实现:printch函数
-
cmdline中添加:earlyprintk
2. earlycon
二 linux/prink.h相关函数
#define pr_emerg(fmt, ...) \ printk(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__) #define pr_alert(fmt, ...) \ printk(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__) #define pr_crit(fmt, ...) \ printk(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__) #define pr_err(fmt, ...) \ printk(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__) #define pr_warning(fmt, ...) \ printk(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__) #define pr_warn pr_warning #define pr_notice(fmt, ...) \ printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__) #define pr_info(fmt, ...) \ printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
/* If you are writing a driver, please use dev_dbg instead */ #if defined(CONFIG_DYNAMIC_DEBUG) #include <linux/dynamic_debug.h> /* dynamic_pr_debug() uses pr_fmt() internally so we don't need it here */ #define pr_debug(fmt, ...) \ dynamic_pr_debug(fmt, ##__VA_ARGS__) #elif defined(DEBUG) #define pr_debug(fmt, ...) \ printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__) #else #define pr_debug(fmt, ...) \ no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__) #endif
pr_emerg到pr_info都是一些基本的kernel打印函数,用来设置内核日志打印级别,可以看到它和下面这种打印本质上并无差异。
printk ( KERN_EMERG "Hello, EMERG.\n" ) ; printk ( KERN_ALERT "Hello, ALERT.\n" ) ; printk ( KERN_CRIT "Hello, CRIT.\n" ) ; printk ( KERN_ERR "Hello, ERR.\n" ) ; printk ( KERN_WARNING "Hello, WARNING.\n" ) ; printk ( KERN_NOTICE "Hello, NOTICE.\n" ) ; printk ( KERN_INFO "Hello, INFO.\n" ) ; printk ( KERN_DEBUG "Hello, DEBUG.\n" ) ;
而pr_debug则有3种输出方式,当开启dynamic_debug后,则走dynamic_pr_debug流程(dynamic_debug见下一节)。当用户开启了DEBUG宏,则走printk流程,否则什么都不打印。
三 dmesg命令
那kernel中printk所有日志级别的打印都会将开机信息存储在ring buffer中。dmesg命令是从kernel ring buffer中读取内核日志信息。开机信息亦保存在/var/log目录中,名称为dmesg的文件里。因此可以用dmesg
命令查看。
-c: 当完成打印显示后清除环缓冲内的内容。
-s: 缓冲区大小
定义一个大小为"缓冲区大小"的缓冲区用于查询内核环缓冲区。默认大小为 8196,如果你设置了一个大于默认值的环缓冲区,那你就可以用这个选项定义一个相当的缓冲区来查看完整的环缓冲区内容。
-n:级别
其实用dmesg -n也是可以设置控制台打印级别。
有时候在调试kernel驱动时内核panic了or死锁了,那么无法敲命令,如何查看日志呢?重启后日志就没了,那么可以敲如下命令:
cat /proc/kmsg > /mnt/data/ker.log & 2>&1
用后台进程将日志导入到文件。