status显示调度次数没有变化(与sched_debug吻合),然而/proc/1400/stat看到的用户态内核态时间却一直在增加,这是否矛盾了?
分析do_task_stat的实现,如果函数参数whole为1的话,则计算整个线程组的运行时间。
由于proc/pid/stat和/proc/pid/tasks/tid/stat分别给do_task_stat传递1和0,也就是说,要观察一个线程的运行时间,还是要通过后者。通过后者查看1400的用户态和内核态时间没有变化。
下面继续来看1486和1400线程在做什么。
~ # cat /proc/1486/stack [<98000000022d6e7c>] schedule+0xad0/0xdd8 [<980000000205299c>] do_softirq+0x84/0xb8 [<9800000002052f38>] irq_exit+0x6c/0x7c [<980000000200b940>] ret_from_irq+0x0/0x4 [<9800000002074c00>] futex_wait+0x55c/0x58c
~ # cat /proc/1400/stack [<98000000022d6e7c>] schedule+0xad0/0xdd8 [<98000000022d8f88>] rt_mutex_slowlock+0x314/0x3b0 [<9800000002040ffc>] sched_setaffinity+0x38/0x2fc [<9800000002041348>] sys_sched_setaffinity+0x88/0xa0 [<980000000201a534>] handle_sys64+0x54/0x70 |
1486的切换栈在futex_wait中不停变化,而1400的切换栈不变,一直保持在设置CPU亲和力处。因此可以推断,1486在用户态不停的调futex,而1400在设置亲和力的时候在内核态被抢占。
2.2 用户栈原始数据分析
为了搞清为何1486不停的在用户态调用futex,于是在内核添加代码,故障复现时,打印1486的用户栈。
由于1486不释放CPU,因此通过/proc/pid/stack看到的是上次切换的现场回溯,如果要查看1486当前在做什么,就要在时钟中断里添加回溯。另外由于1486也有可能是在内核态被中断,因此要获取用户态信息,使用task_pt_regs获取第一次切换内核栈时保存的现场。
复现时打印信息如下:
Call Trace: [<9800000002013cb4>] dump_stack+0x8/0x44 [<980000000205a4fc>] update_process_times+0x80/0x128 [<9800000002005184>] xlr_main_timer_interrupt+0xd0/0xec [<980000000208ac90>] handle_IRQ_event+0x7c/0x178 [<980000000208ae84>] __do_IRQ+0xf8/0x21c [<9800000002003a60>] plat_irq_dispatch+0x410/0x42c [<980000000200b940>] ret_from_irq+0x0/0x4 [<9800000002073668>] get_futex_key+0x20/0x180 [<9800000002074934>] futex_wait+0xb0/0x58c [<9800000002075464>] do_futex+0xb4/0x1dcc [<98000000020772cc>] sys_futex+0x150/0x17c [<980000000201a6b4>] handle_sys64+0x54/0x70
|
current process:[1486]-[0x12101f79c] dump user stack: 000000800018a850:0000000000000000 0000000000000000 ffffffdbfffeffff fffeffedff7fffff 000000800018a870:fffcafffffffffff ffdffbffbfbf7fff ffffffeffff7ffef ffffefaffbffffff 000000800018a890:fffffffefffffbfb ffffffffffffffff fffffbffffffffff ffffd7ffffffdffe 000000800018a8b0:ffffdffffffbfff7 fffffffffff7efff ff7fffefffd5ffff fffefffefdfeffdf 000000800018a8d0:f7fdffffcffdffff ffffffffff7fffbf 0000000121017c7c 000000800018a860 000000800018a8f0:000000012408b4b0 000000800018b0c0 0000000121017bf8 000000800018aa10 000000800018a910:00000000007d0f00 0000000000000001 0000005555f7fdf0 00000001200ee4c0 000000800018a930:0000005555f7fde0 000000012403a4c0 ffffffffddfef7ff fff77fdfbfffffee 000000800018a950:ff7feff7ffffffff fb7ffde6bbff7fff fffffbffdf7fffff ffffffb7fffeefff 000000800018a970:ffffff7fffffffff ffffdfd5bfcfffbf fffdffffffbe7dff 00000000dffffefd 000000800018a990:0000000000000000 0000000000000000 ffffff7ffff7ffbf dfffff7d7ffffffd 000000800018a9b0:000000800018b0c0 ffff7f3f7fff7fef 000000012408b4b0 000000800018b0c0 000000800018a9d0:0000000121017bf8 000000012403a4c0 000000012107432c ffffbffffff7ffaf 000000800018a9f0:0000000121017bf8 000000800018b0c0 00000000007d0f00 fffbbff5fffdffff |
epc的值显示1486位于__lll_lock_wait
抽取用户栈从高到低的有效值,对照反汇编得到的结果为:
1) Entry
2) __thread_start : move a0,v0
3) start_thread
4)bnez v0,121017cb0 <start_thread+0xb8>
从上面的结果我们可以大致猜测出,1486在刚启动到c库的start_thread附近时锁在了
pthread_mutex_lock处。
分析C库pthread_create流程:
其中,Entry是由于被作为函数传参传递导致的压栈,所以不做考虑。其余的栈的值,都能与C库实现对应上。
继续检查start_thread的反汇编,并重点分析0x121017c7c附近的指令,因其是栈里的最后一条有效PC值(请对照C库start_thread)
121017c48: 24050018 li a1,24 121017c4c: 24021494 li v0,5268 执行5268 (set_robust_list)系统调用(系统调用号见内核unistd.h,后同) 121017c50: 0000000c syscall 121017c54: dfa60150 ld a2,336(sp) 121017c58: 8cc203d4 lw v0,980(a2) 121017c5c: 30420004 andi v0,v0,0x4 跳转去执行5014(rt_sigprocmask)系统调用 121017c60: 14400089 bnez v0,121017e88 <start_thread+0x290> 121017c64: df99ad48 ld t9,-21176(gp) unwind_buf.priv.data.prev = NULL; unwind_buf.priv.data.cleanup = NULL; 121017c68: ffa00130 sd zero,304(sp) 121017c6c: ffa00138 sd zero,312(sp) 121017c70: df990458 ld t9,1112(gp) 121017c74: 0320f809 jalr t9 121017c78: 67a40080 daddiu a0,sp,128 v0是上一步setjmp函数的返回值,即not_first_call的值 如果not_first_call不为0,则去执行121017cb0分支,也就是直接跳出if判断。 if (__builtin_expect (! not_first_call, 1)) 121017c7c: 1440000c bnez v0,121017cb0 <start_thread+0xb8> 121017c80: dfa20150 ld v0,336(sp) 121017c84: dfa40150 ld a0,336(sp) 121017c88: 904303d3 lbu v1,979(v0) 121017c8c: 67a20080 daddiu v0,sp,128 此处跳转至lll_lock(pd->lock)所在的if分支0x121017ecc if (__builtin_expect (pd->stopped_start, 0)) 121017c90: 1460008e bnez v1,121017ecc <start_thread+0x2d4> 121017c94: fc8200c0 sd v0,192(a0) 121017c98: dfa20150 ld v0,336(sp) 121017c9c: dc590400 ld t9,1024(v0) 这里调用具体的线程回调,即线程总入口,这里没走到 THREAD_SETMEM (pd, result, pd->start_routine (pd->arg)); 121017ca0: 0320f809 jalr t9 |
根据用户态原始栈的分析,有较大嫌疑的地方是start_thread里的lll_lock(pd->lock)导致的死循环。检查lll_lock的底层实现:
void __lll_lock_wait (int *futex) { do { int oldval = atomic_compare_and_exchange_val_acq (futex, 2, 1); if (oldval != 0) lll_futex_wait (futex, 2); } while (atomic_compare_and_exchange_bool_acq (futex, 2, 0) != 0); } |
表现是用户态不停的通过lll_futex_wait进内核态。
最先怀疑是发生了乒乓pthread_mutex_lock。
即有两个线程A和B。B获取到lock,A尝试获取lock时发现值为1,因此陷入内核态futex_wait尝试将自己挂起,然而A进到内核后却发现锁已经被B释放,因此直接退出到用户态再次尝试加锁,如果B正好此时又把锁获取到,那么A将再次陷入内核态。如此反复造成这种现象。因为受害者A线程,一直是R状态,并且被动抢占计数在增加,所以推测出问题时,futex_wait不会将自己挂起,而是直接返回,即futex_wait可能从如下出口跑掉。
futex_wait: ret = get_futex_key(uaddr, fshared, &q.key); if (unlikely(ret != 0)) goto out_release_sem; ret = get_futex_value_locked(&uval, uaddr); … if (uval != val) goto out_unlock_release_sem;
|
进一步分析get_futex_key的实现,检查里面可能出错的分支,里面有一句比较可疑:
if (unlikely((vma->vm_flags & (VM_IO|VM_READ)) != VM_READ)) return (vma->vm_flags & VM_IO) ? -EPERM : -EACCES; |
即是说,如果futex尝试加锁的用户态地址,所在的vma区,具有VM_IO标志的话,则直接返回权限不够错误。再回忆起此锁的地址,确实是特殊分配而来。(发生死锁的lll_lock(pd->lock),其中pd线程描述符是在线程栈空间的头部分配)
检查分配流程:
确实会对vma区间加上VM_IO属性。
vma->vm_flags |= VM_IO|VM_READ|VM_WRITE|VM_EXEC|VM_MAYREAD|VM_MAYWRITE; |
进一步添加调试代码发现,系统确实是从get_futex_key处返回了无权限错误,导致futex_wait直接返回。其实这也是正常的,用户态不允许对IO空间进行加解锁处理。
因此故障复现的可能场景是:
1)父线程A,在pthread_create时,获取到父子线程同步锁 pd->lock(位于巨页栈),往下执行到准备为子线程设置亲和力时,在内核态被其子线程B抢占;
2)子线程B运行至start_thread --> lll_lock (pd->lock); 尝试获取巨页栈里的lock时,发现已经被父进程加锁,则B陷入内核态futex_wait,尝试将自己挂起。然后按照之前的分析,futex如果处于设置了VM_IO属性的vma区间时,是不会做任何处理而直接从内核态返回-EPERM的,因此B返回到用户态后,发现lock的值仍然为1(因为父线程得不到调度,不释放锁),因此再次进入内核态,也就形成了死循环。
3.2 解决方法及验证
去掉VM_IO属性后进行测试,未再发生故障。
1)练习阅读反汇编的能力对排查问题有很大帮助;
2)平时排查故障时可有意培养走查代码的能力。另外在排查故障时最好能摸清这段代码的实现机制,不是为了故障而排查。如果在跟踪的同时也把相关知识一起掌握,对以后排查相似问题有帮助。
3)其实以后可以不用打印用户态原始栈,可以直接在时钟中断里回溯,这样可以减少分析量。(见mips回溯原理)
1. <glibc 2.5> createthread.c pthread_create.c lowlevellock.c clone.S
2. linux kernel 2.6.21.7 futex.c