[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知道了


相关文章

分类

7 Comments

casualfish said:

感觉rhel 6中加入thp的支持并且默认打开有些鲁莽了,我们在开发中也遇到过thp引起进程挂起的情况,不过从thp的性能测试来看它还是相当有前途的。

casualfish said:

down_read会引起进程休眠,中断中不允许休眠,如果在中断代码中错用了down_read,might_sleep会报告错误信息。

DongHao Author Profile Page said:

@casualfish 我们这边的数据库团队用过THP以后觉得效果明显,因为他们的机器都是大内存

DongHao Author Profile Page said:

@casualfish ,是这样,只不过,为何要在down_read前might_sleep一下,还是让我很费解 :)

casualfish said:

应该只其一个提示作用,提示这里不用能用这个函数,见这个链接http://www.embexperts.com/forum.php?mod=viewthread&tid=570。

DongHao Author Profile Page said:

@casualfish 这篇embexperts的文章有问题,我看到的2.6.32-220代码跟他完全不同。

  1. define might_sleep() do { might_resched(); } while (0)

#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是有可能做调度的

casualfish said:

sorry,看了一下might_sleep的代码,确实在新版本中might_sleep提供了cond_resched功能,所以might_sleep应该也提供了调度检测点的功能。
内核中调用这个函数的地方通常都是进行IO、访问内存等可能引起进程休眠的地方。虽然调用down_read后,进程有可能并不休眠,但我想如果在down_read导致休眠之前主动让出cpu让出给其他进程,会有更好的性能,这应该是放置在这的原因吧:)

留言:

关于文章

This page contains a single entry by DongHao published on 06 6, 2012 4:38 PM.

Overlayfs 简介 was the previous entry in this blog.

netoops启动时报 "XX is a slave device, aborting" is the next entry in this blog.

Find recent content on the main index or look in the 存档 to find all content.