如何让MAP_ANONYMOUS映射的匿名页由内核按LRU回收并通知用户态进程重计算?
这个需求太合理了——想让内核帮你全局统筹内存,自动把不常用的匿名页回收,同时在需要的时候通知你重算内容,不用自己硬扛缓存管理的复杂度,也不用依赖预生成的大文件。好消息是,Linux上确实有现成的机制组合能实现这个,不用自己造轮子。
核心方案:userfaultfd(2) + madvise(2) 组合拳
这两个工具配合起来,正好能覆盖你所有的需求:内核管LRU回收,用户态收通知重算。
1. 用madvise(MADV_FREE)告诉内核:这些页可以丢
首先,在你用mmap(MAP_ANONYMOUS | MAP_PRIVATE)创建好匿名映射后,对整个区域调用:
madvise(addr, len, MADV_FREE);
这个调用(Linux 4.5以上支持)会告诉内核:这些匿名页的内容是可以被丢弃的。当系统内存紧张时,内核会把这些页纳入全局LRU回收队列,和文件页、其他进程的匿名页一起竞争内存——完全由内核根据系统整体负载决定哪些页该被回收,不用你自己定死缓存大小。
和MADV_DONTNEED不同,MADV_FREE不会立刻释放内存,只是标记页为“可回收”,内核会在真正需要内存的时候才动手,这和你用文件mmap时的缓存逻辑完全一致。
2. 用userfaultfd捕获缺页事件,重算内容
光让内核回收还不够,你需要在页被回收后、再次被访问时得到通知,这样才能重算内容。这就是userfaultfd的用武之地:
- 首先创建一个
userfaultfd文件描述符,设置非阻塞和关闭继承标志:int uffd = userfaultfd(O_CLOEXEC | O_NONBLOCK); - 协商API版本并注册你要监控的匿名映射区域:
struct uffdio_api api = { .api = UFFD_API }; ioctl(uffd, UFFDIO_API, &api); struct uffdio_register reg = { .range = { .start = addr, .len = len }, .mode = UFFDIO_REGISTER_MODE_MISSING }; ioctl(uffd, UFFDIO_REGISTER, ®); - 然后在你的进程里开一个线程,监听这个
uffd的事件:当内核发现某个被监控的页需要访问但内容不存在(不管是还没填充过,还是已经被内核回收了),就会把一个UFFD_EVENT_PAGEFAULT事件发给你。 - 收到事件后,你就可以重算该页的内容,然后用
UFFDIO_COPY或者UFFDIO_WRITE把计算好的内容写回对应的页框,让内核继续处理访问请求。
3. 动态标记可回收页
当你刚计算完一个页的内容时,它会被内核标记为“活跃”页,不会被优先回收。如果你希望内核在内存紧张时也可以考虑回收它,只需要对这个页再次调用madvise(addr_of_page, PAGE_SIZE, MADV_FREE)就行——这样内核就知道这个页的内容可以被丢弃,把它放回可回收的LRU队列里。
为什么你之前觉得madvise不支持?
单独的madvise确实没法给你发通知,但和userfaultfd结合后,就补上了“被通知重算”的关键一环。这个组合完全满足你的需求:
- 内核全局管理LRU,根据系统负载决定给你分配多少内存缓存
- 内存紧张时自动回收不常用的页
- 页被再次访问时,用户态收到通知并能重算内容
- 不用自己维护应用层的LRU,也不用依赖预存文件
额外注意事项
userfaultfd需要你的进程有足够的权限(默认情况下,非特权进程可以用,但某些系统可能需要设置/proc/sys/vm/unprivileged_userfaultfd为1)- 如果你需要监控页的写操作(比如写时复制场景),可以把
UFFDIO_REGISTER_MODE_MISSING换成UFFDIO_REGISTER_MODE_WP,但你的场景只需要监控缺页,所以MISSING模式足够 - 计算页内容的逻辑要尽量高效(你说10ms每页,这个开销在缺页时是可接受的,和从磁盘读文件的延迟差不多)
内容来源于stack exchange

