Linux 内核日志系统—printk的机制与应用

1.内核代码
linux 4.9.88:

kernel/printk.c📎printk.c
include/linux/kernel.h📎kernel.h
kernel/printk/internal.h📎internal.h
在 Linux 内核开发中,**printk** 是最常用的调试工具之一,主要用于输出内核日志信息。printk 不仅类似用户空间的 printf,还提供了日志级别管理功能,通过指定日志级别(如 KERN_WARNING),可以精准控制日志信息的输出范围和重要性。本文系统梳理了 printk 的使用方法、日志级别管理、日志信息的存储位置,以及输出过程的核心机制。通过解析源码和示例,详细展示了如何利用 console_loglevel 等宏调控日志输出行为,并剖析了内核是如何通过 call_console_drivers 将日志发送到不同设备。

2.printk的使用

2.1 使用实例
调试内核、驱动的最简单方法,是使用printk函数打印信息。

printk函数与用户空间的printf函数格式完全相同,它所打印的字符串头部可以加入“\001n”样式的字符。

其中n为0~7,表示这条信息的记录级别,n数值越小级别越高。

注意:linux 2.x内核里,打印级别是用""来表示。

在驱动程序中,可以这样使用printk:

printk("This is an example\n");
printk("\0014This is an example\n");
printk("\0014""This is an example\n");
printk(KERN_WARNING"This is an example\n");

在上述例子中:

第一条语句没有明确表明打印级别,它被处理前内核会在前面添加默认的打印级别:“<4>”
KERN_WARNING是一个宏,它也表示打印级别:
#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */

现在知道了,内核的每条打印信息都有自己的级别,当自己的级别在数值上小于某个阈值时,内核才会打印该信息。

2.2 printk的记录级别
在内核代码include/linux/kernel.h中,下面几个宏确定了printk函数怎么处理打印级别:

#define console_loglevel (console_printk[0])
#define default_message_loglevel (console_printk[1])
#define minimum_console_loglevel (console_printk[2])
#define default_console_loglevel (console_printk[3])

举例说明这几个宏的含义:

① 对于printk(“……”),只有n小于console_loglevel时,这个信息才会被打印。

② 假设default_message_loglevel的值等于4,如果printk的参数开头没有“”样式的字符,则在printk函数中进一步处理前会自动加上“<4>”;

③ minimum_console_logleve是一个预设值,平时不起作用。通过其他工具来设置console_loglevel的值时,这个值不能小于minimum_console_logleve。

④ default_console_loglevel也是一个预设值,平时不起作用。它表示设置console_loglevel时的默认值,通过其他工具来设置console_loglevel的值时,用到这个值。

理解(原)
这些宏定义用于简化对 console_printk 数组的访问,该数组用于控制Linux内核的日志输出级别。每个宏代表了一个不同的日志级别或设置。具体含义如下:

console_loglevel:当前控制台的日志级别,决定了内核日志中哪些信息会输出到控制台。
default_message_loglevel:内核日志消息的默认日志级别。
minimum_console_loglevel:控制台日志输出的最低允许级别。
default_console_loglevel:系统启动时的默认控制台日志级别。
举例说明:
假设 console_printk 数组的值为 [4, 3, 1, 7]:

int console_printk[] = {4, 3, 1, 7};
现在基于宏定义:

console_loglevel(console_printk[0]):表示当前控制台的日志级别为4。通常,日志级别为4意味着只会显示警告及以上级别的消息(0-4级)。
示例:console_loglevel = 4,控制台会显示KERN_EMERG、KERN_ALERT、KERN_CRIT、KERN_ERR和KERN_WARNING的消息,但KERN_NOTICE及以下的消息不会显示。
default_message_loglevel(console_printk[1]):表示内核消息的默认日志级别为3。即,如果没有显式指定消息的日志级别,那么这些消息将按照3级别处理。
示例:default_message_loglevel = 3,默认情况下,内核会处理消息为KERN_ERR级别。
minimum_console_loglevel(console_printk[2]):表示控制台允许输出的最低日志级别为1。这意味着即使console_loglevel较高,也不会输出低于1级别的消息。
示例:minimum_console_loglevel = 1,控制台会输出KERN_EMERG和KERN_ALERT级别的消息,不会输出KERN_DEBUG或KERN_INFO级别的消息。
default_console_loglevel(console_printk[3]):系统启动时的默认控制台日志级别为7。这表示系统启动时控制台会显示所有级别的日志(0-7级别)。
示例:default_console_loglevel = 7,启动时,控制台显示包括KERN_DEBUG和KERN_INFO的所有日志信息。
日志级别(从高到低):
0: KERN_EMERG(紧急)
1: KERN_ALERT(警报)
2: KERN_CRIT(严重)
3: KERN_ERR(错误)
4: KERN_WARNING(警告)
5: KERN_NOTICE(通知)
6: KERN_INFO(信息)
7: KERN_DEBUG(调试)
实际应用场景:
当系统运行时,你可以通过更改 console_loglevel 来控制哪些日志消息输出到控制台。例如,设置 console_loglevel 为 3 后,控制台将只显示错误及以上的日志消息。
console_loglevel = 3;
系统启动时,default_console_loglevel 决定了启动时控制台输出的日志级别。如果你希望在系统启动时看到所有日志,可以将其设置为 7。
通过这些宏,开发者能够灵活控制日志输出,避免过多不必要的信息占用控制台,同时确保关键的错误和警报被记录和显示。

上面代码中,console_printk是一个数组,它在kernel/printk.c中定义:

/* 数组里的宏在include/linux/printk.h中定义
*/
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 */
};

/* Linux 4.9.88 include/linux/printk.h */
#define CONSOLE_LOGLEVEL_DEFAULT 7 /* anything MORE serious than KERN_DEBUG */
#define MESSAGE_LOGLEVEL_DEFAULT CONFIG_MESSAGE_LOGLEVEL_DEFAULT
#define CONSOLE_LOGLEVEL_MIN 1 /* Minimum loglevel we let people use */

2.3 在用户空间修改printk函数的记录级别
挂接proc文件系统后,读取/proc/sys/kernel/printk文件可以得知console_loglevel、default_message_loglevel、minimum_console_loglevel和default_console_loglevel这4个值。

比如执行以下命令,它的结果“7 4 1 7”表示这4个值:

也可以直接修改/proc/sys/kernel/printk文件来改变这4个值,比如:

# echo "1 4 1 7" > /proc/sys/kernel/printk
1
这使得console_loglevel被改为1,于是所有的printk信息都不会被打印。

2.4 printk函数记录级别的名称及使用
在内核代码include/linux/kernel.h中,有如下代码,它们表示0~7这8个记录级别的名称:

#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_SOH_ASCII '\001'

#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 */

在使用printk函数时,可以这样使用记录级别;

printk(KERN_WARNING”there is a warning here!\n”)
1
3 printk执行过程
3.1 函数调用过程
在嵌入式Linux开发中,printk信息常常从串口输出,这时串口被称为串口控制台。从内核kernel/printk.c的printk函数开始,往下查看它的调用关系,可以知道printk函数是如何与具体设备的输出函数挂钩的。

printk函数调用的子函数的主要脉落如下:

printk
// linux 4.9: kernel/printk/internal.h
// linux 5.4: kernel/printk/printk_safe.c
vprintk_func
vprintk_default(fmt, args);
vprintk_emit
vprintk_store // 把要打印的信息保存在log_buf中
log_output

preempt_disable();
if (console_trylock_spinning())
console_unlock();
preempt_enable();

console_unlock
for (;;) {

msg = log_from_idx(console_idx);
if (suppress_message_printing(msg->level)) {
/* 如果消息的级别数值大于console_loglevel, 则不打印此信息 */
}

printk_safe_enter_irqsave(flags);
call_console_drivers(ext_text, ext_len, text, len);
printk_safe_exit_irqrestore(flags);
}

call_console_drivers函数调用驱动程序打印信息,此函数在kernel\printk\printk.c中,代码如下:

这个函数 call_console_drivers() 用于将日志信息发送到所有注册的控制台驱动程序(console drivers),从而实现日志的实际输出。内核日志系统中的消息被记录在 log_buf 中,而该函数负责调用控制台驱动程序,将这些消息写到终端或串口等输出设备上。

static void call_console_drivers(int level,
const char *ext_text, size_t ext_len,
const char *text, size_t len)

函数参数:

level:日志级别,表示消息的严重性,如KERN_ERR、KERN_WARNING等。
ext_text:扩展文本(可选的额外日志信息),这是扩展格式的日志内容指针。
ext_len:扩展文本的长度。
text:常规日志内容的指针,指向 log_buf 中要写到控制台的日志内容。
len:常规日志内容的长度。
\1. trace_console_rcuidle(text, len);

调用了跟踪函数 trace_console_rcuidle,它通常用于内核中的调试和性能跟踪。rcuidle 表示即使在 RCU(Read-Copy-Update)空闲期间,也可以追踪日志输出情况。此处记录了日志信息的内容及其长度,便于后续分析日志的写入过程。
\2. if (!console_drivers) return;

检查是否存在任何控制台驱动程序(console_drivers),如果没有控制台驱动程序可用,则直接返回,什么都不做。
\3. for_each_console(con)

遍历系统中所有已注册的控制台驱动。con 是每次循环中的一个控制台驱动指针。内核通过控制台系统可以支持多种类型的输出设备,比如显示器、串口、虚拟终端等。
\4. if (exclusive_console && con != exclusive_console) continue;

检查是否有设置“独占”控制台(exclusive_console)。如果当前控制台不是被设置为独占的控制台,则跳过该控制台,继续循环。独占控制台意味着只允许输出到指定的控制台。
\5. if (!(con->flags & CON_ENABLED)) continue;

检查当前控制台的 flags 中是否包含 CON_ENABLED 标志位。如果该标志位没有设置,说明该控制台没有启用,函数继续下一次循环。

CON_ENABLED:表示控制台启用状态。
\6. if (!con->write) continue;

检查控制台是否有定义 write 函数。如果控制台驱动未定义 write 函数,则无法输出日志,继续循环。每个控制台驱动都应该提供一个 write 函数,负责将日志写到设备上(如显示器、串口等)。
\7. if (!cpu_online(smp_processor_id()) && !(con->flags & CON_ANYTIME)) continue;

检查当前处理器是否在线(cpu_online(smp_processor_id())),以及当前控制台是否允许在非在线CPU上运行(con->flags & CON_ANYTIME)。

CON_ANYTIME:允许在处理器非在线状态时仍然输出日志。这是一些早期引导过程中或紧急情况下需要输出日志的场景。
\8. if (con->flags & CON_EXTENDED)

检查控制台是否设置了 CON_EXTENDED 标志位。如果控制台支持扩展格式(CON_EXTENDED),则调用 con->write 函数,将扩展文本内容 ext_text 写入控制台。

con->write(con, ext_text, ext_len);:控制台的 write 函数负责将日志内容输出到实际设备上,例如终端、串口等。这里传递的是扩展格式文本。
\9. else con->write(con, text, len);

如果控制台不支持扩展格式,执行 else 分支,调用 con->write 函数,将常规日志文本 text 写入控制台。
在Linux内核中,控制台系统负责处理各种日志的输出设备,如显示器、串口和虚拟终端。内核中的每个控制台驱动都应该实现一个 write 函数,这个函数在日志输出时会被调用。call_console_drivers() 就是遍历这些控制台驱动,逐个调用它们的 write 方法,将日志内容输出。

遍历所有控制台:函数通过 for_each_console 遍历内核中所有注册的控制台驱动。
输出条件过滤:对控制台是否启用、是否独占、是否在线、是否定义了 write 函数等条件进行检查,确保只有符合条件的控制台可以输出日志。
根据控制台类型输出:根据控制台是否支持扩展格式,分别调用 con->write,输出扩展或常规格式的日志内容。
3.2 内核打印信息保存在哪
执行dmesg命令可以打印以前的内核信息,所以这些信息必定是保存在内核buffer中。

在kernel\printk\printk.c中,定义有一个全局buffer:

执行dmesg命令时,它就是访问虚拟文件/proc/kmsg,把log_buf中的信息打印出来。

3.3 printk信息从哪些设备打印出来?
在内核的启动信息中,有类似这样的命令行参数:

/* IMX6ULL */
[root@100ask:~]# cat /proc/cmdline
console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw

在命令行参数中,"console=ttymxc0"就是用来选择printk设备的。

可以指定多个"console="参数,表示从多个设备打印信息。

命令行信息来自哪里?

设备树
/ {
chosen {
bootargs = "console=ttymxc1,115200";
};
};

UBOOT根据环境参数修改设备树:IMX6ULL
/* 进入IMX6ULL的UBOOT */
=> print mmcargs
mmcargs=setenv bootargs console=${console},${baudrate} root=${mmcroot}
=> print console
console=ttymxc0
=> print baudrate
baudrate=115200

————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/caiji0169/article/details/144163401

版权声明:
作者:SE_Wang
链接:https://www.cnesa.cn/2529.html
来源:CNESA
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
Linux 内核日志系统—printk的机制与应用
1.内核代码 linux 4.9.88: kernel/printk.c📎printk.c include/linux/kernel.h📎kernel.h kernel/printk/internal.h📎internal.h 在 Linux 内核开发中,**printk** 是最常用的调试工具之一,主要用于输出内核日志信息。printk 不仅类似用户空间的 printf,还提供了日志级别管理功能,通过指定日志级别(如 KERN_WARNING),可以精准控制日志信息的输出范围和重要性。本文系统梳理了 printk 的使用方法、日志级别管理、日志信息的存储位置,以及输出过程的核心机制。通过解析源码和示例,详细展示了如何利用 console_loglevel 等宏调控日志输出行为,并剖析了内核是如何通过 call_console_drivers 将日志发送到不同设备。 2.printk的使用 2.1 使用实例 调试内核、驱动的最简单方法,是使用printk函数打印信息。 printk函数与用户空间的printf函数格式完全相同,它所打印的字符串头部可以加入“\001n”样式的字符。 其中n为0~7,表示这条信息的记录级别,n数值越小级别越高。 注意:linux 2.x内核里,打印级别是用""来表示。 在驱动程序中,可以这样使用printk: printk("This is an example\n"); printk("\0014This is an example\n"); printk("\0014""This is an example\n"); printk(KERN_WARNING"This is an example\n"); 在上述例子中: 第一条语句没有明确表明打印级别,它被处理前内核会在前面添加默认的打印级别:“<4>” KERN_WARNING是一个宏,它也表示打印级别: #define KERN_SOH "\001" /* ASCII Start Of Header */ #define KERN_WARNING KERN_SOH "4" /* warning conditions */ 现在知道了,内核的每条打印信息都有自己的级别,当自己的级别在数值上小于某个阈值时,内核才会打印该信息。 2.2 printk的记录级别 在内核代码include/linux/kernel.h中,下面几个宏确定了printk函数怎么处理打印级别……
<<上一篇
下一篇>>