* [PATCH] kernfs: allocate inodes outside kernfs_rwsem
@ 2026-10-15 22:11 Shakeel Butt
2026-10-15 23:38 ` Shakeel Butt
0 siblings, 1 reply; 2+ messages in thread
From: Shakeel Butt @ 2026-10-15 22:11 UTC (permalink / raw)
To: Greg Kroah-Hartman, Tejun Heo, Christian Brauner
Cc: Meta kernel team, linux-fsdevel, driver-core, linux-kernel
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 with the lock dropped. kernfs_iget() first looks for
an existing inode. If there is none, it pins the node, drops the lock,
allocates, takes the lock again, and adds the inode only if the node is
still active. So __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.
Tested on a 176-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 577 ms 0.7 ms
below, for kernfs_rwsem 412 ms 1.0 ms
systemd, for kernfs_rwsem 449 ms 17 us
readdir of another directory 1154 ms 3.2 ms
The lookup itself is still slow. The VFS still holds its directory's
lock, so other work in that one directory still waits. Cold sysfs and
cgroupfs walks are not slower.
kernfs_fill_super() still allocates the root inode under the lock. It
runs once per superblock.
Signed-off-by: Shakeel Butt <shakeel.butt@linux.dev>
---
fs/kernfs/dir.c | 11 ++++---
fs/kernfs/inode.c | 63 +++++++++++++++++++++++++++++++++++++
fs/kernfs/kernfs-internal.h | 2 ++
fs/kernfs/mount.c | 12 +++----
4 files changed, 77 insertions(+), 11 deletions(-)
diff --git a/fs/kernfs/dir.c b/fs/kernfs/dir.c
index 7e65854a09e4..a0ef95a509d5 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);
@@ -1269,9 +1269,12 @@ static struct dentry *kernfs_iop_lookup(struct inode *dir,
up_read(&root->kernfs_rwsem);
return NULL;
}
- inode = kernfs_get_inode(dir->i_sb, kn);
- if (!inode)
- inode = ERR_PTR(-ENOMEM);
+ inode = kernfs_iget(dir->i_sb, kn);
+ if (!inode) {
+ /* removed while kernfs_iget() dropped the lock */
+ up_read(&root->kernfs_rwsem);
+ return NULL;
+ }
}
/*
* Needed for negative dentry validation.
diff --git a/fs/kernfs/inode.c b/fs/kernfs/inode.c
index 237dcdd73fc2..b20d1c1cf6de 100644
--- a/fs/kernfs/inode.c
+++ b/fs/kernfs/inode.c
@@ -256,6 +256,69 @@ struct inode *kernfs_get_inode(struct super_block *sb, struct kernfs_node *kn)
return inode;
}
+static int kernfs_iget_test(struct inode *inode, void *data)
+{
+ return inode->i_ino == *(u64 *)data;
+}
+
+static int kernfs_iget_set(struct inode *inode, void *data)
+{
+ inode->i_ino = *(u64 *)data;
+ return 0;
+}
+
+/**
+ * kernfs_iget - get the inode of @kn, allocating it outside kernfs_rwsem
+ * @sb: super block
+ * @kn: kernfs_node to get the inode for
+ *
+ * Called with kernfs_rwsem held for read. If @kn has no inode yet, the
+ * lock is dropped while allocating one, as the allocation can stall in
+ * memcg reclaim. The new inode is added only if @kn is still active when
+ * the lock is taken again, so __kernfs_remove() always finds it to clear
+ * i_nlink.
+ *
+ * Return: the inode, %NULL if @kn is inactive and has no inode, or
+ * ERR_PTR(-ENOMEM).
+ */
+struct inode *kernfs_iget(struct super_block *sb, struct kernfs_node *kn)
+{
+ struct kernfs_root *root = kernfs_root(kn);
+ u64 ino = kernfs_ino(kn);
+ struct inode *inode, *new;
+
+ lockdep_assert_held_read(&root->kernfs_rwsem);
+
+ inode = ilookup(sb, ino);
+ if (inode || !kernfs_active(kn))
+ return inode;
+
+ kernfs_get(kn);
+ up_read(&root->kernfs_rwsem);
+ new = new_inode(sb);
+ down_read(&root->kernfs_rwsem);
+
+ if (!new) {
+ inode = ERR_PTR(-ENOMEM);
+ } else if (!kernfs_active(kn)) {
+ inode = NULL;
+ } else {
+ inode = inode_insert5(new, ino, kernfs_iget_test,
+ kernfs_iget_set, &ino);
+ if (inode == new) {
+ kernfs_init_inode(kn, inode);
+ new = NULL;
+ } else if (!inode) {
+ inode = ERR_PTR(-ENOMEM);
+ }
+ }
+
+ kernfs_put(kn);
+ if (new)
+ iput(new);
+ 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..6cc19c9ef47b 100644
--- a/fs/kernfs/kernfs-internal.h
+++ b/fs/kernfs/kernfs-internal.h
@@ -186,6 +186,7 @@ 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_iget(struct super_block *sb, struct kernfs_node *kn);
/*
* dir.c
@@ -194,6 +195,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..a2802d8c772c 100644
--- a/fs/kernfs/mount.c
+++ b/fs/kernfs/mount.c
@@ -125,11 +125,9 @@ static struct dentry *__kernfs_fh_to_dentry(struct super_block *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_iget(). 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);
@@ -146,7 +144,7 @@ static struct dentry *__kernfs_fh_to_dentry(struct super_block *sb,
return ERR_PTR(-ESTALE);
}
- inode = kernfs_get_inode(sb, kn);
+ inode = kernfs_iget(sb, kn);
kernfs_put(kn);
}
@@ -173,7 +171,7 @@ static struct dentry *kernfs_get_parent_dentry(struct dentry *child)
struct kernfs_root *root = kernfs_root(kn);
guard(rwsem_read)(&root->kernfs_rwsem);
- return d_obtain_alias(kernfs_get_inode(child->d_sb, kernfs_parent(kn)));
+ return d_obtain_alias(kernfs_iget(child->d_sb, kernfs_parent(kn)));
}
static const struct export_operations kernfs_export_ops = {
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 2+ messages in thread
* Re: [PATCH] kernfs: allocate inodes outside kernfs_rwsem
2026-10-15 22:11 [PATCH] kernfs: allocate inodes outside kernfs_rwsem Shakeel Butt
@ 2026-10-15 23:38 ` Shakeel Butt
0 siblings, 0 replies; 2+ messages in thread
From: Shakeel Butt @ 2026-10-15 23:38 UTC (permalink / raw)
To: Greg Kroah-Hartman, Tejun Heo, Christian Brauner
Cc: Meta kernel team, linux-fsdevel, driver-core, linux-kernel
On Thu, Oct 15, 2026 at 03:11:10PM -0700, Shakeel Butt wrote:
[...]
>
> @@ -173,7 +171,7 @@ static struct dentry *kernfs_get_parent_dentry(struct dentry *child)
> struct kernfs_root *root = kernfs_root(kn);
>
> guard(rwsem_read)(&root->kernfs_rwsem);
> - return d_obtain_alias(kernfs_get_inode(child->d_sb, kernfs_parent(kn)));
> + return d_obtain_alias(kernfs_iget(child->d_sb, kernfs_parent(kn)));
Sashiko is complaining that inode allocation has been moved outside kernfs_rwsem
but d_obtain_alias() may still allocate dentry under this lock. The fix is
simple. Something like the following:
scoped_guard(rwsem_read, &root->kernfs_rwsem)
inode = kernfs_iget(child->d_sb, kernfs_parent(kn));
return d_obtain_alias(inode);
I am testing this change and I will wait for human response/review before
sending out the v2 of this patch.
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-15 23:38 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-15 22:11 [PATCH] kernfs: allocate inodes outside kernfs_rwsem Shakeel Butt
2026-10-15 23:38 ` Shakeel Butt
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®