如何在 Linux 系统下即使在内存压力下也保持可执行代码在内存中的状态?

10

这里的目标是在Linux内存紧张时保持每个运行进程的可执行代码在内存中。
在Linux中,我可以通过以下命令立即(1秒)引起高内存压力并触发OOM-killer:stress --vm-bytes $(awk '/MemAvailable/{printf "%d\n", $2 + 4000;}' < /proc/meminfo)k --vm-keep -m 4 --timeout 10s(来自这里),在Qubes OS R4.0 Fedora 28 AppVM中最大RAM为24000MB。编辑4:可能相关的是我忘记提到的一点,即我没有启用交换分区(即CONFIG_SWAP未设置)。

dmesg报告:

[  867.746593] Mem-Info:
[  867.746607] active_anon:1390927 inactive_anon:4670 isolated_anon:0
                active_file:94 inactive_file:72 isolated_file:0
                unevictable:13868 dirty:0 writeback:0 unstable:0
                slab_reclaimable:5906 slab_unreclaimable:12919
                mapped:1335 shmem:4805 pagetables:5126 bounce:0
                free:40680 free_pcp:978 free_cma:0

有趣的部分是active_file:94 inactive_file:72,它们以千字节为单位,非常低。

问题在于,在内存压力期间,可执行代码正在从磁盘重新读取,导致磁盘抖动,进而引发操作系统冻结。(但在上述情况下,这种情况只发生了不到1秒钟)

我在内核mm/vmscan.c中看到了一段有趣的代码:

        if (page_referenced(page, 0, sc->target_mem_cgroup,
                            &vm_flags)) {
                nr_rotated += hpage_nr_pages(page);
                /*
                 * Identify referenced, file-backed active pages and
                 * give them one more trip around the active list. So
                 * that executable code get better chances to stay in
                 * memory under moderate memory pressure.  Anon pages
                 * are not likely to be evicted by use-once streaming
                 * IO, plus JVM can create lots of anon VM_EXEC pages,
                 * so we ignore them here.
                 */
                if ((vm_flags & VM_EXEC) && page_is_file_cache(page)) {
                        list_add(&page->lru, &l_active);
                        continue;
                }
        }

我认为如果有人能指出如何更改这个代码,使得它不是“给他们在活动列表上多走一圈”,而是“给他们无限次在活动列表上行走”,那么工作就做好了。或者也许还有其他办法?

我可以打补丁并测试自定义内核。我只是不知道要在代码中更改什么,以便始终将活动可执行代码保留在内存中(实际上,我相信这样可以避免磁盘抖动)。

编辑: 这是我迄今为止取得的成果(应用于内核4.18.5之上):

diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h
index 32699b2..7636498 100644
--- a/include/linux/mmzone.h
+++ b/include/linux/mmzone.h
@@ -208,7 +208,7 @@ enum lru_list {

 #define for_each_lru(lru) for (lru = 0; lru < NR_LRU_LISTS; lru++)

-#define for_each_evictable_lru(lru) for (lru = 0; lru <= LRU_ACTIVE_FILE; lru++)
+#define for_each_evictable_lru(lru) for (lru = 0; lru <= LRU_INACTIVE_FILE; lru++)

 static inline int is_file_lru(enum lru_list lru)
 {
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 03822f8..1f3ffb5 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -2234,7 +2234,7 @@ static void get_scan_count(struct lruvec *lruvec, struct mem_cgroup *memcg,

    anon  = lruvec_lru_size(lruvec, LRU_ACTIVE_ANON, MAX_NR_ZONES) +
        lruvec_lru_size(lruvec, LRU_INACTIVE_ANON, MAX_NR_ZONES);
-   file  = lruvec_lru_size(lruvec, LRU_ACTIVE_FILE, MAX_NR_ZONES) +
+   file  = //lruvec_lru_size(lruvec, LRU_ACTIVE_FILE, MAX_NR_ZONES) +
        lruvec_lru_size(lruvec, LRU_INACTIVE_FILE, MAX_NR_ZONES);

    spin_lock_irq(&pgdat->lru_lock);
@@ -2345,7 +2345,7 @@ static void shrink_node_memcg(struct pglist_data *pgdat, struct mem_cgroup *memc
             sc->priority == DEF_PRIORITY);

    blk_start_plug(&plug);
-   while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] ||
+   while (nr[LRU_INACTIVE_ANON] || //nr[LRU_ACTIVE_FILE] ||
                    nr[LRU_INACTIVE_FILE]) {
        unsigned long nr_anon, nr_file, percentage;
        unsigned long nr_scanned;
@@ -2372,7 +2372,8 @@ static void shrink_node_memcg(struct pglist_data *pgdat, struct mem_cgroup *memc
         * stop reclaiming one LRU and reduce the amount scanning
         * proportional to the original scan target.
         */
-       nr_file = nr[LRU_INACTIVE_FILE] + nr[LRU_ACTIVE_FILE];
+       nr_file = nr[LRU_INACTIVE_FILE] //+ nr[LRU_ACTIVE_FILE]
+           ;
        nr_anon = nr[LRU_INACTIVE_ANON] + nr[LRU_ACTIVE_ANON];

        /*
@@ -2391,7 +2392,8 @@ static void shrink_node_memcg(struct pglist_data *pgdat, struct mem_cgroup *memc
            percentage = nr_anon * 100 / scan_target;
        } else {
            unsigned long scan_target = targets[LRU_INACTIVE_FILE] +
-                       targets[LRU_ACTIVE_FILE] + 1;
+                       //targets[LRU_ACTIVE_FILE] + 
+                       1;
            lru = LRU_FILE;
            percentage = nr_file * 100 / scan_target;
        }

同样也可以在github上这里看到,因为在上面的代码中,tab键被转换成了空格!(镜像1镜像2
我已经测试了上述的补丁(现在我的最大RAM为4000MB,比之前少了20G!),即使是已知会把操作系统磁盘刷入永久冻结状态的Firefox编译,也不会再出现这种情况(oom-killer几乎立即杀死了有问题的进程),还有上面的stress命令,现在的输出结果为:

[  745.830511] Mem-Info:
[  745.830521] active_anon:855546 inactive_anon:20453 isolated_anon:0
                active_file:26925 inactive_file:76 isolated_file:0
                unevictable:10652 dirty:0 writeback:0 unstable:0
                slab_reclaimable:26975 slab_unreclaimable:13525
                mapped:24238 shmem:20456 pagetables:4028 bounce:0
                free:14935 free_pcp:177 free_cma:0
那是active_file:26925 inactive_file:76,近27兆的活跃文件...所以,我不知道这有多好。我是不是把所有的活跃文件都保留在内存中,而不仅仅是可执行文件?在 Firefox 编译期间,我有大约 500 兆的 Active(file)(但根据 dmesg 的上述 active_file: 显示的值与 cat /proc/meminfo|grep -F -- 'Active(file)' 不同!),这让我怀疑它只是 exes/libs...也许有人能建议如何只保留可执行代码吗?(如果已经不是这样的话)你有什么想法?
编辑3:使用上面的补丁似乎需要(定期?)运行sudo sysctl vm.drop_caches=1来释放一些陈旧的内存(?),这样,如果我在 Firefox 编译后调用stress,我会得到:active_file:142281 inactive_file:0 isolated_file:0 (142兆),然后清除文件缓存(另一种方法:echo 1|sudo tee /proc/sys/vm/drop_caches),然后再次运行stress,我会得到:active_file:22233 inactive_file:160 isolated_file:0 (22兆)-我不确定...
没有上面的补丁结果:这里,使用上面的补丁结果:这里

2
如果只想将单个特定的可执行文件保留在内存中,我会尝试在某种程度上利用mlockall()。为了将多个可执行文件保留在内存中,我会考虑创建一个小的ramfs分区,并将所需的可执行文件复制到其中。 - gudok
1
也可以考虑使用 ealyoom - gudok
1
@gudok 我需要将所有活动的可执行文件保存在RAM中,这样当上下文切换发生时(更具体地说,进程恢复执行),它们(文件支持的可执行代码页)就不必从磁盘重新读取(这是导致磁盘抖动的原因)。到目前为止,来自EDIT的补丁完成了将每个活动的可执行文件保存在RAM中的工作(似乎如此),从而几乎完全减少了磁盘抖动,因此我不再遇到操作系统永久冻结的情况。感谢提供earlyoom链接! - user10239615
1
我建议尝试设置活动页面的最小限制,而不是阻止所有活动页面的驱逐。如果活动页面的数量足够低且可用内存也很低,触发OOM killer应该相当容易。这将使内核保持一些智能清理活动列表的能力,但仍限制最坏情况的行为,直到触发OOM killer。 - Mikko Rantalainen
2
@MikkoRantalainen,今天我终于做到了,它也运行了,但是由于某些原因,大多数时间它会使系统冻结(我可能错过了什么?)。但不管怎样,我设法保持了256MiB的“Active(文件):”,因此磁盘抖动已经消失了。现在,如果它不会因为其他原因而冻结就好了!请参见此评论中的le9g.patch https://gist.github.com/constantoverride/84eba764f487049ed642eb2111a20830#gistcomment-2997383 - user11509478
显示剩余3条评论
3个回答

4

警告:如果您启用了交换空间,请勿使用此补丁,因为有两个用户报告称其效果更糟。我只在禁用内核中的交换空间时(即CONFIG_SWAP未设置)测试了此补丁!

在进一步通知之前(或者有人提出更好的解决方案),我正在使用以下补丁,以避免在将要耗尽内存并触发OOM-killer时产生任何磁盘瘫痪/操作系统冻结,从而尽快地(最多1秒钟)触发OOM-killer:

revision 3
preliminary patch to avoid disk thrashing (constant reading) under memory pressure before OOM-killer triggers
more info: https://gist.github.com/constantoverride/84eba764f487049ed642eb2111a20830

diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h
index 32699b2..7636498 100644
--- a/include/linux/mmzone.h
+++ b/include/linux/mmzone.h
@@ -208,7 +208,7 @@ enum lru_list {

 #define for_each_lru(lru) for (lru = 0; lru < NR_LRU_LISTS; lru++)

-#define for_each_evictable_lru(lru) for (lru = 0; lru <= LRU_ACTIVE_FILE; lru++)
+#define for_each_evictable_lru(lru) for (lru = 0; lru <= LRU_INACTIVE_FILE; lru++)

 static inline int is_file_lru(enum lru_list lru)
 {
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 03822f8..1f3ffb5 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -2086,9 +2086,9 @@ static unsigned long shrink_list(enum lr
                 struct scan_control *sc)
 {
    if (is_active_lru(lru)) {
-       if (inactive_list_is_low(lruvec, is_file_lru(lru),
-                    memcg, sc, true))
-           shrink_active_list(nr_to_scan, lruvec, sc, lru);
+       //if (inactive_list_is_low(lruvec, is_file_lru(lru),
+       //           memcg, sc, true))
+       //  shrink_active_list(nr_to_scan, lruvec, sc, lru);
        return 0;
    }

@@ -2234,7 +2234,7 @@ static void get_scan_count(struct lruvec *lruvec, struct mem_cgroup *memcg,

    anon  = lruvec_lru_size(lruvec, LRU_ACTIVE_ANON, MAX_NR_ZONES) +
        lruvec_lru_size(lruvec, LRU_INACTIVE_ANON, MAX_NR_ZONES);
-   file  = lruvec_lru_size(lruvec, LRU_ACTIVE_FILE, MAX_NR_ZONES) +
+   file  = //lruvec_lru_size(lruvec, LRU_ACTIVE_FILE, MAX_NR_ZONES) +
        lruvec_lru_size(lruvec, LRU_INACTIVE_FILE, MAX_NR_ZONES);

    spin_lock_irq(&pgdat->lru_lock);
@@ -2345,7 +2345,7 @@ static void shrink_node_memcg(struct pglist_data *pgdat, struct mem_cgroup *memc
             sc->priority == DEF_PRIORITY);

    blk_start_plug(&plug);
-   while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] ||
+   while (nr[LRU_INACTIVE_ANON] || //nr[LRU_ACTIVE_FILE] ||
                    nr[LRU_INACTIVE_FILE]) {
        unsigned long nr_anon, nr_file, percentage;
        unsigned long nr_scanned;
@@ -2372,7 +2372,8 @@ static void shrink_node_memcg(struct pglist_data *pgdat, struct mem_cgroup *memc
         * stop reclaiming one LRU and reduce the amount scanning
         * proportional to the original scan target.
         */
-       nr_file = nr[LRU_INACTIVE_FILE] + nr[LRU_ACTIVE_FILE];
+       nr_file = nr[LRU_INACTIVE_FILE] //+ nr[LRU_ACTIVE_FILE]
+           ;
        nr_anon = nr[LRU_INACTIVE_ANON] + nr[LRU_ACTIVE_ANON];

        /*
@@ -2391,7 +2392,8 @@ static void shrink_node_memcg(struct pglist_data *pgdat, struct mem_cgroup *memc
            percentage = nr_anon * 100 / scan_target;
        } else {
            unsigned long scan_target = targets[LRU_INACTIVE_FILE] +
-                       targets[LRU_ACTIVE_FILE] + 1;
+                       //targets[LRU_ACTIVE_FILE] + 
+                       1;
            lru = LRU_FILE;
            percentage = nr_file * 100 / scan_target;
        }
@@ -2409,10 +2411,12 @@ static void shrink_node_memcg(struct pgl
        nr[lru] = targets[lru] * (100 - percentage) / 100;
        nr[lru] -= min(nr[lru], nr_scanned);

+       if (LRU_FILE != lru) { //avoid this block for LRU_ACTIVE_FILE
        lru += LRU_ACTIVE;
        nr_scanned = targets[lru] - nr[lru];
        nr[lru] = targets[lru] * (100 - percentage) / 100;
        nr[lru] -= min(nr[lru], nr_scanned);
+       }

        scan_adjusted = true;
    }

很遗憾,以上代码中的制表符已被转换为空格,如果您需要原始补丁,请点击此处
该补丁的作用是在内存压力下不清除Active(file)页面,从而不会导致kswapd0 (但在每个程序本身的iotop中看到) 每次有上下文切换时都要重新读取每个运行进程的可执行页面以允许程序(继续)运行。因此,避免了大量磁盘抖动,操作系统也不会冻结成爬行状态。
以上内容已在Qubes OS 4.0的dom0(Fedora 25)和所有VMs(Fedora 28)中测试过,使用的内核版本为4.18.5(现在测试4.18.7)。
关于这个补丁的第一个版本也同样有效(显然),请查看此答案的问题上的EDIT更新: 在一台装有16G内存(减去512M用于集成显卡的内存)且没有交换空间(内核中也禁用了)的 ArchLinux 笔记本电脑上使用 le9d.patch(版本3)一段时间后,我可以说,系统比没有使用 le9d.patch 更容易耗尽内存,因此 OOM-killer 触发 Xorg 或 Chromium 等进程,而如果没有打补丁,则不会出现这种情况。因此,作为一种缓解措施,目前看来对我有效的是,每当 /proc/meminfo 中的 Active(file) 数量超过 2G 即 2000000 KB 时(例如通过此代码获取 KB 数字:grep 'Active(file):' /proc/meminfo|tr -d ' '|cut -f2 -d:|sed 's/kB//'),我就运行 echo 1 > /proc/sys/vm/drop_caches,然后再进行 sleep 5 检查。但是最近为了在 /tmp 中编译 firefox-hg(使用 tmpfs 最终使用了 12G),并确保它不会被 OOM 杀死,我使用的是 500000 而不是 2000000 KB。这肯定比整个系统死机要好(即在没有 le9d.patch 的情况下),在这种情况下会发生这种情况。如果没有这个检查,Active(file) 不会超过 4G,但这已经足以使 Xorg 被 OOM 杀死,如果有什么需要更多内存的东西,例如在这个 firefox 编译案例中,甚至只是通过 midnight commander 复制许多千兆字节(如果我记得没错的话)。

2
cgroups-v2内存控制器中的memory.min参数应该有所帮助。具体来说,让我引用一下:“硬内存保护。如果cgroup的内存使用量在其有效最小边界内,则在任何情况下都不会回收cgroup的内存。如果没有未受保护的可回收内存可用,则会调用OOM killer。”

https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html


你具体怎么使用它?我问这个是因为我在使用时遇到了问题。 - user11509478

2
回答这个问题,这里有一个简单/初步的补丁,用于不清除Active(file)(如/proc/meminfo中所见),如果它小于256 MiB,这似乎在linux-stable 5.2.4上运行良好(没有磁盘抖动)。
diff --git a/mm/vmscan.c b/mm/vmscan.c
index dbdc46a84f63..7a0b7e32ff45 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -2445,6 +2445,13 @@ static void get_scan_count(struct lruvec *lruvec, struct mem_cgroup *memcg,
            BUG();
        }

+    if (NR_ACTIVE_FILE == lru) {
+      long long kib_active_file_now=global_node_page_state(NR_ACTIVE_FILE) * MAX_NR_ZONES;
+      if (kib_active_file_now <= 256*1024) {
+        nr[lru] = 0; //don't reclaim any Active(file) (see /proc/meminfo) if they are under 256MiB
+        continue;
+      }
+    }
        *lru_pages += size;
        nr[lru] = scan;
    }

请注意,一些尚未发现的回归问题会在内核版本5.3.0-rc4-gd45331b00ddb上导致系统冻结(没有磁盘抖动,但sysrq仍然可以工作),即使没有此补丁也是如此。任何与此相关的新进展应该会在这里发生。

1
@gah,太懒得插入另一个硬盘(好吧,SSD),并在BIOS中重新启用VM相关设置以便Qubes可以运行,但如果管理员不标记的话我可能会这样做。另外,看看更新的补丁le9h.patch,它允许sysctl选项... - user11509478

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接