深入理解Lustre文件系统-第1篇 跟踪调试系统

一直以来,Linus Torvalds对内核调试器都秉持着抵触态度,并且摆出了我是bastard我怕谁的姿态。他保持了一贯风格,言辞尖锐却直指本质。相信这是经验之谈。在调试内核时,最关键的问题是如何获取出错相关的信息,准确定位出错位置。获取信息有很多方法,其中内核调试器只能提供有限的帮助,而分析日志则是最基本也是最主要的方法。为内核层软件提供一种方便的日志工具,将大大简化其调试工作。在Linux社区中,对升级日志打印系统的讨论正进行得如火如荼。而Lustre文件系统则早已拥有了一个强大、高效的内核层跟踪调试系统。本章将分析这个系统。

Lustre的跟踪调试系统可以分成两个部分,一个是跟踪系统,负责内核日志的存储和管理,另一个是调试系统,它向上提供跟踪系统调用接口,并负责跟踪系统状态的管理。

1.1日志的获得

Lustre的日志首先被缓存在内核一块有限的空间中。为了获得这些日志,需要使用Lustre给出的特定工具,即lctl debug_kernel。

除此之外,Lustre还将一部分重要信息输出到系统打印中,因此可以通过dmesg命令或/var/log/messages查看。然而,这些信息是经过删减的。与通过lctldebug_kernel命令获得的输出相比,不仅打印频率受到了限制,而且每条打印给出的信息项也经过了删节。

1.2打印输出的释义

典型地,通过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:(文件名:行数:函数名()) 输出信息

  • 子系统号。子系统号标识了日志输出时所处的功能模块,例如S_MDS对应元数据服务器,S_MDC对应元数据客户端。
  • 掩码号。Lustre的日志区分了多种级别和用途。例如D_TRACE代表上面所述的函数跟踪信息,D_WARNING代表警告信息,而D_ERROR则代表出错信息。可以根据需要,通过sysctl接口更改日志系统的掩码号,从而使能或无效某些日志输出。例如,要打开D_TRACE类型的日志输出,则只需运行:echo “trace” > /proc/sys/lnet/debug
  • CPU号。代表了当前进程运行所在的CPU ID。为了减少冲突,提高性能,Lustre在为每个CPU单独维护一套跟踪信息。
  • 类型。针对Linux环境,Lustre区分了三种类型,分别对应处在硬件中断状态中、处在软中断状态中和正常状态。这三种类型也分别对应不同的跟踪信息。这些信息存储在struct cfs_trace_cpu_data结构体中。每个CPU和每种日志类型都对应着不同的structcfs_trace_cpu_data结构体实例,因此实例的数目为3*NR_CPUS,其中NR_CPUS是内核配置时就已经确定的CPU数目。
  • 秒和微秒。这是日志在打印到内核空间时,通过do_gettimeofday()函数获得的时间。
  • 栈地址。GCC给出了内嵌函数,可以用以获得函数所处的栈地址。不过这些内嵌函数是平台相关的,例如IA64平台是__builtin_dwarf_cfa()函数,而其他平台是__builtin_frame_address()函数。
  • PID。当前进程的PID,即current->pid。
  • 扩展PID。在Linux操作系统中,这个部分没有用到,而在Darwin中,则对应了current_thread()。
  • 文件名、行数、函数名。为了获得这些定位信息,Lustre以宏定义的形式包装了日志打印函数,并在宏定义中保存__FILE__、__LINE__和__FUNCTION__,然后以参数形式传递给日志打印函数。
  • 输出信息。应注意的是,每次调用日志打印函数,都应确保打印信息的最后字符为回车,否则Lustre将在系统日志中输出一行报错。

1.3跟踪调试系统的用途

对函数的跟踪分析是跟踪系统最基本的用处。在Lustre每个重要函数的入口处都添加了宏定义ENTRY。在函数退出前则添加了宏定义EXIT,或使用宏定义RETURN替代return。如果被使能,这些宏定义将在函数进入后和退出前分别在日志中输出一条打印。这些打印信息可以描绘出函数调用的流程,从而方便调试时确定出错位置和出错原因。

内存泄露检测是跟踪调试系统的另外一个重要用途。内存泄露是内核开发人员时常遇到的棘手问题之一。Lustre的跟踪系统为内存泄露检测提供了一种方便实用的方法。Lustre使用宏定义封装内核的内存申请和释放函数,在内存的每次申请和释放时,都在日志中输出一条掩码号为D_MALLOC的打印。这样,日志中将形成成对的内存申请和释放打印。为了检测内存泄露,可以在卸载Lustre客户端实例后,通过lctldebug_kernel命令,获得并分析日志文件,检测是否存在内存泄露。Lustre以Perl脚本(leak_finder.pl)的形式提供了分析工具。

跟踪调试系统还被在Lustre发现严重内部错误时的处理。在Lustre发现内部BUG时,会调用LBUG()宏。在这个宏中,Lustre会进行如下处理:

  • 如果错误发生中断上下文中,即in_interrupt()函数返回1,这种情况是不应存在、不被允许的,Lustre直接调用panic()函数,不进行任何后续处理。
  • 打印函数调用堆栈信息。
  • 如果Lustre不会调用panic()函数,即libcfs_panic_on_lbug变量值为0,Lustre将启动一个内核线程,把Lustre之前记录的日志打印出来
  • 尝试调用路径位于/usr/lib/lustre/lnet_upcall的程序,以进行可能存在的额外处理。
  • 如果libcfs_panic_on_lbug变量值为1,则调用panic()。
  • 将进程状态设置为不可中断的等待状态(TASK_UNINTERRUPTIBLE),并进入schedule()的死循环,反复进行进程切换。

在Lustre发现断言失效时,也会调用LBUG,进行上述操作。

1.4跟踪调试系统的初始化

跟踪调试系统是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等使用了跟踪调试系统的函数是不被允许的。

1.5跟踪调试系统的使用

将打印存储到调试跟踪系统的最常用方法是调用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()函数由于牵涉到缓存的申请和重复利用,所以非常重要,它的流程为:

  • 调用cfs_trace_get_tage_try()函数。在这个函数中,如果在cfs_trace_cpu_data结构中,当前缓存页的剩余空间足够存储打印信息,那么该函数返回该缓存页,否则需要申请一个全新的缓存页。如果当前所使用的缓存数目已经达到上限,那么就不能再申请内存了,这个函数将返回NULL。如果这个函数成功申请了全新的缓存页,且当前页数大于8,thread_running也表明有跟踪服务线程在运行,那么它将向全局变量trace_tctl的tctl_waitq字段发送一个信号,以唤醒服务线程。
  • 如果cfs_trace_get_tage_try()函数成功获得一个缓存页,函数返回。否则继续。
  • 如果有日志守护进程正在运行,即全局变量thread_running被置为有效,则调用cfs_tcd_shrink()函数。这个函数将把最早的10%的缓存页从cfs_trace_cpu_data结构的tcd_pages链表中移到tcd_daemon_pages链表中。如果它的tcd_cur_daemon_pages字段表明,tcd_daemon_pages链表的长度超过了tcd_max_pages字段大小,那么这些缓存页将被直接释放掉,这部分日志也就丢失了。
  • 如果tcd_cur_pages大于0,则表明cfs_trace_cpu_data结构中还有缓存页,那么将最早的缓存页放到tcd_pages链表的最尾端,并设置这个缓存页已使用的长度为0。

1.6服务线程

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()函数将自己唤醒。

1.7跟踪调试系统的退出

Libcfs在卸载模块时将调用cfs_tracefile_exit()函数。这个函数首先调用cfs_trace_stop_thread()函数,把全局变量trace_tctl的tctl_shutdown字段设置为有效,并将全局变量thread_running置零。cfs_tracefile_exit()函数随后调用cfs_trace_cleanup()函数。这个函数将释放所有申请的缓存页和其他内存空间。

1.8用户层的日志解析

向/proc/sys/lnet/dump_kernel写入相应文件路径名,可以将内核中的日志缓存储到对应文件中。然而这个文件中存储的信息是一个二进制信息,其中的日志头是直接以二进制的形式存储的,因此需要通过解析这个文件获得日志的字符串输出。

1.9总结

Lustre的跟踪调试系统是Lustre开发的重要辅助工具。这个工具对于定位和分析错误有着很大的帮助作用。跟踪调试系统不是一个离线调试的工具,它在Lustre的实际运行过程中保持着在线运行,在达到调试效果的同时,跟踪调试系统要减少时间开销,降低内存使用,因而十分注重实现的高效性。本章对这个系统进行了分析。相信读者可以通过本章了解该系统的原理、实现和使用方式,也会对如何设计和实现一个强大而高效的跟踪调试获得自己的认识和理解。

你可能感兴趣的:(文件系统)