目录

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


文件映射: 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 缺页模式.

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