一直以来,Linus Torvalds对内核调试器都秉持着抵触态度,并且摆出了我是bastard我怕谁的姿态。他保持了一贯风格,言辞尖锐却直指本质。相信这是经验之谈。在调试内核时,最关键的问题是如何获取出错相关的信息,准确定位出错位置。获取信息有很多方法,其中内核调试器只能提供有限的帮助,而分析日志则是最基本也是最主要的方法。为内核层软件提供一种方便的日志工具,将大大简化其调试工作。在Linux社区中,对升级日志打印系统的讨论正进行得如火如荼。而Lustre文件系统则早已拥有了一个强大、高效的内核层跟踪调试系统。本章将分析这个系统。
Lustre的跟踪调试系统可以分成两个部分,一个是跟踪系统,负责内核日志的存储和管理,另一个是调试系统,它向上提供跟踪系统调用接口,并负责跟踪系统状态的管理。
Lustre的日志首先被缓存在内核一块有限的空间中。为了获得这些日志,需要使用Lustre给出的特定工具,即lctl debug_kernel。
除此之外,Lustre还将一部分重要信息输出到系统打印中,因此可以通过dmesg命令或/var/log/messages查看。然而,这些信息是经过删减的。与通过lctldebug_kernel命令获得的输出相比,不仅打印频率受到了限制,而且每条打印给出的信息项也经过了删节。
典型地,通过lctl debug_kernel命令获得的打印输出如下所示:
00000080:00000001:0.0:1339902577.938752:0:28824:0:(file.c:507:ll_file_open())Process entered 00000080:00000001:0.0:1339902577.938755:0:28824:0:(file.c:533:ll_file_open())Process leaving (rc=0 : 0 : 0)
其格式为:
子系统号:掩码号:CPU号.类型:秒.微秒:栈地址:PID:扩展PID:(文件名:行数:函数名()) 输出信息
对函数的跟踪分析是跟踪系统最基本的用处。在Lustre每个重要函数的入口处都添加了宏定义ENTRY。在函数退出前则添加了宏定义EXIT,或使用宏定义RETURN替代return。如果被使能,这些宏定义将在函数进入后和退出前分别在日志中输出一条打印。这些打印信息可以描绘出函数调用的流程,从而方便调试时确定出错位置和出错原因。
内存泄露检测是跟踪调试系统的另外一个重要用途。内存泄露是内核开发人员时常遇到的棘手问题之一。Lustre的跟踪系统为内存泄露检测提供了一种方便实用的方法。Lustre使用宏定义封装内核的内存申请和释放函数,在内存的每次申请和释放时,都在日志中输出一条掩码号为D_MALLOC的打印。这样,日志中将形成成对的内存申请和释放打印。为了检测内存泄露,可以在卸载Lustre客户端实例后,通过lctldebug_kernel命令,获得并分析日志文件,检测是否存在内存泄露。Lustre以Perl脚本(leak_finder.pl)的形式提供了分析工具。
跟踪调试系统还被在Lustre发现严重内部错误时的处理。在Lustre发现内部BUG时,会调用LBUG()宏。在这个宏中,Lustre会进行如下处理:
在Lustre发现断言失效时,也会调用LBUG,进行上述操作。
跟踪调试系统是Lustre中Libcfs的一部分。Libcfs是一个适用于多种操作系统的库函数集合,Lustre的其他子系统,包括用户层工具,均用到了Libcfs中定义的函数。Libcfs的模块初始化函数init_libcfs_module()调用libcfs_debug_init()函数进行跟踪调试系统的初始化。
前面已经提到,每个CPU和每种日志类型都对应着不同的structcfs_trace_cpu_data结构体实例。这些实例都放置在cfs_trace_data全局二维数组中。libcfs_debug_init()函数所需要做的就是初始化这个数组。
为此,它首先要确定为日志缓存分配的内存上限。这个值首先来自于libcfs_debug_mb变量,该变量可以通过/proc/sys/lnet/debug_mb设置。如果这个值被设置得太大(大于总物理页数的80%,或大于512MB)或太小(使得每个CPU分配的数目小于1),那么这个值将被修定为5MB*CPU数目。
日志缓存的内存上限被均摊到每个CPU上。对于每个CPU,它的每种日志类型所能分配的日志缓存大小上限按全局数组pages_factor来划分。对于Linux操作系统,每个CPU的日志缓存大小按%10、%10、%80的比例,分别划分给硬件中断状态、软中断状态和正常状态。确定好的这个值被设定在struct cfs_trace_cpu_data结构体实例的tcd_max_pages字段中。跟踪调试系统在初始化时并不申请日志缓存,而是在系统运行过程中动态申请缓存,但是申请的缓存总数将不超过tcd_max_pages字段的限制。
注意在初始化的完成之前,调用ENTRY、LBUG、LASSERT、CERROR等使用了跟踪调试系统的函数是不被允许的。
将打印存储到调试跟踪系统的最常用方法是调用CDEBUG_LIMIT宏,它的定义如下:
#define CDEBUG_LIMIT(mask, format, ...) \ do { \ static cfs_debug_limit_state_t cdls; \ \ __CDEBUG(&cdls, mask, format, ##__VA_ARGS__);\ } while (0)
mask参数是打印的掩码号,随后的参数与常见的printk()函数的参数一致。
__CDEBUG宏首先将记录调用所在的子系统号、文件名、函数名和行号,然后根据子系统号和掩码号确定是否调用libcfs_debug_msg()函数,存储打印信息。掩码号为D_ERROR、D_EMERG、D_WARNING、D_CONSOLE的打印总是会被存储,而其他类型的打印如果被打印,需要满足两个条件:一是全局变量libcfs_debug使能了该类型的打印,而是全局变量libcfs_subsystem_debug使能了该子系统的打印。
libcfs_debug_msg()函数调用libcfs_debug_vmsg2()函数完成大多数的工作。这个函数的处理流程为:
1.根据上下文和所处CPU号确定cfs_trace_cpu_data结构实例。
2.确定打印输出中的头信息,存储在ptldebug_header结构中。
3.如果cfs_trace_cpu_data结构实例tcd_shutting_down字段为有效,则表明跟踪调试系统处在正在退出状态,那么跳到第9步。
4.估计打印信息的长度。注意此时无法确定打印信息的准确长度,因为CDEBUG可变参数的值会影响打印信息长度值,Lustre将这部分信息的平均长度预估为85。而文件名长度、函数名长度、调用深度(Linux未使用)和头信息长度(如果全局变量libcfs_debug_binary未被置零)则是可以此时确定的信息长度,这里称为已知长度。已知长度和预估长度之而后就是估计出的打印信息的总长度。
5.调用cfs_trace_get_tage()函数,该函数将以cfs_trace_cpu_data结构和打印信息长度为参数,返回一个cfs_trace_page结构指针,作为存储打印信息的空间。
6.获取cfs_trace_page结构的page字段的地址,并预留已知长度,然后调用vsnprintf,尝试将可变参数部分打印到该位置中。注意,由于可变参数的最终输出可能大于预估长度,因此可能无法完全将信息打印到缓存中。如果打印成功,则进行下异步。否则,根据vsnprintf输出的输出长度,计算确定的打印信息长度,返回到第5步。由此可见,对平均长度预估的值非常重要,如果预估长度过大,则会造成空间浪费,如果预估长度过小,则会使得打印重复两次的概率过大,造成时间损失。在调用CDEBUG_LIMIT时,也应确保单次打印信息不会过长。
7.判断打印信息的最后一个字符是否为回车,否则打印一行报错。
8.将确定长度的头信息复制到缓存的相应位置。
9.如果掩码号和全局变量libcfs_printk按位与的值为非真,则不需要向系统日志输出,该函数返回。否则继续。
10. 如果__CDEBUG的第一个cfs_debug_limit_state_t类型的参数不为NULL,且全局变量libcfs_console_ratelimit为真,则说明系统日志的输出有打印频率限制。方法是判断cfs_debug_limit_state_t类型的cdls_next字段的值,如果早于当前时间,那么就不向系统日志打印输出,而返回。否则继续。
11. 如果cfs_debug_limit_state_t类型的参数不为NULL,则更新cfs_debug_limit_state_t类型的cdls_next字段的值。这个值是动态变化的,如果当前时间比上次设定cdls_next字段值晚太多,则降低cdls_delay字段的值(除以全局变量libcfs_console_backoff的4倍),否则增加cdls_delay字段(乘以libcfs_console_backoff)。通过cdls_delay字段的值加上当前时间,可以获取cdls_next字段的值。
12. 将打印输出到系统日志中。
cfs_trace_get_tage()函数由于牵涉到缓存的申请和重复利用,所以非常重要,它的流程为:
Lustre的跟踪调试系统提供了一个内核态服务线程,可以自动地收集内核缓存中的日志,将它存储到给定文件中。这个服务例程可以通过/proc/sys/lnet/daemon_file进行控制。向这个文件写入文件名,将启动这个内核态服务;写入“size=”可以设定日志文件的的最大大小;写入“stop”,将终止这个服务。
这个服务线程将运行tracefiled()函数,其每个循环的流程为:
1.通过collect_pages函数,将所有CPU、所有类型的cfs_trace_cpu_data结构中的缓存页,放置在page_collection结构的pc_pages链表中。
2.打开全局变量cfs_tracefile所指定的文件。如果打开失败,则调用put_pages_on_daemon_list()函数,并跳到第6步。put_pages_on_daemon_list()函数将pc_pages链表中的缓存页移动到相应CPU和类型的cfs_trace_cpu_data结构的tcd_daemon_pages链表中。
3.将pc_pages链表中的所有缓存页写入文件中,如果写入失败,则pc_pages链表中的所有页归还至各cfs_trace_cpu_data结构。
4.关闭文件。
5.调用put_pages_on_daemon_list()函数,进行从pc_pages链表到tcd_daemon_pages链表的缓存页移动。
6.如果全局变量trace_tctl的tctl_shutdown被设置,且last_loop变量为1,则退出该循环。如果last_loop不为1,则将其置为1,并返回第1步。重复一次的目的是确保在tctl_shutdown设置之后,把新打印的日志刷入到文件中。
7.使得本线程睡眠在全局变量trace_tctl的tctl_waitq等待队列上,等待cfs_trace_get_tage_try()函数将自己唤醒。
Libcfs在卸载模块时将调用cfs_tracefile_exit()函数。这个函数首先调用cfs_trace_stop_thread()函数,把全局变量trace_tctl的tctl_shutdown字段设置为有效,并将全局变量thread_running置零。cfs_tracefile_exit()函数随后调用cfs_trace_cleanup()函数。这个函数将释放所有申请的缓存页和其他内存空间。
向/proc/sys/lnet/dump_kernel写入相应文件路径名,可以将内核中的日志缓存储到对应文件中。然而这个文件中存储的信息是一个二进制信息,其中的日志头是直接以二进制的形式存储的,因此需要通过解析这个文件获得日志的字符串输出。