* [RFC PATCH v2 0/3] fs/proc: split the inode list for procfs
@ 2026-09-20 7:28 Huang Shijie
2026-09-20 7:28 ` [RFC PATCH v2 1/3] fs/drop_caches: skip filesystems without page cache Huang Shijie
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Huang Shijie @ 2026-09-20 7:28 UTC (permalink / raw)
To: viro, brauner
Cc: zhongyuan, fangbaoshun, yingzhiwei, jack, dev.jain, ljs,
adobriyan, akpm, hpa, ryan.roberts, mjguzik, joel.granados,
jannh, oleg, aleksa, legion, kees, joannelkoong, tj, libaokun,
ebiggers, sandeen, linux-fsdevel, linux-kernel, Huang Shijie
The global s_inode_list_lock is heavily contended in procfs
on a 384-CPU, 12-NUMA-node Hygon machine running Hadoop TestDFSIO:
#hadoop jar xxxx.jar TestDFSIO -read -nrFiles 1000 -size 100MB
The hadoop will create lot of threads during the test.
The perf shows it consuming ~90% of the lock hotspot in procfs.
The lock is hit from both directions:
-- inode creation (~49%) :
getdents64 ->
proc_readfd_common ->
new_inode ->
inode_sb_list_add()
-- inode eviction (~41%):
process exit ->
release_task ->
proc_invalidate_siblings_dcache ->
evict ->
inode_sb_list_del()
This patch set tries to resolve this issue by:
patch 1: "fs/drop_caches: skip filesystems without page cache"
add a new flag for filesystems without page cache.
patch 2: Add a new helper to detect the empty inode list
patch 3: split the inode list.
After this patch set, the above TestDFSIO can get over 50% better performance
in exec time. And the procfs lock hotspot becomes nearly 1%.
Huang Shijie (3):
fs/drop_caches: skip filesystems without page cache
fs/super: introduce a helper sb_inodes_empty()
fs/proc: split the inode list
fs/drop_caches.c | 3 +
fs/inode.c | 79 ++++++++++------
fs/proc/inode.c | 53 +++++++++++
fs/proc/internal.h | 2 +
fs/proc/root.c | 8 +-
fs/super.c | 45 +++++++--
include/linux/fs/super_types.h | 17 ++++
include/linux/proc_fs.h | 1 +
security/landlock/fs.c | 162 ++++++++++++++++++---------------
9 files changed, 260 insertions(+), 110 deletions(-)
--
2.53.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [RFC PATCH v2 1/3] fs/drop_caches: skip filesystems without page cache 2026-09-20 7:28 [RFC PATCH v2 0/3] fs/proc: split the inode list for procfs Huang Shijie @ 2026-09-20 7:28 ` Huang Shijie 2026-09-22 5:41 ` Christoph Hellwig 2026-09-20 7:28 ` [RFC PATCH v2 2/3] fs/super: introduce a helper sb_inodes_empty() Huang Shijie 2026-09-20 7:28 ` [RFC PATCH v2 3/3] fs/proc: split the inode list Huang Shijie 2 siblings, 1 reply; 7+ messages in thread From: Huang Shijie @ 2026-09-20 7:28 UTC (permalink / raw) To: viro, brauner Cc: zhongyuan, fangbaoshun, yingzhiwei, jack, dev.jain, ljs, adobriyan, akpm, hpa, ryan.roberts, mjguzik, joel.granados, jannh, oleg, aleksa, legion, kees, joannelkoong, tj, libaokun, ebiggers, sandeen, linux-fsdevel, linux-kernel, Huang Shijie Add a new flag SB_I_NO_PAGECACHE for superblock. Skip scanning the inode lists of filesystems that have no page cache in drop_pagecache_sb(), as indicated by the SB_I_NO_PAGECACHE flag. This patch only changes the procfs. Signed-off-by: Huang Shijie <huangsj@hygon.cn> --- fs/drop_caches.c | 3 +++ fs/proc/root.c | 2 +- include/linux/fs/super_types.h | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/fs/drop_caches.c b/fs/drop_caches.c index 49f56a598ecb..27caf748b3c5 100644 --- a/fs/drop_caches.c +++ b/fs/drop_caches.c @@ -20,6 +20,9 @@ static void drop_pagecache_sb(struct super_block *sb, void *unused) { struct inode *inode, *toput_inode = NULL; + if (sb->s_iflags & SB_I_NO_PAGECACHE) + return; + spin_lock(&sb->s_inode_list_lock); list_for_each_entry(inode, &sb->s_inodes, i_sb_list) { spin_lock(&inode->i_lock); diff --git a/fs/proc/root.c b/fs/proc/root.c index 99adddfeb4a4..d45f5af5ab53 100644 --- a/fs/proc/root.c +++ b/fs/proc/root.c @@ -266,7 +266,7 @@ static int proc_fill_super(struct super_block *s, struct fs_context *fc) return ret; /* User space would break if executables or devices appear on proc */ - s->s_iflags |= SB_I_NOEXEC | SB_I_NODEV; + s->s_iflags |= SB_I_NOEXEC | SB_I_NODEV | SB_I_NO_PAGECACHE; s->s_flags |= SB_NODIRATIME | SB_NOSUID | SB_NOEXEC; s->s_blocksize = 1024; s->s_blocksize_bits = 10; diff --git a/include/linux/fs/super_types.h b/include/linux/fs/super_types.h index ecd96aeb1cee..1a596caf58a8 100644 --- a/include/linux/fs/super_types.h +++ b/include/linux/fs/super_types.h @@ -352,5 +352,6 @@ struct super_block { #define SB_I_NOIDMAP 0x00002000 /* No idmapped mounts on this superblock */ #define SB_I_ALLOW_HSM 0x00004000 /* Allow HSM events on this superblock */ #define SB_I_NO_DATA_INTEGRITY 0x00008000 /* fs cannot guarantee data persistence on sync */ +#define SB_I_NO_PAGECACHE 0x00010000 /* this file system has no page cache */ #endif /* _LINUX_FS_SUPER_TYPES_H */ -- 2.53.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [RFC PATCH v2 1/3] fs/drop_caches: skip filesystems without page cache 2026-09-20 7:28 ` [RFC PATCH v2 1/3] fs/drop_caches: skip filesystems without page cache Huang Shijie @ 2026-09-22 5:41 ` Christoph Hellwig 2026-09-23 5:49 ` Huang Shijie 0 siblings, 1 reply; 7+ messages in thread From: Christoph Hellwig @ 2026-09-22 5:41 UTC (permalink / raw) To: Huang Shijie Cc: viro, brauner, zhongyuan, fangbaoshun, yingzhiwei, jack, dev.jain, ljs, adobriyan, akpm, hpa, ryan.roberts, mjguzik, joel.granados, jannh, oleg, aleksa, legion, kees, joannelkoong, tj, libaokun, ebiggers, sandeen, linux-fsdevel, linux-kernel On Sun, Sep 20, 2026 at 03:28:09PM +0800, Huang Shijie wrote: > Add a new flag SB_I_NO_PAGECACHE for superblock. > > Skip scanning the inode lists of filesystems that have no page cache > in drop_pagecache_sb(), as indicated by the SB_I_NO_PAGECACHE flag. I don't think we need a new flag. This can check for sb->s_bdi == &noop_backing_dev_info which is actually the more relevant concept - not if a file system uses the page cache, but if it supports page cache writeback. E.g. for ramfs there is page cache, but it is the only data store. For shmemfs, there is writeback, but it is driven in a different way (through the anonymous memory code and not fs writeback). And most importantly it just work, no need to mark file systems, as you're currently missing a lot of those that can have a fair number of inodes, e.g., sysfs, kernfs, cgroupfs or configfs. ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [RFC PATCH v2 1/3] fs/drop_caches: skip filesystems without page cache 2026-09-22 5:41 ` Christoph Hellwig @ 2026-09-23 5:49 ` Huang Shijie 0 siblings, 0 replies; 7+ messages in thread From: Huang Shijie @ 2026-09-23 5:49 UTC (permalink / raw) To: Christoph Hellwig Cc: viro, brauner, zhongyuan, fangbaoshun, yingzhiwei, jack, dev.jain, ljs, adobriyan, akpm, hpa, ryan.roberts, mjguzik, joel.granados, jannh, oleg, aleksa, legion, kees, joannelkoong, tj, libaokun, ebiggers, sandeen, linux-fsdevel, linux-kernel On Mon, Sep 21, 2026 at 10:41:26PM -0700, Christoph Hellwig wrote: > On Sun, Sep 20, 2026 at 03:28:09PM +0800, Huang Shijie wrote: > > Add a new flag SB_I_NO_PAGECACHE for superblock. > > > > Skip scanning the inode lists of filesystems that have no page cache > > in drop_pagecache_sb(), as indicated by the SB_I_NO_PAGECACHE flag. > > I don't think we need a new flag. This can check for > > sb->s_bdi == &noop_backing_dev_info > Okay. I can use this method. > which is actually the more relevant concept - not if a file system > uses the page cache, but if it supports page cache writeback. > E.g. for ramfs there is page cache, but it is the only data store. > For shmemfs, there is writeback, but it is driven in a different > way (through the anonymous memory code and not fs writeback). > > And most importantly it just work, no need to mark file systems, > as you're currently missing a lot of those that can have a fair > number of inodes, e.g., sysfs, kernfs, cgroupfs or configfs. > I ever added support for these file systems, but I deleted the code before I sent out this patch. To Jan Kara, thanks for the information about the issue. I will check the inode iteration issue firstly. Thanks Huang Shijie ^ permalink raw reply [flat|nested] 7+ messages in thread
* [RFC PATCH v2 2/3] fs/super: introduce a helper sb_inodes_empty() 2026-09-20 7:28 [RFC PATCH v2 0/3] fs/proc: split the inode list for procfs Huang Shijie 2026-09-20 7:28 ` [RFC PATCH v2 1/3] fs/drop_caches: skip filesystems without page cache Huang Shijie @ 2026-09-20 7:28 ` Huang Shijie 2026-09-20 7:28 ` [RFC PATCH v2 3/3] fs/proc: split the inode list Huang Shijie 2 siblings, 0 replies; 7+ messages in thread From: Huang Shijie @ 2026-09-20 7:28 UTC (permalink / raw) To: viro, brauner Cc: zhongyuan, fangbaoshun, yingzhiwei, jack, dev.jain, ljs, adobriyan, akpm, hpa, ryan.roberts, mjguzik, joel.granados, jannh, oleg, aleksa, legion, kees, joannelkoong, tj, libaokun, ebiggers, sandeen, linux-fsdevel, linux-kernel, Huang Shijie Introduce a helper sb_inodes_empty() which is used to detect if the inodes list is empty. Signed-off-by: Huang Shijie <huangsj@hygon.cn> --- fs/super.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fs/super.c b/fs/super.c index 9d4025213521..e25ded0bc9a2 100644 --- a/fs/super.c +++ b/fs/super.c @@ -710,6 +710,11 @@ void retire_super(struct super_block *sb) } EXPORT_SYMBOL(retire_super); +static bool sb_inodes_empty(struct super_block *sb) +{ + return list_empty(&sb->s_inodes); +} + /** * generic_shutdown_super - common helper for ->kill_sb() * @sb: superblock to kill @@ -760,7 +765,7 @@ void generic_shutdown_super(struct super_block *sb) */ fscrypt_destroy_keyring(sb); - if (CHECK_DATA_CORRUPTION(!list_empty(&sb->s_inodes), NULL, + if (CHECK_DATA_CORRUPTION(!sb_inodes_empty(sb), NULL, "VFS: Busy inodes after unmount of %s (%s)", sb->s_id, sb->s_type->name)) { /* -- 2.53.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* [RFC PATCH v2 3/3] fs/proc: split the inode list 2026-09-20 7:28 [RFC PATCH v2 0/3] fs/proc: split the inode list for procfs Huang Shijie 2026-09-20 7:28 ` [RFC PATCH v2 1/3] fs/drop_caches: skip filesystems without page cache Huang Shijie 2026-09-20 7:28 ` [RFC PATCH v2 2/3] fs/super: introduce a helper sb_inodes_empty() Huang Shijie @ 2026-09-20 7:28 ` Huang Shijie 2026-09-22 13:29 ` Jan Kara 2 siblings, 1 reply; 7+ messages in thread From: Huang Shijie @ 2026-09-20 7:28 UTC (permalink / raw) To: viro, brauner Cc: zhongyuan, fangbaoshun, yingzhiwei, jack, dev.jain, ljs, adobriyan, akpm, hpa, ryan.roberts, mjguzik, joel.granados, jannh, oleg, aleksa, legion, kees, joannelkoong, tj, libaokun, ebiggers, sandeen, linux-fsdevel, linux-kernel, Huang Shijie The global s_inode_list_lock is heavily contended in procfs on a 384-CPU, 12-NUMA-node Hygon machine running Hadoop TestDFSIO: #hadoop jar xxxx.jar TestDFSIO -read -nrFiles 1000 -size 100MB The perf shows it consuming ~90% of the lock hotspot. The lock is hit from both directions: -- inode creation (~49%) : getdents64 -> proc_readfd_common -> new_inode -> inode_sb_list_add() -- inode eviction (~41%) process exit -> release_task -> proc_invalidate_siblings_dcache -> evict -> inode_sb_list_del() This patch spreads the inode list across per-shard locks for procfs: --- Add three fields in super_block: shards : the pointer for the array of inode_shard. nr_shards: the size of the array s_inode_list_sharded: whether or not to use a sharded inode list struct inode_shard is cacheline-aligned to avoid false sharing between shard locks on different NUMA nodes. --- Add inode_list_add()/inode_list_del() callbacks to super_operations; procfs implements them to round-robin inodes onto nr_shards = DIV_ROUND_UP(num_possible_cpus(), 32) shards allocated at mount time, each protected by its own spinlock. --- For procfs, the "unmount" will call evict_inodes(), generic_shutdown_super() and hook_sb_delete() which will iterate the shards when the super_block inode list is sharded. Change these functions to work with the sharded inode list. This reduces the s_inode_list_lock hotspot from ~90% to ~1% in TestDFSIO. And we can improve the hadoop performance over 50%. Signed-off-by: Huang Shijie <huangsj@hygon.cn> --- fs/inode.c | 79 ++++++++++------ fs/proc/inode.c | 53 +++++++++++ fs/proc/internal.h | 2 + fs/proc/root.c | 6 ++ fs/super.c | 40 ++++++-- include/linux/fs/super_types.h | 16 ++++ include/linux/proc_fs.h | 1 + security/landlock/fs.c | 162 ++++++++++++++++++--------------- 8 files changed, 250 insertions(+), 109 deletions(-) diff --git a/fs/inode.c b/fs/inode.c index ba7da39be4a3..80acb9113487 100644 --- a/fs/inode.c +++ b/fs/inode.c @@ -635,6 +635,10 @@ void inode_sb_list_add(struct inode *inode) { struct super_block *sb = inode->i_sb; + if (sb->s_inode_list_sharded && sb->s_op->inode_list_add) { + sb->s_op->inode_list_add(sb, inode); + return; + } spin_lock(&sb->s_inode_list_lock); list_add(&inode->i_sb_list, &sb->s_inodes); spin_unlock(&sb->s_inode_list_lock); @@ -646,6 +650,10 @@ static inline void inode_sb_list_del(struct inode *inode) struct super_block *sb = inode->i_sb; if (!list_empty(&inode->i_sb_list)) { + if (sb->s_inode_list_sharded && sb->s_op->inode_list_del) { + sb->s_op->inode_list_del(sb, inode); + return; + } spin_lock(&sb->s_inode_list_lock); list_del_init(&inode->i_sb_list); spin_unlock(&sb->s_inode_list_lock); @@ -879,41 +887,56 @@ void evict_inodes(struct super_block *sb) { struct inode *inode; LIST_HEAD(dispose); + struct list_head *head; + spinlock_t *lock; + unsigned int nr, i; + const bool sharded = sb->s_inode_list_sharded; + nr = sharded ? sb->nr_shards : 1; again: - spin_lock(&sb->s_inode_list_lock); - list_for_each_entry(inode, &sb->s_inodes, i_sb_list) { - if (icount_read_once(inode)) - continue; - - spin_lock(&inode->i_lock); - if (icount_read(inode)) { - spin_unlock(&inode->i_lock); - continue; - } - if (inode_state_read(inode) & (I_NEW | I_FREEING | I_WILL_FREE)) { - spin_unlock(&inode->i_lock); - continue; + for (i = 0; i < nr; i++) { + if (sharded) { + head = &sb->shards[i].list; + lock = &sb->shards[i].lock; + } else { + head = &sb->s_inodes; + lock = &sb->s_inode_list_lock; } - inode_state_set(inode, I_FREEING); - inode_lru_list_del(inode); - spin_unlock(&inode->i_lock); - list_add(&inode->i_lru, &dispose); + spin_lock(lock); + list_for_each_entry(inode, head, i_sb_list) { + if (icount_read_once(inode)) + continue; - /* - * We can have a ton of inodes to evict at unmount time given - * enough memory, check to see if we need to go to sleep for a - * bit so we don't livelock. - */ - if (need_resched()) { - spin_unlock(&sb->s_inode_list_lock); - cond_resched(); - dispose_list(&dispose); - goto again; + spin_lock(&inode->i_lock); + if (icount_read(inode)) { + spin_unlock(&inode->i_lock); + continue; + } + if (inode_state_read(inode) & (I_NEW | I_FREEING | I_WILL_FREE)) { + spin_unlock(&inode->i_lock); + continue; + } + + inode_state_set(inode, I_FREEING); + inode_lru_list_del(inode); + spin_unlock(&inode->i_lock); + list_add(&inode->i_lru, &dispose); + + /* + * We can have a ton of inodes to evict at unmount time given + * enough memory, check to see if we need to go to sleep for a + * bit so we don't livelock. + */ + if (need_resched()) { + spin_unlock(lock); + cond_resched(); + dispose_list(&dispose); + goto again; + } } + spin_unlock(lock); } - spin_unlock(&sb->s_inode_list_lock); dispose_list(&dispose); } diff --git a/fs/proc/inode.c b/fs/proc/inode.c index b7634f975d98..aeaf2e94e7ba 100644 --- a/fs/proc/inode.c +++ b/fs/proc/inode.c @@ -65,6 +65,7 @@ static struct inode *proc_alloc_inode(struct super_block *sb) ei->sysctl_entry = NULL; INIT_HLIST_NODE(&ei->sibling_inodes); ei->ns_ops = NULL; + ei->shard_idx = 0; return &ei->vfs_inode; } @@ -80,6 +81,56 @@ static void proc_free_inode(struct inode *inode) kmem_cache_free(proc_inode_cachep, PROC_I(inode)); } +static void proc_inode_list_add(struct super_block *sb, struct inode *inode) +{ + struct proc_fs_info *fs_info = proc_sb_info(sb); + struct proc_inode *ei = PROC_I(inode); + struct inode_shard *shard; + unsigned int idx, seq; + + seq = atomic_fetch_inc(&fs_info->shard_seq); + idx = seq % sb->nr_shards; + ei->shard_idx = idx; + shard = &sb->shards[idx]; + + spin_lock(&shard->lock); + list_add(&inode->i_sb_list, &shard->list); + spin_unlock(&shard->lock); +} + +static void proc_inode_list_del(struct super_block *sb, struct inode *inode) +{ + struct proc_inode *ei = PROC_I(inode); + struct inode_shard *shard = &sb->shards[ei->shard_idx]; + + spin_lock(&shard->lock); + list_del_init(&inode->i_sb_list); + spin_unlock(&shard->lock); +} + +#define PROC_LIST_ALIGN 32 +int proc_init_inode_shards(struct super_block *sb) +{ + struct inode_shard *shards; + unsigned int nr; + int i; + + nr = DIV_ROUND_UP(num_possible_cpus(), PROC_LIST_ALIGN); + shards = kcalloc(nr, sizeof(*shards), GFP_KERNEL); + if (!shards) + return -ENOMEM; + + for (i = 0; i < nr; i++) { + INIT_LIST_HEAD(&shards[i].list); + spin_lock_init(&shards[i].lock); + } + + sb->shards = shards; + sb->nr_shards = nr; + sb->s_inode_list_sharded = true; + return 0; +} + static void init_once(void *foo) { struct proc_inode *ei = (struct proc_inode *) foo; @@ -191,6 +242,8 @@ const struct super_operations proc_sops = { .evict_inode = proc_evict_inode, .statfs = simple_statfs, .show_options = proc_show_options, + .inode_list_add = proc_inode_list_add, + .inode_list_del = proc_inode_list_del, }; enum {BIAS = -1U<<31}; diff --git a/fs/proc/internal.h b/fs/proc/internal.h index 04bd6c9e65a7..edbe357263c2 100644 --- a/fs/proc/internal.h +++ b/fs/proc/internal.h @@ -127,6 +127,7 @@ struct proc_inode { struct hlist_node sibling_inodes; const struct proc_ns_operations *ns_ops; struct inode vfs_inode; + unsigned int shard_idx; } __randomize_layout; /* @@ -317,6 +318,7 @@ void proc_init_kmemcache(void); void proc_invalidate_siblings_dcache(struct hlist_head *inodes, spinlock_t *lock); void set_proc_pid_nlink(void); extern struct inode *proc_get_inode(struct super_block *, struct proc_dir_entry *); +extern int proc_init_inode_shards(struct super_block *); extern void proc_entry_rundown(struct proc_dir_entry *); /* diff --git a/fs/proc/root.c b/fs/proc/root.c index d45f5af5ab53..fb5d68da85f4 100644 --- a/fs/proc/root.c +++ b/fs/proc/root.c @@ -275,6 +275,10 @@ static int proc_fill_super(struct super_block *s, struct fs_context *fc) s->s_time_gran = 1; s->s_fs_info = fs_info; + ret = proc_init_inode_shards(s); + if (ret) + return ret; + if (fs_info->pidonly == PROC_PIDONLY_ON) s->s_iflags |= SB_I_RESTRICTED_VARIANT; @@ -359,6 +363,8 @@ static void proc_kill_sb(struct super_block *sb) struct proc_fs_info *fs_info = proc_sb_info(sb); kill_anon_super(sb); + if (sb->s_inode_list_sharded) + kfree(sb->shards); if (fs_info) { put_pid_ns(fs_info->pid_ns); put_cred(fs_info->mounter_cred); diff --git a/fs/super.c b/fs/super.c index e25ded0bc9a2..e44db4505238 100644 --- a/fs/super.c +++ b/fs/super.c @@ -712,7 +712,16 @@ EXPORT_SYMBOL(retire_super); static bool sb_inodes_empty(struct super_block *sb) { - return list_empty(&sb->s_inodes); + unsigned int i; + + if (!sb->s_inode_list_sharded) + return list_empty(&sb->s_inodes); + + for (i = 0; i < sb->nr_shards; i++) + if (!list_empty(&sb->shards[i].list)) + return false; + + return true; } /** @@ -774,14 +783,29 @@ void generic_shutdown_super(struct super_block *sb) * iput_final() or such crashes cleanly. */ struct inode *inode; - - spin_lock(&sb->s_inode_list_lock); - list_for_each_entry(inode, &sb->s_inodes, i_sb_list) { - inode->i_op = VFS_PTR_POISON; - inode->i_sb = VFS_PTR_POISON; - inode->i_mapping = VFS_PTR_POISON; + struct list_head *head; + spinlock_t *lock; + unsigned int nr, i; + const bool sharded = sb->s_inode_list_sharded; + + nr = sharded ? sb->nr_shards : 1; + for (i = 0; i < nr; i++) { + if (sharded) { + head = &sb->shards[i].list; + lock = &sb->shards[i].lock; + } else { + head = &sb->s_inodes; + lock = &sb->s_inode_list_lock; + } + + spin_lock(lock); + list_for_each_entry(inode, head, i_sb_list) { + inode->i_op = VFS_PTR_POISON; + inode->i_sb = VFS_PTR_POISON; + inode->i_mapping = VFS_PTR_POISON; + } + spin_unlock(lock); } - spin_unlock(&sb->s_inode_list_lock); } } /* diff --git a/include/linux/fs/super_types.h b/include/linux/fs/super_types.h index 1a596caf58a8..f06f293b46be 100644 --- a/include/linux/fs/super_types.h +++ b/include/linux/fs/super_types.h @@ -130,8 +130,20 @@ struct super_operations { /* Report a filesystem error */ void (*report_error)(const struct fserror_event *event); + + void (*inode_list_add)(struct super_block *sb, struct inode *inode); + void (*inode_list_del)(struct super_block *sb, struct inode *inode); }; +/* + * Sharded inode list which is used to spread s_inode_list_lock contention + * across per-shard locks. + */ +struct inode_shard { + struct list_head list; + spinlock_t lock; +} ____cacheline_aligned_in_smp; + struct super_block { struct list_head s_list; /* Keep this first */ dev_t s_dev; /* search index; _not_ kdev_t */ @@ -269,6 +281,10 @@ struct super_block { */ int s_stack_depth; + bool s_inode_list_sharded; + struct inode_shard *shards; + unsigned int nr_shards; + /* s_inode_list_lock protects s_inodes */ spinlock_t s_inode_list_lock ____cacheline_aligned_in_smp; struct list_head s_inodes; /* all inodes */ diff --git a/include/linux/proc_fs.h b/include/linux/proc_fs.h index 47d7deaeed8f..37eed34f3a93 100644 --- a/include/linux/proc_fs.h +++ b/include/linux/proc_fs.h @@ -70,6 +70,7 @@ struct proc_fs_info { const struct cred *mounter_cred; enum proc_hidepid hide_pid; enum proc_pidonly pidonly; + atomic_t shard_seq; struct rcu_head rcu; }; diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 330a1871bf94..8360a6a8a3ad 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -1383,98 +1383,114 @@ static void hook_inode_free_security_rcu(void *inode_security) static void hook_sb_delete(struct super_block *const sb) { struct inode *inode, *prev_inode = NULL; + unsigned int nr, i; if (!landlock_initialized) return; - spin_lock(&sb->s_inode_list_lock); - list_for_each_entry(inode, &sb->s_inodes, i_sb_list) { - struct landlock_object *object; + nr = sb->s_inode_list_sharded ? sb->nr_shards : 1; - /* Only handles referenced inodes. */ - if (!icount_read_once(inode)) - continue; + for (i = 0; i < nr; i++) { + struct list_head *head; + spinlock_t *lock; - /* - * Protects against concurrent modification of inode (e.g. - * from get_inode_object()). - */ - spin_lock(&inode->i_lock); - /* - * Checks I_FREEING and I_WILL_FREE to protect against a race - * condition when release_inode() just called iput(), which - * could lead to a NULL dereference of inode->security or a - * second call to iput() for the same Landlock object. Also - * checks I_NEW because such inode cannot be tied to an object. - */ - if (inode_state_read(inode) & - (I_FREEING | I_WILL_FREE | I_NEW)) { - spin_unlock(&inode->i_lock); - continue; + if (sb->s_inode_list_sharded) { + head = &sb->shards[i].list; + lock = &sb->shards[i].lock; + } else { + head = &sb->s_inodes; + lock = &sb->s_inode_list_lock; } - rcu_read_lock(); - object = rcu_dereference(landlock_inode(inode)->object); - if (!object) { - rcu_read_unlock(); - spin_unlock(&inode->i_lock); - continue; - } - /* Keeps a reference to this inode until the next loop walk. */ - __iget(inode); - spin_unlock(&inode->i_lock); + spin_lock(lock); + list_for_each_entry(inode, head, i_sb_list) { + struct landlock_object *object; - /* - * If there is no concurrent release_inode() ongoing, then we - * are in charge of calling iput() on this inode, otherwise we - * will just wait for it to finish. - */ - spin_lock(&object->lock); - if (object->underobj == inode) { - object->underobj = NULL; - spin_unlock(&object->lock); - rcu_read_unlock(); + /* Only handles referenced inodes. */ + if (!icount_read_once(inode)) + continue; /* - * Because object->underobj was not NULL, - * release_inode() and get_inode_object() guarantee - * that it is safe to reset - * landlock_inode(inode)->object while it is not NULL. - * It is therefore not necessary to lock inode->i_lock. + * Protects against concurrent modification of inode (e.g. + * from get_inode_object()). */ - rcu_assign_pointer(landlock_inode(inode)->object, NULL); + spin_lock(&inode->i_lock); /* - * At this point, we own the ihold() reference that was - * originally set up by get_inode_object() and the - * __iget() reference that we just set in this loop - * walk. Therefore there are at least two references - * on the inode. + * Checks I_FREEING and I_WILL_FREE to protect against a race + * condition when release_inode() just called iput(), which + * could lead to a NULL dereference of inode->security or a + * second call to iput() for the same Landlock object. Also + * checks I_NEW because such inode cannot be tied to an object. */ - iput_not_last(inode); - } else { - spin_unlock(&object->lock); - rcu_read_unlock(); - } + if (inode_state_read(inode) & + (I_FREEING | I_WILL_FREE | I_NEW)) { + spin_unlock(&inode->i_lock); + continue; + } + + rcu_read_lock(); + object = rcu_dereference(landlock_inode(inode)->object); + if (!object) { + rcu_read_unlock(); + spin_unlock(&inode->i_lock); + continue; + } + /* Keeps a reference to this inode until the next loop walk. */ + __iget(inode); + spin_unlock(&inode->i_lock); - if (prev_inode) { - /* - * At this point, we still own the __iget() reference - * that we just set in this loop walk. Therefore we - * can drop the list lock and know that the inode won't - * disappear from under us until the next loop walk. - */ - spin_unlock(&sb->s_inode_list_lock); /* - * We can now actually put the inode reference from the - * previous loop walk, which is not needed anymore. + * If there is no concurrent release_inode() ongoing, then we + * are in charge of calling iput() on this inode, otherwise we + * will just wait for it to finish. */ - iput(prev_inode); - cond_resched(); - spin_lock(&sb->s_inode_list_lock); + spin_lock(&object->lock); + if (object->underobj == inode) { + object->underobj = NULL; + spin_unlock(&object->lock); + rcu_read_unlock(); + + /* + * Because object->underobj was not NULL, + * release_inode() and get_inode_object() guarantee + * that it is safe to reset + * landlock_inode(inode)->object while it is not NULL. + * It is therefore not necessary to lock inode->i_lock. + */ + rcu_assign_pointer(landlock_inode(inode)->object, NULL); + /* + * At this point, we own the ihold() reference that was + * originally set up by get_inode_object() and the + * __iget() reference that we just set in this loop + * walk. Therefore there are at least two references + * on the inode. + */ + iput_not_last(inode); + } else { + spin_unlock(&object->lock); + rcu_read_unlock(); + } + + if (prev_inode) { + /* + * At this point, we still own the __iget() reference + * that we just set in this loop walk. Therefore we + * can drop the list lock and know that the inode won't + * disappear from under us until the next loop walk. + */ + spin_unlock(lock); + /* + * We can now actually put the inode reference from the + * previous loop walk, which is not needed anymore. + */ + iput(prev_inode); + cond_resched(); + spin_lock(lock); + } + prev_inode = inode; } - prev_inode = inode; + spin_unlock(lock); } - spin_unlock(&sb->s_inode_list_lock); /* Puts the inode reference from the last loop walk, if any. */ if (prev_inode) -- 2.53.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [RFC PATCH v2 3/3] fs/proc: split the inode list 2026-09-20 7:28 ` [RFC PATCH v2 3/3] fs/proc: split the inode list Huang Shijie @ 2026-09-22 13:29 ` Jan Kara 0 siblings, 0 replies; 7+ messages in thread From: Jan Kara @ 2026-09-22 13:29 UTC (permalink / raw) To: Huang Shijie Cc: viro, brauner, zhongyuan, fangbaoshun, yingzhiwei, jack, dev.jain, ljs, adobriyan, akpm, hpa, ryan.roberts, mjguzik, joel.granados, jannh, oleg, aleksa, legion, kees, joannelkoong, tj, libaokun, ebiggers, sandeen, linux-fsdevel, linux-kernel On Sun 20-09-26 15:28:11, Huang Shijie wrote: > The global s_inode_list_lock is heavily contended in procfs > on a 384-CPU, 12-NUMA-node Hygon machine running Hadoop TestDFSIO: > #hadoop jar xxxx.jar TestDFSIO -read -nrFiles 1000 -size 100MB > > The perf shows it consuming ~90% of the lock hotspot. > The lock is hit from both directions: > -- inode creation (~49%) : > getdents64 -> > proc_readfd_common -> > new_inode -> > inode_sb_list_add() > > -- inode eviction (~41%) > process exit -> > release_task -> > proc_invalidate_siblings_dcache -> > evict -> > inode_sb_list_del() > > This patch spreads the inode list across per-shard locks for procfs: > --- Add three fields in super_block: > shards : the pointer for the array of inode_shard. > nr_shards: the size of the array > s_inode_list_sharded: whether or not to use a sharded inode list > > struct inode_shard is cacheline-aligned to avoid false > sharing between shard locks on different NUMA nodes. > > --- Add inode_list_add()/inode_list_del() callbacks to super_operations; > procfs implements them to round-robin inodes > onto nr_shards = DIV_ROUND_UP(num_possible_cpus(), 32) > shards allocated at mount time, each protected by its own spinlock. > > --- For procfs, the "unmount" will call evict_inodes(), > generic_shutdown_super() and hook_sb_delete() which will > iterate the shards when the super_block inode list is sharded. > Change these functions to work with the sharded inode list. > > This reduces the s_inode_list_lock hotspot from ~90% to ~1% in TestDFSIO. > And we can improve the hadoop performance over 50%. > > Signed-off-by: Huang Shijie <huangsj@hygon.cn> It has been recognized in the past that superblock global inode list is a scalability bottleneck for certain loads. Not only for procfs but even for XFS or other disk-based filesystems. I think last this was discussed here [1]. So I'm not if favor of trying to deal with it just for procfs, that's just ugly and will backfire pretty quickly. Of course more generic solution needs more work, in particular we first need to abstract out inode iteration. Julian might be already looking into that [2] so better talk to him before investing significant time into that. After inode iteration is abstracted, we can relatively painlessly switch the data structure tracking inodes per superblock to something more scalable. Honza [1] https://lore.kernel.org/all/20241002014017.3801899-1-david@fromorbit.com/ [2] https://lore.kernel.org/all/1891f4ac-139e-4e9c-80ff-802ec2bb2844@bytedance.com/ > --- > fs/inode.c | 79 ++++++++++------ > fs/proc/inode.c | 53 +++++++++++ > fs/proc/internal.h | 2 + > fs/proc/root.c | 6 ++ > fs/super.c | 40 ++++++-- > include/linux/fs/super_types.h | 16 ++++ > include/linux/proc_fs.h | 1 + > security/landlock/fs.c | 162 ++++++++++++++++++--------------- > 8 files changed, 250 insertions(+), 109 deletions(-) > > diff --git a/fs/inode.c b/fs/inode.c > index ba7da39be4a3..80acb9113487 100644 > --- a/fs/inode.c > +++ b/fs/inode.c > @@ -635,6 +635,10 @@ void inode_sb_list_add(struct inode *inode) > { > struct super_block *sb = inode->i_sb; > > + if (sb->s_inode_list_sharded && sb->s_op->inode_list_add) { > + sb->s_op->inode_list_add(sb, inode); > + return; > + } > spin_lock(&sb->s_inode_list_lock); > list_add(&inode->i_sb_list, &sb->s_inodes); > spin_unlock(&sb->s_inode_list_lock); > @@ -646,6 +650,10 @@ static inline void inode_sb_list_del(struct inode *inode) > struct super_block *sb = inode->i_sb; > > if (!list_empty(&inode->i_sb_list)) { > + if (sb->s_inode_list_sharded && sb->s_op->inode_list_del) { > + sb->s_op->inode_list_del(sb, inode); > + return; > + } > spin_lock(&sb->s_inode_list_lock); > list_del_init(&inode->i_sb_list); > spin_unlock(&sb->s_inode_list_lock); > @@ -879,41 +887,56 @@ void evict_inodes(struct super_block *sb) > { > struct inode *inode; > LIST_HEAD(dispose); > + struct list_head *head; > + spinlock_t *lock; > + unsigned int nr, i; > + const bool sharded = sb->s_inode_list_sharded; > > + nr = sharded ? sb->nr_shards : 1; > again: > - spin_lock(&sb->s_inode_list_lock); > - list_for_each_entry(inode, &sb->s_inodes, i_sb_list) { > - if (icount_read_once(inode)) > - continue; > - > - spin_lock(&inode->i_lock); > - if (icount_read(inode)) { > - spin_unlock(&inode->i_lock); > - continue; > - } > - if (inode_state_read(inode) & (I_NEW | I_FREEING | I_WILL_FREE)) { > - spin_unlock(&inode->i_lock); > - continue; > + for (i = 0; i < nr; i++) { > + if (sharded) { > + head = &sb->shards[i].list; > + lock = &sb->shards[i].lock; > + } else { > + head = &sb->s_inodes; > + lock = &sb->s_inode_list_lock; > } > > - inode_state_set(inode, I_FREEING); > - inode_lru_list_del(inode); > - spin_unlock(&inode->i_lock); > - list_add(&inode->i_lru, &dispose); > + spin_lock(lock); > + list_for_each_entry(inode, head, i_sb_list) { > + if (icount_read_once(inode)) > + continue; > > - /* > - * We can have a ton of inodes to evict at unmount time given > - * enough memory, check to see if we need to go to sleep for a > - * bit so we don't livelock. > - */ > - if (need_resched()) { > - spin_unlock(&sb->s_inode_list_lock); > - cond_resched(); > - dispose_list(&dispose); > - goto again; > + spin_lock(&inode->i_lock); > + if (icount_read(inode)) { > + spin_unlock(&inode->i_lock); > + continue; > + } > + if (inode_state_read(inode) & (I_NEW | I_FREEING | I_WILL_FREE)) { > + spin_unlock(&inode->i_lock); > + continue; > + } > + > + inode_state_set(inode, I_FREEING); > + inode_lru_list_del(inode); > + spin_unlock(&inode->i_lock); > + list_add(&inode->i_lru, &dispose); > + > + /* > + * We can have a ton of inodes to evict at unmount time given > + * enough memory, check to see if we need to go to sleep for a > + * bit so we don't livelock. > + */ > + if (need_resched()) { > + spin_unlock(lock); > + cond_resched(); > + dispose_list(&dispose); > + goto again; > + } > } > + spin_unlock(lock); > } > - spin_unlock(&sb->s_inode_list_lock); > > dispose_list(&dispose); > } > diff --git a/fs/proc/inode.c b/fs/proc/inode.c > index b7634f975d98..aeaf2e94e7ba 100644 > --- a/fs/proc/inode.c > +++ b/fs/proc/inode.c > @@ -65,6 +65,7 @@ static struct inode *proc_alloc_inode(struct super_block *sb) > ei->sysctl_entry = NULL; > INIT_HLIST_NODE(&ei->sibling_inodes); > ei->ns_ops = NULL; > + ei->shard_idx = 0; > return &ei->vfs_inode; > } > > @@ -80,6 +81,56 @@ static void proc_free_inode(struct inode *inode) > kmem_cache_free(proc_inode_cachep, PROC_I(inode)); > } > > +static void proc_inode_list_add(struct super_block *sb, struct inode *inode) > +{ > + struct proc_fs_info *fs_info = proc_sb_info(sb); > + struct proc_inode *ei = PROC_I(inode); > + struct inode_shard *shard; > + unsigned int idx, seq; > + > + seq = atomic_fetch_inc(&fs_info->shard_seq); > + idx = seq % sb->nr_shards; > + ei->shard_idx = idx; > + shard = &sb->shards[idx]; > + > + spin_lock(&shard->lock); > + list_add(&inode->i_sb_list, &shard->list); > + spin_unlock(&shard->lock); > +} > + > +static void proc_inode_list_del(struct super_block *sb, struct inode *inode) > +{ > + struct proc_inode *ei = PROC_I(inode); > + struct inode_shard *shard = &sb->shards[ei->shard_idx]; > + > + spin_lock(&shard->lock); > + list_del_init(&inode->i_sb_list); > + spin_unlock(&shard->lock); > +} > + > +#define PROC_LIST_ALIGN 32 > +int proc_init_inode_shards(struct super_block *sb) > +{ > + struct inode_shard *shards; > + unsigned int nr; > + int i; > + > + nr = DIV_ROUND_UP(num_possible_cpus(), PROC_LIST_ALIGN); > + shards = kcalloc(nr, sizeof(*shards), GFP_KERNEL); > + if (!shards) > + return -ENOMEM; > + > + for (i = 0; i < nr; i++) { > + INIT_LIST_HEAD(&shards[i].list); > + spin_lock_init(&shards[i].lock); > + } > + > + sb->shards = shards; > + sb->nr_shards = nr; > + sb->s_inode_list_sharded = true; > + return 0; > +} > + > static void init_once(void *foo) > { > struct proc_inode *ei = (struct proc_inode *) foo; > @@ -191,6 +242,8 @@ const struct super_operations proc_sops = { > .evict_inode = proc_evict_inode, > .statfs = simple_statfs, > .show_options = proc_show_options, > + .inode_list_add = proc_inode_list_add, > + .inode_list_del = proc_inode_list_del, > }; > > enum {BIAS = -1U<<31}; > diff --git a/fs/proc/internal.h b/fs/proc/internal.h > index 04bd6c9e65a7..edbe357263c2 100644 > --- a/fs/proc/internal.h > +++ b/fs/proc/internal.h > @@ -127,6 +127,7 @@ struct proc_inode { > struct hlist_node sibling_inodes; > const struct proc_ns_operations *ns_ops; > struct inode vfs_inode; > + unsigned int shard_idx; > } __randomize_layout; > > /* > @@ -317,6 +318,7 @@ void proc_init_kmemcache(void); > void proc_invalidate_siblings_dcache(struct hlist_head *inodes, spinlock_t *lock); > void set_proc_pid_nlink(void); > extern struct inode *proc_get_inode(struct super_block *, struct proc_dir_entry *); > +extern int proc_init_inode_shards(struct super_block *); > extern void proc_entry_rundown(struct proc_dir_entry *); > > /* > diff --git a/fs/proc/root.c b/fs/proc/root.c > index d45f5af5ab53..fb5d68da85f4 100644 > --- a/fs/proc/root.c > +++ b/fs/proc/root.c > @@ -275,6 +275,10 @@ static int proc_fill_super(struct super_block *s, struct fs_context *fc) > s->s_time_gran = 1; > s->s_fs_info = fs_info; > > + ret = proc_init_inode_shards(s); > + if (ret) > + return ret; > + > if (fs_info->pidonly == PROC_PIDONLY_ON) > s->s_iflags |= SB_I_RESTRICTED_VARIANT; > > @@ -359,6 +363,8 @@ static void proc_kill_sb(struct super_block *sb) > struct proc_fs_info *fs_info = proc_sb_info(sb); > > kill_anon_super(sb); > + if (sb->s_inode_list_sharded) > + kfree(sb->shards); > if (fs_info) { > put_pid_ns(fs_info->pid_ns); > put_cred(fs_info->mounter_cred); > diff --git a/fs/super.c b/fs/super.c > index e25ded0bc9a2..e44db4505238 100644 > --- a/fs/super.c > +++ b/fs/super.c > @@ -712,7 +712,16 @@ EXPORT_SYMBOL(retire_super); > > static bool sb_inodes_empty(struct super_block *sb) > { > - return list_empty(&sb->s_inodes); > + unsigned int i; > + > + if (!sb->s_inode_list_sharded) > + return list_empty(&sb->s_inodes); > + > + for (i = 0; i < sb->nr_shards; i++) > + if (!list_empty(&sb->shards[i].list)) > + return false; > + > + return true; > } > > /** > @@ -774,14 +783,29 @@ void generic_shutdown_super(struct super_block *sb) > * iput_final() or such crashes cleanly. > */ > struct inode *inode; > - > - spin_lock(&sb->s_inode_list_lock); > - list_for_each_entry(inode, &sb->s_inodes, i_sb_list) { > - inode->i_op = VFS_PTR_POISON; > - inode->i_sb = VFS_PTR_POISON; > - inode->i_mapping = VFS_PTR_POISON; > + struct list_head *head; > + spinlock_t *lock; > + unsigned int nr, i; > + const bool sharded = sb->s_inode_list_sharded; > + > + nr = sharded ? sb->nr_shards : 1; > + for (i = 0; i < nr; i++) { > + if (sharded) { > + head = &sb->shards[i].list; > + lock = &sb->shards[i].lock; > + } else { > + head = &sb->s_inodes; > + lock = &sb->s_inode_list_lock; > + } > + > + spin_lock(lock); > + list_for_each_entry(inode, head, i_sb_list) { > + inode->i_op = VFS_PTR_POISON; > + inode->i_sb = VFS_PTR_POISON; > + inode->i_mapping = VFS_PTR_POISON; > + } > + spin_unlock(lock); > } > - spin_unlock(&sb->s_inode_list_lock); > } > } > /* > diff --git a/include/linux/fs/super_types.h b/include/linux/fs/super_types.h > index 1a596caf58a8..f06f293b46be 100644 > --- a/include/linux/fs/super_types.h > +++ b/include/linux/fs/super_types.h > @@ -130,8 +130,20 @@ struct super_operations { > > /* Report a filesystem error */ > void (*report_error)(const struct fserror_event *event); > + > + void (*inode_list_add)(struct super_block *sb, struct inode *inode); > + void (*inode_list_del)(struct super_block *sb, struct inode *inode); > }; > > +/* > + * Sharded inode list which is used to spread s_inode_list_lock contention > + * across per-shard locks. > + */ > +struct inode_shard { > + struct list_head list; > + spinlock_t lock; > +} ____cacheline_aligned_in_smp; > + > struct super_block { > struct list_head s_list; /* Keep this first */ > dev_t s_dev; /* search index; _not_ kdev_t */ > @@ -269,6 +281,10 @@ struct super_block { > */ > int s_stack_depth; > > + bool s_inode_list_sharded; > + struct inode_shard *shards; > + unsigned int nr_shards; > + > /* s_inode_list_lock protects s_inodes */ > spinlock_t s_inode_list_lock ____cacheline_aligned_in_smp; > struct list_head s_inodes; /* all inodes */ > diff --git a/include/linux/proc_fs.h b/include/linux/proc_fs.h > index 47d7deaeed8f..37eed34f3a93 100644 > --- a/include/linux/proc_fs.h > +++ b/include/linux/proc_fs.h > @@ -70,6 +70,7 @@ struct proc_fs_info { > const struct cred *mounter_cred; > enum proc_hidepid hide_pid; > enum proc_pidonly pidonly; > + atomic_t shard_seq; > struct rcu_head rcu; > }; > > diff --git a/security/landlock/fs.c b/security/landlock/fs.c > index 330a1871bf94..8360a6a8a3ad 100644 > --- a/security/landlock/fs.c > +++ b/security/landlock/fs.c > @@ -1383,98 +1383,114 @@ static void hook_inode_free_security_rcu(void *inode_security) > static void hook_sb_delete(struct super_block *const sb) > { > struct inode *inode, *prev_inode = NULL; > + unsigned int nr, i; > > if (!landlock_initialized) > return; > > - spin_lock(&sb->s_inode_list_lock); > - list_for_each_entry(inode, &sb->s_inodes, i_sb_list) { > - struct landlock_object *object; > + nr = sb->s_inode_list_sharded ? sb->nr_shards : 1; > > - /* Only handles referenced inodes. */ > - if (!icount_read_once(inode)) > - continue; > + for (i = 0; i < nr; i++) { > + struct list_head *head; > + spinlock_t *lock; > > - /* > - * Protects against concurrent modification of inode (e.g. > - * from get_inode_object()). > - */ > - spin_lock(&inode->i_lock); > - /* > - * Checks I_FREEING and I_WILL_FREE to protect against a race > - * condition when release_inode() just called iput(), which > - * could lead to a NULL dereference of inode->security or a > - * second call to iput() for the same Landlock object. Also > - * checks I_NEW because such inode cannot be tied to an object. > - */ > - if (inode_state_read(inode) & > - (I_FREEING | I_WILL_FREE | I_NEW)) { > - spin_unlock(&inode->i_lock); > - continue; > + if (sb->s_inode_list_sharded) { > + head = &sb->shards[i].list; > + lock = &sb->shards[i].lock; > + } else { > + head = &sb->s_inodes; > + lock = &sb->s_inode_list_lock; > } > > - rcu_read_lock(); > - object = rcu_dereference(landlock_inode(inode)->object); > - if (!object) { > - rcu_read_unlock(); > - spin_unlock(&inode->i_lock); > - continue; > - } > - /* Keeps a reference to this inode until the next loop walk. */ > - __iget(inode); > - spin_unlock(&inode->i_lock); > + spin_lock(lock); > + list_for_each_entry(inode, head, i_sb_list) { > + struct landlock_object *object; > > - /* > - * If there is no concurrent release_inode() ongoing, then we > - * are in charge of calling iput() on this inode, otherwise we > - * will just wait for it to finish. > - */ > - spin_lock(&object->lock); > - if (object->underobj == inode) { > - object->underobj = NULL; > - spin_unlock(&object->lock); > - rcu_read_unlock(); > + /* Only handles referenced inodes. */ > + if (!icount_read_once(inode)) > + continue; > > /* > - * Because object->underobj was not NULL, > - * release_inode() and get_inode_object() guarantee > - * that it is safe to reset > - * landlock_inode(inode)->object while it is not NULL. > - * It is therefore not necessary to lock inode->i_lock. > + * Protects against concurrent modification of inode (e.g. > + * from get_inode_object()). > */ > - rcu_assign_pointer(landlock_inode(inode)->object, NULL); > + spin_lock(&inode->i_lock); > /* > - * At this point, we own the ihold() reference that was > - * originally set up by get_inode_object() and the > - * __iget() reference that we just set in this loop > - * walk. Therefore there are at least two references > - * on the inode. > + * Checks I_FREEING and I_WILL_FREE to protect against a race > + * condition when release_inode() just called iput(), which > + * could lead to a NULL dereference of inode->security or a > + * second call to iput() for the same Landlock object. Also > + * checks I_NEW because such inode cannot be tied to an object. > */ > - iput_not_last(inode); > - } else { > - spin_unlock(&object->lock); > - rcu_read_unlock(); > - } > + if (inode_state_read(inode) & > + (I_FREEING | I_WILL_FREE | I_NEW)) { > + spin_unlock(&inode->i_lock); > + continue; > + } > + > + rcu_read_lock(); > + object = rcu_dereference(landlock_inode(inode)->object); > + if (!object) { > + rcu_read_unlock(); > + spin_unlock(&inode->i_lock); > + continue; > + } > + /* Keeps a reference to this inode until the next loop walk. */ > + __iget(inode); > + spin_unlock(&inode->i_lock); > > - if (prev_inode) { > - /* > - * At this point, we still own the __iget() reference > - * that we just set in this loop walk. Therefore we > - * can drop the list lock and know that the inode won't > - * disappear from under us until the next loop walk. > - */ > - spin_unlock(&sb->s_inode_list_lock); > /* > - * We can now actually put the inode reference from the > - * previous loop walk, which is not needed anymore. > + * If there is no concurrent release_inode() ongoing, then we > + * are in charge of calling iput() on this inode, otherwise we > + * will just wait for it to finish. > */ > - iput(prev_inode); > - cond_resched(); > - spin_lock(&sb->s_inode_list_lock); > + spin_lock(&object->lock); > + if (object->underobj == inode) { > + object->underobj = NULL; > + spin_unlock(&object->lock); > + rcu_read_unlock(); > + > + /* > + * Because object->underobj was not NULL, > + * release_inode() and get_inode_object() guarantee > + * that it is safe to reset > + * landlock_inode(inode)->object while it is not NULL. > + * It is therefore not necessary to lock inode->i_lock. > + */ > + rcu_assign_pointer(landlock_inode(inode)->object, NULL); > + /* > + * At this point, we own the ihold() reference that was > + * originally set up by get_inode_object() and the > + * __iget() reference that we just set in this loop > + * walk. Therefore there are at least two references > + * on the inode. > + */ > + iput_not_last(inode); > + } else { > + spin_unlock(&object->lock); > + rcu_read_unlock(); > + } > + > + if (prev_inode) { > + /* > + * At this point, we still own the __iget() reference > + * that we just set in this loop walk. Therefore we > + * can drop the list lock and know that the inode won't > + * disappear from under us until the next loop walk. > + */ > + spin_unlock(lock); > + /* > + * We can now actually put the inode reference from the > + * previous loop walk, which is not needed anymore. > + */ > + iput(prev_inode); > + cond_resched(); > + spin_lock(lock); > + } > + prev_inode = inode; > } > - prev_inode = inode; > + spin_unlock(lock); > } > - spin_unlock(&sb->s_inode_list_lock); > > /* Puts the inode reference from the last loop walk, if any. */ > if (prev_inode) > -- > 2.53.0 > > -- Jan Kara <jack@suse.com> SUSE Labs, CR ^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-09-23 5:49 UTC | newest] Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-09-20 7:28 [RFC PATCH v2 0/3] fs/proc: split the inode list for procfs Huang Shijie 2026-09-20 7:28 ` [RFC PATCH v2 1/3] fs/drop_caches: skip filesystems without page cache Huang Shijie 2026-09-22 5:41 ` Christoph Hellwig 2026-09-23 5:49 ` Huang Shijie 2026-09-20 7:28 ` [RFC PATCH v2 2/3] fs/super: introduce a helper sb_inodes_empty() Huang Shijie 2026-09-20 7:28 ` [RFC PATCH v2 3/3] fs/proc: split the inode list Huang Shijie 2026-09-22 13:29 ` Jan Kara
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®