前言
如果你对printk的基本用法还不熟悉,请先阅读:
Linux Kernel调试:强大的printk(一)
上一篇Linux Kernel调试:强大的printk(一)我们介绍了printk的基础知识和基本用法,了解了printk的一些特性,比如支持输出级别等,不过这是最基础的用法,在实际项目中,一般不会直接这样用,本篇就来讲一下printk的进阶用法。
如果对pr_xxx已经熟悉了,可以直接阅读后面的内容:
Linux Kernel调试:强大的printk(三):dev_xxx相关的内容,以及限制打印速率
pr_xxx
使用printk需要指定日志级别,虽然也可以不指定级别(使用默认级别),但是在实际使用中还是推荐每次调用都最好指定级别,不过这样一来就很麻烦,且不够清晰,所以内核在include/linux/printk.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_warn(fmt, ...) \
printk(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
#define pr_notice(fmt, ...) \
printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
#define pr_info(fmt, ...) \
printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
#define pr_cont(fmt, ...) \
printk(KERN_CONT fmt, ##__VA_ARGS__)
// 这里是从源码中精简出来的
#define pr_debug(fmt, ...) \
printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
由这些定义也可以看出,pr_xxx对应于printk的不同级别,例如pr_emerg就等价于KERN_EMERG级别的printk,这样我们在内核编程时,或者写内核模块时,就可以直接使用pr_xxx来输出log了,也是推荐使用这种方式。
我们把上一篇Linux Kernel调试:强大的printk(一)的示例代码替换成pr_xxx即可进行实验,效果和上一篇一样,这里不在赘述。
这里着重讲一下上面代码中的pr_fmt,这是一个宏,默认定义也位于include/linux/printk.h:
/**
* pr_fmt - used by the pr_*() macros to generate the printk format string
* @fmt: format string passed from a pr_*() macro
*
* This macro can be used to generate a unified format string for pr_*()
* macros. A common use is to prefix all pr_*() messages in a file with a common
* string. For example, defining this at the top of a source file:
*
* #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
*
* would prefix all pr_info, pr_emerg... messages in the file with the module
* name.
*/
#ifndef pr_fmt
#define pr_fmt(fmt) fmt
#endif
这个宏的作用就是为我们要打印的信息生成统一的格式字符串。从上面的默认定义也可以看出,如果之前没有定义pr_fmt那就将pr_fmt(fmt)定义为fmt,所以如果我们要使用pr_fmt来统一打印的格式,就需要在包含include/linux/printk.h之前定义pr_fmt,下面我们通过示例代码来演示pr_fmt宏的作用,可到这里获取https://gitee.com/coolloser/linux-kerenl-debug:
// 定义模块的格式化字符串
#define pr_fmt(fmt) "%s:%s():%d: " fmt, KBUILD_MODNAME, __func__, __LINE__
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
// 定义模块加载函数
static int __init pr_fmt_init(void)
{
pr_info("====printk模块加载成功!====\n");
pr_debug("这是一个DEBUG级别的信息\n");
pr_info("这是一个INFO级别的信息\n");
pr_warn("这是一个WARNING级别的信息\n");
pr_err("这是一个ERROR级别的信息\n");
pr_crit("这是一个CRITICAL级别的信息\n");
pr_alert("这是一个ALERT级别的信息\n");
pr_emerg("这是一个EMERGENCY级别的信息\n");
// 使用KERN_CONT继续上一条日志消息
pr_info("这是一条需要继续的信息...");
pr_cont("...这是继续的部分\n");
return 0; // 返回0表示模块加载成功
}
// 定义模块卸载函数
static void __exit pr_fmt_exit(void)
{
pr_info("====printk模块卸载成功!====\n");
}
// 注册模块加载和卸载函数
module_init(pr_fmt_init);
module_exit(pr_fmt_exit);
// 模块信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("your_name");
MODULE_DESCRIPTION("pr_fmt内核模块示例");
MODULE_VERSION("0.1");
# 定义模块名称
MODULE_NAME := pr_fmt
# 定义内核构建目录,替换成你自己的路径
KERNEL_BUILD_DIR := /home/leo/debug_kernel/linux-6.12.28
# 定义目标文件
obj-m += $(MODULE_NAME).o
# 默认目标
all:
@echo "Building the $(MODULE_NAME) kernel module..."
$(MAKE) -C $(KERNEL_BUILD_DIR) M=$(PWD) modules
# 清理目标
clean:
@echo "Cleaning up the build environment..."
$(MAKE) -C $(KERNEL_BUILD_DIR) M=$(PWD) clean
执行make
进行编译,然后执行sudo insmod pr_fmt.ko
加载模块,之后执行dmesg
查看log信息:
可以看到,在每一条log之前都加入了[模块名称]:[函数名]:[行号]
所以使用pr_fmt,一处定义,多处使用,我们就可以很方便的添加我们需要的信息了,这里还是要再提醒一下,定义pr_fmt一定要在包含include/linux/printk.h之前,或者在模块源码文件的最开始定义。
pr_debug和pr_devel
在内核源码中,pr_debug和pr_devel的定义和其他几个pr_xxx不同:
pr_debug的定义如下:
/* If you are writing a driver, please use dev_dbg instead */
#if defined(CONFIG_DYNAMIC_DEBUG) || \
(defined(CONFIG_DYNAMIC_DEBUG_CORE) && defined(DYNAMIC_DEBUG_MODULE))
#include <linux/dynamic_debug.h>
/**
* pr_debug - Print a debug-level message conditionally
* @fmt: format string
* @...: arguments for the format string
*
* This macro expands to dynamic_pr_debug() if CONFIG_DYNAMIC_DEBUG is
* set. Otherwise, if DEBUG is defined, it's equivalent to a printk with
* KERN_DEBUG loglevel. If DEBUG is not defined it does nothing.
*
* It uses pr_fmt() to generate the format string (dynamic_pr_debug() uses
* pr_fmt() internally).
*/
#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
可以看到,如果配置了动态debug则pr_debug定义为了dynamic_pr_debug(关于动态打印以后会单独介绍),如果定义了DEBUG则pr_debug定义为了KERN_DEBUG级别的printk,否则pr_debug就是no_printk
pr_devel定义如下:
/**
* pr_devel - Print a debug-level message conditionally
* @fmt: format string
* @...: arguments for the format string
*
* This macro expands to a printk with KERN_DEBUG loglevel if DEBUG is
* defined. Otherwise it does nothing.
*
* It uses pr_fmt() to generate the format string.
*/
#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
可以看到如果定义了DEBUG,pr_devel和pr_debug一样也是KERN_DEBUG级别的printk,否则也是no_printk
所以我们如果要打印debug信息优先使用pr_debug,尽量不要使用pr_devel,因为pr_debug可以使用动态打印,pr_devel则没有使用动态打印,一旦我们不处于debug模式,就没法再查看对应log了。
pr_debug的打印去哪了
不知道你有没有发现,我们上面的示例代码中明明有:
pr_debug("这是一个DEBUG级别的信息\n");
但是在运行结果中,却没有这句打印:
那pr_debug的打印去哪了?其实从pr_debug的定义中我们也能发现问题,因为我们编译内核模块时没有定义DEBUG宏,所以pr_debug现在是no_printk,自然就没有打印出来了,下面我们修改Makefile使编译选项定义DEBUG宏,注意其中的ccflags-y += -DDEBUG
:
# 定义模块名称
MODULE_NAME := pr_fmt
# 定义内核构建目录,替换成你自己的路径
KERNEL_BUILD_DIR := /home/leo/debug_kernel/linux-6.12.28
# 定义目标文件
obj-m += $(MODULE_NAME).o
ccflags-y += -DDEBUG
# 默认目标
all:
@echo "Building the $(MODULE_NAME) kernel module..."
$(MAKE) -C $(KERNEL_BUILD_DIR) M=$(PWD) modules
# 清理目标
clean:
@echo "Cleaning up the build environment..."
$(MAKE) -C $(KERNEL_BUILD_DIR) M=$(PWD) clean
现在再编译加载模块,执行dmesg查看:
终于打印出来了^_^
其实,代码中,在包含include/linux/printk.h之前通过#define DEBUG
也可以让pr_debug打印出来,这里不再演示,需要你自己动手~
总结
本文重点介绍了pr_xxx的特点以及用法,pr_xxx是对printk的包装,但是其含义更加明确,使用更加方便,所以我们在实际开发中,优先使用pr_xxx进行log输出,还需要特别注意的是pr_debug,这个在我们开发阶段很有用处,且在后面我们介绍的动态打印中也有很大的用处
下一篇我们介绍dev_xxx:
Linux Kernel调试:强大的printk(三)