Linux内核开发技巧

最佳Linux内核开发实践的灵感来自于现有的内核代码。通过这种方式,您当然可以学习到好的例程。也就是说,我们不会白费力气(重复造轮子)。我们将重点讨论本章所必需的内容,即调试。最常用的调试方法包括记录和打印。为了利用这种经过时间检验的调试技术,Linux内核提供了合适的日志APIs,并公开了一个内核消息缓冲区来存储日志。虽然看起来很简单,但我们将重点关注内核日志APIs,并学习如何从内核代码中或从用户空间管理消息缓冲区。

信息打印

消息打印和日志记录是开发所固有的,无论我们是在内核空间还是用户空间。在内核中,printk()函数一直是事实上的内核消息打印函数。它类似于C库中的printf(),但具有日志级别的概念。

如果你看一个实际的驱动程序代码的例子,你会注意到它是这样使用的:

printk(<LOG_LEVEL> "printf like formatted message\n");

这里,LOG_LEVEL是include/linux/kern_levels.h中定义的八个不同日志级别之一,它指定了错误消息的严重程度。需要注意的是,日志级别和格式字符串之间没有逗号(因为预处理器将这两个字符串连接在一起)。

内核日志级别

Linux内核使用级别的概念来确定消息的关键程度。一共有8个,每个都定义为一个字符串,它们的描述如下:

  • KERN_EMERG,定义为“0”。它用于紧急消息,这意味着系统即将崩溃或不稳定(不可用)。
  • KERN_ALERT,定义为“1”,表示发生了不好的事情,必须立即采取行动。
  • KERN_CRIT,定义为“2”,表示发生了危急情况,例如严重的硬件/软件故障。
  • KERN_ERR,定义为“3”,在错误条件下使用,通常由驱动程序使用,表示硬件有困难或与子系统交互失败。
  • KERN_WARNING,定义为“4”,用作警告,本身并不严重,但可能表示有问题。
  • KERN_NOTICE,定义为“5”,表示不严重,但仍然值得注意。这通常用于报告安全事件。
  • KERN_INFO,定义为“6”,用于信息消息,例如驱动程序初始化时的启动信息。
  • KERN_DEBUG,定义为“7”,用于调试目的,仅在启用DEBUG内核选项时才激活。否则,它的内容将被忽略。

如果您没有在消息中指定日志级别,它默认为DEFAULT_MESSAGE_LOGLEVEL(通常是“4”= KERN_WARNING),可以通过CONFIG_DEFAULT_MESSAGE_LOGLEVEL内核配置选项设置。

也就是说,对于新的驱动程序,鼓励你使用更方便的打印APIs,这些APIs将日志级别嵌入到它们的名称中。这些打印帮助程序是pr_emerg、pr_alert、pr_crit、pr_err、pr_warning、pr_warn、pr_notice、pr_info、pr_debug或pr_dbg。除了比等效的printk()调用更简洁之外,它们还可以通过pr_fmt()宏使用一个通用的格式化字符串定义;例如,在源文件的顶部定义以下宏(在任何#include指令之前):

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

这将在该文件中的每个pr_*()消息前加上产生该消息的模块和函数名。如果内核是以DEBUG编译的,pr_devel和pr_debug将被printk(KERN_DEBUG…)替换,否则它们将被替换为空语句。

pr_*()家族宏将用于核心代码中。对于设备驱动程序,你应该使用与设备相关的帮助程序,它们也接受相关的设备结构作为参数。它们还以标准形式打印相关设备的名称,确保始终可以将消息与生成该消息的设备相关联:

dev_emerg(const struct device *dev, const char *fmt, ...);
dev_alert(const struct device *dev, const char *fmt, ...);
dev_crit(const struct device *dev, const char *fmt, ...);
dev_err(const struct device *dev, const char *fmt, ...);
dev_warn(const struct device *dev, const char *fmt, ...);
dev_notice(const struct device *dev, const char *fmt, ...);
dev_info(const struct device *dev, const char *fmt, ...);
dev_dbg(const struct device *dev, const char *fmt, ...);

虽然内核使用日志级别的概念来确定消息的重要性,但它还用于决定是否应该立即将该消息显示给用户,方法是将其打印到当前控制台(其中控制台也可以是串口,甚至是打印机,而不是xterm)。

为了做出决定,内核将消息的日志级别与console_loglevel内核变量进行比较,如果消息日志级别的重要性高于console_loglevel(即较低的值),则消息将被打印到当前控制台。由于默认的内核日志级别通常是“4”,这就是为什么在控制台上看不到pr_info()或pr_notice()甚至pr_warn()消息的原因,因为它们的值高于或等于默认值(这意味着优先级较低)。

要确定系统上当前的控制台日志级别,您可以简单地键入以下命令:

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

第一个整数(4)是当前控制台日志级别,第二个数字(4)是默认级别,第三个数字(1)是可以设置的最低控制台日志级别,第四个数字(7)是启动时默认的控制台日志级别。

要更改当前的console_log级别,只需将其写入相同的文件,即/proc/sys/kernel/printk。因此,为了获得打印到控制台的所有消息,执行以下简单的命令:

# echo 8 > /proc/sys/kernel/printk

每个内核消息都将显示在控制台上。然后你会有以下内容:

# cat /proc/sys/kernel/printk
8 4 1 7

另一种改变控制台日志级别的方法是使用带-n参数的dmesg:

# dmesg -n 5

使用上述命令,将console_loglevel设置为打印KERN_WARNING(4)或更严重的消息。您还可以在boot时使用loglevel引导参数指定console_loglevel(有关详细信息,请参阅Documentation/kernel-parameters.txt)。

还有KERN_CONT和pr_cont,它们有点特殊,因为它们不指定紧急级别,而是指示持续消息。它们应该只在早期启动期间被core/arch代码使用(否则,连续的行不是SMP安全的)。当要打印的部分消息行依赖于计算结果时,这很有用,如下例所示:

[…]
pr_warn("your last operation was ");
if (success)
  pr_cont("successful\n");
else
  pr_cont("NOT successful\n");

我们应该记住,只有最后的打印语句具有结尾的\n字符。

Kernel log buffer

无论是否立即打印到控制台上,每个内核消息都被记录在缓冲区中。这个内核消息缓冲区是一个固定大小的循环缓冲区,这意味着如果缓冲区被填满,它就会被环绕,您可能会丢失一条消息。因此,增加缓冲区大小可能会有所帮助。为了改变内核消息缓冲区的大小,您可以使用LOG_BUF_SHIFT选项,该选项的值将左移1以获得最终的大小,即内核日志缓冲区的大小(例如, 16 => 1<<16 => 64KB, 17 => 1 << 17 => 128KB)。也就是说,它是在编译时定义的静态大小。这个大小也可以通过内核引导参数(bootargs)定义,使用log_buf_len参数,换句话说,log_buf_len=1M(只接受2的n次幂大小的值)。

添加时间信息

有时,向打印的消息添加计时信息是有用的,这样您就可以看到特定事件发生的时间。内核包含一个特性,称为printk times,通过CONFIG_PRINTK_TIME选项启用。在配置内核时,可以在Kernel Hacking菜单中找到该选项。一旦启用,这个定时信息就会在每条日志消息前面添加如下信息:

$ dmesg
[…]
[ 1.260037] loop: module loaded
[ 1.260194] libphy: Fixed MDIO Bus: probed
[ 1.260195] tun: Universal TUN/TAP device driver, 1.6
[ 1.260224] PPP generic driver version 2.4.2
[ 1.260260] ehci_hcd: USB 2.0 'Enhanced' Host Controller
(EHCI) Driver
[ 1.260262] ehci-pci: EHCI PCI platform driver
[ 1.260775] ehci-pci 0000:00:1a.7: EHCI Host Controller
[ 1.260780] ehci-pci 0000:00:1a.7: new USB bus registered,
assigned bus number 1
[ 1.260790] ehci-pci 0000:00:1a.7: debug port 1
[ 1.264680] ehci-pci 0000:00:1a.7: cache line size of 64 is
not supported
[ 1.264695] ehci-pci 0000:00:1a.7: irq 22, io mem 0xf7ffa000
[ 1.280103] ehci-pci 0000:00:1a.7: USB 2.0 started, EHCI
1.00
[ 1.280146] usb usb1: New USB device found, idVendor=1d6b,
idProduct=0002
[ 1.280147] usb usb1: New USB device strings: Mfr=3,
Product=2, SerialNumber=1
[…]

插入到内核消息输出中的时间戳由秒和微秒(秒.微秒)组成,作为从机器操作开始(或从内核计时开始)的绝对值,这对应于引导加载程序将控制传递给内核的时间(当您在控制台上看到“[0.000000] Booting Linux on physical CPU 0x0”时)。

Printk时间可以在运行时通过写入/sys/module/printk/parameters/time来控制,以便启用和禁用printk时间戳。以下是一些例子:

# echo 0 >/sys/module/printk/parameters/time
# cat /sys/module/printk/parameters/time
N
# echo 1 >/sys/module/printk/parameters/time
# cat /sys/module/printk/parameters/time
Y

它不控制是否记录时间戳。它只控制在转储内核消息缓冲区时、在引导时或在使用dmesg时是否打印它。这可能是引导时间优化的一个领域。如果禁用,打印日志所需的时间会更短。

我们现在已经熟悉了内核打印APIs及其日志缓冲区。我们已经了解了如何调整消息缓冲区,以及如何根据需求添加或删除信息。这些技能可以用于通过打印进行调试。除此之外,Linux内核中还提供了其他调试和跟踪工具。

posted @ 2023-02-07 15:33  闹闹爸爸  阅读(357)  评论(0编辑  收藏  举报