[kernel] exit_mmap BUG_ON()
上个月,生产服务器上报来了内核bug:
------------[ cut here ]------------
kernel BUG at mm/mmap.c:2352!
invalid opcode: 0000 [#1] SMP
last sysfs file: /sys/devices/system/cpu/cpu23/cache/index2/shared_cpu_map
CPU 13
....
[<ffffffff8105fb55>] mmput+0x65/0x100
[<ffffffff81066605>] exit_mm+0x105/0x140
[<ffffffff810667ed>] do_exit+0x1ad/0x840
[<ffffffff81078680>] ? __sigqueue_free+0x40/0x50
[<ffffffff81066ec1>] do_group_exit+0x41/0xb0
[<ffffffff8107ccf8>] get_signal_to_deliver+0x1e8/0x430
[<ffffffff8100a554>] do_notify_resume+0xf4/0x8a0
[<ffffffff811f82f6>] ? security_task_kill+0x16/0x20
[<ffffffff8107a992>] ? recalc_sigpending+0x32/0x80
[<ffffffff8107ad15>] ? sigprocmask+0x75/0xf0
[<ffffffff8107ae11>] ? sys_rt_sigprocmask+0x81/0x100
[<ffffffff8107ad15>] ? sigprocmask+0x75/0xf0
[<ffffffff8100b301>] int_signal+0x12/0x17
原因就是 exit_mmap 函数最后一行的BUG_ON被触发了(我们用的是 2.6.32 内核)
void exit_mmap(struct mm_struct *mm)
{
....
BUG_ON(mm->nr_ptes > (FIRST_USER_ADDRESS+PMD_SIZE-1)>>PMD_SHIFT);
}
[ 这个 (FIRST_USER_ADDRESS+PMD_SIZE-1)>>PMD_SHIFT 其实就是0 ]
从代码直观能想到的就是进程在退出的时候pte没有释放对,或者nr_ptes计数漏了,导致最后一步nr_ptes没有变成0。
想归想,这个非常难重现。但是很快,好运来了,同事在开发cgroup的过程中无意中也触发了了这个BUG_ON,触发的方法是在 __mem_cgroup_uncharge_common 函数里加了一对 down_read/up_read (除了这一对,没有任何别的操作)。于是我开始想象:这一对 down_read/up_read 没有做任何与page或者pte相关的事情,却引起 nr_ptes计算出错,那八成是这一对锁的添加触发了原来的某个race condition,最终导致 nr_ptes 没有计算对。
于是,我沿着这条思路,顺着 exit_mmap 一路 trace_printk ,终于发现是在 page_remove_rmap() 前后nr_ptes开始不对,调用路线还挺深(花了不少力气才找到):
--> exit_mmap
--> unmap_vmas
--> unmap_page_range
--> zap_pud_range
--> zap_pmd_range
--> zap_pte_range
--> page_remove_rmap
为什么是 page_remove_rmap ? 又花了两天才发现,原来exit_mmap里用到了 tlb_gather_mmu/tlb_finish_mmu,就是把要清空页表项的page暂存在一个 struct mmu_gather 里,然后在tlb_finish_mmu时统一清空页表,以提高性能。2.6.32内核里的这个 struct mmu_gather 是per cpu变量(每个cpu一个),这就意味着在 tlb_gahter_mmu 和 tlb_finish_mmu 之间进程不能切换CPU,否则拿到的 struct mmu_gather就可能是另一个CPU上的变量,那就彻底不对了(upstream为了支持抢占,已经将mmu_gather改为 stack argument)。糟糕之处就在于,page_remove_rmap调用了mem_cgroup_uncharge_page进而调用了__mem_cgroup_uncharge_common,而我们在里面加了一个down_read,而down_read里有一句might_sleep(),结果,进程睡眠了,等他醒过来,可能已经身处另一个CPU,于是mmu_gather拿错了,于是nr_ptes不对了....(其实我挺好奇:为什么down_read里要加一句might_sleep?求高手解答)
花了三天辛辛苦苦的找,最后发现不是race condition,就是自己加的down_read造成的问题。
但是毕竟,生产报来的bug是没有这一更改的,那就是另有原因,还得查。
上周刘峥同学给了个链接 https://lkml.org/lkml/2012/2/15/322 ,看来redhat也遇到不少exit_mmap BUG_ON(),不过他们发现是Transparent Huge Page造成的,咱们报bug的生产服务器上并没有用到THP,还不是一个问题。不过,我总体感觉,进程退出的代码路径里,这个 BUG_ON(mm->nr_ptes > 0) 是最后一关,结果成了bug触发的汇集处,很多别处的甚至不是mm相关的代码错误都可能触发这个BUG_ON,比如这个 https://lkml.org/lkml/2012/5/25/553
后来,生产服务器从 2.6.32-131 升到 2.6.32-220 后,再没报过这个BUG_ON,至于终极的原因....可能是mm部分的bug fix,也可能是某个驱动的升级,这个,只有redhat知道了
相关文章
- 解block层死锁 - 12 19, 2012
- EMC USD组 招聘 - 09 24, 2012
- hadoop集群上捉到linux kernel bug一个 (答读者提问) - 08 31, 2012
感觉rhel 6中加入thp的支持并且默认打开有些鲁莽了,我们在开发中也遇到过thp引起进程挂起的情况,不过从thp的性能测试来看它还是相当有前途的。
down_read会引起进程休眠,中断中不允许休眠,如果在中断代码中错用了down_read,might_sleep会报告错误信息。
@casualfish 我们这边的数据库团队用过THP以后觉得效果明显,因为他们的机器都是大内存
@casualfish ,是这样,只不过,为何要在down_read前might_sleep一下,还是让我很费解 :)
应该只其一个提示作用,提示这里不用能用这个函数,见这个链接http://www.embexperts.com/forum.php?mod=viewthread&tid=570。
@casualfish 这篇embexperts的文章有问题,我看到的2.6.32-220代码跟他完全不同。
#ifdef CONFIG_PREEMPT_VOLUNTARY
extern int _cond_resched(void);
# define might_resched() _cond_resched()
#else
# define might_resched() do { } while (0)
#endif
rhel6默认是PREEMPT_VOLUNTARY,所以might_sleep并不是啥也没做,而是做了_cond_resched(),
int __sched _cond_resched(void)
{
if (should_resched()) {
__cond_resched();
return 1;
}
return 0;
}
might_sleep是有可能做调度的
sorry,看了一下might_sleep的代码,确实在新版本中might_sleep提供了cond_resched功能,所以might_sleep应该也提供了调度检测点的功能。
内核中调用这个函数的地方通常都是进行IO、访问内存等可能引起进程休眠的地方。虽然调用down_read后,进程有可能并不休眠,但我想如果在down_read导致休眠之前主动让出cpu让出给其他进程,会有更好的性能,这应该是放置在这的原因吧:)