iT邦幫忙

0

為什麼 du 和 Finder 顯示的資料夾大小不一樣?

  • 分享至 

  • xImage
  •  

對一個資料夾執行 du -sh,再跟 Finder 顯示的大小比較,兩者常常對不起來,有時差到好幾 GB。其實兩邊都沒錯,它們回答的是不同的問題:檔案有多長,以及檔案實際佔了多少磁碟空間。磁碟滿的時候只有後者重要,而要算對它,不是把 st_size 加總就好。

下面的規則來自桌面應用程式 osFoundry 的 Disk 畫面。它的掃描器是一個建立在 std::fs 上的簡短 Rust 函式,同樣的邏輯用 Python 寫大約 40 行。所有數字都來自一個 APFS 磁碟區(配置區塊大小 4 KB)上的測試資料夾。

範例家目錄的磁碟地圖,依實際配置的區塊計算大小

算區塊,不是算位元組

st_size 是檔案的長度。st_blocks 是檔案系統配置給這個檔案的 512 位元組單位數,不管磁碟區實際的區塊大小是多少。應用程式的掃描器預設用的是後者:

fn allocated_bytes(meta: &fs::Metadata, apparent: bool) -> u64 {
    if apparent {
        return meta.len();
    }
    #[cfg(unix)]
    {
        use std::os::unix::fs::MetadataExt;
        meta.blocks().saturating_mul(512)
    }
    #[cfg(not(unix))]
    {
        meta.len()
    }
}

測試資料夾裡的三個檔案可以說明原因:

檔案 st_size st_blocks × 512
small.txt(2 位元組的文字) 2 4,096
sparse.img(truncate 成 1 GiB,從未寫入) 1,073,741,824 0
big.bin(200 MiB 的資料) 209,715,200 209,715,200

再小的檔案也會佔掉一整個區塊,所以一個有 100,000 個小檔案的資料夾,在磁碟上會比它的位元組總數多出好幾百 MB。稀疏檔案則相反:VM 磁碟、資料庫檔案和某些下載檔會先保留很大的長度,只有寫入過的部分才真正用到區塊。macOS 的 du 算的是區塊;du -A 則印出表面大小。應用程式裡的 Apparent size 勾選框做的是同樣的切換。

Size、Files、Age 模式與 Apparent size 切換

不跟隨連結,不跨出磁碟區

會跟隨符號連結的掃描器,可能把同一個資料夾算兩次,或是在指向自己上層的連結上無限迴圈。走進已掛載磁碟的掃描器,則會把另一顆磁碟的檔案算進這顆磁碟的總量。應用程式用 symlink_metadata 讀取每個項目,取得的是連結本身而不是它指向的目標,並且跟掃描起點資料夾比較裝置 ID:

if child_meta.file_type().is_symlink() || !same_device(root_meta, &child_meta) {
    continue;
}

same_device 比較的是 st_dev,跟 du -x 或 find -xdev 的檢查一樣。

硬連結與複製檔

硬連結是同一個檔案有好幾個名字。在測試資料夾執行 ln big.bin hard.bin 後,ls -l 會顯示兩個 200 MiB 的檔案,但 du 只算一次資料,因為只要 st_nlink 大於 1,它就會記住看過的每一組 (st_dev, st_ino)。任何加總區塊的掃描器都應該這樣做。HFS+ 磁碟上的 Time Machine 備份和部分套件管理工具大量使用硬連結。

APFS 的 clone 更麻煩。cp -c big.bin clone.bin(或 Finder 的「複製」)會建立第二個檔案,在任一方被修改之前,都和原檔共用區塊。每個檔案都有自己的 inode、自己的 st_blocks,連結數是 1,所以 du 會把這一對算成 400 MiB。用 df 在前後量測,clone 佔用的可用空間是 0 KB,而同一個檔案用一般的 cp 複製則佔了 204,984 KB。沒有任何 POSIX 欄位會顯示這種共用,所以以區塊計算的總量,會高估刪除 clone 能空出的空間。對於放滿重複專案或照片圖庫的資料夾,在前後跑 df 是唯一可靠的確認方式。

Python 版本

同樣的規則,只用標準函式庫:

import os
import stat
import sys

def usage(root):
    root_dev = os.lstat(root).st_dev
    seen = set()
    allocated = apparent = 0
    skipped = 0
    stack = [root]
    while stack:
        path = stack.pop()
        try:
            entries = list(os.scandir(path))
        except OSError:
            skipped += 1
            continue
        for e in entries:
            try:
                st = e.stat(follow_symlinks=False)
            except OSError:
                skipped += 1
                continue
            if stat.S_ISLNK(st.st_mode) or st.st_dev != root_dev:
                continue
            if stat.S_ISDIR(st.st_mode):
                stack.append(e.path)
                continue
            if st.st_nlink > 1:
                key = (st.st_dev, st.st_ino)
                if key in seen:
                    continue
                seen.add(key)
            allocated += st.st_blocks * 512
            apparent += st.st_size
    return allocated, apparent, skipped

if __name__ == "__main__":
    alloc, appar, skipped = usage(sys.argv[1])
    print(f"on disk  {alloc / 2**30:8.2f} GiB")
    print(f"apparent {appar / 2**30:8.2f} GiB")
    if skipped:
        print(f"skipped  {skipped} unreadable entries")

在測試資料夾上,它印出磁碟上 0.39 GiB、表面大小 1.39 GiB,跟 du -sk(409,604 KB)和 du -sAk 一致。在一個 14 GiB 的原始碼目錄上,它跟 du -sk 的結果到小數點後兩位都相同。記得保留略過的項目數:在 macOS 上,受隱私權限保護的資料夾(Mail、Messages、Safari)沒有「完整磁碟取用權限」就讀不到,悄悄把它們漏掉的總量,看起來會比實際更完整。

應用程式會把結果縮小到畫得出來的程度。每個資料夾只保留最大的 36 個子項目,其餘的加總成一個「Other items」節點,所以打開一個有上千個套件的 node_modules 資料夾,看到的是 36 個方塊加上一個代表其他全部的方塊,而不是上千條細縫。

安全地刪除

磁碟工具真正的風險在刪除這一步。應用程式用 trash crate 把項目移到系統的垃圾桶,而不是直接 unlink,並且在移動任何東西之前檢查每一個路徑:

if !target.starts_with(&root) || target == root || home.as_ref() == Some(&target) {
    return Err("Cannot move a scan root or home folder to Trash".into());
}
if mount_points
    .iter()
    .any(|mount| target == *mount || target.parent() == Some(mount.as_path()))
{
    return Err("Cannot move a volume or its top-level items to Trash".into());
}

路徑會先被正規化,所以 .. 或符號連結都沒辦法把目標偷渡到掃描資料夾之外。如果選取的項目裡有一個資料夾包含另一個資料夾,也會被拒絕,因為上層一旦被移到垃圾桶,子項目的路徑就不存在了,整批操作會在中途失敗。

確認步驟:移到垃圾桶之前列出每個標記的路徑和大小

這個桌面應用程式可以從 osFoundry 下載頁面免費下載。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言