[Question] UFS/FFS: why a block is always allocated at EOF when truncating up

David Timber <[email protected]>
Newsgroups gmane.os.freebsd.devel.file-systems,gmane.linux.file-systems.zfs.devel
Message-ID <[email protected]>
Hi,

I've been working on some things related to SEEK_DATA and SEEK_HOLE in
Linux kernel(you'll find my work on linux-fsdevel). I still believe
there's future with POSIX so portability has always been of my
interests. They will probably hit the future revision of the C
programming language, so we might as well be preprared.

SEEK_DATA and SEEK_HOLE are rather recent addition to the spec, so it
seems that only the three "Unices"(Solaris, FreeBSD, Linux) implement
them at the moment. While observing the bevaviour of the API, I found
something strange in ffs: when a file gets "truncated up"(i.e. the new
file length becomes larger), a block at the new EOF is always allocated.

The culprit(sys/ufs/ffs/ffs_inode.c:360):

	/*
	 * Lengthen the size of the file. We must ensure that the
	 * last byte of the file is allocated. Since the smallest
	 * value of osize is 0, length will be at least 1.
	 */
	if (osize < length) {
		vnode_pager_setsize(vp, length);
		flags |= BA_CLRBUF;
		error = UFS_BALLOC(vp, length - 1, 1, cred, flags, &bp);
		if (error) {
			vnode_pager_setsize(vp, osize);
			return (error);
		}
		ip->i_size = length;
		DIP_SET(ip, i_size, length);
		if (bp->b_bufsize == fs->fs_bsize)
			bp->b_flags |= B_CLUSTEROK;
		ffs_inode_bwrite(vp, bp, flags);
		UFS_INODE_SET_FLAG(ip, IN_SIZEMOD | IN_CHANGE | IN_UPDATE);
		return (ffs_update(vp, waitforupdate));
	}


So, using the tool I made
<https://github.com/dxdxdt/gists/tree/master/writeups/lshole> to list
"holes" in files, say if a whole file(i.e. no wholes. empty or filled
with data) gets truncated upwards more than the block size of the file
system, you get this behaviour:

	$ gmake test-freebsd-ufs-phantom-data-at-eof
	truncate -s 0  phantom-data && [ -e phantom-data ]
	dd status=none conv=notrunc bs=1M if=/dev/urandom of=phantom-data count=1 seek=0
	truss ./lshole phantom-data 2>&1 | grep -E '^(lseek|phantom-data:)'
	lseek(3,0x0,SEEK_HOLE)                 = 1048576 (0x100000)
	lseek(3,0x100000,SEEK_DATA)             ERR#6 'Device not configured'
	lseek(3,0x0,SEEK_END)                 = 1048576 (0x100000)
	phantom-data:
	truncate -s 2M phantom-data
	truss ./lshole phantom-data 2>&1 | grep -E '^(lseek|phantom-data:)'
	lseek(3,0x0,SEEK_HOLE)                 = 1048576 (0x100000)
*lseek(3,0x100000,SEEK_DATA) = 2064384 (0x1f8000)*
	lseek(3,0x1f8000,SEEK_HOLE)             = 2097152 (0x200000)
	lseek(3,0x200000,SEEK_DATA)             ERR#6 'Device not configured'
	lseek(3,0x0,SEEK_END)                 = 2097152 (0x200000)
	phantom-data: 1048576-2064384
	dd status=none conv=notrunc bs=1M if=/dev/urandom of=phantom-data count=1 seek=2
	truss ./lshole phantom-data 2>&1 | grep -E '^(lseek|phantom-data:)'
	lseek(3,0x0,SEEK_HOLE)                 = 1048576 (0x100000)
	lseek(3,0x100000,SEEK_DATA)             = 2064384 (0x1f8000)
	lseek(3,0x1f8000,SEEK_HOLE)             = 3145728 (0x300000)
	lseek(3,0x300000,SEEK_DATA)             ERR#6 'Device not configured'
	lseek(3,0x0,SEEK_END)                 = 3145728 (0x300000)
	phantom-data: 1048576-2064384
	truncate -s 4M phantom-data
	truss ./lshole phantom-data 2>&1 | grep -E '^(lseek|phantom-data:)'
	lseek(3,0x0,SEEK_HOLE)                 = 1048576 (0x100000)
	lseek(3,0x100000,SEEK_DATA)             = 2064384 (0x1f8000)
	lseek(3,0x1f8000,SEEK_HOLE)             = 3145728 (0x300000)
*lseek(3,0x300000,SEEK_DATA) = 4161536 (0x3f8000)*
	lseek(3,0x3f8000,SEEK_HOLE)             = 4194304 (0x400000)
	lseek(3,0x400000,SEEK_DATA)             ERR#6 'Device not configured'
	lseek(3,0x0,SEEK_END)                 = 4194304 (0x400000)
	phantom-data: 1048576-2064384
	phantom-data: 3145728-4161536

While if you run the same script in ZFS, you get the layout you'd pretty
much expect:

	lseek(3,0x0,SEEK_HOLE)				 = 1048576 (0x100000)
	lseek(3,0x100000,SEEK_DATA)			 = 2097152 (0x200000)
	lseek(3,0x200000,SEEK_HOLE)			 = 3145728 (0x300000)
	lseek(3,0x300000,SEEK_DATA)			 ERR#6 'Device not configured'
	lseek(3,0x0,SEEK_END)				 = 4194304 (0x400000)
	phantom-data: 1048576-2097152
	phantom-data: 3145728-4194304


Yes, I've read the forum guidelines. This is not "why FreeBSD does X
while Y doesn't" type of question or complaint. I'm just curious about
the technical background that lead to this behaviour in UFS. I tried to
hunt it down myself, but the code predates VCS era(1994) and probably
way back to 386BSD era. It's probably documented somewhere, but I
decided asking would be much faster in this case.

My theories are:

 1. to serve the EOF block as "size markers" for fsck in case of power
    loss when inode write is deferred
 2. to reduce fragmentation on HDDs back in the days??

I think it's probably both although I don't have much understanding in
the design of UFS to explain why. So... any attempt to enlighten me
would be much appreciated!

Btw, FreeBSD's ext2 mirrors the exact same behaviour when the standard
implementation(Linux) doesn't. I wonder what benefit it could bring to
other extent-based filesystems like Ext? Or perhaps it was just one of
"monkey see, monkey do" thingies?

Davo
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.