[apue] 文件中的空洞


空洞的概念

linux 上普通文件的大小與占用空間是兩個概念,前者表示文件中數據的長度,后者表示數據占用的磁盤空間,通常后者大於前者,因為需要一些額外的空間用來記錄文件的某些統計信息或附加信息、以及切分為塊的數據信息 (通常不會占用太多)。文件占用空間也可以小於文件尺寸,此時文件內部就存在空洞了。

所謂空洞其實就是沒有分配存儲空間的數據塊,當訪問這些數據塊時,系統返回 0,就如同讀到空文件一般,當寫這些塊時,系統再實地分配對應的存儲空間。其實這個和內存中的虛址地址與物理地址的概念非常相似——操作系統可以預分配一大塊內存地址,這個地址只是一段連續的數字,用來保證虛擬地址不會被其它人占用,而對應的物理地址只在用到時才分配,這樣就避免了一下分配一大塊內存帶來的浪費問題。同理,如果抽象出一個文件地址和存儲地址來的話,完全可以套用上面的結論:連續的文件地址保證用戶可以訪問任意偏移的文件數據;文件中的空洞又避免了一下子分配太多的物理存儲帶來的浪費。

所以空洞不光針對文件,也可以針對內存,可以將虛址中的缺頁中斷理解為填補內存空洞的過程,文件中也有類似的機制。不過也有一些差異,例如內存因進程間共享而引入的 copy-on-write 機制,文件中就沒有。文件同一地址的數據如果被多個進程同時寫入時,只有最后一個寫入的會生效,前面的那些都會被覆蓋,因為文件是系統級別的概念,不像內存一樣專屬於某個進程。

空洞的產生

下面分平台說明。

Linux

所有的類 Unix 系統都差不多,方法比較簡單,滿足以下兩點即可:

  • 設置文件的偏移量 (lseek) 超過文件尾端
  • 並寫了某些數據后 (write)

此時原文件末尾到新文件末尾之間將標記為空洞。甚至都不需要寫一個程序,就可以驗證:

$ 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 + 重定向,沒有,請看下面的例子:

$ 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)

並且需要在這樣做之前聲明文件為稀疏文件,系統才會為它生成空洞節省空間:

DeviceIoControl(hFile, FSCTL_SET_SPARSE, NULL, 0, NULL, 0, &dwTemp, NULL);

hFile 為打開的文件句柄。widnows 的空洞本質上是一種數據壓縮,將很多 0 壓縮在一起,不過確確實實起到了節省存儲空間的目的。

空洞的應用

下面的腳本可以搜索文件系統中帶空洞的文件:

#! /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 "$@"

在我的一台筆記本設備上的確產生了輸出:

$ 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稀疏文件


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM