mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Heming Zhao <heming.zhao@suse.com>
To: Joseph Qi <joseph.qi@linux.alibaba.com>
Cc: Andrew Morton <akpm@linux-foundation.org>,
	 Mark Fasheh <mark@fasheh.com>, Joel Becker <jlbec@evilplan.org>,
	ocfs2-devel@lists.linux.dev,  linux-kernel@vger.kernel.org
Subject: Re: [PATCH] ocfs2: defer suballocator block group reclaim to workqueue
Date: Fri, 28 Aug 2026 13:47:43 +0800	[thread overview]
Message-ID: <apEeXAAo1wd5E5Or@p15> (raw)
In-Reply-To: <20260827124313.2692416-1-joseph.qi@linux.alibaba.com>

On Thu, Aug 27, 2026 at 08:43:13PM +0800, Joseph Qi wrote:
> When the last bit in a suballocator block group is freed,
> _ocfs2_free_suballoc_bits() reclaims the group back to the global
> bitmap.  The reclaim takes inode_lock() on the global bitmap inode
> while running inside the freeing transaction, adding a lock
> dependency of
> 
>   j_trans_barrier -> global bitmap inode i_rwsem
> 
> This forms a circular dependency with paths such as
> ocfs2_shutdown_local_alloc(), which take the global bitmap inode
> lock before starting a transaction:
> 
>   Task1 (dealloc):
>     ocfs2_run_deallocs
>       ocfs2_free_cached_blocks
>         ocfs2_start_trans
>           down_read(j_trans_barrier)
>         _ocfs2_free_suballoc_bits
>           _ocfs2_reclaim_suballoc_to_main
>             inode_lock(main_bm_inode)  <- wait on Task2
> 
>   Task2 (dismount):
>     ocfs2_shutdown_local_alloc
>       inode_lock(main_bm_inode)
>       ocfs2_start_trans
>         down_read(j_trans_barrier)  <- wait on Task3
> 
>   Task3 (ocfs2cmt):
>     ocfs2_commit_cache
>       down_write(j_trans_barrier)  <- wait on Task1's handle
>       jbd2_journal_flush
> 
> Task1 waits for Task2's inode_lock(), Task2 waits for the
> j_trans_barrier down_write() held by ocfs2cmt, and ocfs2cmt waits
> for Task1's running transaction to commit - a real deadlock,
> observed with aio-stress direct IO writes racing dismount.
> 
> Fix it by deferring the reclaim to the per-superblock ocfs2_wq
> workqueue, so the freeing transaction no longer takes the global
> bitmap inode lock.  The worker re-checks under the suballocator
> locks that the block group is still fully freed (it may have been
> allocated from again in the meantime), takes the global bitmap
> inode locks before starting its own transaction, and performs the
> same suballocator cleanup and space return.  The inode lock order
> (suballocator inode -> global bitmap inode) is consistent with the
> existing "inode lock before transaction" order, breaking the cycle.
> 
> Reclaim work can still be queued late in dismount, e.g. when orphan
> dir recovery frees inode bits, so both ocfs2_dismount_volume() and
> the mount error path flush ocfs2_wq right before the journal is
> shut down to make sure no reclaim work is left running.  The worker
> also bails out if the journal is already gone.
> 
> Tested with the ocfs2 testsuite (including aio-stress direct IO)
> and umount/mount cycles on a CONFIG_PROVE_LOCKING kernel: the
> circular locking dependency is gone and freed block groups are
> still returned to the global bitmap.
> 
> Fixes: 4a54331616b3 ("ocfs2: give ocfs2 the ability to reclaim suballocator free bg")
> Assisted-by: Qoder:Qwen3.8-Max
> Signed-off-by: Joseph Qi <joseph.qi@linux.alibaba.com>
> ---
>  fs/ocfs2/ocfs2.h    |   5 ++
>  fs/ocfs2/suballoc.c | 202 +++++++++++++++++++++++++++++++++++++-------
>  fs/ocfs2/suballoc.h |   2 +-
>  fs/ocfs2/super.c    |  12 +++
>  4 files changed, 191 insertions(+), 30 deletions(-)
> 
> diff --git a/fs/ocfs2/ocfs2.h b/fs/ocfs2/ocfs2.h
> index 62cad6522c7a..b747cdec1787 100644
> --- a/fs/ocfs2/ocfs2.h
> +++ b/fs/ocfs2/ocfs2.h
> @@ -502,6 +502,11 @@ struct ocfs2_super
>  	 */
>  	struct workqueue_struct *ocfs2_wq;
>  
> +	/* deferred reclaim of fully freed suballocator block groups */
> +	spinlock_t os_suballoc_reclaim_lock;
> +	struct list_head os_suballoc_reclaim_list;
> +	struct work_struct os_suballoc_reclaim_work;
> +
>  	/* sysfs directory per partition */
>  	struct kset *osb_dev_kset;
>  
> diff --git a/fs/ocfs2/suballoc.c b/fs/ocfs2/suballoc.c
> index 20c3aec6b987..453b56be9624 100644
> --- a/fs/ocfs2/suballoc.c
> +++ b/fs/ocfs2/suballoc.c
> @@ -2687,16 +2687,24 @@ static int ocfs2_block_group_clear_bits(handle_t *handle,
>   * cleanup rec/alloc_inode job, then switches to the main bitmap
>   * to reclaim released space.
>   *
> + * Callers must hold inode_lock() and ocfs2_inode_lock() on
> + * main_bm_inode, i.e. the global bitmap inode locks must be taken
> + * before starting the transaction.
> + *
>   * handle: The transaction handle
>   * alloc_inode: The suballoc inode
>   * alloc_bh: The buffer_head of suballoc inode
>   * group_bh: The group descriptor buffer_head of suballocator managed.
> - *           Caller should release the input group_bh.
> + *           This function takes ownership of it and will release it.
> + * main_bm_inode: The global bitmap inode
> + * main_bm_bh: The buffer_head of the global bitmap inode
>   */
>  static int _ocfs2_reclaim_suballoc_to_main(handle_t *handle,
>  			struct inode *alloc_inode,
>  			struct buffer_head *alloc_bh,
> -			struct buffer_head *group_bh)
> +			struct buffer_head *group_bh,
> +			struct inode *main_bm_inode,
> +			struct buffer_head *main_bm_bh)
>  {
>  	int idx, status = 0;
>  	int i, next_free_rec, len = 0;
> @@ -2706,8 +2714,6 @@ static int _ocfs2_reclaim_suballoc_to_main(handle_t *handle,
>  	u64 bg_blkno, start_blk;
>  	unsigned int count;
>  	struct ocfs2_chain_rec *rec;
> -	struct buffer_head *main_bm_bh = NULL;
> -	struct inode *main_bm_inode = NULL;
>  	struct ocfs2_super *osb = OCFS2_SB(alloc_inode->i_sb);
>  	struct ocfs2_dinode *fe = (struct ocfs2_dinode *) alloc_bh->b_data;
>  	struct ocfs2_chain_list *cl = &fe->id2.i_chain;
> @@ -2794,24 +2800,12 @@ static int _ocfs2_reclaim_suballoc_to_main(handle_t *handle,
>  	ocfs2_remove_from_cache(INODE_CACHE(alloc_inode), group_bh);
>  	memset(group, 0, sizeof(struct ocfs2_group_desc));
>  
> -	/* prepare job for reclaim clusters */
> -	main_bm_inode = ocfs2_get_system_file_inode(osb,
> -						    GLOBAL_BITMAP_SYSTEM_INODE,
> -						    OCFS2_INVALID_SLOT);
> -	if (!main_bm_inode)
> -		goto bail; /* ignore the error in reclaim path */
> -
> -	inode_lock(main_bm_inode);
> -
> -	status = ocfs2_inode_lock(main_bm_inode, &main_bm_bh, 1);
> -	if (status < 0)
> -		goto free_bm_inode; /* ignore the error in reclaim path */
> -
>  	ocfs2_block_to_cluster_group(main_bm_inode, start_blk, &bg_blkno,
>  				     &start_bit);
>  	fe = (struct ocfs2_dinode *) main_bm_bh->b_data;
>  	cl = &fe->id2.i_chain;
> -	/* reuse group_bh, caller will release the input group_bh */
> +	/* release the suballocator group descriptor before reuse */
> +	brelse(group_bh);
>  	group_bh = NULL;
>  
>  	/* reclaim clusters to global_bitmap */
> @@ -2819,7 +2813,7 @@ static int _ocfs2_reclaim_suballoc_to_main(handle_t *handle,
>  					     &group_bh);
>  	if (status < 0) {
>  		mlog_errno(status);
> -		goto free_bm_bh;
> +		goto bail;
>  	}
>  	group = (struct ocfs2_group_desc *) group_bh->b_data;
>  
> @@ -2827,7 +2821,7 @@ static int _ocfs2_reclaim_suballoc_to_main(handle_t *handle,
>  		ocfs2_error(alloc_inode->i_sb,
>  			"reclaim length (%d) beyands block group length (%d)",
>  			count + start_bit, le16_to_cpu(group->bg_bits));
> -		goto free_group_bh;
> +		goto bail;
>  	}
>  
>  	old_bg_contig_free_bits = group->bg_contig_free_bits;
> @@ -2837,7 +2831,7 @@ static int _ocfs2_reclaim_suballoc_to_main(handle_t *handle,
>  					      _ocfs2_clear_bit);
>  	if (status < 0) {
>  		mlog_errno(status);
> -		goto free_group_bh;
> +		goto bail;
>  	}
>  
>  	status = ocfs2_journal_access_di(handle, INODE_CACHE(main_bm_inode),
> @@ -2847,7 +2841,7 @@ static int _ocfs2_reclaim_suballoc_to_main(handle_t *handle,
>  		ocfs2_block_group_set_bits(handle, main_bm_inode, group, group_bh,
>  				start_bit, count,
>  				le16_to_cpu(old_bg_contig_free_bits), 1);
> -		goto free_group_bh;
> +		goto bail;
>  	}
>  
>  	idx = le16_to_cpu(group->bg_chain);
> @@ -2858,19 +2852,168 @@ static int _ocfs2_reclaim_suballoc_to_main(handle_t *handle,
>  	fe->id1.bitmap1.i_used = cpu_to_le32(tmp_used - count);
>  	ocfs2_journal_dirty(handle, main_bm_bh);
>  
> -free_group_bh:
> +bail:
>  	brelse(group_bh);
> +	return status;
> +}
> +
> +/*
> + * When a suballocator block group becomes fully freed, its space is
> + * reclaimed back to the global bitmap. Taking the global bitmap inode
> + * lock inside the freeing transaction would create a lock dependency
> + * of "j_trans_barrier -> global bitmap inode i_rwsem", which forms a
> + * circular dependency with paths like ocfs2_shutdown_local_alloc() that
> + * take the inode lock before starting a transaction, and can lead to a
> + * real deadlock with the ocfs2cmt journal commit thread. So queue the
> + * reclaim to the workqueue and let it run outside the freeing
> + * transaction.
> + */
> +struct ocfs2_suballoc_reclaim_work {
> +	struct list_head list;
> +	struct inode *alloc_inode;
> +	u64 bg_blkno;
> +};
> +
> +static void ocfs2_queue_suballoc_reclaim(struct ocfs2_super *osb,
> +					 struct inode *alloc_inode,
> +					 u64 bg_blkno)
> +{
> +	struct ocfs2_suballoc_reclaim_work *reclaim_work;
> +
> +	reclaim_work = kmalloc_obj(*reclaim_work, GFP_NOFS);
> +	if (!reclaim_work) {
> +		/*
> +		 * Reclaim is only a space return optimization. If we can't
> +		 * queue it, the freed block group just stays owned by the
> +		 * suballocator.
> +		 */
> +		return;
> +	}
> +
> +	igrab(alloc_inode);
> +	reclaim_work->alloc_inode = alloc_inode;
> +	reclaim_work->bg_blkno = bg_blkno;
> +
> +	spin_lock(&osb->os_suballoc_reclaim_lock);
> +	list_add_tail(&reclaim_work->list, &osb->os_suballoc_reclaim_list);
> +	spin_unlock(&osb->os_suballoc_reclaim_lock);
>  
> -free_bm_bh:
> +	queue_work(osb->ocfs2_wq, &osb->os_suballoc_reclaim_work);
> +}
> +
> +static void ocfs2_do_suballoc_reclaim(struct ocfs2_super *osb,
> +				struct ocfs2_suballoc_reclaim_work *reclaim_work)
> +{
> +	int status, i;
> +	handle_t *handle;
> +	struct inode *alloc_inode = reclaim_work->alloc_inode;
> +	struct inode *main_bm_inode;
> +	struct buffer_head *alloc_bh = NULL, *group_bh = NULL;
> +	struct buffer_head *main_bm_bh = NULL;
> +	struct ocfs2_dinode *fe;
> +	struct ocfs2_chain_list *cl;
> +	struct ocfs2_chain_rec *rec;
> +
> +	/* journal already gone, e.g. during dismount cleanup */
> +	if (!osb->journal)
> +		return;
> +
> +	inode_lock(alloc_inode);
> +	status = ocfs2_inode_lock(alloc_inode, &alloc_bh, 1);
> +	if (status < 0)
> +		goto out_alloc;
> +
> +	fe = (struct ocfs2_dinode *) alloc_bh->b_data;
> +	cl = &fe->id2.i_chain;
> +
> +	/*
> +	 * The block group may have been allocated from again since the
> +	 * reclaim work was queued, re-check that it is still fully freed.
> +	 * A stale work item can also reference a group that is no longer
> +	 * chained, whose descriptor would fail validation and trigger a
> +	 * spurious ocfs2_error(), so verify chain membership first.
> +	 */
> +	for (i = 0; i < le16_to_cpu(cl->cl_next_free_rec); i++) {
> +		rec = &cl->cl_recs[i];
> +		if (le64_to_cpu(rec->c_blkno) == reclaim_work->bg_blkno)
> +			break;
> +	}
> +	if (i == le16_to_cpu(cl->cl_next_free_rec) ||
> +	    ocfs2_is_cluster_bitmap(alloc_inode) ||
> +	    (le32_to_cpu(rec->c_free) != (le32_to_cpu(rec->c_total) - 1)) ||
> +	    (le16_to_cpu(cl->cl_next_free_rec) == 1))
> +		goto out_alloc_unlock;
> +
> +	status = ocfs2_read_group_descriptor(alloc_inode, fe,
> +					     reclaim_work->bg_blkno, &group_bh);
> +	if (status < 0)
> +		goto out_alloc_unlock;
> +
> +	main_bm_inode = ocfs2_get_system_file_inode(osb,
> +						    GLOBAL_BITMAP_SYSTEM_INODE,
> +						    OCFS2_INVALID_SLOT);
> +	if (!main_bm_inode)
> +		goto out_group;
> +
> +	inode_lock(main_bm_inode);
> +	status = ocfs2_inode_lock(main_bm_inode, &main_bm_bh, 1);
> +	if (status < 0)
> +		goto out_main;
> +
> +	handle = ocfs2_start_trans(osb, OCFS2_SUBALLOC_FREE);
> +	if (IS_ERR(handle)) {
> +		status = PTR_ERR(handle);
> +		mlog_errno(status);
> +		goto out_main_unlock;
> +	}
> +
> +	status = _ocfs2_reclaim_suballoc_to_main(handle, alloc_inode,
> +						 alloc_bh, group_bh,
> +						 main_bm_inode, main_bm_bh);
> +	/* group_bh ownership passed to _ocfs2_reclaim_suballoc_to_main() */
> +	group_bh = NULL;
> +	if (status < 0)
> +		mlog_errno(status);
> +
> +	ocfs2_commit_trans(osb, handle);
> +
> +out_main_unlock:
>  	ocfs2_inode_unlock(main_bm_inode, 1);
>  	brelse(main_bm_bh);
> -
> -free_bm_inode:
> +out_main:
>  	inode_unlock(main_bm_inode);
>  	iput(main_bm_inode);
> +out_group:
> +	brelse(group_bh);
> +out_alloc_unlock:
> +	ocfs2_inode_unlock(alloc_inode, 1);
> +	brelse(alloc_bh);
> +out_alloc:
> +	inode_unlock(alloc_inode);
> +}
>  
> -bail:
> -	return status;
> +void ocfs2_suballoc_reclaim_worker(struct work_struct *work)
> +{
> +	struct ocfs2_super *osb = container_of(work, struct ocfs2_super,
> +					       os_suballoc_reclaim_work);
> +	struct ocfs2_suballoc_reclaim_work *reclaim_work;
> +
> +	while (1) {
> +		spin_lock(&osb->os_suballoc_reclaim_lock);
> +		if (list_empty(&osb->os_suballoc_reclaim_list)) {
> +			spin_unlock(&osb->os_suballoc_reclaim_lock);
> +			break;
> +		}
> +		reclaim_work = list_first_entry(&osb->os_suballoc_reclaim_list,
> +					struct ocfs2_suballoc_reclaim_work,
> +					list);
> +		list_del(&reclaim_work->list);
> +		spin_unlock(&osb->os_suballoc_reclaim_lock);
> +
> +		ocfs2_do_suballoc_reclaim(osb, reclaim_work);
> +		iput(reclaim_work->alloc_inode);
> +		kfree(reclaim_work);
> +	}
>  }
>  
>  /*
> @@ -2955,7 +3098,8 @@ static int _ocfs2_free_suballoc_bits(handle_t *handle,
>  		goto bail;
>  	}
>  
> -	_ocfs2_reclaim_suballoc_to_main(handle, alloc_inode, alloc_bh, group_bh);
> +	ocfs2_queue_suballoc_reclaim(OCFS2_SB(alloc_inode->i_sb), alloc_inode,
> +				     bg_blkno);
>  
>  bail:
>  	brelse(group_bh);
> diff --git a/fs/ocfs2/suballoc.h b/fs/ocfs2/suballoc.h
> index bcf2ed4a8631..6042abc032f9 100644
> --- a/fs/ocfs2/suballoc.h
> +++ b/fs/ocfs2/suballoc.h
> @@ -206,7 +206,7 @@ int ocfs2_lock_allocators(struct inode *inode, struct ocfs2_extent_tree *et,
>  
>  int ocfs2_test_inode_bit(struct ocfs2_super *osb, u64 blkno, int *res);
>  
> -
> +void ocfs2_suballoc_reclaim_worker(struct work_struct *work);
>  
>  /*
>   * The following two interfaces are for ocfs2_create_inode_in_orphan().
> diff --git a/fs/ocfs2/super.c b/fs/ocfs2/super.c
> index c62e389d4dd6..c1aafbb9500a 100644
> --- a/fs/ocfs2/super.c
> +++ b/fs/ocfs2/super.c
> @@ -1779,6 +1779,9 @@ static int ocfs2_mount_volume(struct super_block *sb)
>  	if (osb->local_alloc_state == OCFS2_LA_ENABLED)
>  		ocfs2_shutdown_local_alloc(osb);
>  	ocfs2_release_system_inodes(osb);
> +	/* Drain pending suballoc reclaim work before the journal goes away */
> +	if (osb->ocfs2_wq)
> +		flush_workqueue(osb->ocfs2_wq);

The flush_workqueue() calls ocfs2_get_system_file_inode() to grab main_bm_inode.
However, after ocfs2_release_system_inodes(), all the cached system inodes are gone,
And the _ocfs2_get_system_file_inode() uses osb->sys_root_inode, which is already
freed by ocfs2_release_system_inodes(). So we should move the
flush_workqueue() calls to before ocfs2_release_system_inodes().

>  	/* before journal shutdown, we should release slot_info */
>  	ocfs2_free_slot_info(osb);
>  	ocfs2_journal_shutdown(osb);
> @@ -1850,6 +1853,10 @@ static void ocfs2_dismount_volume(struct super_block *sb, int mnt_err)
>  
>  	ocfs2_release_system_inodes(osb);
>  
> +	/* Drain pending suballoc reclaim work before the journal goes away */
> +	if (osb->ocfs2_wq)
> +		flush_workqueue(osb->ocfs2_wq);
> +

ditto

Thanks,
Heming
>  	ocfs2_journal_shutdown(osb);
>  
>  	/*
> @@ -2134,6 +2141,11 @@ static int ocfs2_initialize_super(struct super_block *sb,
>  	INIT_WORK(&osb->dquot_drop_work, ocfs2_drop_dquot_refs);
>  	init_llist_head(&osb->dquot_drop_list);
>  
> +	spin_lock_init(&osb->os_suballoc_reclaim_lock);
> +	INIT_LIST_HEAD(&osb->os_suballoc_reclaim_list);
> +	INIT_WORK(&osb->os_suballoc_reclaim_work,
> +		  ocfs2_suballoc_reclaim_worker);
> +
>  	/* get some pseudo constants for clustersize bits */
>  	osb->s_clustersize_bits =
>  		le32_to_cpu(di->id2.i_super.s_clustersize_bits);
> -- 
> 2.39.3
> 

  reply	other threads:[~2026-08-28  5:47 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-27 12:43 Joseph Qi
2026-08-28  5:47 ` Heming Zhao [this message]
2026-08-28  7:06   ` Joseph Qi

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=apEeXAAo1wd5E5Or@p15 \
    --to=heming.zhao@suse.com \
    --cc=akpm@linux-foundation.org \
    --cc=jlbec@evilplan.org \
    --cc=joseph.qi@linux.alibaba.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=mark@fasheh.com \
    --cc=ocfs2-devel@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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®