From 5d81fbfc8ff9bdbdd381041aa083a1173a4190ad Mon Sep 17 00:00:00 2001 From: "Yan, Zheng" Date: Mon, 20 Jul 2026 09:12:11 +0800 Subject: [PATCH] fuse: update atime locally and flush it via SETATTR Make fuse track kernel atime update and flush it back to the daemon via SETATTR. Dirty atime and ctime/mtime are tracked in the inode state, marked by inode's update_time() operation. SETATTR initiated by fuse_flush_times() only updates the timestamps that were dirty. Signed-off-by: "Yan, Zheng" --- fs/fuse/dir.c | 92 +++++++++++++++++++++++++++++++++++++++++++----- fs/fuse/file.c | 5 +-- fs/fuse/fuse_i.h | 4 +++ fs/fuse/inode.c | 17 ++++++++- 4 files changed, 107 insertions(+), 11 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index 5ca3ebb36591e6..9fdf935aadd215 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -144,6 +144,10 @@ static void fuse_dir_changed(struct inode *dir) */ void fuse_invalidate_atime(struct inode *inode) { + /* don't invalidate atime if it's updated by kernel locally */ + if (!(inode->i_flags & S_NOATIME)) + return; + if (!IS_RDONLY(inode)) fuse_invalidate_attr_mask(inode, STATX_ATIME); } @@ -1011,6 +1015,9 @@ static void fuse_update_ctime_in_cache(struct inode *inode) { if (!IS_NOCMTIME(inode)) { inode_set_ctime_current(inode); + /* see comment in fuse_update_time() */ + smp_mb__before_atomic(); + set_bit(FUSE_I_CMTIME_DIRTY, &get_fuse_inode(inode)->state); mark_inode_dirty_sync(inode); fuse_flush_time_update(inode); } @@ -1947,11 +1954,43 @@ static void fuse_setattr_fill(struct fuse_conn *fc, struct fuse_args *args, } /* - * Flush inode->i_mtime to the server + * Record which timestamps the VFS updated locally, so that + * fuse_flush_times() only sends the ones that are actually dirty. + */ +static int fuse_update_time(struct inode *inode, int flags) +{ + struct fuse_inode *fi = get_fuse_inode(inode); + int updated; + int dirty_flags = 0; + + updated = inode_update_timestamps(inode, flags); + if (updated & (S_ATIME|S_MTIME|S_CTIME)) { + /* + * Pair with smp_mb__after_atomic() in redfs_flush_times(). + * Ensure timestamps are visible before we set the dirty bits. + */ + smp_mb__before_atomic(); + + if (updated & S_ATIME) + set_bit(FUSE_I_ATIME_DIRTY, &fi->state); + if (updated & (S_CTIME | S_MTIME)) + set_bit(FUSE_I_CMTIME_DIRTY, &fi->state); + + dirty_flags = inode->i_sb->s_flags & SB_LAZYTIME ? I_DIRTY_TIME : I_DIRTY_SYNC; + } + + __mark_inode_dirty(inode, dirty_flags); + + return updated; +} + +/* + * Flush locally maintained timestamps to the server */ int fuse_flush_times(struct inode *inode, struct fuse_file *ff) { struct fuse_mount *fm = get_fuse_mount(inode); + struct fuse_inode *fi = get_fuse_inode(inode); FUSE_ARGS(args); struct fuse_setattr_in inarg; struct fuse_attr_out outarg; @@ -1959,14 +1998,44 @@ int fuse_flush_times(struct inode *inode, struct fuse_file *ff) memset(&inarg, 0, sizeof(inarg)); memset(&outarg, 0, sizeof(outarg)); - inarg.valid = FATTR_MTIME; - inarg.mtime = inode_get_mtime_sec(inode); - inarg.mtimensec = inode_get_mtime_nsec(inode); - if (fm->fc->minor >= 23) { - inarg.valid |= FATTR_CTIME; - inarg.ctime = inode_get_ctime_sec(inode); - inarg.ctimensec = inode_get_ctime_nsec(inode); + /* + * Only flush the timestamps that were dirtied locally. The other + * cached values originate from the server and pushing them back + * could overwrite newer server state. + */ + if (test_and_clear_bit(FUSE_I_CMTIME_DIRTY, &fi->state)) { + /* + * Pair with smp_mb__before_atomic() in redfs_update_time(). + * If we clear the dirty bit, we must see the matching or + * the subsequent ctime/mtime. + */ + smp_mb__after_atomic(); + + inarg.valid |= FATTR_MTIME; + inarg.mtime = inode_get_mtime_sec(inode); + inarg.mtimensec = inode_get_mtime_nsec(inode); + if (fm->fc->minor >= 23) { + inarg.valid |= FATTR_CTIME; + inarg.ctime = inode_get_ctime_sec(inode); + inarg.ctimensec = inode_get_ctime_nsec(inode); + } + } + if (test_and_clear_bit(FUSE_I_ATIME_DIRTY, &fi->state)) { + smp_mb__after_atomic(); + + /* + * The server, not the client, is the authority of timestamps. + * Ask the server to update atime according the dirty value. + * FATTR_ATIME_NOW tells the server that the update is not + * from utimes(2) + */ + inarg.valid |= FATTR_ATIME | FATTR_ATIME_NOW; + inarg.atime = inode_get_atime_sec(inode); + inarg.atimensec = inode_get_atime_nsec(inode); } + if (!inarg.valid) + return 0; + if (ff) { inarg.valid |= FATTR_FH; inarg.fh = ff->fh; @@ -2106,6 +2175,10 @@ int fuse_do_setattr(struct dentry *dentry, struct iattr *attr, } spin_lock(&fi->lock); + /* explicitly set atime, discard the local dirty value */ + if ((attr->ia_valid & ATTR_ATIME) && (attr->ia_valid & ATTR_ATIME_SET)) + clear_bit(FUSE_I_ATIME_DIRTY, &fi->state); + /* the kernel maintains i_mtime locally */ if (trust_local_cmtime) { if (attr->ia_valid & ATTR_MTIME) @@ -2272,6 +2345,7 @@ static const struct inode_operations fuse_dir_inode_operations = { .mknod = fuse_mknod, .permission = fuse_permission, .getattr = fuse_getattr, + .update_time = fuse_update_time, .listxattr = fuse_listxattr, .get_inode_acl = fuse_get_inode_acl, .get_acl = fuse_get_acl, @@ -2295,6 +2369,7 @@ static const struct inode_operations fuse_common_inode_operations = { .setattr = fuse_setattr, .permission = fuse_permission, .getattr = fuse_getattr, + .update_time = fuse_update_time, .listxattr = fuse_listxattr, .get_inode_acl = fuse_get_inode_acl, .get_acl = fuse_get_acl, @@ -2307,6 +2382,7 @@ static const struct inode_operations fuse_symlink_inode_operations = { .setattr = fuse_setattr, .get_link = fuse_get_link, .getattr = fuse_getattr, + .update_time = fuse_update_time, .listxattr = fuse_listxattr, }; diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 55ab5e9cf61715..8d14cd874fd2a7 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1960,7 +1960,7 @@ static struct fuse_file *fuse_write_file_get(struct fuse_inode *fi) int fuse_write_inode(struct inode *inode, struct writeback_control *wbc) { struct fuse_inode *fi = get_fuse_inode(inode); - struct fuse_file *ff; + struct fuse_file *ff = NULL; int err; /* @@ -1974,7 +1974,8 @@ int fuse_write_inode(struct inode *inode, struct writeback_control *wbc) */ WARN_ON(wbc->for_reclaim); - ff = __fuse_write_file_get(fi); + if (S_ISREG(inode->i_mode)) + ff = __fuse_write_file_get(fi); err = fuse_flush_times(inode, ff); if (ff) fuse_file_put(ff, false); diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 72aabf8dd5ff31..b03f7e8df4e9cf 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -224,6 +224,10 @@ enum { FUSE_I_BTIME, /* Wants or already has page cache IO */ FUSE_I_CACHE_IO_MODE, + /* Locally updated atime needs to be flushed to the server */ + FUSE_I_ATIME_DIRTY, + /* Locally updated mtime/ctime need to be flushed to the server */ + FUSE_I_CMTIME_DIRTY, }; struct fuse_conn; diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index a2d496160da4c5..7b122b3d626005 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -400,6 +400,19 @@ void fuse_change_attributes_common(struct inode *inode, struct fuse_attr *attr, attr->mtimensec = min_t(u32, attr->mtimensec, NSEC_PER_SEC - 1); attr->ctimensec = min_t(u32, attr->ctimensec, NSEC_PER_SEC - 1); + if (test_bit(FUSE_I_ATIME_DIRTY, &fi->state)) { + /* + * The kernel updated atime, but another client may have + * advanced it on the server. Keep whichever is newer. + */ + struct timespec64 atime = inode_get_atime(inode); + if ((u64) atime.tv_sec > attr->atime || + ((u64) atime.tv_sec == attr->atime && + (u32) atime.tv_nsec > attr->atimensec)) { + attr->atime = atime.tv_sec; + attr->atimensec = atime.tv_nsec; + } + } inode_set_atime(inode, attr->atime, attr->atimensec); /* mtime from server may be stale due to local buffered write */ if (!(cache_mask & STATX_MTIME)) { @@ -571,6 +584,7 @@ static void fuse_init_inode(struct inode *inode, struct fuse_attr *attr, { inode->i_mode = attr->mode & S_IFMT; inode->i_size = attr->size; + inode_set_atime(inode, attr->atime, attr->atimensec); inode_set_mtime(inode, attr->mtime, attr->mtimensec); inode_set_ctime(inode, attr->ctime, attr->ctimensec); if (S_ISREG(inode->i_mode)) { @@ -655,7 +669,8 @@ struct inode *fuse_iget(struct super_block *sb, u64 nodeid, return NULL; if ((inode->i_state & I_NEW)) { - inode->i_flags |= S_NOATIME; + if (sb->s_flags & SB_NOATIME) + inode->i_flags |= S_NOATIME; if (!fc->writeback_cache || !S_ISREG(attr->mode)) inode->i_flags |= S_NOCMTIME; inode->i_generation = generation;