[PATCH RFC 1/2] fs: add superblock deferred iput infrastructure

From: Mikhail Rudenko

Date: Thu Aug 13 2026 - 05:48:09 EST


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@xxxxxxxxxxxxxx>
---
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