在集成preempt rt到loongarch4.19代码的过程中,使用ltpstress做内核压力测试,ltpstress运行一小时左右后死机,没有重启,鼠标键盘串口等没有任何反应。对比了一下标准的loongarch4.19测试结果,标准的loongarch4.19并没有这个现象和问题。
现象:ltpstress运行一小时左右后死机,没有重启,鼠标键盘串口等没有任何反应。
日志:串口中没有OOPS这种直接表明死机原因的日志,但发现了很多如下的warning:
[ 1108.265436][ 0] BUG: scheduling while atomic: mmap1/89622/0x00000002
[ 1108.265543][ 0] Preemption disabled at:
[ 1108.265553][ 0] [<900000000043fe30>] quicklist_trim+0x40/0x198
[ 1108.265559][ 0] CPU: 0 PID: 89622 Comm: mmap1 Tainted: G E 4.19.90-40.0.rt.v2207.a.ky10.loongarch64 #1
...
[ 1108.265615][ 0] Call Trace:
[ 1108.265623][ 0] [<900000000020a4c4>] show_stack+0x34/0x140
[ 1108.265631][ 0] [<90000000010d1894>] dump_stack+0x9c/0xe0
[ 1108.265637][ 0] [<900000000025a0c8>] __schedule_bug+0x98/0xc0
[ 1108.265642][ 0] [<90000000010f0db4>] __schedule+0x854/0xae0
[ 1108.265645][ 0] [<90000000010f108c>] schedule+0x4c/0x130
[ 1108.265649][ 0] [<90000000010f2b7c>] rt_spin_lock_slowlock_locked+0x114/0x290
[ 1108.265651][ 0] [<90000000010f2d40>] rt_spin_lock_slowlock+0x48/0x70
[ 1108.265655][ 0] [<90000000003ba1d8>] free_pcppages_bulk+0x50/0x558
[ 1108.265657][ 0] [<90000000003bc304>] free_unref_page+0x18c/0x1d8
[ 1108.265660][ 0] [<900000000043fef8>] quicklist_trim+0x108/0x198
[ 1108.265665][ 0] [<900000000040aa14>] arch_tlb_finish_mmu+0x84/0x140
[ 1108.265668][ 0] [<900000000040ac98>] tlb_finish_mmu+0x28/0x50
[ 1108.265671][ 0] [<9000000000405cf4>] unmap_region+0xdc/0x120
[ 1108.265674][ 0] [<9000000000407e3c>] do_munmap+0x264/0x438
[ 1108.265676][ 0] [<900000000040806c>] vm_munmap+0x5c/0xb0
[ 1108.265678][ 0] [<90000000004080d0>] sys_munmap+0x10/0x20
[ 1108.265682][ 0] [<9000000000212354>] syscall_common+0x24/0x38
[ 1108.265692][ 0] ------------[ cut here ]------------
我们大致可以从日志中看出来,当代码运行到quicklist_trim的时候,调用了preempt_disable,是当前任务的preempt count大于0,也就是禁止其他任务抢占当前的任务。但在随后的代码中,调用了rt_spin_lock_slowpath。
spinlock在标准内核的语义中,是一种忙等状态,中间没有任务抢占和切换,但在preempt rt中,spinlock被改造成立rt_mutex,那么当代码调用rt_spin_lock_slowpath申请资源的时候,如果该资源目前被别的任务所占用,当前任务则放弃cpu,主动调用schedule放弃cpu,进入休眠状态,等待资源的释放。
这样就产生了一个warning,在preempt rt中,这是一个不合理的存在。
1)代码分析
从waring中可以看出ltpstress的genload进程正在调用unmap函数,我们根据warning的call stack来看一下调用过程:
do_munmap -->
tlb_finish_mmu -->
arch_tlb_finish_mmu -->
quicklist_trim -->
get_cpu_var
free_pages -->
rt_spin_lock_slowlock -->
rt_spin_lock_slowlock -->
__schedule
put_cpu_var
其中,quicklist_trim函数在get_cpu_var中关闭抢占,在put_cpu_war打开抢占,但在free_pages中调用了rt_spin_lock_slowpath申请锁,如果申请不到,则调用schedule释放cpu,进入休眠状态。
我不是很了解内存管理的quicklist,在网上查了一下,讲解如下:
内核在分配和释放页帧时需要做很多事情,会耗费较多的时钟周期,为此,我们可以通过一个链表来缓存将要被释放的页,也就是不真正的去释放,而是将其放在一个链表上,下次在分配页时直接从链表上取,这样可以大大减少页帧在释放和分配时的工作量,进而提高性能。在内核中,有这样一个链表——quicklist,它是 per cpu 变量,每个 cpu 对应一个 struct quicklist 类型的结构体变量,通过这个结构体实现了缓存页帧的管理。
具体信息请参阅如下链接:
2)与arm64测试的对比
之前在arm64平台做ltpstress测试的时候,并没有发现这个warning,保险起见,还是用arm64的设备再跑一次测试,验证一下。
果然,在若干小时的测试之后,运行正常,并没有这样的warning,那接下来看一下两个平台代码的不同。
3)与arm64代码的对比
Loongarch在quicklist_trim确实与社区的代码和arm64平台的代码有所不同,一开始,我们想把loongarch新增的代码逻辑去掉,再进行一次测试。
但是,通过继续阅读代码,发现arm64的代码会调用到free_page,虽然与loongarch调用的free_pages((unsigned long)p, q->order);有所不同,但最终也会去调用rt_spin_lock_slowpath,那么为什么arm64平台没有出现这个warning?
4)使用function graph分析
是不是arm64平台的代码根本没有调用quicklist_trim,要验证这个问题,我们使用了ftrace的function_graph:
我们在一个shell中运行ltpstress,另一个shell使用如下命令来抓取ftrace log:
trace-cmd start -p function_graph -l ‘do_munmap’; sleep 1;trace-cmd stop
trace-cmd show > do_munmap_log.txt
Function graph可以帮助我们得到一个do_munmap函数完整的调用路径,如下:
2) | do_munmap() {
2) | find_vma() {
=====
2) | unmap_region() {
...
===
2) | tlb_finish_mmu() {
===
2) | arch_tlb_finish_mmu() {
2) | tlb_flush_mmu() {
2) | tlb_flush_mmu_free() {
2) 0.542 us | tlb_table_flush();
2) | free_pages_and_swap_cache() {
......
2) + 13.354 us | }
2) + 32.896 us | }
2) + 35.104 us | }
2) + 72.000 us | }
2) + 74.229 us | }
2) + 75.437 us | }
2) + 76.604 us | }
2) + 77.708 us | }
2) ! 240.542 us | }
可以看到,在arm64的unmap中,并没有调用到quicklist_trim。
进一步阅读代码发现,在函数arch_tlb_finish_mmu中,调用了函数check_pgt_cache,在arm64的代码中函数check_pgt_cache是一个空函数,在loongarch的代码中,函数check_pgt_cache的代码如下:
arch/loongarch/include/asm/pgalloc.h
static inline void check_pgt_cache(void)
{
#ifdef CONFIG_QUICKLIST
quicklist_trim(QUICKLIST_PGD, NULL, 128, 16);
quicklist_trim(QUICKLIST_PMD, NULL, 128, 16);
quicklist_trim(QUICKLIST_PTE, NULL, 128, 16);
#endif
}
我对quicklist并不是很了解,同时,到目前为止,我们只有这个warning,还不能确认它就是死机的rootcause,但已经可以将CONFIG_QUICKLIST关掉试一下了。
经过测试,ltpstress运行48小时,正常。
长时间的关闭抢占会被内核的soft lockups检测出来的,如果打开如下开关的话,可以重启系统:
结合kdump是可以找到谁在持有锁,让等待着超过了120秒。
但这种关闭了抢占之后让出cpu的情况,我确实没有做过类似的实验.
理论上,preempt count就是task_struct中的一个变量,一个任务级别的变量,尽管该任务调用了preempt_disable,是preempt count计数器增加,但它自己主动放弃cpu,会造成什么后果,目前还不清楚。从代码来分析,该任务主动让出cpu,那么调度器会正常调度其他任务,包括检测soft lockup的watchdog,这个watchdog的线程cpu_stopper_thread会调用__touch_watchdog来踢狗,那么在hrtimer的is_softlockup是不会检测到的。
或者会不会是在free_pcgpages_bulk中申请zone->lock的时候,zone->lock被长期占用,没有释放。
此外,还有一个可以从代码中分析出来的问题,quicklist_trim在get_cpu_var获取本cpu的quicklist,同时关闭抢占后放弃cpu,当这个任务再次被调度的时候,使用的quicklist就不是这个cpu的了,这样就是一个内存的错误了。这也是get_cpu_var为什么要关闭抢占的原因。
可以写一个实验代码来构造这个环境,需要写一个内核模块,在里面创建若干个内核线程,这几个线程同时关闭抢占,然后申请同一个锁,看看内核如何运行。