git: 26d84bb8bf9b - main - ufs: flush soft updates dependencies for O_SYNC and O_DSYNC writes
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Tue, 13 Oct 2026 04:29:54 UTC
The branch main has been updated by sobomax:
URL: https://cgit.FreeBSD.org/src/commit/?id=26d84bb8bf9b4acd0aa80b0208c1ba4fc9646ff3
commit 26d84bb8bf9b4acd0aa80b0208c1ba4fc9646ff3
Author: Maxim Sobolev <sobomax@FreeBSD.org>
AuthorDate: 2026-10-13 02:42:31 +0000
Commit: Maxim Sobolev <sobomax@FreeBSD.org>
CommitDate: 2026-10-13 04:28:32 +0000
ufs: flush soft updates dependencies for O_SYNC and O_DSYNC writes
For a synchronous write, ffs_write() writes the data buffers with
bwrite() and then the inode with ffs_update(vp, 1). With soft updates,
a new block pointer can only reach the disk after the dependencies of
the block, such as the cylinder group map that allocates it, are
written. Until then initiate_write_inodeblock_ufs[12]() and
initiate_write_indirdep() roll the pointer, and for an extending write
the size, back in the buffer being written. Nothing in the write path
flushes those dependencies, so the inode went out with the old size and
pointers, ffs_update() cleared IN_SIZEMOD and IN_IBLKDATA, and a crash
before the syncer rewrote the inode lost data that O_SYNC or O_DSYNC
promised was on stable storage. The same held for a new block below an
indirect block, which ffs_balloc() writes synchronously but with the
pointer rolled back.
When soft updates are enabled and the write allocated blocks, finish it
with ffs_syncvnode(), as fsync(2) and fdatasync(2) do: it flushes the
dependencies, rewrites the rolled back indirect blocks and writes the
inode, or for O_DSYNC only writes the inode when IN_SIZEMOD or
IN_IBLKDATA say it is needed. The flags cannot tell whether the write
allocated anything, because allocating a block below an existing
indirect block sets neither, so compare the inode's block count before
and after the write instead. A write that allocated nothing, such as
an overwrite of existing blocks, adds no dependencies of its own and
keeps writing only the inode, so that it does not pay for a sync of the
whole vnode. ffs_syncvnode() only drops the vnode lock when it has to
resolve directory dependencies, which ffs_write() never sees.
ffs_syncvnode() also writes whatever other descriptors left dirty on
the vnode, so the first synchronous write after a lot of buffered
writes would pay for all of them. To put that cost on the open
instead, where it is less unexpected for a descriptor opened for
synchronous writes, add ffs_open() and, with soft updates, sync the
vnode there when a regular file is opened for writing with O_SYNC or
O_DSYNC, the same way the writes would: a full sync for O_SYNC, a
data-only one for O_DSYNC. It calls ffs_syncvnode() rather than
VOP_FSYNC(), whose softdep_fsync() may drop the vnode lock in the middle
of the open. The sync on open only moves work that the writes would
otherwise do, so it is skipped rather than waited for when writes are
suspended, and its errors do not fail the open. Without soft updates
the writes sync nothing but their own blocks and the inode, so there is
nothing to move and the open does not sync.
Sponsored by: Sippy Software, Inc.
Reviewed by: kib
Tested by: pho
Differential Revision: https://reviews.freebsd.org/D60134
MFC after: 1 month
---
sys/ufs/ffs/ffs_vnops.c | 60 ++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 59 insertions(+), 1 deletion(-)
diff --git a/sys/ufs/ffs/ffs_vnops.c b/sys/ufs/ffs/ffs_vnops.c
index 4044ecf20f92..493936578095 100644
--- a/sys/ufs/ffs/ffs_vnops.c
+++ b/sys/ufs/ffs/ffs_vnops.c
@@ -71,6 +71,7 @@
#include <sys/buf.h>
#include <sys/conf.h>
#include <sys/extattr.h>
+#include <sys/fcntl.h>
#include <sys/kernel.h>
#include <sys/limits.h>
#include <sys/malloc.h>
@@ -117,6 +118,7 @@ static vop_lock1_t ffs_lock;
#ifdef INVARIANTS
static vop_unlock_t ffs_unlock_debug;
#endif
+static vop_open_t ffs_open;
static vop_read_t ffs_read;
static vop_write_t ffs_write;
static int ffs_extread(struct vnode *vp, struct uio *uio, int ioflag);
@@ -145,6 +147,7 @@ struct vop_vector ffs_vnodeops1 = {
#ifdef INVARIANTS
.vop_unlock = ffs_unlock_debug,
#endif
+ .vop_open = ffs_open,
.vop_read = ffs_read,
.vop_reallocblks = ffs_reallocblks,
.vop_write = ffs_write,
@@ -180,6 +183,7 @@ struct vop_vector ffs_vnodeops2 = {
#ifdef INVARIANTS
.vop_unlock = ffs_unlock_debug,
#endif
+ .vop_open = ffs_open,
.vop_read = ffs_read,
.vop_reallocblks = ffs_reallocblks,
.vop_write = ffs_write,
@@ -455,6 +459,41 @@ ffs_fdatasync(struct vop_fdatasync_args *ap)
return (ffs_syncvnode(ap->a_vp, MNT_WAIT, DATA_ONLY));
}
+/*
+ * With soft updates, an O_SYNC or O_DSYNC write that allocates blocks
+ * finishes with ffs_syncvnode(), which also writes whatever other
+ * descriptors left dirty on the vnode. Sync the vnode when it is opened
+ * for writing with either flag, so that this cost is mostly paid once, by
+ * the open, and not by the first synchronous write.
+ *
+ * This only moves work that the writes would otherwise do, so skip it
+ * rather than wait when writes are suspended, and do not fail the open on
+ * its errors. Without soft updates the writes sync nothing but their own
+ * blocks and the inode, so there is nothing to move and the open does not
+ * sync. A writable open locks the vnode exclusively.
+ */
+static int
+ffs_open(struct vop_open_args *ap)
+{
+ struct vnode *vp;
+ struct mount *mp;
+ int error;
+
+ error = VOP_OPEN_APV(&ufs_vnodeops, ap);
+ vp = ap->a_vp;
+ if (error != 0 || vp->v_type != VREG || !DOINGSOFTDEP(vp) ||
+ (ap->a_mode & FWRITE) == 0 ||
+ (ap->a_mode & (FFSYNC | FDSYNC)) == 0)
+ return (error);
+ ASSERT_VOP_ELOCKED(vp, "ffs_open");
+ if (vn_start_write(vp, &mp, V_NOWAIT) != 0)
+ return (0);
+ (void)ffs_syncvnode(vp, MNT_WAIT,
+ (ap->a_mode & FFSYNC) != 0 ? 0 : DATA_ONLY);
+ vn_finished_write(mp);
+ return (0);
+}
+
static int
ffs_lock(
struct vop_lock1_args /* {
@@ -822,6 +861,7 @@ ffs_write(
struct buf *bp;
ufs_lbn_t lbn;
off_t osize;
+ uint64_t oblocks;
ssize_t resid, r;
int seqcount;
int blkoffset, error, flags, ioflag, size, xfersize;
@@ -885,6 +925,7 @@ ffs_write(
resid = uio->uio_resid;
osize = ip->i_size;
+ oblocks = DIP(ip, i_blocks);
if (seqcount > BA_SEQMAX)
flags = BA_SEQMAX << BA_SEQSHIFT;
else
@@ -1023,7 +1064,24 @@ ffs_write(
uio->uio_resid = resid;
}
} else if (resid > uio->uio_resid && (ioflag & IO_SYNC)) {
- if (!(ioflag & IO_DATASYNC) ||
+ if (DOINGSOFTDEP(vp) && DIP(ip, i_blocks) != oblocks) {
+ /*
+ * With soft updates, new block pointers in the
+ * inode and in indirect blocks are rolled back
+ * when those are written before the dependencies
+ * of the new blocks, such as the cylinder group
+ * maps, are on disk. If this write allocated
+ * blocks, let ffs_syncvnode() flush the
+ * dependencies first. It only drops the vnode
+ * lock for directories. A write that allocated
+ * nothing adds no dependencies of its own and only
+ * needs the inode written, as without soft updates.
+ */
+ error = ffs_syncvnode(vp, MNT_WAIT,
+ (ioflag & IO_DATASYNC) != 0 ? DATA_ONLY : 0);
+ KASSERT(error != ERELOOKUP,
+ ("ffs_write: vnode %p unlocked by sync", vp));
+ } else if (!(ioflag & IO_DATASYNC) ||
(ip->i_flag & (IN_SIZEMOD | IN_IBLKDATA)))
error = ffs_update(vp, 1);
if (ffs_fsfail_cleanup(VFSTOUFS(vp->v_mount), error))