* [PATCH RFC 1/2] fs: add superblock deferred iput infrastructure
2026-08-13 9:43 [PATCH RFC 0/2] Defer final iputs for dirty-time inodes in PF_MEMALLOC context Mikhail Rudenko
@ 2026-08-13 9:43 ` Mikhail Rudenko
2026-08-13 9:43 ` [PATCH RFC 2/2] fs: defer dirtytime iput from PF_MEMALLOC context Mikhail Rudenko
` (2 subsequent siblings)
3 siblings, 0 replies; 7+ messages in thread
From: Mikhail Rudenko @ 2026-08-13 9:43 UTC (permalink / raw)
To: Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, Mikhail Rudenko
Add the superblock side of deferred final iput processing. The new
helper queues an inode on a per-superblock list and schedules an
unbound WQ_MEM_RECLAIM worker to retry iput() outside the caller's
context.
Superblock teardown shuts the queue down and flushes pending work after
the shrinker is unregistered and before ->kill_sb(). This processes
queued inodes while the superblock and filesystem are still alive.
This is preparation for deferring dirtytime final iput from PF_MEMALLOC
context.
Signed-off-by: Mikhail Rudenko <xyzzy@yandex-team.ru>
---
fs/dcache.c | 1 +
fs/internal.h | 3 ++
fs/super.c | 92 ++++++++++++++++++++++++++++++++++++++++++
include/linux/fs/super_types.h | 6 +++
4 files changed, 102 insertions(+)
diff --git a/fs/dcache.c b/fs/dcache.c
index 3e9af9de7074..5465d8dbfdd2 100644
--- a/fs/dcache.c
+++ b/fs/dcache.c
@@ -3511,6 +3511,7 @@ void __init vfs_caches_init(void)
{
filename_init();
dcache_init();
+ super_init();
inode_init();
files_init();
files_maxfiles_init();
diff --git a/fs/internal.h b/fs/internal.h
index 355d93f92208..641b9a0c612d 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -9,6 +9,7 @@ struct super_block;
struct file_system_type;
struct iomap;
struct iomap_ops;
+struct inode;
struct linux_binprm;
struct path;
struct mount;
@@ -138,6 +139,8 @@ extern bool super_trylock_shared(struct super_block *sb);
struct super_block *user_get_super(dev_t, bool excl);
void put_super(struct super_block *sb);
extern bool mount_capable(struct fs_context *);
+void __init super_init(void);
+int super_defer_iput(struct inode *inode);
/*
* Prepare superblock for changing its read-only state (i.e., either remount
diff --git a/fs/super.c b/fs/super.c
index a8fd61136aaf..2f318694c98b 100644
--- a/fs/super.c
+++ b/fs/super.c
@@ -37,6 +37,7 @@
#include <linux/user_namespace.h>
#include <linux/fs_context.h>
#include <linux/fserror.h>
+#include <linux/workqueue.h>
#include <uapi/linux/mount.h>
#include "internal.h"
@@ -45,6 +46,7 @@ static int thaw_super_locked(struct super_block *sb, enum freeze_holder who,
static LIST_HEAD(super_blocks);
static DEFINE_SPINLOCK(sb_lock);
+static struct workqueue_struct *super_deferred_iput_wq __ro_after_init;
static char *sb_writers_name[SB_FREEZE_LEVELS] = {
"sb_writers",
@@ -52,6 +54,89 @@ static char *sb_writers_name[SB_FREEZE_LEVELS] = {
"sb_internal",
};
+/*
+ * sb->s_deferred_iput_lock protects sb->s_deferred_iputs and inode->i_lru
+ * while the inode is queued there.
+ */
+static void super_deferred_iput_work(struct work_struct *work)
+{
+ struct super_block *sb = container_of(work, struct super_block,
+ s_deferred_iput_work);
+ LIST_HEAD(pending);
+
+ for (;;) {
+ spin_lock(&sb->s_deferred_iput_lock);
+ list_splice_init(&sb->s_deferred_iputs, &pending);
+ spin_unlock(&sb->s_deferred_iput_lock);
+
+ if (list_empty(&pending))
+ break;
+
+ while (!list_empty(&pending)) {
+ struct inode *inode;
+
+ inode = list_first_entry(&pending, struct inode, i_lru);
+ list_del_init(&inode->i_lru);
+ iput(inode);
+ cond_resched();
+ }
+ }
+}
+
+static void __init super_deferred_iput_wq_init(void)
+{
+ super_deferred_iput_wq = alloc_workqueue("super_deferred_iput",
+ WQ_UNBOUND | WQ_MEM_RECLAIM, 0);
+ if (!super_deferred_iput_wq)
+ panic("Failed to allocate super deferred iput workqueue\n");
+}
+
+void __init super_init(void)
+{
+ super_deferred_iput_wq_init();
+}
+
+static void super_deferred_iput_init(struct super_block *sb)
+{
+ spin_lock_init(&sb->s_deferred_iput_lock);
+ INIT_LIST_HEAD(&sb->s_deferred_iputs);
+ INIT_WORK(&sb->s_deferred_iput_work, super_deferred_iput_work);
+ sb->s_deferred_iput_shutdown = false;
+}
+
+static void super_deferred_iput_shutdown(struct super_block *sb)
+{
+ bool empty;
+
+ spin_lock(&sb->s_deferred_iput_lock);
+ sb->s_deferred_iput_shutdown = true;
+ spin_unlock(&sb->s_deferred_iput_lock);
+
+ flush_work(&sb->s_deferred_iput_work);
+
+ spin_lock(&sb->s_deferred_iput_lock);
+ empty = list_empty(&sb->s_deferred_iputs);
+ spin_unlock(&sb->s_deferred_iput_lock);
+ WARN_ON_ONCE(!empty);
+}
+
+int super_defer_iput(struct inode *inode)
+{
+ struct super_block *sb = inode->i_sb;
+ int ret = -ESHUTDOWN;
+
+ spin_lock(&sb->s_deferred_iput_lock);
+ if (!sb->s_deferred_iput_shutdown) {
+ list_add_tail(&inode->i_lru, &sb->s_deferred_iputs);
+ /* Queue under the lock so shutdown cannot miss this work. */
+ queue_work(super_deferred_iput_wq, &sb->s_deferred_iput_work);
+ ret = 0;
+ }
+ spin_unlock(&sb->s_deferred_iput_lock);
+
+ return ret;
+}
+
static inline void __super_lock(struct super_block *sb, bool excl)
{
if (excl)
@@ -363,6 +448,7 @@ static struct super_block *alloc_super(struct file_system_type *type, int flags,
mutex_init(&s->s_sync_lock);
INIT_LIST_HEAD(&s->s_inodes);
spin_lock_init(&s->s_inode_list_lock);
+ super_deferred_iput_init(s);
INIT_LIST_HEAD(&s->s_inodes_wb);
spin_lock_init(&s->s_inode_wblist_lock);
fserror_mount(s);
@@ -474,6 +560,12 @@ void deactivate_locked_super(struct super_block *s)
struct file_system_type *fs = s->s_type;
if (atomic_dec_and_test(&s->s_active)) {
shrinker_free(s->s_shrink);
+ /*
+ * The shrinker can leave final inode references queued for
+ * processing outside reclaim. Drain them before
+ * filesystem-specific shutdown starts.
+ */
+ super_deferred_iput_shutdown(s);
fs->kill_sb(s);
kill_super_notify(s);
diff --git a/include/linux/fs/super_types.h b/include/linux/fs/super_types.h
index ef7941e9dc79..9bd4abbdbd51 100644
--- a/include/linux/fs/super_types.h
+++ b/include/linux/fs/super_types.h
@@ -255,6 +255,12 @@ struct super_block {
*/
struct list_lru s_dentry_lru;
struct list_lru s_inode_lru;
+ /* Protects s_deferred_iputs and s_deferred_iput_shutdown. */
+ spinlock_t s_deferred_iput_lock;
+ /* Inodes whose final iput was deferred from PF_MEMALLOC context. */
+ struct list_head s_deferred_iputs;
+ struct work_struct s_deferred_iput_work;
+ bool s_deferred_iput_shutdown;
struct rcu_head rcu;
struct work_struct destroy_work;
--
2.55.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH RFC 2/2] fs: defer dirtytime iput from PF_MEMALLOC context
2026-08-13 9:43 [PATCH RFC 0/2] Defer final iputs for dirty-time inodes in PF_MEMALLOC context Mikhail Rudenko
2026-08-13 9:43 ` [PATCH RFC 1/2] fs: add superblock deferred iput infrastructure Mikhail Rudenko
@ 2026-08-13 9:43 ` Mikhail Rudenko
2026-09-01 10:10 ` [PATCH RFC 0/2] Defer final iputs for dirty-time inodes in " Mikhail Rudenko
2026-09-01 12:24 ` Jan Kara
3 siblings, 0 replies; 7+ messages in thread
From: Mikhail Rudenko @ 2026-08-13 9:43 UTC (permalink / raw)
To: Alexander Viro, Christian Brauner, Jan Kara
Cc: linux-fsdevel, linux-kernel, Mikhail Rudenko
Both kswapd and direct reclaim run shrinkers with PF_MEMALLOC set.
Dcache reclaim can remove a dentry and drop the final reference to a
linked inode with I_DIRTY_TIME. iput() then calls sync_lazytime(), which
promotes the update to I_DIRTY_SYNC and invokes the filesystem
dirty_inode callback. ext4 can allocate an inode-table buffer with
__GFP_NOFAIL from there, triggering the PF_MEMALLOC allocator warning.
Use the superblock deferred-iput infrastructure to transfer that final
inode reference to an unbound worker instead. Remove the inode from the
inode LRU first so i_lru can serve as the allocation-free queue entry.
This preserves normal dentry eviction policy and only defers work where
iput() would otherwise promote lazytime state from PF_MEMALLOC context.
Signed-off-by: Mikhail Rudenko <xyzzy@yandex-team.ru>
---
fs/inode.c | 45 ++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 44 insertions(+), 1 deletion(-)
diff --git a/fs/inode.c b/fs/inode.c
index 31c5b9ee3a81..e71750bf7c82 100644
--- a/fs/inode.c
+++ b/fs/inode.c
@@ -18,6 +18,7 @@
#include <linux/mount.h>
#include <linux/posix_acl.h>
#include <linux/ratelimit.h>
+#include <linux/sched.h>
#include <linux/list_lru.h>
#include <linux/iversion.h>
#include <linux/rw_hint.h>
@@ -35,7 +36,7 @@
* inode->i_lock protects:
* inode->i_state, inode->i_hash, __iget(), inode->i_io_list
* Inode LRU list locks protect:
- * inode->i_sb->s_inode_lru, inode->i_lru
+ * inode->i_sb->s_inode_lru, inode->i_lru when on the inode LRU
* inode->i_sb->s_inode_list_lock protects:
* inode->i_sb->s_inodes, inode->i_sb_list
* bdi->wb.list_lock protects:
@@ -2019,6 +2020,43 @@ static void iput_final(struct inode *inode)
evict(inode);
}
+/* Like iput(), but defer sync_lazytime(). Used in reclaim paths. */
+static void iput_memalloc(struct inode *inode)
+{
+ spin_lock(&inode->i_lock);
+ if (unlikely((inode_state_read(inode) & I_DIRTY_TIME) &&
+ inode->i_nlink)) {
+ int ret;
+
+ if (atomic_add_unless(&inode->i_count, -1, 1)) {
+ spin_unlock(&inode->i_lock);
+ return;
+ }
+
+ inode_lru_list_del(inode);
+ spin_unlock(&inode->i_lock);
+
+ ret = super_defer_iput(inode);
+ if (!ret)
+ return;
+
+ /*
+ * The superblock is no longer accepting deferred iputs.
+ * This shouldn't happen.
+ */
+ WARN_ON_ONCE(ret == -ESHUTDOWN);
+ spin_lock(&inode->i_lock);
+ inode_state_clear(inode, I_DIRTY_TIME);
+ }
+
+ if (!atomic_dec_and_test(&inode->i_count)) {
+ spin_unlock(&inode->i_lock);
+ return;
+ }
+
+ iput_final(inode);
+}
+
/**
* iput - put an inode
* @inode: inode to put
@@ -2047,6 +2085,11 @@ void iput(struct inode *inode)
if (atomic_add_unless(&inode->i_count, -1, 1))
return;
+ if (unlikely(current->flags & PF_MEMALLOC)) {
+ iput_memalloc(inode);
+ return;
+ }
+
if (inode->i_nlink && sync_lazytime(inode))
goto retry;
--
2.55.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* Re: [PATCH RFC 0/2] Defer final iputs for dirty-time inodes in PF_MEMALLOC context
2026-08-13 9:43 [PATCH RFC 0/2] Defer final iputs for dirty-time inodes in PF_MEMALLOC context Mikhail Rudenko
2026-08-13 9:43 ` [PATCH RFC 1/2] fs: add superblock deferred iput infrastructure Mikhail Rudenko
2026-08-13 9:43 ` [PATCH RFC 2/2] fs: defer dirtytime iput from PF_MEMALLOC context Mikhail Rudenko
@ 2026-09-01 10:10 ` Mikhail Rudenko
2026-09-01 12:24 ` Jan Kara
3 siblings, 0 replies; 7+ messages in thread
From: Mikhail Rudenko @ 2026-09-01 10:10 UTC (permalink / raw)
To: Mikhail Rudenko
Cc: Alexander Viro, Christian Brauner, Jan Kara, linux-fsdevel,
linux-kernel
Gentle ping!
This is my first contribution to VFS (but not the first to the
kernel). At this point I'd like to know whether my approach makes sense
at all.
--
Best regards,
Mikhail Rudenko
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH RFC 0/2] Defer final iputs for dirty-time inodes in PF_MEMALLOC context
2026-08-13 9:43 [PATCH RFC 0/2] Defer final iputs for dirty-time inodes in PF_MEMALLOC context Mikhail Rudenko
` (2 preceding siblings ...)
2026-09-01 10:10 ` [PATCH RFC 0/2] Defer final iputs for dirty-time inodes in " Mikhail Rudenko
@ 2026-09-01 12:24 ` Jan Kara
2026-09-01 12:54 ` Mikhail Rudenko
2026-09-04 9:41 ` Christian Brauner
3 siblings, 2 replies; 7+ messages in thread
From: Jan Kara @ 2026-09-01 12:24 UTC (permalink / raw)
To: Mikhail Rudenko
Cc: Alexander Viro, Christian Brauner, Jan Kara, linux-fsdevel,
linux-kernel
I'm sorry for the delayed reply. Due to vacation and backlog after it it
took me long to get back to this.
On Thu 13-08-26 12:43:28, Mikhail Rudenko wrote:
> The motivation for this small series is the following kernel WARNING:
>
> [ 893.915584] ------------[ cut here ]------------
> [ 893.915589] WARNING: mm/page_alloc.c:4749 at __alloc_pages_slowpath.constprop.0+0xd4d/0x10f0, CPU#1: stress-ng-vm/3784
<snip>
> It was initially observed in the wild on linux 6.12.y, but later I
> reproduced it in Qemu with linux 7.0.11. I believe it is still an
> issue in the mainline master. Similar issue was reported earlier [1],
> but looks like no real analysis was performed then.
>
> The warning is triggered when a process with PF_MEMALLOC flag set
> requests a __GFP_NOFAIL page allocation. In this case, the allocation
> comes from ext4_dirty_inode() called when shrinker frees a dentry,
> resulting in i_count of an inode going zero, which in it turn can
> trigger sync_lazytime() if said inode has I_DIRTY_TIME flag, resulting
> in inode becoming dirty. On ext4 this causes an immediate journal
> transaction, which may lead to __GFP_NOFAIL allocation in the
> slowpath.
Yes, we are well aware of this problem. It actually goes significantly
further than just lazytime processing.
> The proposed approach to fix this basically boils down to "if need to
> do sync_lazytime() in PF_MEMALLOC context, defer instead".
>
> Patch 1 adds basic per-superblock infrastracture for deferring iputs:
> a workqueue, a work_struct, a list, a bool flag, a spinlock, and some
> glue code. A tricky place is superblock shutdown: we should stop
> accepting deferred iputs not too early, so that no legit cases fail to
> defer, and not too late, so that superblock is still operational and
> we can flush the workqueue. Since we cannot enter
> deactivate_locked_super() from PF_MEMALLOC context, stopping after
> shrinker_free() seems safe.
>
> Patch 2 uses this infrastructure to defer iputs of inodes with
> I_DIRTY_TIME, which would trigger sync_lazytime() otherwise, when
> running in PF_MEMALLOC context. Clumping default and PF_MEMALLOC paths
> together in iput() would have resulted in decreased readablity, so I
> avoided that by setting up a separate iput_memalloc(), at cost of
> slight code duplication.
Thanks for your patches. I've actually submitted patches to address this
problem back at the end of April [1]. In particularly the problem you
report was addressed somewhat differently by patch 1. We've then discussed
them at LSF/MM/BPF summit and people mostly agreed with the approach, just
there were some requests for better parallelization of deferred inode
reclaim (which I so far failed to implement). But it's currently high on my
todo list so I should hopefully send v2 of the patches for the next merge
window.
Honza
[1] https://lore.kernel.org/all/20260429174850.18223-1-jack@suse.cz/
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: [PATCH RFC 0/2] Defer final iputs for dirty-time inodes in PF_MEMALLOC context
2026-09-01 12:24 ` Jan Kara
@ 2026-09-01 12:54 ` Mikhail Rudenko
2026-09-04 9:41 ` Christian Brauner
1 sibling, 0 replies; 7+ messages in thread
From: Mikhail Rudenko @ 2026-09-01 12:54 UTC (permalink / raw)
To: Jan Kara; +Cc: Alexander Viro, Christian Brauner, linux-fsdevel, linux-kernel
Hi Jan,
On 2026-09-01 at 14:24 +02, Jan Kara <jack@suse.cz> wrote:
> Thanks for your patches. I've actually submitted patches to address this
> problem back at the end of April [1]. In particularly the problem you
> report was addressed somewhat differently by patch 1. We've then discussed
> them at LSF/MM/BPF summit and people mostly agreed with the approach, just
> there were some requests for better parallelization of deferred inode
> reclaim (which I so far failed to implement). But it's currently high on my
> todo list so I should hopefully send v2 of the patches for the next merge
> window.
>
> Honza
>
> [1] https://lore.kernel.org/all/20260429174850.18223-1-jack@suse.cz/
My bad, should've done better prior art reasearch. :) Looking forward
to your v2 then.
--
Best regards,
Mikhail Rudenko
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH RFC 0/2] Defer final iputs for dirty-time inodes in PF_MEMALLOC context
2026-09-01 12:24 ` Jan Kara
2026-09-01 12:54 ` Mikhail Rudenko
@ 2026-09-04 9:41 ` Christian Brauner
1 sibling, 0 replies; 7+ messages in thread
From: Christian Brauner @ 2026-09-04 9:41 UTC (permalink / raw)
To: Jan Kara; +Cc: Mikhail Rudenko, Alexander Viro, linux-fsdevel, linux-kernel
> Thanks for your patches. I've actually submitted patches to address this
> problem back at the end of April [1]. In particularly the problem you
> report was addressed somewhat differently by patch 1. We've then discussed
> them at LSF/MM/BPF summit and people mostly agreed with the approach, just
> there were some requests for better parallelization of deferred inode
> reclaim (which I so far failed to implement). But it's currently high on my
> todo list so I should hopefully send v2 of the patches for the next merge
> window.
Thanks for working on this. This is good to hear! :)
^ permalink raw reply [flat|nested] 7+ messages in thread