一、概述
众所周知,从事linux内核开发的工程师或多或少都会遇到内核panic,亦或者是soft lockup,前者多半是因为内存泄露、内存互踩、访问空地址等错误导致的,而后者可以肯定是因为代码的逻辑不当,进而导致内核进入一个死循环。问题可大可小,当问题足够隐蔽又难以复现时通常会让程序猿们十分抓狂,我前些日子有幸体验了一把,足足花费了我一周时间才成功找到问题,为了让自己以后能从容的面对内核panic,也为了能积累更多的经验,还是用文字记录下来才是最好的形式。
提到内核panic就不得不提kdump,这是对付内核panic的利器,kdump实际上是一个工具集,包括一个带有调试信息的内核镜像(捕获内核),以及kexec、kdump、crash三个部分,kdump本质上是一个内核崩溃转储工具,即在内核崩溃时捕获尽量多的信息生成内核core文件。工作原理如下:
(1) kexec分为内核态的kexec_load和用户态的kexec-tools,系统启动时将原内核加载到内存中,kexec_load将捕获内核也一起加载到内存中,加载地址可在grub.conf中配置,默认为auto,kexec-tools则将捕获内核的加载地址传给原内核;
(2) 原内核系统崩溃的触发点设置在die()、die_nmi()、panic(),前两者最终都会调用panic(),发生崩溃时dump出进程信息、函数栈、寄存器、日志等信息,并使用ELF文件格式编码保存在内存中,调用machine_kexec(addr)启动捕获内核,捕获内核最终转储core文件。捕获内核通常可以采用两种方式访问原内核的内存信息,一种是/dev/oldmem,另一种则是/proc/vmcore;
(3) crash工具提供一些调试命令,可以查看函数栈,寄存器,以及内存信息等,这也是分析问题的关键,下面列举几个常用命令。
log:显示日志;
bt:加参数-a时显示各任务栈信息;
sym:显示虚拟地址对应的标志符,或相反;
ps:显示进程信息;
dis:反汇编函数或者虚拟地址,通过与代码对比,结合寄存器地址找出出错代码中相关变量的地址;
kmem:显示内存地址对应的内容,或slab节点的信息,若出错地方涉及slab,通过kmem则可看到slab的分配释放情况;
rd:显示指定内存的内容;
struct:显示虚拟地址对应的结构体内容,-o参数显示结构体各成员的偏移;
下面借用一下别人画的内核panic流程框图:
通常内核soft lockup问题不会导致内核崩溃,只有设置softlockup_panic才会触发panic,所以若未设置可在崩溃前自行查看系统日志查找原因,如果查找不出原因,再借助人为panic转储core文件,这时通过crash分析问题。
linux中借助看门狗来检测soft lockup问题,每个cpu对应一个看门狗进程,当进程出现死锁或者进入死循环时看门狗进程则得不到调度,系统检测到某进程占用cpu超过20秒时会强制打印soft lockup警告,警告中包含占用时长和进程名及pid。
linux内核设置不同错误来触发panic,其触发选项均在/proc/sysy/kernel目录下,包含sysrq、softlockup_panic、panic、panic_on_io_nmi、panic_on_io_nmi、panic_on_oops、panic_on_unrecovered_nmi、unknown_nmi_panic等。
二、panic实例分析
涉及的代码是处理DNS请求,在DNS请求中需要对重复出现的域名进行压缩,以达到节约带宽的目的,压缩的思想很简单,采用最长匹配算法,偏移量基于DNS头地址。系统中每个cpu维护一个偏移链表,每次处理一个域名前都会到链表中查询这个域名是否已经处理过,若处理过这时会使用一个偏移值。
例如:www.baidu.com,链表中会存储www.baidu.com、baidu.com、com三个节点,之后每次查询链表时若查到则使用节点中的偏移,若未查到则将新出现的域名按照这个规律加到链表中。
这里贴出基本的数据结构:
struct data_buf {
unsigned char* buf;
u16 buflen;
u16 pos;
};
struct dns_buf {
struct data_buf databuf;
struct sk_buff* skb;
u32 cpuid;
};
代码中静态分配了50个节点,每处理一个域名时取一个节点挂入链表中,每次panic都发生在查询节点函数get_offset_map(data_buf *dbuf, char *domain)的list_for_each()中。因此,猜测其可能是由于遍历到空指针或非法地址导致的,原因应该是由于内存溢出导致的内存互踩,每次panic都是发生在晚上,一般会发生几次。
起初的办法是在函数中加入判断条件来调用BUG_ON()函数来提前触发panic,通过验证发现有时会出现list_head节点地址为空,有时会出现遍历节点数超过50次,用反汇编dis命令打印出错节点的地址,最终也只看出了某个节点为空,不过对第二种情况有了一些猜测,压缩时所指向的域名可能出现了问题,在这个函数中徘徊了很久都没有定论,最终得出结论:单纯从这个函数入手是不可能找到原因的,只有打印出原始请求信息和响应信息才能进一步分析,但这个函数所传入的信息很有限,最后只能把所有信息都存储在data_buf这个结构体中,这是我能想到的最直接也是代码改动最小的办法,随着问题的深入这个结构体已经达到了几十个变量,甚至超过了2k大小,也就是超过了函数栈的大小。问题向上追溯到了pskb_expand_head()函数,在这个函数之前打印原始的skb内容,之后打印当前处理的skb内容,发现两次skb的内容不同,当时就误认为问题出在pskb_expand_head()函数上,下面贴出当时的分析报告: