环境
- 内核版本: 3.0.8
- CPU信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
问题
2013年8月份的时候,在一些板子(海思3531平台)上面出现了一些奇怪的问题,查看dmesg都出现了如下打印:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
分析
针对上面的dmesg信息进行分析后,得出了如下结论:
- 导致出错的当前(current)进程是
pidof
- pidof程序的原理就是:遍历所有进程的stat(/proc/PID/stat)文件,找出和参数一样的进程ID(pid)。
- /proc/PID/stat文件的内容是由
do_task_stat
(见fs/proc/array.c)函数生成 do_task_stat
会调用get_wchan
(见arch/arm/kernel/process.c),来获取进程的Waiting Channnelget_wchan
函数使用unwind_frame
(见arch/arm/kernel/stacktrace.c)函数回溯进程的栈指针(fp/sp),试图获取进程最后一个非调度函数
的函数调用unwind_frame
却导致了一个内核paging错误
根据上面的初步分析,结合我们自己的应用程序,推测造成这个问题的应用层场景如下: 1. 有两个进程p1/busy_worker 2. busy_worker有30多个线程 3. p1不停执行system(pidof(“busy_worker”))
根据内核打印出来的PC/LR寄存器,可以知道,是unwind_frame函数中出现的内存访问错误:
1
|
|
反汇编内核代码(#号开头的行, 是c语言注释)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
反汇编结合内核源代码, 可知出错的时候执行到下面的语句:
1 2 |
|
出错时候的平台寄存器如下:
1 2 3 4 5 |
|
在出现的内存访问错误之前, fp寄存器值已经保存在寄存器r2中了, 对应的值是ffffffff
。
也就是说, 传递给unwind_frame函数的stackframe->fp = 0xffffffff。
出现问题的情况下应用层的用法都是没有问题的,所以这应该是一个内核BUG。
虽然是很清楚导致此内核Bug的原因,但是相关内核函数都没有处理竞争情况,当时就怀疑这个BUG应该和抢占有关系。
由于不太确定,就去Stack Overflow上面问了个问题:
unwind_frame cause a kernel paging error,不过也一直没有任何有价值的回复。
巧合
时隔3个月的后,又想起了这个问题,于是去kernel git上搜索了一番,发现2013年后针对unwind_frame函数的修改挺多。 其中Konstantin Khlebnikov提交的一个patch就解决了这个内核Bug。 更巧的是:修改日志中引用了我在Stack Overflow上提的问题。
Root cause
Konstantin Khlebnikov在修改日志中写道:
get_wchan() is lockless. Task may wakeup at any time and change its own stack, thus each next stack frame may be overwritten and filled with random stuff. /proc/$pid/stack interface had been disabled for non-current tasks, see [1] But ‘wchan’ still allows to trigger stack frame unwinding on volatile stack. This patch fixes oops in unwind_frame() by adding stack pointer validation on each step (as x86 code do), unwind_frame() already checks frame pointer.
大意就是: get_wchan()是没有加锁的,而进程可以在任何时候执行并修改自己的栈,所以进程的栈可能会被修改并填充随机的值。
假设我们的pidof进程PID为100,记做pidof(100),busy_worker进程PID为99,记做busy_worker(99), 我们碰到的问题就是: * pidof(100)通过读/proc/99/stat文件触发了内核去获取busy_worker(99)的Waiting Channnel(get_wchan) * busy_worker(99)执行到获取文件系统信息时候被抢占,此时其调用栈看起来可能是这个样子:
1 2 3 4 5 6 |
|
- get_wchan将busy_worker(99)的fp/sp/lr/pc都保存在stackframe结构体中
1 2 3 4 |
|
- 由于没有锁,此时busy_worker(99)又开始在CPU上执行(抢占),它可能又去执行其他功能代码,调用栈可能是:
1 2 3 4 5 6 |
|
- 内核调用unwind_frame开始回溯busy_worker_main(99)的栈。
这个时候问题出现了,此时busy_worker_main(99)的栈已经和第2步中完全不一样了,内核再调用下面语句去访问
上次记录
的栈里面内存,有点类似于访问已经释放了的内存:
1 2 3 |
|
这个时候,这块内存保存的可能是某个函数的局部变量、或者被push进来的其他寄存器。 总而言之,访问这块内存会得到一个随机值,而这里恰好是0xffffffff,导致了内核访问错误的内存,从而出现paging error.