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

BiscuitOS 内存管理之分页大专题订阅入口

目录

  • 文件映射: File-Mapped

  • File-Mapped with Shared

  • File-Mapped with Private/COW

  • File-Mapped Readhead with Fault Around

  • File-Mapped with BIO Request

  • 文件映射缺页场景汇总

    • MINIXFS 文件系统缺页场景

    • EXT2 文件系统缺页场景

    • EXT3 文件系统缺页场景

    • EXT4 文件系统缺页场景

    • VFAT 文件系统缺页场景

    • MSDOS 文件系统缺页场景

    • FAT 文件系统缺页场景

    • CRAMFS 文件系统缺页场景

    • BFS 文件系统缺页场景

    • JFFS2 文件系统缺页场景

    • UBIFS 文件系统缺页场景

    • SQUASHFS 文件系统缺页场景

    • BTRFS 文件系统缺页场景

    • REISERFS 文件系统缺页场景

    • JFS 文件系统缺页场景

    • XFS 文件系统缺页场景

    • GFS2 文件系统缺页场景

    • F2FS 文件系统缺页场景

    • TMPFS 文件系统缺页场景

    • Huge-Tmpfs 文件系统缺页场景

    • XFS DAX 缺页场景

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


文件映射: File-Mapped

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

在 Linux 中,文件是计算机存储数据的基本单元,它是计算机系统中用于存储、组织和管理信息的主要方式之一. 文件在 Linux 和其他操作系统中都扮演着重要的角色,文件用于存储各种类型的数据,包括文本、图像、音频、视频、程序代码等。文件是计算机上保存和检索信息的主要手段, 文件可用于组织和结构化数据。例如,文本文件可用于存储文档、配置文件和日志,而目录(文件夹)可用于组织文件和其他目录. 在硬件拓扑结构上,文件是存储在磁盘的,系统要访问文件需要经过以下步骤:

  • 应用程序请求: 流程的开始是由应用程序触发的,应用程序通过文件系统接口(如 open、read、write 等) 发起对文件的读写请求
  • 文件描述符: 当应用程序请求打开文件时,内核为该文件分配一个文件描述符。文件描述符是一个整数,用于标识打开的文件
  • 文件操作:
    • 读取文件: 当应用程序请求读取文件时,内核会检查文件是否在 Page Cache 中,如果在缓存中,则直接从缓存中返回数据。如果不在缓存中,则内核会从磁盘读取数据,并将数据复制到内存中,并可能将其放入 Page Cache 中以备将来使用
    • 写入文件: 当应用程序请求写入文件时,内核会将数据写入内存中的缓冲区。然后内核根据文件系统的策略将数据写入 Page Cache 中,并在适当的时候将数据刷新到磁盘上的文件中
  • 同步和缓存管理:
    • 同步(Sync): 对于写操作,内核通常会将数据写入 Page Cache中,并不立即写入磁盘,以提高性能。然而,应用程序可以通过调用 fsync 或 fdatasync 等函数来强制刷新 Page Cache 中的数据到磁盘,以确保数据持久性
    • Page Cache 管理: 内核会自动管理 Page Cache,根据内存压力和策略来选择哪些数据块保留在缓存中,哪些数据块被丢弃
  • 文件关闭: 当应用程序不再需要文件时,通常会使用 close 系统调用关闭文件描述符。这将释放相关的资源,并可能导致数据刷新到磁盘

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

在 Linux 里,访问普通文件(Regular File) 的方式通常有: Buffered I/OFile-MappedDirect I/O 以及 DAX(Direct Access), 通常文件访问使用 Buffered I/O 方式,它通过使用缓冲区(buffer)来管理文件数据,通常在用户空间和内核之间进行数据传输, 并提供了 POSIX 接口(Read/Write/OPEN/CLOSE) 用于文件访问。Linux 根据不同的场景,也会使用其他方式进行文件访问,这些方式直接的差异如下:

  • Buffered I/O: Buffered I/O 使用缓冲区来管理文件数据,它是默认的文件 I/O 方式。数据首先从磁盘读取到内核的 Page Cache 中,然后从 Page Cache 复制到用户空间的缓冲区。类似地,在写入文件时,数据首先被写入用户空间的缓冲区,然后从缓冲区复制到内核的 Page Cache,并最终刷新到磁盘。Buffered I/O 通过缓存来优化磁盘 I/O 操作,减少磁盘访问次数,提高文件访问速度
  • Page Cache: Page Cache 是内核中的一个缓存机制,用于存储文件数据的页。它是 Buffered I/O 的一部分,用于缓存文件数据块以加速文件读取和写入操作。Page Cache 的数据存储在内存中,并在文件 I/O 操作期间自动加载和卸载. Page Cache 的主要作用是减少磁盘 I/O,提高文件访问性能。当应用程序请求读取文件时,内核首先检查 Page Cache 中是否已经有所需的数据,如果有,就直接从内存中返回数据,而不需要从磁盘读取
  • Direct I/O: Direct I/O 是一种绕过 Page Cache 的文件 I/O 方式。它不使用内核的 Page Cache 来缓存文件数据,而是直接在用户空间和磁盘之间传输数据。这意味着数据不会在内核中缓存,而是直接读取或写入磁盘. Direct I/O 可以提供更精确的 I/O 控制,避免了Page Cache 可能引入的不确定性。它通常用于需要高度可预测性的 I/O 操作,如数据库管理系统
  • DAX(Direct Access): DAX 是一种特殊的 Direct I/O,用于直接在用户空间和存储设备之间进行数据访问,绕过了操作系统的文件系统和 Page Cache。DAX 允许应用程序将数据直接映射到存储设备上,以实现低延迟、高性能的 I/O 操作. DAX 适用于需要极低延迟和高性能的应用程序,如高速数据库系统和大规模数据分析

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

在 Linux 中,文件映射方式分配内存是一种通过将文件内容映射到进程的地址空间来分配内存的方法。这通常使用 mmap 系统调用实现,可以将文件的一部分或整个文件映射到进程的内存中,使得进程可以直接读取和写入文件内容,就好像它们在内存中一样。这与匿名映射有以下区别:

  • 源数据: 使用文件映射时,内存区域的内容来自磁盘上的文件。进程可以访问文件的内容,对文件所做的更改也会反映回文件; 匿名映射是一种不与文件相关的内存映射,它没有关联的文件内容。通常匿名映射用于进程之间共享数据或分配零初始化的内存块
  • 数据可持久性: 文件映射通过文件映射分配的内存区域是持久性的,即进程终止后,文件内容仍然存在于磁盘上。这使得文件映射适合需要持久性数据存储和共享的情况; 匿名映射分配的内存区域在进程终止后被释放,其内容不会保留在磁盘上。这适用于临时数据存储和进程间通信,但不保留数据
  • 文件描述符: 与文件映射关联的内存区域通常需要文件描述符,这样进程可以通过文件描述符来操作文件内容; 匿名映射不需要文件描述符,它通常用于分配内存块,而不是操作文件
  • 初始化: 文件映射通常不需要显式初始化,因为它们的内容来自文件; 匿名映射通常需要显式初始化,因为它们没有关联的文件内容,可以使用 mmap 标志中的 MAP_ANONYMOUS 来分配零初始化的内存

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

在 Linux 里,文件映射(File-Mapped) 的数据架构如上图,用户进程使用文件映射将文件映射到进程地址空间之后,进程使用 VMA 描述映射之后的虚拟内存区域,不同的文件系统会为 VMA 提供相应的 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 映射,因此形成了一个闭环.

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

Linux 支持多种文件系统,每个文件系统都会为其下创建的文件(Regular File)提供文件操作接口和文件映射虚拟内存操作接口,重点看一下与文件映射有关的接口,STRUCT file_operations 提供的 mmap 函数用于将该文件系统的文件映射到进程的地址空间,get_unmapped_area 接口用于将该文件系统映射到进程地址空间指定位置或者对齐方式. STRUCT vm_operation_struct 提供了该文件系统文件映射到虚拟内存区域之后,虚拟内存区域的操作接口,其中 fault 接口用于进程访问该虚拟内存发生缺页的处理方法, huge_fault 接口用于在支持文件大页映射的虚拟内存发生缺页时的处理方法.

File-Mapped with Shared

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

在 Linux 里,多个进程可以同时打开同一个文件,并且可以将该文件映射到进程的地址空间,那么多个进程实现文件内容共享。例如多个进程读取的内容都是一致的,另外当有一个进程对文件写操作之后,多个进程读到是进程写入的新值。该机制可以使用 mmap 映射内存时使用 MAP_SHARED 标志. 共享文件一般是只读的,这样的好处时系统只需维护一套 PAGE CACHE 就可以实现多个进程对文件的读请求,这样大大减少了内存消耗。共享方式的文件映射通常用来只读的共享库场景,每个进程都会加载很多相同的只读共享库,那么可以使用这种方法减少大量的内存消耗.

File-Mapped with Private/COW

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

在 Linux 里,进程可以私有化打开一个文件,即只有该进程可以对该文件有独占写(Exclusive Write), 而进程如果通过 fork 创建了子进程,那么子进程可以和父进程共享读(Shared Read) 文件的内容,直到子进程后者父进程向文件发起写操作时,那么 Linux 会执行 COW(Copy-On-Write) 机制, 该机制会在写的时候分配一个新的匿名页,并将原先 PAGE CACHE 的内容拷贝到新的匿名页里,那么写操作写到新的匿名页里,这样将会大大降低内存消耗.

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

在 File-Mapped 里,COW 场景不止发生在 FORK 操作,另外当一个进程使用 mmap 映射文件时采用了 MAP_PRIVATE 标志,那么文件映射的这段内存就会发生 COW。在这个场景下,当进程首先对文件映射的内存进行读操作时,此时内存是 MAY_SHARED 的属性,虽然此时会触发缺页异常,但缺页异常处理函数主要是分配 PAGE CACHE,并将文件内容从磁盘加载到 PAGE CACHE 里,最后建立虚拟内存到 PAGE CACHE 的页表,此后只要进程只对文件映射的内存执行读操作,那么 COW 不会发生。一旦进程开始发起写操作,那么再次触发缺页异常,缺页异常处理函数会重新分配一个新的匿名页,并将 PAGE CACHE 的内容拷贝到匿名页上,最后建立虚拟内存到匿名页的页表,那么接下来文件的读写操作都会落到这个匿名页上,换句话说就是写操作不会落盘.

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

对于文件映射的内存,同样都是 COW 场景,但由于对匿名内存的读写顺序导致缺页流程存在差异。当进程对文件映射内存先读后写,那么缺页异常处理函数先建立虚拟内存到 PAGECACHE 的映射,待再次写时缺页异常处理函数再次建立虚拟内存到匿名页的映射; 反之进程对文件映射先写后读,那么缺页异常处理函数直接建立虚拟内存到匿名页的映射. 文件映射的 COW 应用场景包括:

  • 进程复制: 当一个进程使用 fork 系统调用创建一个子进程时,子进程通常会继承父进程的虚拟内存映射。为了节省内存和资源,内核可以使用 COW 来处理这些共享映射。当父进程或子进程尝试修改共享映射中的数据时,COW 会创建副本,以确保各自的修改不会影响其他进程
  • 虚拟内存快照: 在某些情况下,应用程序可能需要创建虚拟内存的快照,以便在以后的某个时候回滚到先前的状态。COW 允许应用程序创建虚拟内存的快照,其中包括共享的文件映射。这可以用于实现撤销操作或在应用程序的不同状态之间切换
  • 共享库加载: 当多个进程使用相同的共享库时,它们可以共享相同的文件映射,而不是为每个进程创建单独的副本。COW 确保在进程修改库中的数据时不会影响其他进程对该库的访问
  • 内存映射文件的编辑器: 编辑器(如文本编辑器或图像编辑器)可以使用内存映射文件来加载大文件,以便快速访问和编辑。COW 可用于确保多个编辑器实例可以共享相同的文件映射,并在编辑时创建副本以避免相互干扰
  • 虚拟机内存快照: 虚拟机管理器(如 VMware 或 VirtualBox)可以使用 COW 技术来创建虚拟机内存快照。这允许管理员捕获虚拟机的当前状态,随时回滚到以前的状态,而无需复制整个虚拟内存
File-Mapped Readhead with Fault Around

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

在 Linux 中,FAULT AROUND 是一种缺页处理(Page Fault Handling) 的优化策略,旨在改善内存访问性能,特别是在具有局部性的内存访问模式下。当一个进程访问文件映射的内存中的某个地址时,如果所需的页面不在物理内存中,就会发生缺页(Page Fault),这需要将缺失的页面从磁盘加载到物理内存中,然后允许进程继续执行。FAULT AROUND 策略的目标是减少未来可能的缺页次数,通过在页面访问周围预取(Prefetch) 附近的页面来实现这一目标. 在许多情况下,应用程序访问的数据在空间上具有局部性,即近期访问的数据很可能在不久的将来再次被访问。因此预取附近的页面有助于利用局部性,减少未来的缺页.

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

缺页异常处理函数处理 FAULT AROUND 的逻辑如上,缺页异常处理分配了多个 PAGECACHE,并将文件内容从磁盘加载到多个 PAGECACHE 里,此时并为所有的 PAGECACHE 的建立页表,缺页异常处理函数调用 should_fault_around 函数判断可以执行 FAULT AROUND 之后,就调用 filemap_map_pages 为缺页地址相邻的多个虚拟内存建立页表,那么进程接下来访问相邻的虚拟内存时不再发生缺页.

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

FAULT AROUND 的条件包括三个(三个满足才能发生 FAULT AROUND),第一个条件是文件系统为 VMA 提供了 map_pages 接口,目前主流的文件系统都会提供该接口; 其次是 uffd_disable_fault_around 支持 FAULT AROUND,这个与用户空间发生缺页有关; 第三个是 fault_around_bytes 阈值要大于一个页. 有了三个条件其实还不能满足真正的 FAULT AROUND,还需要进程 mmap 区域的范围大于 1 个 PAGE SIZE. 最后开发者可以通过如下命令控制 FAULT AROUND 的粒度:

# FT_BYTES: 表示预设内存大小,按字节计算
echo ${FT_BYTES} > /sys/kernel/debug/fault_around_bytes
File-Mapped with BIO Request

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

在 Linux 里,进程将文件映射到进程的地址空间之后,文件系统会在虚拟内存和磁盘之间使用物理页来缓存从块设备读取的数据,这个物理页称为 PAGE CACHE(页缓存). 另外文件系统与磁盘之间使用 BUFFER-HEAD 管理缓冲区,以便进行读取和写入磁盘之间的数据传输。总的来说,文件系统想向磁盘上的文件写入或读取数据时,都会通过 BUFFER-HEAD 将其打包成 BIO 请求,然后下发到系统的 BLOCK IO 层,待请求完成时,文件系统可以在页缓存上看到最新的数据。

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

BUFFER-HEAD 与 PAGECACHE 之间的关系如上图,当文件系统需要向磁盘文件读取或写入文件时,文件系统按 STRUCT buffer_head 描述的区域作为请求发送给 BIO,BIO 完成硬件上的磁盘数据读写,待请求完成之后磁盘可以通过中断等方式通知文件系统,此时 STRUCT buffer_head 指向的区域为最新的数据.

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

BiscuitOS 针对 File-Mapped 缺页专题提供了多个 Virtio-BLK 的磁盘,每个磁盘均采用不同的文件系统,当 VFS 向磁盘写入或读取文件时,其向 BIO 层发送请求,BIO 层最终将请求转换成 Virtio-BLK Kick-OFF 请求,待 Virtio-BLK 处理完成请求之后通过 MSIX 中断进行通知,BIO 收到通知之后将请求完成信息继续上报到 VFS 层,这样就完成了磁盘文件的读写.

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

上图是文件系统发起磁盘文件读操作的代码流程图,以 EXT4 文件和 Virtio-BLK 磁盘为例子进行讲解:

  • A: VFS 层 READ 操作处理
  • B: EXT4 文件系统的 READ 处理
  • C: 将 READ 操作打包成为 BIO 请求发送到 BIO 层
  • D: BIO 层将 BIO 请求通过同步或者异步队列最后下发到 Virtio-BLK 磁盘
  • E: Virtio-BLK 磁盘处理完读请求将读取的内容放到指定位置并发送 MSIX 中断通知 BIO 层
  • F: BIO 层将请求完成信息向上传递给文件系统
  • G: 文件系统完成 BIO 请求向上传递给 VFS 层
  • H: VFS 层完成最终的 COPY TO USER 操作

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

采用文件映射的虚拟内存发生缺页是,缺页异常处理函数在特定代码路径会执两次, 即 VM_FAULT_RETRY 导致的重试. VM_FAULT_RETRY 的缘故,handle_mm_fault 会被执行两次,这么做的原因有如下:

  • 磁盘 I/O 延迟: 当页面不在物理内存中,需要从磁盘加载时,可能由于磁盘 I/O 操作的延迟,内核无法立即获取所需页面。此时,内核可能会将页面标记为需要重新尝试,并等待磁盘 I/O 完成
  • 页面锁定: 在某些情况下,内核可能会锁定某些页面,以确保它们在内存中不会被交换出去或释放。如果页面被锁定,内核可能会等待解锁页面后才能重新加载
  • 其他临时情况: 还可能存在其他临时情况,导致页面无法立即加载到内存中。在这种情况下,内核可能会标记页面并尝试重新加载

VM_FAULT_RETRY 并不是 Linux 文件映射的特定特性,而是内核内存管理的一般机制。它在多种上下文中都有用,不仅限于文件映射。具体的行为可能因 Linux 内核版本、硬件配置和文件系统类型而异,因此要根据具体情况考虑如何处理 VM_FAULT_RETRY 错误码.

文件映射缺页场景汇总

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

文件映射的虚拟内存发生缺页场景的场景是: 当 CPU 访问了未建立页表的文件映射内存,MMU 检查到物理内存不存在即触发缺页异常. 但在 Linux 里还有很多场景会导致文件映射内存发生缺页,总结包括如下场景:

  • 可读可写文件映射内存场景: 进程将文件映射到进程地址空间,且该区域可读可写可共享,当进程首次对文件映射内存发起写操作,MMU 检查到物理内存不存在而触发缺页异常,缺页异常处理函数会分配多个 PAGECACHE,并将文件内容从磁盘加载到 PAGECACHE 里,最后在建立虚拟内存到 PAGECACHE 的页表. 之后进程可以正常访问文件映射内存.
  • FAULT AROUND 场景: 进程将文件映射到进程地址空间,且该区域可读可写可共享,当进程首次对文件映射内存发起读操作,MMU 检查到物理内存不存在而触发缺页异常,缺页异常处理函数会分配多个 PAGECACHE,并将文件内容从磁盘加载到 PAGECACHE 里,FAULT AROUND 不仅会建立虚拟内存到 PAGECACHE 的页表,其一同将虚拟内存相邻的虚拟内存也建立到 PAGECACHE 的页表. 之后进程访问相邻的虚拟内存时不再会触发缺页直接访问.
  • COW/FORK 场景: 当进程将文件映射到进程地址空间,且该区域可读可写但私有,如果进程发起写操作时,如果虚拟内存还没有建立页表,那么 MMU 会发起缺页异常,然后缺页异常处理函数分配 PAGECACHE,并从磁盘加载文件到 PAGECACHE 里,接下来缺页异常处理函数新分配一个匿名页,并将 PAGECACHE 内容直接拷贝到匿名页里,最后建立虚拟内存到匿名页的页表; 反之虚拟内存的页表已经建立,那么 MMU 检查到权限异常触发缺页异常,缺页异常处理函数新分配一个匿名页,然后将 PAGECACHE 内容拷贝到匿名页,最后将虚拟内存页表更新映射到匿名页上. 最后进程对文件映射的读写内容都落在匿名页上
  • DROP CACHE 场景: 当进程将文件映射到进程地址空间,并建立映射之后可以对内存进行读写操作,但由于内存压力系统执行 DROP CACHE 操作将不常使用的 PAGECACHE 进行回收,被回收的 PAGECACHE 会将内容更新到磁盘上并释放物理内存。当进程再次访问内存时,由于 PAGECACHE 不存在而再次触发缺页异常,缺页异常处理函数再次分配 PAGECAHCE 并从磁盘读取内容到 PAGECACHE,最后建立页表映射到 PAGECACHE,那么进程可以继续访问内存.
  • DAX 场景: 当进程将支持 DAX 的文件映射到进程地址空间,进程访问这段内存时,MMU 发现物理内存不存在而触发缺页异常,在缺页异常处理函数中,文件系统提供 DAX 相关的操作,让页表直接建立到 PFN 上而不使用 PAGECACHE,待缺页异常处理返回之后,那么进程可以正常访问内存.

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

对 Linux 支持的部分文件系统支持的缺页模型进行了统计,其中对于可读可写文件系统支持 FAULT-AROUND 和 COW,对于只读文件系统只支持 FAULT-AROUND,而对于 XFS 或者 EXT4 支持 DAX 缺页模式.

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


EXT4 文件系统缺页场景

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 虚拟内存区域时,会触发缺页异常构造这些逻辑。那么接下来通过一个实践案例了解这种异常场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

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

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

实践案例由一个应用程序构成,进程在 23 行在 “/mnt/ext4/” 目录下打开文件 BiscuitOS.txt 文件,该目录已经挂载为 EXT4 文件系统,进程接着在 29 行调用 mmap 函数将文件映射到进程的地址空间,并在 40 行对文件对应的虚拟内存进行写操作,然后在 42 行对虚拟内存进行读操作,操作完毕之后就是释放虚拟内存和关闭文件. 以上便是一个最基础的实践案例,可以知道 40 行读操作就会触发缺页,为了可以看到内存在缺页异常里的流动,在 40 行前后加上 BS_DEBUG 开关:

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

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

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

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

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

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

对于 EXT4 文件系统映射文件到地址空间之后,进程访问该虚拟内存时,由于 MMU 发现对于的物理内存不存在,那么触发缺页异常。在缺页异常处理函数里,其主要做三个事情,首先是分配 PAGECACHE,如上图调用 filemap_alloc_folio 函数进行分配; 当分配 PAGECACHE 之后 EXT4 文件系统向 BIO 层发送请求从磁盘里读取读取多个页表内容到 PAGECACHE, 由于磁盘 I/O 延迟无法立即获得文件内存,因此缺页异常会通过 VM_FAULT_RETRY 再次进行缺页处理,以确保文件内容已经更新到 PAGECACHE 里; 最后一个任务就是更新页表指向新的 PAGECACHE,以及更新页表标记为脏页等. 完成三个任务之后缺页异常处理函数就返回,那么进程可以正常访问虚拟内存. 因为 VM_FAULT_RETRY 的缘故,handle_mm_fault 会被执行两次,这么做的原因有如下:

  • 磁盘 I/O 延迟: 当页面不在物理内存中,需要从磁盘加载时,可能由于磁盘 I/O 操作的延迟,内核无法立即获取所需页面。此时,内核可能会将页面标记为需要重新尝试,并等待磁盘 I/O 完成
  • 页面锁定: 在某些情况下,内核可能会锁定某些页面,以确保它们在内存中不会被交换出去或释放。如果页面被锁定,内核可能会等待解锁页面后才能重新加载
  • 其他临时情况: 还可能存在其他临时情况,导致页面无法立即加载到内存中。在这种情况下,内核可能会标记页面并尝试重新加载

VM_FAULT_RETRY 并不是 Linux 文件映射的特定特性,而是内核内存管理的一般机制。它在多种上下文中都有用,不仅限于文件映射。具体的行为可能因 Linux 内核版本、硬件配置和文件系统类型而异,因此要根据具体情况考虑如何处理 VM_FAULT_RETRY 错误码.

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

do_shared_fault 函数作为 EXT4 文件系统映射内存写操作导致缺页的核心处理函数,函数调用 __do_fault 函数分配物理内存,其细节涉及 XARRY 等映射这里不放开讲。当分配物理内存之后,文件映射内存没有提供 VMA 对应的 page_mkwrite 接口,于是直接调用 finish_fault 函数进行页表设置,最后如果页变成脏页,则调用 fault_dirty_shared_page 函数进行标脏,以上便是可读可写共享内存的缺页过程。对于读操作导致的缺页,其核心调用 do_read_fault 函数,对于共享内存来说,其逻辑与 do_shared_fault 函数无异. 另外 EXT4 文件系统提供的 VMA 缺页处理函数是 filemap_fault,该函数负责分配 PAGECACHE 和发起 BIO 请求读取文件内存.

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

EXT4 文件系统使用 ext4_readahead 函数从磁盘上读取文件内容,其核心通过 EXT4 文件系统提供的 ext4_mpage_readpages 执行实际的读取操作. 在读取的时候, EXT4 文件系统不是一个页一个页的读,而是一次性读取 ra->ra_pages 个页,另外 EXT4 文件向 BIO 层发送请求时不是按 PAGECACHE 的粒度,而是按 Buffer Head 的粒度发送请求.


EXT4 with FAULT AROUND

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

在 Linux 中,FAULT AROUND 是一种缺页处理(Page Fault Handling) 的优化策略,旨在改善内存访问性能,特别是在具有局部性的内存访问模式下。当一个进程访问文件映射的内存中的某个地址时,如果所需的页面不在物理内存中,就会发生缺页(Page Fault),这需要将缺失的页面从磁盘加载到物理内存中,然后允许进程继续执行。FAULT AROUND 策略的目标是减少未来可能的缺页次数,通过在页面访问周围预取(Prefetch) 附近的页面来实现这一目标. 在许多情况下,应用程序访问的数据在空间上具有局部性,即近期访问的数据很可能在不久的将来再次被访问。因此预取附近的页面有助于利用局部性,减少未来的缺页. 接下来通过一个实践案例了解该场景,实践案例在 BiscuitOS 上的部署逻辑如下:

cd BiscuitOS
make menuconfig

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

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

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

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

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

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

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

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

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

对于 EXT4 文件系统映射文件到地址空间之后,进程对虚拟内存发起读请求时,由于 MMU 发现对于的物理内存不存在,那么触发缺页异常。在缺页异常处理函数里,其主要做三个事情,首先是分配 PAGECACHE,如上图调用 filemap_alloc_folio 函数进行分配,EXT4 文件系统支持一次性从磁盘读取多个文件内容到 PAGECACHE,因此这里会分配多个 PAGECACHE; 当分配 PAGECACHE 之后 EXT4 文件系统向 BIO 层发送请求从磁盘里读取读取多个页表内容到 PAGECACHE, 由于磁盘 I/O 延迟无法立即获得文件内存,因此缺页异常会通过 VM_FAULT_RETRY 再次进行缺页处理,以确保文件内容已经更新到 PAGECACHE 里; 最后一个任务就是 EXT4 文件系统会执行 FAULT AROUND 操作,将此时缺页相邻的虚拟内存一同建立页表,那么进程接下来对相邻虚拟内存的访问不会引起缺页,因此调用 do_fault_around 函数完成 FAULT AROUND.

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

do_read_fault 函数作为 EXT4 文件系统映射内存读操作导致缺页的核心处理函数,与 do_shared_fault 写缺页不同的是,其在 4522-4526 行提供了 do_fault_around 函数,该函数在文件映射的内存发生缺页时用于一种优化策略,其作用是预取(prefetch)附近的页面,以减少未来可能的缺页。这个优化策略基于以下假设:

  • 文件访问通常具有局部性(locality): 即如果一个页面被访问,那么附近的页面可能在不久的将来也会被访问。这是因为文件通常以较小的块进行读取,而不是逐个字节或逐个页面
  • 文件映射的内存通常是按顺序或近似顺序访问的

EXT4 文件系统对于 FAULT-AROUND 机制提供了 filemap_map_pages(vmf->vma->v-m_ops->map_pages) 函数来预读文件, 该函数是通用的文件预读操作,其主要任务就是未相邻的虚拟内存提前建立映射到相应 PAGECACHE 的页表,注意这里不是从磁盘读取内容,而是建立页表. FAULT AROUND 直接返回 VM_FAULT_NOPAGE,那么缺页异常函数可以直接完成. 接下来进程访问相邻的虚拟内存不会发生缺页.


EXT4 with COW

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

COW(Copy-on-Write)机制是一种内存管理和数据复制策略,通常用于优化资源的利用和提高性能。COW 机制的核心思想是只有在需要修改资源时才进行复制,而在读取或共享资源时,可以共享原始资源,从而节省了内存和计算资源。在 Linux 中,COW 机制主要应用于以下几个方面:

  • 进程复制: 当一个进程通过 fork 系统调用创建子进程时,子进程会继承父进程的内存映像,包括虚拟内存地址空间和内存页面。在这种情况下,COW 机制确保子进程和父进程共享相同的物理内存页面,只有当其中一个进程尝试修改页面中的内容时,才会复制页面并使其成为独立的,从而保持进程间的内存隔离
  • 共享库和可执行文件: 多个进程可以共享相同的共享库和可执行文件的内存映像,而不是为每个进程都复制一份相同的代码和数据。只有在某个进程尝试修改共享库或可执行文件中的内容时,才会使用 COW 机制来创建一个副本
  • 写时复制文件系统: 一些文件系统,如 ZFS 和 Btrfs,使用 COW 机制来实现文件系统快照和版本控制。当文件被修改时,文件系统不会立即在磁盘上复制整个文件,而是创建一个新的数据块并将修改写入新块中。这减少了存储空间的浪费并提高了效率
  • 虚拟机管理: 虚拟化环境中,COW 机制通常用于创建虚拟机的快照。初始时,快照与原始虚拟机共享相同的虚拟硬盘,但当虚拟机修改数据时,只有已修改的数据块才会被复制到新的快照中,从而节省存储空间

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

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

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

测试用例由一个用户程序构成,程序首先在 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 场景进行处理,此时该虚拟内存还是会被设置为写保护。

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

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

BiscuitOS 内存管理之分页大专题订阅入口

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