[PATCH v4] fs: avoid spurious dentry ref/unref cycle on open

Mateusz Guzik <[email protected]>
Newsgroups gmane.linux.file-systems,gmane.linux.kernel
Message-ID <[email protected]>
Opening a file grabs a reference on the terminal dentry in
__legitimize_path(), then another one in do_dentry_open() and finally
drops the initial reference in terminate_walk().

That's 2 modifications which don't need to be there -- do_dentry_open()
can consume the already held reference instead.

When benchmarking on a 20-core vm using will-it-scale to open the same
file read-only, the results are (ops/s):
before:	4043375
after:	5629378 (+39%)

Signed-off-by: Mateusz Guzik <[email protected]>
---

The spurious ref cycle remains an issue and it is trivially avoidable,
for the most common case anyway.

Al Viro had a more involved patchset which got stalled, see:
https://lore.kernel.org/linux-fsdevel/20240822003359.GO504335@ZenIV/

I already pointed this out over a year ago when sending v3.

Given lack of traffic on the more involved variant, the nice win from my
simple patch and its overall triviality, I think it should go in. Worst
case, if the more involved work ever gets off the ground it can be
trivially reverted later.

bench is:
$ cat tests/openro3.c

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>

static char tmpfile[] = "/tmp/willitscale.XXXXXX";

char *testcase_description = "Same file open/close read-only";

void testcase_prepare(unsigned long nr_tasks)
{
	int fd = mkstemp(tmpfile);

	assert(fd >= 0);
	close(fd);
}

void testcase(unsigned long long *iterations, unsigned long nr)
{
	while (1) {
		int fd = open(tmpfile, O_RDONLY);
		assert(fd >= 0);
		close(fd);

		(*iterations)++;
	}
}

void testcase_cleanup(void)
{
	unlink(tmpfile);
}

v4:
- rebase
- don't grab the extra ref on mnt for truncate
- bench opening things r/o. note perf improved from last year thanks to
  other changes

 fs/internal.h |  1 +
 fs/namei.c    | 13 ++++++++++---
 fs/open.c     | 27 ++++++++++++++++++++++++++-
 3 files changed, 37 insertions(+), 4 deletions(-)

diff --git a/fs/internal.h b/fs/internal.h
index c658c8a5ebd5..9632239036ac 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -205,6 +205,7 @@ int do_fchownat(int dfd, const char __user *filename, uid_t user, gid_t group,
 		int flag);
 int chown_common(const struct path *path, uid_t user, gid_t group);
 extern int vfs_open(const struct path *, struct file *);
+int vfs_open_consume(struct path *, struct file *);
 
 /*
  * inode.c
diff --git a/fs/namei.c b/fs/namei.c
index 3f9bf103ba12..cf79ecedf288 100644
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -4789,6 +4789,7 @@ static const char *open_last_lookups(struct nameidata *nd,
 static int do_open(struct nameidata *nd,
 		   struct file *file, const struct open_flags *op)
 {
+	struct vfsmount *mnt;
 	struct mnt_idmap *idmap;
 	int open_flag = op->open_flag;
 	bool do_truncate;
@@ -4827,14 +4828,20 @@ static int do_open(struct nameidata *nd,
 		open_flag &= ~O_TRUNC;
 		acc_mode = 0;
 	} else if (d_is_reg(nd->path.dentry) && open_flag & O_TRUNC) {
-		error = mnt_want_write(nd->path.mnt);
+		/*
+		 * Stash the mount point before vfs_open_consume() whacks nd->path.
+		 * It is safely accessible because the file obj using it is guaranteed
+		 * to not disappear while we execute.
+		 */
+		mnt = nd->path.mnt;
+		error = mnt_want_write(mnt);
 		if (error)
 			return error;
 		do_truncate = true;
 	}
 	error = may_open(idmap, &nd->path, acc_mode, open_flag);
 	if (!error && !(file->f_mode & FMODE_OPENED))
-		error = vfs_open(&nd->path, file);
+		error = vfs_open_consume(&nd->path, file);
 	if (!error)
 		error = security_file_post_open(file, op->acc_mode);
 	if (!error && do_truncate)
@@ -4844,7 +4851,7 @@ static int do_open(struct nameidata *nd,
 		error = -EINVAL;
 	}
 	if (do_truncate)
-		mnt_drop_write(nd->path.mnt);
+		mnt_drop_write(mnt);
 	return error;
 }
 
diff --git a/fs/open.c b/fs/open.c
index 6b1c14e684a9..2a7697cee00b 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -931,6 +931,11 @@ static inline int file_get_write_access(struct file *f)
 	return error;
 }
 
+/*
+ * Populate struct file
+ *
+ * NOTE: it assumes f_path is populated and consumes the caller's reference.
+ */
 static int do_dentry_open(struct file *f,
 			  int (*open)(struct inode *, struct file *))
 {
@@ -938,7 +943,6 @@ static int do_dentry_open(struct file *f,
 	struct inode *inode = f->f_path.dentry->d_inode;
 	int error;
 
-	path_get(&f->f_path);
 	f->f_inode = inode;
 	f->f_mapping = inode->i_mapping;
 	f->f_wb_err = filemap_sample_wb_err(f->f_mapping);
@@ -1055,6 +1059,7 @@ int finish_open(struct file *file, struct dentry *dentry,
 	BUG_ON(file->f_mode & FMODE_OPENED); /* once it's opened, it's opened */
 
 	file->__f_path.dentry = dentry;
+	path_get(&file->f_path);
 	return do_dentry_open(file, open);
 }
 EXPORT_SYMBOL(finish_open);
@@ -1098,6 +1103,7 @@ int vfs_open(const struct path *path, struct file *file)
 	int ret;
 
 	file->__f_path = *path;
+	path_get(&file->f_path);
 	ret = do_dentry_open(file, NULL);
 	if (!ret) {
 		/*
@@ -1110,6 +1116,25 @@ int vfs_open(const struct path *path, struct file *file)
 	return ret;
 }
 
+/**
+ * vfs_open_consume - open the file at the given path and consume the reference
+ * @path: path to open
+ * @file: newly allocated file with f_flag initialized
+ */
+int vfs_open_consume(struct path *path, struct file *file)
+{
+	int ret;
+
+	file->__f_path = *path;
+	path->mnt = NULL;
+	path->dentry = NULL;
+	ret = do_dentry_open(file, NULL);
+	if (!ret) {
+		fsnotify_open(file);
+	}
+	return ret;
+}
+
 struct file *dentry_open(const struct path *path, int flags,
 			 const struct cred *cred)
 {
-- 
2.53.0
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.