〇、全景:磁盘和用户之间的缓存层
前面几篇讲了物理内存怎么管理(#1 memblock/buddy)、虚拟地址怎么映射到物理页(#3 页表缺页)。但缺页分配出来的页,数据从哪来 ?匿名页(堆/栈)是凭空清零,文件页则来自磁盘------而内核不会每次都去读磁盘,它在内存里维护了一层 page cache(页缓存):
- 读 :先查 page cache,命中就直接拷给用户(
copy_folio_to_iter);没命中才读盘填进去; - 写 :先写进 page cache 标脏,不立刻落盘(延迟写 write-back),由后台 flush 线程异步刷回磁盘。
#mermaid-svg-ZJwJrUpeUXz69QCx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZJwJrUpeUXz69QCx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZJwJrUpeUXz69QCx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZJwJrUpeUXz69QCx .error-icon{fill:#552222;}#mermaid-svg-ZJwJrUpeUXz69QCx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZJwJrUpeUXz69QCx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZJwJrUpeUXz69QCx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZJwJrUpeUXz69QCx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZJwJrUpeUXz69QCx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZJwJrUpeUXz69QCx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZJwJrUpeUXz69QCx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZJwJrUpeUXz69QCx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZJwJrUpeUXz69QCx .marker.cross{stroke:#333333;}#mermaid-svg-ZJwJrUpeUXz69QCx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZJwJrUpeUXz69QCx p{margin:0;}#mermaid-svg-ZJwJrUpeUXz69QCx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZJwJrUpeUXz69QCx .cluster-label text{fill:#333;}#mermaid-svg-ZJwJrUpeUXz69QCx .cluster-label span{color:#333;}#mermaid-svg-ZJwJrUpeUXz69QCx .cluster-label span p{background-color:transparent;}#mermaid-svg-ZJwJrUpeUXz69QCx .label text,#mermaid-svg-ZJwJrUpeUXz69QCx span{fill:#333;color:#333;}#mermaid-svg-ZJwJrUpeUXz69QCx .node rect,#mermaid-svg-ZJwJrUpeUXz69QCx .node circle,#mermaid-svg-ZJwJrUpeUXz69QCx .node ellipse,#mermaid-svg-ZJwJrUpeUXz69QCx .node polygon,#mermaid-svg-ZJwJrUpeUXz69QCx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZJwJrUpeUXz69QCx .rough-node .label text,#mermaid-svg-ZJwJrUpeUXz69QCx .node .label text,#mermaid-svg-ZJwJrUpeUXz69QCx .image-shape .label,#mermaid-svg-ZJwJrUpeUXz69QCx .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZJwJrUpeUXz69QCx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZJwJrUpeUXz69QCx .rough-node .label,#mermaid-svg-ZJwJrUpeUXz69QCx .node .label,#mermaid-svg-ZJwJrUpeUXz69QCx .image-shape .label,#mermaid-svg-ZJwJrUpeUXz69QCx .icon-shape .label{text-align:center;}#mermaid-svg-ZJwJrUpeUXz69QCx .node.clickable{cursor:pointer;}#mermaid-svg-ZJwJrUpeUXz69QCx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZJwJrUpeUXz69QCx .arrowheadPath{fill:#333333;}#mermaid-svg-ZJwJrUpeUXz69QCx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZJwJrUpeUXz69QCx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZJwJrUpeUXz69QCx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZJwJrUpeUXz69QCx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZJwJrUpeUXz69QCx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZJwJrUpeUXz69QCx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZJwJrUpeUXz69QCx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZJwJrUpeUXz69QCx .cluster text{fill:#333;}#mermaid-svg-ZJwJrUpeUXz69QCx .cluster span{color:#333;}#mermaid-svg-ZJwJrUpeUXz69QCx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZJwJrUpeUXz69QCx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZJwJrUpeUXz69QCx rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZJwJrUpeUXz69QCx .icon-shape,#mermaid-svg-ZJwJrUpeUXz69QCx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZJwJrUpeUXz69QCx .icon-shape p,#mermaid-svg-ZJwJrUpeUXz69QCx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZJwJrUpeUXz69QCx .icon-shape .label rect,#mermaid-svg-ZJwJrUpeUXz69QCx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZJwJrUpeUXz69QCx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZJwJrUpeUXz69QCx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZJwJrUpeUXz69QCx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 命中 → 拷贝给用户
miss → 读盘填进去
预读 readahead
提前读后续页
写进缓存标脏
不立刻落盘
脏页超阈值
balance_dirty_pages 限速
异步刷回磁盘
清除脏标记
page cache
每个文件一个 address_space
用 xarray 按页偏移索引 folio
读 read()
filemap_read
写 write()
generic_perform_write
回写 writeback
wb_workfn flush 线程
一句话主线:page cache 是"磁盘 ↔ 用户"之间的一层内存缓存------每个文件对应一个
address_space,用 xarray 按页偏移索引 folio;读 = 先查缓存(命中直接拷、miss 读盘filemap_read_folio填进去),写 = 写进缓存标脏(write_end)、由后台 flush 线程wb_workfn异步回写,脏页太多时balance_dirty_pages限速写进程;预读readahead提前把后续页读进来 。它和上一篇 #3 缺页是同一枚硬币的两面:文件页缺页do_fault本质就是"从 page cache 里找不到这一页,去磁盘读进来"。
一、核心数据结构:address_space + xarray + folio
1.1 一个文件一个 address_space
每个 inode(文件)对应一个 address_space,它就是这个文件的 page cache 的"账簿":
c
// include/linux/fs.h (v6.6, line 470)
struct address_space {
struct inode *host; // 归属哪个文件(inode)
struct xarray i_pages; // 按页偏移索引 folio 的树
struct rb_root_cached i_mmap; // 映射这个文件的所有 vma
unsigned long nrpages; // 缓存了多少页
pgoff_t writeback_index; // 回写进度(写到哪一页了)
const struct address_space_operations *a_ops; // 读/写/回写的回调
// ...
};
i_pages(xarray) :核心索引结构,key 是"页偏移"(文件第几个页),value 是struct folio *。旧内核用 radix tree,v6.6 已换成更通用的 xarray。a_ops(address_space_operations) :具体文件系统提供的回调------read_folio(读一页)、readahead(预读一批)、write_begin/write_end(写)、writepages(回写落盘)。page cache 是通用框架,具体怎么读盘/写盘交给文件系统。
1.2 folio:页缓存的单位(不再是单 struct page)
v6.6 的 page cache 基本单位是 struct folio (include/linux/mm_types.h:293),不再是单个 struct page。一个 folio 可以包含多个物理页(order 0 单页,或 order 9 = 2MB 大页),把"一组连续页"当整体管理,减少了逐页加锁/记数的开销,也让 THP 大页能直接作为缓存单位。
1.3 xarray 的 tag:标记"脏"和"回写中"
xarray 除了存 folio,还能给每个条目打 tag。page cache 用两个 tag 标记状态:
c
// include/linux/fs.h (v6.6, line 498)
#define PAGECACHE_TAG_DIRTY XA_MARK_0 // 这页被改过,要回写
#define PAGECACHE_TAG_WRITEBACK XA_MARK_1 // 正在回写中
回写时,内核用 xa_marked 扫出所有打了 DIRTY tag 的 folio 拿去落盘,省得遍历整个文件的所有页。
二、读路径:查缓存 → 预读 → 读盘
read(fd, buf, n) 走 generic_file_read_iter(filemap.c:2790),它对 buffered 读(非 O_DIRECT)最后落到 filemap_read(filemap.c:2617):
c
// mm/filemap.c (v6.6, line 2790,节选)
ssize_t generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
// O_DIRECT 直接读盘,绕过 page cache(:2798 direct_IO)
if (iocb->ki_flags & IOCB_DIRECT) { ... }
// 普通 buffered 读 → 走 page cache
return filemap_read(iocb, iter, retval); // :2831
}
filemap_read(filemap.c:2617)是一个循环:每次用 filemap_get_pages(filemap.c:2535)取一批 folio,再 copy_folio_to_iter 拷给用户。核心在 filemap_get_pages,它按"页在不在缓存里"分三种情况:
c
// mm/filemap.c (v6.6, line 2535,节选)
filemap_get_read_batch(mapping, index, last_index - 1, fbatch); // ① 查 xarray
if (!folio_batch_count(fbatch)) { // 缓存 miss
page_cache_sync_readahead(mapping, ra, filp, index, ...); // ② 同步预读
filemap_get_read_batch(mapping, index, last_index - 1, fbatch);
}
if (!folio_batch_count(fbatch)) { // 还是没有
err = filemap_create_folio(filp, mapping, ...); // ③ 分配 + 读盘
// ...
}
- ① 命中 :
filemap_get_read_batch(filemap.c:2329)从 xarray 批量捞出 folio,直接拷给用户,零磁盘 I/O; - ② 预读 :miss 时先
page_cache_sync_readahead把当前及后续一批页读进来(见 §五); - ③ 读盘 :还 miss,
filemap_create_folio(filemap.c:2478)分配一个新 folio、filemap_add_folio挂进 xarray、filemap_read_folio(filemap.c:2365)调用a_ops->read_folio真正从磁盘读;对"预读已部分填好但还没读完"的 folio,则走filemap_update_page(filemap.c:2422)补读并等待 I/O 完成。
注意
generic_file_read_iter里的O_DIRECT分支(:2798):用O_DIRECT打开的文件绕过 page cache ,direct_IO直接读盘。这是"要不要缓存"的选择------默认要缓存(page cache),数据库等自己管缓存的场景才用 O_DIRECT 绕过。
三、写路径:写进缓存 + 标脏(延迟写)
write(fd, buf, n) 走 generic_file_write_iter(filemap.c:4081),buffered 写落到 generic_perform_write(filemap.c:3932):
c
// mm/filemap.c (v6.6, line 3932,节选)
do {
status = a_ops->write_begin(file, mapping, pos, bytes, ...); // ① 准备页
// ...
copied = copy_page_from_iter_atomic(page, offset, bytes, i); // ② 拷用户数据
// ...
status = a_ops->write_end(file, mapping, pos, bytes, copied, ...); // ③ 标脏
// ...
balance_dirty_pages_ratelimited(mapping); // ④ 脏页多了就限速
} while (iov_iter_count(i));
- ①
write_begin:文件系统拿到(或分配)page cache 里的目标页,必要时读盘先填满要改的部分; - ② 拷贝 :
copy_page_from_iter_atomic把用户缓冲区数据拷进 page cache 页; - ③
write_end:标记这页为 dirty (打上PAGECACHE_TAG_DIRTY),但绝不写盘; - ④ 限速 :
balance_dirty_pages_ratelimited(page-writeback.c:2062)检查脏页是否超阈值,超了就慢下来(见 §四)。
这就是"延迟写(write-back)" :write() 返回时数据只进了内存缓存,还没到磁盘。好处是快(攒一批再落盘、合并连续写),代价是掉电/崩溃会丢还没回写的数据(所以有 fsync/O_SYNC 强制落盘)。
四、回写 writeback:balance_dirty_pages 限速 + flush 线程
脏页不能无限攒,内核用两条腿把它刷回磁盘:一条是"写进程侧"的限速,一条是"后台 flush 线程"的刷盘。
4.1 三个阈值:free-run → 后台回写 → 节流
脏页占"可脏内存"的比例由两个可配置阈值(默认 dirty_background_ratio = 10、vm_dirty_ratio = 20,page-writeback.c:74 / :91)和派生的 freerun 上限一起分档:
| 脏页比例 | 内核动作 |
|---|---|
< bg_thresh(10%) |
写进程全速,无动作 |
> bg_thresh(10%) |
启动后台回写 wb_start_background_writeback |
> freerun ceiling((thresh+bg_thresh)/2 = 15%) |
写进程开始被节流 (wb_position_ratio 算比例,pause) |
> thresh(20%) |
严重节流,写进程基本停下来等回写 |
balance_dirty_pages(page-writeback.c:1669)是限速的核心,它在一个循环里反复判断:
c
// mm/page-writeback.c (v6.6, line 1669,节选)
for (;;) {
// ① 脏页超过后台阈值 → 启动后台回写
if (!laptop_mode && nr_reclaimable > gdtc->bg_thresh &&
!writeback_in_progress(wb))
wb_start_background_writeback(wb); // :1754
// ② 还没到 freerun 上限 → 写进程全速,跳出(if 在 :1768)
if (dirty <= dirty_freerun_ceiling(thresh, bg_thresh) && ...)
break; // :1783
// ③ 超过 thresh → 算 pos_ratio,按比例 pause 写进程
wb_position_ratio(gdtc); // :1813
// ... pause 一段时间后回到循环重新判断 ...
}
dirty_freerun_ceiling(page-writeback.c:813)就是 (thresh + bg_thresh) / 2。核心思想是"让写盘速度追上写内存速度":脏页涨到 bg_thresh 就派后台线程回写;回写还追不上、脏页继续涨到 15%/20%,就直接把写进程摁住(节流),等脏页降下来再放行------保证脏页不会把可回收内存吞光。
4.2 flush 线程:wb_workfn 真正落盘
后台刷盘由 writeback 工作队列的 worker 执行------每个 backing device 一个 bdi_writeback 上下文,worker 显示名就是 ps 里看到的 flush-<设备名> 线程,入口 wb_workfn(fs/fs-writeback.c:2245):
c
// fs/fs-writeback.c (v6.6, line 2245,节选)
void wb_workfn(struct work_struct *work)
{
struct bdi_writeback *wb = container_of(..., struct bdi_writeback, dwork);
do {
pages_written = wb_do_writeback(wb); // → wb_writeback(:2022)
} while (!list_empty(&wb->work_list)); // 还有活就继续
// ...
}
wb_writeback(fs-writeback.c:2022)扫出打 PAGECACHE_TAG_DIRTY 的 folio,调用 a_ops->writepages(具体文件系统,如 ext4)把它们写回磁盘,完成后清除 DIRTY tag 和脏位。这整个过程是异步 的------写进程早 write() 完事了,flush 线程在后台慢慢刷。
五、预读 readahead:提前把后续页读进来
顺序读文件时,如果每次都等"缺这一页才去读盘",磁盘 I/O 延迟会卡住读进程。预读就是"读第 N 页时,顺手把 N+1、N+2... 也读进来",用空间换延迟。两条触发路径:
- 同步预读
page_cache_sync_ra(readahead.c:671):读 miss 时(§二 ②),把请求页及后续一批页读进来; - 异步预读
page_cache_async_ra(readahead.c:699):读到一个打了PG_readahead标记的页时,说明"预读的页快被消费完了",后台再预读下一批。
预读窗口记在 struct file_ra_state(fs.h:968)里------start(窗口起点)、size(本次预读多少)、async_size(异步预读的提前量)、ra_pages(最大预读页数):
c
// include/linux/fs.h (v6.6, line 968)
struct file_ra_state {
pgoff_t start; // 预读窗口起点
unsigned int size; // 本次预读页数
unsigned int async_size; // 异步预读提前量
unsigned int ra_pages; // 最大预读页数
// ...
};
两条 *_ra 最终都进 ondemand_readahead → page_cache_ra_unbounded(readahead.c:205)→ read_pages(readahead.c:146),read_pages 调 a_ops->readahead(文件系统批量读)或逐页 a_ops->read_folio 提交 I/O。预读的页先加进 page cache 但标记 PG_readahead/!uptodate,读进程走到那一页时若还没读完,就 filemap_update_page 等它(§二 ③)。
六、与 mmap 缺页的关系:filemap_fault
上一篇 #3 讲文件页缺页 do_fault → __do_fault → vm_ops->fault。对普通文件,这个 fault 回调就是 filemap_fault (filemap.c:3255)------它做的事情和 read() 的读路径几乎一样:在 page cache 里找这一页,找不到就读盘填进去,然后映射进页表。所以 mmap 的文件映射,底层数据也是存在 page cache 里的:
read()/write()通过filemap_read/generic_perform_write操作 page cache;mmap后的内存访问通过缺页filemap_fault操作同一个 page cache;- 两者看到的是一致的缓存,这正是
mmap和read/write能共享同一份文件缓存的原因。
七、为什么这样设计(Why 层)
7.1 为什么读/写都要过 page cache,而不是直接读盘
因为磁盘慢、内存快 ,且文件访问有强时间局部性和重复性(同一文件反复读、反复写)。page cache 把"最近访问过的文件页"留在内存,命中就免掉磁盘 I/O,性能提升数量级。这也是为什么刚读过的文件再次读会快得多、为什么内存里总有大量"可回收的"文件页(见 #8 页回收)。
7.2 为什么写用"延迟写(write-back)"而不是"同步写(write-through)"
同步写每 write() 一次就落盘一次,磁盘 I/O 立刻成为瓶颈。延迟写让写进程只写内存、立刻返回,把"落盘"这个慢动作攒成一批、交给后台线程异步做------合并连续写、批量下盘 ,吞吐量大幅提升。代价是崩溃可能丢数据,所以内核提供 fsync/O_SYNC 给需要持久化保证的调用者显式强制落盘。"默认快(延迟写)+ 需要时慢(fsync)"是通用折中。
7.3 为什么要 balance_dirty_pages 限速写进程
如果写进程全速往 page cache 灌、回写线程又刷不过来,脏页会无限上涨,最终把可回收内存(NR_FILE_DIRTY)全部吞掉、触发 OOM。限速的本质是"让写内存的速度和写盘的速度匹配":脏页一超阈值就摁住写进程,逼迫整体吞吐收敛到磁盘带宽------这是"生产者(写进程)vs 消费者(回写线程)"的背压(backpressure)机制。
7.4 为什么要预读 readahead
顺序读是文件 I/O 的常态(日志、数据文件、模型权重)。如果只按需读,每次读盘都要等磁盘寻道/延迟,CPU 空转。预读用"读一页、顺手读一批"把未来的缺页 I/O 提前、合并成顺序大 I/O(磁盘顺序读远快于随机读),把延迟摊销掉。预读是"用多读一点内存的代价,换磁盘顺序 I/O"的投机优化。
7.5 为什么用 xarray 而不是简单数组
文件的页偏移是稀疏的------一个 1GB 的文件,用户可能只访问头尾两页。简单数组要为整个文件每个页都建条目,稀疏访问时浪费巨大;xarray(radix tree 的通用化)按"用到的偏移"建树,只占实际访问到的部分,和 #3 多级页表"稀疏地址空间只占实际用到的部分"是同一个思想。
八、与已有博客的交叉引用
| 已有博客 | 本篇覆盖的关系 |
|---|---|
| 内存 #3 页表缺页 | 文件页缺页 do_fault → __do_fault → vm_ops->fault = filemap_fault(:3255),本质是"从 page cache 读"。本篇 §六 讲清了这条线;page cache 是文件缺页的"数据源" |
| 内存 #2 buddy | page cache 的 folio 从哪来?filemap_alloc_folio 底层就是 buddy 分配物理页(__alloc_pages)。page cache 是 buddy 的又一个大消费者 |
| 内存 #4 大页 | folio 可以是 order=9 的大页,THP 大页能直接作为 page cache 的缓存单位(CONFIG_READ_ONLY_THP_FOR_FS,fs.h:476) |
| IOMMU 系列 | page cache 管的是"CPU/文件"侧缓存;设备 DMA 读写文件(如 GPU 直读 SSD)绕不绕 page cache 是另一套(O_DIRECT / GUP,见后续 \[blog-memory-expansion] GUP 篇) |
附:本篇关键源码索引
| 符号 | 位置 |
|---|---|
struct address_space(host/i_pages/a_ops/nrpages/writeback_index) |
include/linux/fs.h:470 |
PAGECACHE_TAG_DIRTY / PAGECACHE_TAG_WRITEBACK |
include/linux/fs.h:498 / :499 |
struct file_ra_state(start/size/async_size/ra_pages) |
include/linux/fs.h:968 |
struct folio(页缓存单位) |
include/linux/mm_types.h:293 |
generic_file_read_iter(读入口,O_DIRECT 分流) |
mm/filemap.c:2790 |
filemap_read(读主循环) |
mm/filemap.c:2617 |
filemap_get_pages(查缓存/预读/读盘三态) |
mm/filemap.c:2535 |
filemap_get_read_batch(xarray 批量查) |
mm/filemap.c:2329 |
filemap_create_folio(分配 + 读盘) |
mm/filemap.c:2478 |
filemap_update_page(补读 + 等 I/O) |
mm/filemap.c:2422 |
filemap_read_folio(→ a_ops->read_folio) |
mm/filemap.c:2365 |
filemap_fault(mmap 文件缺页) |
mm/filemap.c:3255 |
generic_perform_write(写主循环) |
mm/filemap.c:3932 |
generic_file_write_iter(写入口) |
mm/filemap.c:4081 |
balance_dirty_pages(限速核心) |
mm/page-writeback.c:1669 |
balance_dirty_pages_ratelimited |
mm/page-writeback.c:2062 |
dirty_background_ratio / vm_dirty_ratio(默认 10 / 20) |
mm/page-writeback.c:74 / :91 |
dirty_freerun_ceiling((thresh+bg_thresh)/2) |
mm/page-writeback.c:813 |
wb_workfn(flush 线程) |
fs/fs-writeback.c:2245 |
wb_writeback(回写落盘) |
fs/fs-writeback.c:2022 |
page_cache_sync_ra / page_cache_async_ra(同步/异步预读) |
mm/readahead.c:671 / :699 |
page_cache_ra_unbounded / read_pages(预读执行) |
mm/readahead.c:205 / :146 |