Linux内核崩溃诊断实战指南
Linux崩溃了,你能干什么
如果你发现你的Linux机器重启了,你能查出来是什么原因导致的吗。
绝大多数人是束手无策的,今天,本文(结合真实案例)
教你怎么做。
一、首先你要有dump文件
接触过Linux的,大概知道有个core dump的,很多人以为这是核心转储,nonono,它是内存转储的意思,core来源于早年间磁芯存储器的那个磁芯(core)。
实现core dump机制有很多工具,kdump技术是目前最可靠、最常用的,已被主要的Linux厂商选用。当Linux内核崩溃时,kdump工具捕获当时内存等状态信息,生成转储文件(vmcore),保留现场证据,然后才重启。
当然,如果要用kdump,需要安装kdump工具、修改内核启动参数、修改kdump配置文件参数、启动kdump服务功能。
二、怎么解析dump文件
如果你装了kdump,Linux崩溃重启后,你会在/var/crash目录发现vmcore文件,恭喜你,你可以用它来发现根因了。
为了分析core dump,你需要安装crash工具,crash工具是RedHat公司提供的一个开源的内核分析工具,它在gdb的基础上实现了解析内核的功能。
你还需要安装Linux内核相应版本的debuginfo包,这个包安装好后会在操作系统上生成一个vmlinux文件,该文件包含完整的符号信息,用于提供调试信息。
三、怎么使用crash工具
crash工具包括了很多命令,包括查看内核日志的log命令、查看调用栈的bt命令、查看进程情况的ps命令、查看某个地址对应符号的sym命令、查看文件系统信息的files命令等。
总之,有了这个大杀器,你再有一点操作系统知识,有一点编程知识,就能干一般人干不了的事了。
比如,有一天,你发现你的若干台服务器意外重启了,你一脸懵,你的领导责令你务必找到根因。
你就可以悠然打开crash工具,来一趟探因之旅:
crash /usr/lib/debug/lib/modules/2.6.32-754.35.1.el6.x86_64/vmlinux /var/crash/vmcore
上面那个vmlinux,是调试所需的内核镜像;上面那个vmcore,就是core dump。
四、用sys命令看看基本信息
用crash打开vmcore文件后,使用sys命令,你可以看到系统内核的基本信息,比如崩溃时系统中的进程数量、系统内核版本、内存大小、系统崩溃时的报错信息等。
比如,报错信息是:BUG: unable to handle kernel paging request at ffffffffa0395070
懂的人都懂,这是内核分页请求报错。
可能的原因是:错误的内存访问、内存不足、硬件故障等等。
当然最可能的是:错误的内存访问。
下面,我们用bt命令看看内核崩溃的调用栈信息。
所谓调用栈,就是谁调用了谁,谁又进一步调用了谁。
五、用bt命令查看进程信息
这一步是非常重要的,毕竟,我们非常想看到,机器是怎么一步步崩溃的。
使用crash工具的bt命令(栈跟踪backtrace的缩写)查看调用栈信息,显示信息如下:
crash> bt
第1行 PID: 177488 TASK: ffff880435b92ab0 CPU: 2 COMMAND: "ss"
第2行 #0 [ffff880437c0b7e0] machine_kexec at ffffffff8104179b
第3行 #1 [ffff880437c0b840] crash_kexec at ffffffff810d7a52
第4行 #2 [ffff880437c0b910] oops_end at ffffffff81560310
……
第10行 #8 [ffff880437c0bb40] page_fault at ffffffff8155f265
第11行 [exception RIP: strnlen+9]
第12行 RIP: ffffffff812ae3a9 RSP: ffff880437c0bbf8 RFLAGS: 00010286
……
第25行 #15 [ffff880437c0be70] proc_reg_read at ffffffff8120faf0
第26行 #16 [ffff880437c0bec0] vfs_read at ffffffff811a3447
第27行 #17 [ffff880437c0bf00] sys_read at ffffffff811a3791
第28行 #18 [ffff880437c0bf50] system_call_fastpath at ffffffff81566391
不用细看,bt在第一行就说了,崩溃就是“ss”这个程序引起的。从第28行开始,ss调用了system_call_fastpath、然后是sys_read(第27行)、然后是我这里省略了的一连串调用,然后是第10行令人胆战心惊的page_fault,然后是oops_end(哦,要完蛋了,第4行)、crash_kexec(准备kdump,第3行)、machine_kexec(调起一个新内核采集信息,第1行),然后kdump就生成了core dump文件:vmcore。
注意第25行,可以看出ss干了一件事,调用了proc_reg_read,这表明它读了proc文件系统。
proc是一个虚拟文件系统,提供了内核和进程的运行信息。
ss读proc文件是正常的,因为要读取一些内核层面的信息,但正是这个读,导致了崩溃。
注:ss是一个用于查看和分析Linux系统中的网络连接和套接字(socket)状态的工具。它是 netstat命令的替代品,通常比netstat更加高效和快速。
下面我们看看ss这个进程的具体信息。
六、用ps命令查看都有哪些进程
使用crash工具的ps命令查询内核崩溃前所有进程的状态信息。
比如:
crash> ps
PID PPID CPU TASK ST %MEM VSZ RSS COMM
...
177488 64302 1 ffff880436662ab0 RU 0.0 6280 568 ss
...
当然,崩溃时的进程有很多了,这里只显示ss进程,可以看到它的进程号为177488。
有了ss的进程信息,现在我们看看ss到底调用了哪个文件。
七、用files命令查看进程访问了哪个文件
一般是使用struct file命令查看某进程访问文件的信息。
刚才我们用ps命令查到ss的进程地址为ffff880436662ab0,用它作为struct file的输入参数。
crash> struct file.f_path ffff880436662ab0
f_path = {
mnt = 0xffff880432adbe80,
dentry = 0xffff880101cae5c0
}
该命令显示了ss进程访问文件路径的结构信息,mnt表示文件所在的挂载点,dentry表示文件名在地址ffff880101cae5c0。
接下来使用files命令来解析这个dentry地址。
crash> files -d 0xffff880101cae5c0
DENTRY INODE SUPERBLK TYPE PATH
ffff880101cae5c0 ffff880101c1d598 ffff88043a23e800 REG /proc/slabinfo
此时可以得知,故障时,ss进程访问的文件是/proc/slabinfo。
注:/proc/slabinfo文件包含了当前内核中所有slab内存的详细信息。
八、使用sym命令看故障相关源码
从前面bt命令的调用栈信息看到,异常报错位置为[exception RIP: strnlen+9](第11行),RIP指向异常调用地址为ffffffff812ae3a9(第12行)。
使用sym命令,看看这个异常地址是个啥。
crash> sym ffffffff812ae3a9
ffffffff812ae3a9 (T) strnlen+9 /usr/src/debug/kernel-2.6.32-754.35.1.el6/ linux-2.6.32-754.35.1.el6.x86_64/lib/string.c: 407
这个命令很牛,它告诉我们,这个异常地址,对应的源码(string.c)和行号(第407行)都告诉我们了。
我第一次见到这个的时候,不禁惊呼,这么牛啊,从内存dump能看出问题源码在哪?
对,就这么牛,kdump很牛,debuginfo也很牛,一个用于调试,一个提供调试信息,程序员不会亏待自己的。
注:当你调试一个内核时,需要安装对应版本的debuginfo包,如下:
debuginfo-install kernel-debuginfo-common-2.6.32-754.35.1.el6.x86_64.rpm
当然,看到源码并不稀奇,Linux是开源的嘛!(如果你玩Windows,如果机器崩溃了,那就崩溃了吧。)
这段完整代码如下:
size_t strlen(const char *s) {
const char *sc;
for (sc = s; *sc != '\0'; ++sc)/*这就是那个第407行*/;
return sc - s;
}
这段代码通过for循环,从输入字符串初始字符s开始,遍历其所指的内容,循环直到遇到字符串结尾的空字符'\0',最后返回字符串长度。
那个*sc就是读sc这个地址里的内容。
但是,读着读着,就崩溃了,因为读到翔了。
八、用log命令查更多的内容
使用log命令查到的信息如下:
crash> log
...
VMAGENTMOD: 3846: init_module: get into init_module, syshook_enable:1
VMAGENTMOD: 177350: cleanup_module: get into cleanup_module
...
可以看出,内核崩溃前,有加载和卸载某个驱动模块的动作。相关的进程号是3846和177350。
经查询,这2个进程号都属于防病毒工具的进程。它调用的cleanup_module是内核函数,用于卸载驱动模块。
用crash的mod -t命令,显示内核模块加载的详细信息:
crash> mod -t
NAME TAINTS
syshook_linux (U)
vmsecmod (U)
在log命令的输出中,还可以看到“[last unloaded: vmsecmod]”,这说明,内核最后卸载的驱动模块就是vmsecmod。
另外,防病毒工具的本地日志也显示,服务器重启前刚刚执行了停止防病毒进程的操作。
这些信息都告诉我们,这次崩溃的发生,防病毒工具有相当的嫌疑。
注:mod的-t选项,是显示taints信息。所谓taints(污点),是内核运行时的一个标志,用来指示内核在运行过程中遇到了某些潜在问题或非标准情况。如果taints是U,表明该模块是未经签名的,是用户开发的。
九、回到sys命令
我们最开始使用sys命令查到有如下的报错信息:
BUG: unable to handle kernel paging request at ffffffffa0395070
用sym命令看看这个地方到底是何方神圣。
crash> sym ffffffffa0395070
ffffffffa0395070 (r) hash_info_mempool_name [vmsecmod]
可以看到,这个地址来自vmsecmod驱动,对应的源码是hash_info_mempool_name。
现在基本可以判断出来是怎么回事:
防病毒工具申请并使用了slab分配器提供的内存,相关信息记录在/proc/slabinfo中,ss进程会去查询slabinfo,获取必要的信息。就是在访问slabinfo时,造成了内核崩溃。
有人在崩溃前卸载了防病毒的驱动,停止了防病毒服务,按道理,防病毒申请的slab内存应该也释放掉,slabinfo中也不会有对应信息。但是,ss进程居然还在访问这块数据,说明防病毒进程申请的slab内存未正常释放!
知识普及:slab主要用于内核态中的内存分配,可高效分配和管理小块内存。在/proc/slabinfo这个虚拟文件中,记录了系统中所有slab内存块的信息,如对象数量和内存使用量等。ss读取/proc/slabinfo,获取与网络套接字相关的内存使用和分配信息,提供详细的网络连接状态。
九、原来是防病毒工具的bug
把上述信息给到防病毒厂商,他们的研发工程师分析确认,确实是防病毒客户端有bug,导致了这次重启。
bug很简单,就是在卸载vmsecmod驱动时,应该同步释放所申请的slab内存区,但程序员没有这么做。
在重启的服务器上,有服务器管理工具,它会定期调用ss命令,ss会读取slabinfo,防病毒没有释放slab内存,所以ss仍然可以读取slabinfo中的指针,该指针却指向了已经释放了内存区(vmsecmod驱动曾经用过的地方)。
由于指针指向的内存已经释放,所以这就是访问非法地址,其实就是分页机制无法将该地址映射到物理地址,此时处理器就会向操作系统发出一个“page fault”错误,如果处理器此时处于超级用户模式,系统就会产生一个Oops,哦,完蛋了。
注:如果在用户态访问了非法地址,那么,你大概会得到一个经典的Segmentation fault(初级程序员的噩梦)。
十、当时发生了什么?
那天,某个工程师做了一件事,更新防病毒工具的许可,这个防病毒工具是企业版的,有一个管理端,还有运行在若干台服务器上的防病毒客户端。
他先在防病毒管理端导入软件许可,接着管理端将软件许可分发给每台服务器上的客户端,由客户端本地更新许可文件。
在客户端更新许可文件时,会先停止防病毒客户端进程(更新完许可文件后,再启动进程),停止进程会导致vmsecmod驱动模块的卸载,由于有bug,清理动作不完善,残留了无主的slab内存。
而服务器上部署的自动化工具,会定时执行ss命令,ss遍历slabinfo信息时,读取了在野的指针,引发page fault,内核崩溃。
后记
Linux这么稳定的内核都会崩溃。
做一个内核稳定的人,很不容易呢。