Linux内核日志调试方法

1 概要

背景、范围、术语

本文档主要介绍在Linux系统下常用的一些内核日志调试方法
 oops: 内核告知用户有不幸发生的最常用的方式。
 进程上下文:进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈上的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
 中断上下文中:就是硬件通过触发信号,导致内核调用中断处理程序,进入内核空间,这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。
 持锁:持有Linux常见的几种锁,包括自旋锁spinlock、互斥锁mutex、读写锁rwlock等。
#1024程序员节|用代码,改变世界#

2 常用内核日志调试方法与使用

2.1 printk 使用及日志等级

内核提供的打印函数printk()在实际的调试中是高频被使用的函数,原因在于其用法简单类似于C库提供的printf()函数,同时printk()函数适用范围很广,无论是在进程上下文中还是再中断上下文中,或者任何持有锁的场景都可以使用。

同样printk()也并不是无所不能,在一些会被频繁调用的函数中,例如某个会频繁触发的中断处理函数中添加打印信息可能会导致性能下降,再例如系统启动过程中,终端还没有初始化之前也无法使用。同时使用printk()函数本身也有一些限制,例如:单次打印的内容会被拷贝到printk_buf[1024]中,所以单次调用printk()打印的信息不能大于1024字节,否则会被截断。

printk函数原型:int printk(const char *fmt, …);这里的fmt一般格式为日志等级+打印信息如下示例:
e.g. :printk(KERN_ERR “it is a test\n”);

如果不加日志等级系统会使用默认的日志等级DEFAULT_MESSAGE_LOGLEVEL。日志等级定义如下:

#define KERN_EMERG	"<0>"	/* system is unusable			*/
#define KERN_ALERT	"<1>"	/* action must be taken immediately	*/
#define KERN_CRIT	"<2>"	/* critical conditions			*/
#define KERN_ERR	"<3>"	/* error conditions			*/
#define KERN_WARNING	"<4>"	/* warning conditions			*/
#define KERN_NOTICE	"<5>"	/* normal but significant condition	*/
#define KERN_INFO	"<6>"	/* informational			*/
#define KERN_DEBUG	"<7>"	/* debug-level messages			*/

我们注意到并不是所有使用printk()函数打印的信息都会显示到终端上,原因在于Linux会根据log等级进行相关日志的过滤,这样做的好处是终端显示的日志不会被众多调试信息刷屏,仅显示重要关键的信息,当然我们在调试阶段也可以将所有的日志都输出到终端显示。

关于log等级以及相关控制我们需要关注printk.c文件中如下定义:

#define MINIMUM_CONSOLE_LOGLEVEL  1   /*可以使用的最小日志级别*/ 
#define DEFAULT_CONSOLE_LOGLEVEL  7     /*比KERN_DEBUG 更重要的消息都被打印*/ 

int console_printk[4] = { DEFAULT_CONSOLE_LOGLEVEL,
                         /*控制台日志级别,优先级高于该值的消息将在控制台显示*/ 
						  DEFAULT_MESSAGE_LOGLEVEL, 
						  /*默认消息日志级别,printk没定义优先级时,打印这个优先级以上的消息*/ 
						  MINIMUM_CONSOLE_LOGLEVEL,
						  /*最小控制台日志级别,控制台日志级别可被设置的最小值(最高优先级)*/ 
						  DEFAULT_CONSOLE_LOGLEVEL,
						  /* 默认的控制台日志级别*/ 
						};

我们可以cat /proc/sys/kernel/printk来查看这四个值,如下:# cat /proc/sys/kernel/printk
7 4 1 7

4个值分别对应:控制台的日志级别、默认消息日志级别、最小控制台日志级别,默认控制台日志级别。
只有日志级别小于控制台日志级别,对应的日志才会输出到终端被显示出来,因此调整控制台的日志等级将所有等级的日志都输出到控制台,可以使用如下操作:

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

2.2 printk与记录缓冲区

所有调用printk()打印的日志信息都会被保存在大小为__LOG_BUF_LEN的环形队列中,该大小与CONFIG_LOG_BUF_SHIFT宏定义有关如下图,该值不同的平台配置有所差异,部分平台默认CONFIG_LOG_BUF_SHIFT为16,也就是说环形队列总大小为64K。

#define CONFIG_LOG_BUF_SHIFT 16
#define __LOG_BUF_LEN	(1 << CONFIG_LOG_BUF_SHIFT)

记录缓冲区有2点需要注意下:
1、消息被读出到用户空间时,此消息就会从环形队列中删除。
2、当消息缓冲区满时,如果再有printk()调用时,新消息将覆盖队列中的老消息。

2.3 printk_ratelimit()与printk_ratelimited()

你是否遇到过内核调试过程中需要打印一些调试信息,但是直接使用printk()函数由于调试部分会被反复调用会造成频繁打印刷屏的情况,对于这种场景我们可以使用printk()的变种printk_ratelimit()与printk_ratelimited()。

先说说printk_ratelimit(),在/proc/sys/kernel/目录下有几个与内核日志相关的节点,如下:

/proc/sys/kernel # ls printk*
printk                  printk_ratelimit
printk_delay            printk_ratelimit_burst

printk_delay:默认值为0,在printk()函数实现中如果该delay非0则进行延迟再输出。

printk_ratelimit:默认值5,表示时间间隔,与printk_ratelimit_burst参数配合使用,其默认值为10,两个参数合起来表示每5秒时间最多只能打印10次。实际内核调试中我们遇到需要打印一些提示信息,但是我们又不需要他们一直重复刷屏打印,避免短时间产生大量重复信息而覆盖掉有用信息就可以调整该参数。

调用printk_ratelimit()函数(位于printk.h),其会根据printk_ratelimit和printk_ratelimit_burst参数判断打印是否过于频繁,可参考如下使用示例:

if(printk_ratelimit())
{
    printk(KERNEL_ERR "xxx driver error!!!\n");
}

与此同时内核建议我们不要使用printk_ratelimit()函数,建议使用printk_ratelimited()去替换,原因在于printk_ratelimit()函数并不会判断打印的内容,如果有多个函数都调用了这个printk_ratelimit()函数的话,那么它们是会共享系统中的频次条件,内核注释解释如下:

/*
 * Please don't use printk_ratelimit(), because it shares ratelimiting state
 * with all other unrelated printk_ratelimit() callsites.  Instead use
 * printk_ratelimited() or plain old __ratelimit().
 */
extern int __printk_ratelimit(const char *func);
#define printk_ratelimit() __printk_ratelimit(__func__)
extern bool printk_timed_ratelimit(unsigned long *caller_jiffies,
				   unsigned int interval_msec);
extern int printk_delay_msec;
extern int dmesg_restrict;
extern int kptr_restrict;

而使用printk_ratelimited()可以根据内容来进行过滤,避免同一条日志频繁打印刷屏,多个函数调用printk_ratelimited()只要打印内容不一样,就不会相互影响。其参数在ratelimit.h中定义与printk_ratelimit及printk_ratelimit_burst参数表示时间间隔和频次,如下:

#define DEFAULT_RATELIMIT_INTERVAL	(5 * HZ)
#define DEFAULT_RATELIMIT_BURST		10

2.4 Syslogd与klogd

syslog用来记录应用程序或者硬件设备的日志,syslogd日志记录器由两个守护进程(klogd,syslogd)和一个配置文件(syslog.conf)组成。klogd首先接收内核的日志,它既可以从/proc/kmsg文件中,也可以通过syslog()系统调用读取内核消息,默认情况下,它选择读取/proc方式实现。然后将之发送给syslogd。syslogd是一个分发器,它将接收到的所有日志按照/etc/syslog.conf的配置策略发送到这些日志应该去的地方,当然也包括从klogd接收到的日志。默认情况下其把接收到的消息写入/var/log/messages文件中,示意图如下:
Linux内核日志调试方法_第1张图片

2.5 内核日志获取方式

第一种是使用dmesg命令,它被用于检查和控制内核的环形缓冲区。 kernel会将开机信息存储在ring buffer中,若是开机时来不及查看信息, 可利用dmesg来查看, 开机信息保存在 /var/log/dmesg 文件里也可以查看。
dmesg(选项)

  1. -c:显示信息后, 清除ring buffer中的内容;
  2. -s<缓冲区大小>:设置ring buffer的大小;
  3. -n:设置记录信息的层级。

第二种是通过cat /proc/kmsg来查看打印信息,对于不支持dmesg命令的调试环境,我们可以查看该节点信息获取内核日志。

2.6 dev_dbg()使用简介

在Linux驱动代码中我们常常遇到内核使用dev_dbg()来控制调试信息输出,该函数在包含,如下:

#if defined(CONFIG_DYNAMIC_DEBUG)
#define dev_dbg(dev, format, ...)		     \
do {						     \
	dynamic_dev_dbg(dev, format, ##__VA_ARGS__); \
} while (0)
#elif defined(DEBUG)
#define dev_dbg(dev, format, arg...)		\
	dev_printk(KERN_DEBUG, dev, format, ##arg)
#else
#define dev_dbg(dev, format, arg...)				\
({								\
	if (0)							\
		dev_printk(KERN_DEBUG, dev, format, ##arg);	\
	0;							\
})
#endif

可以看到该函数在定义CONFIG_DYNAMIC_DEBUG宏时会调用dynamic_dev_dbg()函数输出调试信息,这里需要内核支持动态调试,内核配置选项定义CONFIG_DEBUG_FS=y,CONFIG_DYNAMIC_DEBUG=y
可以按照如下方式打开或关闭某个需要调试文件的dynamic debug输出:

mount -t debugfs none /sys/kernel/debug                       
挂载内核虚拟调试文件系统

echo -n 'file xxx.c +p' > /data/debugfs/dynamic_debug/control  
增加xxx.c文件dynamic debug的输出

echo -n 'file xxx.c -p' > /data/debugfs/dynamic_debug/control  
去掉xxx.c文件dynamic debug的输出

关于动态调试日志更详细的介绍可以参数内核kernel/Documentation/dynamic-debug-howto.txt

第二种情况在定义DEBUG宏时调用dev_printk()函数输出调试信息,该函数与printk()函数类似最终会调用vprintk_emit()将日志输出,需要注意的是这里的log等级为KERN_DEBUG要让其真正输出到终端还依赖控制台日志等级,可以参考上面章节进行设置。

2.7 自定义日志调试函数方法

我们有时候调试驱动需要添加自己的调试日志,但是正式版本又不需要打印这部分信息出来,同时也不想直接使用dev_dbg()等函数,这样会将系统默认使用该函数的其他调试打印与我们的混在一起。此时我们可以参考内核的写法,自定义日志调试的函数,我们可以在需要调试的C文件添加定义MY_DEBUG宏,或者在Makefile文件中添加CFLAGS += -DMY_DEBUG,然后添加如下代码,这样我们就可以在调试阶段使用my_printk()函数进行打印了。

#if defined(MY_DEBUG)
#define my_printk(dev, format, arg...)		\
	dev_printk(KERN_INFO, dev, format, ##arg)
#endif

另外一种方法是不使用宏去控制日志的输出与否,而通过固定的参数去打开/关闭日志输出,这个参数我们可以利用文件系统进行手动调整,例如添加如下代码:

static unsigned my_debug = 0;
module_param(my_debug, uint, 0644);

#define my_printk(my_debug,dev, format, arg...) do {\
	if (my_debug) {\
		dev_printk(KERN_INFO, dev, format, ##arg);
	}\
} while(0)

我们可以使用debug串口或者其他调试终端进入文件系统,然后通过修改节点my_debug的值,进而去控制是否将调试信息输出。

2.8 日志系统的总体结构

Linux内核日志调试方法_第2张图片

3 其他内核日志调试

1、 BUG()与BUG_ON()

该宏位于,一旦调用会引发oops导致栈的回溯和错误消息的打印,可以帮助我们分析一些问题。

#ifndef HAVE_ARCH_BUG
#define BUG() do { \
	printk("BUG: failure at %s:%d/%s()!\n", __FILE__, __LINE__, __func__); \
	panic("BUG!"); \
} while (0)
#endif

#ifndef HAVE_ARCH_BUG_ON
#define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while (0)
#endif

Oops打印信息如下:

[   16.181500] Unable to handle kernel paging request at virtual address ffffffb8
[   16.181517] pgd = ca480000
[   16.181525] [ffffffb8] *pgd=8f7d4861, *pte=00000000, *ppte=00000000
[   16.181542] Internal error: Oops: 37 [#1] PREEMPT ARM
[   16.181551] Modules linked in: qcom_emac(+) quectel_phy qca8337 of_mdio libphy embms_kernel(O)
[   16.181574] CPU: 0 PID: 909 Comm: insmod Tainted: G        W  O   3.18.44 #1
[   16.181583] task: cbb01980 ti: ca43a000 task.ti: ca43a000
[   16.181627] PC is at phy_resume+0x4/0x1c [libphy]
[   16.181678] LR is at emac_pm_resume+0x90/0xac [qcom_emac]
[   16.181689] pc : [<bf0062c4>]    lr : [<bf0231c4>]    psr: 200f0113
[   16.181689] sp : ca43bce8  ip : 00004000  fp : 00000000
[   16.181698] r10: 00110009  r9 : 00000004  r8 : ca43a018
[   16.181707] r7 : 00000000  r6 : cafeb4e0  r5 : cf328010  r4 : cafeb000
[   16.181715] r3 : 00000000  r2 : 004dd036  r1 : 2000819f  r0 : cad03c00
[   16.181725] Flags: nzCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment none
[   16.181734] Control: 10c53c7d  Table: 8a480059  DAC: 00000051
[   16.181743] Process insmod (pid: 909, stack limit = 0xca43a208)
[   16.181751] Stack: (0xca43bce8 to 0xca43c000)
[   16.181764] bce0:                   ca43a038 cf328010 c02be2cc c02bf930 cf328010 ca43a000
[   16.181777] bd00: 00000000 c02bf994 cf328010 00000000 00000001 c02c0cec cafeb000 cafeb4e0
[   16.181790] bd20: cf328010 cf328000 ca43bd28 ca43bd28 00000000 ca43a010 60030113 cf328010
[   16.181803] bd40: cf328000 cafebd70 00000000 c02c0e68 cafeb000 cafeb4e0 cf328010 bf023c44
[   16.181815] bd60: 00000010 c0132250 bf028710 0000000c 00000014 001312d0 001b7740 002b7cd0
[   16.181828] bd80: 001b7740 00000000 c0a48564 ffffffed cf328010 bf02a7bc bf02a7bc 00000046
[   16.181841] bda0: c0a48564 ca6da1e4 bf02ab20 c02bb034 cf328010 c0b7c350 00000000 c02b96a0
[   16.181854] bdc0: cf328010 cf328044 bf02a7bc c0a6b9e0 c0a48564 c02b9838 00000000 bf02a7bc
[   16.181867] bde0: c02b97d0 c02b7f34 cf21504c cf31e8b0 bf02a7bc 00000000 cafa0e00 c02b8ef0
[   16.182123] [<bf0062c4>] (phy_resume [libphy]) from [<bf0231c4>] (emac_pm_resume+0x90/0xac [qcom_emac])
[   16.182164] [<bf0231c4>] (emac_pm_resume [qcom_emac]) from [<c02bf930>] (__rpm_callback+0x5c/0x80)
[   16.182180] [<c02bf930>] (__rpm_callback) from [<c02bf994>] (rpm_callback+0x40/0x74)
[   16.182194] [<c02bf994>] (rpm_callback) from [<c02c0cec>] (rpm_resume+0x4d4/0x604)
[   16.182209] [<c02c0cec>] (rpm_resume) from [<c02c0e68>] (__pm_runtime_resume+0x4c/0x84)
[   16.182242] [<c02c0e68>] (__pm_runtime_resume) from [<bf023c44>] (emac_probe+0x9c8/0xb14 [qcom_emac])
[   16.182278] [<bf023c44>] (emac_probe [qcom_emac]) from [<c02bb034>] (platform_drv_probe+0x30/0x7c)
[   16.182294] [<c02bb034>] (platform_drv_probe) from [<c02b96a0>] (driver_probe_device+0xb8/0x1e8)
[   16.182309] [<c02b96a0>] (driver_probe_device) from [<c02b9838>] (__driver_attach+0x68/0x8c)
[   16.182323] [<c02b9838>] (__driver_attach) from [<c02b7f34>] (bus_for_each_dev+0x6c/0x90)
[   16.182337] [<c02b7f34>] (bus_for_each_dev) from [<c02b8ef0>] (bus_add_driver+0xcc/0x1e4)
[   16.182352] [<c02b8ef0>] (bus_add_driver) from [<c02ba28c>] (driver_register+0x9c/0xe0)
[   16.182368] [<c02ba28c>] (driver_register) from [<c00089a0>] (do_one_initcall+0xf8/0x1a0)
[   16.182385] [<c00089a0>] (do_one_initcall) from [<c006e864>] (load_module+0x1508/0x1abc)
[   16.182400] [<c006e864>] (load_module) from [<c006eef8>] (SyS_init_module+0xe0/0xe8)
[   16.182415] [<c006eef8>] (SyS_init_module) from [<c000de40>] (ret_fast_syscall+0x0/0x44)

2、 WARN(x) 和 WARN_ON(x)

该宏同样位于中,与BUG()及BUG_ON()不同的是WARN() 和WARN_ON()不会触发oops,WARN_ON()最终会调用dump_stack()函数打印调用栈信息。

#ifndef WARN_ON
#define WARN_ON(condition) ({						\
	int __ret_warn_on = !!(condition);				\
	if (unlikely(__ret_warn_on))					\
		__WARN();						\
	unlikely(__ret_warn_on);					\
})
#endif

3、 dump_stack()

该函数头文件为,它可以在我们调试内核的过程中打印出函数调用关系。打印信息如下:

[ 46.942987] Call Trace:
[ 46.942993]  [<ffffffffa0072017>] hello_init+0x17/0x1000 [hello]
[ 46.942999]  [<ffffffff81002042>] do_one_initcall+0x42/0x180
[ 46.943003]  [<ffffffff810a011e>] sys_init_module+0xbe/0x230
[ 46.943006]  [<ffffffff815fd202>] system_call_fastpath+0x16/0x1b

通过打印的调用栈信息我们可以更加清晰的了解软件运行的调用过程。

4 参考文档

《Linux内核设计与实现》

你可能感兴趣的:(Linux驱动,1024程序员节)