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

cd BiscuitOS
make menuconfig

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

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

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

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

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

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

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

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

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

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

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

对于 EXT3 文件系统映射文件到地址空间之后,进程访问该虚拟内存时,由于 MMU 发现对于的物理内存不存在,那么触发缺页异常。在缺页异常处理函数里,其主要做三个事情,首先是分配 PAGECACHE,如上图调用 filemap_alloc_folio 函数进行分配; 当分配 PAGECACHE 之后 EXT3 文件系统向 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 函数作为 EXT3 文件系统映射内存写操作导致缺页的核心处理函数,函数调用 __do_fault 函数分配物理内存,其细节涉及 XARRY 等映射这里不放开讲。当分配物理内存之后,文件映射内存没有提供 VMA 对应的 page_mkwrite 接口,于是直接调用 finish_fault 函数进行页表设置,最后如果页变成脏页,则调用 fault_dirty_shared_page 函数进行标脏,以上便是可读可写共享内存的缺页过程。对于读操作导致的缺页,其核心调用 do_read_fault 函数,对于共享内存来说,其逻辑与 do_shared_fault 函数无异. 另外 EXT3 文件系统提供的 VMA 缺页处理函数是 filemap_fault,该函数负责分配 PAGECACHE 和发起 BIO 请求读取文件内存.

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

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


EXT3 with FAULT AROUND

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

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

cd BiscuitOS
make menuconfig

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


EXT3 with COW

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

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

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

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 场景进行处理,此时该虚拟内存还是会被设置为写保护。

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