linux内核调试-printk

发布时间 2023-03-26 20:49:32作者: 大奥特曼打小怪兽

----------------------------------------------------------------------------------------------------------------------------

内核版本: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中定义:

#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);

内核日志缓冲区__log_buf的大小为$2^{CONFIG\_LOG\_BUF\_SHIFT}$ ,CONFIG_LOG_BUF_SHIFT默认为17,即128KB,可以通过make menuconfig配置,对应的配置项为LOG_BUF_SHIFT。

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要打印的信息才会在控制台打印出来,否则不会显示在控制台。

但无论当前控制台日志级别是何值,即使没有在控制台打印出来,都可以通过下面两种方法查看日志:

  • 第一种是使用dmesg命令查看日志;
  • 第二种是通过cat /proc/kmsg来查看日志;

另外如果配置好并运行了syslogd 或klogd,没有在控制台上显示的printk的信息也会追加到/var/log/messages.log中。

1.3.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.3.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.4 调整日志级别

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.4.1 配置menuconfig

修改CONFIG_MESSAGE_LOGLEVEL_DEFAULT的值,然后重新编译,更新内核。menuconfig配置路径如下:

Kernel hacking  --->
    printk and dmesg options  --->
        (4) Default message log level (1-7)

如下图所示:

1.4.2 在系统中修改

在系统运行期间,可以通过执行以下命令,修改当前控制态的日志级别:

echo "新的打印级别  4    1    7" > /proc/sys/kernel/printk

如屏蔽掉所有的内核printk打印,只需要把第一个数值调到最小值1或者0,此时可以敲如下命令:

echo "1       4       1      7" > /proc/sys/kernel/printk

二、其他常见用法

为了方便开发者使用,在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, dev_fmt(fmt), ##__VA_ARGS__); \
})
#endif

如上dev_xx定义,其和pr_xx类似,都可以直接理解为printk+日志级别的封装,一般在linux驱动程序中使用;

同样需要注意,dev_dbg也不是无条件打印的,除非定义了DEBUG或设定了CONFIG_DYNAMIC_DEBUG。一个相关约定是在已经开启了 DEBUG 时,使用VERBOSE_DEBUG来添加dev_vdbg。

2.3 测试

目前在内核驱动代码中,都不再建议直接使用printk直接添加打印信息,而是使用dev_info、dev_dbg、dev_err之类的函数代替,虽然这些 dev_xxx 函数的本质还是使用printk打印的,但是相比起printk,dev_xxx 的好处是:

  • 支持打印模块信息、dev信息;
  • 支持动态调试(dynamic debug)方式;

比如,如下代码:

dev_warn(&rtc->dev, "invalid alarm value: %ptR\n", &alarm->time);

输出日志如下:

rtc rtc0: invalid alarm value: 1900-02-01T00:00:00

参考文章

[1]34.Linux-printk分析、使用__FILE__, __FUNCTION__, __LINE__ 调试

[2]Linux内核之 printk 打印(转)

[3]内核printk原理介绍