From: Jori Koolstra <jkoolstra@xs4all.nl>
To: Shakeel Butt <shakeel.butt@linux.dev>
Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>,
Tejun Heo <tj@kernel.org>,
Christian Brauner <christian@brauner.io>,
Meta kernel team <kernel-team@meta.com>,
linux-fsdevel@vger.kernel.org, driver-core@lists.linux.dev,
linux-kernel@vger.kernel.org
Subject: Re: [PATCH v3] kernfs: allocate inodes and dentry outside kernfs_rwsem
Date: Sun, 18 Oct 2026 22:47:56 +0200 [thread overview]
Message-ID: <atUkrrdfu_ihfmZ8@daedalus> (raw)
In-Reply-To: <20261016220029.3276154-1-shakeel.butt@linux.dev>
On Fri, Oct 16, 2026 at 03:00:29PM -0700, Shakeel Butt wrote:
> In the Meta fleet, on 6.16 with the kernfs changes queued for 7.4
> backported, we still see long cgroupfs stalls:
>
> - oomd and below waited 72 seconds in kernfs_fop_readdir() while an out
> of memory container created cgroups. They resumed when the kernel OOM
> killer killed the container's workload.
>
> - oomd waited 15 minutes in kernfs_iop_lookup(). PID 1 waited for
> kernfs_rwsem in cgroup_rmdir() while holding cgroup_mutex.
>
> - A lock profiler sampling part of the fleet saw kernfs_iop_lookup()
> hold kernfs_rwsem across memcg reclaim on 26 hosts in 29 days, for up
> to 625 ms.
>
> The profiler shows where these stalls come from. kernfs_iop_lookup()
> and the export ops allocate a new inode while holding kernfs_rwsem for
> read. The inode is charged to the caller's memcg. If that memcg is at
> memory.max, the allocation can stay in reclaim for a long time,
> sometimes until an OOM kill. A writer then queues on the lock and every
> new reader queues behind it. On cgroup2 this blocks every cgroupfs user
> on the machine.
>
> Such lookups are common in a memcg that is out of memory. Its reclaim
> drops its kernfs dentries fast, and the files of a new cgroup have no
> inodes yet.
>
> Allocate the inode before taking kernfs_rwsem, and free it if it is not
> used, as commit 8b64f32fee95 ("kernfs: allocate the new name outside
> kernfs_rwsem") and commit 5ad03af18434 ("kernfs: allocate the open node
> outside the open file mutex") do. Everything else still runs under the
> lock, so a new inode is added only for an active node, and
> __kernfs_remove() still finds every inode it must clear i_nlink on, as
> commit dc1a0cc95737 ("kernfs: don't lose IN_DELETE_SELF when decoding a
> file handle") needs.
>
> The inode goes unused when the name does not exist, the node is
> inactive, or it already has an inode. In a VM, a lookup of a missing
> name takes about 0.35 us longer, 1.65 us instead of 1.3 us on
> cgroupfs. Cold lookups of existing sysfs and cgroupfs files are not
> slower.
>
> kernfs_get_parent_dentry() also held the lock across d_obtain_alias(),
> which allocates a dentry. Hold it only around the inode lookup, as
> __kernfs_fh_to_dentry() already does. This also gives ->get_parent the
> active test that the IN_DELETE_SELF fix left out: a parent that is
> being removed and has no inode now gets -ESTALE instead of a new inode
> with i_nlink 1.
>
> kernfs_fill_super() preallocates the root inode the same way, so no
> inode is allocated under kernfs_rwsem any more.
>
> Tested on a 316-CPU machine. A task in a full 256 MB memcg with slow
> swap looks up the files of new cgroups. Worst waits in 60 seconds:
>
> before after
> oomd, for kernfs_rwsem 236 ms 0.1 ms
> below, for kernfs_rwsem 356 ms 0.8 ms
> systemd, for kernfs_rwsem 160 ms 37 us
> readdir of another directory 530 ms 3.9 ms
>
> The lookup itself is still slow. The VFS still holds its directory's
> lock, so other work in that one directory still waits.
>
> Signed-off-by: Shakeel Butt <shakeel.butt@linux.dev>
> Suggested-by: Tejun Heo <tj@kernel.org>
> ---
> Changes since v2:
> - Allocate the inode before taking kernfs_rwsem and free it if unused,
> instead of dropping and retaking the lock. kernfs_iget() becomes
> kernfs_get_inode_locked() (Tejun).
> - Do the same in kernfs_fill_super() (Tejun).
> - Describe the ->get_parent active test (Tejun).
> - Retested on another machine, numbers updated.
>
> v2: https://lore.kernel.org/20261016164345.31618-1-shakeel.butt@linux.dev
>
> Changes since v1:
> - dentry allocation outside kernfs_rwsem (Sashiko).
> - Couple of comments updated.
>
> v1: https://lore.kernel.org/20261015221110.3183461-1-shakeel.butt@linux.dev
>
> fs/kernfs/dir.c | 13 +++++----
> fs/kernfs/inode.c | 56 +++++++++++++++++++++++++++++++++++++
> fs/kernfs/kernfs-internal.h | 4 +++
> fs/kernfs/mount.c | 46 +++++++++++++++++-------------
> 4 files changed, 94 insertions(+), 25 deletions(-)
>
> diff --git a/fs/kernfs/dir.c b/fs/kernfs/dir.c
> index 7e65854a09e4..f3aaa89ad0f5 100644
> --- a/fs/kernfs/dir.c
> +++ b/fs/kernfs/dir.c
> @@ -37,7 +37,7 @@ static bool __kernfs_active(struct kernfs_node *kn)
> return atomic_read(&kn->active) >= 0;
> }
>
> -static bool kernfs_active(struct kernfs_node *kn)
> +bool kernfs_active(struct kernfs_node *kn)
> {
> lockdep_assert_held(&kernfs_root(kn)->kernfs_rwsem);
> return __kernfs_active(kn);
> @@ -1251,9 +1251,12 @@ static struct dentry *kernfs_iop_lookup(struct inode *dir,
> struct kernfs_node *parent = dir->i_private;
> struct kernfs_node *kn;
> struct kernfs_root *root;
> - struct inode *inode = NULL;
> + struct inode *inode = NULL, *new;
> const struct ns_common *ns = NULL;
>
> + /* allocate outside kernfs_rwsem, see kernfs_get_inode_locked() */
> + new = new_inode(dir->i_sb);
> +
> root = kernfs_root(parent);
> down_read(&root->kernfs_rwsem);
> if (kernfs_ns_enabled(parent))
> @@ -1267,11 +1270,10 @@ static struct dentry *kernfs_iop_lookup(struct inode *dir,
> */
> if (!kernfs_active(kn)) {
> up_read(&root->kernfs_rwsem);
> + iput(new);
> return NULL;
> }
> - inode = kernfs_get_inode(dir->i_sb, kn);
> - if (!inode)
> - inode = ERR_PTR(-ENOMEM);
> + inode = kernfs_get_inode_locked(dir->i_sb, kn, &new);
> }
> /*
> * Needed for negative dentry validation.
> @@ -1282,6 +1284,7 @@ static struct dentry *kernfs_iop_lookup(struct inode *dir,
> if (!IS_ERR(inode))
> kernfs_set_rev(parent, dentry);
> up_read(&root->kernfs_rwsem);
> + iput(new);
>
> /* instantiate and hash (possibly negative) dentry */
> return d_splice_alias(inode, dentry);
> diff --git a/fs/kernfs/inode.c b/fs/kernfs/inode.c
> index 237dcdd73fc2..030302185933 100644
> --- a/fs/kernfs/inode.c
> +++ b/fs/kernfs/inode.c
> @@ -256,6 +256,62 @@ struct inode *kernfs_get_inode(struct super_block *sb, struct kernfs_node *kn)
> return inode;
> }
>
> +static int kernfs_inode_test(struct inode *inode, void *data)
> +{
> + return inode->i_ino == *(u64 *)data;
> +}
> +
> +static int kernfs_inode_set(struct inode *inode, void *data)
> +{
> + inode->i_ino = *(u64 *)data;
> + return 0;
> +}
> +
> +/**
> + * kernfs_get_inode_locked - get the inode of @kn under kernfs_rwsem
> + * @sb: super block
> + * @kn: kernfs_node to get the inode for
> + * @new: points to an inode from new_inode(@sb), or to %NULL if that failed
> + *
> + * The caller allocates *@new before taking kernfs_rwsem, as the allocation
> + * can stall in memcg reclaim. If @kn needs a new inode, *@new is used and
I wouldn't mention memcg here per se, it is not relevant to grasp its
use. In another series I have implemented almost the same thing (but you
did it better!).
> + * set to %NULL. Otherwise the caller frees it with iput() after dropping
> + * the lock.
> + *
> + * A new inode is added only with kernfs_rwsem held and @kn active, so
> + * __kernfs_remove() always finds it to clear i_nlink.
> + *
> + * Context: kernfs_rwsem held for read.
> + *
> + * Return: the inode, %NULL if @kn is inactive and has no inode, or
> + * ERR_PTR(-ENOMEM) if @kn needs an inode and *@new is %NULL.
> + */
> +struct inode *kernfs_get_inode_locked(struct super_block *sb,
> + struct kernfs_node *kn,
> + struct inode **new)
I understand this is intended to have some kind of ownership semantics,
but I must say for such a simple function, I find the contract quite
complex to check. Do we really need an out parameter here?
> +{
> + u64 ino = kernfs_ino(kn);
> + struct inode *inode;
> +
> + lockdep_assert_held_read(&kernfs_root(kn)->kernfs_rwsem);
> +
> + inode = ilookup(sb, ino);
> + if (inode || !kernfs_active(kn))
> + return inode;
> + if (!*new)
> + return ERR_PTR(-ENOMEM);
> +
> + inode = inode_insert5(*new, ino, kernfs_inode_test, kernfs_inode_set,
> + &ino);
> + if (inode == *new) {
> + kernfs_init_inode(kn, inode);
> + *new = NULL;
> + } else if (!inode) {
> + inode = ERR_PTR(-ENOMEM);
> + }
> + return inode;
> +}
> +
> /*
> * The kernfs_node serves as both an inode and a directory entry for
> * kernfs. To prevent the kernfs inode numbers from being freed
> diff --git a/fs/kernfs/kernfs-internal.h b/fs/kernfs/kernfs-internal.h
> index 1609c1519698..bf8752434b34 100644
> --- a/fs/kernfs/kernfs-internal.h
> +++ b/fs/kernfs/kernfs-internal.h
> @@ -186,6 +186,9 @@ int kernfs_iop_getattr(struct mnt_idmap *idmap,
> u32 request_mask, unsigned int query_flags);
> ssize_t kernfs_iop_listxattr(struct dentry *dentry, char *buf, size_t size);
> int __kernfs_setattr(struct kernfs_node *kn, const struct iattr *iattr);
> +struct inode *kernfs_get_inode_locked(struct super_block *sb,
> + struct kernfs_node *kn,
> + struct inode **new);
>
> /*
> * dir.c
> @@ -194,6 +197,7 @@ extern const struct dentry_operations kernfs_dops;
> extern const struct file_operations kernfs_dir_fops;
> extern const struct inode_operations kernfs_dir_iops;
>
> +bool kernfs_active(struct kernfs_node *kn);
> struct kernfs_node *kernfs_get_active(struct kernfs_node *kn);
> void kernfs_put_active(struct kernfs_node *kn);
> int kernfs_add_one(struct kernfs_node *kn);
> diff --git a/fs/kernfs/mount.c b/fs/kernfs/mount.c
> index c15ba6357162..3633cf3b6bef 100644
> --- a/fs/kernfs/mount.c
> +++ b/fs/kernfs/mount.c
> @@ -99,8 +99,8 @@ static struct dentry *__kernfs_fh_to_dentry(struct super_block *sb,
> int fh_type, bool get_parent)
> {
> struct kernfs_super_info *info = kernfs_info(sb);
> + struct inode *inode = ERR_PTR(-ESTALE), *new;
> struct kernfs_node *kn;
> - struct inode *inode;
> u64 id;
>
> if (fh_len < 2)
> @@ -124,32 +124,29 @@ static struct dentry *__kernfs_fh_to_dentry(struct super_block *sb,
> return NULL;
> }
>
> + new = new_inode(sb);
> +
> /*
> - * Hold kernfs_rwsem across the lookup as well as kernfs_get_inode().
> - * __kernfs_remove() deactivates the subtree and clears i_nlink on its
> - * inodes under the write lock, so under the read lock either
> - * kernfs_find_and_get_node_by_id() refuses the node, or the inode is
> - * in the inode hash before the ilookup() pass goes looking for it.
> + * Hold kernfs_rwsem from the lookup through kernfs_get_inode_locked().
> + * Then __kernfs_remove() either finds the inode to clear i_nlink, or
> + * the node is already inactive and no inode is created.
> */
> scoped_guard(rwsem_read, &info->root->kernfs_rwsem) {
> kn = kernfs_find_and_get_node_by_id(info->root, id);
> - if (!kn)
> - return ERR_PTR(-ESTALE);
> -
> - if (get_parent) {
> + if (kn && get_parent) {
> struct kernfs_node *parent;
>
> parent = kernfs_get_parent(kn);
> kernfs_put(kn);
> kn = parent;
> - if (!kn)
> - return ERR_PTR(-ESTALE);
> }
> -
> - inode = kernfs_get_inode(sb, kn);
> - kernfs_put(kn);
> + if (kn) {
> + inode = kernfs_get_inode_locked(sb, kn, &new);
> + kernfs_put(kn);
> + }
> }
>
> + iput(new);
> return d_obtain_alias(inode);
> }
>
> @@ -171,9 +168,16 @@ static struct dentry *kernfs_get_parent_dentry(struct dentry *child)
> {
> struct kernfs_node *kn = kernfs_dentry_node(child);
> struct kernfs_root *root = kernfs_root(kn);
> + struct inode *inode, *new;
> +
> + new = new_inode(child->d_sb);
> + scoped_guard(rwsem_read, &root->kernfs_rwsem) {
> + inode = kernfs_get_inode_locked(child->d_sb, kernfs_parent(kn),
> + &new);
> + }
>
> - guard(rwsem_read)(&root->kernfs_rwsem);
> - return d_obtain_alias(kernfs_get_inode(child->d_sb, kernfs_parent(kn)));
> + iput(new);
> + return d_obtain_alias(inode);
> }
>
> static const struct export_operations kernfs_export_ops = {
> @@ -294,7 +298,7 @@ static int kernfs_fill_super(struct super_block *sb, struct kernfs_fs_context *k
> {
> struct kernfs_super_info *info = kernfs_info(sb);
> struct kernfs_root *kf_root = kfc->root;
> - struct inode *inode;
> + struct inode *inode, *new;
> struct dentry *root;
>
> info->sb = sb;
> @@ -314,10 +318,12 @@ static int kernfs_fill_super(struct super_block *sb, struct kernfs_fs_context *k
> sb->s_shrink->seeks = 0;
>
> /* get root inode, initialize and unlock it */
> + new = new_inode(sb);
> down_read(&kf_root->kernfs_rwsem);
> - inode = kernfs_get_inode(sb, info->root->kn);
> + inode = kernfs_get_inode_locked(sb, info->root->kn, &new);
What are you serializing against here? I've been looking at this as well
recently, and I thought we could just get rid of the locking here. But
on second look, I am not so sure what is correct.
Is there anything that serializes kernfs_get_tree() with root node
removal?
> up_read(&kf_root->kernfs_rwsem);
> - if (!inode) {
> + iput(new);
> + if (IS_ERR_OR_NULL(inode)) {
> pr_debug("kernfs: could not get root inode\n");
> return -ENOMEM;
> }
> --
> 2.53.0-Meta
>
Thanks,
Jori.
prev parent reply other threads:[~2026-10-18 20:46 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-16 22:00 Shakeel Butt
2026-10-16 22:42 ` Tejun Heo
2026-10-17 9:14 ` Greg Kroah-Hartman
2026-10-17 17:21 ` Shakeel Butt
2026-10-18 20:47 ` Jori Koolstra [this message]
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=atUkrrdfu_ihfmZ8@daedalus \
--to=jkoolstra@xs4all.nl \
--cc=christian@brauner.io \
--cc=driver-core@lists.linux.dev \
--cc=gregkh@linuxfoundation.org \
--cc=kernel-team@meta.com \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=shakeel.butt@linux.dev \
--cc=tj@kernel.org \
/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®