当前位置: 首页 > news >正文

Linux Kernel调试:强大的printk(三)

前言

前面两篇我们介绍了printk的基本用法和pr_xxx的用法:

Linux Kernel调试:强大的printk(一)

Linux Kernel调试:强大的printk(二)

本篇我们来介绍开发驱动程序时用到的dev_xxx,以及限制输出的频率

dev_xxx

内核在include/linux/dev_printk.h中同样定义了一系列的dev_xxx:

/** #defines for all the dev_<level> macros to prefix with whatever* possible use of #define dev_fmt(fmt) ...*/#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, ...)                                                \dev_no_printk(KERN_DEBUG, dev, dev_fmt(fmt), ##__VA_ARGS__)
#endif

(这里不再对这些宏进行展开了,需要你自己去查看内核源码)

这些函数本质上也是对 printk 的封装,但相比 printk,它们具有以下优点:

  • 它们都有一个dev参数,这使得它们支持打印模块信息和设备信息:可以自动将设备名称等信息添加到打印信息的前头,便于定位问题。

  • 支持动态调试:可以动态选择打开或关闭某个内核子系统或模块的输出,更加灵活。

我们还是以代码示例来进行演示,写一个虚拟设备的驱动程序,可到这里获取:https://gitee.com/coolloser/linux-kerenl-debug

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/uaccess.h>#define DEVICE_NAME "vdev"
#define CLASS_NAME "vclass"static int major;
static struct class *vdev_class;
static struct cdev vdev_cdev;
static struct device *vdev_device; // 新增设备指针// 设备数据结构
struct vdev_data {char *buffer;size_t size;struct device *dev; // 每个设备实例保存设备指针
};// 打开设备
static int vdev_open(struct inode *inode, struct file *file)
{struct vdev_data *data;data = kmalloc(sizeof(struct vdev_data), GFP_KERNEL);if (!data) {pr_err("Failed to allocate memory\n");return -ENOMEM;}data->buffer = kmalloc(1024, GFP_KERNEL);if (!data->buffer) {pr_err("Failed to allocate buffer\n");kfree(data);return -ENOMEM;}data->size = 0;data->dev = vdev_device; // 保存设备指针file->private_data = data;dev_info(data->dev, "Device opened\n");return 0;
}// 释放设备
static int vdev_release(struct inode *inode, struct file *file)
{struct vdev_data *data = file->private_data;if (data) {kfree(data->buffer);kfree(data);}dev_info(vdev_device, "Device closed\n");return 0;
}// 读取设备
static ssize_t vdev_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{struct vdev_data *data = file->private_data;if (*pos >= data->size)return 0;if (copy_to_user(buf, data->buffer + *pos, data->size - *pos))return -EFAULT;*pos += data->size - *pos;dev_dbg(data->dev, "Read %zu bytes\n", data->size);return data->size;
}// 写入设备
static ssize_t vdev_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{struct vdev_data *data = file->private_data;if (count > 1024) {dev_warn(data->dev, "Write size too big: %zu\n", count);return -EFBIG;}if (copy_from_user(data->buffer, buf, count))return -EFAULT;data->size = count;*pos = count;dev_info(data->dev, "Received %zu bytes\n", count);return count;
}// 文件操作结构体
static const struct file_operations vdev_fops = {.owner = THIS_MODULE,.open = vdev_open,.release = vdev_release,.read = vdev_read,.write = vdev_write,
};// 模块初始化
static int __init vdev_init(void)
{dev_t dev;int ret;// 动态分配主设备号ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);if (ret < 0) {pr_err("Failed to allocate device number\n");return ret;}major = MAJOR(dev);// 创建设备类(修正参数)vdev_class = class_create(CLASS_NAME);if (IS_ERR(vdev_class)) {unregister_chrdev_region(dev, 1);return PTR_ERR(vdev_class);}// 初始化字符设备cdev_init(&vdev_cdev, &vdev_fops);vdev_cdev.owner = THIS_MODULE;// 添加字符设备ret = cdev_add(&vdev_cdev, dev, 1);if (ret < 0) {class_destroy(vdev_class);unregister_chrdev_region(dev, 1);return ret;}// 创建设备节点并保存设备指针vdev_device = device_create(vdev_class, NULL, dev, NULL, DEVICE_NAME);if (IS_ERR(vdev_device)) {cdev_del(&vdev_cdev);class_destroy(vdev_class);unregister_chrdev_region(dev, 1);return PTR_ERR(vdev_device);}dev_info(vdev_device, "Device initialized\n");return 0;
}// 模块退出
static void __exit vdev_exit(void)
{dev_t dev = MKDEV(major, 0);device_destroy(vdev_class, dev);class_destroy(vdev_class);cdev_del(&vdev_cdev);unregister_chrdev_region(dev, 1);pr_info("Device unloaded\n");
}module_init(vdev_init);
module_exit(vdev_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple virtual device driver demo");

(看不懂不要紧,我们只是关注其中的dev_xxx调用)

# 定义模块名称
MODULE_NAME := dev_xxx# 定义内核构建目录,替换成你自己的路径
KERNEL_BUILD_DIR := /home/leo/debug_kernel/linux-6.12.28# 定义目标文件
obj-m += $(MODULE_NAME).occflags-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

执行make进行编译,然后加载模块,通过dmesg查看log:

能看到log前面加入了vclass_vdev

然后我们进行如下操作:

sudo mknod /dev/vdev c 240 0
sudo chmod 666 /dev/vdev
sudo echo "test" > /dev/vdev
sudo cat /dev/vdev

再执行dmesg查看log:

同样也插入了设备信息

限制输出的频率

有时候在一个函数中进行打印以方便查看log,但是这个函数被无限次调用,就会输出一堆信息,不但影响我们定位问题,还会对内核的运行产生影响,所以内核定义了如下接口:

#ifdef CONFIG_PRINTK
#define printk_ratelimited(fmt, ...)                                        \
({                                                                        \static DEFINE_RATELIMIT_STATE(_rs,                                \DEFAULT_RATELIMIT_INTERVAL,        \DEFAULT_RATELIMIT_BURST);                \\if (__ratelimit(&_rs))                                                \printk(fmt, ##__VA_ARGS__);                                \
})
#else
#define printk_ratelimited(fmt, ...)                                        \no_printk(fmt, ##__VA_ARGS__)
#endif#define pr_emerg_ratelimited(fmt, ...)                                        \printk_ratelimited(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)
#define pr_alert_ratelimited(fmt, ...)                                        \printk_ratelimited(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_crit_ratelimited(fmt, ...)                                        \printk_ratelimited(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_err_ratelimited(fmt, ...)                                        \printk_ratelimited(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warn_ratelimited(fmt, ...)                                        \printk_ratelimited(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
#define pr_notice_ratelimited(fmt, ...)                                        \printk_ratelimited(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
#define pr_info_ratelimited(fmt, ...)                                        \printk_ratelimited(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
/* no pr_cont_ratelimited, don't do that... */// 精简后的
#define pr_devel_ratelimited(fmt, ...)                                        \printk_ratelimited(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)// 精简后的
#define pr_debug_ratelimited(fmt, ...)                                        \printk_ratelimited(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)

以及

#define dev_emerg_ratelimited(dev, fmt, ...)                                \dev_level_ratelimited(dev_emerg, dev, fmt, ##__VA_ARGS__)
#define dev_alert_ratelimited(dev, fmt, ...)                                \dev_level_ratelimited(dev_alert, dev, fmt, ##__VA_ARGS__)
#define dev_crit_ratelimited(dev, fmt, ...)                                \dev_level_ratelimited(dev_crit, dev, fmt, ##__VA_ARGS__)
#define dev_err_ratelimited(dev, fmt, ...)                                \dev_level_ratelimited(dev_err, dev, fmt, ##__VA_ARGS__)
#define dev_warn_ratelimited(dev, fmt, ...)                                \dev_level_ratelimited(dev_warn, dev, fmt, ##__VA_ARGS__)
#define dev_notice_ratelimited(dev, fmt, ...)                                \dev_level_ratelimited(dev_notice, dev, fmt, ##__VA_ARGS__)
#define dev_info_ratelimited(dev, fmt, ...)                                \dev_level_ratelimited(dev_info, dev, fmt, ##__VA_ARGS__)// 精简后的
#define dev_dbg_ratelimited(dev, fmt, ...)                                \
do {                                                                        \static DEFINE_RATELIMIT_STATE(_rs,                                \DEFAULT_RATELIMIT_INTERVAL,        \DEFAULT_RATELIMIT_BURST);                \if (__ratelimit(&_rs))                                                \dev_printk(KERN_DEBUG, dev, dev_fmt(fmt), ##__VA_ARGS__); \
} while (0)

同样的,在内核编程中推荐使用pr_xxx_ratelimited,驱动程序中使用dev_xxx_ratelimited

这些函数会记录上次打印的时间,并与当前时间进行比较。如果两次打印的时间间隔小于预设的阈值则不会打印当前的消息,从而避免在短时间内重复打印大量相同或相似的消息。同时还能控制在每个时间间隔内允许打印的最大次数。

这里就牵涉到两个参数:

  • 时间间隔,单位是秒

  • 时间间隔内允许打印的最大次数

可以有三种方式控制这两个参数:

  1. 在启动内核时,通过参数进行控制:

printk_ratelimit=2 printk_ratelimit_burst=5
  1. 直接修改内核代码

#define DEFAULT_RATELIMIT_INTERVAL 2 /* 2 seconds */
#define DEFAULT_RATELIMIT_BURST 5 /* 5 messages */
  1. 通过/proc/sys/kernel目录下的printk_ratelimit和printk_ratelimit_burst文件进行控制

$ cat /proc/sys/kernel/printk_ratelimit
5$ cat /proc/sys/kernel/printk_ratelimit_burst 
10

下面我们通过代码演示pr_xxx_ratelimited的使用:

// 定义模块的格式化字符串
#define pr_fmt(fmt) "%s:%s():%d: " fmt, KBUILD_MODNAME, __func__, __LINE__#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/delay.h>static int print_num = 12; 
module_param(print_num, int, 0644);
MODULE_PARM_DESC(print_num, "Number of print");// 定义模块加载函数
static int __init pr_fmt_init(void)
{int i;pr_info("====printk模块加载成功!====\n");for (i = 0; i < print_num; i++) {pr_info_ratelimited("print count = %d\n", i); mdelay(100);}   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_ratelimited# 定义内核构建目录,替换成你自己的路径
KERNEL_BUILD_DIR := /home/leo/debug_kernel/linux-6.12.28# 定义目标文件
obj-m += $(MODULE_NAME).occflags-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

执行make进行编译,然后sudo insmod pr_ratelimited.ko加载模块,使用dmesg查看信息:

可以看到pr_info_ratelimited就打印了10次,而我们print_num是12,这是因为/proc/sys/kernel/printk_ratelimit_burst 文件中的值是10

我们先卸载该模块sudo rmmod pr_ratelimited,然后加载模块时将print_num的值修改为100,再看一下现象:

sudo insmod pr_ratelimited.ko print_num=100

执行dmesg,查看log:

可以看到每次输出10条,中间被抑制了40条,而我们程序中每次延时100ms,(40+10)* 100ms = 5s,这正好就是

/proc/sys/kernel/printk_ratelimit中的值

只打印一次

此外内核还定义了如下一些函数:

#ifdef CONFIG_PRINTK
#define printk_once(fmt, ...)                                        \DO_ONCE_LITE(printk, fmt, ##__VA_ARGS__)
#define printk_deferred_once(fmt, ...)                                \DO_ONCE_LITE(printk_deferred, fmt, ##__VA_ARGS__)
#else
#define printk_once(fmt, ...)                                        \no_printk(fmt, ##__VA_ARGS__)
#define printk_deferred_once(fmt, ...)                                \no_printk(fmt, ##__VA_ARGS__)
#endif#define pr_emerg_once(fmt, ...)                                        \printk_once(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)
#define pr_alert_once(fmt, ...)                                        \printk_once(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_crit_once(fmt, ...)                                        \printk_once(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_err_once(fmt, ...)                                        \printk_once(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warn_once(fmt, ...)                                        \printk_once(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
#define pr_notice_once(fmt, ...)                                \printk_once(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
#define pr_info_once(fmt, ...)                                        \printk_once(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
/* no pr_cont_once, don't do that... */#if defined(DEBUG)
#define pr_devel_once(fmt, ...)                                        \printk_once(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define pr_devel_once(fmt, ...)                                        \no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif/* If you are writing a driver, please use dev_dbg instead */
#if defined(DEBUG)
#define pr_debug_once(fmt, ...)                                        \printk_once(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define pr_debug_once(fmt, ...)                                        \no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif

#define dev_emerg_once(dev, fmt, ...)                                        \dev_level_once(dev_emerg, dev, fmt, ##__VA_ARGS__)
#define dev_alert_once(dev, fmt, ...)                                        \dev_level_once(dev_alert, dev, fmt, ##__VA_ARGS__)
#define dev_crit_once(dev, fmt, ...)                                        \dev_level_once(dev_crit, dev, fmt, ##__VA_ARGS__)
#define dev_err_once(dev, fmt, ...)                                        \dev_level_once(dev_err, dev, fmt, ##__VA_ARGS__)
#define dev_warn_once(dev, fmt, ...)                                        \dev_level_once(dev_warn, dev, fmt, ##__VA_ARGS__)
#define dev_notice_once(dev, fmt, ...)                                        \dev_level_once(dev_notice, dev, fmt, ##__VA_ARGS__)
#define dev_info_once(dev, fmt, ...)                                        \dev_level_once(dev_info, dev, fmt, ##__VA_ARGS__)
#define dev_dbg_once(dev, fmt, ...)                                        \dev_level_once(dev_dbg, dev, fmt, ##__VA_ARGS__)

这些函数不管调用多少次,都只会打印一次消息。即便在多线程环境中也只会打印一次。

通过使用这些函数,你可以更有效地控制内核日志的输出,避免不必要的重复信息,使日志更加清晰和易于管理。

总结

printk相关的用法还有很多,这里不可能面面俱到,只讲一些常用的用法,后面还有讲到动态打印,在实际debug过程中有很大用处。

http://www.xdnf.cn/news/637759.html

相关文章:

  • Kotlin Native与C/C++高效互操作:技术原理与性能优化指南
  • 论文审稿之我对SCI写作的思考
  • 聊一聊接口测试如何设计有效的错误响应测试用例
  • Multivalued Dependencies
  • CMake指令:find_package()
  • 【HarmonyOS5】DevEco Studio 使用指南:代码阅读与编辑功能详解
  • Java 接口
  • Flink 常用算子详解与最佳实践
  • PySide6 GUI 学习笔记——常用类及控件使用方法(常用图像类)
  • 运维Linux之Ansible详解学习(更新中)
  • 【linux篇】系统世界跳跃的音符:指令
  • SheetMetal_Unfold方法 FreeCAD_SheetMetal deepwiki 源码笔记
  • 【时时三省】Python 语言----牛客网刷题笔记
  • 【电路笔记】-音频变压器(Audio Transformer)
  • RAG系统构建之嵌入模型性能优化完整指南
  • 永磁同步电机控制算法--IP调节器
  • 前端面试热门知识点总结
  • MongoDB分布式架构详解:复制与分片的高可用与扩展之道
  • 【Vue3】(二)vue3语法详解:自定义泛型、生命周期、Hooks、路由
  • C51单片机学习笔记——矩阵按键
  • 【硬件测试】基于FPGA的BPSK+卷积编码Viterbi译码系统开发,包含帧同步,信道,误码统计,可设置SNR
  • 平流层通信系统的深度论述:其技术成熟将推动通信范式从“地面-卫星”二元架构向“地-空-天”三维融合跃迁
  • Linux初始-历史(1)
  • Java并发编程:全面解析锁策略、CAS与synchronized优化机制
  • 关于 Web 安全:5. 认证绕过与权限控制分析
  • L1-110 这不是字符串题 - java
  • Magic Resume:开源免费的AI简历制作应用(使用指南、场景分析)
  • 网络基础学习
  • TTL和死信交换机实现延迟队列
  • 测试工程师如何通俗理解和入门RAG:从“查资料”到“写答案”的智能升级