Openwrt系统内核的一个缺陷

开源软件的缺陷

从所周知Windows系统每隔一段时间就会推送系统补丁;一些补丁出于系统的安全性考虑,而另一些补丁则是对现有系统功能的缺陷修复。同样的,开源软件的缺陷也非常多,例如笔者就遇到过linaro开源编译器生成的可执行文件运行不正确的问题——一些有很大影响力的开源软件,其可靠性在某些时候会让开发者失望。

本文简要记录了笔者在使用树莓派的openwrt系统遇到的一个内核缺陷,以及一个初步但有效的解决方法。笔者在树莓派设备上使用的系统为Openwrt,内核版本为6.1(笔者验证Openwrt系统提供的5.15版本内核也存在该问题),系统基于以下Commit的代码仓库编译构建:

commit 02214ab8dce59ee6b599f8dfdacb0297dc5dcc24
Author: Hank Moretti <mchank9999@gmail.com>
Date:   Mon Aug 28 15:45:05 2023 +0000

    mediatek: fix sysupgrade error for WR30U

    The NMBM-Enabled layout did not use fit image,
    it just need default process. So it was been removed in platform.sh.

    It will fix sysupgrade error for xiaomi,mi-router-wr30u-112m-nmbm.

    Signed-off-by: Hank Moretti <mchank9999@gmail.com>

内核缺失的OOM-Kill

简单地说,当系统的内存被应用消耗完时,内核的默认配置会触发[OOM Killer](Out Of Memory Management (kernel.org)),通常会牺牲一个占用过多内存的进程以释放内存资源维持系统的正常运行。笔者基于之前博客文章中的代码做了以下修改:

diff --git a/fork-vfork.c b/fork-vfork.c
index 77d38ce..ba13b39 100644
--- a/fork-vfork.c
+++ b/fork-vfork.c
@@ -317,6 +317,12 @@ again:
     return 0;
 }

+static pid_t do_not_fork(void)
+{
+    errno = EINVAL;
+    return -1;
+}
+
 int main(int argc, char *argv[])
 {
     int ret, rval;
@@ -327,7 +333,10 @@ int main(int argc, char *argv[])
     rval = 0;
     count = 0;
     if (argc > 0x3) {
+        const char * arg3 = argv[3];
         g_fork = vfork;
+        if (arg3[0] == '0' && arg3[1] == '\0')
+            g_fork = do_not_fork;
         fputs("Using vfork to create child process.\n", stdout);
         fflush(stdout);
     } else {

然而,上面的代码编译生成的fork-vfork简单应用却没有被内核的OOM中止或杀死,而是停止运行了:

root@OpenWrt:~# uname -a
Linux OpenWrt 6.1.50 #0 SMP Fri Sep  1 21:45:47 2023 aarch64 GNU/Linux
root@OpenWrt:~# ./fork-vfork 128 0x800000 0
Using vfork to create child process.
INFO: memory blocks: 128, block size: 0x800000, total: 0x040000000
Memory allocated: 0x1/0x80, 8.00 MB / 1024.00 MB
	Error, failed to vfork process: Invalid argument
...
Memory allocated: 0x66/0x80, 816.00 MB / 1024.00 MB
	Error, failed to vfork process: Invalid argument
Memory allocated: 0x67/0x80, 824.00 MB / 1024.00 MB
	Error, failed to vfork process: Invalid argument

当应用停止运行时,只有在树莓派的串口终端上使用CTRL-C才能中止;如果通过网络SSH连接,会发生网络连接中断;实际上是网络连接超时了,该fork-vfork依然不会被内核的OOM Kill中止。

Openwrt内核OOM-Killer不工作的解决方法

这里直接给出解决方法。第一种比较简单直接,更改Linux内核运行时的配置,禁用内核的MULTIGEN_LRU功能:

echo 0 > /sys/kernel/mm/lru_gen/enabled

第二种解决方案是笔者偶然在以下代码中加入调试printk得出的;当增加内核调试printk时,OOM-Killer不工作的问题不再复现,于是笔者猜测内核的printk调试信息引入了必要的延迟,导致问题消失。不过因时间有限,笔者并没有继续分析更为根本的原因,只是调试确认以下内核改动有效:

diff --git a/mm/vmscan.c b/mm/vmscan.c
index 7e7d62099..8b1ac6920 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -6419,7 +6419,13 @@ static void shrink_node(pg_data_t *pgdat, struct scan_control *sc)
        bool reclaimable = false;

        if (lru_gen_enabled() && global_reclaim(sc)) {
+               int isint, iskswapd;
+               isint = in_interrupt();
+               iskswapd = current_is_kswapd();
                lru_gen_shrink_node(pgdat, sc);
+               if (!isint && iskswapd) {
+                       mdelay(15);
+               }
                return;
        }

以上调试过程,可以确认内核中的kswapd工作线程占用CPU使用率特别高。该内核线程会频繁间接调用到shrink_node函数,笔者在这里加了0.015秒的休眠,可以避免kswapd线程的CPU使用率过高。修改之后,Linux内核的OOM Killer可以正常工作:

[ 4779.375234] fork-vfork invoked oom-killer: gfp_mask=0x140dca(GFP_HIGHUSER_MOVABLE|__GFP_COMP|__GFP_ZERO), order=0, oom_score_adj=0
[ 4779.388752] CPU: 1 PID: 2225 Comm: fork-vfork Tainted: G         C         6.1.50 #0
[ 4779.398167] Hardware name: Raspberry Pi 3 Model B Plus Rev 1.3 (DT)
[ 4779.405153] Call trace:
[ 4779.408284]  dump_backtrace.part.0+0xc0/0xd0
[ 4779.413246]  show_stack+0x18/0x30
[ 4779.417227]  dump_stack_lvl+0x6c/0x88
[ 4779.421531]  dump_stack+0x18/0x34
[ 4779.425451]  dump_header+0x44/0x190
[ 4779.429525]  oom_kill_process+0x26c/0x270
[ 4779.434105]  out_of_memory+0x1c4/0x30c
[ 4779.438398]  __alloc_pages+0xcfc/0xeec
[ 4779.442668]  __folio_alloc+0x14/0x20
[ 4779.446753]  alloc_zeroed_user_highpage_movable+0x24/0x30
[ 4779.452675]  __handle_mm_fault+0x3b0/0xa7c
[ 4779.457301]  handle_mm_fault+0x168/0x2a0
[ 4779.461738]  do_page_fault+0x15c/0x3e0
[ 4779.465999]  do_translation_fault+0x88/0xa0
[ 4779.470682]  do_mem_abort+0x44/0x94
[ 4779.474644]  el1_abort+0x40/0x64

OOM-Kill不可用:该内核缺陷的来源

为何fork-vfork应用在运行过程中会停止运行?笔者的调试有两点结论:一,应用fork-vfork仍在不断地触发页错误;二,kswpad内核线程频繁尝试回收并释放内存,但一直未能释放足够的内存。这两个过程达到一种动态的平衡,其现象就是fork-vfork不会被OOM-Killer杀死,也不能继续运行。经笔者搜寻,Linux内核官方邮件组存在kswapd线程负载过高的讨论,不过笔者使用了相关的两个内核补丁确认修改后无效。查看Openwrt工程源码可知,这是Openwrt系统对Linux-6.1.50内核增加的LRU相关的内存管理补丁引入的系统缺陷:

020-v6.3-06-BACKPORT-mm-multi-gen-LRU-per-node-lru_gen_folio-lis.patch:656:+static void lru_gen_shrink_node(struct pglist_data *pgdat, struct scan_control *sc)
020-v6.3-06-BACKPORT-mm-multi-gen-LRU-per-node-lru_gen_folio-lis.patch:849:+static void lru_gen_shrink_node(struct pglist_data *pgdat, struct scan_control *sc)
020-v6.3-06-BACKPORT-mm-multi-gen-LRU-per-node-lru_gen_folio-lis.patch:865:@@ -6102,6 +6405,11 @@ static void shrink_node(pg_data_t *pgdat
020-v6.3-06-BACKPORT-mm-multi-gen-LRU-per-node-lru_gen_folio-lis.patch:870:+            lru_gen_shrink_node(pgdat, sc);
020-v6.3-07-BACKPORT-mm-multi-gen-LRU-clarify-scan_control-flags.patch:164:@@ -5338,11 +5331,19 @@ static void lru_gen_shrink_node(struct p
020-v6.3-07-BACKPORT-mm-multi-gen-LRU-clarify-scan_control-flags.patch:185:@@ -5360,7 +5361,7 @@ static void lru_gen_shrink_node(struct p

可见,开源嵌入式系统Openwrt系统的内核也或多或少存在一些系统缺陷。实际上,这类问题并不罕见:缺陷是软件开发过程中不可避免的,使用得越多,遇到的问题也会越多。Openwrt开源社区的主要负责人没有足够的资源对系统进行测试,这种引入大量backport的补丁是不推荐的。当然,笔者的首要目地是初步解决这个问题,尽管最终的方案并不十分完美(甚至可能引入其他未知的内核问题)。Linux内核的内存管理的分析,会花费大量的时间;感兴趣的伙伴可以调试一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值