Re: [REGRESSION] kernfs: empty cgroup rmdir latency after delete-notification changes
From: Chengfeng Lin
Date: Mon Sep 14 2026 - 14:08:51 EST
Hi T.J.,
Thanks for the patch. Your changes reduced rmdir() latency by
21.87-22.86% in the same empty-cgroup benchmark, with sched_ext disabled.
I tested these two states on 2f0c1cf72f46 (7.3-rc2):
A: baseline + Shakeel's file-handle decoding read-lock fix [1]
B: A + your INODE_INITED changes, without modifying your code
I used the original binary on the bare-metal i7-12700KF, pinned to CPU0,
with full preemption, the performance governor and Turbo disabled.
Both kernels used GCC 15.2 and the same config except LOCALVERSION.
The clean timing sequence used four fresh boots: A1 -> B1 -> B2 -> A2.
A1 B1 B2 A2
rmdir (us) 9.727 7.600 7.548 9.784
Each value is the median of nine sample means. Each sample contains
128 measured removals after 16 warmups. All four B/A comparisons showed
the improvement above. Removal CVs were below 2.03%. The drift between
boots was +0.59% for A and -0.68% for B. The improvement held after
dropping the first sample from each boot.
Separate, untimed traces showed the same 26 kernfs nodes per removal,
but ilookup() calls fell from 26 to 1. Deletion lock counts were unchanged.
Basic checks of file/directory deletion notifications and link counts
with FDs held open passed on both kernels.
I also timed the full mkdir() -> stat() -> rmdir() sequence using a
separate binary on the same kernels. The latest continuous follow-up used
four fresh boots, with nine samples per boot. Each sample had 16 warmups
and 32 measured sequences, with no pauses between them. The full sequence
fell from 30.07-30.65 us to 27.97-28.34 us, a 5.77-8.76% reduction.
The maximum CV was 1.76%, and boot drift stayed below 2% in both states.
In the earlier continuous follow-up with 128 operations per sample, the
full sequence was 5.15-6.52% shorter, but CVs of 4.52-6.46% were too high
to establish a stable overall improvement percentage.
Untimed tracing placed the added write lock in first stat(), during inode
initialization. The patch skips lookups for nodes whose inodes were never
initialized; it does not move those lookups into stat(). The individual
mkdir() and stat() timings remained too variable to quantify their changes.
I also checked whether the benefit persisted with less frequent operations.
For this diagnostic, I added 50 and 200 ms pauses between sequences,
outside the timers. I kept 16 warmups and 32 measured sequences per sample.
rmdir() still saved 2.10-2.58 us, but its baseline rose from about 10 us
to 18 us, reducing the percentage gain. The full sequence was 0.66-2.85%
shorter, with a maximum CV of 1.53%.
I included [1] because the file-handle path on this base lacks
kernfs_rwsem protection. Source review suggests a possible wait cycle
when that path recreates an evicted inode. Initialization waits for
kernfs_iattr_rwsem, while removal holds it and waits for I_NEW to clear.
I have not reproduced this possible deadlock in the kernel.
Does the base for your diff already include [1] or equivalent locking?
[1] [PATCH v2 3/4] kernfs: don't lose IN_DELETE_SELF
when decoding a file handle
https://lists.openwall.net/linux-kernel/2026/09/05/828
Results and reproducers:
https://github.com/lcf0399/linux-regression-evidence/tree/f101b1b9c0d3dfce07f81b11ca2da2c252150e4c/kernfs-empty-cgroup-removal/bare-metal/inode-inited
Thanks,
Chengfeng
T.J. Mercier <tjmercier@xxxxxxxxxx> 于2026年9月14日周一 00:17写道:
>
> On Sun, Sep 13, 2026 at 8:47 AM Chengfeng Lin <lin2530632123@xxxxxxxxx> wrote:
> >
> > Hi Tejun,
> >
> > Thanks. I found this during research into kernel performance regressions,
> > using a microbenchmark of cgroup creation and removal. This was not
> > prompted by a production issue.
> >
> > I reported it to document the added removal cost and ask whether some
> > of it could be reduced while preserving the fixes. I agree that the
> > roughly 3 us increase per removal does not, by itself, demonstrate a
> > significant impact on real workloads.
> >
> > Thanks,
> > Chengfeng
> >
> > Tejun Heo <tj@xxxxxxxxxx> 于2026年9月13日周日 23:03写道:
> > >
> > > Hello,
> > >
> > > On Sun, Sep 13, 2026 at 10:43:04PM +0800, Chengfeng Lin wrote:
> > > > I found an increase in empty cgroup removal latency across 507d8ce13f5b
> > > > ("kernfs: Don't set_nlink for directories being removed") and eea5d2bb34ba
> > > > ("kernfs: Send IN_DELETE_SELF and IN_IGNORED"). With sched_ext disabled,
> > > > rmdir() went from about 6.5 us to 9.6 us, an increase of 46-47%.
> > > > The result held in two independent runs.
> > >
> > > I don't want to make cgroup removal unnecessarily expensive but at the same
> > > time it's not an operation that I consider to be a hot path, so as long as
> > > the operaiton can finish in a reasonable amount of time and single digit us
> > > definitely is, performance of rmdir usually isn't something which is high in
> > > priority. Can you please detail why this matters for you?
> > >
> > > Thanks.
> > >
> > > --
> > > tejun
>
> Hi Chengfeng and Tejun,
>
> We use this kernfs IN_DELETE_SELF feature on Android where multiple
> cgroups are created and removed frequently on a per-application basis,
> but we hadn't noticed a significant delay due to the inotify
> functionality during cgroup removal. It's probably masked by much
> larger delays (milliseconds) we regularly see due to unreleated issues
> with cgroup locks like priority inversion with cgroup_mutex, and
> contention for cgroup_threadgroup_rwsem.
>
> As far as what we can do, I think avoiding inode lookups for kernfs
> nodes that never had an inode created should improve the situation. It
> doesn't eliminate the locking overhead, which I don't think can easily
> be eliminated. The code below doesn't deal with inode eviction either,
> but at least files which are never accessed won't introduce the inode
> lookup overhead.
>
> diff --git a/fs/kernfs/dir.c b/fs/kernfs/dir.c
> index 4f9ade82b08a..3ec9f79b1aa2 100644
> --- a/fs/kernfs/dir.c
> +++ b/fs/kernfs/dir.c
> @@ -1523,6 +1523,9 @@ static void kernfs_clear_inode_nlink(struct
> kernfs_node *kn)
> struct kernfs_root *root = kernfs_root(kn);
> struct kernfs_super_info *info;
>
> + if (!(kn->flags & KERNFS_INODE_INITED))
> + return;
> +
> lockdep_assert_held_read(&root->kernfs_supers_rwsem);
>
> list_for_each_entry(info, &root->supers, node) {
> diff --git a/fs/kernfs/inode.c b/fs/kernfs/inode.c
> index 38b28aa7cd02..f8b4a8b8ade4 100644
> --- a/fs/kernfs/inode.c
> +++ b/fs/kernfs/inode.c
> @@ -208,6 +208,10 @@ static void kernfs_init_inode(struct kernfs_node
> *kn, struct inode *inode)
> set_default_inode_attr(inode, kn->mode);
> kernfs_refresh_inode(kn, inode);
>
> + down_write(&kernfs_root(kn)->kernfs_iattr_rwsem);
> + kn->flags |= KERNFS_INODE_INITED;
> + up_write(&kernfs_root(kn)->kernfs_iattr_rwsem);
> +
> /* initialize inode according to type */
> switch (kernfs_type(kn)) {
> case KERNFS_DIR:
> diff --git a/include/linux/kernfs.h b/include/linux/kernfs.h
> index e21b2f7f4159..87f074ce19d3 100644
> --- a/include/linux/kernfs.h
> +++ b/include/linux/kernfs.h
> @@ -113,6 +113,7 @@ enum kernfs_node_flag {
> KERNFS_EMPTY_DIR = 0x1000,
> KERNFS_HAS_RELEASE = 0x2000,
> KERNFS_REMOVING = 0x4000,
> + KERNFS_INODE_INITED = 0x8000,
> };
>
> -T.J.