[apue] 檔案中的空洞

2022-07-28 12:00:41

空洞的概念

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稀疏檔案