Linux Kernel 编程-你不知道的printk(2)

news2024/12/23 22:33:04

在这里插入图片描述

内核版本:6.1+

书接上回:Linux Kernel 编程-你不知道的printk(1):https://mp.weixin.qq.com/s/TIuxhG3b-KBYXzrDYy__Aw

上回我们介绍了:

  • printk()的简单使用
  • pintk 的实现:ring buffer
  • 使用 systemd 命令 journalctl 查看日志
  • printk 日志级别
  • printk 替代者 pr_<foo>

下面我们继续介绍一些 printk 在实际工程中的一些使用细节。

1. printk 输出到控制台 console

回想一下,printk 日志一般输出到三个位置:

  • 第一个是内核内存日志缓冲区(默认输出)
  • 第二种是非易失性磁盘/文件(systemd-journal )
  • 最后一个是控制台设备 (可配置)

控制台设备是纯粹的内核特性,即用户在非图形界面环境中登录的初始终端窗口( /dev/console)。在 Linux 中,我们可以定义多个控制台:终端 tty 窗口(如 /dev/console)、文本模式 VGA 控制台、帧缓冲区,甚至是通过 USB 提供的串口(在嵌入式系统开发期间很常见)。

例如,当我们通过 USB 转 RS232 TTL UART(USB 转串扣)线将 linux 开发板连接到 x86_64 笔记本电脑时,然后使用终端仿真软件(如 putty、SecureCRT等),输入 tty 命令,可以查看对应的设置。

# tty
/dev/ttyS0

控制台通常是比较重要的日志,包括来自内核底层的日志打印。 Linux 的 printk 使用 sysctl 机制,将其日志传送到控制台设备。为了更好地理解这一点,我们首先查看相关的 proc 信息(这里,在我们的 x86_64 Ubuntu 虚拟机):

$ cat /proc/sys/kernel/printk
4	4	1	7

前面的四个数字为 printk 日志级别(0 表示最高,7 表示最低)。前面的四个整数序列的含义是这样的:

  • 当前控制台日志级别。含义是所有小于该值的日志都将被发送到控制台设备。
  • 缺乏显式日志级别的默认级别。
  • 允许的最低日志级别。
  • 启动时默认日志级别。

我们可以看到日志级别4对应的是KERN_WARNING。因此,第一个数字为 4(实际上,Linux 发行版上的典型默认值),所有低于日志级别 4 的 printk 除了被记录到内核日志缓冲区和文件外,还将被发送到控制台设备。实际上,这些设置,适用于以下日志级别的所有内核消息:KERN_EMERG、KERN_ALERT、KERN_CRIT 和 KERN_ERR 默认情况下也会自动发送到控制台设备。

现在将 /proc/sys/kernel/printk 文件的第一个整数值设置为 8 ,保证所有 printk 也直接发送到控制台,从而使 printk 的行为就像常规 printf 一样!在这里,需要使用 root 用户进行设置:

$ sudo sh -c "echo '8 4 1 7' > /proc/sys/kernel/printk"
$ cat /proc/sys/kernel/printk
8	4	1	7

然而,通过 proc 的设置是暂时的,一旦系统重新启动,所做的更改就会丢失。如果要想进行永久设置,则需要通过 sysctl 命令进行设置。同样,需要以 root 身份运行命令:

$ sudo sysctl -w kernel.printk='8 4 1 7'

2. 使用 printk 动态打印日志

作为程序员,我们需要调试技术。根据日志,我们可以了解代码的执行流程,从而可以发现错误。对于 Linux 内核,那些以日志级别 KERN_DEBUG 就是所谓的调试打印,内核中可以通过一些函数进行 “DEBUG” 级别日志输出:

pr_debug("<fmt str>"[, args…]);
dev_dbg(dev, "<fmt str>"[, args…]);
printk(KERN_DEBUG "<fmt str>"[, args…]);

printk DEBUG 级别在默认情况下始终处于关闭状态,只有定义了 “DEBUG” 后,它们才真正生效。

这似乎很方便,但是,在实际生产环境中,如果需要从给定模块查看一些调试打印,该怎么办?有一种方法是定义符号 DEBUG(可以在 Makefle 中定义)、重新编译驱动、卸载并重新加载驱动模块。然而,这种方法在大多数生产系统上并不实用(甚至不允许)。因此,我们需要一种更加动态的方法,这正是内核的动态调试功能所提供的。

通过内核动态调试功能,日志级别为 KERN_DEBUG 的每个 printk 输出都会被编译到内核中。在 make menuconfig 中,动态调试选项位于:

Kernel hacking > printk and dmesg options > Enable dynamic printk() support

confg 配置为 CONFIG_DYNAMIC_DEBUG ,我们假设此配置已经启用(设置为y)。

要检查内核的动态调试是否可用(在 x86 上),如果显示为 y 则可用,否则不可用:

$ grep "DYNAMIC_DEBUG=y" /boot/config-$(uname -r)
CONFIG_DYNAMIC_DEBUG=y

接下来,我们使用动态调试的接口是 debugfs 文件系统:<debugfs_mount_point>/dynamic_debug/control。然而,在许多生产系统上,debugfs 可能未配置(或保持为“不可见”);在这种情况下,通过配置 proc : /proc/dynamic_debug/control 也能实现相同的效果。

查看 control 文件内容,会显示编译到内核中的所有调试打印或调用点:

$ head -n1 /proc/dynamic_debug/control
# filename:lineno [module]function flags format

第一行显示格式,除 flags 的字段外,其它字段含义都是不言而喻的。

例如,让我们查找 virtio_net 驱动程序(模块)的一些调试打印调用点:

$ grep "virtio_net" /proc/dynamic_debug/control | head -n 3
drivers/net/virtio_net.c:3361 [virtio_net]virtnet_probe =_ "virtnet: registered device %s with %d RX and TX vq's\012"
drivers/net/virtio_net.c:3338 [virtio_net]virtnet_probe =_ "virtio_net: registering cpu notifier failed\012"
drivers/net/virtio_net.c:3327 [virtio_net]virtnet_probe =_ "virtio_net: registering device failed\012"

可以看到调试打印已关闭(“=_” 表示关闭)。要打开它,需要将值 +p 写入标志字段。还可以配置其他几个可选值:m、f、l 和 t。

下面是一些示例,在不同级别的范围内运行动态调试工具。首先确保满足基本先决条件 (CONFIG_DYNAMIC_DEBUG=y)。另外,可以用路径 /proc/dynamic_debug/control 替换下面使用的 /sys/kernel/debug/dynamic_debug/control 的文件路径:

  • 开启路径名包含字符串“virtio_net”的所有文件中的所有调试消息
echo -n 'file *virtio_net* +p' > /sys/kernel/debug/dynamic_debug/control 

在另一个终端中使用 journalctl -k -f 查看有大量日志输出:

$ journalctl -k -f
kernel: Sent skb 00000000ed86f85a
kernel: ens3: xmit 00000000e94cce87 cc:d8:1f:1f:be:1b
kernel: Sent skb 00000000e94cce87
kernel: Receiving skb proto 0x0800 len 46 type 0
kernel: Receiving skb proto 0x0800 len 46 type 0
kernel: Receiving skb proto 0x8100 len 81 type 1
kernel: Receiving skb proto 0x0800 len 136 type 0
kernel: ens3: xmit 000000005dc578b3 cc:d8:1f:1f:be:1b

使用 -p 将其关闭:

echo -n 'file *virtio_net* -p' > /sys/kernel/debug/dynamic_debug/control 
  • 启用所有 printk DEBUG 输出(所有 pr_debug()|dev_dbg() 调用点)

    Note:谨慎使用,这会导致非常大量内核日志输出

echo -n 'module * +pflmt' > /sys/kernel/debug/dynamic_debug/control

关闭输出:

echo -n 'module * -pflmt' > /sys/kernel/debug/dynamic_debug/control

其中 flags:pflmt 含义如下:

static struct { unsigned flag:8; char opt_char; } opt_array[] = {
	{ _DPRINTK_FLAGS_PRINT, 'p' },
	{ _DPRINTK_FLAGS_INCL_MODNAME, 'm' },
	{ _DPRINTK_FLAGS_INCL_FUNCNAME, 'f' },
	{ _DPRINTK_FLAGS_INCL_LINENO, 'l' },
	{ _DPRINTK_FLAGS_INCL_TID, 't' },
	{ _DPRINTK_FLAGS_NONE, '_' },
};

内核文档中有更多示例,参见:https://www.kernel.org/doc/html/v6.1/admin-guide/dynamic-debug-howto.html#examples。

3. 限制 printk 的输出速率

如果频繁执行的代码路径中,有 printk 打印时,输出可能很快就会溢出内核日志缓冲区(在内存中,它是一个循环缓冲区),从而可能覆盖关键信息。除此之外,不断增长的磁盘日志文件无限地重复几乎相同的内核日志消息也很浪费磁盘空间。试想,中断处理程序代码路径中的 printk,如果以 100 Hz 的频率(即每秒 100 次)调用硬件中断会怎样?

为了缓解这些问题,内核提供了一个有趣且有用的替代方案:rate-limited printk 。 pr_<foo>_ratelimited() 宏(其中 foo 是 emerg、alert、crit、err、warn、notice、info 或 debug 之一)与常规 printk 具有相同的语法;当输出太频繁的时候,它可以有效地抑制打印速率。

内核提供了两个名为 printk_ratelimit 和 printk_ratelimit_burst 的 sysctl 文件。我们直接查看sysctl文档(https://www.kernel.org/doc/Documentation/sysctl/kernel.txt)来了解确切的含义:

printk_ratelimit:
Some warning messages are rate limited. printk_ratelimit specifies
the minimum length of time between these messages (in jiffies), by
default we allow one every 5 seconds.
A value of 0 will disable rate limiting.
==============================================
printk_ratelimit_burst:
While long term we enforce one message per printk_ratelimit
seconds, we do allow a burst of messages to pass through.
printk_ratelimit_burst specifies the number of messages we can
send before ratelimiting kicks in.

在 x86_64 Ubuntu 22.04 LTS 上,它们的默认值如下:

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

这意味着默认情况下,在 5 秒时间间隔内发生的同一消息最多有10 个打印。printk ratelimit 在抑制内核 printk 输出时,会显示一条有用的消息,说明有多少早期的 printk 输出被抑制。

ratelimit_test_init: 41 callbacks suppressed
ratelimit_test:ratelimit_test_init():45: [51] ratelimited printk @ KERN_INFO
[6]

内核提供以下宏替代 printk,可以起到限制打印/日志记录的速率的作用:

printk_ratelimited(): Warning! Do not use it – the kernel warns against this.

pr_*_ratelimited(): Where the wildcard * is replaced by the usual – one of emerg, alert, crit, err, warn, notice, info, or debug.

dev_*_ratelimited(): Where the wildcard * is replaced by the usual – one of emerg, alert, crit, err, warn, notice, info, or debug.

优先使用 pr_*_ratelimited() 宏而不是 printk_ratelimited();驱动程序应使用 dev_*_ratelimited() 宏。

4. 从用户空间输出内核日志

我们经常使用的一种流行的调试技术是在代码中的各个点进行打印,通常可以让我们缩小问题的根源。这确实是一种有用的调试技术,更正式地称为检测代码。内核开发人员经常使用古老的 printk API 来达到这个目的。

假设我们已经编写了一个内核模块,并且正在对其进行调试(通过在代码中的适当位置添加多个 printk 打印),这些输出可以在运行时通过 dmesg (或journalctl)看到它们。

这样很好,但是,如果正在运行一些用户空间测试脚本,并希望通过打印出特定消息来查看脚本和内核模块的执行顺序,该怎么办?比如,假设我们希望日志看起来像这样:

test_script: @user msg 1
kernel_module: msg n, msg n+1, ..., msg n+m 
test_script: @user msg 2 ...

我们可以让用户空间测试脚本将消息写入内核日志缓冲区,就像内核 printk 一样,通过将所述消息写入特殊的 /dev/kmsg 设备文件来实现:

echo "test_script: @user msg 1" > /dev/kmsg
$ dmesg -w
[55527.523756] test_script: @user msg 1

dmesg 有多个选项,使输出更易于阅读,我们可以为dmesg 设置一个别名:

$ alias dmesg='sudo dmesg --decode --nopager --color --ctime'
$ dmesg -w
kern :debug : [Mon Jul 10 08:14:32 2023] wlo1: Limiting TX power to 30 (30 -
0) dBm as advertised by b8:<...>
user :info : [Mon Jul 10 08:25:27 2023] test_script: test msg at KERN_INFO

5. 通过 pr_fmt 宏格式化 printk 输出

通常,我们需要为 printk() 提供上下文输出,例如:它到底发生在哪里?当然我们可以编写这样的代码,利用各种 GCC 宏,如 __FILE____func__ __LINE__,来输出文件名、函数名和行号,标记打印从哪里发生的:

pr_info("%s:%s:%s():%d: mywork XY failed!\n", OURMODNAME, __FILE__, __func__, __LINE__);

问题是,如果项目中有很多 printk 打印,那么保证标准 printk 格式可能会很痛苦(例如,首先显示模块名称,后跟文件名,然后是函数名称,可能还有行号),很难确保项目中所有开发者一致且正确的遵循。

pr_fmt()宏就是为了解决这个问题的,在代码的开头定义这个宏(它必须在第一个 #include) 之前,保证代码中的每个后续 printk 都将被预设使用此宏指定的格式。

/**
... 
*/

#define pr_fmt(fmt) "%s:%s(): " fmt, KBUILD_MODNAME, __func__

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

static int __init helloworld_init(void)
{
	pr_info("inserted\n");
	 ... 
}

pr_fmt() 宏使用预定义的 KBUILD_MODNAME 宏来替换内核模块的名称,并使用 GCC __func__ 说明符来显示我们当前正在运行的函数的名称。还可以为 __FILE__ 添加 %s,或者 __LINE__ 宏匹配的 %d 以显示行号。

现在,我们在这个内核模块的 init 函数中打印的 pr_info() 将在内核日志中显示如下:

[381534.391966] helloworld:helloworld_init(): inserted

pr_fmt() 宏非常有用,在内核中,有大量源文件以 pr_fmt() 开头。pr_fmt() 还会通过 dev_*() 宏影响驱动 printk 输出。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1821756.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机视觉全系列实战教程:(九)图像滤波操作

1.图像滤波的概述 (1)Why (为什么要进行图像滤波) 去噪&#xff1a;去除图像在获取、传输等过程中的各种噪音干扰提取特征&#xff1a;使用特定的图像滤波器提取图像特定特征 (2)What (什么是图像滤波) 使用滤波核对图像进行卷积运算或非线性运算&#xff0c;以达到去噪或提…

经典的网站系统架构(入门级)

从开发到部署&#xff0c;从用户访问到底层数据库&#xff0c;介绍搭建网站系统的经典架构的10个核心部分。 &#xff08;图转自bytebytego&#xff0c;翻译整理by dogstar&#xff09; 1、使用Git管理和协同源代码&#xff0c;通过CI/CD或Git的Webhook方式自动同步更新部署到服…

Python开源项目周排行 2024年第9周

#2024年第9周2024年6月3日1buku强大的浏览器书签管理工具。这是一款开源的书签命令行管理工具&#xff0c;它轻量、隐私安全且易于使用&#xff0c;支持从主流浏览器导入书签、自动获取书签信息、跨平台同步和强大的搜索功能。2flagsmith轻松管理功能开关和配置的平台。这是一个…

计算机网络(6) UDP协议

一.UDP数据报格式 UDP&#xff08;User Datagram Protocol&#xff0c;用户数据报协议&#xff09;是一种简单的传输层协议&#xff0c;与TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;相比&#xff0c;UDP提供一种无连接、不可靠的数据传…

【建议收藏】技术人必看:如何选择适合你公司的消息队列工具

我是小米,一个喜欢分享技术的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货! 大家好,我是小米,一个充满活力、喜欢分享技术的程序员。今天我想和大家聊聊消息队列这个主题。对于许多开发者来说,消息队列并不是一个陌生的概念,…

Docker部署常见应用之企业级堡垒机JumpServer-问题记录

文章目录 项目场景问题1问题描述原因分析解决方案 问题2问题描述原因分析解决方案 参考文章 项目场景 项目场景&#xff1a;Docker部署常见应用之企业级堡垒机JumpServer 问题1 问题描述 docker-compose.yml 中使用 $SECRET_KEY 和 $BOOTSTRAP_TOKEN加载 ~/.bashrc 环境变量…

2024 年最新 Python 使用 gewe 框架搭建微信机器人实现语音智能回复(详细教程)

Gewe 个微框架 GeWe&#xff08;个微框架&#xff09;是一个创新性的软件开发框架&#xff0c;专注于IPAD协议&#xff0c;为个人微信号以及企业信息安全提供了强大的功能和保障。GeWe的设计旨在简化开发过程&#xff0c;使开发者能够高效、灵活地构建和定制通信协议&#xff…

C++ 35 之 对象模型基础

#include <iostream> #include <string.h> using namespace std;class Students05{ public:// 只有非静态成员变量才算存储空间&#xff0c;其他都不算int s_a; // 非静态成员变量&#xff0c;算对象的存储空间double s_c;// 成员函数 不算对象的存储空间void f…

1毛钱1百万token,写2遍红楼梦!国产大模型下一步还想卷什么?

大模型价格战&#xff0c;这匹国产黑马又破纪录了&#xff01;最低的GLM-4 Flash版本&#xff0c;百万token价格已经低至0.1元&#xff0c;可以说是击穿地心。MaaS 2.0大升级&#xff0c;让企业训练私有模型的成本无限降低。 刚刚&#xff0c;智谱AI开放日上&#xff0c;新一代…

神经网络字符分类

按照题目要求修改了多层感知机 题目将图片的每个点作为输入&#xff0c;其中大小为28*28&#xff0c;中间有两个大小为100的隐藏层&#xff0c;激活函数是relu&#xff0c;然后输出大小是10&#xff0c;激活函数是softmax 优化器是Adam&#xff0c;结合了AdaGrad和RMSProp算法…

六、高级路由交换技术

目录 一、Eth-trunk&#xff08;以太通道或链路捆绑&#xff09; 1.1、 链路聚合模式 1.2、链路选举规则&#xff08;选举活跃和备份&#xff09; 1.3、负载分担方式 1.4、配置流程 二、vlan聚合 三、MUX vlan&#xff08;混合vlan&#xff09; 四、QinQ 五、V…

一个顶级产品经理的自我修养,从掌控AI工具开始

前言 在数字化浪潮的推动下&#xff0c;人工智能&#xff08;AI&#xff09;技术的快速发展正深刻地改变着各行各业的运营模式与竞争格局。产品经理&#xff0c;作为连接用户需求与产品设计之间的桥梁&#xff0c;在这场变革中扮演着至关重要的角色。随着AI技术的广泛应用&…

实战计算机网络02——物理层

实战计算机网络02——物理层 1、物理层实现的功能2、数据与信号2.1 数据通信模型2.2 通信领域常用术语2.3 模拟信号和数字信号 3、信道和调制3.1 信道3.2 单工通信、半双工通信、全双工通信3.3 调制3.4 奈式准则3.5 香农定律 4、传输媒体4.1 导向传输媒体4.2 非导向传输媒体 5、…

二刷算法训练营Day30 | 回溯算法(6/6)

目录 详细布置&#xff1a; 1. 回溯总结 2. 332. 重新安排行程 3. 51. N 皇后 4. 37. 解数独 详细布置&#xff1a; 1. 回溯总结 回溯是递归的副产品&#xff0c;只要有递归就会有回溯&#xff0c;所以回溯法也经常和二叉树遍历&#xff0c;深度优先搜索混在一起&#x…

KafkaQ - 好用的 Kafka Linux 命令行可视化工具

软件效果前瞻 ~ 鉴于并没有在网上找到比较好的linux平台的kafka可视化工具&#xff0c;今天为大家介绍一下自己开发的在 Linux 平台上使用的可视化工具KafkaQ 虽然简陋&#xff0c;主要可以实现下面的这些功能&#xff1a; 1&#xff09;查看当前topic的分片数量和副本数量 …

docker通过容器id查看运行命令

1、docker通过容器id查看运行命令 参考&#xff1a;https://blog.csdn.net/a772304419/article/details/138732138 docker inspect 运行镜像id“Cmd”: [ “–model”, “/qwen-7b”, “–port”, “10860”, “–max-model-len”, “4096”, “–trust-remote-code”, “–t…

LabVIEW 32位与64位版本比较分析:性能与兼容性详解

LabVIEW的32位和64位版本在功能、性能、兼容性和应用场景等方面存在差异。本文从系统要求、内存管理、性能、兼容性、驱动支持和开发维护等多个角度进行详细分析&#xff0c;帮助用户选择合适的版本。 一、系统要求 操作系统支持&#xff1a; 32位LabVIEW&#xff1a;可以在32位…

深入解析MySQL的层次化设计

一、基础架构 1.连接器 1.会先连接到这个数据库上&#xff0c;这时候接待你的就是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接 2.用户密码连接成功之后&#xff0c;会从权限表中拿出你的权限&#xff0c;后续操作权限都依赖于此时拿出的权限,这就意味着当链…

springboot项目中使用 @Lazy 注解懒加载解决循环依赖问题,以及 @Lazy 标注顺序

场景&#xff1a; Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name taskServiceImpl: Bean with name taskServiceImpl has been injected into other beans [groupServiceImpl] in its raw version as part…

Application Studio 学习笔记(1)

一、导航树 1、设置AAA的Page Type属性需设置为Tab(注意&#xff1a;有多个Tab类型Page时导航树会失效&#xff0c;并且设置为Tab后&#xff0c;该Page将不能编辑)&#xff0c;并勾选Enable Navigation&#xff0c;其中AAA为导航树起始页的父页。 2、导航树起始页及其子页的Ta…