mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Jonathan Flynn <jonathan.flynn@hammerspace.com>
To: NeilBrown <neil@brown.name>,
	Alexander Viro <viro@zeniv.linux.org.uk>,
	Christian Brauner <brauner@kernel.org>,
	Trond Myklebust <trondmy@kernel.org>,
	Anna Schumaker <anna@kernel.org>
Cc: Jeff Layton <jlayton@kernel.org>, Chuck Lever <cel@kernel.org>,
	Jan Kara <jack@suse.cz>, Jonathan Corbet <corbet@lwn.net>,
	linux-doc@vger.kernel.org, linux-nfs@vger.kernel.org,
	linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [PATCH RFC 09/12] nfs: do not cache a name from a listing that a removal overlapped
Date: Thu, 29 Oct 2026 12:09:54 -0600	[thread overview]
Message-ID: <20261029180957.387138-10-jonathan.flynn@hammerspace.com> (raw)
In-Reply-To: <20261029180957.387138-1-jonathan.flynn@hammerspace.com>

With a removal holding the directory lock shared, a directory listing,
which holds it shared too, can run beside it, and the server can build
the listing before the removal while the client reads it after.  The
client would then put the removed name back in its cache as a valid
entry, and keep it for as long as it trusts its cached view of the
directory.

Count, per directory, the removals made with the directory locked shared:
those waiting for the server's answer, and those that have had it.  A
listing notes the second count before it is requested, and adds no new
name to the cache while a removal is waiting or once one has been
answered since.  Positive entries already in the cache are covered by
"nfs: keep the dentry of a shared-parent removal hashed".

Nothing counts a removal yet, so nothing changes.  NFSv2 and NFSv3
listings note the count too, and NFSv3 listings that return attributes
make the same check; the behavior of both is unchanged.

Signed-off-by: Jonathan Flynn <jonathan.flynn@hammerspace.com>
---
 fs/nfs/dir.c           | 16 +++++++++--
 fs/nfs/inode.c         |  2 ++
 fs/nfs/internal.h      | 63 ++++++++++++++++++++++++++++++++++++++++++
 include/linux/nfs_fs.h |  8 ++++++
 4 files changed, 87 insertions(+), 2 deletions(-)

diff --git a/fs/nfs/dir.c b/fs/nfs/dir.c
index 2a4c059064..d9ba3cb1d7 100644
--- a/fs/nfs/dir.c
+++ b/fs/nfs/dir.c
@@ -165,6 +165,7 @@ struct nfs_readdir_descriptor {
 
 	__be32		verf[NFS_DIR_VERIFIER_SIZE];
 	unsigned long	dir_verifier;
+	unsigned long	shared_removal_seq;
 	unsigned long	timestamp;
 	unsigned long	gencount;
 	unsigned long	attr_gencount;
@@ -603,6 +604,7 @@ static int nfs_readdir_xdr_filler(struct nfs_readdir_descriptor *desc,
 	timestamp = jiffies;
 	gencount = nfs_inc_attr_generation_counter();
 	desc->dir_verifier = nfs_save_change_attribute(inode);
+	desc->shared_removal_seq = nfs_dir_shared_removal_seq(inode);
 	error = NFS_PROTO(inode)->readdir(&arg, &res);
 	if (error < 0) {
 		/* We requested READDIRPLUS, but the server doesn't grok it */
@@ -723,7 +725,7 @@ static void nfs_lookup_advise_force_readdirplus(struct inode *dir,
 
 static
 void nfs_prime_dcache(struct dentry *parent, struct nfs_entry *entry,
-		unsigned long dir_verifier)
+		      unsigned long dir_verifier, unsigned long removal_seq)
 {
 	struct qstr filename = QSTR_INIT(entry->name, entry->len);
 	struct dentry *dentry;
@@ -825,6 +827,15 @@ void nfs_prime_dcache(struct dentry *parent, struct nfs_entry *entry,
 		d_lookup_done(dentry);
 		goto out;
 	}
+	/*
+	 * Make no dentry for a name that a removal under a shared parent may
+	 * have taken away after the server built this listing; see
+	 * nfs_dir_shared_removal_since().
+	 */
+	if (nfs_dir_shared_removal_since(d_inode(parent), removal_seq)) {
+		d_lookup_done(dentry);
+		goto out;
+	}
 
 	nfs_set_verifier(dentry, dir_verifier);
 	inode = nfs_fhget(dentry->d_sb, entry->fh, entry->fattr);
@@ -853,7 +864,8 @@ static int nfs_readdir_entry_decode(struct nfs_readdir_descriptor *desc,
 	ret = xdr_decode(desc, entry, stream);
 	if (ret || !desc->plus)
 		return ret;
-	nfs_prime_dcache(file_dentry(desc->file), entry, desc->dir_verifier);
+	nfs_prime_dcache(file_dentry(desc->file), entry, desc->dir_verifier,
+			 desc->shared_removal_seq);
 	return 0;
 }
 
diff --git a/fs/nfs/inode.c b/fs/nfs/inode.c
index e0a2e92952..7885216a48 100644
--- a/fs/nfs/inode.c
+++ b/fs/nfs/inode.c
@@ -426,6 +426,8 @@ static void nfs_inode_init_dir(struct nfs_inode *nfsi)
 	nfsi->cache_change_attribute = 0;
 	memset(nfsi->cookieverf, 0, sizeof(nfsi->cookieverf));
 	init_rwsem(&nfsi->rmdir_sem);
+	nfsi->shared_removal_seq = 0;
+	nfsi->shared_removals = 0;
 }
 
 /*
diff --git a/fs/nfs/internal.h b/fs/nfs/internal.h
index 907ea27ea2..9cd8e5e808 100644
--- a/fs/nfs/internal.h
+++ b/fs/nfs/internal.h
@@ -152,6 +152,69 @@ static inline void nfs_d_drop_unless_anchored(struct dentry *dentry)
 	spin_unlock(&dentry->d_lock);
 }
 
+/*
+ * readdir holds the directory shared, so a listing can run beside a removal
+ * made under a shared parent, and the server can build the listing before
+ * the REMOVE while the client decodes it after.  Once the removal is done
+ * its anchor no longer stands in the way - the remover's d_delete() leaves
+ * the dentry unhashed or negative - and priming the dcache from such a
+ * listing would put the removed name back, as a positive dentry with a
+ * valid verifier.
+ *
+ * So a remover counts itself in ->shared_removals from before it sends its
+ * REMOVE until the reply is in, and then advances ->shared_removal_seq,
+ * before anything it does can unhash the anchor.  readdir samples the
+ * sequence before it sends the READDIR, and makes no new dentry from the
+ * listing while a removal is counted or once the sequence has moved.  A
+ * listing whose sample is the advanced value was sent after the server had
+ * answered the REMOVE.  The count makes priming stand aside even if the
+ * anchor were to be lost while the REMOVE is in flight.  A dentry that
+ * priming finds hashed is left to the test in nfs_prime_dcache(), which
+ * leaves it alone while it is anchored and once it has been unhashed;
+ * one that the removal left negative is dropped there, and its name
+ * then gets no new dentry from such a listing either.
+ *
+ * Both fields change under the directory's ->i_lock, and the test reads
+ * them under it.  The sample is taken without the lock.
+ */
+static inline unsigned long nfs_dir_shared_removal_seq(struct inode *dir)
+{
+	return READ_ONCE(NFS_I(dir)->shared_removal_seq);
+}
+
+/*
+ * Has a removal under a shared parent been in flight in @dir since @seq was
+ * sampled, or is one in flight now?
+ */
+static inline bool nfs_dir_shared_removal_since(struct inode *dir,
+						unsigned long seq)
+{
+	struct nfs_inode *nfsi = NFS_I(dir);
+	bool since;
+
+	spin_lock(&dir->i_lock);
+	since = nfsi->shared_removals || nfsi->shared_removal_seq != seq;
+	spin_unlock(&dir->i_lock);
+	return since;
+}
+
+static inline void nfs_dir_start_shared_removal(struct inode *dir)
+{
+	spin_lock(&dir->i_lock);
+	NFS_I(dir)->shared_removals++;
+	spin_unlock(&dir->i_lock);
+}
+
+static inline void nfs_dir_end_shared_removal(struct inode *dir)
+{
+	struct nfs_inode *nfsi = NFS_I(dir);
+
+	spin_lock(&dir->i_lock);
+	nfsi->shared_removals--;
+	WRITE_ONCE(nfsi->shared_removal_seq, nfsi->shared_removal_seq + 1);
+	spin_unlock(&dir->i_lock);
+}
+
 static inline fmode_t flags_to_mode(int flags)
 {
 	fmode_t res = (__force fmode_t)flags & FMODE_EXEC;
diff --git a/include/linux/nfs_fs.h b/include/linux/nfs_fs.h
index b45952727c..3e86449485 100644
--- a/include/linux/nfs_fs.h
+++ b/include/linux/nfs_fs.h
@@ -205,6 +205,14 @@ struct nfs_inode {
 			/* Readers: in-flight sillydelete RPC calls */
 			/* Writers: rmdir */
 			struct rw_semaphore	rmdir_sem;
+			/*
+			 * Removals made with i_rwsem held shared: how many
+			 * have finished, and how many are waiting for the
+			 * reply to their REMOVE.  Both change under
+			 * ->i_lock; see fs/nfs/internal.h.
+			 */
+			unsigned long	shared_removal_seq;
+			unsigned int	shared_removals;
 		};
 		/* Regular file */
 		struct {
-- 
2.43.0


  parent reply	other threads:[~2026-10-29 18:10 UTC|newest]

Thread overview: 31+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-28  4:06 [PATCH v4 0/9] nfs: prepare for locking changes NeilBrown
2026-10-28  4:06 ` [PATCH v4 1/9] nfs: fix open-blocking with d_fsdata NeilBrown
2026-10-28  4:06 ` [PATCH v4 2/9] nfs: fix nfs_call_unlink() NeilBrown
2026-10-29 18:09   ` [PATCH 0/3] nfs: fix the silly-delete hand-off that 2/9 makes reachable Jonathan Flynn
2026-10-29 18:09     ` [PATCH 1/3] nfs: do not transfer a sillydelete onto a blocked dentry Jonathan Flynn
2026-10-29 18:09     ` [PATCH 2/3] nfs: answer a handed-off silly-delete on a removal target with -EBUSY Jonathan Flynn
2026-10-29 18:09     ` [PATCH 3/3] nfs: take rmdir_sem for a silly-delete only when its REMOVE is sent Jonathan Flynn
2026-10-29 21:58     ` [PATCH 0/3] nfs: fix the silly-delete hand-off that 2/9 makes reachable NeilBrown
2026-10-28  4:06 ` [PATCH v4 3/9] nfs: remove d_drop()/d_alloc_parallel() from nfs_atomic_open() NeilBrown
2026-10-28  4:06 ` [PATCH v4 4/9] nfs: use d_splice_alias() in nfs_link() NeilBrown
2026-10-28  4:06 ` [PATCH v4 5/9] nfs: don't d_drop() before d_splice_alias() NeilBrown
2026-10-28  4:06 ` [PATCH v4 6/9] nfs: don't d_drop() before d_splice_alias() in atomic_create NeilBrown
2026-10-28  4:06 ` [PATCH v4 7/9] nfs: Use d_alloc_trylock() in nfs_prime_dcache() NeilBrown
2026-10-28  4:06 ` [PATCH v4 8/9] nfs: use d_alloc_trylock() in silly-rename NeilBrown
2026-10-28  4:06 ` [PATCH v4 9/9] nfs: use d_duplicate() NeilBrown
2026-10-28 17:21 ` [PATCH v4 0/9] nfs: prepare for locking changes Anna Schumaker
2026-10-29 18:09 ` [PATCH RFC 00/12] vfs, nfs: concurrent create and unlink in one directory Jonathan Flynn
2026-10-29 18:09   ` [PATCH RFC 01/12] nfs: update a server's capabilities in one step Jonathan Flynn
2026-10-29 18:09   ` [PATCH RFC 02/12] vfs: allow the parent lock to be shared for O_CREAT Jonathan Flynn
2026-10-29 18:09   ` [PATCH RFC 03/12] nfs: tell the VFS when two names can be one entry on the server Jonathan Flynn
2026-10-29 22:23     ` NeilBrown
2026-10-29 18:09   ` [PATCH RFC 04/12] nfs: concurrent O_CREAT in one NFSv4 directory Jonathan Flynn
2026-10-29 18:09   ` [PATCH RFC 05/12] vfs: factor file removal into a helper, no functional change Jonathan Flynn
2026-10-29 18:09   ` [PATCH RFC 06/12] vfs: let a filesystem remove files with the parent directory locked shared Jonathan Flynn
2026-10-29 18:09   ` [PATCH RFC 07/12] nfs: add a second mark for a removal made with the parent " Jonathan Flynn
2026-10-29 18:09   ` [PATCH RFC 08/12] nfs: keep the dentry of a shared-parent removal hashed Jonathan Flynn
2026-10-29 18:09   ` Jonathan Flynn [this message]
2026-10-29 18:09   ` [PATCH RFC 10/12] nfs: add the removal method for a parent directory locked shared Jonathan Flynn
2026-10-29 18:09   ` [PATCH RFC 11/12] nfs: look a name up again once its removal has unhashed it Jonathan Flynn
2026-10-29 18:09   ` [PATCH RFC 12/12] nfs: remove files with the parent directory locked shared on NFSv4 Jonathan Flynn
2026-10-29 22:18   ` [PATCH RFC 00/12] vfs, nfs: concurrent create and unlink in one directory NeilBrown

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261029180957.387138-10-jonathan.flynn@hammerspace.com \
    --to=jonathan.flynn@hammerspace.com \
    --cc=anna@kernel.org \
    --cc=brauner@kernel.org \
    --cc=cel@kernel.org \
    --cc=corbet@lwn.net \
    --cc=jack@suse.cz \
    --cc=jlayton@kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-nfs@vger.kernel.org \
    --cc=neil@brown.name \
    --cc=trondmy@kernel.org \
    --cc=viro@zeniv.linux.org.uk \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®