0


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

往期内容

本专栏往期内容:Uart子系统

  1. UART串口硬件介绍
  2. 深入理解TTY体系:设备节点与驱动程序框架详解
  3. Linux串口应用编程:从UART到GPS模块及字符设备驱动
  4. 解UART 子系统:Linux Kernel 4.9.88 中的核心结构体与设计详解
  5. IMX 平台UART驱动情景分析:注册篇
  6. IMX 平台UART驱动情景分析:open篇
  7. IMX 平台UART驱动情景分析:read篇–从硬件驱动到行规程的全链路剖析
  8. IMX 平台UART驱动情景分析:write篇–从 TTY 层到硬件驱动的写操作流程解析
  9. 深入浅出UART驱动开发与调试:从基础调试到虚拟驱动实现

interrupt子系统专栏:

  1. 专栏地址:interrupt子系统
  2. Linux 链式与层级中断控制器讲解:原理与驱动开发 – 末片,有专栏内容观看顺序

pinctrl和gpio子系统专栏:

  1. 专栏地址:pinctrl和gpio子系统
  2. 编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用– 末片,有专栏内容观看顺序

input子系统专栏:

  1. 专栏地址:input子系统
  2. input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序 – 末片,有专栏内容观看顺序

I2C子系统专栏:

  1. 专栏地址:IIC子系统
  2. 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客 – 末篇,有专栏内容观看顺序

总线和设备树专栏:

  1. 专栏地址:总线和设备树
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客 – 末篇,有专栏内容观看顺序

img

目录

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的使用

img

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是一个宏,它也表示打印级别:
#defineKERN_SOH"\001"/* ASCII Start Of Header */#defineKERN_WARNINGKERN_SOH "4"/* warning conditions */

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

2.2 printk的记录级别

在内核代码

include/linux/kernel.h

中,下面几个宏确定了printk函数怎么处理打印级别:

#defineconsole_loglevel(console_printk[0])#definedefault_message_loglevel(console_printk[1])#defineminimum_console_loglevel(console_printk[2])#definedefault_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};

现在基于宏定义:

  1. console_loglevelconsole_printk[0]):表示当前控制台的日志级别为4。通常,日志级别为4意味着只会显示警告及以上级别的消息(0-4级)。
    • 示例console_loglevel = 4,控制台会显示KERN_EMERGKERN_ALERTKERN_CRITKERN_ERRKERN_WARNING的消息,但KERN_NOTICE及以下的消息不会显示。
  1. default_message_loglevelconsole_printk[1]):表示内核消息的默认日志级别为3。即,如果没有显式指定消息的日志级别,那么这些消息将按照3级别处理。
    • 示例default_message_loglevel = 3,默认情况下,内核会处理消息为KERN_ERR级别。
  1. minimum_console_loglevelconsole_printk[2]):表示控制台允许输出的最低日志级别为1。这意味着即使console_loglevel较高,也不会输出低于1级别的消息。
    • 示例minimum_console_loglevel = 1,控制台会输出KERN_EMERGKERN_ALERT级别的消息,不会输出KERN_DEBUGKERN_INFO级别的消息。
  1. default_console_loglevelconsole_printk[3]):系统启动时的默认控制台日志级别为7。这表示系统启动时控制台会显示所有级别的日志(0-7级别)。
    • 示例default_console_loglevel = 7,启动时,控制台显示包括KERN_DEBUGKERN_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(调试)

实际应用场景:

  1. 当系统运行时,你可以通过更改 console_loglevel 来控制哪些日志消息输出到控制台。例如,设置 console_loglevel3 后,控制台将只显示错误及以上的日志消息。
console_loglevel = 3;
  1. 系统启动时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 */#defineCONSOLE_LOGLEVEL_DEFAULT7/* anything MORE serious than KERN_DEBUG */#defineMESSAGE_LOGLEVEL_DEFAULTCONFIG_MESSAGE_LOGLEVEL_DEFAULT#defineCONSOLE_LOGLEVEL_MIN1/* 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个值:

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

# echo "1 4 1 7" > /proc/sys/kernel/printk

这使得console_loglevel被改为1,于是所有的printk信息都不会被打印。

img

2.4 printk函数记录级别的名称及使用

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

#defineKERN_SOH"\001"/* ASCII Start Of Header */#defineKERN_SOH_ASCII'\001'#defineKERN_EMERGKERN_SOH "0"/* system is unusable */#defineKERN_ALERTKERN_SOH "1"/* action must be taken immediately */#defineKERN_CRITKERN_SOH "2"/* critical conditions */#defineKERN_ERRKERN_SOH "3"/* error conditions */#defineKERN_WARNINGKERN_SOH "4"/* warning conditions */#defineKERN_NOTICEKERN_SOH "5"/* normal but significant condition */#defineKERN_INFOKERN_SOH "6"/* informational */#defineKERN_DEBUGKERN_SOH "7"/* debug-level messages */

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

printk(KERN_WARNING”there is a warning here!\n”)

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

中,代码如下:

img

这个函数

call_console_drivers()

用于将日志信息发送到所有注册的控制台驱动程序(

console drivers

),从而实现日志的实际输出。内核日志系统中的消息被记录在

log_buf

中,而该函数负责调用控制台驱动程序,将这些消息写到终端或串口等输出设备上。

staticvoidcall_console_drivers(int level,constchar*ext_text,size_t ext_len,constchar*text,size_t len)
  • 函数参数:- level:日志级别,表示消息的严重性,如KERN_ERRKERN_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

方法,将日志内容输出。

  1. 遍历所有控制台:函数通过 for_each_console 遍历内核中所有注册的控制台驱动。
  2. 输出条件过滤:对控制台是否启用、是否独占、是否在线、是否定义了 write 函数等条件进行检查,确保只有符合条件的控制台可以输出日志。
  3. 根据控制台类型输出:根据控制台是否支持扩展格式,分别调用 con->write,输出扩展或常规格式的日志内容。

3.2 内核打印信息保存在哪

执行

dmesg

命令可以打印以前的内核信息,所以这些信息必定是保存在内核buffer中。

kernel\printk\printk.c

中,定义有一个全局buffer:

img

执行

dmesg

命令时,它就是访问虚拟文件

/proc/kmsg

,把log_buf中的信息打印出来。

3.3 printk信息从哪些设备打印出来?

在内核的启动信息中,有类似这样的命令行参数:

/* IMX6ULL */
[root@100ask:~]# cat /proc/cmdlineconsole=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

本文转载自: https://blog.csdn.net/caiji0169/article/details/144163401
版权归原作者 憧憬一下 所有, 如有侵权,请联系我们删除。

“Linux 内核日志系统—printk的机制与应用”的评论:

还没有评论