稀疏文件 (sparse file)
简单来说,稀疏文件就是「实际占用的磁盘空间 < 文件的声明空间」的文件。
利用稀疏文件,可以在一个 100GB 大小的磁盘中创建一个 100TB 的文件。稀疏文件中没有真正分配物理空间的地方称为「空洞(hole)」,在读取空洞时,空洞部分会为 0,在对空洞进行写入时,会为写入的内容分配实际物理空间。
- 1 创建
- 1.1 利用 fallocate
- 1.2 利用 dd
- 1.3 利用 truncate
- 1.4 利用系统调用
- 2 识别/验证
- 3 转换
- 4 实现原理
- 4.1 ext4
创建
利用 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
利用系统调用
可以直接利用 fallocate 或 truncate 系统调用来创建稀疏文件,也可以直接 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