您的当前位置:首页正文

loongarch集成preempt rt后ltpstress死机的问题分析

2024-11-20 来源:个人技术集锦
  • 问题描述

在集成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为什么要关闭抢占的原因。

可以写一个实验代码来构造这个环境,需要写一个内核模块,在里面创建若干个内核线程,这几个线程同时关闭抢占,然后申请同一个锁,看看内核如何运行。

 

显示全文