Commit 0ef625bba6fb ("vfs: support statx(..., NULL, AT_EMPTY_PATH, ...)") added support for passing in NULL when AT_EMPTY_PATH is given, improving performance when statx is used for fetching stat informantion from a given fd, which is especially important for 32-bit platforms. This commit also improved the performance when an empty string is given by short-circuiting the handling of such paths.
This series is based on the commits in the Linus’ tree. Modifications are applied to vfs_statx_path(). In the original patch, vfs_statx_path() was created to warp around the call to vfs_getattr() after filename_lookup() in vfs_statx(). Since the coresponding code is different in 5.15 and 5.10, the content of vfs_statx_path() is modified to match this. The original patch also moved path_mounted() from namespace.c to internal.h, which is not applicable for 5.15 and 5.10 since it has not been introduced before 6.5. The original patch also used CLASS(fd_raw, ) to convert a file descriptor number provided from the user space in to a struct and automatically release it afterwards. Since CLASS mechanism is only available since 6.1.79, obtaining and releasing fd struct is done manually. do_statx() was directly handling filename string instead of a struct filename * before 5.18, as a result short-circuiting is implemented in do_statx() instead of sys_statx, without the need of introducing do_statx_fd().
Tested-by: Xi Ruoyao xry111@xry111.site Signed-off-by: Miao Wang shankerwangmiao@gmail.com --- Christian Brauner (2): fs: new helper vfs_empty_path() stat: use vfs_empty_path() helper
Linus Torvalds (1): vfs: mostly undo glibc turning 'fstat()' into 'fstatat(AT_EMPTY_PATH)'
Mateusz Guzik (1): vfs: support statx(..., NULL, AT_EMPTY_PATH, ...)
fs/stat.c | 73 +++++++++++++++++++++++++++++++++++++++++++++--------- include/linux/fs.h | 17 +++++++++++++ 2 files changed, 78 insertions(+), 12 deletions(-) --- base-commit: 3a5928702e7120f83f703fd566082bfb59f1a57e change-id: 20240918-statx-stable-linux-5-15-y-9a30358a7d47
Best regards,
From: Linus Torvalds torvalds@linux-foundation.org
commit 9013c51 upstream.
Mateusz reports that glibc turns 'fstat()' calls into 'fstatat()', and that seems to have been going on for quite a long time due to glibc having tried to simplify its stat logic into just one point.
This turns out to cause completely unnecessary overhead, where we then go off and allocate the kernel side pathname, and actually look up the empty path. Sure, our path lookup is quite optimized, but it still causes a fair bit of allocation overhead and a couple of completely unnecessary rounds of lockref accesses etc.
This is all hopefully getting fixed in user space, and there is a patch floating around for just having glibc use the native fstat() system call. But even with the current situation we can at least improve on things by catching the situation and short-circuiting it.
Note that this is still measurably slower than just a plain 'fstat()', since just checking that the filename is actually empty is somewhat expensive due to inevitable user space access overhead from the kernel (ie verifying pointers, and SMAP on x86). But it's still quite a bit faster than actually looking up the path for real.
To quote numers from Mateusz: "Sapphire Rapids, will-it-scale, ops/s
stock fstat 5088199 patched fstat 7625244 (+49%) real fstat 8540383 (+67% / +12%)"
where that 'stock fstat' is the glibc translation of fstat into fstatat() with an empty path, the 'patched fstat' is with this short circuiting of the path lookup, and the 'real fstat' is the actual native fstat() system call with none of this overhead.
Link: https://lore.kernel.org/lkml/20230903204858.lv7i3kqvw6eamhgz@f/ Reported-by: Mateusz Guzik mjguzik@gmail.com Signed-off-by: Linus Torvalds torvalds@linux-foundation.org
Cc: stable@vger.kernel.org # 5.10.x-5.15.x Signed-off-by: Miao Wang shankerwangmiao@gmail.com Tested-by: Xi Ruoyao xry111@xry111.site --- fs/stat.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+)
diff --git a/fs/stat.c b/fs/stat.c index 246d138ec066..9669f3268286 100644 --- a/fs/stat.c +++ b/fs/stat.c @@ -240,6 +240,22 @@ static int vfs_statx(int dfd, const char __user *filename, int flags, int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat, int flags) { + /* + * Work around glibc turning fstat() into fstatat(AT_EMPTY_PATH) + * + * If AT_EMPTY_PATH is set, we expect the common case to be that + * empty path, and avoid doing all the extra pathname work. + */ + if (dfd >= 0 && flags == AT_EMPTY_PATH) { + char c; + + ret = get_user(c, filename); + if (unlikely(ret)) + return ret; + + if (likely(!c)) + return vfs_fstat(dfd, stat); + } return vfs_statx(dfd, filename, flags | AT_NO_AUTOMOUNT, stat, STATX_BASIC_STATS); }
From: Christian Brauner brauner@kernel.org
commit 1bc6d44 upstream.
Make it possible to quickly check whether AT_EMPTY_PATH is valid. Note, after some discussion we decided to also allow NULL to be passed instead of requiring the empty string.
Signed-off-by: Christian Brauner brauner@kernel.org
Cc: stable@vger.kernel.org # 5.10.x-5.15.x Signed-off-by: Miao Wang shankerwangmiao@gmail.com Tested-by: Xi Ruoyao xry111@xry111.site --- include/linux/fs.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+)
diff --git a/include/linux/fs.h b/include/linux/fs.h index 27da89d0ed5a..6fe2ae89bba6 100644 --- a/include/linux/fs.h +++ b/include/linux/fs.h @@ -3736,4 +3736,21 @@ static inline int inode_drain_writes(struct inode *inode) return filemap_write_and_wait(inode->i_mapping); }
+static inline bool vfs_empty_path(int dfd, const char __user *path) +{ + char c; + + if (dfd < 0) + return false; + + /* We now allow NULL to be used for empty path. */ + if (!path) + return true; + + if (unlikely(get_user(c, path))) + return false; + + return !c; +} + #endif /* _LINUX_FS_H */
From: Christian Brauner brauner@kernel.org
commit 27a2d0c upstream.
Use the newly added helper for this.
Signed-off-by: Christian Brauner brauner@kernel.org
Cc: stable@vger.kernel.org # 5.10.x-5.15.x Signed-off-by: Miao Wang shankerwangmiao@gmail.com Tested-by: Xi Ruoyao xry111@xry111.site --- fs/stat.c | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-)
diff --git a/fs/stat.c b/fs/stat.c index 9669f3268286..b8faa3f4b046 100644 --- a/fs/stat.c +++ b/fs/stat.c @@ -246,16 +246,9 @@ int vfs_fstatat(int dfd, const char __user *filename, * If AT_EMPTY_PATH is set, we expect the common case to be that * empty path, and avoid doing all the extra pathname work. */ - if (dfd >= 0 && flags == AT_EMPTY_PATH) { - char c; + if (flags == AT_EMPTY_PATH && vfs_empty_path(dfd, filename)) + return vfs_fstat(dfd, stat);
- ret = get_user(c, filename); - if (unlikely(ret)) - return ret; - - if (likely(!c)) - return vfs_fstat(dfd, stat); - } return vfs_statx(dfd, filename, flags | AT_NO_AUTOMOUNT, stat, STATX_BASIC_STATS); }
From: Mateusz Guzik mjguzik@gmail.com
commit 0ef625b upstream.
The newly used helper also checks for empty ("") paths.
NULL paths with any flag value other than AT_EMPTY_PATH go the usual route and end up with -EFAULT to retain compatibility (Rust is abusing calls of the sort to detect availability of statx).
This avoids path lookup code, lockref management, memory allocation and in case of NULL path userspace memory access (which can be quite expensive with SMAP on x86_64).
Benchmarked with statx(..., AT_EMPTY_PATH, ...) running on Sapphire Rapids, with the "" path for the first two cases and NULL for the last one.
Results in ops/s: stock: 4231237 pre-check: 5944063 (+40%) NULL path: 6601619 (+11%/+56%)
Signed-off-by: Mateusz Guzik mjguzik@gmail.com Link: https://lore.kernel.org/r/20240625151807.620812-1-mjguzik@gmail.com Tested-by: Xi Ruoyao xry111@xry111.site [brauner: use path_mounted() and other tweaks] Signed-off-by: Christian Brauner brauner@kernel.org
Cc: stable@vger.kernel.org # 5.10.x-5.15.x Signed-off-by: Miao Wang shankerwangmiao@gmail.com Tested-by: Xi Ruoyao xry111@xry111.site --- fs/stat.c | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 12 deletions(-)
diff --git a/fs/stat.c b/fs/stat.c index b8faa3f4b046..f02361a2ae54 100644 --- a/fs/stat.c +++ b/fs/stat.c @@ -184,6 +184,37 @@ int vfs_fstat(int fd, struct kstat *stat) return error; }
+static int vfs_statx_path(struct path *path, int flags, struct kstat *stat, + u32 request_mask) +{ + int error = vfs_getattr(path, stat, request_mask, flags); + + stat->mnt_id = real_mount(path->mnt)->mnt_id; + stat->result_mask |= STATX_MNT_ID; + if (path->mnt->mnt_root == path->dentry) + stat->attributes |= STATX_ATTR_MOUNT_ROOT; + stat->attributes_mask |= STATX_ATTR_MOUNT_ROOT; + + return error; +} + +static int vfs_statx_fd(int fd, int flags, struct kstat *stat, + u32 request_mask) +{ + int rc; + struct fd f = fdget_raw(fd); + + if (!f.file) { + rc = -EBADF; + goto err; + } + rc = vfs_statx_path(&f.file->f_path, flags, stat, request_mask); + +err: + fdput(f); + return rc; +} + /** * vfs_statx - Get basic and extra attributes by filename * @dfd: A file descriptor representing the base dir for a relative filename @@ -220,20 +251,13 @@ static int vfs_statx(int dfd, const char __user *filename, int flags, retry: error = user_path_at(dfd, filename, lookup_flags, &path); if (error) - goto out; - - error = vfs_getattr(&path, stat, request_mask, flags); - stat->mnt_id = real_mount(path.mnt)->mnt_id; - stat->result_mask |= STATX_MNT_ID; - if (path.mnt->mnt_root == path.dentry) - stat->attributes |= STATX_ATTR_MOUNT_ROOT; - stat->attributes_mask |= STATX_ATTR_MOUNT_ROOT; + return error; + error = vfs_statx_path(&path, flags, stat, request_mask); path_put(&path); if (retry_estale(error, lookup_flags)) { lookup_flags |= LOOKUP_REVAL; goto retry; } -out: return error; }
@@ -615,13 +639,28 @@ int do_statx(int dfd, const char __user *filename, unsigned flags, { struct kstat stat; int error; + unsigned lflags;
if (mask & STATX__RESERVED) return -EINVAL; if ((flags & AT_STATX_SYNC_TYPE) == AT_STATX_SYNC_TYPE) return -EINVAL;
- error = vfs_statx(dfd, filename, flags, &stat, mask); + /* + * Short-circuit handling of NULL and "" paths. + * + * For a NULL path we require and accept only the AT_EMPTY_PATH flag + * (possibly |'d with AT_STATX flags). + * + * However, glibc on 32-bit architectures implements fstatat as statx + * with the "" pathname and AT_NO_AUTOMOUNT | AT_EMPTY_PATH flags. + * Supporting this results in the uglification below. + */ + lflags = flags & ~(AT_NO_AUTOMOUNT | AT_STATX_SYNC_TYPE); + if (lflags == AT_EMPTY_PATH && vfs_empty_path(dfd, filename)) + error = vfs_statx_fd(dfd, flags & ~AT_NO_AUTOMOUNT, &stat, mask); + else + error = vfs_statx(dfd, filename, flags, &stat, mask); if (error) return error;
@@ -631,13 +670,14 @@ int do_statx(int dfd, const char __user *filename, unsigned flags, /** * sys_statx - System call to get enhanced stats * @dfd: Base directory to pathwalk from *or* fd to stat. - * @filename: File to stat or "" with AT_EMPTY_PATH + * @filename: File to stat or either NULL or "" with AT_EMPTY_PATH * @flags: AT_* flags to control pathwalk. * @mask: Parts of statx struct actually required. * @buffer: Result buffer. * * Note that fstat() can be emulated by setting dfd to the fd of interest, - * supplying "" as the filename and setting AT_EMPTY_PATH in the flags. + * supplying "" (or preferably NULL) as the filename and setting AT_EMPTY_PATH + * in the flags. */ SYSCALL_DEFINE5(statx, int, dfd, const char __user *, filename, unsigned, flags,
linux-stable-mirror@lists.linaro.org