Sample output
1.3G -rw-r--r-- 1 ada ada 40G Oct 14 09:12 build-runner.img
7.8G -rw-r--r-- 1 ada ada 20G Oct 14 09:40 db-replica.img
0 -rw-r--r-- 1 ada ada 100G Oct 14 09:41 scratch.img
Three files that claim 160G between them occupy 9.1G of real disk. The first column is the one df cares about. scratch.img has never been written to at all: it is a 100G promise with zero blocks behind it, and it only starts costing space when something writes inside it.
When you would reach for it
du and ls disagree by an order of magnitude and you want to know which one is lying. A backup of a VM directory came out five times larger than the directory itself. A disk image you copied to a new server filled the target disk even though it barely used anything on the old one. In every case the answer is the same: apparent size and allocated size are two separate numbers, and ls -ls puts them next to each other.
Gotchas
- Copying can quietly fill the holes.
scp,cat a.img > b.img, and most upload tools read the zeros and write them out as real blocks, so a 1.3G image arrives as a 40G one. GNUcppreserves holes by default (--sparse=auto), butrsyncneeds-S(--sparse) and GNUtarneeds-Swhen creating the archive. If the copy already happened,cp --sparse=alwaysrebuilds a sparse version. - Sparse files let you overcommit a disk.
dfcounts allocated blocks, so three 100G images can sit on a 50G filesystem without complaint. The bill arrives later, when the guest writes into a hole and the host has no block to give it. The VM sees an I/O error on a disk it believes is mostly empty. - On ZFS and btrfs with compression, a small -s number does not necessarily mean holes. Compressed blocks also shrink
st_blocks, and ZFS stores runs of zeros as holes even if you wrote them explicitly. ZFS also reports freshly written data late: allocation can show near zero for several seconds after a write, until the transaction group commits. Runsyncbefore trusting the number. - Without -h the units are not portable. GNU ls counts the -s column in 1 KiB blocks (512 bytes if
POSIXLY_CORRECTis set), and BSD ls on macOS uses 512-byte blocks, so the same file prints different raw numbers on the two systems. Keep -h, or pass-kon either to force KiB. macOS lackstruncateand GNUfindtoo, so the variants below needbrew install coreutils findutils(asgtruncateandgfind).
Variants
$ truncate -s 20G test.img && ls -lsh test.img
Make a sparse file to experiment with. truncate extends the length without writing anything, so the -s column reads 0, or a single block on some filesystems, next to 20G
$ find . -type f -printf '%S\t%p\n' | awk '$1 < 0.5'
GNU find. List every file whose allocation is less than half its length. %S is the sparseness ratio, allocated bytes divided by apparent bytes: 1.0 is fully allocated, near 0 is mostly hole, and small files often show above 1.0 because they round up to a whole block
$ cp --sparse=always bloated.img slim.img && ls -lsh bloated.img slim.img
GNU cp. Write a new copy that turns every all-zero block back into a hole, which reclaims the space a careless scp filled in. Check that the copy works before you delete the original