什么是 Flash 友好文件系统 (F2FS)?

基于 NAND 闪存的存储设备,例如 SSD、eMMC 和 SD 卡,已配备在从移动设备到服务器系统的各种系统中。由于已知它们与传统的旋转磁盘具有不同的特性,因此文件系统(存储设备的上层)应在设计层面从草图开始适应这些变化。

F2FS 是一种利用基于 NAND 闪存的存储设备的文件系统,它基于日志结构文件系统 (LFS)。该设计主要关注解决 LFS 中的基本问题,即游走树的雪球效应和高清理开销。

由于基于 NAND 闪存的存储设备根据其内部几何结构或闪存管理方案(即 FTL)显示出不同的特性,因此 F2FS 及其工具支持各种参数,不仅可以配置磁盘布局,还可以选择分配和清理算法。

以下 git 树提供了文件系统格式化工具 (mkfs.f2fs)、一致性检查工具 (fsck.f2fs) 和调试工具 (dump.f2fs)。

  • git://git.kernel.org/pub/scm/linux/kernel/git/jaegeuk/f2fs-tools.git

对于发送补丁,请使用以下邮件列表

对于报告错误,请使用以下 f2fs 错误跟踪器链接

背景和设计问题

日志结构文件系统 (LFS)

“日志结构文件系统以日志状结构将所有修改顺序写入磁盘,从而加快文件写入和崩溃恢复的速度。该日志是磁盘上唯一的结构;它包含索引信息,以便可以有效地从日志中读取文件。为了在磁盘上维护大的可用区域以进行快速写入,我们将日志划分为段,并使用段清理器来压缩来自高度碎片化段的实时信息。” 来自 Rosenblum, M. 和 Ousterhout, J. K.,1992,“日志结构文件系统的设计和实现”,ACM Trans。计算机系统 10, 1, 26–52。

游走树问题

在 LFS 中,当文件数据被更新并写入日志末尾时,其直接指针块会因位置更改而更新。然后,间接指针块也会因直接指针块更新而更新。以这种方式,诸如 inode、inode 映射和检查点块之类的上层索引结构也会被递归更新。这个问题被称为游走树问题 [1],为了提高性能,应该尽可能消除或放宽更新传播。

[1] Bityutskiy, A. 2005。JFFS3 设计问题。 http://www.linux-mtd.infradead.org/

清理开销

由于 LFS 基于异地写入,因此会产生许多分散在整个存储中的过时块。为了服务新的空日志空间,它需要无缝地向用户回收这些过时块。这项工作称为清理过程。

该过程包括以下三个操作。

  1. 通过引用段使用表来选择受害者段。

  2. 它加载段摘要块标识的受害者中所有数据的父索引结构。

  3. 它检查数据及其父索引结构之间的交叉引用。

  4. 它选择性地移动有效数据。

此清理工作可能会导致意外的长时间延迟,因此最重要的目标是向用户隐藏延迟。而且,绝对应该减少要移动的有效数据量,并尽快移动它们。

主要特性

闪存感知

  • 扩大随机写入区域以获得更好的性能,但提供高空间局部性

  • 尽最大努力将 FS 数据结构与 FTL 中的操作单元对齐

游走树问题

  • 使用术语“节点”表示 inode 以及各种指针块

  • 引入节点地址表 (NAT),其中包含所有“节点”块的位置;这将切断更新传播。

清理开销

  • 支持后台清理过程

  • 支持用于受害者选择策略的贪婪和成本效益算法

  • 支持用于静态/动态冷热数据分离的多头日志

  • 引入自适应日志记录以实现高效的块分配

挂载选项

background_gc=%s

打开/关闭清理操作,即垃圾回收,当 I/O 子系统空闲时在后台触发。如果 background_gc=on,它将打开垃圾回收,如果 background_gc=off,垃圾回收将被关闭。如果 background_gc=sync,它将打开在后台运行的同步垃圾回收。此选项的默认值为 on。因此,默认情况下垃圾回收是开启的。

gc_merge

当 background_gc 打开时,可以启用此选项,让后台 GC 线程处理前台 GC 请求,它可以消除由于 GC 从 I/O 和 CPU 资源有限的进程触发时慢速前台 GC 操作导致的迟缓问题。

nogc_merge

禁用 GC 合并功能。

disable_roll_forward

禁用前滚恢复例程

norecovery

禁用前滚恢复例程,以只读方式挂载(即,-o ro,disable_roll_forward)

discard/nodiscard

在 f2fs 中启用/禁用实时丢弃,如果启用丢弃,f2fs 将在清理段时发出丢弃/TRIM 命令。

heap/no_heap

已弃用。

nouser_xattr

禁用扩展用户属性。注意:如果选择了 CONFIG_F2FS_FS_XATTR,则默认启用 xattr。

noacl

禁用 POSIX 访问控制列表。注意:如果选择了 CONFIG_F2FS_FS_POSIX_ACL,则默认启用 acl。

active_logs=%u

支持配置活动日志的数量。在当前设计中,f2fs 仅支持 2、4 和 6 个日志。默认数量为 6。

disable_ext_identify

禁用 mkfs 配置的扩展列表,因此 f2fs 不会意识到冷文件(如媒体文件)。

inline_xattr

启用内联 xattr 功能。

noinline_xattr

禁用内联 xattr 功能。

inline_xattr_size=%u

支持配置内联 xattr 大小,它取决于灵活的内联 xattr 功能。

inline_data

启用内联数据功能:新创建的小(<~3.4k)文件可以写入 inode 块。

inline_dentry

启用内联目录功能:新创建的目录条目中的数据可以写入 inode 块。用于存储内联目录条目的 inode 块空间限制为 ~3.4k。

noinline_dentry

禁用内联目录条目功能。

flush_merge

尽可能合并并发的 cache_flush 命令,以消除冗余命令问题。如果底层设备处理 cache_flush 命令的速度相对较慢,建议启用此选项。

nobarrier

如果底层存储保证其缓存的数据应写入非易失区域,则可以使用此选项。如果设置此选项,则不会发出 cache_flush 命令,但 f2fs 仍然保证所有数据写入的写入顺序。

barrier

如果设置此选项,则允许发出 cache_flush 命令。

fastboot

当系统希望尽可能减少挂载时间时,即使可以牺牲正常性能,也可以使用此选项。

extent_cache

启用基于 rb 树的范围缓存,它可以缓存每个 inode 中尽可能多的在连续逻辑地址和物理地址之间映射的范围,从而提高缓存命中率。默认设置。

noextent_cache

显式禁用基于 rb 树的范围缓存,请参阅上面的 extent_cache 挂载选项。

noinline_data

禁用内联数据功能,默认情况下启用内联数据功能。

data_flush

在检查点之前启用数据刷新,以便持久化常规和符号链接的数据。

reserve_root=%d

支持配置保留空间,该空间用于指定 uid 或 gid 的特权用户的分配,单位:4KB,默认限制为用户块的 0.2%。

resuid=%d

可以使用保留块的用户 ID。

resgid=%d

可以使用保留块的组 ID。

fault_injection=%d

以指定的注入率在所有支持的类型中启用故障注入。

fault_type=%d

支持配置故障注入类型,应与 fault_injection 选项一起启用,故障类型值如下所示,它支持单个或组合类型。

类型名称

类型值

FAULT_KMALLOC

0x000000001

FAULT_KVMALLOC

0x000000002

FAULT_PAGE_ALLOC

0x000000004

FAULT_PAGE_GET

0x000000008

FAULT_ALLOC_BIO

0x000000010(已过时)

FAULT_ALLOC_NID

0x000000020

FAULT_ORPHAN

0x000000040

FAULT_BLOCK

0x000000080

FAULT_DIR_DEPTH

0x000000100

FAULT_EVICT_INODE

0x000000200

FAULT_TRUNCATE

0x000000400

FAULT_READ_IO

0x000000800

FAULT_CHECKPOINT

0x000001000

FAULT_DISCARD

0x000002000

FAULT_WRITE_IO

0x000004000

FAULT_SLAB_ALLOC

0x000008000

FAULT_DQUOT_INIT

0x000010000

FAULT_LOCK_OP

0x000020000

FAULT_BLKADDR_VALIDITY

0x000040000

FAULT_BLKADDR_CONSISTENCE

0x000080000

FAULT_NO_SEGMENT

0x000100000

mode=%s

控制块分配模式,支持“adaptive”和“lfs”。在“lfs”模式下,不应向主区域进行随机写入。“fragment:segment”和“fragment:block”是此处新增的。这些是供开发者进行实验以模拟文件系统碎片/GC后情况的选项。开发者可以使用这些模式来很好地了解文件系统碎片/GC后的状况,并最终获得一些见解以更好地处理它们。在“fragment:segment”模式下,f2fs会在随机位置分配一个新的段。通过这种方式,我们可以模拟GC后的情况。在“fragment:block”模式下,我们可以使用“max_fragment_chunk”和“max_fragment_hole” sysfs 节点来分散块分配。我们为块和空洞的大小都添加了一些随机性,使其更接近真实的 IO 模式。因此,在此模式下,f2fs 将以轮流方式分配 1..<max_fragment_chunk> 个块的块,并创建一个长度为 1..<max_fragment_hole> 的空洞。这样,新分配的块将分散在整个分区中。请注意,“fragment:block”会隐式启用“fragment:segment”选项以获得更多的随机性。请将这些选项用于您的实验,我们强烈建议在使用这些选项后重新格式化文件系统。

usrquota

启用普通用户磁盘配额记帐。

grpquota

启用普通组磁盘配额记帐。

prjquota

启用普通项目配额记帐。

usrjquota=<文件>

在挂载期间指定文件和类型,以便配额

grpjquota=<文件>

信息可以在恢复流程中正确更新,

prjjquota=<文件>

<配额文件>:必须位于根目录中;

jqfmt=<配额类型>

<配额类型>:[vfsold,vfsv0,vfsv1]。

offusrjquota

关闭用户日志配额。

offgrpjquota

关闭组日志配额。

offprjjquota

关闭项目日志配额。

quota

启用普通用户磁盘配额记帐。

noquota

禁用所有普通磁盘配额选项。

alloc_mode=%s

调整块分配策略,支持“reuse”和“default”。

fsync_mode=%s

控制 fsync 的策略。目前支持“posix”、“strict”和“nobarrier”。在默认的“posix”模式下,fsync 将遵循 POSIX 语义,并执行轻量级操作以提高文件系统性能。在“strict”模式下,fsync 将是重量级的,其行为与 xfs、ext4 和 btrfs 一致,其中 xfstest generic/342 将通过,但性能将下降。“nobarrier”基于“posix”,但不会像“nobarrier”挂载选项那样为非原子文件发出刷新命令。

test_dummy_encryption

test_dummy_encryption=%s

启用虚拟加密,它提供一个伪造的 fscrypt 上下文。伪造的 fscrypt 上下文由 xfstests 使用。参数可以是“v1”或“v2”,以选择相应的 fscrypt 策略版本。

checkpoint=%s[:%u[%]]

设置为“disable”以关闭检查点。设置为“enable”以重新启用检查点。默认情况下启用。禁用时,任何卸载或意外关闭都会导致文件系统内容显示为使用该选项挂载文件系统时的状态。使用 checkpoint=disable 挂载时,文件系统必须运行垃圾回收以确保可以使用所有可用空间。如果这花费太长时间,则挂载可能会返回 EAGAIN。您可以选择添加一个值,以指示您愿意临时放弃多少磁盘空间以避免额外的垃圾回收。这可以以块数或百分比给出。例如,使用 checkpoint=disable:100% 挂载将始终成功,但它可能会隐藏最多所有剩余可用空间。无法使用的实际空间可以在 /sys/fs/f2fs/<disk>/unusable 中查看。一旦 checkpoint=enable,此空间将被回收。

checkpoint_merge

启用检查点时,可以使用此选项创建一个内核守护进程,并使其尽可能合并并发的检查点请求,以消除冗余的检查点问题。此外,我们可以消除在具有低 I/O 预算和 CPU 份额的 cgroup 的进程上下文中完成检查点时,由于检查点操作缓慢而导致的迟缓问题。为了更好地完成此操作,我们将内核守护进程的默认 I/O 优先级设置为“3”,使其优先级高于其他内核线程。这与为 ext4 文件系统的 jbd2 日志线程赋予 I/O 优先级的方式相同。

nocheckpoint_merge

禁用检查点合并功能。

compress_algorithm=%s

控制压缩算法,目前 f2fs 支持“lzo”、“lz4”、“zstd”和“lzo-rle”算法。

compress_algorithm=%s:%d

控制压缩算法及其压缩级别,现在只有“lz4”和“zstd”支持压缩级别配置。算法级别范围 lz4 3 - 16 zstd 1 - 22

compress_log_size=%u

支持配置压缩簇大小。大小将为 4KB * (1 << %u)。默认和最小大小为 16KB。

compress_extension=%s

支持添加指定的扩展名,以便 f2fs 可以在那些相应的文件上启用压缩,例如,如果所有带有“.ext”的文件都具有高压缩率,我们可以在压缩扩展名列表中设置“.ext”,并默认在这些文件上启用压缩,而不是通过 ioctl 启用。对于其他文件,我们仍然可以通过 ioctl 启用压缩。请注意,有一个保留的特殊扩展名“*”,可以将其设置为启用所有文件的压缩。

nocompress_extension=%s

支持添加指定的扩展名,以便 f2fs 可以在那些相应的文件上禁用压缩,这与压缩扩展名相反。如果您确切知道哪些文件无法压缩,则可以使用此选项。相同的扩展名不能同时出现在压缩和非压缩扩展名中。如果压缩扩展名指定所有文件,则非压缩扩展名指定的类型将被视为特殊情况,并且不会被压缩。不允许使用“*”来在非压缩扩展名中指定所有文件。添加非压缩扩展名后,优先级应为:dir_flag < comp_extention,nocompress_extension < comp_file_flag,no_comp_file_flag。有关更多信息,请参见压缩部分。

compress_chksum

支持验证压缩簇中原始数据的校验和。

compress_mode=%s

控制文件压缩模式。这支持“fs”和“user”模式。在“fs”模式(默认)下,f2fs 对启用了压缩的文件执行自动压缩。在“user”模式下,f2fs 禁用自动压缩,并由用户自行决定选择目标文件和时间。用户可以使用 ioctl 对启用了压缩的文件执行手动压缩/解压缩。

compress_cache

支持使用文件系统管理的 inode 的地址空间来缓存压缩块,以提高随机读取的缓存命中率。

inlinecrypt

在可能的情况下,使用 blk-crypto 框架而不是文件系统层加密来加密/解密加密文件的内容。这允许使用内联加密硬件。磁盘上的格式不受影响。有关更多详细信息,请参见内联加密

atgc

启用基于年龄阈值的垃圾回收,它为后台 GC 提供高效率和高效率。

discard_unit=%s

控制丢弃单元,参数可以是“block”、“segment”和“section”,发出的丢弃命令的偏移量/大小将与该单元对齐,默认情况下,设置“discard_unit=block”,以便启用小丢弃功能。对于 blkzoned 设备,默认情况下将设置“discard_unit=section”,这对于大型 SMR 或 ZNS 设备很有帮助,可以通过摆脱 fs 元数据对小丢弃的支持来降低内存成本。

memory=%s

控制内存模式。这支持“normal”和“low”模式。“low”模式是为了支持低内存设备而引入的。由于低内存设备的性质,在此模式下,f2fs 将有时尝试通过牺牲性能来节省内存。“normal”模式是默认模式,与以前相同。

age_extent_cache

启用基于 rb 树的年龄范围缓存。它记录每个 inode 的范围的数据块更新频率,以便为数据块分配提供更好的温度提示。

errors=%s

指定 f2fs 在关键错误时的行为。这支持以下模式:“panic”、“continue”和“remount-ro”,分别表示立即触发恐慌,继续而不执行任何操作,以及以只读模式重新挂载分区。默认情况下,它使用“continue”模式。====================== =============== =============== ======== 模式 continue remount-ro panic ====================== =============== =============== ======== 访问操作 normal normal N/A 系统调用错误 -EIO -EROFS N/A 挂载选项 rw ro N/A 待处理的目录写入 keep keep N/A 待处理的非目录写入 drop keep N/A 待处理的节点写入 drop keep N/A 待处理的元数据写入 keep keep N/A ====================== =============== =============== ========

Debugfs 条目

/sys/kernel/debug/f2fs/ 包含有关所有以 f2fs 挂载的分区的信息。每个文件都显示整个 f2fs 信息。

/sys/kernel/debug/f2fs/status 包括

  • 当前由 f2fs 管理的主要文件系统信息

  • 有关整个段的平均 SIT 信息

  • f2fs 当前消耗的内存占用空间。

Sysfs 条目

有关已挂载的 f2fs 文件系统的信息可以在 /sys/fs/f2fs 中找到。每个挂载的文件系统都将在 /sys/fs/f2fs 中有一个目录,该目录基于其设备名称(即,/sys/fs/f2fs/sda)。每个设备目录中的文件如下表所示。

/sys/fs/f2fs/<devname> 中的文件(另请参阅 Documentation/ABI/testing/sysfs-fs-f2fs)

用法

  1. 下载用户空间工具并进行编译。

  2. 如果 f2fs 是在内核内部静态编译的,则跳过此步骤。否则,插入 f2fs.ko 模块

    # insmod f2fs.ko
    
  3. 创建一个在挂载时使用的目录

    # mkdir /mnt/f2fs
    
  4. 格式化块设备,然后将其挂载为 f2fs

    # mkfs.f2fs -l label /dev/block_device
    # mount -t f2fs /dev/block_device /mnt/f2fs
    

mkfs.f2fs

mkfs.f2fs 用于将分区格式化为 f2fs 文件系统,该文件系统构建基本的磁盘布局。

快速选项包括

-l [标签]

提供卷标,最多 512 个 Unicode 名称。

-a [0 1]

分割每个区域的起始位置以进行基于堆的分配。

默认情况下设置为 1,这将执行此操作。

-o [整数]

设置卷大小的过度配置比例,以百分比表示。

默认设置为 5。

-s [int]

设置每个分段的段数。

默认设置为 1。

-z [int]

设置每个区域的分段数。

默认设置为 1。

-e [str]

设置基本扩展名列表。 例如:“mp3,gif,mov”

-t [0 1]

禁用 discard 命令或不禁用。

默认设置为 1,即执行 discard。

注意:请参考 mkfs.f2fs(8) 的手册页以获取完整的选项列表。

fsck.f2fs

fsck.f2fs 是一个用于检查 f2fs 格式化分区一致性的工具,它会检查文件系统元数据和用户生成的数据是否正确交叉引用。请注意,该工具的初始版本不会修复任何不一致性。

快速选项包括

-d debug level [default:0]

注意:请参考 fsck.f2fs(8) 的手册页以获取完整的选项列表。

dump.f2fs

dump.f2fs 显示特定 inode 的信息,并将 SSA 和 SIT 转储到文件中。每个文件分别为 dump_ssa 和 dump_sit。

dump.f2fs 用于调试 f2fs 文件系统的磁盘数据结构。它显示由给定 inode 号识别的磁盘 inode 信息,并能够将所有 SSA 和 SIT 条目转储到预定义的文件中,分别为 ./dump_ssa 和 ./dump_sit。

选项包括

-d debug level [default:0]
-i inode no (hex)
-s [SIT dump segno from #1~#2 (decimal), for all 0~-1]
-a [SSA dump segno from #1~#2 (decimal), for all 0~-1]

示例

# dump.f2fs -i [ino] /dev/sdx
# dump.f2fs -s 0~-1 /dev/sdx (SIT dump)
# dump.f2fs -a 0~-1 /dev/sdx (SSA dump)

注意:请参考 dump.f2fs(8) 的手册页以获取完整的选项列表。

sload.f2fs

sload.f2fs 提供了一种在现有磁盘映像中插入文件和目录的方法。当构建给定编译文件的 f2fs 映像时,此工具非常有用。

注意:请参考 sload.f2fs(8) 的手册页以获取完整的选项列表。

resize.f2fs

resize.f2fs 允许用户调整 f2fs 格式化的磁盘映像的大小,同时保留存储在映像中的所有文件和目录。

注意:请参考 resize.f2fs(8) 的手册页以获取完整的选项列表。

defrag.f2fs

defrag.f2fs 可用于整理磁盘上分散写入的数据以及文件系统元数据。这可以通过提供更多连续的可用空间来提高写入速度。

注意:请参考 defrag.f2fs(8) 的手册页以获取完整的选项列表。

f2fs_io

f2fs_io 是一个简单的工具,用于发出各种文件系统 API 以及 f2fs 特定的 API,这对于 QA 测试非常有用。

注意:请参考 f2fs_io(8) 的手册页以获取完整的选项列表。

设计

磁盘布局

F2FS 将整个卷划分为多个段,每个段的大小固定为 2MB。一个分段由连续的段组成,一个区域由一组分段组成。默认情况下,分段和区域大小设置为相同的段大小,但用户可以通过 mkfs 轻松修改大小。

F2FS 将整个卷划分为六个区域,除了超级块之外的所有区域都由多个段组成,如下所述

                                        align with the zone size <-|
             |-> align with the segment size
 _________________________________________________________________________
|            |            |   Segment   |    Node     |   Segment  |      |
| Superblock | Checkpoint |    Info.    |   Address   |   Summary  | Main |
|    (SB)    |   (CP)     | Table (SIT) | Table (NAT) | Area (SSA) |      |
|____________|_____2______|______N______|______N______|______N_____|__N___|
                                                                   .      .
                                                         .                .
                                             .                            .
                                ._________________________________________.
                                |_Segment_|_..._|_Segment_|_..._|_Segment_|
                                .           .
                                ._________._________
                                |_section_|__...__|_
                                .            .
                                .________.
                                |__zone__|
  • 超级块 (SB)

    它位于分区的开头,并且存在两个副本以避免文件系统崩溃。它包含基本分区信息和 f2fs 的一些默认参数。

  • 检查点 (CP)

    它包含文件系统信息、有效 NAT/SIT 集的位图、孤儿 inode 列表以及当前活动段的摘要条目。

  • 段信息表 (SIT)

    它包含段信息,例如有效块计数和所有块有效性的位图。

  • 节点地址表 (NAT)

    它由存储在主区域中的所有节点块的块地址表组成。

  • 段摘要区域 (SSA)

    它包含摘要条目,其中包含存储在主区域中的所有数据和节点块的所有者信息。

  • 主区域

    它包含文件和目录数据,包括它们的索引。

为了避免文件系统和基于闪存的存储之间的不对齐,F2FS 将 CP 的起始块地址与段大小对齐。此外,它通过在 SSA 区域中保留一些段,将主区域的起始块地址与区域大小对齐。

有关其他技术细节,请参考以下调查。 https://wiki.linaro.org/WorkingGroups/Kernel/Projects/FlashCardSurvey

文件系统元数据结构

F2FS 采用检查点方案来维护文件系统一致性。在挂载时,F2FS 首先尝试通过扫描 CP 区域来查找最后有效的检查点数据。为了减少扫描时间,F2FS 仅使用两个 CP 副本。其中一个始终指示最后有效的数据,这被称为影子复制机制。除了 CP 之外,NAT 和 SIT 也采用影子复制机制。

为了确保文件系统的一致性,每个 CP 都指向哪些 NAT 和 SIT 副本有效,如下所示

+--------+----------+---------+
|   CP   |    SIT   |   NAT   |
+--------+----------+---------+
.         .          .          .
.            .              .              .
.               .                 .                 .
+-------+-------+--------+--------+--------+--------+
| CP #0 | CP #1 | SIT #0 | SIT #1 | NAT #0 | NAT #1 |
+-------+-------+--------+--------+--------+--------+
   |             ^                          ^
   |             |                          |
   `----------------------------------------'

索引结构

管理数据位置的关键数据结构是“节点”。与传统文件结构类似,F2FS 有三种类型的节点:inode、直接节点、间接节点。F2FS 为 inode 块分配 4KB,其中包含 923 个数据块索引、两个直接节点指针、两个间接节点指针和一个双重间接节点指针,如下所述。一个直接节点块包含 1018 个数据块,一个间接节点块也包含 1018 个节点块。因此,一个 inode 块(即一个文件)覆盖

4KB * (923 + 2 * 1018 + 2 * 1018 * 1018 + 1018 * 1018 * 1018) := 3.94TB.

 Inode block (4KB)
   |- data (923)
   |- direct node (2)
   |          `- data (1018)
   |- indirect node (2)
   |            `- direct node (1018)
   |                       `- data (1018)
   `- double indirect node (1)
                       `- indirect node (1018)
                                    `- direct node (1018)
                                               `- data (1018)

请注意,所有节点块都由 NAT 映射,这意味着每个节点的位置都由 NAT 表转换。考虑到游荡树问题,F2FS 能够切断由叶数据写入引起的节点更新的传播。

目录结构

一个目录项占用 11 个字节,其中包含以下属性。

  • hash 文件名的哈希值

  • ino inode 号

  • len 文件名的长度

  • type 文件类型,例如目录、符号链接等

一个 dentry 块由 214 个 dentry 插槽和文件名组成。其中,使用位图来表示每个 dentry 是否有效。一个 dentry 块占用 4KB,其组成如下。

Dentry Block(4 K) = bitmap (27 bytes) + reserved (3 bytes) +
                    dentries(11 * 214 bytes) + file name (8 * 214 bytes)

                       [Bucket]
           +--------------------------------+
           |dentry block 1 | dentry block 2 |
           +--------------------------------+
           .               .
     .                             .
.       [Dentry Block Structure: 4KB]       .
+--------+----------+----------+------------+
| bitmap | reserved | dentries | file names |
+--------+----------+----------+------------+
[Dentry Block: 4KB] .   .
               .               .
          .                          .
          +------+------+-----+------+
          | hash | ino  | len | type |
          +------+------+-----+------+
          [Dentry Structure: 11 bytes]

F2FS 为目录结构实现多级哈希表。每个级别都有一个哈希表,其中包含专用数量的哈希桶,如下所示。请注意,“A(2B)”表示一个桶包含 2 个数据块。

----------------------
A : bucket
B : block
N : MAX_DIR_HASH_DEPTH
----------------------

level #0   | A(2B)
        |
level #1   | A(2B) - A(2B)
        |
level #2   | A(2B) - A(2B) - A(2B) - A(2B)
    .     |   .       .       .       .
level #N/2 | A(2B) - A(2B) - A(2B) - A(2B) - A(2B) - ... - A(2B)
    .     |   .       .       .       .
level #N   | A(4B) - A(4B) - A(4B) - A(4B) - A(4B) - ... - A(4B)

块和桶的数量由以下因素确定

                          ,- 2, if n < MAX_DIR_HASH_DEPTH / 2,
# of blocks in level #n = |
                          `- 4, Otherwise

                           ,- 2^(n + dir_level),
                           |        if n + dir_level < MAX_DIR_HASH_DEPTH / 2,
# of buckets in level #n = |
                           `- 2^((MAX_DIR_HASH_DEPTH / 2) - 1),
                                    Otherwise

当 F2FS 在目录中查找文件名时,首先会计算文件名的哈希值。然后,F2FS 扫描级别 #0 中的哈希表,以查找包含文件名及其 inode 号的 dentry。如果未找到,则 F2FS 扫描级别 #1 中的下一个哈希表。通过这种方式,F2FS 从 1 到 N 逐步扫描每个级别的哈希表。在每个级别中,F2FS 只需要扫描由以下等式确定的一个桶,这显示了 O(log(# 文件数)) 的复杂度

bucket number to scan in level #n = (hash value) % (# of buckets in level #n)

在文件创建的情况下,F2FS 会查找覆盖文件名的空连续插槽。F2FS 在从 1 到 N 的整个级别的哈希表中搜索空插槽,方式与查找操作相同。

下图显示了两个包含子项的示例

    --------------> Dir <--------------
    |                                 |
 child                             child

 child - child                     [hole] - child

 child - child - child             [hole] - [hole] - child

Case 1:                           Case 2:
Number of children = 6,           Number of children = 3,
File size = 7                     File size = 7

默认块分配

在运行时,F2FS 在“主”区域内管理六个活动日志:热/温/冷节点和热/温/冷数据。

  • 热节点包含目录的直接节点块。

  • 温节点包含除热节点块之外的直接节点块。

  • 冷节点包含间接节点块

  • 热数据包含 dentry 块

  • 温数据包含除热数据和冷数据块之外的数据块

  • 冷数据包含多媒体数据或迁移的数据块

LFS 有两种用于空闲空间管理的方案:线程日志和复制和压缩。复制和压缩方案(称为清理)非常适合显示出色的顺序写入性能的设备,因为始终提供可用段来写入新数据。但是,它在高利用率下会遇到清理开销。相反,线程日志方案会遇到随机写入,但不需要清理过程。F2FS 采用混合方案,其中默认采用复制和压缩方案,但会根据文件系统状态动态更改为线程日志方案。

为了使 F2FS 与底层基于闪存的存储保持一致,F2FS 以分段为单位分配段。F2FS 期望分段大小与 FTL 中的垃圾回收的单位大小相同。此外,对于 FTL 中的映射粒度,F2FS 尽可能从不同的区域分配活动日志的每个分段,因为 FTL 可以根据其映射粒度将活动日志中的数据写入一个分配单元。

清理过程

F2FS 根据需要和在后台进行清理。当没有足够的可用段来服务 VFS 调用时,会触发按需清理。后台清理器由内核线程操作,并在系统空闲时触发清理作业。

F2FS 支持两种受害者选择策略:贪婪算法和成本效益算法。在贪婪算法中,F2FS 选择具有最少有效块的受害者段。在成本效益算法中,F2FS 根据段的年龄和有效块的数量选择受害者段,以解决贪婪算法中的日志块抖动问题。F2FS 对按需清理器采用贪婪算法,而后台清理器采用成本效益算法。

为了识别受害者段中的数据是否有效,F2FS 管理一个位图。每个位代表一个块的有效性,并且该位图由覆盖主区域中所有块的位流组成。

写入提示策略

F2FS 始终使用以下策略设置 whint。

用户

F2FS

块设备

N/A

META

WRITE_LIFE_NONE|REQ_META

N/A

HOT_NODE

WRITE_LIFE_NONE

N/A

WARM_NODE

WRITE_LIFE_MEDIUM

N/A

COLD_NODE

WRITE_LIFE_LONG

ioctl(COLD)

COLD_DATA

WRITE_LIFE_EXTREME

扩展列表

-- 缓冲 I/O

N/A

COLD_DATA

WRITE_LIFE_EXTREME

N/A

HOT_DATA

WRITE_LIFE_SHORT

N/A

WARM_DATA

WRITE_LIFE_NOT_SET

-- 直接 I/O

WRITE_LIFE_EXTREME

COLD_DATA

WRITE_LIFE_EXTREME

WRITE_LIFE_SHORT

HOT_DATA

WRITE_LIFE_SHORT

WRITE_LIFE_NOT_SET

WARM_DATA

WRITE_LIFE_NOT_SET

WRITE_LIFE_NONE

WRITE_LIFE_NONE

WRITE_LIFE_MEDIUM

WRITE_LIFE_MEDIUM

WRITE_LIFE_LONG

WRITE_LIFE_LONG

Fallocate(2) 策略

默认策略遵循以下 POSIX 规则。

分配磁盘空间

fallocate() 的默认操作(即,模式为零)会在 offset 和 len 指定的范围内分配磁盘空间。如果 offset+len 大于文件大小,则文件大小(由 stat(2) 报告)将会更改。在调用之前不包含数据的 offset 和 len 指定范围内的任何子区域都将初始化为零。此默认行为与 posix_fallocate(3) 库函数的行为非常相似,旨在作为实现该函数的最佳方法。

但是,一旦 F2FS 在 fallocate(fd, DEFAULT_MODE) 之前收到 ioctl(fd, F2FS_IOC_SET_PIN_FILE),它将分配具有零数据或随机数据的磁盘块地址,这对于以下场景很有用,其中

  1. create(fd)

  2. ioctl(fd, F2FS_IOC_SET_PIN_FILE)

  3. fallocate(fd, 0, 0, size)

  4. address = fibmap(fd, offset)

  5. open(blkdev)

  6. write(blkdev, address)

压缩实现

  • 新术语“簇”定义为压缩的基本单元,文件可以在逻辑上划分为多个簇。一个簇包括 4 << n (n >= 0) 个逻辑页,压缩大小也是簇大小,每个簇可以压缩或不压缩。

  • 在簇元数据布局中,使用一个特殊的块地址来指示簇是压缩的还是正常的;对于压缩簇,以下元数据将簇映射到 [1, 4 << n - 1] 个物理块,其中 f2fs 存储包括压缩标头和压缩数据的数据。

  • 为了消除覆盖期间的写放大,F2FS 仅支持一次写入文件的压缩,只有当簇中的所有逻辑块都包含有效数据并且簇数据的压缩比低于指定阈值时,才能压缩数据。

  • 要在常规 inode 上启用压缩,有四种方法

    • chattr +c 文件

    • chattr +c 目录;touch 目录/文件

    • 挂载时使用 -o compress_extension=ext;touch 文件.ext

    • 挂载时使用 -o compress_extension=*;touch 任何文件

  • 要在常规 inode 上禁用压缩,有两种方法

    • chattr -c 文件

    • 挂载时使用 -o nocompress_extension=ext;touch 文件.ext

  • FS_COMPR_FL、FS_NOCOMP_FS 和扩展之间的优先级

    • compress_extension=so; nocompress_extension=zip; chattr +c 目录; touch 目录/foo.so; touch 目录/bar.zip; touch 目录/baz.txt; 那么 foo.so 和 baz.txt 应该被压缩,bar.zip 应该是不压缩的。chattr +c 目录/bar.zip 可以启用对 bar.zip 的压缩。

    • compress_extension=so; nocompress_extension=zip; chattr -c 目录; touch 目录/foo.so; touch 目录/bar.zip; touch 目录/baz.txt; 那么 foo.so 应该被压缩,bar.zip 和 baz.txt 应该是不压缩的。chattr+c 目录/bar.zip; chattr+c 目录/baz.txt; 可以启用对 bar.zip 和 baz.txt 的压缩。

  • 此时,压缩功能不会直接向用户公开压缩空间,以保证以后对该空间进行潜在的数据更新。相反,主要目标是尽可能减少对闪存磁盘的数据写入,从而延长磁盘寿命并缓解 IO 拥塞。或者,我们添加了 ioctl(F2FS_IOC_RELEASE_COMPRESS_BLOCKS) 接口来回收压缩空间,并在将特殊标志设置为 inode 后将其显示给用户。一旦压缩空间被释放,该标志将阻止向文件写入数据,直到通过 ioctl(F2FS_IOC_RESERVE_COMPRESS_BLOCKS) 保留压缩空间或将文件大小截断为零。

压缩元数据布局

                            [Dnode Structure]
            +-----------------------------------------------+
            | cluster 1 | cluster 2 | ......... | cluster N |
            +-----------------------------------------------+
            .           .                       .           .
      .                      .                .                      .
.         Compressed Cluster       .        .        Normal Cluster            .
+----------+---------+---------+---------+  +---------+---------+---------+---------+
|compr flag| block 1 | block 2 | block 3 |  | block 1 | block 2 | block 3 | block 4 |
+----------+---------+---------+---------+  +---------+---------+---------+---------+
           .                             .
        .                                           .
    .                                                           .
    +-------------+-------------+----------+----------------------------+
    | data length | data chksum | reserved |      compressed data       |
    +-------------+-------------+----------+----------------------------+

压缩模式

f2fs 通过“compression_mode”挂载选项支持“fs”和“user”压缩模式。使用此选项,f2fs 提供了一种选择,以选择如何压缩启用了压缩的文件(有关如何在常规 inode 上启用压缩,请参阅“压缩实现”部分)。

1) compress_mode=fs 这是默认选项。f2fs 在启用压缩的文件的写回中进行自动压缩。

2) compress_mode=user 这会禁用自动压缩,并让用户自行决定选择目标文件和时间。用户可以使用 F2FS_IOC_DECOMPRESS_FILE 和 F2FS_IOC_COMPRESS_FILE ioctl 对启用压缩的文件执行手动压缩/解压缩,如下所示。

要解压缩文件,

fd = open(filename, O_WRONLY, 0); ret = ioctl(fd, F2FS_IOC_DECOMPRESS_FILE);

要压缩文件,

fd = open(filename, O_WRONLY, 0); ret = ioctl(fd, F2FS_IOC_COMPRESS_FILE);

NVMe 分区命名空间设备

  • ZNS 定义了每个分区的容量,该容量可以等于或小于分区大小。分区容量是分区中可用的块数。F2FS 检查分区容量是否小于分区大小,如果是,则在初始挂载时,任何在分区容量之后开始的段都会在空闲段位图中标记为非空闲。这些段被标记为永久使用,因此不会分配用于写入,因此不需要进行垃圾回收。如果分区容量与默认段大小 (2MB) 不对齐,则段可以在分区容量之前开始并跨越分区容量边界。这种跨越段也被视为可用段。这些段中所有超过分区容量的块都被视为不可用。

设备别名功能

f2fs 可以利用一个名为“设备别名文件”的特殊文件。此文件允许将整个存储设备映射到一个大的范围,而不是使用通常的 f2fs 节点结构。此映射区域被固定,主要用于保存空间。

本质上,此机制允许暂时保留 f2fs 区域的一部分,并由另一个文件系统或用于其他目的。一旦外部使用完成,可以删除设备别名文件,将保留的空间释放回 F2FS 供其自己使用。

<用例>

# ls /dev/vd* /dev/vdb (32GB) /dev/vdc (32GB) # mkfs.ext4 /dev/vdc # mkfs.f2fs -c /dev/vdc@vdc.file /dev/vdb # mount /dev/vdb /mnt/f2fs # ls -l /mnt/f2fs vdc.file # df -h /dev/vdb 64G 33G 32G 52% /mnt/f2fs

# mount -o loop /dev/vdc /mnt/ext4 # df -h /dev/vdb 64G 33G 32G 52% /mnt/f2fs /dev/loop7 32G 24K 30G 1% /mnt/ext4 # umount /mnt/ext4

# f2fs_io getflags /mnt/f2fs/vdc.file 获取 /mnt/f2fs/vdc.file 上的标志 ret=0, flags=nocow(pinned),immutable # f2fs_io setflags noimmutable /mnt/f2fs/vdc.file 获取 noimmutable 上的标志 ret=0, flags=800010 设置 /mnt/f2fs/vdc.file 上的标志 ret=0, flags=noimmutable # rm /mnt/f2fs/vdc.file # df -h /dev/vdb 64G 753M 64G 2% /mnt/f2fs

因此,关键思想是,用户可以对 /dev/vdc 执行任何文件操作,并在使用后回收空间,同时该空间计为 /data。这不需要修改分区大小和文件系统格式。