首页 > Sysadmin > 正文

Linux Disk Usage: Why du and df Disagree

2026-10-27 Sysadmin

df reads one ledger: the whole filesystem. du counts file by file. When the two numbers disagree, in most cases neither side miscalculated — they simply measure on different bases.

1. Bottom line first: df for the whole disk, du for a directory

df only ever talks about one thing: the filesystem. With no arguments it lists every mounted filesystem; given a file argument, it reports only the filesystem that file lives on [1]. du totals the blocks actually used by a set of files (recursing into directories) [4]. So check disk space with df and find the big directory with du — you can’t reconcile the two numbers directly.

df -k .                  # 整个文件系统的三本账:Total / Used / Available
du -sk .                 # 只数它走到的文件:实测小目录 32 KB
du -x -sk .              # 加 -x:只算当前这一个文件系统

Measured on WSL2 Ubuntu 24.04.4, coreutils 9.4, ext4 root partition: df -k / reports Used 8,492,304 KB, du -x -sk / reports 8,490,100 KB — a gap of about 2.2 MB. That gap is the filesystem’s own metadata (block bitmaps, inode tables, journal, group descriptors): df reads it from the filesystem’s statistics, while du can only count the directories and files it walks [1][4].

2. Each of df’s three columns has its own basis: Total excludes metadata, Available excludes reserved blocks

df -k /
tune2fs -l /dev/sdd | grep -iE "Block count|Reserved block count|Reserved blocks uid"

On the same machine, df -k / reports Total 1,055,762,868 KB, Used 8,492,304 KB, Available 993,567,092 KB, while tune2fs -l shows Block count 268435456 (4 KB blocks), Reserved block count 13421772, Reserved blocks uid 0 in the superblock [5]. Two bases have to be lined up:

  • Total sits 17,978,956 KB below Block count × 4 KB: that part is filesystem metadata, and df doesn’t count it into Total.
  • Total − Used − Available = 53,703,472 KB, essentially matching the reserved blocks 13421772 × 4 KB = 53,687,088 KB (about 16 MB off): reserved blocks can only be spent by privileged processes, so the Available that ordinary users see excludes them [5].
  • So “df says 950 GB is still free, yet du adds up to only 8 GB” is normal: df speaks for the whole disk, du only for the tree it walked.

    3. The five places where they disagree most (all measured in WSL)

    mkdir -p /tmp/du_df && echo hi > /tmp/du_df/a && ln /tmp/du_df/a /tmp/du_df/b
    du -sk /tmp/du_df                     # 12:同一个 inode 只数一次
    du -sk --count-links /tmp/du_df       # 20:同一个 inode 数两次
    du /tmp/du_df/a /tmp/du_df/b          # 只打印 a;参数反过来写就只打印 b
    
    truncate -s 10M /tmp/du_df/sparse.bin
    du -sk /tmp/du_df/sparse.bin          # 0:按实际占用的块算
    du --apparent-size -sk /tmp/du_df/sparse.bin    # 10240:按字节数算

    1. Hard links: du counts the same inode once by default; --count-links (-l) makes it count twice [3][4]; argument order decides which name is printed — measured: du a b prints only a, du b a prints only b. df is a disk-wide statistic, so it has no such issue. 2. Sparse files: du reports blocks actually allocated by default; --apparent-size reports the byte count [3][4]. Measured: a plain 3000-byte file — du reports 4 (one 4 KB block), du -B 512 reports 8, du --apparent-size reports 3; a 10 MB sparse file — du reports 0, --apparent-size reports 10240. 3. Files deleted while still open by a process: df still counts them in Used, but du and ls can’t see them — the directory entry is gone while the blocks aren’t freed. Measured on tmpfs: df -k shows Used 20480 KB both before and after deletion, while du -sk drops from 20480 to 0; lsof +L1 lists the file, and readlink /proc/<pid>/fd/3 shows (deleted). This one is an observed behavior: the df manual only says df reads filesystem statistics — it doesn’t spell out this scenario [1]. 4. Mount points: du crosses mount points by default; only -x restricts it to the current filesystem [4]. Measured by mounting a tmpfs inside a directory and writing 1 MB: du -sk reports 1032, du -sk -x reports 8. Also, df hides duplicate bind mounts and pseudo-device entries by default, keeping only the entry with the shortest mount-point name [2]. 5. Permissions: df produces a number as long as the path resolves (it never reads file contents), while du reports an error and exits 1 when it can’t read a directory — yet still prints what it did read. Measured as the nobody user: df prints normally with exit code 0; du reports cannot read directory '...': Permission denied, prints 4 (the directory entry itself), exit code 1 [3][1].

    4. Block-size basis: 1024 by default, 512 under POSIXLY_CORRECT

    du -sk dir                    # 默认按 1024 字节块
    du -B 512 dir                 # 显式指定块大小:同一棵树 12 → 24
    POSIXLY_CORRECT=1 du dir      # 12 → 24
    POSIXLY_CORRECT=1 df -P .     # 1,055,762,868 → 2,111,525,736

    GNU df/du default to 1024-byte units; setting POSIXLY_CORRECT switches them to 512 [1][4][2] — the POSIX basis (with POSIXLY_CORRECT set) is 512 bytes [1] — so the same number can double on a different machine. Fractional values always round up [2]. For stable numbers in scripts, use df -k and du -sk, never -h: measured, df -h renders the same disk as 1007G / 8.1G / 948G.

    5. Common idioms and a troubleshooting cheat-sheet

    df -i                          # inode 用量(整盘口径:实测 122498 已用 / 67108864 总量)
    du --inodes dir                # 按目录树数 inode(GNU 扩展,手册单列一条)
    df -k -t ext4 dir              # 只看某类文件系统;没有匹配时不输出且退出码 1(实测)
    du --max-depth=1 dir; du --total dir; du --exclude=cache dir

    Troubleshooting cheat-sheet (symptom / cause / fix):

  • du is a few GB smaller than df — filesystem metadata and reserved blocks, which du can’t count — cross-check the superblock with tune2fs -l
  • df says nearly full, du adds up to much less — deleted-but-still-open files — lsof +L1 or readlink /proc/<pid>/fd/N
  • du prints Permission denied yet still outputs a number — insufficient permissions; it counted only what it could read — run as root or add sudo; exit code 1 [3]
  • du’s number is bigger than expected — it crossed a mount point (network share, /mnt) — add -x
  • du is much smaller than df — sparse files — use --apparent-size
  • df -k -t ext4 dir exits 1 — that path isn’t on ext4 (no output means failure, measured) [1]
  • Rollback and undo: df and du are read-only commands and change no files. Test files lived under /tmp/du_df and /tmp/hl, were deleted after the run, the tmpfs was unmounted, and nothing was installed (lsof was already in this machine’s baseline). The only side-effecting option is tune2fs -m <percent> (changes reserved blocks); this article used only the read-only tune2fs -l [5].

    Four common questions: why du -sk . and df differ by 2 MB (metadata); why du a b prints only one name (the same inode was already counted); why du -h and du -B 512 show different numbers (block-size basis); and the difference between df -i and du --inodes (the first is disk-wide, the second is just this tree).

    Status: draft pending review. Test environment: WSL2 Ubuntu 24.04.4 (kernel 6.6.114.1-microsoft-standard-WSL2), coreutils 9.4, e2fsprogs, util-linux, dated 2026-10-27. Actually tested: the three df columns against the superblock; hard links counted once, and argument order; sparse files and --apparent-size; deleted-but-open files (df still counts them, du can’t see them); mount points and -x; exit code under insufficient permissions; the 512-byte basis under POSIXLY_CORRECT; df -t ext4 and df -i; du --inodes/--max-depth/--exclude/--total. Documentation-checked only, not yet executed: df hiding duplicate bind-mount entries; default block sizes on BSD/macOS and busybox; changing reserved blocks with tune2fs -m. What this adds beyond existing tutorials: it lines up df’s Total/Used/Available columns against the superblock one by one, and gives the measured conclusion on hard-link argument order. Topic sourced from a 2025-03-26 listing on the reference site, “Checking disk usage on Linux: du vs df”; during this round that site was blocked by a BaoTa (BT) WAF (403/502), so this article was rewritten entirely from official documentation and hands-on tests without consulting its body.


    References

    The main commands in this article were tested on WSL Ubuntu (24.04); a few items (missing tools / install-type commands) were only verified against official documentation, not executed.