----------------------------------------------------------------------------------------------------------------------------
内核版本:linux 5.2.8
根文件系统:busybox 1.25.0
u-boot:2016.05
----------------------------------------------------------------------------------------------------------------------------
一、printk介绍
我们在学习C语言的时候,经常使用printf函数将内容输出到控制台,printf 是格式化输出函数,主要功能是向标准输出设备按规定格式输出信息。printf是C语言标准库函数,定义于头文件 <stdio.h>。
而在linux内核中是无法使用printf 函数的,取而代之的是printk函数。printk在内核源码中用来记录日志信息的函数,只能在内核源码范围内使用,用法类似于printf函数。
一个较大的差别在于printk支持多种日志级别,从而允许printk根据消息的等级选择性进行打印。
printk函数主要做两件事情:
- 将信息记录日志文件中(一般为/var/log/message);
- 调用控制台驱动来将信息输出到控制台;
1.1 日志缓冲区
printk将内核日志输出到内核日志缓冲区中,缓冲区定义在在kernel/printk/printk.c文件中:
/* record buffer */
#define LOG_ALIGN __alignof__(struct printk_log)
#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)
#define LOG_BUF_LEN_MAX (u32)(1 << 31)
static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);
static char *log_buf = __log_buf;
static u32 log_buf_len = __LOG_BUF_LEN;
内核日志缓冲区__log_buf的大小为$2^{CONFIG\_LOG\_BUF\_SHIFT}$ ,CONFIG_LOG_BUF_SHIFT默认为17,即128KB,可以通过make menuconfig配置,对应的配置项为LOG_BUF_SHIFT。
用户态可以通过syslog相关的系统调用或者/proc文件以及/dev/kmsg设备节点来查看__log_buf的信息,这些操作都是通过do_syslog的系统调用接口来实现的。
1.2 日志级别
日志级别用来控制printk打印的这条信息是否在控制台上显示,linux内核共提供了8种不同的日志级别,分为级别 0~7。数值越大,表示级别越低,对应的消息越不重要。
linux内核日志级别相应的宏定义在include/linux/kern_levels.h 文件中。
#define KERN_EMERG KERN_SOH "0" /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
#define KERN_DEFAULT "" /* the default kernel loglevel */
各个日志级别含义如下:
- KERN_EMERG 表示紧急事件,一般是系统崩溃之前提示的消息;
- KERN_ALERT 表示必须立即采取行动的消息;
- KERN_CRIT 表示临界状态,通常涉及严重的硬件或软件操作失败;
- KERN_ERR 用于报告错误状态,设备驱动程序会经常使用该级别来报告来自硬件的问题;
- KERN_WARNING 对可能出现问题的情况进行警告,这类情况通常不会对系统造成严重的问题;
- KERN_NOTICE 表示有必要进行提示的正常情形,许多与安全相关的状况用这个级别进行汇报;
- KERN_INFO 表示内核提示信息,很多驱动程序在启动的时候,用这个级别打印出它们找到的硬件信息;
- KERN_DEBUG 用于调试信息;
1.3 函数使用
在使用printk时,会将日志级别放到最开始的位置,使用方式如下:
printk(log_level "message...");
例如:
printk(KERN_EMERG "GetIot: KERN_EMERG\n");
printk(KERN_ALERT "GetIot: KERN_ALERT\n");
printk(KERN_CRIT "GetIot: KERN_CRIT\n");
printk(KERN_ERR "GetIot: KERN_ERR\n");
printk(KERN_WARNING "GetIot: KERN_WARNING\n");
printk(KERN_NOTICE "GetIot: KERN_NOTICE\n");
printk(KERN_INFO "GetIot: KERN_INFO\n");
printk(KERN_DEBUG "GetIot: KERN_DEBUG\n");
若未设置日志级别,printk默认使用内核定义的全局变量default_message_loglevel作为的默认打印的日志级别。
当printk中的消息日志级别小于当前控制台日志级别时,printk要打印的信息才会在控制台打印出来,否则不会显示在控制台。
printk的用法与printf基本一致, 最大的区别是printk对%p的扩展,在应用层使用printf打印一个指针的地址时直接使用%p即可, 但是内核为了防止泄漏内存布局的敏感信息,直接使用%p输出的地址是经过hash处理的, 如果想实现和printf的%p一样的效果, printk需要使用%px。
1.4 查看日志
无论当前控制台日志级别是何值,即使没有在控制台打印出来,都可以通过下面两种方法查看日志:
- 第一种是使用dmesg命令查看日志;
- 第二种是通过cat /proc/kmsg来查看日志;
另外如果配置好并运行了syslogd 或klogd,没有在控制台上显示的printk的信息也会追加到/var/log/messages.log中。
1.4.1 dmesg
执行dmesg查看日志:
[root@zy:/]# dmesg
Booting Linux on physical CPU 0x0
Linux version 5.2.8 (root@zhengyang) (gcc version 4.8.3 20140320 (prerelease) (Sourcery CodeBench Lite 2014.05-29)) #23 Sun Mar 26 14:53:14 CST 2023
...
1.4.2 /proc/kmsg
执行cat /proc/kmsg查看日志:
[root@zy:/]# cat /proc/kmsg
<6>Booting Linux on physical CPU 0x0
<5>Linux version 5.2.8 (root@zhengyang) (gcc version 4.8.3 20140320 (prerelease) (Sourcery CodeBench Lite 2014.05-29)) #23 Sun Mar 26 14:53:14 CST 2023
...
1.5 调整日志级别
linux系统支持在运行时,通过proc文件系统查看和调整内核日志的输出等级。查看当前控制台的打印级别的命令如下:
[root@zy:/]# cat /proc/sys/kernel/printk
7 4 1 7
该文件有4个数字值,含义如下:
- 控制台日志级别:优先级高于该值的消息将被打印至控制台;
- 默认的消息日志级别:将用该优先级来打印没有优先级的消息(即 printk 没有指定消息级别);
- 最低的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级);
- 默认的控制台日志级别:控制台日志级别的缺省值,如果没有指定控制台日志级别,则使用这个;
这四个值是在 kernel/printk/printk.c 中被定义的,如下:
int console_printk[4] = {
CONSOLE_LOGLEVEL_DEFAULT, /* console_loglevel */
MESSAGE_LOGLEVEL_DEFAULT, /* default_message_loglevel */
CONSOLE_LOGLEVEL_MIN, /* minimum_console_loglevel */
CONSOLE_LOGLEVEL_DEFAULT, /* default_console_loglevel */
};
EXPORT_SYMBOL_GPL(console_printk);
修改日志级别有两种方式,配置menuconfig和修改/proc/sys/kernel/printk文件。
1.5.1 配置menuconfig
修改CONFIG_MESSAGE_LOGLEVEL_DEFAULT的值,然后重新编译,更新内核。menuconfig配置路径如下:
Kernel hacking --->
printk and dmesg options --->
(4) Default message log level (1-7)
如下图所示:
1.5.2 在系统中修改
在系统运行期间,可以通过执行以下命令,修改当前控制态的日志级别:
echo "新的打印级别 4 1 7" > /proc/sys/kernel/printk
如屏蔽掉所有的内核printk打印,只需要把第一个数值调到最小值1或者0,此时可以敲如下命令:
echo "1 4 1 7" > /proc/sys/kernel/printk
1.5.3 打开调试日志
内核中的大部分驱动都使用了dev_dbg接口打印调试信息,日志级别为7,默认是不会输出到控制台的。在需要打印dev_dbg调试信息的驱动文件开头定义DEBUG宏,注意必须是在include/linux/device.h前面:
#define DEBUG
打开DEBUG宏是第一步,这个时候还是不能输出到控制台的,还必须要修改控制台日志级别为8。
二、其他常见用法
为了方便开发者使用,在linux内核中除了直接使用printk加消息级别的方式,内核还提供了pr_xx和dev_xx系列的打印接口,本质上,它们都是基于printk实现的。其中:
- pr_xx系列函数简化了日志级别的使用;
- dev_xx系列函数可以用于设备驱动程序中,便于打印设备相关的信息;
2.1 pr_xx系列函数
在 <linux/printk.h> 中定义了 pr_notice、pr_info、pr_warn、pr_err 等接口。使用这些 pr_xxx 接口,就可以省去指定消息级别的麻烦。
#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__)
#ifdef DEBUG
#define pr_devel(fmt, ...) \
printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define pr_devel(fmt, ...) \
no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif
#if defined(CONFIG_DYNAMIC_DEBUG)
#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_xx定义,都是对printk+日志级别的封装,故都能和prink一样在linux内核中直接使用;
但是对于pr_devel需要在定义了DEBUG宏,pr_debug需要在定义了DEBUG或CONFIG_DYNAMIC_DEBUG宏才有实意。
2.2 dev_xx系列函数
在 <linux/dev_printk.h> 里也提供了一些驱动模型诊断宏,例如dev_err、dev_warn、dev_info等等。使用它们,不仅可以按标记的消息级别打印,还会打印对应的设备和驱动信息,这对于驱动调试来说相当重要。
#define dev_emerg(dev, fmt, ...) \
dev_printk_index_wrap(_dev_emerg, KERN_EMERG, dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_crit(dev, fmt, ...) \
dev_printk_index_wrap(_dev_crit, KERN_CRIT, dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_alert(dev, fmt, ...) \
dev_printk_index_wrap(_dev_alert, KERN_ALERT, dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_err(dev, fmt, ...) \
dev_printk_index_wrap(_dev_err, KERN_ERR, dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_warn(dev, fmt, ...) \
dev_printk_index_wrap(_dev_warn, KERN_WARNING, dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_notice(dev, fmt, ...) \
dev_printk_index_wrap(_dev_notice, KERN_NOTICE, dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_info(dev, fmt, ...) \
dev_printk_index_wrap(_dev_info, KERN_INFO, dev, dev_fmt(fmt), ##__VA_ARGS__)
#if defined(CONFIG_DYNAMIC_DEBUG) || \
(defined(CONFIG_DYNAMIC_DEBUG_CORE) && defined(DYNAMIC_DEBUG_MODULE))
#define dev_dbg(dev, fmt, ...) \
dynamic_dev_dbg(dev, dev_fmt(fmt), ##__VA_ARGS__)
#elif defined(DEBUG)
#define dev_dbg(dev, fmt, ...) \
dev_printk(KERN_DEBUG, dev, dev_fmt(fmt), ##__VA_ARGS__)
#else
#define dev_dbg(dev, fmt, ...) \
({ \
if (0) \
dev_printk(KERN_DEBUG, dev,