LRU 不只是最近最少使用active/inactive、anon/file、folio 和分代 LRU番外一讲到内存压力上来以后kswapd会从balance_pgdat走到shrink_node、shrink_lruvec开始扫描可回收页。这一篇就进入kswapd真正要扫描的核心结构LRU。这里最容易误解的一点是Linux 的 LRU 不是教科书里那个“把所有页严格排成一条最近最少使用队列”的算法。真实内核要在多核、NUMA、memcg、匿名页、文件页、硬件访问位、反向映射、IO 成本之间折中所以它维护的是一套近似冷热分层。这一篇回答四个问题为什么 LRU 要分active和inactive为什么匿名页和文件页要分开folio出现后LRU 上到底挂的是页还是 folioMulti-Gen LRU 又是在传统 active/inactive 模型上解决什么问题先把结论放前面Linux 不维护一个精确的全局 LRU 队列。 它把可回收内存按 lruvec 管起来 lruvec 一个 NUMA node 一个 memory cgroup 的回收视角 传统视角下每个 lruvec 有几类 LRU inactive_anon / active_anon inactive_file / active_file unevictable active/inactive 表达冷热层次。 anon/file 表达回收成本差异。 folio 表达一次 LRU 管理单位可能覆盖多个 base page。 Multi-Gen LRU 用 generation 把冷热从“两层”扩展成“多代”。一、LRU 的管理单位不是全局一条链如果把系统所有物理页塞进一条全局 LRU 链表问题会很快出现每次访问都移动页锁竞争会很重。匿名页和文件页回收成本不同不能只看“多久没访问”。NUMA node 和 memcg 都要求局部回收不能只看全局。硬件通常只给访问位、脏位这类粗粒度信号不会替内核维护访问时间戳。这里的memcg是 memory cgroup。可以把它先理解成 Linux 给“一组进程”建立的内存账本和限制边界Docker container / Kubernetes Pod / systemd service │ ▼ memory cgroup │ ├─ 统计这组进程用了多少匿名页 ├─ 统计这组进程用了多少 page cache ├─ 用 memory.max 等参数限制上限 └─ 超过限制时只在这组进程的账本里回收或 OOM所以内存回收不总是全局行为。宿主机可能还有空闲内存但某个容器自己的memory.max快满了这时内核要优先在这个容器所属的 memcg 里找可回收页。这就是后面说lruvec node memcg的原因同一个 NUMA node 上不同 memcg 有各自的 LRU 回收视角。所以 Linux 的回收视角更接近这样system │ ├─ node 0 │ ├─ root memcg 的 lruvec │ ├─ cgroup A 的 lruvec │ └─ cgroup B 的 lruvec │ └─ node 1 ├─ root memcg 的 lruvec ├─ cgroup A 的 lruvec └─ cgroup B 的 lruveclruvec可以先理解成某个 node 上、某个 memcg 视角下 可被页面回收框架扫描的一组 LRU 状态。传统 LRU 视角里一个lruvec里面最重要的是这几类lruvec ├─ inactive_anon - 冷一点的匿名页匿名页回收候选 ├─ active_anon - 热一点的匿名页暂时保护 ├─ inactive_file - 冷一点的文件页文件页回收候选 ├─ active_file - 热一点的文件页暂时保护 └─ unevictable - mlock、特殊映射等不可普通回收的页画成二维表会更清楚维度anonfileinactive冷匿名页候选但不一定能回收冷文件页干净时通常容易回收active热匿名页先保护热文件页先保护unevictable不在普通回收候选里不在普通回收候选里这张表比“最近最少使用”更接近 Linux 的真实心智模型回收不是问 谁是全系统最久没访问的那一页 回收更像问 当前 node/memcg 里 anon 和 file 各有多少冷热页 哪一类扫描更可能释放内存 扫描成本、IO 成本、refault页被回收后又很快被访问重新缺页进入内存风险能不能接受二、active 和 inactive 不是“安全”和“马上回收”active容易被误解成“不会回收”inactive容易被误解成“马上释放”。这两个理解都太硬。更准确的理解是active: 最近更像工作集先给一次保护。 inactive: 更像冷页优先作为扫描和回收候选。页面会在两类状态之间流动新页 / refault 页 / 被判断为热的页 │ ▼ active │ │ 压力下老化或者访问证据不足 ▼ inactive │ ├─ 扫描时发现最近又被访问 │ │ │ └─ 重新激活 / 继续保护 │ └─ 扫描时仍然冷 │ ├─ 文件页clean 可丢dirty 要 writeback └─ 匿名页通常要 swap不能随便丢用流程图看所以 active/inactive 的本质不是二元裁决而是“冷热概率”的分层。Linux 通过硬件访问位、PG_referenced、反向映射扫描、workingset/refault 统计等信号去近似判断一个页是不是工作集的一部分。三、anon 和 file 分开是因为回收成本不同匿名页和文件页最大的差异是后盾不同。匿名页来自堆。用户栈。匿名mmap。写时复制后的私有页。这些页没有一个普通文件可以重新读取。回收匿名页时内核通常要把内容写到 swap然后把页表项从 present PTE 改成 swap entryanonymous page reclaim │ ▼ rmap 找到映射它的 PTE │ ▼ 分配 swap slot │ ▼ 写出页内容 │ ▼ PTE: present PFN - not-present swap entry文件页来自read()/write()背后的 page cache。文件mmap。可执行文件、动态库的文件映射。干净文件页有文件作为后盾丢掉以后需要时可以从磁盘再读clean file page reclaim │ ├─ 如果被映射先 unmap PTE └─ 从 page cache 移除释放物理页脏文件页不能直接丢dirty file page reclaim │ ├─ 发起或等待 writeback └─ 写回完成变 clean 后才能真正回收这就是为什么回收代码要分anon和file它们不是同一种成本模型。但也不能把它简化成“永远先回收文件页”。真实选择还会受这些因素影响swappiness。文件页是否干净。匿名页是否有 swap。最近 refault 的文件页是不是工作集。memcg 限制。当前回收优先级。direct reclaim 还是 kswapd。一张图压缩这个差异┌───────────────────────────────┬──────────────────────────────────┐ │ anon │ file │ ├───────────────────────────────┼──────────────────────────────────┤ │ 堆、栈、匿名 mmap、COW 私有页 │ page cache、文件 mmap、可执行映射 │ │ 没有普通文件后盾 │ 有文件后盾 │ │ 回收通常需要 swap │ clean page 可以直接丢弃 │ │ 无 swap 时常常很难回收 │ dirty page 需要 writeback │ └───────────────────────────────┴──────────────────────────────────┘四、folioLRU 上不一定只是一张 4KB 页前面一直说“页”但现代内核里你会越来越多看到folio。可以先这样理解page: 传统 struct page 视角常被读者理解成一个 base page例如 4KB。 folio: 一组属于同一个内存对象、同一类用途的连续 base pages。 小 folio 可以只有 1 个 base page。 大 folio 可以覆盖多个 base pages。为什么要有folio核心原因是内核不想永远以 4KB 为粒度管理 page cache、匿名大页、THPTransparent Huge Pages透明大页内核自动把连续小页合成更大的页映射常见是 2MB以减少 TLB 压力或其他大块映射。一个 folio 可以让内核把“这一组页是一个整体”表达得更清楚减少重复元数据和重复操作。在 LRU 语境里要注意两点内核实现里很多地方已经以folio为单位加入、隔离、移动、回收。/proc/vmstat、/proc/meminfo里的许多统计仍按 base page 或 KiB 展示。所以正文里说“LRU 上的页”时读者心里要保留这个翻译讲概念时说 page。 看现代源码时常常是 folio。 看 /proc 统计时很多值仍折算成 base pages。示意图LRU list │ ├─ folio A: 1 个 4KB page │ └─ page │ ├─ folio B: 16 个 4KB pages │ ├─ page │ ├─ page │ └─ ... │ └─ folio C: 512 个 4KB pages例如 2MB THP ├─ page ├─ page └─ ...这也是为什么实验输出里看到的是nr_active_file 338224 pages它的单位是 base page不代表 LRU 链表上真的挂了 338224 个独立链表节点。五、传统 LRU 回收大概怎么扫把番外一的kswapd路径接进来kswapd / direct reclaim │ ▼ shrink_node │ ▼ shrink_lruvec │ ▼ 选择 anon/file、active/inactive 的扫描比例 │ ▼ isolate 一批 folio/page │ ├─ 还热 - 放回或激活 ├─ clean file - unmap/drop ├─ dirty file - writeback 相关路径 ├─ anon - swap 相关路径 └─ unevictable - 移到 unevictable 或跳过流程图这里的get_scan_count不是简单地说“文件页永远比匿名页优先”。它会综合当前压力、匿名/文件的 LRU 大小、是否有 swap、swappiness、refault/工作集迹象等决定各类页扫描多少。六、Multi-Gen LRU把两层冷热扩展成多代冷热传统 active/inactive 模型只有两层active - 更热 inactive - 更冷但真实工作集不是只有“热”和“冷”两档。一个页面可能是刚刚访问过。100ms 内访问过。1s 内访问过。很久没访问但还没被扫描到。曾经被回收马上 refault说明可能误杀了工作集。Multi-Gen LRU 的思路是用 generation 表达更细的时间/冷热层次。older generation │ │ 更冷更可能被 eviction 选择 ▼ gen N gen N1 gen N2 gen N3 ▲ │ 更年轻最近更可能被访问 younger generation它的核心动作可以压缩成两个词aging: 创建新 generation把最近访问过的 folio 归入更年轻的代。 eviction: 从较老 generation 里选择回收候选。这里要注意两个细节。第一aging 不是给每个被访问的 folio 单独创建一个 generation。更准确地说是 aging 推进全局的年轻代序号创建一个新的 youngest generation然后把这一轮扫描中发现“最近访问过”的 folio 归入这个新代或更年轻的代。举个抽象例子aging 前 gen 10 老 gen 11 gen 12 gen 13 新max_seq aging 发生 创建 gen 14max_seq 前进 最近访问过的 folio - 移到 gen 14 或更年轻的位置 没有访问证据的 folio - 大多留在原来的 gen 10/11/12/13所以从概念上看generation 数字越大越新。 generation 数字越小越老。但看源码时不能把内部数组下标直接当成真实年龄。Multi-Gen LRU 用循环数组承载 generation数组槽位会复用。真实的年龄边界要看单调推进的序号max_seq: 当前 youngest generation。 min_seq: 当前 oldest generation。第二eviction 不是从某个代开始把它之前所有代一次性全部回收。它通常从min_seq指向的最老可回收 generation 开始扫描。仍然用上面的例子gen 10 最老min_seq - 优先扫描 gen 11 gen 12 gen 13 gen 14 最新max_seq回收器先看gen 10。扫描gen 10时每个 folio 还要再判断folio 仍然冷 - 尝试回收。 folio 最近其实访问过 - 保护提升到更年轻的 generation。 folio 暂时不能回收 - 跳过或放回。等gen 10被处理到可以推进时min_seq才往前走min_seq: 10 - 11这里的“可以推进”不是说gen 10里的物理页必须全部成功释放。更准确地说是gen 10这个最老 generation 里已经没有需要继续留在gen 10的可扫描候选。扫描最老代时里面的 folio 可能有几种去向gen 10 里的 folio │ ├─ 确实冷而且能释放 │ └─ isolate 后回收掉 │ ├─ 扫描时发现其实还热 │ └─ 提升到更年轻的 generation │ ├─ 暂时不适合回收例如 dirty / writeback / busy │ └─ 跳过、放回或推迟到后续路径处理 │ └─ unevictable或者不符合当前 reclaim 条件 └─ 移出普通回收候选或暂时不作为这轮目标所以min_seq推进的含义更接近gen 10 不再是一个还有可继续扫描候选的最老代。而不是gen 10 里的每一页都已经释放回 buddy。如果gen 10已经空了或者里面的候选都被处理到不再阻塞最老代推进min_seq就可以变成11。如果后面的某些老 generation 本来也是空的内核还可以继续跳过空代但它仍然要保留最少数量的 generation最新的若干代通常不会直接变成 eviction 目标。下一轮最老代才变成gen 11。所以更准确的心智模型是aging: 推进 max_seq制造新的年轻代把热 folio 往新代搬。 eviction: 从 min_seq 指向的最老代开始扫描。 老代被消耗或推进后min_seq 才继续往前移动。Multi-Gen LRU 还会保护最新的若干 generation。它们更接近传统 LRU 里的 active 区域不会轻易作为 eviction 目标。一个简化图是gen 10 old, min_seq - 优先回收 gen 11 old/cold - gen 10 推进后才轮到它 gen 12 inactive-ish gen 13 protected-ish - 通常先保护 gen 14 youngest, max_seq - 通常先保护更完整一点这和 active/inactive 并不是两套完全无关的概念。Multi-Gen LRU 仍然要兼容很多现有统计和语义例如/proc/vmstat里依然能看到nr_active_file、nr_inactive_file这类计数。可以这样建立对应关系传统 LRU active / inactive 是两层冷热表达。 Multi-Gen LRU generation 是多层冷热表达。 最年轻的若干代更像 active。 更老的代更像 inactive。现代内核如果启用 Multi-Gen LRU常见观察入口是/sys/kernel/mm/lru_gen/enabled /sys/kernel/mm/lru_gen/min_ttl_ms /sys/kernel/debug/lru_gen /sys/kernel/debug/lru_gen_full但这些入口依赖内核配置、权限和 debugfs 是否挂载。容器里尤其要小心能读/proc/vmstat不代表一定能读 debugfs。七、完整实验代码下面这份代码做几件事打印uname、cgroup 内存限制、RLIMIT_MEMLOCK。探测 Multi-Gen LRU 的 sysfs/debugfs 入口。读取/proc/meminfo和/proc/vmstat里的 LRU 相关字段。创建 512 MiB 匿名映射并逐页写入让匿名页真实分配。mlock其中 16 页观察unevictable/mlock统计。创建 512 MiB 文件fsync后读入 page cache观察文件页统计。清理映射后再次观察统计回落。同一份代码也保存为 assets/lru-active-inactive-demo.c。#define _GNU_SOURCE #include errno.h #include fcntl.h #include inttypes.h #include limits.h #include stdio.h #include stdlib.h #include string.h #include sys/mman.h #include sys/resource.h #include sys/stat.h #include sys/utsname.h #include time.h #include unistd.h struct vm_counter { const char *name; unsigned long long value; int found; }; static const char *const vmstat_keys[] { nr_inactive_anon, nr_active_anon, nr_inactive_file, nr_active_file, nr_unevictable, nr_mlock, nr_anon_pages, nr_file_pages, nr_mapped, nr_dirty, nr_writeback, workingset_refault_anon, workingset_refault_file, workingset_activate_anon, workingset_activate_file, workingset_restore_anon, workingset_restore_file, pgactivate, pgdeactivate, pgrefill, pgscan_kswapd, pgscan_direct, pgsteal_kswapd, pgsteal_direct, pgscan_anon, pgscan_file, pgsteal_anon, pgsteal_file, }; static volatile unsigned long long sink; static void die(const char *msg) { perror(msg); exit(1); } static void sleep_seconds(unsigned int seconds) { struct timespec req; req.tv_sec seconds; req.tv_nsec 0; while (nanosleep(req, req) ! 0) { if (errno ! EINTR) die(nanosleep); } } static void print_cgroup_memory(void) { const char *paths[] { /sys/fs/cgroup/memory.current, /sys/fs/cgroup/memory.max, /sys/fs/cgroup/memory.swap.current, /sys/fs/cgroup/memory.swap.max, }; size_t i; printf(cgroup memory:\n); for (i 0; i sizeof(paths) / sizeof(paths[0]); i) { FILE *fp fopen(paths[i], r); char buf[128]; if (!fp) continue; if (fgets(buf, sizeof(buf), fp)) { buf[strcspn(buf, \n)] \0; printf( %s %s\n, paths[i], buf); } fclose(fp); } } static void print_meminfo(const char *tag) { const char *wanted[] { MemTotal:, MemAvailable:, Cached:, Active:, Inactive:, Active(anon):, Inactive(anon):, Active(file):, Inactive(file):, Unevictable:, Mlocked:, AnonPages:, Mapped:, Dirty:, Writeback:, SwapTotal:, SwapFree:, }; FILE *fp fopen(/proc/meminfo, r); char line[256]; size_t i; if (!fp) die(fopen /proc/meminfo); printf([%s] /proc/meminfo selected fields\n, tag); while (fgets(line, sizeof(line), fp)) { for (i 0; i sizeof(wanted) / sizeof(wanted[0]); i) { if (strncmp(line, wanted[i], strlen(wanted[i])) 0) { fputs(line, stdout); break; } } } fclose(fp); } static int read_first_line(const char *path, char *buf, size_t len) { FILE *fp fopen(path, r); if (!fp) return -1; if (!fgets(buf, len, fp)) { fclose(fp); return -1; } buf[strcspn(buf, \n)] \0; fclose(fp); return 0; } static void print_lru_gen_status(void) { char buf[256]; FILE *fp; int lines 0; printf(multi-gen LRU interfaces:\n); if (read_first_line(/sys/kernel/mm/lru_gen/enabled, buf, sizeof(buf)) 0) printf( /sys/kernel/mm/lru_gen/enabled %s\n, buf); else printf( /sys/kernel/mm/lru_gen/enabled not readable: %s\n, strerror(errno)); if (read_first_line(/sys/kernel/mm/lru_gen/min_ttl_ms, buf, sizeof(buf)) 0) printf( /sys/kernel/mm/lru_gen/min_ttl_ms %s\n, buf); else printf( /sys/kernel/mm/lru_gen/min_ttl_ms not readable: %s\n, strerror(errno)); fp fopen(/sys/kernel/debug/lru_gen, r); if (!fp) { printf( /sys/kernel/debug/lru_gen not readable: %s\n, strerror(errno)); return; } printf( /sys/kernel/debug/lru_gen first lines:\n); while (lines 12 fgets(buf, sizeof(buf), fp)) { buf[strcspn(buf, \n)] \0; printf( %s\n, buf); lines; } fclose(fp); } static void read_vmstat(struct vm_counter *counters, size_t nr) { FILE *fp fopen(/proc/vmstat, r); char name[128]; unsigned long long value; size_t i; if (!fp) die(fopen /proc/vmstat); for (i 0; i nr; i) { counters[i].value 0; counters[i].found 0; } while (fscanf(fp, %127s %llu, name, value) 2) { for (i 0; i nr; i) { if (strcmp(name, counters[i].name) 0) { counters[i].value value; counters[i].found 1; break; } } } fclose(fp); } static const struct vm_counter *find_counter(const struct vm_counter *counters, size_t nr, const char *name) { size_t i; for (i 0; i nr; i) { if (strcmp(counters[i].name, name) 0) return counters[i]; } return NULL; } static double pages_to_mib(unsigned long long pages, size_t page_size) { return (double)pages * (double)page_size / 1024.0 / 1024.0; } static void print_lru_summary(const char *tag, const struct vm_counter *counters, size_t nr, size_t page_size) { const char *names[] { nr_inactive_anon, nr_active_anon, nr_inactive_file, nr_active_file, nr_unevictable, nr_mlock, }; size_t i; printf([%s] LRU size summary from /proc/vmstat\n, tag); for (i 0; i sizeof(names) / sizeof(names[0]); i) { const struct vm_counter *c find_counter(counters, nr, names[i]); if (!c || !c-found) { printf( %-20s not-found\n, names[i]); continue; } printf( %-20s %12llu pages %10.1f MiB\n, names[i], c-value, pages_to_mib(c-value, page_size)); } } static void print_vmstat_delta(const char *tag, const struct vm_counter *base, const struct vm_counter *now, size_t nr) { size_t i; printf([%s] /proc/vmstat selected counters\n, tag); for (i 0; i nr; i) { if (!now[i].found) { printf(%-28s not-found\n, now[i].name); continue; } printf(%-28s %llu, now[i].name, now[i].value); if (base base[i].found) printf( delta%lld, (long long)(now[i].value - base[i].value)); putchar(\n); } } static void init_counters(struct vm_counter *counters, size_t nr) { size_t i; for (i 0; i nr; i) { counters[i].name vmstat_keys[i]; counters[i].value 0; counters[i].found 0; } } static void *map_anon(size_t len, size_t page_size, size_t *locked_len) { unsigned char *p; size_t i; size_t lock_len; p mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (p MAP_FAILED) die(mmap anonymous); printf(touching anonymous mapping: %zu pages\n, len / page_size); for (i 0; i len; i page_size) p[i] (unsigned char)(i / page_size); lock_len 16 * page_size; if (lock_len len) lock_len len; *locked_len 0; if (lock_len 0) { if (mlock(p, lock_len) 0) { *locked_len lock_len; printf(mlock succeeded: %zu bytes\n, lock_len); } else { printf(mlock skipped: %s\n, strerror(errno)); } } return p; } static int create_file(char *path, size_t path_len, size_t len) { char template[] /tmp/lru-file-demo-XXXXXX; unsigned char *buf; size_t chunk 1024 * 1024; size_t written 0; int fd; fd mkstemp(template); if (fd 0) die(mkstemp); snprintf(path, path_len, %s, template); buf malloc(chunk); if (!buf) die(malloc file buffer); memset(buf, 0x5a, chunk); printf(creating clean file cache source: %s (%zu MiB)\n, path, len / 1024 / 1024); while (written len) { size_t todo len - written; ssize_t ret; if (todo chunk) todo chunk; ret write(fd, buf, todo); if (ret 0) die(write test file); if (ret 0) { fprintf(stderr, short write creating test file\n); exit(1); } written (size_t)ret; } free(buf); if (fsync(fd) ! 0) die(fsync test file); #ifdef __linux__ { int ret posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED); if (ret ! 0) printf(posix_fadvise(DONTNEED) was ignored or unsupported: %s\n, strerror(ret)); } #endif if (lseek(fd, 0, SEEK_SET) 0) die(lseek test file); return fd; } static void *map_file_and_touch(int fd, size_t len, size_t page_size, unsigned int hot_rounds) { unsigned char *p; size_t pages len / page_size; size_t hot_pages pages; unsigned long long local 0; unsigned int round; size_t i; p mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0); if (p MAP_FAILED) die(mmap file); printf(reading file mapping once: %zu pages\n, pages); for (i 0; i len; i page_size) local p[i]; if (hot_pages 16384) hot_pages 16384; printf(re-reading hot file subset: %zu pages, %u rounds\n, hot_pages, hot_rounds); for (round 0; round hot_rounds; round) { for (i 0; i hot_pages * page_size; i page_size) local p[i]; } sink local; return p; } static size_t mib_to_len(unsigned long long mib) { return (size_t)mib * 1024UL * 1024UL; } int main(int argc, char **argv) { unsigned long long anon_mib 512; unsigned long long file_mib 512; unsigned int hot_rounds 4; unsigned int hold_seconds 1; size_t page_size; size_t anon_len; size_t file_len; size_t locked_len 0; unsigned char *anon NULL; unsigned char *file_map NULL; int file_fd -1; char file_path[PATH_MAX] {0}; struct utsname uts; struct rlimit rlim; size_t nr_counters sizeof(vmstat_keys) / sizeof(vmstat_keys[0]); struct vm_counter before[sizeof(vmstat_keys) / sizeof(vmstat_keys[0])]; struct vm_counter after_anon[sizeof(vmstat_keys) / sizeof(vmstat_keys[0])]; struct vm_counter after_file[sizeof(vmstat_keys) / sizeof(vmstat_keys[0])]; struct vm_counter after_sleep[sizeof(vmstat_keys) / sizeof(vmstat_keys[0])]; struct vm_counter after_cleanup[sizeof(vmstat_keys) / sizeof(vmstat_keys[0])]; if (argc 2) anon_mib strtoull(argv[1], NULL, 10); if (argc 3) file_mib strtoull(argv[2], NULL, 10); if (argc 4) hot_rounds (unsigned int)strtoul(argv[3], NULL, 10); if (argc 5) hold_seconds (unsigned int)strtoul(argv[4], NULL, 10); if (anon_mib 0 file_mib 0) { fprintf(stderr, usage: %s [anon_mib] [file_mib] [hot_rounds] [hold_seconds]\n, argv[0]); return 2; } init_counters(before, nr_counters); init_counters(after_anon, nr_counters); init_counters(after_file, nr_counters); init_counters(after_sleep, nr_counters); init_counters(after_cleanup, nr_counters); if (uname(uts) ! 0) die(uname); page_size (size_t)sysconf(_SC_PAGESIZE); anon_len mib_to_len(anon_mib); file_len mib_to_len(file_mib); printf(machine%s sysname%s release%s\n, uts.machine, uts.sysname, uts.release); printf(page_size%zu bytes\n, page_size); printf(anon%llu MiB, file%llu MiB, hot_rounds%u, hold_seconds%u\n, anon_mib, file_mib, hot_rounds, hold_seconds); if (getrlimit(RLIMIT_MEMLOCK, rlim) 0) { printf(RLIMIT_MEMLOCK soft%llu hard%llu bytes\n, (unsigned long long)rlim.rlim_cur, (unsigned long long)rlim.rlim_max); } print_cgroup_memory(); print_lru_gen_status(); read_vmstat(before, nr_counters); print_meminfo(before); print_lru_summary(before, before, nr_counters, page_size); print_vmstat_delta(before, NULL, before, nr_counters); if (anon_len 0) { anon map_anon(anon_len, page_size, locked_len); read_vmstat(after_anon, nr_counters); print_meminfo(after anonymous touch); print_lru_summary(after anonymous touch, after_anon, nr_counters, page_size); print_vmstat_delta(after anonymous touch, before, after_anon, nr_counters); } if (file_len 0) { file_fd create_file(file_path, sizeof(file_path), file_len); file_map map_file_and_touch(file_fd, file_len, page_size, hot_rounds); read_vmstat(after_file, nr_counters); print_meminfo(after file cache touch); print_lru_summary(after file cache touch, after_file, nr_counters, page_size); print_vmstat_delta(after file cache touch, before, after_file, nr_counters); } if (hold_seconds 0) { printf(sleeping %u seconds before cleanup...\n, hold_seconds); sleep_seconds(hold_seconds); } read_vmstat(after_sleep, nr_counters); print_meminfo(after sleep); print_lru_summary(after sleep, after_sleep, nr_counters, page_size); print_vmstat_delta(after sleep, before, after_sleep, nr_counters); if (file_map munmap(file_map, file_len) ! 0) die(munmap file); if (file_fd 0) { close(file_fd); if (file_path[0] ! \0) unlink(file_path); } if (anon) { if (locked_len 0 munlock(anon, locked_len) ! 0) die(munlock); if (munmap(anon, anon_len) ! 0) die(munmap anonymous); } sleep_seconds(1); read_vmstat(after_cleanup, nr_counters); print_meminfo(after cleanup); print_lru_summary(after cleanup, after_cleanup, nr_counters, page_size); print_vmstat_delta(after cleanup, before, after_cleanup, nr_counters); printf(sink%llu\n, sink); return 0; }编译和运行命令docker run --rm --platform linux/amd64 \ -v $PWD/os/memory/assets:/src:ro -w /tmp \ gcc:13 \ bash -lc gcc -O2 -Wall -Wextra -stdc11 /src/lru-active-inactive-demo.c -o /tmp/lru-demo /tmp/lru-demo 512 512 4 1这里的参数含义是512 - 触摸 512 MiB 匿名页 512 - 创建并读取 512 MiB 文件页 4 - 对文件映射的热子集重复读取 4 轮 1 - 清理前保持 1 秒