From fb9e3045235fa12e77ba6b084e77ac07a5c1bdfe Mon Sep 17 00:00:00 2001 From: Yong Ze Chen Date: Sat, 10 Oct 2026 08:41:42 +0000 Subject: [PATCH] Linux 6.12:fuse: do not zero-fill another node's bytes past a stale EOF under DLM With the writeback cache and DLM, ior-hard (47008-byte transfers into one shared file from several clients) reads back zeros in pages that ranks on two client nodes share. Under DLM the local i_size is only this node's view: a write refreshes STATX_MODE, not the size, and the grant does not carry it. Once another node has extended the file, two paths trust the stale size and zero that node's bytes, and writeback sends the zeros over them once this node's i_size covers the folio. fuse_write_begin() skips the read-modify-write READ for a partial folio that starts at or past the local i_size and zero-fills it instead. Under DLM, read it. The write grant is page-aligned and was granted only after the previous holder's revoke flushed the page, so the read is coherent, and a true hole still reads back as zeros. It adds no new kind of wait under the LOCKED range: the same READ is already issued for every partial folio inside the known size. The cost is at most two READs per write beyond the local i_size. fuse_zero_eof_gap() zeroes the cached old-EOF folio from the local i_size to its end before an extending write, assuming the server's side of that gap is a hole. Under DLM a folio read from the server can hold another node's bytes there, and the read above makes that common. The bytes this function exists to clear can only come from a shared writable mapping, so add FUSE_I_SHARED_WRITE_MMAP, set in fuse_file_mmap() for such a mapping and never cleared, and skip the zeroing under DLM with the writeback cache unless it is set. A file both mapped shared-writable and written from several nodes keeps the old behavioour. Signed-off-by: Co-Authored-By: Claude Opus 5.5 --- fs/fuse/file.c | 32 +++++++++++++++++++++++++------- fs/fuse/fuse_i.h | 8 ++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index ccc2dd45aeec71..fffef155ceb586 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1476,14 +1476,25 @@ static ssize_t fuse_send_write(struct fuse_io_args *ia, loff_t pos, * i_size still at the value read means nothing landed in the gap. When it * moved, the mover zeroed its own gap before publishing, and only what lies * past the new size is left to do. + * + * Under DLM with the writeback cache, i_size is only this node's view and the + * server's side of the gap need not be a hole: the folio may have been read + * from the server with another node's bytes past it. Zeroing would send + * zeros over them at writeback. Only a shared writable mapping can put bytes + * past EOF that need zeroing, so skip unless the inode has had one. */ void fuse_zero_eof_gap(struct inode *inode, loff_t to) { + struct fuse_conn *fc = get_fuse_conn(inode); struct fuse_inode *fi = get_fuse_inode(inode); struct folio *folio; loff_t from, end; bool done; + if (fc->dlm && fc->writeback_cache && + !test_bit(FUSE_I_SHARED_WRITE_MMAP, &fi->state)) + return; + do { from = i_size_read(inode); if (from >= to || !(from & (PAGE_SIZE - 1))) @@ -3192,9 +3203,16 @@ static int fuse_write_begin(struct file *file, struct address_space *mapping, /* * Check if the start of this folio comes after the end of file, * in which case the readpage can be optimized away. + * + * Not under DLM: there i_size is only this node's view. Another + * node extending the file does not update it, so a folio "past EOF" + * here may hold that node's bytes on the server. Zero-filling it + * would send zeros over them once writeback covers the whole folio. + * The page-aligned write grant this write holds makes the server's + * copy current, so read it. */ fsize = i_size_read(mapping->host); - if (fsize <= folio_pos(folio)) { + if (!fc->dlm && fsize <= folio_pos(folio)) { size_t off = offset_in_folio(folio, pos); if (off) folio_zero_segment(folio, 0, off); @@ -3254,11 +3272,9 @@ static int fuse_write_end(struct file *file, struct address_space *mapping, * than by an unlocked read-modify-write two concurrent extenders could * lose an update to. * - * Growing i_size just behind the write cursor, rather than claiming - * the whole extension up front, also keeps fuse_write_begin()'s - * beyond-EOF optimisation effective: folios wholly past EOF are zeroed - * locally instead of sending the server a read-modify-write READ for - * data that does not exist yet. + * This i_size is not what fuse_write_begin() trusts to skip its + * read-modify-write READ: under DLM it reads every partial folio, + * because another node may have written past this node's i_size. */ if (pos > inode->i_size) { struct fuse_inode *fi = get_fuse_inode(inode); @@ -3961,8 +3977,10 @@ static int fuse_file_mmap(struct file *file, struct vm_area_struct *vma) return rc; } - if ((vma->vm_flags & VM_SHARED) && (vma->vm_flags & VM_MAYWRITE)) + if ((vma->vm_flags & VM_SHARED) && (vma->vm_flags & VM_MAYWRITE)) { + set_bit(FUSE_I_SHARED_WRITE_MMAP, &get_fuse_inode(inode)->state); fuse_link_write_file(file); + } file_accessed(file); vma->vm_ops = &fuse_file_vm_ops; diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 99eda51ba1bc0e..4d68ce8a3d17fd 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -313,6 +313,14 @@ enum { * See fuse_reverse_inval_inode()/fuse_file_io_open(). */ FUSE_I_FORCE_DIO, + /* + * Has had a shared writable mapping. Only such a mapping can leave + * bytes past EOF in the last folio, so under DLM fuse_zero_eof_gap() + * zeroes them only when this is set. Set in fuse_file_mmap() and + * never cleared for the life of the inode: the bytes can outlive the + * mapping in the page cache. + */ + FUSE_I_SHARED_WRITE_MMAP, }; struct fuse_conn;