关于在io-uring异步非阻塞架构下兼顾mmap系统级缓存优势的技术方案问询
我一直对io-uring异步IO的设计理念非常认可——它简直是网络通信场景的绝配。用异步模型最大的好处就是能消除阻塞调用,这样就不用为了最大化吞吐量,启动比CPU(超线程)核心数还多的应用线程了。
但这里有个棘手的问题:要完全发挥异步模型的优势,我必须消除所有阻塞调用,而不只是网络IO相关的阻塞。如果用传统的“每个交互对应一个线程(或线程池)”的设计,我可能会用内存映射IO(mmap)来实现持久化存储的读访问。mmap的简洁性确实很吸引人,但它有个致命问题:如果工作线程访问的页不在缓存里,线程会被阻塞,直到页被加载到内存——这对异步非阻塞的设计来说完全不可接受。
我很欣赏mmap的一点是,操作系统会根据主机上所有活跃进程的需求,把文件中能放进内存的部分尽可能缓存起来。但我完全无法接受的是,当我的进程访问未被缓存的页时,整个进程会被阻塞,直到该页被读入内存。我真的希望:如果页不在内存里,当前线程能被异步挂起,等页加载完成后(通过完成队列事件)再恢复执行。
我意识到,如果自己实现一个应用层的LRU页缓存,并用io-uring的io_uring_prep_read()接口来读取单个页,确实能获得我想要的异步非阻塞行为。但这种方案有个明显的缺点(除了要花精力实现应用层LRU之外):我的进程无法和主机上的其他进程协作,没法让缓存内存在所有进程间合理分配。
我现在的疑问是:有没有什么我没注意到的机制,能让我两全其美?既能只用异步(非阻塞)调用来最小化线程数量,实现最大吞吐量;又能让我的(超大)数据文件的页缓存采用LRU策略,还能充分利用主机上的可用内存(同时不会影响其他进程)?难道采用异步模型的程序,就只能在“用mmap借助系统共享LRU缓存”和“自己实现静态页缓存+用io-uring的io_uring_prep_read()填充”之间二选一吗?
我后续查到的相关信息:
- 异步页故障的概念大概在15年前就出现在KVM/QEMU的场景中,但我搞不清楚这些内核内部机制和io-uring的功能有什么关联。
- 2021年有一条“Support Asynchronous Page Fault”的消息,看起来现代Linux内核可能已经支持类似我想要的功能,但我不确定怎么在io-uring导向的异步编程中使用这些现有特性。
- 我找到了
userfaultfd(2)和ioctl_userfaultfd(2)的手册页,示例代码用的是poll()而不是io-uring,不过或许可以通过io-uring异步读取来实现页故障事件的异步通知? - 我也读了相关的内核文档,但还是搞不懂怎么把这些特性和io-uring的异步模型结合,来避免进程在访问未缓存页时被阻塞。我知道用户态可以收到页故障的通知,但完全不知道怎么避免线程在访问未缓存虚拟地址时被阻塞——我没法让用户态代码在遇到页故障时,调用类似
co_await的异步原语挂起,而不是一直阻塞到页故障解决。 - 我也想不到怎么标记“某个页即将被需要”,同时让发起这个需求的线程不用休眠等待页加载完成,而是去做其他事情。
- 2022年某篇论文的3.2节也提到,io-uring和mmap似乎存在一定的兼容性问题。
内容来源于stack exchange

