Home > technology > 文件页:dirty、write、stale TLB 等等

文件页:dirty、write、stale TLB 等等

本文主要介绍脏页在内存回收、读写操作并发这些内存活动中,是如何保证一致性的。尤其是是回收过程,对于有页表映射的文件页面,涉及到刷 tlb 相关的,这些地方尤为谨慎。


对于具有写权限、被修改为脏页(Dirty)、且需要写回磁盘的共享文件映射页面(MAP_SHARED + PROT_WRITE),结合内核源码(vmscan.c 和 rmap.c),整个回收与写回生命周期以及在各时间节点通过 stale TLB 访问 的行为分析如下:

回收过程

一般情况下,是不会回收脏页的,尤其是对于普通文件页。而那些后备是 zram 的匿名脏页,回收的概率倒是比较大。他们在处理上基本相似。

我们这里列举最坏的情况下,比如 kswapd 即回收文件脏页,也回收匿名脏页。

[用户态修改页面] -> PTE 变 Dirty, CPU 缓存 Writable TLB
       │
       ▼
[回收开始: shrink_folio_list()]
       │
       ▼
[1. try_to_unmap()] ──────► 清除 PTE (PTE 变无效),但延迟刷新 TLB (未发 IPI)
       │                    此时 CPU 仍持有 Stale Writable TLB
       │                    ┌───────────────────────────────────────────┐
       │                    │ 竞态窗口 1:若此时用户态通过 Stale TLB 写入     │
       │                    │ 数据直接写入物理页,由于尚未下发 I/O,不会丢数据  │
       │                    └───────────────────────────────────────────┘
       ▼
[2. try_to_unmap_flush_dirty()] ──► 必须在此处强制广播 IPI 刷新 TLB!
       │                            彻底切断用户态通过 Stale TLB 的写入通路
       ▼
[3. pageout() / a_ops->writepage()]
       │ ──► 清除 Dirty 标志,设置 Writeback 标志
       │ ──► 启动磁盘 / DMA 异步写回
       │                    ┌────────────────────────────────────────────┐
       │                    │ 此时若用户态再次访问 (TLB 已失效,走 PageFault)  │
       │                    │  - 读:命中 PageCache,正常读取                │
       │                    │  - 写:触发 Fault 并阻塞等待 Writeback 完成    │
       │                    └────────────────────────────────────────────┘
       ▼
[4. 磁盘写回完成 (end_page_writeback)] ──► 页面变 Clean
       │
       ▼
[5. __remove_mapping()] ──► 从 PageCache 中脱钩
       │
       ▼
[6. try_to_unmap_flush() (L1292)] ──► 最终兜底刷新 TLB,防止内存泄露
       │
       ▼
[7. free_unref_folios()] ──► 物理页安全交还伙伴系统

在整个过程分成四个节点看:

阶段 1:try_to_unmap() 执行完毕,但尚未 Flush TLB

在 try_to_unmap_one() 中:

  • 内核获取 PTL 锁,将硬件页表项(PTE)清空(ptep_get_and_clear())。
  • 发现 PTE 是 Dirty 的,把脏标志同步回物理页(folio_mark_dirty(folio)),并标记当前任务的批处理标志:tlb_ubc->writable = true。
  • 此时的状态:内存中的 PTE 已经失效,但远程 CPU 的硬件 TLB 中依然缓存着该物理页的可写条目(Stale Writable TLB Entry)。

如果此时用户态通过 Stale TLB 执行读/写:

  • 读操作:直接命中物理页,读到当前数据。
  • 写操作:硬件直接将数据写入物理页内存。因为此时磁盘 I/O 尚未开始,这个新写入的数据仍然安全地保存在物理内存中,不会引发崩溃。

在 try_to_unmap 函数中,可能刷了 TLB 也可能等着攒着批量刷。在这里 TLB flush 是一个不确定的行为。



阶段 2:下发 I/O 前的必须刷 TLB —— try_to_unmap_flush_dirty()

当前不得不回收脏页时,就必须的及时的刷 TLB。

在进入 pageout() 之前,会强制调用 try_to_unmap_flush_dirty():向所有相关 CPU 广播 IPI,强制作废所有的 Stale Writable TLB。

如果这里不刷 TLB 会发生什么?

如果不清理 stale TLB,如何发生写内存,页面的数据将会在内核无感知的情况下被修改,这个时候写回到磁盘的数据就不是最新的。以数据库、存储场景为例,就会出现存储不一致的问题。

​

回收发现:dirty or write pte,在回收最后的环节:pageout,会反向映射检查。比前面还严格,前面仅检查 dirty 位,现在只要属于 write pte,也无脑的 flush tlb,以防万一。

在回收最后的环节:pageout,会反向映射检查

                    pageout() 返回 PAGE_SUCCESS
                   (此时 folio 已被 writepage 解锁)
                               │
            ┌──────────────────┴──────────────────┐
            ▼                                     ▼
   【异步 I/O (NVMe/SSD/HDD)】             【同步 I/O (Ramdisk/ZRAM)】
   folio_test_writeback 为真              writeback 已结束, 页面已干净
            │                                     │
            ▼                                     ▼
      goto keepit;                        尝试重新拿锁 folio_trylock()
 (放回 LRU,等待 DMA 完成)                          │
                                        ┌─────────┴─────────┐
                                      成功                 失败
                                        │                   │
                                   双重状态复查              ▼
                               (是否重新变脏/writeback)    goto keepit;
                                        │
                               ┌────────┴────────┐
                              正常              异常
                               │                 │
                               ▼                 ▼
                          fallthrough;      goto locked_keepit;
                       调用 folio_free()     (解锁并放回 LRU)
                        【立即释放物理页】

上面是根据后备文件,异步或同步写回脏数据。个人认为其实都实现成异步的没啥问题,对于 ramdisk 或者 zram,再快也得花点时间,直接返回处理下一个页面,效率上,这样反而更理想。



阶段 3:磁盘回写中(In-Flight Writeback)的用户态访问

此时 TLB 已经被彻底清空:

  1. 用户态再次尝试【读取】:
  • 触发 TLB Miss -> 遍历页表发现 PTE 为空 -> 触发缺页异常(filemap_fault)。
  • 在 PageCache 中找到正在回写的 folio,建立只读 PTE,用户可以正常、无阻塞地读取该页面。
  1. 用户态再次尝试【写入】:
  • 触发缺页异常(wp_page_shared() -> folio_wait_writeback())。
  • 内核会显式阻塞该写入进程,等待磁盘写回完全结束。这是为了防止并发修改正在进行磁盘 I/O 的缓冲区导致数据校验错误或文件系统损坏。


阶段 4:写回完成与最终释放(try_to_unmap_flush)

  1. 磁盘写回完成后,中断处理程序调用 end_page_writeback() 清除 writeback 标志,页面变为 clean。
  2. 在随后的回收轮次中,内核通过 __remove_mapping() 将该 clean 页面从文件的 PageCache 树中解绑。
  3. 最后执行try_to_unmap_flush(),确保所有阶段的所有剩余 TLB 项全部失效,随后安全调用 free_unref_folios() 将物理页归还伙伴系统。



其实,最后来看,在回收过程中,对于有写权限且需回写的文件页,发生用户写内存的行为只存在两种情况:try_to_unmap_flush_dirty 之前和之后,两种情况,对于这两种情况的行为:

  1. 在 try_to_unmap_flush_dirty() 之前:通过 Stale TLB 写入的数据会被暂存在物理页中,随后随回写一起刷盘,不会丢数据。
  2. 在 try_to_unmap_flush_dirty() 之后:Stale TLB 彻底消失,后续写操作被页表截获并强制走 Page Fault,被严格同步/阻塞在 Writeback 之后,保证了磁盘数据与内存的一致性,防止了数据丢失与内存损坏。

​

主动回收 + 文件页路径:

  1. dirty pte or 脏页:
  1. 对于 kswapd:pageout 中发现,还是会设置为 clean+wrprotect(pageout -> folio_clear_dirty_for_io);
  2. + write pte:同上处理
  1. write pte:属于 clean 页
  1. 同时是 !dirty pte?这种组会是否有可能?
  1. tmpfs:建立 vma(有写权限)-> 第一次读 -> pte_write == 1 且 !pte_dirty ?
  2. ramfs
  1. 其他 pte:可能刷了 tlb,也可能没刷
  1. clean 页面(刚发生写回),处于 clean + wrprotect pte,可以直接回收,如果发生写内存,通过缺页异常保证一致性;

在 真开始回收前,基本可以说 pte 处于 clean+wrprotect 状态,后面不可能发生无感知写操作了。


脏页写回

Linux内核由于存在page cache, 一般修改的文件数据并不会马上同步到磁盘,会缓存在内存的page cache中,我们把这种和磁盘数据不一致的页称为脏页,脏页会在合适的时机同步到磁盘。为了回写page cache中的脏页,需要标记页为脏。

脏页跟踪是指内核如何在合适的时机记录文件页为脏,以便内核在进行脏页回写时,知道将哪些页面回写到磁盘。匿名页不需要跟踪脏页,因为不需要同步到磁盘;私有文件页也不需要跟踪脏页,因为映射的时候,可写页会映射为只读,写访问会发生写时复制,转变为匿名页;所以只有共享的文件页需要跟踪脏页。跟踪有两个层面:一个是页表项记录,一个是页描述符记录。

访问文件页有两种方式:一种是通过mmap映射文件,一种是通过文件系统的write接口操作文件,本文将对这两种方式进行讲解。在Linux内核中,因为跟踪脏页会涉及到文件回写、缺页异常、反向映射等技术,这里重点记录在何时何地标记脏页、脏页写回以及文件大页的写操作处理。


脏页出现的几个场景

sys_write()

__block_commit_write()
mark_buffer_dirty()
__set_page_dirty() // fs/buffer.c

​

mmap()+MAP_SHARED

写page导致缺页,设置该页为脏页。

do_page_fault()
  handle_pte_fault()
    do_linear_fault()
      __do_fault
        filemap_fault()
          set_page_dirty_balance()
            set_page_dirty()
              __set_page_dirty_buffers()
    do_fault
      do_page_mkwrite
        xfs_filemap_page_mkwrite
          __xfs_filemap_fault
            iomap_page_mkwrite
              iomap_apply
                xfs_buffered_write_iomap_begin

有些缺页是无法处理的,只能返回错误,对于缺页但处理异常,用户看到的类似“Bus error (core dumped)”。

mmap()脏页回写后再次写操作

mmap()的脏页回写时会将对应的页表项置为只读,再次写page时,由于权限不够触发缺页异常。

do_page_fault
  handle_pte_fault
    do_wp_page
      set_page_dirty_balance
        set_page_dirty
          __set_page_dirty_buffers

​

文件系统元数据块为脏页

文件系统在分配,释放数据块时,会弄脏元数据块,元数据块所在的page也会变脏。

ext2_alloc_branch
  mark_buffer_dirty_inode
    mark_buffer_dirty
      __set_page_dirty


​

脏页写回

/proc/sys/vm/dirty_writeback_centisecs

另外,dirty_expire_centisecs设置,默认值是3000。单位是百分之一秒,所以就是脏页过了30秒就会被内核线程认为需要写回到磁盘了。

# cat /proc/sys/vm/dirty_expire_centisecs
3000

绝大部分情况都是写入到PageCache中就返回了,这时并没有真正写入磁盘。我们的数据会在如下三个时机下被真正发起写磁盘IO请求:

  • 第一种情况,如果write系统调用时,如果发现PageCache中脏页占比太多,超过了dirty_ratio或dirty_bytes,write就必须等待了。
  • 第二种情况,write写到PageCache就已经返回了。worker内核线程异步运行的时候,再次判断脏页占比,如果超过了dirty_background_ratio或dirty_background_bytes,也发起写回请求。
  • 第三种情况,这时同样write调用已经返回了。worker内核线程异步运行的时候,虽然系统内脏页一直没有超过dirty_background_ratio或dirty_background_bytes,但是脏页在内存中呆的时间超过dirty_expire_centisecs了,也会发起回写。

内核线程刷脏页

流程:

wb_check_old_data_flush()

wb_workfn()
    --> wb_writeback()
        --> __writeback_inodes_wb()
            --> writeback_sb_inodes()
                --> __writeback_single_inode()
                    --> do_writepages()
                        --> xfs_vm_writepages() /* 进入XFS */

xfs_vm_writepages()
    --> iomap_writepages()
        --> write_cache_pages()
            --> iomap_do_writepage()
                --> iomap_writepage_map() /* 提交bio */


脏页写回的方式:

iomap_do_writepage
    --> iomap_writepage_map()



iomap_writepage_map()处理的核心部分:
static int
iomap_writepage_map(struct iomap_writepage_ctx *wpc,
>------->-------struct writeback_control *wbc, struct inode *inode,
>------->-------struct page *page, u64 end_offset)
{
>-------struct iomap_page *iop = to_iomap_page(page);
>-------struct iomap_ioend *ioend, *next;
>-------unsigned len = i_blocksize(inode);
>-------u64 file_offset; /* file offset of page */
>-------int error = 0, count = 0, i;
>-------LIST_HEAD(submit_list);

>-------WARN_ON_ONCE(i_blocksize(inode) < PAGE_SIZE && !iop);
>-------WARN_ON_ONCE(iop && atomic_read(&iop->write_count) != 0);

>-------/*
>------- * Walk through the page to find areas to write back. If we run off the
>------- * end of the current map or find the current map invalid, grab a new
>------- * one.
>------- */
>-------for (i = 0, file_offset = page_offset(page);
>-------     i < (PAGE_SIZE >> inode->i_blkbits) && file_offset < end_offset;
>-------     i++, file_offset += len) {
>------->-------if (iop && !test_bit(i, iop->uptodate))
>------->------->-------continue;

>------->-------error = wpc->ops->map_blocks(wpc, inode, file_offset);
>------->-------if (error)
>------->------->-------break;
>------->-------if (wpc->iomap.type == IOMAP_HOLE)
>------->------->-------continue;
>------->-------iomap_add_to_ioend(inode, file_offset, page, iop, wpc, wbc,
>------->------->------->------- &submit_list);
>------->-------count++;
>-------}
...
...
}

iomap_writepage_map()中只处理一个 page 中需要写回的部分。


5.4 版本中,对于脏页,其 xarry entry 带有 tag:PAGECACHE_TAG_DIRTY(必要不充分);

/* XArray tags, for tagging dirty and writeback pages in the pagecache. */
#define PAGECACHE_TAG_DIRTY>----XA_MARK_0
#define PAGECACHE_TAG_WRITEBACK>XA_MARK_1
#define PAGECACHE_TAG_TOWRITE>--XA_MARK_2


fsync 写回脏页

fsync 写回脏页流程:

命令:./tpoint -p 2139481 -s iomap:iomap_writepage

do_fsync()
    --> vfs_fsync_range()
        --> xfs_file_fsync()
            --> file_write_and_wait_range()
                --> __filemap_fdatawrite_range()
                    --> do_writepages()
                        --> xfs_vm_writepages()

xfs_vm_writepages()
    --> iomap_writepages()
        --> write_cache_pages()
            --> iomap_do_writepage()

fsync 和内核线程脏页写回部分最终都会调用do_writepages()写回脏页。

​

REQ_SYNC
bio->bi_opf = REQ_OP_WRITE | REQ_SYNC;

​

close 写回脏页

总结:刚创建的文件,文件系统还未分配数据块,close后不会立即写回,如果已经存在数据块,close后,脏页差不多会在很短时间内立即写回(在 XFS 和 EXT4 上已确认);

task_work_run()
    --> ____fput()
        --> __fput()
            --> ext4_release_file()
                --> ext4_alloc_da_blocks()
                    --> filemap_flush()
                        --> do_writepages()



回写总览


映射之初

最开始映射:

do_mmap
->mmap_region
    ->vma_set_page_prot(vma)
        ->vm_page_prot = vm_pgprot_modify(vma->vm_page_prot, vm_flags);  ---------1
            ->pgprot_modify(oldprot, vm_get_page_prot(vm_flags))
        ->WRITE_ONCE(vma->vm_page_prot, vm_page_prot);  ---------------2

​

  /* description of effects of mapping type and prot in current implementation.
   * this is due to the limited x86 page protection hardware.  The expected
   * behavior is in parens:
   *
   * map_type>----prot
   *>----->-------PROT_NONE>------PROT_READ>------PROT_WRITE>-----PROT_EXEC
   * MAP_SHARED>--r: (no) no>-----r: (yes) yes>---r: (no) yes>----r: (no) yes
   *>----->-------w: (no) no>-----w: (no) no>-----w: (yes) yes>---w: (no) no
   *>----->-------x: (no) no>-----x: (no) yes>----x: (no) yes>----x: (yes) yes
   *
   * MAP_PRIVATE>-r: (no) no>-----r: (yes) yes>---r: (no) yes>----r: (no) yes
   *>----->-------w: (no) no>-----w: (no) no>-----w: (copy) copy>-w: (no) no
   *>----->-------x: (no) no>-----x: (no) yes>----x: (no) yes>----x: (yes) yes
   */

/* Update vma->vm_page_prot to reflect vma->vm_flags. */
 void vma_set_page_prot(struct vm_area_struct *vma)
 {
 >-------unsigned long vm_flags = vma->vm_flags;
 >-------pgprot_t vm_page_prot;
 
 >-------vm_page_prot = vm_pgprot_modify(vma->vm_page_prot, vm_flags);
 >-------if (vma_wants_writenotify(vma, vm_page_prot)) {
 >------->-------vm_flags &= ~VM_SHARED; // 最开始共享页会被去掉VM_SHARED,并且后面映射的PTE没有写权限
 >------->-------vm_page_prot = vm_pgprot_modify(vm_page_prot, vm_flags);
 >-------}
 >-------/* remove_protection_ptes reads vma->vm_page_prot without mmap_lock */
 >-------WRITE_ONCE(vma->vm_page_prot, vm_page_prot);
 }

这里冒出了vm_page_prot(主要基于vm_flags的差异),vm_page_prot表示真正的该页的可操作权限。

对于为MAP_SHARED建立映射过程中,最开始为其设置的PTE没有写权限,即表示首次读没有任何问题,但首次写会触发缺页异常,进入内核态,设置该页为脏页(一段时间后写回磁盘,在还是脏页的这段时间内,再次发生写,不会触发缺页异常)。

​

写文件页

一个简单场景,如下:

1)通过mmap映射共享文件。
2)第一次访问文件页时,发生缺页后读文件页到page cache, 如果是写访问则设置相应进程的页表项为脏、可写。
3)脏页回写时,会通过反向映射机制,查找映射这个页的每一个vma, 设置相应进程的页表项为只读,清脏标记。
4)假如第二次写访问这个文件页时,脏页的处理有两种情况:
page cache中的文件页还未回写到磁盘(3步骤之前), 此刻,这个文件页依然是脏页。因为相应进程的页表项为脏、可写,所以可以直接写这个页。

page cache中的文件页已经回写到磁盘(3步骤之后), 此刻,这个文件页不再是脏页。因为页表项为只读,所以写访问会发生写时复制缺页异常,异常处理中将处理共享文件页映射,重新将相应进程的页表项为设置为脏、可写。

​

第一次写访问文件页时

//mm/memory.c 
handle_pte_fault
    -> do_fault 
        -> do_shared_fault // 写触发导致的缺页异常
            -> __do_fault  // 读文件页到page cache
            -> do_page_mkwrite 
                -> vmf->vma->vm_ops->page_mkwrite() 
    			    -> ext4_page_mkwrite, // 对于ext4 或 filemap_page_mkwrite, // 对于ext2
                        -> set_page_dirty(page) 
                            -> __set_page_dirty_buffers
                                -> __set_page_dirty// page cache中标记页为脏 
                                    -> TestSetPageDirty(page) // 设置页描述符脏标记

                -> finish_fault  // 设置页表项
                    -> alloc_set_pte
                        -> if (write) // 如果是写触发导致的缺页异常
                            entry = maybe_mkwrite(pte_mkdirty(entry), vma) // 设置页表项脏、可写

vma->vm_page_prot与vma->vm_flags不同。另外对于写触发的缺页异常,会在下面的调用链路中创建page->private:

ext4_page_mkwrite()->block_page_mkwrite()->__block_write_begin()->create_page_buffers()

如果是“先读后写”,会在do_wp_page()->wp_page_shared()->do_page_mkwrite()申请page->private。

对于XFS文件系统,这块的路径是:xfs_filemap_page_mkwrite()->iomap_dirty_folio()分配private(对于blocksize大于或等于pagesize的此处不会分配);

​

​

提示:filemap_map_pages()不会读取磁盘中的文件建立pagecache,只会从寻找pagecache中已有的page然后建立页表。以读一个文件的大致过程为例:

do_read_fault() 
    -> map_pages() 失败,pagecache中没有对应的page;
      -> __do_fault() 预读pagecache成功,但是返回VM_FAULT_RETRY;
    	-> do_read_fault() 未返回用户态,第二次进入;
          -> map_pages() 成功,直接return;

并且对于只读操作,不会分配page->private,只有发生写的时候才会为page分配private(即buffer_head)。另外,对于写rw-p类型的文件页,会走do_cow_fault()路径,最终文件页会变为匿名页。

​

​

写回脏页:

// mm/page-writeback.c 

write_cache_pages
->clear_page_dirty_for_io(page) //对于回写的每一个页
   ->page_mkclean(page) //清脏标记  mm/rmap.c 
     ->page_mkclean_one //反向映射查找这个页的每个vma,调用清脏标记和写保护处理
       ->entry = pte_wrprotect(entry);     //写保护处理,设置只读
         entry = pte_mkclean(entry); //清脏标记 set_pte_at(vma->vm_mm, address, pte, entry) //设置到页表项中

    ->TestClearPageDirty(page) //清页描述符脏标记

// folio_clear_dirty_for_io
		if (folio_mkclean(folio))
			folio_mark_dirty(folio);

这个时候page->prrvate是否非空(即page_has_private(page)成立)。脏页写回的时候,只要发现有 dirty or write pte,立马会执行 tlb flush。

写过程,一般会持有 folio lock:

ext4_write_begin

    folio_lock(folio);
	if (folio->mapping != mapping) {
		/* The folio got truncated from under us */
		folio_unlock(folio);
		folio_put(folio);
		ext4_journal_stop(handle);
		goto retry_grab;
	}
	/* In case writeback began while the folio was unlocked */
	folio_wait_stable(folio);


/**
 * folio_wait_stable() - wait for writeback to finish, if necessary.
 * @folio: The folio to wait on.
 *
 * This function determines if the given folio is related to a backing
 * device that requires folio contents to be held stable during writeback.
 * If so, then it will wait for any pending writeback to complete.
 *
 * Context: Sleeps.  Must be called in process context and with
 * no spinlocks held.  Caller should hold a reference on the folio.
 * If the folio is not locked, writeback may start again after writeback
 * has finished.
 */
void folio_wait_stable(struct folio *folio)
{
	if (mapping_stable_writes(folio_mapping(folio)))
		folio_wait_writeback(folio);
}
EXPORT_SYMBOL_GPL(folio_wait_stable);

相关的写流程读会等待写回过程完成后,才能继续写些数据。

​

一个场景:进程A,以private方式映射了文件F,并读取了数据,进程B对文件进行了写操作,那么A再次读同一个位置时,与进程B写的值是否相同?

"If MAP_PRIVATE is specified, modifications to the mapped data by the calling process shall be visible only to the calling process and shall not change the underlying object. It is unspecified whether modifications to the underlying object done after the MAP_PRIVATE mapping is established are visible through the MAP_PRIVATE mapping."

再次写文件页:

//mm/memory.c 
handle_pte_fault 
->if (vmf->flags & FAULT_FLAG_WRITE) { //vma可写
    if (!pte_write(entry)) //页表项没有可写属性 return do_wp_page(vmf) //写时复制缺页异常处理
      do_wp_page 
         ->} else if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) == (VM_WRITE|VM_SHARED)) { //是共享可写的文件映射vma 
                return wp_page_shared(vmf);
                    ->do_page_mkwrite -> vmf->vma->vm_ops->page_mkwrite()
                         ->filemap_page_mkwrite, //对于ext2 ->set_page_dirty(page)
                             ->__set_page_dirty_buffers  //page cache中标记页为脏 
                                 ->TestSetPageDirty(page) //设置页描述符脏标记
                         ->finish_mkwrite_fault -> wp_page_reuse
                             ->entry = maybe_mkwrite(pte_mkdirty(entry), vma)

​

EXT4 读写文件


================================================================================
                    EXT4 文件读写核心函数调用栈 (Call Stack)
================================================================================

1. Ext4 缓冲读 (Buffered Read)
--------------------------------------------------------------------------------
sys_read() / ksys_read()
  └── vfs_read()
      └── call_read_iter()                           [file->f_op->read_iter]
          └── ext4_file_read_iter()                  [fs/ext4/file.c]
              └── generic_file_read_iter()           [mm/filemap.c]
                  └── filemap_read()
                      ├── filemap_get_pages()
                      │   ├── filemap_get_read_batch()       [命中 PageCache 场景]
                      │   └── [未命中 PageCache / 触发预读场景]
                      │       └── page_cache_sync_readahead() / page_cache_async_ra()
                      │           └── ondemand_readahead()
                      │               └── ra_submit()
                      │                   └── read_pages()
                      │                       └── a_ops->readahead() -> ext4_readahead() [fs/ext4/inode.c]
                      │                           └── ext4_mpage_readpages()             [fs/ext4/readpage.c]
                      │                               ├── ext4_map_blocks()              [fs/ext4/extents.c]
                      │                               │   └── ext4_ext_map_blocks()
                      │                               └── ext4_alloc_io_submit()
                      │                                   └── submit_bio()
                      │                       [或单一 Folio 同步缺页: a_ops->read_folio() -> ext4_read_folio()]
                      ├── copy_folio_to_iter()       [从内核 PageCache 拷贝数据至用户态 Buffer]
                      └── file_accessed()            [更新 inode 的访问时间 atime]


2. Ext4 直接读 (Direct I/O Read - O_DIRECT)
--------------------------------------------------------------------------------
sys_read() / ksys_read()
  └── vfs_read()
      └── ext4_file_read_iter()                      [fs/ext4/file.c]
          └── ext4_dio_read_iter()
              ├── inode_lock_shared(inode)           [获取 i_rwsem 共享锁]
              ├── ext4_should_use_dio()              [检查地址对齐与特性,不支持则回退 buffered]
              ├── iomap_dio_rw()                     [fs/iomap/direct-io.c]
              │   └── __iomap_dio_rw()
              │       ├── iomap_iter()
              │       │   └── ext4_iomap_ops.iomap_begin() -> ext4_iomap_begin() [fs/ext4/inode.c]
              │       │       └── ext4_map_blocks()  [查询物理 Block 映射]
              │       └── iomap_dio_bio_iter()
              │           ├── bio_alloc() / bio_add_page()
              │           └── submit_bio()           [直接向通用块层提交 BIO]
              ├── inode_unlock_shared(inode)
              └── file_accessed()

读过程并没有 lock folio,仅有 folio get 操作,说明当前从 page cache 获取到要拷贝的页面后,且在执行正式的copy_folio_to_iter 拷贝前,这个页面有可能在回收流程中已经从 page cache 解除了(remove mapping)。读的数据还是对的。

3. Ext4 缓冲写 (Buffered Write - 延迟分配 Delalloc)
--------------------------------------------------------------------------------
sys_write() / ksys_write()
  └── vfs_write()
      └── call_write_iter()                          [file->f_op->write_iter]
          └── ext4_file_write_iter()                 [fs/ext4/file.c]
              └── ext4_buffered_write_iter()
                  ├── inode_lock(inode)              [获取 i_rwsem 排他锁]
                  ├── ext4_write_checks()            [检查文件大小限制、追加模式、安全属性]
                  ├── generic_perform_write()        [mm/filemap.c]
                  │   ├── a_ops->write_begin() -> ext4_da_write_begin() [fs/ext4/inode.c]
                  │   │   ├── __filemap_get_folio()  [查找/分配 Folio 并加锁]
                  │   │   ├── ext4_da_reserve_space()[预留 Block 与元数据配额,不立即分配物理块]
                  │   │   └── create_empty_buffers() [绑定 buffer_head]
                  │   ├── copy_folio_from_iter_atomic() [将用户态 Buffer 写入内核 PageCache]
                  │   └── a_ops->write_end() -> ext4_da_write_end()     [fs/ext4/inode.c]
                  │       └── ext4_da_do_write_end()
                  │           ├── ext4_update_i_disksize() [更新内存中的 inode 尺寸]
                  │           ├── folio_unlock(folio)
                  │           ├── folio_put(folio)
                  │           └── block_dirty_folio() / folio_mark_dirty() [标记为脏页]
                  ├── inode_unlock(inode)
                  └── generic_write_sync()           [若带有 O_SYNC/O_DSYNC 则同步触发回写]


4. Ext4 直接写 (Direct I/O Write - O_DIRECT)
--------------------------------------------------------------------------------
sys_write() / ksys_write()
  └── vfs_write()
      └── ext4_file_write_iter()                     [fs/ext4/file.c]
          └── ext4_dio_write_iter()
              ├── ext4_dio_write_checks()            [判定是否需升级排他锁/处理未对齐写]
              ├── inode_lock() / inode_lock_shared()
              ├── ext4_journal_start() / ext4_orphan_add() [若为扩展写,开启 JBD2 事务记录 orphan]
              ├── iomap_dio_rw()                     [fs/iomap/direct-io.c]
              │   └── __iomap_dio_rw()
              │       ├── iomap_iter()
              │       │   └── ext4_iomap_ops.iomap_begin() -> ext4_iomap_begin() [fs/ext4/inode.c]
              │       │       └── ext4_map_blocks()  [预分配 unwritten extents 或映射已有块]
              │       └── iomap_dio_bio_iter()
              │           └── submit_bio()
              ├── ext4_dio_write_ops.end_io() -> ext4_dio_write_end_io() [I/O 完成回调]
              │   ├── ext4_convert_unwritten_extents() [将 unwritten extent 转为 written]
              │   └── ext4_handle_inode_extension()    [提交 JBD2 事务更新磁盘 inode 尺寸]
              ├── ext4_inode_extension_cleanup()
              └── inode_unlock() / inode_unlock_shared()


5. Ext4 后台脏页回写 (Writeback / Flusher)
--------------------------------------------------------------------------------
wb_workfn() / sync() / fsync() / 内存回收
  └── wb_do_writeback()
      └── __writeback_inodes_wb()
          └── writeback_sb_inodes()
              └── __writeback_single_inode()
                  └── do_writepages()
                      └── a_ops->writepages() -> ext4_writepages() [fs/ext4/inode.c]
                          ├── ext4_do_writepages()
                          │   ├── mpage_prepare_extent_to_map()
                          │   ├── mpage_map_and_submit_extent()
                          │   │   ├── mpage_da_map_blocks() / ext4_map_blocks()
                          │   │   │   └── ext4_mb_new_blocks() [mballoc 多块分配器分配物理磁盘块]
                          │   │   └── ext4_alloc_io_submit()
                          │   │       └── submit_bio()
                          │   └── ext4_journal_start() / ext4_journal_stop() [提交 JBD2 日志事务]

​

​

XFS 读写文件


================================================================================
                    XFS 文件读写核心函数调用栈 (Call Stack)
================================================================================

1. XFS 缓冲读 (Buffered Read)
--------------------------------------------------------------------------------
sys_read() / ksys_read()
  └── vfs_read()
      └── call_read_iter()                           [file->f_op->read_iter]
          └── xfs_file_read_iter()                   [fs/xfs/xfs_file.c]
              └── xfs_file_buffered_read()
                  ├── xfs_ilock(ip, XFS_IOLOCK_SHARED) [获取 XFS 专属 IOLOCK 共享锁]
                  ├── generic_file_read_iter()       [mm/filemap.c]
                  │   └── filemap_read()
                  │       ├── filemap_get_pages()
                  │       │   ├── filemap_get_read_batch()   [命中 PageCache]
                  │       │   └── [未命中 PageCache 触发预读]
                  │       │       └── page_cache_sync_readahead()
                  │       │           └── ondemand_readahead()
                  │       │               └── read_pages()
                  │       │                   └── a_ops->readahead() -> xfs_vm_readahead() [fs/xfs/xfs_aops.c]
                  │       │                       └── iomap_readahead()                    [fs/iomap/buffered-io.c]
                  │       │                           └── iomap_readahead_folio()
                  │       │                               ├── xfs_read_iomap_ops.iomap_begin() -> xfs_read_iomap_begin() [fs/xfs/xfs_iomap.c]
                  │       │                               │   └── xfs_bmapi_read()         [查询 B+Tree 获取 block 映射]
                  │       │                               └── submit_bio()
                  │       │                   [或单 Folio 缺页: a_ops->read_folio() -> xfs_vm_read_folio() -> iomap_read_folio()]
                  │       ├── copy_folio_to_iter()   [从内核 PageCache 拷贝到用户态 Buffer]
                  │       └── file_accessed()
                  └── xfs_iunlock(ip, XFS_IOLOCK_SHARED)


2. XFS 直接读 (Direct I/O Read - O_DIRECT)
--------------------------------------------------------------------------------
sys_read() / ksys_read()
  └── vfs_read()
      └── xfs_file_read_iter()                       [fs/xfs/xfs_file.c]
          └── xfs_file_dio_read()
              ├── xfs_ilock_iocb(iocb, XFS_IOLOCK_SHARED)
              ├── file_accessed()
              ├── iomap_dio_rw()                     [fs/iomap/direct-io.c]
              │   └── __iomap_dio_rw()
              │       ├── iomap_iter()
              │       │   └── xfs_read_iomap_ops.iomap_begin() -> xfs_read_iomap_begin() [fs/xfs/xfs_iomap.c]
              │       │       └── xfs_bmapi_read()   [遍历 B+Tree 检索 Extent]
              │       └── iomap_dio_bio_iter()
              │           └── submit_bio()
              └── xfs_iunlock(ip, XFS_IOLOCK_SHARED)


3. XFS 缓冲写 (Buffered Write - iomap + 延迟分配 Delalloc)
--------------------------------------------------------------------------------
sys_write() / ksys_write()
  └── vfs_write()
      └── call_write_iter()                          [file->f_op->write_iter]
          └── xfs_file_write_iter()                  [fs/xfs/xfs_file.c]
              └── xfs_file_buffered_write()
                  ├── xfs_ilock(ip, XFS_IOLOCK_EXCL) [获取 XFS IOLOCK 排他锁]
                  ├── xfs_file_write_checks()        [检查权限、文件配额、时间戳]
                  ├── iomap_file_buffered_write()    [fs/iomap/buffered-io.c] (注意: XFS 缓冲写全面采用 iomap 框架)
                  │   └── iomap_iter()
                  │       ├── xfs_buffered_write_iomap_ops.iomap_begin() -> xfs_buffered_write_iomap_begin() [fs/xfs/xfs_iomap.c]
                  │       │   ├── xfs_bmapi_reserve_delalloc() [在内存 B+Tree 中预留 delalloc extent]
                  │       │   └── xfs_reflink_allocate_cow()   [若是 CoW / reflink 场景]
                  │       └── iomap_write_iter()
                  │           ├── iomap_write_begin()
                  │           │   └── iomap_get_folio()        [获取并加锁 Folio]
                  │           ├── copy_folio_from_iter_atomic()[用户态拷贝数据至 PageCache]
                  │           └── iomap_write_end()
                  │               ├── iomap_set_range_dirty() -> iomap_dirty_folio() [标记脏页]
                  │               ├── folio_unlock(folio)
                  │               └── folio_put(folio)
                  ├── xfs_iunlock(ip, XFS_IOLOCK_EXCL)
                  └── generic_write_sync()


4. XFS 直接写 (Direct I/O Write - O_DIRECT)
--------------------------------------------------------------------------------
sys_write() / ksys_write()
  └── vfs_write()
      └── xfs_file_write_iter()                      [fs/xfs/xfs_file.c]
          └── xfs_file_dio_write()
              ├── xfs_file_dio_write_aligned() / xfs_file_dio_write_unaligned()
              │   ├── xfs_ilock_iocb_for_write()     [按是否为 overwrite/unaligned 获取 XFS_IOLOCK_SHARED 或 EXCL 锁]
              │   ├── xfs_file_write_checks()
              │   ├── iomap_dio_rw()                 [fs/iomap/direct-io.c]
              │   │   └── __iomap_dio_rw()
              │   │       ├── iomap_iter()
              │   │       │   └── xfs_direct_write_iomap_ops.iomap_begin() -> xfs_direct_write_iomap_begin() [fs/xfs/xfs_iomap.c]
              │   │       │       └── xfs_iomap_write_direct()
              │   │       │           └── xfs_bmapi_write()    [向 B+Tree 分配或转换物理 Extents]
              │   │       └── iomap_dio_bio_iter()
              │   │           └── submit_bio()
              │   └── xfs_dio_write_ops.end_io() -> xfs_dio_write_end_io() [fs/xfs/xfs_file.c]
              │       ├── xfs_iomap_write_unwritten()[提交事务将 unwritten extents 转为 written]
              │       └── xfs_setfilesize()          [提交事务更新磁盘 inode 文件大小]
              └── xfs_iunlock()


5. XFS 脏页回写 (Writeback / Flusher)
--------------------------------------------------------------------------------
wb_workfn() / sync() / fsync() / 内存回收
  └── wb_do_writeback()
      └── __writeback_inodes_wb()
          └── writeback_sb_inodes()
              └── __writeback_single_inode()
                  └── do_writepages()
                      └── a_ops->writepages() -> xfs_vm_writepages() [fs/xfs/xfs_aops.c]
                          └── iomap_writepages()                     [fs/iomap/buffered-io.c]
                              └── write_cache_pages() / iomap_do_writepage()
                                  ├── xfs_writeback_ops.map_blocks() -> xfs_map_blocks() [fs/xfs/xfs_aops.c]
                                  │   ├── xfs_bmapi_write()          [将 delalloc 块正式分配磁盘物理 block]
                                  │   └── xfs_trans_alloc() / xfs_trans_commit()
                                  ├── iomap_add_to_ioend()
                                  └── iomap_submit_ioend()
                                      └── submit_bio()
                              [I/O 完成工作队列回调: xfs_end_io() -> 事务提交将 extent 标记为 written]


file THP

如果强制将一个rwxs的文件页转换为大页后:

首次写时,由于不是脏页或者说pmd没有写权限,会发生缺页异常,执行__handle_mm_fault->wp_huge_pmd->__split_huge_pmd(),清pmd,回退到do_fault()路径从pagecache获取页(由于pagecache上还是大页,导致后续如果只发生读操作,此处还是大页)。

​

主要揭秘写file THP会发生什么。

以及为什么需要对齐。

batch flush dirty

感觉下面的sysctl_vm_batch_dirty_tlb_flush 有点问题:

			/*
			 * For anon, we should only see swap cache (anon) and
			 * the list pinning the page. For file page, the filemap
			 * and the list pins it. Combined with the page_ref_freeze
			 * in pageout_batch ensure nothing else touches the page
			 * during lock unlocked.
			 */
			if (sysctl_vm_batch_dirty_tlb_flush) {
				folio_unlock(folio);
				if (!folio_batch_add(&batch_out_folios, folio))
					pageout_batch(&batch_out_folios,
						      &ret_folios, &free_folios,
						      sc, stat, &plug,
						      folio_list);
			} else {
				/*
				 * Folio is dirty. Flush the TLB if a writable entry
				 * potentially exists to avoid CPU writes after I/O
				 * starts and then write it out here.
				 */
				try_to_unmap_flush_dirty();
				pageout_one(folio, &ret_folios, &free_folios,
					    sc, stat, &plug, folio_list);
			}


参考

https://www.cnblogs.com/linhaostudy/p/15678371.html

https://blog.csdn.net/hu1610552336/article/details/120857139

​

​

  1. No comments yet.