稀疏文件 (sparse file)

简单来说,稀疏文件就是「实际占用的磁盘空间 < 文件的声明空间」的文件。

利用稀疏文件,可以在一个 100GB 大小的磁盘中创建一个 100TB 的文件。稀疏文件中没有真正分配物理空间的地方称为「空洞(hole)」,在读取空洞时,空洞部分会为 0,在对空洞进行写入时,会为写入的内容分配实际物理空间。

创建

利用 fallocate

fallocate 拥有「跳过一些长度(写入空洞)然后写入内容」的能力

利用 offset 指定要跳过的字节(跳过的部分会用空洞实现)、length 指定要真正写入(用 0 填充)的长度

例如如下的命令,就是先跳过 1TB 的内容(空洞)然后写入 1 字节 0

fallocate -o $((1024*1024*1024*1024)) -l 1 test

利用 dd

利用 dd 的 seek 可以做到「跳过一些长度(写入空洞)」

参数 seek/oseek 用于在写入时对 of 跳过一些长度写入
参数 skip/iseek 用于在读取时对 if 跳过一些长度再读取

例如如下的命令,就是创建了 1TB 空洞的稀疏文件

上面 fallocate 所创建出的文件有 1Byte 的非稀疏部分(因为 length 参数不能为 0)
而这里创建的则是完全稀疏的 1TB 文件(因为 count 参数为 0,实际上没有写入任何实际内容)

dd if=/dev/zero of=test bs=1 count=0 seek=1T

利用 truncate

利用 truncate 也可以做到创建指定大小的稀疏文件

truncate -s 1T test

利用系统调用

可以直接利用 fallocatetruncate 系统调用来创建稀疏文件,也可以直接 lseek 到指定位置然后写入

truncate 可以用于创建一个「纯的稀疏文件(不包含任何非空洞部分)」

而 lseek 则必须在 seek 以后写入一些内容(否则不会创建空洞)

识别/验证

想要识别/验证一个文件是稀疏文件,本质上是两个方法,一是去检查其声明大小实际占用磁盘的大小是否一致,另一是去检查是否存在空洞

ls -s

ls 默认展示的大小是 file size 文件的声明大小,而使用 -s (--size) 参数则会额外打印出 allocated size of each file, in blocks 即实际占用的磁盘空间

例如用我们之前的 test 文件(1T 空洞 + 1Byte 内容),执行 ls -lsd test 即可看到

1099511627777 的值是(声明大小)正确的 1024*1024*1024*1024+1 字节
4 则是代表着实际占用空间 4KB(在 ls 中,没有特殊指定 BLOCK_SIZE 时,始终使用 1KB,而不考虑实际底层文件系统占用的 block 数量)

du + ls

du 展示的大小始终是文件真实占用操作系统的大小,因此可以用 du 和 ls 的输出做比较来判断文件是否是稀疏文件

stat

可以通过阅读 stat 的返回值(程序执行输出系统调用)比较文件的声明大小和实际大小

比较方法为比较文件的声明大小(Size st_size)和实际大小(Blocks st_blocks * 512)

转换

将普通文件转换为 sparse file

将 sparse file 转换为普通文件

实现原理

sparse file 或者说 file hole 其实只是一个非强制的规范,实现在底层的文件系统中完成,并非所有文件系统都支持 hole,但是常见的 ext4、xfs、btrfs 等都支持了相关功能

  • 针对 fallocate 而言,是否使用 file hole 实现 allocate 完全由文件系统底层的 fallocate 决定

  • 针对 truncate 而言,Linux 的 VFS 就是简单的修改了 inode 中存储的 size,对于未分配的内容的读写由文件系统底层的 read write 决定

  • 针对 lseek 而言,Linux 的 VFS 也是简单转发请求给底层文件系统的 llseek,由文件系统自身决定如何实现 —— 但是通常,文件系统会将请求转发给 Linux 定义的 generic_file_llseek_size,而这个函数就是简单的将判断 seek 到的位置是否小于文件的最大大小(没错,稀疏文件也受到这个限制,但这个限制通常很大,例如 ext4 的最大文件大小为 1EB)然后设置指针到对应的位置 —— 不会做任何预留空间之类的操作 —— 也就是最终效果其实仍然由最终文件系统底层对于未分配的内容的 read write 行为决定(同上面的 truncate)

ext4

ext4 中文件的读取入口为 ext4_file_read_iter,根据打开文件时是否设置了 O_DIRECT 来判断使用直接 IO ext4_dio_read_iter 或通用的 linux 缓存的 generic_file_read_iter

对于直接 IO,会走到 iomap_dio_rw__iomap_dio_rw

 

写入入口为 ext4_file_write_iter