目录

图片无法显示,请右键点击新窗口打开图片


COW(Copy-On-Write) 机制

图片无法显示,请右键点击新窗口打开图片

在 Linux 和其他操作系统中,COW 是写时复制(Copy-On-Write)的缩写。这是一种计算机程序设计领域的优化策略,用于高效地管理系统资源,如内存。这种方法的主要思想是,只有在必须修改数据时才复制该数据,从而减少不必要的数据复制. 写时复制在多个领域都很有用,但在操作系统和文件系统中尤其常见。以下是写时复制的几个关键应用领域:

  • 内存管理: 在虚拟内存系统中,当多个进程共享相同的内存页面(例如通过 fork() 系统调用创建的子进程)时,COW 策略允许这些进程共享相同的物理内存,直到其中一个进程尝试写入(修改)内存。一旦发生这种情况,操作系统会创建一个新的内存页面副本供写入操作使用,而其他进程继续使用原始未修改的页面. 这种方法减少了不必要的内存使用,因为只有在写入操作发生时才会分配新的内存.
  • 文件系统: 在某些支持写时复制的文件系统(例如 Btrfs 和 ZFS)中,当文件系统数据被请求修改时,原始数据的副本会被创建,在新的副本上进行修改,而原始数据保持不变. 这不仅有助于节省空间(通过避免不必要的复制),还提供了一种自然的快照机制,因为旧数据版本保持不变并可用于备份或恢复.
  • 数据库和应用程序: 写时复制策略也在数据库管理系统和高性能应用程序中得到应用,因为它可以减少不必要的数据复制,提高性能和数据完整性. 这在多线程或多处理器环境中尤其有用,其中数据复制和一致性是关键挑战.

写时复制是一种优化技术,它通过延迟数据复制直到绝对必要来减少资源使用,提高性能,并在某些情况下增加系统的可靠性和恢复能力. 在没有写时复制(Copy-On-Write, COW) 技术之前,操作系统和应用程序通常使用更为传统、直接且资源密集的方法来处理数据复制和进程分叉(forking):

  • 进程分叉(Forking): 在早期的 Unix 和 Linux 系统中,fork() 系统调用会通过复制整个进程的地址空间来创建一个新进程,即使这个新进程可能最终只需要访问其中的一小部分数据。这意味着,每次 fork() 调用,系统都会复制父进程的所有内存页面到子进程,这在资源使用和性能上是非常昂贵的,尤其是对于大型进程。这种方法会导致大量的 CPU 和内存资源使用,因为操作系统需要复制所有的页面,即使这些页面从未被修改过.
  • 文件系统和数据操作: 在没有 COW 的文件系统中,修改文件或数据结构通常意味着直接覆写原始数据。这不仅增加了数据损坏的风险(如果在写入过程中出现故障),而且还使得无法有效地保留数据的历史版本(对于备份和恢复很有用). 对于数据库和其他需要事务支持的系统,不使用 COW 可能需要更复杂的锁定机制和更多的冗余数据复制来维护一致性和数据完整性.

引入 COW 技术后,系统可以延迟数据复制,直到实际需要修改数据时才进行。这不仅减少了不必要的数据复制,从而节省了内存和 CPU 资源,还允许系统维护数据版本和状态的更详细记录,从而提高了数据的完整性和系统的可靠性。因此,COW 是操作系统、文件系统和高性能应用程序中一个重要的优化和增强特性.

COW 在内存管理的使用场景

在 Linux 中,写时复制(Copy-On-Write,COW) 技术被广泛用于多种内存管理场景,主要包括以下几个方面:

  • 进程 fork: 当一个进程执行 fork() 系统调用以创建一个新进程时,Linux 不会为子进程复制整个地址空间。相反父进程和子进程共享相同的物理内存页面,这些页面都被标记为只读。当父进程或子进程试图修改这些共享页面时,内核会捕捉到这一点(通过缺页),并仅为写入操作的进程创建所需页面的复制品,这就是经典的 COW 实现.
  • 内存映射文件(File-mapped): 当多个进程映射同一文件到它们的地址空间时,它们实际上会共享对应于该文件内容的物理内存页面. 如果进程以只读方式打开文件,那么它们可以共享同一物理页面。但是如果某个进程需要写入映射文件,COW 机制就会介入,内核会为该进程创建页面的私有复制品,以便更改不会影响到其他进程.
  • 页缓存(Page Cache) 和 写回缓存(Write-back Caching): Linux 使用页缓存来缓存磁盘上文件系统数据的内存副本。这允许快速访问这些数据,因为来自内存的访问比磁盘 I/O 快得多, 当文件系统或直接访问磁盘的应用程序修改缓存的数据时,COW 可以用来保持原始数据页面的不变副本,直到实际进行磁盘写操作.
  • 内核数据结构: COW 还用于内核自身的数据管理,例如在管理内核数据结构时。某些情况下,当数据结构需要更新而又不希望阻塞读取者时,内核可以使用 COW 创建数据的新版本
  • 虚拟化: 在虚拟化环境中,例如使用 KVM 的场景,COW 可以用来有效地管理虚拟机的磁盘镜像。通过使用 QCOW2(一种支持 COW 的磁盘镜像格式),新的虚拟机可以基于现有的镜像快速创建,而不需要复制整个镜像.
COW 机制的实现原理

在 Linux 中,写时复制(Copy-On-Write,COW) 是通过一系列内核机制和页面表管理策略实现的。以下是 COW 在 Linux 中的基本实现步骤:

图片无法显示,请右键点击新窗口打开图片

  • 共享页面标记: 当进行如 fork() 这样的操作时,子进程的地址空间最初是父进程的一个完整副本。但是,实际的物理页面并没有复制。相反,父子进程的页面表条目都指向相同的物理内存页面,并且这些页面都标记为只读。上图为 FORK 系统调用逻辑,可以看到系统调用在调用 dup_mmap 复制父进程的地址空间时,通过 copy_page_range 函数在复制过程中,如果虚拟区域支持 COW,那么调用 ptep_set_wrprotect 将页表标记为写保护(Write-Protection). 通过这种方式,任何对这些页面的修改尝试都会触发一个缺页(page fault),因为它们是只读的.

图片无法显示,请右键点击新窗口打开图片

  • 处理缺页(PageFault): 当进程(无论是父进程还是子进程)试图写入一个共享的、标记为只读(写保护)的页面时,MMU 会因为违反保护规则而触发一个缺页异常,这会导致内核的缺页异常处理函数介入. 上图为一个匿名内存的 COW 场景缺页异常处理逻辑,缺页异常处理函数首先辨识出 COW 缺页,其根据触发缺页虚拟地址对应的最后一级页表内容(可能是 PMD 和 PTE),此时页表如果 Present 标志位存在,当 R/W 标志位没有置位,另外本次缺页原因是 PF_WRITE,那么缺页异常处理函数可以判断这次是一个 COW 缺页,于是调用 do_wp_page 函数处理 COW 缺页.
  • 页面复制: 一旦确定需要进行 COW 缺页,内核会为写入操作的进程分配一个新的物理页面。然后内核会复制原始页面的内容到新页面,例如上图缺页异常处理函数调用 alloc_page_vma 分配一个新的物理页,然后调用 __wp_page_copy_user 函数将旧物理页的内容拷贝新的物理页上. COW 缺页还存在另外一种情况,就是缺页异常处理函数发现此时只有一个进程映射到 COW 物理页上,那么缺页异常处理函数可能不会分配新的物理页,而是重新使用(REUSER)该物理页.
  • 更新页面表: 当缺页异常处理函数准备好数据之后,缺页异常处理函数会更新页表为可写,并将页表映射到指定的页表.
  • 继续执行: 页表更新完毕之后,缺页异常处理函数返回,那么进程会再次执行引发缺页的指令,此时由于虚拟内存的页表已经没有问题,进程可以向虚拟内存写入数据.

这个机制的优点在于,只有在必要时才复制页面(即当某个进程试图写入页面时)。这避免了不必要的内存使用,尤其是在 fork() 之后,如果子进程立即使用 exec() 调用一个新程序,那么几乎所有页面的复制都是多余的,因为新程序会占用一个全新的地址空间。COW 机制不仅用于进程间的内存管理,还广泛用于文件系统(如 Btrfs 和 ZFS) 和虚拟化技术中.在这些场景中,COW 可以最小化对磁盘的写入操作,并允许快照和版本控制,这对于数据恢复和系统完整性至关重要.

COW 与缺页异常

图片无法显示,请右键点击新窗口打开图片

在 Linux 里,缺页异常处理函数识别 COW 缺页的第一个条件是: 发生缺页的页表必须是写保护(Write-Protection) 的,也就是页表 Entry 的 R/W 标志位清零,另外 Present 标志位是存在的,页表其余位置与正常页表无异,上图是 PTE Entry、PMD Entry 和 PUD Entry 写保护模式下的页表位图,可以看到除了 R/W(BIT1) 位清零,其他位和一个正常映射物理页的页表无异.

图片无法显示,请右键点击新窗口打开图片

缺页异常处理函数识别 COW 缺页的第二个条件是: 引起缺页的原因必须包括 PF_WRITE, 也就是发生缺页是正在执行写操作.

图片无法显示,请右键点击新窗口打开图片

缺页异常处理函数识别 COW 缺页的第三个条件是: 虚拟内存区域是私有映射,不能是共享映射,即 mmap 包含 MAP_PRIVATE 标志.

图片无法显示,请右键点击新窗口打开图片

在 XFS 文件系统里,同样也存在 COW,例如上图 XFS 文件映射的虚拟内存采用了 MAP_PRIVATE,并且执行 fork 动作之后进程对虚拟内存发起了写操作,那么此时正好构造 COW 缺页,可以看看缺页异常处理函数的处理逻辑,在 handle_pte_fault 函数里,函数首先缺页时是否为 FAULT_FLAGS_WRITE 或者 FAULT_FLAGS_UNSHARE, 那么此时满足条件二和三,接着缺页异常函数调用 pte_write 函数检查 PTE Entry 是否为包含 R/W 标志,对于 COW 缺页是不包含 R/W 标志,那么缺页异常函数识别出是 COW 缺页,那么调用 do_wp_page 函数处理 COW 缺页. 对于不同类型的 COW 缺页细节上可能存在差异,那么接下来对不同类型的 COW 缺页进行研究:

图片无法显示,请右键点击新窗口打开图片


匿名映射 COW(Copy-On-Write) 内存缺页场景

图片无法显示,请右键点击新窗口打开图片

写时拷贝(COW)匿名内存是当进程调用 fork() 函数构造子进程时,子进程继承了父进程的匿名内存,但 fork 系统调用会将父进程和子进程的匿名内存都修改为写保护,那么可以确保父子进程读到同样的内容,而父子进程对匿名内存要写的时候,才会在属于自己的匿名内存上执行写操作。COW 的核心是在写的时候才会分配物理内存承接写操作,其存在两种缺页场景:

  • 当 CPU 写入父子进程还在共享的 COW 匿名内存: MMU 因为写保护异常,于是触发缺页异常,在缺页异常处理函数里分配一个新的物理页,并将页表更新映射到新的物理页上,然后添加 _PAGE_RW 属性,最后将原始物理页的内容拷贝到新的物理页上,那么 CPU 后续的读写操作不会触发缺页.
  • 当 CPU 写入父进程或者子进程独占的 COW 匿名内存: MMU 因为写保护异常,于是触发缺页异常,在缺页异常处理函数会重新 REUSE 该物理页,并添加页表的 _PAGE_RW 属性,那么 CPU 后续的读写操作不会触发缺页.

图片无法显示,请右键点击新窗口打开图片

在 Linux 里使用 struct anon_vma 和 struct anon_vma_chain 两个数据结构体维护匿名内存的逆向映射,所有逆向映射就是通过匿名页被哪些 VMA 映射。AV 是 struct anon_vma 的简称,AVC 是 struct anon_vma_chain 的简称,每个 VMA 一一对应一个 AV,每个 AV 维护一颗红黑树,红黑树的节点表示映射到 AV 的不同 VMA,每个 VMA 通过 AVC 进行表示,那么只要遍历这颗红黑树就可以知道有多少进程的 VMA 映射到该 AV,那么可以看到一个 AVC 就是一次映射,可以通过 AVC 找到对应的 VMA吗,那么可以将匿名页指向 AV,那么就可以知道有多少进程映射到自己. 每当进程发送一次 fork 动作时,子进程会在父进程 AV 红黑树下新增一个 AVC 节点,AVC 节点指向子进程的 VMA,并且子进程 VMA 对应的 AV 的 Parent 指向父进程 VMA 对应 AV. 以此类推无论进程 fork 多少次,都可以知道一个匿名页被多少进程映射. 为了更加深刻了解 COW 匿名内存缺页,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with Anonymous on COW(Copy-On-Write) --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ANON-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-ANON-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,进程在 21 行调用 mmap 函数并使用 MAP_PRIVATE 和 MAP_ANONYMOUS 标志分配一段匿名内存, 并且赋予 PROT_READ 和 PROT_WRITE 属性,然后在 32 行对匿名内存进行写操作,然后在 34 行再次对匿名内存进行读操作. 程序接着在 37 行调用 fork 函数创建子进程,此时匿名内存变成写保护匿名内存,接着为了构造另外一个场景,39 行为子进程分支,其首先对 COW 匿名内存进行写操作,由于此时父子进程都映射到 COW 匿名内存,那么 39 行的写操作会触发缺页,但缺页异常处理函数会分配一个新匿名页,然后将内容拷贝到新匿名页上,那么接下来子进程在新的匿名页上执行写操作; 同理 41 行是父进程分支,其先 sleep 一段时间之后,COW 匿名内存此时只有父进程映射,因此 43 行的写操作同样会引起缺页,但缺页异常处理函数只是在原匿名页的基础上增加了写权限,那么父进程继续在原来的匿名页上进行写操作. 以上便是一个最基础的实践案例,可以知道 39 行读操作和 43 行写操作都会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 39 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在匿名内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-ANON-COW-default/
# 编译内核
make kernel
# 编程程序
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “COW Anonymous Memory on wp_page_copy 0x6000000000”, 子进程访问 COW 匿名内存时,缺页中断将原始内容拷贝到新匿名页上,子进程接下来都是访问新的匿名页. 通过实践可以看到写保护匿名内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

对于 COW 匿名内存,其缺页流程如上图,当子进程/父进程对两者共同映射的 COW 匿名内存发起写操作时,那么进入 “COPY PAGE” 分支进行处理; 当子进程/父进程对独占的 COW 匿名内存发起写操作时,那么进入 “REUSE PAGE” 分支进行处理.

图片无法显示,请右键点击新窗口打开图片

对于 CPU 写入父子进程还在共享的 COW 匿名内存, wp_page_copy 函数为核心处理函数,由于此时匿名内存并不是 ZERO Page,那么函数直接进入 3113 行分支,函数首先调用 alloc_page_vma 函数分配一个新的匿名页,然后在 3118 将调用 __wp_page_copy_user 函数将原始匿名页的内容拷贝到新的物理页上,此时页表已经更新映射到新的匿名页上,并且具有写权限,那么接下来对该匿名内存的读写操作不再会引起缺页异常.

图片无法显示,请右键点击新窗口打开图片

对于 CPU 写入父进程或者子进程独占的 COW 匿名内存, wp_page_reuse 函数为核心处理函数,函数的主要目的将匿名页设置为独占,并为页表添加 _PAGE_RW 标志,使原来的物理页具有写权限. 函数在 3066-3067 行调用 pte_mkyoung 和 maybe_mkwrite 函数向页表增加 _PAGE_RW 和 _PAGE_ACCESS 标志,最后在 3068 行调用 ptep_set_access_flags 函数更新 PTE Entry 页表. 那么 CPU 接下来的读写操作不会再触发缺页异常.

图片无法显示,请右键点击新窗口打开图片

上图是 fork 系统调用处理逻辑,其中核心逻辑是新进程会调用 dup_mmap 函数拷贝父进程的地址空间,其遍历父进程的所有的 VMA,对于匿名映射的 VMA,那么调用 anon_vma_fork 函数建立逆向映射,最后调用 copy_page_range 函数遍历 VMA 对于的页表,将 COW 匿名内存对应页表的 _PAGE_RW 标志去掉,此时可以看到调用 ptep_set_wrprotect 函数进行写保护,最终更新页表. 那么无论是父进程还是子进程的 COW 匿名内存都变成了写保护.

图片无法显示,请右键点击新窗口打开图片


MINIXFS 文件系统 COW 缺页场景

MINIX 文件系统(MINIX FS) 是一个轻量级的文件系统,最初由 Andrew S. Tanenbaum 为教育和研究目的而创建。MINIX 是一个微内核操作系统的一部分,并在教育领域广泛用于教授操作系统和文件系统的原理。MINIX 文件系统的设计目标之一是简单和可理解性,以便于教育和研究。它的代码和数据结构相对较小,易于阅读和学习。MINIX 文件系统最初是为 MINIX 操作系统设计的,但它也可以在其他 Linux 系统上运行。MINIXFS 文件系统具有如下特点:

  • MINIX 文件系统使用类似于 UNIX 文件系统的层次结构,包括超级块、i 节点表、数据块等
  • 支持文件和目录的创建、删除、读取和写入等基本文件操作
  • 支持文件权限和所有权
  • 支持文件系统检查和修复工具。

MINIX 文件系统通常不用于生产环境或大规模部署,而是作为教育和研究的工具。在 Linux 系统中,通常使用更高性能和功能更丰富的文件系统,如 Ext4、XFS、Btrfs 等,以满足各种应用场景的需求。MINIX 文件系统的设计和原理仍然具有教育和研究的价值,但在实际生产环境中使用时需要谨慎考虑其局限性。在 Linux 中使用 MINIXFS 需要打开内核宏 CONFIG_MINIX_FS.

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

MINIXFS 文件系统提供的文件操作如上,可以看到映射文件的 mmap 接口使用了通用的 generic_file_mmap 函数,该函数为文件映射的 VMA 提供的 vm_ops 接口为 generic_file_vm_ops,该数据接口实现了 fault 接口 filemap_fault,那么文件映射 VMA 发生缺页时 filemap_fault 函数会被调用.

图片无法显示,请右键点击新窗口打开图片

在 MINIXFS 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,MINIXFS 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。

图片无法显示,请右键点击新窗口打开图片

MINIXFS 文件系统也支持 COW 机制,进程在使用 mmap 将文件映射到进程地址空间时,使用 MAP_PRIVATE 标志即可启用 COW 机制。在 MINIXFS 文件系统了,当对 COW 的文件映射内存发起写操作时,MMU 检查到进程对 COW 内存没有写权限,于是触发缺页异常,缺页异常会为 VMA 新分配一个 COPY PAGE 作为副本,此时 COPY PAGE 为匿名页,接着将 VMA 对应的 PAGE CACHE 内容拷贝到 COPY PAGE 里,并将页表更新到 COPY PAGE,当缺页异常返回之后,进程对 COW 内存的读写操作都落到 COPY PAGE 里,但 COPY PAGE 的内容不会回写到磁盘文件上. 那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如 下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Virtio-BLK: MINIX Filesystem Disk  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped MINIXFS COW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-MINIXFS-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-FILE-MINIXFS-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

测试用例由一个用户程序构成,程序首先在 24 行调用 open 函数打开 MINIXFS 文件系统目录下的 BiscuitOS.txt 文件,然后在 30 行调用 mmap 函数将文件映射到进程的地址空间, 此时使用了 MAP_PRIVATE 标志,接下来在 41 行对文件映射的虚拟内存进行读操作,此时会发送 FAULT AROUND 将附近的虚拟内存页表一共建立,接下来在 44 行对虚拟内存进行写操作,此时会触发 COW,并在 46 行打印读到的数据,最后就是释放内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 41 行读操作和 46 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 46 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 MINIXFS 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_MINIX_FS):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-MINIXFS-COW-default/
# 编译内核
make kernel
# 编译实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “MINIXFS COW on wp_page_copy 0x6000000000”, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行了读操作,那么会触发相应的缺页异常,缺页异常处理函数会分配 PAGECACHE,并从磁盘加载文件内容到 PAGECACHE,最后建立页表映射到 PAGECACHE。在这种场景下,进程对该内存执行写操作,MMU 检查到没有写权限于是触发缺页异常,此时缺页异常处理函数的处理逻辑如上,由于此时页表已经存在,只是因为写保护触发的缺页,那么 do_wp_page 函数进行逻辑处理,由于此时 PAGECACHE 不是匿名内存,那么调用 wp_page_copy 函数,该函数首先分配一个匿名页,然后调用 __wp_page_copy_user 函数将 PAGECACHE 内容拷贝到匿名页,此时匿名页就是 PAGECACHE 的一个副本,缺页异常将页表映射到匿名页上,那么后续进程的读写操作都在该副本匿名页上.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行写操作,那么会触发上图逻辑,由于页表不存在,那么缺页异常处理函数会调用 do_cow_fault 函数,该函数首先分配一个匿名页,然后调用 __do_fault 函数分配 PAGECACHE,并将磁盘文件内容加载到 PAGECACHE 里,接着调用 copy_user_highpage 将 PAGECACHE 内容拷贝到匿名内存里,此时匿名内存就是 PAGECACHE 的副本,缺页异常将页表映射到匿名内存上,那么接下来进程对 COW 内存的读写都落到匿名内存上.

图片无法显示,请右键点击新窗口打开图片

do_cow_fault 函数的逻辑如上,MINIXFS 文件系统没有特除的处理逻辑,此处可以看到 COW 过后,虽然虚拟内存是文件映射,但虚拟内存映射的匿名内存,因此如果进程再发生 fork 动作,那么就要按匿名内存 COW 场景进行处理,此时该虚拟内存还是会被设置为写保护。

图片无法显示,请右键点击新窗口打开图片


EXT2 文件系统 COW 缺页场景

EXT2(Extended File System 2)是 Linux 操作系统中使用的一种文件系统类型。它是 Linux 内核早期版本中广泛使用的文件系统,是 EXT 文件系统家族的第二代成员。EXT2 被设计用于提供可靠的文件存储和管理,并且在 Linux 社区中得到了广泛的支持。以下是一些关于 EXT2 文件系统的主要特点和概述:

  • 稳定性和可靠性: EXT2 文件系统被设计为稳定和可靠的文件系统。它使用一种称为日志文件系统(journaling filesystem)的技术,可以在系统崩溃或意外关机时减少文件系统损坏的风险
  • 支持大容量磁盘: EXT2 支持大容量硬盘驱动器,可以管理大型文件和大容量的存储设备。这使得它在服务器环境和桌面系统中都非常有用
  • 文件和目录管理: EXT2 提供了对文件和目录的有效管理,支持常见的文件操作,如创建、删除、重命名和权限管理
  • UNIX样式的权限管理: EXT2 采用了类似 UNIX 的权限模型,允许用户为每个文件和目录分配读、写和执行权限,以确保数据的安全性和隔离性
  • 文件系统检查工具: EXT2 配备了 fsck(文件系统检查)工具,可以用于检查和修复文件系统中的错误和损坏,以提高文件系统的可靠性
  • 不支持日志: 与后续版本的 EXT 文件系统(如 EXT3 和 EXT4)相比,EXT2 没有原生的日志功能,这意味着在突然断电或崩溃时,文件系统可能需要花费更多的时间来检查和修复,而且有更大的风险导致数据损坏

尽管 EXT2 是一个成熟的文件系统,但在某些情况下,更先进的文件系统如 EXT4 可能更适合用于现代 Linux 系统,因为它们提供了更好的性能、容错性和扩展性。EXT2 通常用于旧的 Linux 系统或特定的嵌入式系统,而不是最新的 Linux 发行版. 在 Linux 中使用 EXT2 需要打开内核宏 CONFIG_EXT2_FS.

图片无法显示,请右键点击新窗口打开图片

EXT2 文件系统提供的文件操作如上,可以看到映射文件的 mmap 接口使用了 ext2_file_mmap 函数,改函数起始就是 generic_file_mmap,该函数为文件映射的 VMA 提供的 vm_ops 接口为 generic_file_vm_ops,该数据接口实现了 fault 接口 filemap_fault,那么文件映射 VMA 发生缺页时 filemap_fault 函数会被调用.

图片无法显示,请右键点击新窗口打开图片

在 EXT2 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,EXT2 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。

图片无法显示,请右键点击新窗口打开图片

EXT2 文件系统也支持 COW 机制,进程在使用 mmap 将文件映射到进程地址空间时,使用 MAP_PRIVATE 标志即可启用 COW 机制。在 EXT2 文件系统里,当对 COW 的文件映射内存发起写操作时,MMU 检查到进程对 COW 内存没有写权限,于是触发缺页异常,缺页异常会为 VMA 新分配一个 COPY PAGE 作为副本,此时 COPY PAGE 为匿名页,接着将 VMA 对应的 PAGE CACHE 内容拷贝到 COPY PAGE 里,并将页表更新到 COPY PAGE,当缺页异常返回之后,进程对 COW 内存的读写操作都落到 COPY PAGE 里,但 COPY PAGE 的内容不会回写到磁盘文件上. 那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Virtio-BLK: EXT2 Filesystem Disk  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped EXT2 COW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-EXT2-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-FILE-EXT2-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

测试用例由一个用户程序构成,程序首先在 23 行调用 open 函数打开 EXT2 文件系统目录下的 BiscuitOS.txt 文件,然后在 29 行调用 mmap 函数将文件映射到进程的地址空间, 此时使用了 MAP_PRIVATE 标志,接下来在 40 行对文件映射的虚拟内存进行读操作,此时会发送 FAULT AROUND 将附近的虚拟内存页表一共建立,接下来在 45 行对虚拟内存进行写操作,此时会触发 COW,并在 45 行打印读到的数据,最后就是释放内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作和 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 EXT2 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_EXT2_FS):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-EXT2-COW-default/
# 编译内核
make kernel
# 编译实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “EXT2 COW on wp_page_copy 0x6000000000”, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行了读操作,那么会触发相应的缺页异常,缺页异常处理函数会分配 PAGECACHE,并从磁盘加载文件内容到 PAGECACHE,最后建立页表映射到 PAGECACHE。在这种场景下,进程对该内存执行写操作,MMU 检查到没有写权限于是触发缺页异常,此时缺页异常处理函数的处理逻辑如上,由于此时页表已经存在,只是因为写保护触发的缺页,那么 do_wp_page 函数进行逻辑处理,由于此时 PAGECACHE 不是匿名内存,那么调用 wp_page_copy 函数,该函数首先分配一个匿名页,然后调用 __wp_page_copy_user 函数将 PAGECACHE 内容拷贝到匿名页,此时匿名页就是 PAGECACHE 的一个副本,缺页异常将页表映射到匿名页上,那么后续进程的读写操作都在该副本匿名页上.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行写操作,那么会触发上图逻辑,由于页表不存在,那么缺页异常处理函数会调用 do_cow_fault 函数,该函数首先分配一个匿名页,然后调用 __do_fault 函数分配 PAGECACHE,并将磁盘文件内容加载到 PAGECACHE 里,接着调用 copy_user_highpage 将 PAGECACHE 内容拷贝到匿名内存里,此时匿名内存就是 PAGECACHE 的副本,缺页异常将页表映射到匿名内存上,那么接下来进程对 COW 内存的读写都落到匿名内存上.

图片无法显示,请右键点击新窗口打开图片

do_cow_fault 函数的逻辑如上,EXT2 文件系统没有特除的处理逻辑,此处可以看到 COW 过后,虽然虚拟内存是文件映射,但虚拟内存映射的匿名内存,因此如果进程再发生 fork 动作,那么就要按匿名内存 COW 场景进行处理,此时该虚拟内存还是会被设置为写保护。

图片无法显示,请右键点击新窗口打开图片


EXT3 文件系统 COW 缺页场景

EXT3(Extended File System 3) 是 Linux 操作系统中使用的一种文件系统类型,它是 EXT2 文件系统的升级版本,为文件系统添加了日志功能。EXT3 被广泛用于 Linux 系统,特别是在较早的Linux发行版中,以提供更好的数据一致性和可靠性。以下是 EXT3 文件系统的一些主要特点和概述:

  • 日志功能: EXT3 引入了日志文件系统(journaling filesystem)的概念,这意味着它在文件系统上有一个事务日志(journal),记录文件系统的更改操作。这使得在系统崩溃或非正常关机时,文件系统能够更容易地恢复到一致的状态,减少了数据损坏的风险
  • 向后兼容: EXT3 文件系统是 EXT2 文件系统的扩展,因此它与 EXT2 文件系统向后兼容。这意味着您可以将现有的 EXT2 文件系统升级为 EXT3,而不会丢失数据
  • UNIX样式的权限管理: 与 EXT2 一样,EXT3 采用了 UNIX 样式的权限模型,允许用户为每个文件和目录分配读、写和执行权限,以确保数据的安全性和隔离性
  • 支持大容量磁盘: EXT3 支持大容量硬盘驱动器,可以管理大型文件和大容量的存储设备,类似于 EXT2
  • 文件系统检查工具: EXT3 同样配备了 fsck(文件系统检查)工具,用于检查和修复文件系统中的错误和损坏,以提高文件系统的可靠性
  • 适用于服务器和桌面系统: 由于其日志功能和可靠性,EXT3 在服务器环境中广泛使用,但也适用于桌面系统和嵌入式系统

虽然 EXT3 在其引入时带来了显著的改进,但在现代 Linux 系统中,更先进的文件系统如 EXT4 通常更受欢迎,因为它们提供了更好的性能、扩展性和一致性。EXT3 文件系统在较早的 Linux 发行版中可能仍然存在,但在大多数情况下,用户会倾向于使用 EXT4 或其他更现代的文件系统. 在 Linux 中使用 EXT3 需要打开内核宏 CONFIG_EXT3_FS.

图片无法显示,请右键点击新窗口打开图片

EXT3 文件系统提供的文件操作与 EXT4 文件系统一致,可以看到映射文件的 mmap 接口使用了 ext4_file_mmap 函数,ext4_file_mmap 函数为文件映射的 VMA 提供的 vm_ops 接口为 ext4_file_vm_ops,该数据接口实现了 fault 接口 filemap_fault,那么文件映射 VMA 发生缺页时 filemap_fault 函数会被调用.

图片无法显示,请右键点击新窗口打开图片

在 EXT3 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,EXT3 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。

图片无法显示,请右键点击新窗口打开图片

EXT3 文件系统也支持 COW 机制,进程在使用 mmap 将文件映射到进程地址空间时,使用 MAP_PRIVATE 标志即可启用 COW 机制。在 EXT3 文件系统里,当对 COW 的文件映射内存发起写操作时,MMU 检查到进程对 COW 内存没有写权限,于是触发缺页异常,缺页异常会为 VMA 新分配一个 COPY PAGE 作为副本,此时 COPY PAGE 为匿名页,接着将 VMA 对应的 PAGE CACHE 内容拷贝到 COPY PAGE 里,并将页表更新到 COPY PAGE,当缺页异常返回之后,进程对 COW 内存的读写操作都落到 COPY PAGE 里,但 COPY PAGE 的内容不会回写到磁盘文件上. 那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Virtio-BLK: EXT3 Filesystem Disk  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped EXT3 COW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-EXT3-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-FILE-EXT3-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

测试用例由一个用户程序构成,程序首先在 23 行调用 open 函数打开 EXT3 文件系统目录下的 BiscuitOS.txt 文件,然后在 29 行调用 mmap 函数将文件映射到进程的地址空间, 此时使用了 MAP_PRIVATE 标志,接下来在 40 行对文件映射的虚拟内存进行读操作,此时会发送 FAULT AROUND 将附近的虚拟内存页表一共建立,接下来在 45 行对虚拟内存进行写操作,此时会触发 COW,并在 45 行打印读到的数据,最后就是释放内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作和 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 EXT3 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_EXT3_FS):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-EXT3-COW-default/
# 编译内核
make kernel
# 编译实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “EXT3 COW on wp_page_copy 0x6000000000”, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行了读操作,那么会触发相应的缺页异常,缺页异常处理函数会分配 PAGECACHE,并从磁盘加载文件内容到 PAGECACHE,最后建立页表映射到 PAGECACHE。在这种场景下,进程对该内存执行写操作,MMU 检查到没有写权限于是触发缺页异常,此时缺页异常处理函数的处理逻辑如上,由于此时页表已经存在,只是因为写保护触发的缺页,那么 do_wp_page 函数进行逻辑处理,由于此时 PAGECACHE 不是匿名内存,那么调用 wp_page_copy 函数,该函数首先分配一个匿名页,然后调用 __wp_page_copy_user 函数将 PAGECACHE 内容拷贝到匿名页,此时匿名页就是 PAGECACHE 的一个副本,缺页异常将页表映射到匿名页上,那么后续进程的读写操作都在该副本匿名页上.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行写操作,那么会触发上图逻辑,由于页表不存在,那么缺页异常处理函数会调用 do_cow_fault 函数,该函数首先分配一个匿名页,然后调用 __do_fault 函数分配 PAGECACHE,并将磁盘文件内容加载到 PAGECACHE 里,接着调用 copy_user_highpage 将 PAGECACHE 内容拷贝到匿名内存里,此时匿名内存就是 PAGECACHE 的副本,缺页异常将页表映射到匿名内存上,那么接下来进程对 COW 内存的读写都落到匿名内存上.

图片无法显示,请右键点击新窗口打开图片

do_cow_fault 函数的逻辑如上,EXT3 文件系统没有特除的处理逻辑,此处可以看到 COW 过后,虽然虚拟内存是文件映射,但虚拟内存映射的匿名内存,因此如果进程再发生 fork 动作,那么就要按匿名内存 COW 场景进行处理,此时该虚拟内存还是会被设置为写保护。

图片无法显示,请右键点击新窗口打开图片


EXT4 文件系统 COW 缺页场景

EXT4(Fourth Extended File System) 是 Linux 操作系统中广泛使用的文件系统类型,它是 EXT3 文件系统的升级版本,为文件系统带来了更多的性能和功能改进。EXT4 是Linux 上使用最广泛的文件系统之一,它在许多 Linux 发行版中是默认的文件系统选择。以下是 EXT4 文件系统的一些主要特点和概述:

  • 高性能: EXT4 相对于 EXT3 在性能方面有显著的改进。它支持更大的文件系统和更大的文件,同时提供更快的文件读写操作
  • 扩展性: EXT4支持更大的文件系统和文件大小,使其成为处理大容量存储设备的理想选择。它支持最大 16TB 的单个文件和 1EB(exabyte) 的文件系统大小
  • 日志功能: EXT4 继承了 EXT3 的日志功能,可以记录文件系统的更改操作,以提供更好的数据一致性和可靠性
  • 快速恢复: EXT4 具有更快的文件系统检查和修复速度,减少了文件系统检查所需的时间,从而减少了系统维护的停机时间
  • 延迟分配: EXT4 采用了一种称为延迟分配(delayed allocation) 的技术,可以提高文件写入性能,避免了不必要的磁盘 I/O 操作
  • 稀疏文件支持: EXT4 支持稀疏文件,这些文件包含大量零字节数据的区域,而不会占用实际的磁盘空间
  • 适用于固态硬盘(SSD): EXT4 针对固态硬盘的特性进行了优化,包括 TRIM 支持,以提高 SSD 的性能和寿命
  • 适用于大型服务器和桌面系统: EXT4 适用于各种场景,包括大型服务器和桌面系统,它提供了高性能和可靠性

EXT4 文件系统已成为 Linux 生态系统中的标准文件系统之一,它提供了很多改进,使其适用于现代计算环境中的各种用途。大多数 Linux 发行版都支持 EXT4,并且通常将其作为默认文件系统。

图片无法显示,请右键点击新窗口打开图片

EXT4 文件系统提供的文件操作与 EXT4 文件系统一致,可以看到映射文件的 mmap 接口使用了 ext4_file_mmap 函数,ext4_file_mmap 函数为文件映射的 VMA 提供的 vm_ops 接口为 ext4_file_vm_ops,该数据接口实现了 fault 接口 filemap_fault,那么文件映射 VMA 发生缺页时 filemap_fault 函数会被调用.

图片无法显示,请右键点击新窗口打开图片

在 EXT4 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,EXT4 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。

图片无法显示,请右键点击新窗口打开图片

EXT4 文件系统也支持 COW 机制,进程在使用 mmap 将文件映射到进程地址空间时,使用 MAP_PRIVATE 标志即可启用 COW 机制。在 EXT4 文件系统里,当对 COW 的文件映射内存发起写操作时,MMU 检查到进程对 COW 内存没有写权限,于是触发缺页异常,缺页异常会为 VMA 新分配一个 COPY PAGE 作为副本,此时 COPY PAGE 为匿名页,接着将 VMA 对应的 PAGE CACHE 内容拷贝到 COPY PAGE 里,并将页表更新到 COPY PAGE,当缺页异常返回之后,进程对 COW 内存的读写操作都落到 COPY PAGE 里,但 COPY PAGE 的内容不会回写到磁盘文件上. 那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Virtio-BLK: EXT4 Filesystem Disk  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped EXT4 COW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-EXT4-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-FILE-EXT4-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

测试用例由一个用户程序构成,程序首先在 23 行调用 open 函数打开 EXT4 文件系统目录下的 BiscuitOS.txt 文件,然后在 29 行调用 mmap 函数将文件映射到进程的地址空间, 此时使用了 MAP_PRIVATE 标志,接下来在 40 行对文件映射的虚拟内存进行读操作,此时会发送 FAULT AROUND 将附近的虚拟内存页表一共建立,接下来在 45 行对虚拟内存进行写操作,此时会触发 COW,并在 45 行打印读到的数据,最后就是释放内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作和 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 EXT4 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_EXT4_FS):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-EXT4-COW-default/
# 编译内核
make kernel
# 编译实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “EXT4 COW on wp_page_copy 0x6000000000”, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行了读操作,那么会触发相应的缺页异常,缺页异常处理函数会分配 PAGECACHE,并从磁盘加载文件内容到 PAGECACHE,最后建立页表映射到 PAGECACHE。在这种场景下,进程对该内存执行写操作,MMU 检查到没有写权限于是触发缺页异常,此时缺页异常处理函数的处理逻辑如上,由于此时页表已经存在,只是因为写保护触发的缺页,那么 do_wp_page 函数进行逻辑处理,由于此时 PAGECACHE 不是匿名内存,那么调用 wp_page_copy 函数,该函数首先分配一个匿名页,然后调用 __wp_page_copy_user 函数将 PAGECACHE 内容拷贝到匿名页,此时匿名页就是 PAGECACHE 的一个副本,缺页异常将页表映射到匿名页上,那么后续进程的读写操作都在该副本匿名页上.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行写操作,那么会触发上图逻辑,由于页表不存在,那么缺页异常处理函数会调用 do_cow_fault 函数,该函数首先分配一个匿名页,然后调用 __do_fault 函数分配 PAGECACHE,并将磁盘文件内容加载到 PAGECACHE 里,接着调用 copy_user_highpage 将 PAGECACHE 内容拷贝到匿名内存里,此时匿名内存就是 PAGECACHE 的副本,缺页异常将页表映射到匿名内存上,那么接下来进程对 COW 内存的读写都落到匿名内存上.

图片无法显示,请右键点击新窗口打开图片

do_cow_fault 函数的逻辑如上,EXT4 文件系统没有特除的处理逻辑,此处可以看到 COW 过后,虽然虚拟内存是文件映射,但虚拟内存映射的匿名内存,因此如果进程再发生 fork 动作,那么就要按匿名内存 COW 场景进行处理,此时该虚拟内存还是会被设置为写保护。

图片无法显示,请右键点击新窗口打开图片


VFAT 文件系统 COW 缺页场景

VFAT(Virtual File Allocation Table) 是一种文件系统,主要用于存储在磁盘上的文件和目录的管理。VFAT 文件系统最初是为 DOS 和 Windows 操作系统开发的,但它也在 Linux 和其他操作系统上得到了广泛的支持,用于与 FAT12、FAT16 和 FAT32 格式的存储设备交互。以下是 VFAT 文件系统的一些主要特点和概述:

  • 兼容性: VFAT 文件系统是一种 FAT(File Allocation Table)文件系统的扩展,它与早期的 FAT 文件系统(如 FAT12 和 FAT16) 和较新的 FAT32 格式兼容。这使得它成为与 Windows 和其他操作系统兼容的通用文件系统
  • 长文件名支持: VFAT 引入了对长文件名(Long File Names,LFN) 的支持,允许文件名包含多达 255 个字符,并支持 Unicode 字符。这改善了文件和目录命名的灵活性,使其适用于多语言环境
  • 8.3 文件名兼容: 尽管 VFAT 支持长文件名,但它也保留了对传统的 8.3 文件名格式(8 个字符的文件名 +3 个字符的文件扩展名)的兼容性。这允许 VFAT 文件系统与不支持长文件名的老式操作系统和应用程序兼容
  • 跨平台兼容性: VFAT 文件系统通常用于可移动存储介质,如 USB 闪存驱动器和 SD 卡。由于其广泛的兼容性,这些存储设备可以轻松地在不同的操作系统之间共享文件
  • 文件和目录权限: VFAT 文件系统在权限管理方面较简单,通常使用简单的读写执行标志来控制文件和目录的访问。它不支持像 Linux 上的更复杂的权限和 ACL(Access Control List) 系统
  • 可用性: VFAT 文件系统通常在 Linux、Windows、macOS 和其他操作系统中内置支持,因此可以方便地访问和使用

尽管 VFAT 文件系统在交互性和兼容性方面具有一定优势,但它也有一些限制,如不支持 Unix 样式的文件权限、不支持链接等。因此,对于在 Linux 系统上进行高级文件操作或需要更复杂的权限控制的用例,可能需要使用其他文件系统. 在 Linux 中使用 VFAT 需要打开内核宏 CONFIG_FAT_FS.

图片无法显示,请右键点击新窗口打开图片

VFAT 文件系统提供的文件操作与 VFAT 文件系统一致,可以看到映射文件的 mmap 接口使用了 generic_file_mmap 函数,该函数为文件映射的 VMA 提供的 vm_ops 接口为 generic_file_vm_ops,该数据接口实现了 fault 接口 filemap_fault,那么文件映射 VMA 发生缺页时 filemap_fault 函数会被调用.

图片无法显示,请右键点击新窗口打开图片

在 VFAT 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,VFAT 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。

图片无法显示,请右键点击新窗口打开图片

VFAT 文件系统也支持 COW 机制,进程在使用 mmap 将文件映射到进程地址空间时,使用 MAP_PRIVATE 标志即可启用 COW 机制。在 VFAT 文件系统里,当对 COW 的文件映射内存发起写操作时,MMU 检查到进程对 COW 内存没有写权限,于是触发缺页异常,缺页异常会为 VMA 新分配一个 COPY PAGE 作为副本,此时 COPY PAGE 为匿名页,接着将 VMA 对应的 PAGE CACHE 内容拷贝到 COPY PAGE 里,并将页表更新到 COPY PAGE,当缺页异常返回之后,进程对 COW 内存的读写操作都落到 COPY PAGE 里,但 COPY PAGE 的内容不会回写到磁盘文件上. 那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Virtio-BLK: VFAT Filesystem Disk  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped VFAT COW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-VFAT-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-FILE-VFAT-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

测试用例由一个用户程序构成,程序首先在 23 行调用 open 函数打开 VFAT 文件系统目录下的 BiscuitOS.txt 文件,然后在 29 行调用 mmap 函数将文件映射到进程的地址空间, 此时使用了 MAP_PRIVATE 标志,接下来在 40 行对文件映射的虚拟内存进行读操作,此时会发送 FAULT AROUND 将附近的虚拟内存页表一共建立,接下来在 45 行对虚拟内存进行写操作,此时会触发 COW,并在 45 行打印读到的数据,最后就是释放内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作和 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 VFAT 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_VFAT_FS):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-VFAT-COW-default/
# 编译内核
make kernel
# 编译实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “VFAT COW on wp_page_copy 0x6000000000”, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行了读操作,那么会触发相应的缺页异常,缺页异常处理函数会分配 PAGECACHE,并从磁盘加载文件内容到 PAGECACHE,最后建立页表映射到 PAGECACHE。在这种场景下,进程对该内存执行写操作,MMU 检查到没有写权限于是触发缺页异常,此时缺页异常处理函数的处理逻辑如上,由于此时页表已经存在,只是因为写保护触发的缺页,那么 do_wp_page 函数进行逻辑处理,由于此时 PAGECACHE 不是匿名内存,那么调用 wp_page_copy 函数,该函数首先分配一个匿名页,然后调用 __wp_page_copy_user 函数将 PAGECACHE 内容拷贝到匿名页,此时匿名页就是 PAGECACHE 的一个副本,缺页异常将页表映射到匿名页上,那么后续进程的读写操作都在该副本匿名页上.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行写操作,那么会触发上图逻辑,由于页表不存在,那么缺页异常处理函数会调用 do_cow_fault 函数,该函数首先分配一个匿名页,然后调用 __do_fault 函数分配 PAGECACHE,并将磁盘文件内容加载到 PAGECACHE 里,接着调用 copy_user_highpage 将 PAGECACHE 内容拷贝到匿名内存里,此时匿名内存就是 PAGECACHE 的副本,缺页异常将页表映射到匿名内存上,那么接下来进程对 COW 内存的读写都落到匿名内存上.

图片无法显示,请右键点击新窗口打开图片

do_cow_fault 函数的逻辑如上,VFAT 文件系统没有特除的处理逻辑,此处可以看到 COW 过后,虽然虚拟内存是文件映射,但虚拟内存映射的匿名内存,因此如果进程再发生 fork 动作,那么就要按匿名内存 COW 场景进行处理,此时该虚拟内存还是会被设置为写保护。

图片无法显示,请右键点击新窗口打开图片


MSDOS 文件系统 COW 缺页场景

MS-DOS 文件系统通常简称为 FAT(File Allocation Table)文件系统,是一种最早用于 PC 和 MS-DOS 操作系统的文件系统格式。FAT 文件系统最初由 Microsoft 开发,旨在用于早期的个人计算机,如 IBM PC 和兼容机。它后来演化成了 FAT12、FAT16 和 FAT32 等不同版本,每个版本都具有不同的功能和限制。以下是 MS-DOS 文件系统(FAT文件系统) 的一些主要特点和概述:

  • 简单性: FAT 文件系统设计非常简单,易于实现和理解。它使用一种称为文件分配表(File Allocation Table)的数据结构来跟踪文件和目录的存储位置
  • 兼容性: FAT 文件系统具有很好的跨平台兼容性,可以在多个操作系统中访问和使用。它广泛用于可移动存储介质,如 USB 闪存驱动器和 SD 卡
  • 8.3 文件名格式: FAT 文件系统最初使用的是 8.3 文件名格式,即文件名最多由 8 个字符组成,后面跟着一个点和最多 3 个字符的文件扩展名。这种格式在早期个人计算机上非常常见
  • 文件和目录权限: FAT 文件系统的权限管理相对简单,通常使用简单的读写执行标志来控制文件和目录的访问。它不支持像 Linux 上的更复杂的权限和 ACL(Access Control List) 系统
  • 不支持日志功能: 与一些现代文件系统(如 EXT4)不同,FAT 文件系统不提供原生的日志功能,这意味着在意外断电或系统崩溃时,可能需要较长时间来恢复文件系统,并且有一定的数据损坏风险
  • 不支持链接: FAT文件系统不支持硬链接或符号链接,这在某些情况下可能限制了文件的组织和管理方式
  • 限制: 不同版本的FAT文件系统存在一些存储容量和文件大小的限制。例如,FAT12 支持的最大分区大小和文件大小较小,而FAT32允许更大的存储容量和文件大小

MSDOS 文件系统仍然在某些情况下广泛使用,特别是在可移动存储设备上。但在现代操作系统中,如 Windows、Linux 和 macOS,更复杂和功能丰富的文件系统,如 NTFS、EXT4 和 HFS+ 等,通常用于处理更大容量的磁盘和提供更多高级功能. 在 Linux 中使用 MSDOS 需要打开内核宏 CONFIG_FAT_FS.

图片无法显示,请右键点击新窗口打开图片

MSDOS 文件系统提供的文件操作与 VFAT 文件系统一致,可以看到映射文件的 mmap 接口使用了 generic_file_mmap 函数,该函数为文件映射的 VMA 提供的 vm_ops 接口为 generic_file_vm_ops,该数据接口实现了 fault 接口 filemap_fault,那么文件映射 VMA 发生缺页时 filemap_fault 函数会被调用.

图片无法显示,请右键点击新窗口打开图片

在 MSDOS 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,MSDOS 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。

图片无法显示,请右键点击新窗口打开图片

MSDOS 文件系统也支持 COW 机制,进程在使用 mmap 将文件映射到进程地址空间时,使用 MAP_PRIVATE 标志即可启用 COW 机制。在 MSDOS 文件系统里,当对 COW 的文件映射内存发起写操作时,MMU 检查到进程对 COW 内存没有写权限,于是触发缺页异常,缺页异常会为 VMA 新分配一个 COPY PAGE 作为副本,此时 COPY PAGE 为匿名页,接着将 VMA 对应的 PAGE CACHE 内容拷贝到 COPY PAGE 里,并将页表更新到 COPY PAGE,当缺页异常返回之后,进程对 COW 内存的读写操作都落到 COPY PAGE 里,但 COPY PAGE 的内容不会回写到磁盘文件上. 那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Virtio-BLK: MSDOS Filesystem Disk  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped MSDOS COW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-MSDOS-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-FILE-MSDOS-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

测试用例由一个用户程序构成,程序首先在 23 行调用 open 函数打开 MSDOS 文件系统目录下的 BiscuitOS.txt 文件,然后在 29 行调用 mmap 函数将文件映射到进程的地址空间, 此时使用了 MAP_PRIVATE 标志,接下来在 40 行对文件映射的虚拟内存进行读操作,此时会发送 FAULT AROUND 将附近的虚拟内存页表一共建立,接下来在 45 行对虚拟内存进行写操作,此时会触发 COW,并在 45 行打印读到的数据,最后就是释放内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作和 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 MSDOS 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_MSDOS_FS):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-MSDOS-COW-default/
# 编译内核
make kernel
# 编译实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “MSDOS COW on wp_page_copy 0x6000000000”, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行了读操作,那么会触发相应的缺页异常,缺页异常处理函数会分配 PAGECACHE,并从磁盘加载文件内容到 PAGECACHE,最后建立页表映射到 PAGECACHE。在这种场景下,进程对该内存执行写操作,MMU 检查到没有写权限于是触发缺页异常,此时缺页异常处理函数的处理逻辑如上,由于此时页表已经存在,只是因为写保护触发的缺页,那么 do_wp_page 函数进行逻辑处理,由于此时 PAGECACHE 不是匿名内存,那么调用 wp_page_copy 函数,该函数首先分配一个匿名页,然后调用 __wp_page_copy_user 函数将 PAGECACHE 内容拷贝到匿名页,此时匿名页就是 PAGECACHE 的一个副本,缺页异常将页表映射到匿名页上,那么后续进程的读写操作都在该副本匿名页上.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行写操作,那么会触发上图逻辑,由于页表不存在,那么缺页异常处理函数会调用 do_cow_fault 函数,该函数首先分配一个匿名页,然后调用 __do_fault 函数分配 PAGECACHE,并将磁盘文件内容加载到 PAGECACHE 里,接着调用 copy_user_highpage 将 PAGECACHE 内容拷贝到匿名内存里,此时匿名内存就是 PAGECACHE 的副本,缺页异常将页表映射到匿名内存上,那么接下来进程对 COW 内存的读写都落到匿名内存上.

图片无法显示,请右键点击新窗口打开图片

do_cow_fault 函数的逻辑如上,MSDOS 文件系统没有特除的处理逻辑,此处可以看到 COW 过后,虽然虚拟内存是文件映射,但虚拟内存映射的匿名内存,因此如果进程再发生 fork 动作,那么就要按匿名内存 COW 场景进行处理,此时该虚拟内存还是会被设置为写保护。

图片无法显示,请右键点击新窗口打开图片


FAT 文件系统 COW 缺页场景

FAT(File Allocation Table) 文件系统是一种简单而古老的文件系统格式,最初由 Microsoft 开发,广泛用于早期的个人计算机和操作系统,如 MS-DOS 和早期版本的 Windows。FAT 文件系统在 Linux 系统中也得到了支持,用于与 FAT 格式的存储设备进行交互。以下是 FAT 文件系统的一些主要特点和概述:

  • 简单性: FAT 文件系统设计非常简单,易于实现和理解。它使用一种称为文件分配表(File Allocation Table) 的数据结构来跟踪文件和目录的存储位置
  • 兼容性: FAT 文件系统具有很好的跨平台兼容性,可以在多个操作系统中访问和使用。这使得它成为可移动存储介质(如 USB 闪存驱动器和 SD 卡)的常见文件系统格式
  • 文件名格式: FAT 文件系统最初使用的是 8.3 文件名格式,即文件名最多由 8 个字符组成,后面跟着一个点和最多 3 个字符的文件扩展名。这种格式在早期个人计算机上非常常见
  • 文件和目录权限: FAT 文件系统的权限管理相对简单,通常使用简单的读写执行标志来控制文件和目录的访问。它不支持像 Linux 上的更复杂的权限和 ACL(Access Control List) 系统
  • 不支持日志功能: 与一些现代文件系统(如 EXT4)不同,FAT 文件系统不提供原生的日志功能,这意味着在意外断电或系统崩溃时,可能需要较长时间来恢复文件系统,并且有一定的数据损坏风险
  • 不支持链接: FAT 文件系统不支持硬链接或符号链接,这在某些情况下可能限制了文件的组织和管理方式
  • 存储容量限制: 不同版本的FAT文件系统存在一些存储容量和文件大小的限制。例如,FAT12 支持的最大分区大小和文件大小较小,而 FAT32 允许更大的存储容量和文件大小

FAT 文件系统仍然在某些情况下广泛使用,特别是在可移动存储设备上,因为它的兼容性和简单性。然而,在现代操作系统中,如 Windows、Linux 和 macOS,更复杂和功能丰富的文件系统,如 NTFS、EXT4 和 HFS+ 等,通常用于处理更大容量的磁盘和提供更多高级功能. 在 Linux 中使用 FAT 需要打开内核宏 CONFIG_FAT_FS.

图片无法显示,请右键点击新窗口打开图片

FAT 文件系统提供映射文件的 mmap 接口使用了 generic_file_mmap 函数,该函数为文件映射的 VMA 提供的 vm_ops 接口为 generic_file_vm_ops,该数据接口实现了 fault 接口 filemap_fault,那么文件映射 VMA 发生缺页时 filemap_fault 函数会被调用.

图片无法显示,请右键点击新窗口打开图片

在 FAT 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,FAT 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。

图片无法显示,请右键点击新窗口打开图片

FAT 文件系统也支持 COW 机制,进程在使用 mmap 将文件映射到进程地址空间时,使用 MAP_PRIVATE 标志即可启用 COW 机制。在 FAT 文件系统里,当对 COW 的文件映射内存发起写操作时,MMU 检查到进程对 COW 内存没有写权限,于是触发缺页异常,缺页异常会为 VMA 新分配一个 COPY PAGE 作为副本,此时 COPY PAGE 为匿名页,接着将 VMA 对应的 PAGE CACHE 内容拷贝到 COPY PAGE 里,并将页表更新到 COPY PAGE,当缺页异常返回之后,进程对 COW 内存的读写操作都落到 COPY PAGE 里,但 COPY PAGE 的内容不会回写到磁盘文件上. 那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Virtio-BLK: FAT Filesystem Disk  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped FAT COW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-FAT-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-FILE-FAT-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

测试用例由一个用户程序构成,程序首先在 23 行调用 open 函数打开 FAT 文件系统目录下的 BiscuitOS.txt 文件,然后在 29 行调用 mmap 函数将文件映射到进程的地址空间, 此时使用了 MAP_PRIVATE 标志,接下来在 40 行对文件映射的虚拟内存进行读操作,此时会发送 FAULT AROUND 将附近的虚拟内存页表一共建立,接下来在 45 行对虚拟内存进行写操作,此时会触发 COW,并在 45 行打印读到的数据,最后就是释放内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作和 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 FAT 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_FAT_FS):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-FAT-COW-default/
# 编译内核
make kernel
# 编译实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “FAT COW on wp_page_copy 0x6000000000”, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行了读操作,那么会触发相应的缺页异常,缺页异常处理函数会分配 PAGECACHE,并从磁盘加载文件内容到 PAGECACHE,最后建立页表映射到 PAGECACHE。在这种场景下,进程对该内存执行写操作,MMU 检查到没有写权限于是触发缺页异常,此时缺页异常处理函数的处理逻辑如上,由于此时页表已经存在,只是因为写保护触发的缺页,那么 do_wp_page 函数进行逻辑处理,由于此时 PAGECACHE 不是匿名内存,那么调用 wp_page_copy 函数,该函数首先分配一个匿名页,然后调用 __wp_page_copy_user 函数将 PAGECACHE 内容拷贝到匿名页,此时匿名页就是 PAGECACHE 的一个副本,缺页异常将页表映射到匿名页上,那么后续进程的读写操作都在该副本匿名页上.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行写操作,那么会触发上图逻辑,由于页表不存在,那么缺页异常处理函数会调用 do_cow_fault 函数,该函数首先分配一个匿名页,然后调用 __do_fault 函数分配 PAGECACHE,并将磁盘文件内容加载到 PAGECACHE 里,接着调用 copy_user_highpage 将 PAGECACHE 内容拷贝到匿名内存里,此时匿名内存就是 PAGECACHE 的副本,缺页异常将页表映射到匿名内存上,那么接下来进程对 COW 内存的读写都落到匿名内存上.

图片无法显示,请右键点击新窗口打开图片

do_cow_fault 函数的逻辑如上,FAT 文件系统没有特除的处理逻辑,此处可以看到 COW 过后,虽然虚拟内存是文件映射,但虚拟内存映射的匿名内存,因此如果进程再发生 fork 动作,那么就要按匿名内存 COW 场景进行处理,此时该虚拟内存还是会被设置为写保护。

图片无法显示,请右键点击新窗口打开图片


BFS 文件系统 COW 缺页场景

BFS 文件系统通常由 SCO UnixWare 操作系统用于 /stand 分区,该分区通常包含内核镜像和引导过程所需的几个其他文件。要在 Linux 下访问 /stand 分区,需要知道分区号,并且内核必须支持 UnixWare 磁盘切片(CONFIG_UNIXWARE_DISKLABEL 配置选项). 但 BFS 支持并不依赖于具备 UnixWare 磁盘标签支持,因为也可以通过回环设备挂载 BFS 文件系统. 在 Linux 中使用 BFS 需要打开内核宏 CONFIG_BFS_FS.

图片无法显示,请右键点击新窗口打开图片

BFS 文件系统映射文件的 mmap 接口使用了 generic_file_mmap 函数,generic_file_mmap 函数为文件映射的 VMA 提供的 vm_ops 接口为 generic_file_vm_ops,该数据接口实现了 fault 接口 filemap_fault,那么文件映射 VMA 发生缺页时 filemap_fault 函数会被调用.

图片无法显示,请右键点击新窗口打开图片

在 BFS 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,BFS 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。

图片无法显示,请右键点击新窗口打开图片

BFS 文件系统也支持 COW 机制,进程在使用 mmap 将文件映射到进程地址空间时,使用 MAP_PRIVATE 标志即可启用 COW 机制。在 BFS 文件系统里,当对 COW 的文件映射内存发起写操作时,MMU 检查到进程对 COW 内存没有写权限,于是触发缺页异常,缺页异常会为 VMA 新分配一个 COPY PAGE 作为副本,此时 COPY PAGE 为匿名页,接着将 VMA 对应的 PAGE CACHE 内容拷贝到 COPY PAGE 里,并将页表更新到 COPY PAGE,当缺页异常返回之后,进程对 COW 内存的读写操作都落到 COPY PAGE 里,但 COPY PAGE 的内容不会回写到磁盘文件上. 那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Virtio-BLK: BFS Filesystem Disk  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped BFS COW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-BFS-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-FILE-BFS-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

测试用例由一个用户程序构成,程序首先在 23 行调用 open 函数打开 BFS 文件系统目录下的 BiscuitOS.txt 文件,然后在 29 行调用 mmap 函数将文件映射到进程的地址空间, 此时使用了 MAP_PRIVATE 标志,接下来在 40 行对文件映射的虚拟内存进行读操作,此时会发送 FAULT AROUND 将附近的虚拟内存页表一共建立,接下来在 45 行对虚拟内存进行写操作,此时会触发 COW,并在 45 行打印读到的数据,最后就是释放内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作和 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 BFS 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_BFS_FS):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-BFS-COW-default/
# 编译内核
make kernel
# 编译实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “BFS COW on wp_page_copy 0x6000000000”, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行了读操作,那么会触发相应的缺页异常,缺页异常处理函数会分配 PAGECACHE,并从磁盘加载文件内容到 PAGECACHE,最后建立页表映射到 PAGECACHE。在这种场景下,进程对该内存执行写操作,MMU 检查到没有写权限于是触发缺页异常,此时缺页异常处理函数的处理逻辑如上,由于此时页表已经存在,只是因为写保护触发的缺页,那么 do_wp_page 函数进行逻辑处理,由于此时 PAGECACHE 不是匿名内存,那么调用 wp_page_copy 函数,该函数首先分配一个匿名页,然后调用 __wp_page_copy_user 函数将 PAGECACHE 内容拷贝到匿名页,此时匿名页就是 PAGECACHE 的一个副本,缺页异常将页表映射到匿名页上,那么后续进程的读写操作都在该副本匿名页上.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行写操作,那么会触发上图逻辑,由于页表不存在,那么缺页异常处理函数会调用 do_cow_fault 函数,该函数首先分配一个匿名页,然后调用 __do_fault 函数分配 PAGECACHE,并将磁盘文件内容加载到 PAGECACHE 里,接着调用 copy_user_highpage 将 PAGECACHE 内容拷贝到匿名内存里,此时匿名内存就是 PAGECACHE 的副本,缺页异常将页表映射到匿名内存上,那么接下来进程对 COW 内存的读写都落到匿名内存上.

图片无法显示,请右键点击新窗口打开图片

do_cow_fault 函数的逻辑如上,BFS 文件系统没有特除的处理逻辑,此处可以看到 COW 过后,虽然虚拟内存是文件映射,但虚拟内存映射的匿名内存,因此如果进程再发生 fork 动作,那么就要按匿名内存 COW 场景进行处理,此时该虚拟内存还是会被设置为写保护。

图片无法显示,请右键点击新窗口打开图片


BTRFS 文件系统 COW 缺页场景

BTRFS(B-tree File System) 是一个现代的、复杂的文件系统,最初由 Oracle 开发,现在是 Linux 内核的一部分,并由社区维护。Btrfs 的目标是提供高级的文件系统管理功能,同时具备高性能、数据完整性和数据保护。以下是 Btrfs 文件系统的一些主要特点和概述:

  • 复杂的数据管理: Btrfs 支持许多高级的数据管理功能,包括快照(Snapshots)、校验和(Checksums)、数据压缩、RAID(Redundant Array of Independent Disks) 支持和数据迁移等。这些功能使 Btrfs 成为数据存储和管理的强大工具

  • 快照: Btrfs 可以轻松创建快照,这是文件系统状态的副本,用于备份和版本控制。快照可以在不复制实际数据的情况下保存文件系统的状态
  • 校验和 Btrfs 使用校验和来检测数据损坏或位翻转错误。这有助于提高数据完整性,并防止数据损坏
  • 数据压缩: Btrfs 支持数据压缩,可以减小文件系统的存储空间占用。压缩可以在写入数据时自动进行,也可以手动触发
  • RAID支持: Btrfs 支持软件 RAID,允许将多个硬盘组合成一个冗余或条带化的存储池,以提高数据冗余和性能
  • 在线文件系统扩展和收缩: Btrfs 允许在线扩展和收缩文件系统,而无需卸载或重建文件系统。这有助于管理文件系统的大小和容量
  • 文件系统检查和修复: Btrfs 可以在运行时检测文件系统错误,并尝试自动修复它们。这有助于提高文件系统的稳定性
  • 社区支持: Btrfs 是一个开源项目,得到了 Linux 社区的广泛支持和发展。它在许多 Linux 发行版中提供了作为根文件系统或数据存储的选项

Btrfs 的灵活性和强大功能使其成为许多用例的理想选择,包括服务器存储、嵌入式设备和桌面系统。然而 Btrfs 仍然在不断发展,有一些高级功能可能需要小心配置和管理,以确保最佳性能和数据保护。在选择文件系统时,您应该根据具体的需求和用例来决定是否使用 Btrfs, 在 Linux 中使用 BTRFS 需要打开内核宏 CONFIG_BTRFS_FS.

图片无法显示,请右键点击新窗口打开图片

BTRFS 文件系统提供映射文件的 mmap 接口使用了 btrfs_file_mmap 函数,btrfs_file_mmap 函数为文件映射的 VMA 提供的 vm_ops 接口为 btrfs_file_vm_ops,该数据接口实现了 fault 接口 filemap_fault,那么文件映射 VMA 发生缺页时 filemap_fault 函数会被调用.

图片无法显示,请右键点击新窗口打开图片

在 BTRFS 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,BTRFS 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。

图片无法显示,请右键点击新窗口打开图片

BTRFS 文件系统也支持 COW 机制,进程在使用 mmap 将文件映射到进程地址空间时,使用 MAP_PRIVATE 标志即可启用 COW 机制。在 BTRFS 文件系统里,当对 COW 的文件映射内存发起写操作时,MMU 检查到进程对 COW 内存没有写权限,于是触发缺页异常,缺页异常会为 VMA 新分配一个 COPY PAGE 作为副本,此时 COPY PAGE 为匿名页,接着将 VMA 对应的 PAGE CACHE 内容拷贝到 COPY PAGE 里,并将页表更新到 COPY PAGE,当缺页异常返回之后,进程对 COW 内存的读写操作都落到 COPY PAGE 里,但 COPY PAGE 的内容不会回写到磁盘文件上. 那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Virtio-BLK: BTRFS Filesystem Disk  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped BTRFS COW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-BTRFS-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-FILE-BTRFS-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

测试用例由一个用户程序构成,程序首先在 23 行调用 open 函数打开 BTRFS 文件系统目录下的 BiscuitOS.txt 文件,然后在 29 行调用 mmap 函数将文件映射到进程的地址空间, 此时使用了 MAP_PRIVATE 标志,接下来在 40 行对文件映射的虚拟内存进行读操作,此时会发送 FAULT AROUND 将附近的虚拟内存页表一共建立,接下来在 45 行对虚拟内存进行写操作,此时会触发 COW,并在 45 行打印读到的数据,最后就是释放内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作和 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 BTRFS 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_BTRFS_FS):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-BTRFS-COW-default/
# 编译内核
make kernel
# 编译实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “BTRFS COW on wp_page_copy 0x6000000000”, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行了读操作,那么会触发相应的缺页异常,缺页异常处理函数会分配 PAGECACHE,并从磁盘加载文件内容到 PAGECACHE,最后建立页表映射到 PAGECACHE。在这种场景下,进程对该内存执行写操作,MMU 检查到没有写权限于是触发缺页异常,此时缺页异常处理函数的处理逻辑如上,由于此时页表已经存在,只是因为写保护触发的缺页,那么 do_wp_page 函数进行逻辑处理,由于此时 PAGECACHE 不是匿名内存,那么调用 wp_page_copy 函数,该函数首先分配一个匿名页,然后调用 __wp_page_copy_user 函数将 PAGECACHE 内容拷贝到匿名页,此时匿名页就是 PAGECACHE 的一个副本,缺页异常将页表映射到匿名页上,那么后续进程的读写操作都在该副本匿名页上.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行写操作,那么会触发上图逻辑,由于页表不存在,那么缺页异常处理函数会调用 do_cow_fault 函数,该函数首先分配一个匿名页,然后调用 __do_fault 函数分配 PAGECACHE,并将磁盘文件内容加载到 PAGECACHE 里,接着调用 copy_user_highpage 将 PAGECACHE 内容拷贝到匿名内存里,此时匿名内存就是 PAGECACHE 的副本,缺页异常将页表映射到匿名内存上,那么接下来进程对 COW 内存的读写都落到匿名内存上.

图片无法显示,请右键点击新窗口打开图片

do_cow_fault 函数的逻辑如上,BTRFS 文件系统没有特除的处理逻辑,此处可以看到 COW 过后,虽然虚拟内存是文件映射,但虚拟内存映射的匿名内存,因此如果进程再发生 fork 动作,那么就要按匿名内存 COW 场景进行处理,此时该虚拟内存还是会被设置为写保护。

图片无法显示,请右键点击新窗口打开图片


REISERFS 文件系统 COW 缺页场景

ReiserFS(Reiser File System) 是一个 Linux 文件系统,最初由 Hans Reiser 开发。它的设计旨在提供高性能、快速文件访问和对小文件的高效支持。然而由于一些历史和法律问题,ReiserFS 在 Linux 社区中已经不再广泛使用,并且在大多数 Linux 发行版中不再是默认的文件系统选择。以下是 ReiserFS 文件系统的一些主要特点和概述:

  • 高性能: ReiserFS 旨在提供高性能的文件访问速度,特别是对于小文件和元数据的访问。这使得它在某些特定用例中表现出色,例如数据库应用程序和邮件服务器
  • B-树数据结构: ReiserFS 使用 B-树(一种自平衡树结构)来组织和管理文件系统的数据和元数据。这种数据结构使得文件系统能够高效地处理大量文件和目录
  • 快速创建和删除: ReiserFS 在创建和删除文件时表现出色,特别是在目录中有大量文件的情况下
  • 适用于大文件: 尽管 ReiserFS 擅长处理小文件,但它也支持大文件,并且能够有效地处理大容量数据
  • 日志功能: ReiserFS 支持日志功能,可以在写入操作期间记录文件系统的更改。这有助于提供数据一致性和恢复能力

目前,大多数 Linux 发行版更倾向于使用其他文件系统,如 EXT4、XFS 或 Btrfs,因为它们具有更广泛的支持、更好的性能和更活跃的维护。如果您正在考虑选择文件系统,在 Linux 中使用 REISERFS 需要打开内核宏 CONFIG_REISERFS_FS.

图片无法显示,请右键点击新窗口打开图片

REISERFS 文件系统提供映射文件的 mmap 接口使用了 generic_file_mmap 函数,generic_file_mmap 函数为文件映射的 VMA 提供的 vm_ops 接口为 generic_file_vm_ops,该数据接口实现了 fault 接口 filemap_fault,那么文件映射 VMA 发生缺页时 filemap_fault 函数会被调用.

图片无法显示,请右键点击新窗口打开图片

在 REISERFS 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,REISERFS 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。

图片无法显示,请右键点击新窗口打开图片

REISERFS 文件系统也支持 COW 机制,进程在使用 mmap 将文件映射到进程地址空间时,使用 MAP_PRIVATE 标志即可启用 COW 机制。在 REISERFS 文件系统里,当对 COW 的文件映射内存发起写操作时,MMU 检查到进程对 COW 内存没有写权限,于是触发缺页异常,缺页异常会为 VMA 新分配一个 COPY PAGE 作为副本,此时 COPY PAGE 为匿名页,接着将 VMA 对应的 PAGE CACHE 内容拷贝到 COPY PAGE 里,并将页表更新到 COPY PAGE,当缺页异常返回之后,进程对 COW 内存的读写操作都落到 COPY PAGE 里,但 COPY PAGE 的内容不会回写到磁盘文件上. 那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Virtio-BLK: REISERFS Filesystem Disk  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped REISERFS COW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-REISERFS-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-FILE-REISERFS-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

测试用例由一个用户程序构成,程序首先在 23 行调用 open 函数打开 REISERFS 文件系统目录下的 BiscuitOS.txt 文件,然后在 29 行调用 mmap 函数将文件映射到进程的地址空间, 此时使用了 MAP_PRIVATE 标志,接下来在 40 行对文件映射的虚拟内存进行读操作,此时会发送 FAULT AROUND 将附近的虚拟内存页表一共建立,接下来在 45 行对虚拟内存进行写操作,此时会触发 COW,并在 45 行打印读到的数据,最后就是释放内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作和 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 REISERFS 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_REISERFS_FS):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-REISERFS-COW-default/
# 编译内核
make kernel
# 编译实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “REISERFS COW on wp_page_copy 0x6000000000”, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行了读操作,那么会触发相应的缺页异常,缺页异常处理函数会分配 PAGECACHE,并从磁盘加载文件内容到 PAGECACHE,最后建立页表映射到 PAGECACHE。在这种场景下,进程对该内存执行写操作,MMU 检查到没有写权限于是触发缺页异常,此时缺页异常处理函数的处理逻辑如上,由于此时页表已经存在,只是因为写保护触发的缺页,那么 do_wp_page 函数进行逻辑处理,由于此时 PAGECACHE 不是匿名内存,那么调用 wp_page_copy 函数,该函数首先分配一个匿名页,然后调用 __wp_page_copy_user 函数将 PAGECACHE 内容拷贝到匿名页,此时匿名页就是 PAGECACHE 的一个副本,缺页异常将页表映射到匿名页上,那么后续进程的读写操作都在该副本匿名页上.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行写操作,那么会触发上图逻辑,由于页表不存在,那么缺页异常处理函数会调用 do_cow_fault 函数,该函数首先分配一个匿名页,然后调用 __do_fault 函数分配 PAGECACHE,并将磁盘文件内容加载到 PAGECACHE 里,接着调用 copy_user_highpage 将 PAGECACHE 内容拷贝到匿名内存里,此时匿名内存就是 PAGECACHE 的副本,缺页异常将页表映射到匿名内存上,那么接下来进程对 COW 内存的读写都落到匿名内存上.

图片无法显示,请右键点击新窗口打开图片

do_cow_fault 函数的逻辑如上,REISERFS 文件系统没有特除的处理逻辑,此处可以看到 COW 过后,虽然虚拟内存是文件映射,但虚拟内存映射的匿名内存,因此如果进程再发生 fork 动作,那么就要按匿名内存 COW 场景进行处理,此时该虚拟内存还是会被设置为写保护。

图片无法显示,请右键点击新窗口打开图片


JFS 文件系统 COW 缺页场景

JFS(Journaled File System) 是 IBM 开发的一种文件系统,最初用于 IBM AIX 操作系统,后来也移植到 Linux 等其他平台上。它是一种支持日志功能的文件系统,旨在提供高性能、数据完整性和可恢复性。以下是JFS文件系统的一些主要特点和概述:

  • 日志功能: JFS 是一种日志文件系统,它通过记录文件系统更改的日志来提供额外的数据一致性和可恢复性。这有助于防止在系统崩溃或意外断电时数据损坏
  • 高性能: JFS 被设计用于提供高性能的文件访问速度,特别是在大文件和高负载情况下。它采用了一些优化措施来减少磁盘I/O并提高文件访问性能
  • 支持大容量存储: JFS 支持大容量磁盘和文件系统,使其适用于数据密集型应用程序和服务器
  • 快速恢复: 由于具有日志功能,JFS 在系统故障或不正常关闭后能够更快速地恢复文件系统到一致的状态
  • 多平台支持: JFS 最初是为 IBM AIX 开发的,但后来被移植到 Linux 和其他操作系统上,因此它在多个平台上可用

JFS 曾经在 Linux 社区中很受欢迎,但随着时间的推移,其他文件系统如 EXT4、XFS 和 Btrfs 等也得到了广泛采用,并提供了更多的功能和性能优势。因此在选择文件系统时,您可能会更倾向于使用这些较新的文件系统。然而,JFS 仍然可以在某些特定用例中提供良好的性能和可靠性,因此它仍然在一些系统中使用, 在 Linux 中使用 JFS 需要打开内核宏 CONFIG_JFS_FS.

图片无法显示,请右键点击新窗口打开图片

JFS 文件系统提供映射文件的 mmap 接口使用了 generic_file_mmap 函数,generic_file_mmap 函数为文件映射的 VMA 提供的 vm_ops 接口为 generic_file_vm_ops,该数据接口实现了 fault 接口 filemap_fault,那么文件映射 VMA 发生缺页时 filemap_fault 函数会被调用.

图片无法显示,请右键点击新窗口打开图片

在 JFS 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,JFS 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。

图片无法显示,请右键点击新窗口打开图片

JFS 文件系统也支持 COW 机制,进程在使用 mmap 将文件映射到进程地址空间时,使用 MAP_PRIVATE 标志即可启用 COW 机制。在 JFS 文件系统里,当对 COW 的文件映射内存发起写操作时,MMU 检查到进程对 COW 内存没有写权限,于是触发缺页异常,缺页异常会为 VMA 新分配一个 COPY PAGE 作为副本,此时 COPY PAGE 为匿名页,接着将 VMA 对应的 PAGE CACHE 内容拷贝到 COPY PAGE 里,并将页表更新到 COPY PAGE,当缺页异常返回之后,进程对 COW 内存的读写操作都落到 COPY PAGE 里,但 COPY PAGE 的内容不会回写到磁盘文件上. 那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Virtio-BLK: JFS Filesystem Disk  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped JFS COW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-JFS-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-FILE-JFS-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

测试用例由一个用户程序构成,程序首先在 23 行调用 open 函数打开 JFS 文件系统目录下的 BiscuitOS.txt 文件,然后在 29 行调用 mmap 函数将文件映射到进程的地址空间, 此时使用了 MAP_PRIVATE 标志,接下来在 40 行对文件映射的虚拟内存进行读操作,此时会发送 FAULT AROUND 将附近的虚拟内存页表一共建立,接下来在 45 行对虚拟内存进行写操作,此时会触发 COW,并在 45 行打印读到的数据,最后就是释放内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作和 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 JFS 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_JFS_FS):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-JFS-COW-default/
# 编译内核
make kernel
# 编译实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “JFS COW on wp_page_copy 0x6000000000”, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行了读操作,那么会触发相应的缺页异常,缺页异常处理函数会分配 PAGECACHE,并从磁盘加载文件内容到 PAGECACHE,最后建立页表映射到 PAGECACHE。在这种场景下,进程对该内存执行写操作,MMU 检查到没有写权限于是触发缺页异常,此时缺页异常处理函数的处理逻辑如上,由于此时页表已经存在,只是因为写保护触发的缺页,那么 do_wp_page 函数进行逻辑处理,由于此时 PAGECACHE 不是匿名内存,那么调用 wp_page_copy 函数,该函数首先分配一个匿名页,然后调用 __wp_page_copy_user 函数将 PAGECACHE 内容拷贝到匿名页,此时匿名页就是 PAGECACHE 的一个副本,缺页异常将页表映射到匿名页上,那么后续进程的读写操作都在该副本匿名页上.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行写操作,那么会触发上图逻辑,由于页表不存在,那么缺页异常处理函数会调用 do_cow_fault 函数,该函数首先分配一个匿名页,然后调用 __do_fault 函数分配 PAGECACHE,并将磁盘文件内容加载到 PAGECACHE 里,接着调用 copy_user_highpage 将 PAGECACHE 内容拷贝到匿名内存里,此时匿名内存就是 PAGECACHE 的副本,缺页异常将页表映射到匿名内存上,那么接下来进程对 COW 内存的读写都落到匿名内存上.

图片无法显示,请右键点击新窗口打开图片

do_cow_fault 函数的逻辑如上,JFS 文件系统没有特除的处理逻辑,此处可以看到 COW 过后,虽然虚拟内存是文件映射,但虚拟内存映射的匿名内存,因此如果进程再发生 fork 动作,那么就要按匿名内存 COW 场景进行处理,此时该虚拟内存还是会被设置为写保护。

图片无法显示,请右键点击新窗口打开图片


XFS 文件系统 COW 缺页场景

XFS(X File System) 是一种高性能的日志文件系统,最初由 SGI(Silicon Graphics, Inc.)开发。XFS 的设计目标是提供在大容量存储系统上具有高性能、可扩展性和可靠性的文件存储解决方案。它在 Linux 系统中广泛使用,并在服务器、高性能计算、存储设备和云环境中得到了广泛采用。以下是 XFS 文件系统的一些主要特点和概述:

  • 高性能: XFS 被设计为高性能文件系统,特别适用于大文件和高负载环境。它采用了多种技术来提高文件访问速度,如延迟分配、预分配和高效的元数据操作
  • 日志功能: XFS 支持日志功能,通过将文件系统更改记录到日志中,以提供额外的数据一致性和可恢复性。这有助于防止数据损坏,并允许更快速地恢复文件系统到一致状态
  • 可扩展性: XFS 具有很强的可扩展性,可以处理大容量存储和大文件。它支持动态增加文件系统大小,而无需卸载文件系统
  • 支持大文件: XFS 支持极大文件(大于 16TB),因此适用于需要处理大文件的应用程序,如媒体和图形处理
  • 元数据优化: XFS 采用了高效的元数据布局和管理策略,以提高元数据操作的性能
  • 快速恢复: 由于具有日志功能,XFS在系统崩溃或不正常关闭后能够更快速地恢复文件系统
  • 多平台支持: XFS 最初是为 SGI IRIX 操作系统开发的,但后来被移植到 Linux 和其他操作系统上,因此它在多个平台上可用
  • 广泛应用: XFS 广泛用于服务器环境、高性能计算、云存储和媒体处理等领域,它被认为是一个强大的文件系统选项

XFS 通常用于数据密集型应用程序和大容量存储,因此在选择文件系统时,您可以考虑 XFS 作为文件存储解决方案的候选之一,尤其是在需要高性能和可靠性的情况下。不过,每种文件系统都有其优点和局限性,因此请根据您的具体需求和用例来选择适合您的文件系统, 在 Linux 中使用 XFS 需要打开内核宏 CONFIG_XFS_FS.

图片无法显示,请右键点击新窗口打开图片

XFS 文件系统提供映射文件的 mmap 接口使用了 xfs_file_mmap 函数,xfs_file_mmap 函数为文件映射的 VMA 提供的 vm_ops 接口为 xfs_file_vm_ops,该数据接口实现了 fault 接口 xfs_filemap_fault,那么文件映射 VMA 发生缺页时 xfs_filemap_fault 函数会被调用.

图片无法显示,请右键点击新窗口打开图片

在 XFS 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,XFS 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。

图片无法显示,请右键点击新窗口打开图片

XFS 文件系统也支持 COW 机制,进程在使用 mmap 将文件映射到进程地址空间时,使用 MAP_PRIVATE 标志即可启用 COW 机制。在 XFS 文件系统里,当对 COW 的文件映射内存发起写操作时,MMU 检查到进程对 COW 内存没有写权限,于是触发缺页异常,缺页异常会为 VMA 新分配一个 COPY PAGE 作为副本,此时 COPY PAGE 为匿名页,接着将 VMA 对应的 PAGE CACHE 内容拷贝到 COPY PAGE 里,并将页表更新到 COPY PAGE,当缺页异常返回之后,进程对 COW 内存的读写操作都落到 COPY PAGE 里,但 COPY PAGE 的内容不会回写到磁盘文件上. 那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Virtio-BLK: XFS Filesystem Disk  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped XFS COW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-XFS-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-FILE-XFS-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

测试用例由一个用户程序构成,程序首先在 23 行调用 open 函数打开 XFS 文件系统目录下的 BiscuitOS.txt 文件,然后在 29 行调用 mmap 函数将文件映射到进程的地址空间, 此时使用了 MAP_PRIVATE 标志,接下来在 40 行对文件映射的虚拟内存进行读操作,此时会发送 FAULT AROUND 将附近的虚拟内存页表一共建立,接下来在 45 行对虚拟内存进行写操作,此时会触发 COW,并在 45 行打印读到的数据,最后就是释放内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作和 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 XFS 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_XFS_FS):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-XFS-COW-default/
# 编译内核
make kernel
# 编译实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “XFS COW on wp_page_copy 0x6000000000”, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行了读操作,那么会触发相应的缺页异常,缺页异常处理函数会分配 PAGECACHE,并从磁盘加载文件内容到 PAGECACHE,最后建立页表映射到 PAGECACHE。在这种场景下,进程对该内存执行写操作,MMU 检查到没有写权限于是触发缺页异常,此时缺页异常处理函数的处理逻辑如上,由于此时页表已经存在,只是因为写保护触发的缺页,那么 do_wp_page 函数进行逻辑处理,由于此时 PAGECACHE 不是匿名内存,那么调用 wp_page_copy 函数,该函数首先分配一个匿名页,然后调用 __wp_page_copy_user 函数将 PAGECACHE 内容拷贝到匿名页,此时匿名页就是 PAGECACHE 的一个副本,缺页异常将页表映射到匿名页上,那么后续进程的读写操作都在该副本匿名页上.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行写操作,那么会触发上图逻辑,由于页表不存在,那么缺页异常处理函数会调用 do_cow_fault 函数,该函数首先分配一个匿名页,然后调用 __do_fault 函数分配 PAGECACHE,并将磁盘文件内容加载到 PAGECACHE 里,接着调用 copy_user_highpage 将 PAGECACHE 内容拷贝到匿名内存里,此时匿名内存就是 PAGECACHE 的副本,缺页异常将页表映射到匿名内存上,那么接下来进程对 COW 内存的读写都落到匿名内存上.

图片无法显示,请右键点击新窗口打开图片

do_cow_fault 函数的逻辑如上,XFS 文件系统没有特除的处理逻辑,此处可以看到 COW 过后,虽然虚拟内存是文件映射,但虚拟内存映射的匿名内存,因此如果进程再发生 fork 动作,那么就要按匿名内存 COW 场景进行处理,此时该虚拟内存还是会被设置为写保护。

图片无法显示,请右键点击新窗口打开图片


GFS2 文件系统 COW 缺页场景

GFS2(Global File System 2) 是一种高可用性分布式文件系统,最初由 Red Hat 开发。它旨在支持多台计算机之间共享存储并提供高可用性和数据冗余,从而确保在一个节点故障时文件系统仍然可用。以下是 GFS2 文件系统的一些主要特点和概述:

  • 分布式文件系统: GFS2 是一种分布式文件系统,允许多台计算机同时访问和共享相同的文件系统。这使得它非常适用于集群环境,如高性能计算集群和企业应用程序集群
  • 高可用性: GFS2 旨在提供高可用性,确保文件系统的持续可用性。如果一个节点故障,其他节点可以继续访问文件系统,而无需中断
  • 共享存储: GFS2 要求所有节点都能访问相同的共享存储设备,这通常是通过网络附加存储(如 SAN 或 NAS)来实现的
  • 日志功能: GFS2 支持日志功能,通过记录文件系统更改来提供数据一致性和可恢复性。这有助于防止数据损坏,并允许快速恢复
  • 数据冗余: GFS2 支持数据冗余,可在多个节点之间复制数据,以提高数据的可靠性和容错性
  • 并发性: GFS2 具有良好的并发性,可以支持多个节点同时对文件系统进行读写操作,而不会出现冲突
  • 动态增加容量: GFS2 允许动态增加文件系统的容量,而无需停机或中断服务
  • 多平台支持: GFS2 最初是为 Linux 开发的,但它也可以在其他 Unix-like 操作系统上运行。

GFS2 是专门为集群环境和高可用性需求而设计的,因此对于单个独立的 Linux 桌面系统可能不是最佳选择。它通常用于需要多台计算机之间共享存储和文件系统的场景,以确保在节点故障时系统仍然可用。如果您需要在集群中共享文件系统,并且需要高可用性和数据冗余,那么 GFS2 可能是一个适合的解决方案, 在 Linux 中使用 GFS2 需要打开内核宏 CONFIG_GFS2_FS.

图片无法显示,请右键点击新窗口打开图片

GFS2 文件系统提供映射文件的 mmap 接口使用了 gfs2_mmap 函数,gfs2_mmap 函数为文件映射的 VMA 提供的 vm_ops 接口为 gfs2_vm_ops,该数据接口实现了 fault 接口 gfs2_fault,那么文件映射 VMA 发生缺页时 gfs2_fault 函数会被调用.

图片无法显示,请右键点击新窗口打开图片

在 GFS2 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,GFS2 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。

图片无法显示,请右键点击新窗口打开图片

GFS2 文件系统也支持 COW 机制,进程在使用 mmap 将文件映射到进程地址空间时,使用 MAP_PRIVATE 标志即可启用 COW 机制。在 GFS2 文件系统里,当对 COW 的文件映射内存发起写操作时,MMU 检查到进程对 COW 内存没有写权限,于是触发缺页异常,缺页异常会为 VMA 新分配一个 COPY PAGE 作为副本,此时 COPY PAGE 为匿名页,接着将 VMA 对应的 PAGE CACHE 内容拷贝到 COPY PAGE 里,并将页表更新到 COPY PAGE,当缺页异常返回之后,进程对 COW 内存的读写操作都落到 COPY PAGE 里,但 COPY PAGE 的内容不会回写到磁盘文件上. 那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Virtio-BLK: GFS2 Filesystem Disk  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped GFS2 COW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-GFS2-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-FILE-GFS2-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

测试用例由一个用户程序构成,程序首先在 23 行调用 open 函数打开 GFS2 文件系统目录下的 BiscuitOS.txt 文件,然后在 29 行调用 mmap 函数将文件映射到进程的地址空间, 此时使用了 MAP_PRIVATE 标志,接下来在 40 行对文件映射的虚拟内存进行读操作,此时会发送 FAULT AROUND 将附近的虚拟内存页表一共建立,接下来在 45 行对虚拟内存进行写操作,此时会触发 COW,并在 45 行打印读到的数据,最后就是释放内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作和 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 GFS2 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_GFS2_FS):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-GFS2-COW-default/
# 编译内核
make kernel
# 编译实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “GFS2 COW on wp_page_copy 0x6000000000”, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行了读操作,那么会触发相应的缺页异常,缺页异常处理函数会分配 PAGECACHE,并从磁盘加载文件内容到 PAGECACHE,最后建立页表映射到 PAGECACHE。在这种场景下,进程对该内存执行写操作,MMU 检查到没有写权限于是触发缺页异常,此时缺页异常处理函数的处理逻辑如上,由于此时页表已经存在,只是因为写保护触发的缺页,那么 do_wp_page 函数进行逻辑处理,由于此时 PAGECACHE 不是匿名内存,那么调用 wp_page_copy 函数,该函数首先分配一个匿名页,然后调用 __wp_page_copy_user 函数将 PAGECACHE 内容拷贝到匿名页,此时匿名页就是 PAGECACHE 的一个副本,缺页异常将页表映射到匿名页上,那么后续进程的读写操作都在该副本匿名页上.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行写操作,那么会触发上图逻辑,由于页表不存在,那么缺页异常处理函数会调用 do_cow_fault 函数,该函数首先分配一个匿名页,然后调用 __do_fault 函数分配 PAGECACHE,并将磁盘文件内容加载到 PAGECACHE 里,接着调用 copy_user_highpage 将 PAGECACHE 内容拷贝到匿名内存里,此时匿名内存就是 PAGECACHE 的副本,缺页异常将页表映射到匿名内存上,那么接下来进程对 COW 内存的读写都落到匿名内存上.

图片无法显示,请右键点击新窗口打开图片

do_cow_fault 函数的逻辑如上,GFS2 文件系统没有特除的处理逻辑,此处可以看到 COW 过后,虽然虚拟内存是文件映射,但虚拟内存映射的匿名内存,因此如果进程再发生 fork 动作,那么就要按匿名内存 COW 场景进行处理,此时该虚拟内存还是会被设置为写保护。

图片无法显示,请右键点击新窗口打开图片


F2FS 文件系统 COW 缺页场景

F2FS(Flash-Friendly File System) 是一种专为闪存存储设备(如固态硬盘和闪存卡)而设计的文件系统,最初由三星电子开发。F2FS 旨在充分利用闪存设备的特性,并提供高性能、高效的文件系统操作,同时减小对闪存设备的磨损。以下是 F2FS 文件系统的一些主要特点和概述:

  • 闪存优化: F2FS 专门为闪存设备而设计,以最大程度地减小对闪存设备的写入和磨损。它采用了诸如写入放大(Write Amplification)减小、块分配和垃圾回收等技术来延长闪存设备的寿命
  • 高性能: F2FS 在闪存设备上表现出色,特别是对于读取操作和写入小文件的操作。它具有低延迟和高吞吐量,适用于闪存驱动的高性能应用
  • TRIM支持: F2FS 支持 TRIM 命令,该命令可以通知闪存设备哪些数据块不再需要,从而帮助提高性能和延长设备寿命
  • 日志功能: F2FS 支持日志功能,通过记录文件系统更改来提供数据一致性和可恢复性。这有助于防止数据损坏,并允许更快速地恢复文件系统到一致状态
  • 支持大容量存储: F2FS 支持大容量闪存设备,并能够有效地处理大文件
  • 多平台支持: 虽然最初是为 Android 设备而设计的,但 F2FS 现在也可以在许多 Linux 发行版上使用,以及其他一些操作系统上运行
  • 适用于嵌入式系统: F2FS 通常用于嵌入式系统和移动设备,例如智能手机和平板电脑,以及一些嵌入式控制器

F2FS 是一种闪存优化的文件系统,具有高性能、高效率和闪存设备寿命延长等优点。它特别适用于使用闪存存储的系统和设备,但也可以在一些通用计算机上使用。如果您在使用闪存设备的 Linux 系统上寻找高性能的文件系统选项,那么 F2FS 可能是一个很好的选择, 在 Linux 中使用 F2FS 需要打开内核宏 CONFIG_F2FS_FS.

图片无法显示,请右键点击新窗口打开图片

F2FS 文件系统提供映射文件的 mmap 接口使用了 f2fs_file_mmap 函数,f2fs_file_mmap 函数为文件映射的 VMA 提供的 vm_ops 接口为 f2fs_file_vm_ops,该数据接口实现了 fault 接口 f2fs_filemap_fault,那么文件映射 VMA 发生缺页时 f2fs_filemap_fault 函数会被调用.

图片无法显示,请右键点击新窗口打开图片

在 F2FS 文件系统里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,F2FS 文件系统会为 VMA 提供相应 generic_file_vm_ops,另外 vm_file 指向映射的文件(struct file), 其又指向唯一的 STRUCT inode, 其 mapping 成员用于指向 STRUCT address_space, 该数据结构用于维护文件与 PAGE CACHE 和 VMA 的映射关系,其中 i_mmap 成员指向一颗区间树(RB-TREE), 该区间树维护了映射到该文件的 VMA. 另外 i_pages 指向 XARRAY 数组,该数组维护了文件映射的 PAGE CACHE,每个 PAGE CACHE 对应一个 STRUCT page 数据结构,STRUCT page 的 mapping 成员反过来指向 STRUCT address_space, 那么可以知道 PAGE CACHE 被哪些 VMA 映射,因此形成了一个闭环. 当进程首次访问 VMA 虚拟内存区域时,会触发缺页异常构造这些逻辑。

图片无法显示,请右键点击新窗口打开图片

F2FS 文件系统也支持 COW 机制,进程在使用 mmap 将文件映射到进程地址空间时,使用 MAP_PRIVATE 标志即可启用 COW 机制。在 F2FS 文件系统里,当对 COW 的文件映射内存发起写操作时,MMU 检查到进程对 COW 内存没有写权限,于是触发缺页异常,缺页异常会为 VMA 新分配一个 COPY PAGE 作为副本,此时 COPY PAGE 为匿名页,接着将 VMA 对应的 PAGE CACHE 内容拷贝到 COPY PAGE 里,并将页表更新到 COPY PAGE,当缺页异常返回之后,进程对 COW 内存的读写操作都落到 COPY PAGE 里,但 COPY PAGE 的内容不会回写到磁盘文件上. 那么接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Virtio-BLK: F2FS Filesystem Disk  --->
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with File-Mapped F2FS COW --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-F2FS-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-FILE-F2FS-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

测试用例由一个用户程序构成,程序首先在 23 行调用 open 函数打开 F2FS 文件系统目录下的 BiscuitOS.txt 文件,然后在 29 行调用 mmap 函数将文件映射到进程的地址空间, 此时使用了 MAP_PRIVATE 标志,接下来在 40 行对文件映射的虚拟内存进行读操作,此时会发送 FAULT AROUND 将附近的虚拟内存页表一共建立,接下来在 45 行对虚拟内存进行写操作,此时会触发 COW,并在 45 行打印读到的数据,最后就是释放内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作和 43 行写操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 43 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 F2FS 文件映射内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 wp_page_copy 函数的 3102 行加上 bs_debug 打印,以此确认内存流动到这里,接下来执行如下命令进行实践(需要提前打开内核宏: CONFIG_F2FS_FS):

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-FILE-F2FS-COW-default/
# 编译内核
make kernel
# 编译实践案例
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “F2FS COW on wp_page_copy 0x6000000000”, 那么说明实践案例分配了 PAGECACHE,同时也可以看到 PAGECACHE 按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看 PAGECACHE 在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行了读操作,那么会触发相应的缺页异常,缺页异常处理函数会分配 PAGECACHE,并从磁盘加载文件内容到 PAGECACHE,最后建立页表映射到 PAGECACHE。在这种场景下,进程对该内存执行写操作,MMU 检查到没有写权限于是触发缺页异常,此时缺页异常处理函数的处理逻辑如上,由于此时页表已经存在,只是因为写保护触发的缺页,那么 do_wp_page 函数进行逻辑处理,由于此时 PAGECACHE 不是匿名内存,那么调用 wp_page_copy 函数,该函数首先分配一个匿名页,然后调用 __wp_page_copy_user 函数将 PAGECACHE 内容拷贝到匿名页,此时匿名页就是 PAGECACHE 的一个副本,缺页异常将页表映射到匿名页上,那么后续进程的读写操作都在该副本匿名页上.

图片无法显示,请右键点击新窗口打开图片

当进程首先对 COW 的文件映射内存执行写操作,那么会触发上图逻辑,由于页表不存在,那么缺页异常处理函数会调用 do_cow_fault 函数,该函数首先分配一个匿名页,然后调用 __do_fault 函数分配 PAGECACHE,并将磁盘文件内容加载到 PAGECACHE 里,接着调用 copy_user_highpage 将 PAGECACHE 内容拷贝到匿名内存里,此时匿名内存就是 PAGECACHE 的副本,缺页异常将页表映射到匿名内存上,那么接下来进程对 COW 内存的读写都落到匿名内存上.

图片无法显示,请右键点击新窗口打开图片

do_cow_fault 函数的逻辑如上,F2FS 文件系统没有特除的处理逻辑,此处可以看到 COW 过后,虽然虚拟内存是文件映射,但虚拟内存映射的匿名内存,因此如果进程再发生 fork 动作,那么就要按匿名内存 COW 场景进行处理,此时该虚拟内存还是会被设置为写保护。

图片无法显示,请右键点击新窗口打开图片


HugeTLB Memory: COW 缺页场景

图片无法显示,请右键点击新窗口打开图片

写时拷贝(COW)匿名内存是当进程调用 fork() 函数构造子进程时,子进程继承了父进程的 HugeTLB 内存,但 fork 系统调用会将父进程和子进程的 HugeTLB 内存都修改为写保护,那么可以确保父子进程读到同样的内容,而父子进程对 HugeTLB 内存要写的时候,才会在属于自己的 HugeTLB 内存上执行写操作。COW 的核心是在写的时候才会分配物理内存承接写操作,其存在两种缺页场景:

  • 当 CPU 写入父子进程还在共享的 COW HugeTLB 内存: MMU 因为写保护异常,于是触发缺页异常,在缺页异常处理函数发现 HugeTLB 大页被多个进程映射,于是分配一个新的 HugeTLB 大页,并将页表更新映射到新的 HugeTLB 大页上,然后添加 _PAGE_RW 属性,最后将原始物理页的内容拷贝到新的物理页上,那么 CPU 后续的读写操作不会触发缺页.
  • 当 CPU 写入父进程或者子进程独占的 COW HugeTLB 内存: MMU 因为写保护异常,于是触发缺页异常,缺页异常处理函数发现 HugeTLB 大页只被一个进程映射,那么缺页异常处理函数会重新 REUSE 该物理页,并添加页表的 _PAGE_RW 属性,那么 CPU 后续的读写操作不会触发缺页.

图片无法显示,请右键点击新窗口打开图片

在 Linux 里使用 struct anon_vma 和 struct anon_vma_chain 两个数据结构体维护匿名 HugeTLB 内存的逆向映射,所谓逆向映射就是知道匿名页被哪些 VMA 映射。AV 是 struct anon_vma 的简称,AVC 是 struct anon_vma_chain 的简称,每个 VMA 一一对应一个 AV,每个 AV 维护一颗红黑树,红黑树的节点表示映射到 AV 的不同 VMA,每个 VMA 通过 AVC 进行表示,那么只要遍历这颗红黑树就可以知道有多少进程的 VMA 映射到该 AV,那么可以看到一个 AVC 就是一次映射,可以通过 AVC 找到对应的 VMA,那么可以将匿名页指向 AV,那么就可以知道有多少进程映射到自己. 每当进程发送一次 fork 动作时,子进程会在父进程 AV 红黑树下新增一个 AVC 节点,AVC 节点指向子进程的 VMA,并且子进程 VMA 对应的 AV 的 Parent 指向父进程 VMA 对应 AV. 以此类推无论进程 fork 多少次,都可以知道一个匿名 HugeTLB 页被多少进程映射. 为了更加深刻了解 COW 匿名 HugeTLB 内存缺页,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig
  
  [*] DIY BiscuitOS/Broiler Hardware  --->
      [*] Support Host CPU Feature Passthrough
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with HUGETLB Memory on COW(Copy-On-Write) --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-COW-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-HUGETLB-COW-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,程序首先在 21 行调用 mmap 函数分配一段 2MiB 的虚拟内存,这段虚拟内存采用了 MAP_HUGETLB 和 MAP_ANONYMOUS,因此绑定了公共 2MiB HugeTLB 大页池子了的大页. 程序接着在 32 行对这段虚拟内存执行写操作,由于此时页表没有建立会触发缺页异常. 程序在 34 行调用 fork 函数创建子进程,然后在 36 行子进程首先对虚拟内存发起写操作,紧随其后父进程在 40 行也对虚拟内存发起了写操作,最后父子进程在 44 行再次对虚拟内存执行读操作. 以上便是一个最基础的实践案例,可以知道 36 行和 40 行写操作会触发缺页,为了可以看到 COW 内存在缺页异常里的流动,本次在 36 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 HugeTLB 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 hugetlb_fault 函数的 5733 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-COW-default/
# 编译内核
make kernel
# 编程程序
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HUGETLB COW PF on hugetlb_fault 0x6000000000”, 说明缺页异常处理函数执行过 HUGETLB COW 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

当 COW HugeTLB 内存发生缺页时,其缺页处理函数流程如上,缺页异常函数识别出异常内存是 HugeTLB 内存,于是调用 hugetlb_fault 函数进行处理,函数检查到 PMD Entry 存在,并且 VMA 不是共享的,另外 HugeTLB 大页只有被一个进程映射,那么进入 COW-RESUE 分支,其核心目的是将页表修改为可读可写,于是调用 set_huge_ptep_writable 函数完成该任务; 而对于有多个进程映射的 HugeTLB 大页,缺页异常处理函数进入 COW-COPY 分支,并调用 alloc_huge_page 新分配一个 HugeTLB 大页,然后将可读可写的页表映射新的 HugeTLB 大页上,并且调用 hugepage_add_new_anon_rmap 更新 HugeTLB 大页的逆向映射,那么进程后续就在各自的 HugeTLB 大页上进行读写操作.

图片无法显示,请右键点击新窗口打开图片

上图是 HugeTLB 场景下的 fork 系统调用处理逻辑,其中核心逻辑是新进程会调用 dup_mmap 函数拷贝父进程的地址空间,其遍历父进程的所有的 VMA,对于匿名映射的 VMA,那么调用 anon_vma_fork 函数建立逆向映射,最后针对 HugeTLB 调用 reset_vma_resv_huge_pages 函数遍历 VMA 对应的页表,将 COW HugeTLB 内存对应页表的 _PAGE_RW 标志去掉,此时可以看到调用 huge_ptep_set_wrprotect 函数进行写保护,最终更新页表. 那么无论是父进程还是子进程的 COW HugeTLB 内存都变成了写保护.

HugeTLB COW 导致进程 SegmentFault

图片无法显示,请右键点击新窗口打开图片

HugeTLB 大页池子在为用户进程分配虚拟内存时,会将对应数量的 HugeTLB 大页进行预留,这样做的好处是防止 HugeTLB 虚拟内存缺页时保证可以分配到 HugeTLB 大页,坏处也很明显就是 HugeTLB 虚拟内存没有被使用,那么大量的预留 HugeTLB 大页也就浪费了,另外结合 FORK 系统调用对 HugeTLB 的处理过程,如果某个进程分配的 HugeTLB 虚拟内存发生了多次 FORK 动作,FORK 动作并不会为新创建的子进程预留 HugeTLB 大页,那么会出现某个子进程访问这段虚拟内存时,缺页异常处理函数无法从 HugeTLB 大页池子里分配到可用的 HugeTLB 内存,那么缺页进程只能向进程发送 SIG_BUS,最后导致子进程 SegmentFault 异常退出. 为了对该问题有更深刻的认知,开发者可以通过一个实践案例进行了解,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig
  
  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Page Fault with HUGETLB Memory on COW SegmentFault --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-COW-FAULT-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-PF-HUGETLB-COW-FAULT-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,程序首先在 21 行调用 mmap 函数分配一段 2MiB 的虚拟内存,这段虚拟内存采用了 MAP_HUGETLB 和 MAP_ANONYMOUS,因此绑定了公共 2MiB HugeTLB 大页池子了的大页. 程序接着在 32 行对这段虚拟内存执行写操作,由于此时页表没有建立会触发缺页异常. 程序在 34 行调用 fork 函数创建子进程,然后在 37 行子进程首先对虚拟内存发起写操作,紧随其后父进程在 41 行也对虚拟内存发起了写操作,最后父子进程在 45 行再次对虚拟内存执行读操作. 以上便是一个最基础的实践案例,可以知道 37 行和 41 行写操作会触发缺页,为了可以看到 COW 内存在缺页异常里的流动,本次在 37 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在 HugeTLB 内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 hugetlb_fault 函数的 5733 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-PF-HUGETLB-COW-FAULT-default/
# 编译内核
make kernel
# 编程程序
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “HUGETLB COW FAULT PF on hugetlb_fault 0x6000000000”, 另外只有父进程打印了读操作的数据, 说明缺页异常处理函数执行过 HUGETLB COW FAULT 缺页. 通过实践可以看到实践案例按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

同 COW HugeTLB 内存发生缺页时处理逻辑类似,可以直接看 WP-HUGETLB 分支,当在 hugetlb_wp 函数时,缺页异常函数发现 HugeTLB 大页被多个进程映射,因此它会调用 alloc_huge_page 为新进程分配新的 HugeTLB 大页,但 dequeue_huge_page_vma 函数并没有从 HugeTLB 大页池子里获得可用的大页,于是继续调用 alloc_surplus_huge_page 函数通过超发方式从系统里分配新的大页,但是此时并没有超发名额,因此分配内存失败,此时缺页异常处理函数直接返回 VM_FAULT_SIGBUS,有了这个返回值之后,缺页异常处理函数直接发送 SIG_BUS,让进程 SegmentFault 异常退出.

图片无法显示,请右键点击新窗口打开图片


THP 透明大页 COW 缺页场景

图片无法显示,请右键点击新窗口打开图片

写时拷贝(COW)匿名透明大页内存是当进程调用 fork() 函数构造子进程时,子进程继承了父进程的匿名透明大页内存,但 fork 系统调用会将父进程和子进程的匿名透明大页内存都修改为写保护,那么可以确保父子进程读到同样的内容,而父子进程对匿名透明大页内存要写的时候,才会在属于自己的匿名内存(匿名透明大页内存)上执行写操作。COW 的核心是在写的时候才会分配物理内存承接写操作,其存在两种缺页场景:

  • 当 CPU 写入父子进程还在共享的 COW 匿名透明大页内存: MMU 因发现权限异常而触发缺页异常,缺页异常处理函数是被该内存为匿名透明大页,并且此时页的引用计数大于 1,那么缺页异常函数将匿名透明大页拆分成写保护的匿名页,然后按匿名页写保护缺页的流程处理,此时缺页异常处理函数会新分配一个匿名页,然后将原先内容拷贝到新的匿名页上,并将页表更新到新的匿名页上。此时匿名透明大页依旧存在,但名存实亡,其与内存也变成了写保护的匿名页。最后进程可以对该匿名内存进行读写操作.
  • 当 CPU 写入父子进程独占的 COW 匿名透明大页内存: MMU 因发现权限异常而触发缺页异常,缺页异常处理函数是被该内存为匿名透明大页,并且此时页的引用计数为 1,同样的缺页异常函数将匿名透明大页拆分成写保护的匿名页,然后按匿名页写保护缺页的流程处理,此时缺页异常处理函数会新分配一个匿名页,然后将原先内容拷贝到新的匿名页上,并将页表更新到新的匿名页上。此时匿名透明大页已经不存在,其余内存也变成了写保护的匿名页。最后进程可以对该匿名内存进行读写操作.

图片无法显示,请右键点击新窗口打开图片

在 Linux 里使用 struct anon_vma 和 struct anon_vma_chain 两个数据结构体维护匿名透明大页内存的逆向映射,所有逆向映射就是通过匿名页被哪些 VMA 映射。AV 是 struct anon_vma 的简称,AVC 是 struct anon_vma_chain 的简称,每个 VMA 一一对应一个 AV,每个 AV 维护一颗红黑树,红黑树的节点表示映射到 AV 的不同 VMA,每个 VMA 通过 AVC 进行表示,那么只要遍历这颗红黑树就可以知道有多少进程的 VMA 映射到该 AV,那么可以看到一个 AVC 就是一次映射,可以通过 AVC 找到对应的 VMA吗,那么可以将匿名透明大页指向 AV,那么就可以知道有多少进程映射到自己. 每当进程发送一次 fork 动作时,子进程会在父进程 AV 红黑树下新增一个 AVC 节点,AVC 节点指向子进程的 VMA,并且子进程 VMA 对应的 AV 的 Parent 指向父进程 VMA 对应 AV. 以此类推无论进程 fork 多少次,都可以知道一个匿名透明大页被多少进程映射. 为了更加深刻了解 COW 匿名透明大页内存缺页,接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

  [*] Package  --->
      [*] Paging Mechanism  --->
          [*] Huge PageFault with COW(Copy-On-Write) Anonymous THP --->

# 部署实践案例
make
# 源码目录
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-HUGE-PF-COW-ANON-THP-default/
# 部署源码
make download
# 在 BiscuitOS 中实践
make build

BiscuitOS-PAGING-HUGE-PF-COW-ANON-THP-default Source Code on Gitee

图片无法显示,请右键点击新窗口打开图片

实践案例由一个应用程序构成,进程在 21 行调用 mmap 函数并使用 MAP_PRIVATE 和 MAP_ANONYMOUS 标志分配一段匿名透明大页内存, 并且赋予 PROT_READ 和 PROT_WRITE 属性,然后在 31 行对匿名透明大页内存进行写操作. 程序接着在 33 行调用 fork 函数创建子进程,此时匿名透明大页内存变成写保护匿名内存,接着为了构造另外一个场景,35 行为子进程分支,其首先对 COW 匿名透明大页内存进行写操作,由于此时父子进程都映射到 COW 匿名透明大页内存,那么 35 行的写操作会触发缺页,但缺页异常处理函数会分配一个新匿名页,然后将内容拷贝到新匿名页上,那么接下来子进程在新的匿名页上执行写操作; 同理 39 行是父进程分支,其先 sleep 一段时间之后,COW 匿名透明大页内存此时只有父进程映射,因此 39 行的写操作同样会引起缺页,但缺页异常处理函数的处理和子进程访问时一致,新分配一个匿名页,并将原先内容拷贝到该匿名页上,那么父进程继续在新的匿名页上进行写操作. 以上便是一个最基础的实践案例,可以知道 35 行读操作和 39 行写操作都会触发缺页,为了可以看到内存在缺页异常里的流动,本次在 35 行前后加上 BS_DEBUG 开关:

图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片 图片无法显示,请右键点击新窗口打开图片

接着在匿名透明大页内存缺页流程必经之路上任意位置加上 BS_DEBUG 函数,以此观察内存在某个函数里的流动,例如上图在 do_wp_page 函数的 3368 行和 do_huge_pmd_wp_page 函数 1315 行加上 bs_debug 打印,以此确认内存是向哪个函数流动,接下来执行如下命令进行实践:

# 编译应用程序
cd BiscuitOS/output/linux-6.0-x86_64/package/BiscuitOS-PAGING-HUGE-PF-COW-ANON-THP-default/
# 编译内核
make kernel
# 编程程序
make build

图片无法显示,请右键点击新窗口打开图片

当 BiscuitOS 启动之后,直接运行 RunBiscuitOS.sh 脚本,脚本里包括实践所需的命令,可以看到进程执行之后对虚拟内存的访问引起了缺页异常,并且该案例的缺页异常处理流程打印了字符串 “COW ANON-THP HUGE-PF on do_huge_pmd_wp_page 0x6000000000”“COW ANON-THP HUGE-PF on do_wp_page 0x6000000000”, 子进程访问 COW 匿名透明大页内存时,缺页中断将原始内容拷贝到新匿名页上,子进程接下来都是访问新的匿名页. 通过实践可以看到写保护匿名内存按着之前分析的代码路径流动。最后开发者可以在该路径上的任何地方使用 bs_debug 查看匿名内存在缺页异常处理流程里的流动.

图片无法显示,请右键点击新窗口打开图片

对于匿名透明大页 COW 的缺页处理流程主要完成两个任务: 首先是将写保护的匿名透明大页拆分成更小粒度的匿名页,然后将匿名页内容拷贝到一个新的匿名页上,最后更新页表到新的匿名页上. 缺页异常处理函数首先会进入到 do_huge_pmd_wp_page 函数,通过一些列检查之后发现需要将透明大页拆分成匿名页,那么调用 __split_huge_pmd_locked 函数进行拆分,接下来将拆分之后的匿名页对应的页表修改为写保护,此时直接返回 VM_FAULT_FALLBACK,那么缺页异常处理函数继续处理而不是返回. 接下来利用 handle_pte_fault 提供的 do_wp_page 来处理 COW,此时调用 alloc_page_vma 函数分配一个新的匿名页,接着调用 __wp_page_copy_user 将内容拷贝到新的匿名页上并更新页表.

图片无法显示,请右键点击新窗口打开图片

上图是将匿名透明大页拆分成匿名页的过程,主要逻辑位于 2182-2202 行,此时 page 指向匿名透明大页,2183 行调用 mk_pte 函数都在 PTE Entry 内容,此时仅包含 PFN 信息和 VMA 使用的页表信息,2184 行调用 maybe_mkwrite 函数判断如果 VMA 具有写权限,那么向 PTE Entry 里添加 _PAGE_RW 标志,2187 行如果 write 变量不为真,从上下文可知这里不为真,那么调用 pte_wrprotect 函数将 PTE Entry 修改为写保护的,最后 2196 行调用 pte_offset_map 函数获得 PTE Entry 页表的位置,最后调用 set_pte_at 函数设置最终的页表.

图片无法显示,请右键点击新窗口打开图片

一个有意思的地方,也是和写保护匿名内存不同的地方,当对一个写保护的匿名透明大页写操作之后,被写的那部分变成了可读可写的匿名内存,而匿名透明大页会被拆分,其余部分均变成了写保护的匿名页. 匿名透明大页 COW 其余流程和匿名页场景一样,因此共用了这部分代码.

图片无法显示,请右键点击新窗口打开图片

上图是 fork 系统调用处理逻辑,其中核心逻辑是新进程会调用 dup_mmap 函数拷贝父进程的地址空间,其遍历父进程的所有的 VMA,对于匿名映射的 VMA,那么调用 anon_vma_fork 函数建立逆向映射,最后调用 copy_page_range 函数遍历 VMA 对于的页表,将 COW 匿名内存对应页表的 _PAGE_RW 标志去掉,此时可以看到调用 ptep_set_wrprotect 函数进行写保护,最终更新页表. 那么无论是父进程还是子进程的 COW 匿名内存都变成了写保护.

图片无法显示,请右键点击新窗口打开图片