空洞的概念
linux 上普通文件的大小与占用空间是两个概念,前者表示文件中数据的长度,后者表示数据占用的磁盘空间,通常后者大于前者,因为需要一些额外的空间用来记录文件的某些统计信息或附加信息、以及切分为块的数据信息 (通常不会占用太多)。文件占用空间也可以小于文件尺寸,此时文件内部就存在空洞了。
所谓空洞其实就是没有分配存储空间的数据块,当访问这些数据块时,系统返回 0,就如同读到空文件一般,当写这些块时,系统再实地分配对应的存储空间。其实这个和内存中的虚址地址与物理地址的概念非常相似——操作系统可以预分配一大块内存地址,这个地址只是一段连续的数字,用来保证虚拟地址不会被其它人占用,而对应的物理地址只在用到时才分配,这样就避免了一下分配一大块内存带来的浪费问题。同理,如果抽象出一个文件地址和存储地址来的话,完全可以套用上面的结论:连续的文件地址保证用户可以访问任意偏移的文件数据;文件中的空洞又避免了一下子分配太多的物理存储带来的浪费。
所以空洞不光针对文件,也可以针对内存,可以将虚址中的缺页中断理解为填补内存空洞的过程,文件中也有类似的机制。不过也有一些差异,例如内存因进程间共享而引入的 copy-on-write 机制,文件中就没有。文件同一地址的数据如果被多个进程同时写入时,只有最后一个写入的会生效,前面的那些都会被覆盖,因为文件是系统级别的概念,不像内存一样专属于某个进程。
空洞的产生
下面分平台说明。
Linux
所有的类 Unix 系统都差不多,方法比较简单,满足以下两点即可:
- 设置文件的偏移量 (lseek) 超过文件尾端
- 并写了某些数据后 (write)
此时原文件末尾到新文件末尾之间将标记为空洞。甚至都不需要写一个程序,就可以验证:
代码语言:javascript复制$ echo "this is a test" > test.txt
$ ls -lh test.txt
-rw-rw-r-- 1 yunh yunh 15 Oct 30 16:14 test.txt
$ stat test.txt
File: test.txt
Size: 15 Blocks: 8 IO Block: 4096 regular file
Device: 805h/2053d Inode: 35259462 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ yunh) Gid: ( 1000/ yunh)
Access: 2021-10-30 16:15:00.767760242 0800
Modify: 2021-10-30 16:14:58.160147599 0800
Change: 2021-10-30 16:14:58.160147599 0800
Birth: -
$ du -sh test.txt
4.0K test.txt
$ truncate -s 1M test.txt
$ ls -lh test.txt
-rw-rw-r-- 1 yunh yunh 1.0M Oct 30 16:16 test.txt
$ stat test.txt
File: test.txt
Size: 1048576 Blocks: 8 IO Block: 4096 regular file
Device: 805h/2053d Inode: 35259462 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ yunh) Gid: ( 1000/ yunh)
Access: 2021-10-30 16:15:00.767760242 0800
Modify: 2021-10-30 16:16:02.914508936 0800
Change: 2021-10-30 16:16:02.914508936 0800
Birth: -
$ du -sh test.txt
4.0K test.txt
上面的例子中,标称 1MB 的 test.txt 文件只占用 4KB 空间。带有空洞的文件复制后还有空洞吗?这要看你用什么方式复制了,如果是 cp 答案是有,如果是 cat + 重定向,没有,请看下面的例子:
代码语言:javascript复制$ cp test.txt foo.txt
$ cat test.txt > bar.txt
$ ls -lh *.txt
-rw-rw-r-- 1 yunh yunh 1.0M Oct 30 16:29 bar.txt
-rw-rw-r-- 1 yunh yunh 1.0M Oct 30 16:29 foo.txt
-rw-rw-r-- 1 yunh yunh 1.0M Oct 30 16:16 test.txt
$ stat *.txt
File: bar.txt
Size: 1048576 Blocks: 2048 IO Block: 4096 regular file
Device: 805h/2053d Inode: 35259560 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ yunh) Gid: ( 1000/ yunh)
Access: 2021-10-30 16:29:21.921709008 0800
Modify: 2021-10-30 16:29:21.925707851 0800
Change: 2021-10-30 16:29:21.925707851 0800
Birth: -
File: foo.txt
Size: 1048576 Blocks: 8 IO Block: 4096 regular file
Device: 805h/2053d Inode: 35259559 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ yunh) Gid: ( 1000/ yunh)
Access: 2021-10-30 16:29:16.751224261 0800
Modify: 2021-10-30 16:29:16.755223068 0800
Change: 2021-10-30 16:29:16.755223068 0800
Birth: -
File: test.txt
Size: 1048576 Blocks: 8 IO Block: 4096 regular file
Device: 805h/2053d Inode: 35259462 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ yunh) Gid: ( 1000/ yunh)
Access: 2021-10-30 16:19:51.460219249 0800
Modify: 2021-10-30 16:16:02.914508936 0800
Change: 2021-10-30 16:16:02.914508936 0800
Birth: -
$ du -sh *.txt
1.0M bar.txt
4.0K foo.txt
4.0K test.txt
cp 后的文件保留了相同的空洞,cat + 重定向的则生成了没有空洞的文件。从另一个侧面说明读取空洞时,系统是返回了 0 的。
Windows
与类 Unix 系统不同,windows 使用稀疏文件 (sparse) 来表示含有空洞的文件。不光是概念上有区别,实现上也有差别,例如使用类似 linux 的超出文件末尾写策略,并不能生成一个稀疏文件。当然了,首先要保证文件系统是 NTFS,其次需要使用 windows 特定的 api 来完成这项工作。
- SetFilePointer (lseek)
- WriteFile (write)
- SetEndOfFile (n/a)
并且需要在这样做之前声明文件为稀疏文件,系统才会为它生成空洞节省空间:
代码语言:javascript复制DeviceIoControl(hFile, FSCTL_SET_SPARSE, NULL, 0, NULL, 0, &dwTemp, NULL);
hFile 为打开的文件句柄。widnows 的空洞本质上是一种数据压缩,将很多 0 压缩在一起,不过确确实实起到了节省存储空间的目的。
空洞的应用
下面的脚本可以搜索文件系统中带空洞的文件:
代码语言:javascript复制#! /bin/sh
function main()
{
local path="."
if [ $# -gt 0 ]; then
path="$1"
fi
echo "detect hole under ${path}"
local size=0
local space=0
for file in $(find "${path}" -type f); do
if [ -f "${file}" ]; then
size=$(stat -c "%s" "${file}")
space=$(($(du -k "${file}" | awk '{print $1}')*1024))
if [ ${size} -gt ${space} ]; then
echo "${file} has hole, space ${space}, size ${size}"
fi
else
# file-name has chinese character ?
#echo "no ${file}"
:
fi
done
echo "done!"
}
main "$@"
在我的一台笔记本设备上的确产生了输出:
代码语言:javascript复制$ bash -f find_hole.sh /home 2>/dev/null
detect hole under /home
/home/yunh/snap/ohmygiraffe/common/.cache/mesa_shader_cache/index has hole, space 0, size 1310728
/home/yunh/.config/baidunetdisk/GPUCache/data_0 has hole, space 12288, size 45056
/home/yunh/.config/baidunetdisk/GPUCache/index has hole, space 45056, size 262512
/home/yunh/.config/baidunetdisk/GPUCache/data_3 has hole, space 98304, size 4202496
/home/yunh/.config/baidunetdisk/GPUCache/data_1 has hole, space 12288, size 270336
/home/yunh/.cache/mesa_shader_cache/index has hole, space 753664, size 1310728
/home/yunh/code/apue/04.chapter/foo.txt has hole, space 4096, size 1048576
/home/yunh/code/apue/04.chapter/test.txt has hole, space 4096, size 1048576
/home/yunh/code/apue/08.chapter/file.map has hole, space 20480, size 1048576
/home/yunh/.mozilla/firefox/g6azoga7.default-release/storage/default/https mail.126.com/cache/caches.sqlite has hole, space 86016, size 98304
/home/yunh/.mozilla/firefox/g6azoga7.default-release/storage/default/https 126.com/cache/caches.sqlite has hole, space 86016, size 98304
/home/yunh/.mozilla/firefox/g6azoga7.default-release/storage/default/moz-extension d24d4498-4011-4423-805a-f6f4f5ace4f7^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite has hole, space 45056, size 49152
/home/yunh/.mozilla/firefox/g6azoga7.default-release/storage/default/https blog.csdn.net/cache/caches.sqlite has hole, space 61440, size 65536
/home/yunh/.mozilla/firefox/g6azoga7.default-release/cookies.sqlite has hole, space 98304, size 524288
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/moz-extension 15f580b5-b741-4f58-b7b2-50144c678660^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite has hole, space 45056, size 49152
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/http www.chinadegrees.cn/idb/3178482897EPkc.sqlite has hole, space 45056, size 49152
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/https mail.126.com/cache/caches.sqlite has hole, space 176128, size 196608
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/https translate.yandex.kz/idb/3977681304ystnro_ictoclel.sqlite has hole, space 45056, size 49152
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/moz-extension 5cbf54b3-f5e8-493a-9096-76e3ad392e45^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite has hole, space 45056, size 49152
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/http www.cdgdc.edu.cn/idb/3178482897EPkc.sqlite has hole, space 45056, size 49152
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/https account.cnblogs.com/idb/1170976282GNEEEKROATNMDO.sqlite has hole, space 45056, size 49152
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/moz-extension f17a211a-4d0c-413c-8ced-df3b145f19ec^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite has hole, space 45056, size 49152
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/moz-extension 56a2ddfb-80ba-4bd7-913a-8ae756a8dc6f^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite has hole, space 45056, size 49152
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/http www.chinadegrees.com.cn/idb/3178482897EPkc.sqlite has hole, space 45056, size 49152
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/https 126.com/cache/caches.sqlite has hole, space 122880, size 131072
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/https 126.com/idb/4197078560wnooriktbaorxi-pex.sqlite has hole, space 61440, size 65536
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/https newtab.firefoxchina.cn/cache/caches.sqlite has hole, space 94208, size 98304
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/https www.recaptcha.net/idb/548905059db.sqlite has hole, space 45056, size 49152
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/https blog.csdn.net/cache/caches.sqlite has hole, space 61440, size 65536
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/storage/default/moz-extension ae22fc49-8037-4c59-8299-2523bd5c1548^userContextId=4294967295/idb/3647222921wleabcEoxlt-eengsairo.sqlite has hole, space 45056, size 49152
/home/yunh/.mozilla/firefox/l7y3lhkj.default-esr/cookies.sqlite has hole, space 196608, size 524288
看起来像是用来做 cache 的,不明觉厉~
能想到的另一个应用场景就是下载大文件,例如一个 2GB 的文件,如果害怕因下载时间太长导致后面磁盘空间不足而失败的情况,可以预先将文件扩展到 2GB,再分别填充其中的数据。不过这个更像是 windows 上的 SetEndOfFile 的应用场景,因为需要事先分配这么多存储空间,而不是像文件空洞那样只给一个标称的 2GB 文件而实际不分配存储空间。从这个角度看,windows 确实有一定的优势,因为在 linux 上占用 2GB 空间还真不是几个调用就可以搞定的。
还能想到的一个场景就是分块下载,这个和文件空洞确实可以产生一些化学反应。当大文件被切分为多个数据块同时下载以提高速度时,传统的方式是按块号顺序合并,如果中间有一个块没有下载完成,那么之后的数据块都不能合并到目标文件里去。如果使用文件空洞,哪个块下载完了就可以先合并到目标文件,不存在合并顺序的问题,从而解决上面的问题,防止太多块文件留存在文件系统中。不过只要还有一个块没下载完,文件就是不完整的,肯定会影响后期的解压、播放、加载,因此并没有解决很大的问题。
最终结论就是,文件空洞并没有内存空洞那么有用,如果你遇到过它的应用场景,欢迎在评论区拍砖斧正~~
参考
[1]. lseek函数与文件空洞
[2]. windows稀疏文件