Unix files have (at least) two sizes

April 13, 2025

I'll start by presenting things in illustrated form:

; ls -l testfile
-rw-r--r-- 1 cks 262144 Apr 13 22:03 testfile
; ls -s testfile
1 testfile
; ls -slh testfile
512 -rw-r--r-- 1 cks 256K Apr 13 22:03 testfile

The two well known sizes that Unix files have are the logical 'size' in bytes and what stat.h describes as "the number of blocks allocated for this object", often converted to some number of bytes (as ls is doing here in the last command). A file's size in bytes is roughly speaking the last file offset that has been written to in the file, and not all of the bytes covered by it may have actually been written; when this is the case, the result is a sparse file. Sparse files are the traditional cause of a mismatch between the byte size and the number of blocks a file uses. However, that is not what is happening here.

This file is on a ZFS filesystem with ZFS's compression turned on, and it was created with 'dd if=/dev/zero of=testfile bs=1k count=256'. In ZFS, zeroes compress extremely well, and so ZFS has written basically no physical data blocks and faithfully reported that (minimal) number in the stat() st_blocks field. However, at the POSIX level we have indeed written data to all 256 KBytes of the file; it's not a sparse file. This is an extreme example of filesystem compression, and there are plenty of lesser ones.

This leaves us with a third size, which is the number of logical blocks for this file. When a filesystem is doing data compression, this number will be different from the number of physical blocks used. As far as I can tell, the POSIX stat.h description doesn't specify which one you have to report for st_blocks. As we can see, ZFS opts to report the physical block size of the file, which is probably the more useful number for the purposes of things like 'du'. However, it does leave us with no way of finding out the logical block size, which we may care about for various reasons (for example, if our backup system can skip unwritten sparse blocks but always writes out uncompressed blocks).

This also implies that a non-sparse file can change its st_blocks number if you move it from one filesystem to another. One filesystem might have compression on and the other one have it off, or they might have different compression algorithms that give different results. In some cases this will cause the file's space usage to expand so that it doesn't actually fit into the new filesystem (or for a tree of files to expand their space usage).

(I don't know if there are any Unix filesystems that report the logical block size in st_blocks and only report the physical block size through a private filesystem API, if they report it at all.)


Comments on this page:

By loreb at 2025-04-14 05:09:23:

zfs+compression reports the compressed size; fools me everytime, I need to remember "du --apparent-size" or similar (I expect "ls -lh" and "ls -sShr" to report the same size - they don't)

btrfs+compression reports the uncompressed size - more intuitive for me, but last I checked you need a special tool to see the uncompressed size ("btrfs property get" to check if compression is on/off)

Even worse, ZFS compresses asynchronously, so st_blocks will change when the file is large and new...

By ghop at 2025-04-16 21:11:14:

As far as I can tell, the POSIX stat.h description doesn't specify which one you have to report for st_blocks.

It kind of does: neither, because st_blocks is an optional feature; a POSIX implementation does not have to support the XSI option.

But, assuming an implementation does advertise support, I think it's relatively clear. If ZFS does not reserve space for the full uncompressed data, it would be incorrect to use its full size for st_blocks. That makes it implementation-defined, although I'm not aware of any requirement for the implementor to formally document their choice.

Written on 13 April 2025.
« Mandatory short duration TLS certificates are probably coming soon
ZFS's delayed compression of written data (when compression is enabled) »

Page tools: View Source, View Normal.
Search:
Login: Password:

Last modified: Sun Apr 13 23:02:05 2025
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.