往期内容
本专栏往期内容:Uart子系统
- UART串口硬件介绍
- 深入理解TTY体系:设备节点与驱动程序框架详解
- Linux串口应用编程:从UART到GPS模块及字符设备驱动
- 解UART 子系统:Linux Kernel 4.9.88 中的核心结构体与设计详解
- IMX 平台UART驱动情景分析:注册篇
- IMX 平台UART驱动情景分析:open篇
- IMX 平台UART驱动情景分析:read篇–从硬件驱动到行规程的全链路剖析
- IMX 平台UART驱动情景分析:write篇–从 TTY 层到硬件驱动的写操作流程解析
- 深入浅出UART驱动开发与调试:从基础调试到虚拟驱动实现
interrupt子系统专栏:
- 专栏地址:interrupt子系统
- Linux 链式与层级中断控制器讲解:原理与驱动开发 – 末片,有专栏内容观看顺序
pinctrl和gpio子系统专栏:
- 专栏地址:pinctrl和gpio子系统
- 编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用– 末片,有专栏内容观看顺序
input子系统专栏:
- 专栏地址:input子系统
- input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序 – 末片,有专栏内容观看顺序
I2C子系统专栏:
- 专栏地址:IIC子系统
- 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客 – 末篇,有专栏内容观看顺序
总线和设备树专栏:
- 专栏地址:总线和设备树
- 设备树与 Linux 内核设备驱动模型的整合-CSDN博客 – 末篇,有专栏内容观看顺序
目录
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是一个宏,它也表示打印级别:
#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};
现在基于宏定义:
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 */#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个值:
也可以直接修改/proc/sys/kernel/printk文件来改变这4个值,比如:
# echo "1 4 1 7" > /proc/sys/kernel/printk
这使得console_loglevel被改为1,于是所有的printk信息都不会被打印。
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
中,代码如下:
这个函数
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_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/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
版权归原作者 憧憬一下 所有, 如有侵权,请联系我们删除。