evict_inodes() traverses sb->s_inodes to reclaim all zero-refcount inodes at unmount time. When the traversal yields to the scheduler via cond_resched(), the current code jumps back to a "goto again" label and restarts the list_for_each_entry loop from the head. Inodes already marked I_FREEING are skipped cheaply, but the repeated scans make the total work O(n * r) where r is the number of reschedule events -- proportional to O(n^2) in the worst case on systems with many inodes and memory pressure. Replace the goto-again pattern with a sentinel cursor node embedded in sb->s_inodes. list_move() advances the cursor past each visited inode in O(1). Dropping and reacquiring s_inode_list_lock around cond_resched() leaves the cursor in place, so the loop resumes from exactly the current position rather than the list head, giving O(n) total traversal. Co-developed-by: Qiliang Yuan Signed-off-by: Qiliang Yuan Signed-off-by: Jing Wu --- evict_inodes() is called at unmount time to reclaim all zero-refcount inodes on a superblock. Its current implementation uses a "goto again" pattern: after dropping s_inode_list_lock for cond_resched() and dispose_list(), it restarts list_for_each_entry from the list head. In the common case (clean unmount, all files closed) the goto loop terminates quickly because dispose_list() removes evicted inodes before the restart. However, when many inodes have a positive refcount and cannot be evicted -- as happens with a force-unmount while file descriptors are still open -- the restarts scan O(n) skippable inodes each time, making total work O(n * r) proportional to O(n^2) in the worst case. This series replaces the goto-again pattern with a sentinel cursor node embedded in sb->s_inodes, allowing the traversal to resume from its current position after each lock drop. --- fs/inode.c | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/fs/inode.c b/fs/inode.c index acf206beb2e03..19197368c6a67 100644 --- a/fs/inode.c +++ b/fs/inode.c @@ -880,15 +880,31 @@ static void dispose_list(struct list_head *head) * called by superblock shutdown after having SB_ACTIVE flag removed, * so any inode reaching zero refcount during or after that call will * be immediately evicted. + * + * Use a cursor node embedded in sb->s_inodes to resume traversal after + * dropping s_inode_list_lock for cond_resched() + dispose_list(). This + * avoids restarting the scan from the list head on each reschedule, giving + * O(n) total traversal instead of O(n * r) where r is the reschedule count. */ void evict_inodes(struct super_block *sb) { struct inode *inode; LIST_HEAD(dispose); + /* + * Embed a cursor node directly in sb->s_inodes. list_move() advances + * it past each visited inode in O(1), so the loop resumes from exactly + * the current position after lock drop rather than from the list head. + */ + struct list_head cursor; -again: spin_lock(&sb->s_inode_list_lock); - list_for_each_entry(inode, &sb->s_inodes, i_sb_list) { + list_add(&cursor, &sb->s_inodes); + + while (cursor.next != &sb->s_inodes) { + inode = list_entry(cursor.next, struct inode, i_sb_list); + /* Leave the cursor immediately after the current inode. */ + list_move(&cursor, &inode->i_sb_list); + if (icount_read_once(inode)) continue; @@ -916,9 +932,11 @@ void evict_inodes(struct super_block *sb) spin_unlock(&sb->s_inode_list_lock); cond_resched(); dispose_list(&dispose); - goto again; + spin_lock(&sb->s_inode_list_lock); } } + + list_del(&cursor); spin_unlock(&sb->s_inode_list_lock); dispose_list(&dispose); --- base-commit: 502d801f0ab03e4f32f9a33d203154ce84887921 change-id: 20260713-feat-fs-evict-inodes-cursor-c23c9bd3c11f Best regards, -- Jing Wu