Re: [RFC PATCH] btrfs: trigger cow fixup via dirty_folio()
Boris Burkov <[email protected]> Sun, 26 Jul 2026 20:57:02 -0700
| Newsgroups | org.kernel.vger.linux-btrfs |
|---|---|
| Message-ID | <[email protected]> |
On Sat, Jul 25, 2026 at 07:34:46PM +0930, Qu Wenruo wrote: > > > 在 2026/7/25 15:54, Boris Burkov 写道: > [...]> > > Link: https://lore.kernel.org/linux-btrfs/[email protected]/ > > Signed-off-by: Boris Burkov <[email protected]> > > Assisted-by: LLM > > Without LLM it's very hard to come up with such huge change in such a short > time, especially considering this fix is a little urgent due to both large > folio and removal of cow fixup are in the same release. > > So I'm totally fine with the involvement of LLM, just we will spend quite > some time reworking/removing a lot of comments/commit message. > > Not because they are not not grammarly sound (unlike my replies), but a lot > of time they are too lengthy but brings not too much value compared to the > number of lines. > > But still, very impressed to see a working RFC this soon. > > > --- > > fs/btrfs/btrfs_inode.h | 1 + > > fs/btrfs/defrag.c | 1 + > > fs/btrfs/disk-io.c | 6 +- > > fs/btrfs/extent_io.c | 127 +++++++++++++++++ > > fs/btrfs/file.c | 2 + > > fs/btrfs/fs.h | 20 +++ > > fs/btrfs/inode.c | 262 ++++++++++++++++++++++++++++++++++- > > fs/btrfs/reflink.c | 1 + > > fs/btrfs/relocation.c | 1 + > > fs/btrfs/subpage.c | 93 +++++++++++++ > > fs/btrfs/subpage.h | 43 +++++- > > include/trace/events/btrfs.h | 35 +++++ > > 12 files changed, 585 insertions(+), 7 deletions(-) > > > > diff --git a/fs/btrfs/btrfs_inode.h b/fs/btrfs/btrfs_inode.h > > index 7fdc6c3fd066..037445586b31 100644 > > --- a/fs/btrfs/btrfs_inode.h > > +++ b/fs/btrfs/btrfs_inode.h > > @@ -600,6 +600,7 @@ int btrfs_prealloc_file_range_trans(struct inode *inode, > > loff_t actual_len, u64 *alloc_hint); > > int btrfs_run_delalloc_range(struct btrfs_inode *inode, struct folio *locked_folio, > > u64 start, u64 end, struct writeback_control *wbc); > > +int btrfs_queue_writepage_fixup(struct btrfs_inode *inode, struct folio *folio); > > int btrfs_encoded_io_compression_from_extent(struct btrfs_fs_info *fs_info, > > int compress_type); > > int btrfs_encoded_read_regular_fill_pages(struct btrfs_inode *inode, > > diff --git a/fs/btrfs/defrag.c b/fs/btrfs/defrag.c > > index 6ec5dd760d42..7de5febee9d1 100644 > > --- a/fs/btrfs/defrag.c > > +++ b/fs/btrfs/defrag.c > > @@ -1158,6 +1158,7 @@ static void defrag_one_locked_target(struct btrfs_inode *inode, > > start + len <= folio_pos(folio)) > > continue; > > btrfs_folio_clamp_set_dirty(fs_info, folio, start, len); > > + btrfs_folio_cancel_fixup(fs_info, folio, start, len); > > This call is very frequent, and to be honest, it's redundant. > > We can merge the cancel into btrfs_folio*_set_dirty(), as the > btrfs_folio_*set_dirty() is the "proper dirty" entrance, so we know if we're > going through that interface, we are ensured to cancel the fixup. > > > + > > + if (!folio_test_fixup_pending(folio)) > > if (likely()) > > > + return 0; > > + > > + for_each_set_bit(bit, bio_ctrl->submit_bitmap, blocks_per_folio) { > > + const u64 start = page_start + (bit << fs_info->sectorsize_bits); > > + > > + if (btrfs_test_range_bit_exists(&inode->io_tree, start, > > + start + sectorsize - 1, > > + EXTENT_DELALLOC)) > > Can we simplify the check to just check if the fixup bit is not set? > > To me, without fixup bit it means it went through the regular write > preparation, thus should have EXTENT_DELALLOC. > And checking a bit is way faster than check an extent range. > > > + continue; > > + if (btrfs_is_subpage(fs_info, folio) && > > + !btrfs_subpage_test_fixup(fs_info, folio, start, sectorsize)) { > > Again, we prefer not to use btrfs_is_subpage() and btrfs_subpage_*() out of > subpage.c when possible. > > So just some helper, like btrfs_folio_test_fixup() would make it more > readable. > > > + struct btrfs_ordered_extent *ordered; > > + > > + /* > > + * Not a suspect: the only legal explanation for a > > + * dirty, unfunded block is that it is pending its own > > + * submission into a covering ordered extent - punting > > + * it would starve that ordered extent forever, and > > + * the worker could not see it anyway (its work list > > + * is the fixup bits). Verify the ordered extent > > + * exists; if not, the invariant is broken - leave the > > + * block in the bitmap so submission fails loudly > > + * (EUCLEAN) instead of silently repairing. > > + */ > > The block can be dirtied properly before, then ->dirty_folio() callback is > triggered. > > In that case, we may have not run delalloc for the folio, thus there may be > no OE created yet. > > It looks like the assumption is not correct to me, and may cause false > alerts. > > But on the other hand, it's already very hard to hit a case where we have > some proper dirty blocks, and ->dirty_folio() is called. > E.g. for the direct IO case, ->dirty_folio() is only called when the folio > is clean. > > Thus this false alerts case is not easy to test. > > > + ordered = btrfs_lookup_ordered_range(inode, start, > > + sectorsize); > > Doing this is a little overkilled. > > As the recent patch "btrfs: use ordered extent to grab the logical address > for submission" will do it at submission time, with better cached OE usage. > > I'd prefer skip the OE lookup completely, and let the final submission path > to grab the OE (which can reuse bbio->ordered when possible). > > + if (unlikely(!ordered)) > > + DEBUG_WARN(); > > + else > > + btrfs_put_ordered_extent(ordered); > > + continue; > > + } > > + /* > > + * Block re-dirtied via dirty_folio(), requires async fixup. > > + * All other blocks in the folio should proceed as normal so > > + * that their OEs can make progress and complete. > > + */ > > + bitmap_clear(bio_ctrl->submit_bitmap, bit, 1); > > + punt = true; > > The variable naming choice is definitely very LLM. > > Non-native speaker like me may need to google the word first... > > > + } > > + if (punt) { > > + btrfs_queue_writepage_fixup(inode, folio); > > + /* > > + * Keep the folio dirty for the punted blocks. If nothing > > + * is left to submit this pass, the folio is done and we own > > + * the unlock, matching the async-submission contract. > > + */ > > + folio_redirty_for_writepage(bio_ctrl->wbc, folio); > > We haven't yet cleared any bit nor folio dirty in the first place. > So not sure if this is really needed, nor the comment. > This was just inherited from the original fixup which had similar conditions of dirtiness. I think it is probably vestigial. I am leaving it in v2 for now since that is what I have tested the most, but I think it is likely very possible to remove. > > + if (bitmap_empty(bio_ctrl->submit_bitmap, blocks_per_folio)) { > > + folio_unlock(folio); > > + return 1; > > + } > > + return 0; > > + } > > + /* Every dirty block is now covered; the folio is ordinary again. */ > > + if (btrfs_is_subpage(fs_info, folio)) > > + btrfs_subpage_clear_fixup(fs_info, folio, page_start, > > + folio_size(folio)); > > Again, a btrfs_folio_clear_fixup() is more helpful. > > [...] > > +/* > > + * The folio has (or, for single-block folios, is) dirty blocks that were > > + * dirtied without notifying the filesystem (e.g. set_page_dirty_lock() on a > > + * GUP pin) and carry no reservation. Set by btrfs_data_dirty_folio(), which > > + * records the affected blocks in the subpage fixup bitmap; cleared when the > > + * writepage fixup has covered every dirty block, or by a reserving write > > + * that covers the whole folio. Reuses the PG_owner_2 bit that the folio > > + * ordered flag occupied before it was removed. > > Again, very LLM comments. That's why always delete all comments from LLM by > default. > > The older, removed comment is very simple for folio_ordered: > > * We use folio flag owner_2 to indicate there is an ordered extent with > * unfinished IO. > > > [...] > > +static void btrfs_writepage_fixup_worker(struct btrfs_work *work) > > +{ > > + struct btrfs_writepage_fixup *fixup = > > + container_of(work, struct btrfs_writepage_fixup, work); > > + struct extent_state *cached_state = NULL; > > + struct extent_changeset *data_reserved = NULL; > > + unsigned long funded[BITS_TO_LONGS(BTRFS_MAX_BLOCKS_PER_FOLIO)] = { 0 }; > > + struct folio *folio = fixup->folio; > > + struct btrfs_inode *inode = fixup->inode; > > + struct btrfs_fs_info *fs_info = inode->root->fs_info; > > + const unsigned int blocks_per_folio = btrfs_blocks_per_folio(fs_info, folio); > > + const u32 sectorsize = fs_info->sectorsize; > > + const u64 page_start = folio_pos(folio); > > + const u64 page_end = folio_next_pos(folio) - 1; > > + unsigned int start_bit; > > + unsigned int end_bit; > > + unsigned int bit; > > + bool reserved; > > + int ret; > > + > > + /* > > + * Reserve for the whole folio before taking the folio lock, like > > + * page_mkwrite: the reservation may flush, and flushing writeback > > + * takes folio locks. Which blocks actually need funding is only > > + * knowable under the lock, so reserve the worst case and release > > + * the unused ranges once the funded set is known. > > + */ > > + ret = btrfs_delalloc_reserve_space(inode, &data_reserved, page_start, > > + folio_size(folio)); > > You fall into the same pitfall I'm worried about, but unfortunately or > fortunately, it's very hard to hit. > > The problem here is, we can over-reserve, e.g. some range is already > properly dirtied, thus they have already space reserved. > > And you can not release the reserved space for them, because release will > also clear the qgroup reserved extent flag. > > This is the biggest problem that I have no way to fix. > As there is no way to ensure the space we reserved is really needed before > holding the folio lock. > > > But since it's really hard to get a dirty folio to be called with > ->dirty_folio(), thus it may not be exposed through tests. > > > + reserved = (ret == 0); > > +again: > > + folio_lock(folio); > > + > > + /* > > + * The queueing writeback pass took references on the folio and the > > + * inode, but nothing else is stable: an in-band write may have funded > > + * the blocks and cancelled the fixup state, or the folio may have > > + * been invalidated. Then there is nothing left to do - and a failed > > + * reservation against a folio that no longer needs one is no error. > > + */ > > + if (!folio->mapping || !folio_test_dirty(folio) || > > + !folio_test_fixup_pending(folio)) { > > + ret = 0; > > + goto out; > > + } > > + > > + /* The folio still needs funding we could not get. */ > > + if (ret) > > + goto out_error; > > + > > + btrfs_lock_extent(&inode->io_tree, page_start, page_end, &cached_state); > > + > > + for (bit = 0; bit < blocks_per_folio; bit++) { > > + struct btrfs_ordered_extent *ordered; > > + const u64 start = page_start + (bit << fs_info->sectorsize_bits); > > + > > + if (test_bit(bit, funded)) > > + continue; > > + if (btrfs_is_subpage(fs_info, folio) && > > + !btrfs_subpage_test_fixup(fs_info, folio, start, sectorsize)) > > + continue; > > + /* An in-band write may have funded the block while we waited. */ > > + if (btrfs_test_range_bit_exists(&inode->io_tree, start, > > + start + sectorsize - 1, > > + EXTENT_DELALLOC)) > > + continue; > > + ordered = btrfs_lookup_ordered_range(inode, start, sectorsize); > > + if (ordered) { > > + trace_btrfs_writepage_fixup_defer(inode, ordered); > > + btrfs_unlock_extent(&inode->io_tree, page_start, > > + page_end, &cached_state); > > + folio_unlock(folio); > > + btrfs_start_ordered_extent(ordered); > > + btrfs_put_ordered_extent(ordered); > > + goto again; > > + } > > + ret = btrfs_set_extent_delalloc(inode, start, > > + start + sectorsize - 1, 0, > > + &cached_state); > > + if (ret) { > > + btrfs_unlock_extent(&inode->io_tree, page_start, > > + page_end, &cached_state); > > + goto out_error; > > + } > > + trace_btrfs_writepage_fixup_reserve(inode, start, sectorsize); > > + if (btrfs_is_subpage(fs_info, folio)) > > + btrfs_subpage_clear_fixup(fs_info, folio, start, > > + sectorsize); > > + __set_bit(bit, funded); > > Why __set_bit() not set_bit()? > I think just being cute cause it's a local variable. I'll use set_bit to not make it stick out as if it's an important choice. > > + } > > + > > + /* Every fixup block is funded; the folio is ordinary again. */ > > + folio_clear_fixup_pending(folio); > > + btrfs_unlock_extent(&inode->io_tree, page_start, page_end, &cached_state); > > + goto out; > > + > > +out_error: > > + /* > > + * The remaining unnotified blocks cannot be funded. There is no > > + * syscall to return this through, so surface it the way a failed > > + * writeback would - record the error on the mapping - and drop the > > + * blocks, or writeback would find and requeue them forever. > > + */ > > + mapping_set_error(folio->mapping, ret); > > + if (btrfs_is_subpage(fs_info, folio)) { > > + bool last = false; > > + > > + for (bit = 0; bit < blocks_per_folio; bit++) { > > Again, a proper btrfs_folio_clear_fixup() and pass the folio range into it > should handle the loop. > > > + const u64 start = page_start + > > + (bit << fs_info->sectorsize_bits); > > + > > + if (!btrfs_subpage_test_fixup(fs_info, folio, start, > > + sectorsize)) > > + continue; > > + last = btrfs_subpage_clear_and_test_dirty(fs_info, > > + folio, start, > > + sectorsize); > > + } > > + btrfs_folio_cancel_fixup(fs_info, folio, page_start, > > + folio_size(folio)); > > + if (last) > > + folio_clear_dirty_for_io(folio); > > Again, the regular btrfs_folio_clear_dirty() is going to handle all these, > and no need for a special subpage branch. > > > + } else { > > + folio_clear_dirty_for_io(folio); > > + } > > + folio_clear_fixup_pending(folio); > > +out: > > + if (reserved) { > > + btrfs_delalloc_release_extents(inode, folio_size(folio)); > > + for_each_clear_bitrange(start_bit, end_bit, funded, > > + blocks_per_folio) > > + btrfs_delalloc_release_space(inode, data_reserved, > > + page_start + (start_bit << fs_info->sectorsize_bits), > > + (end_bit - start_bit) << fs_info->sectorsize_bits, > > + true); > > + } > > + folio_unlock(folio); > > + folio_put(folio); > > + kfree(fixup); > > + extent_changeset_free(data_reserved); > > + /* > > + * As a precaution, do a delayed iput in case it would be the last > > + * iput that could need flushing space. Recursing back to the fixup > > + * worker would deadlock. > > + */ > > + btrfs_add_delayed_iput(inode); > > +} > [...] > > +static bool btrfs_data_dirty_folio(struct address_space *mapping, > > + struct folio *folio) > > +{ > > + struct btrfs_inode *inode = BTRFS_I(mapping->host); > > + struct btrfs_fs_info *fs_info = inode->root->fs_info; > > + bool marked = false; > > + > > + if (btrfs_is_subpage(fs_info, folio)) { > > + const struct btrfs_folio_state *bfs = folio_get_private(folio); > > + const u64 page_start = folio_pos(folio); > > + const u64 range_end = min_t(u64, page_start + folio_size(folio), > > + round_up(i_size_read(&inode->vfs_inode), > > + fs_info->sectorsize)); > > + > > + if (!READ_ONCE(bfs->reserving_dirty) && > > + folio_test_uptodate(folio) && range_end > page_start) > > + marked = btrfs_subpage_set_fixup_dirty(fs_info, folio, > > + page_start, > > + range_end - page_start); > > + } else if (!folio_test_dirty(folio) && > > + !btrfs_test_range_bit_exists(&inode->io_tree, > > + folio_pos(folio), > > + folio_next_pos(folio) - 1, > > + EXTENT_DELALLOC)) { > > Again, the same question why the folio_test_dirty() check is not enough. > > The subpage part has no such check. > > > + marked = true; > > + } > > + if (marked) > > + folio_set_fixup_pending(folio); > > + return filemap_dirty_folio(mapping, folio); > > +} > > + > [..] > > --- a/fs/btrfs/subpage.c > > +++ b/fs/btrfs/subpage.c > > @@ -356,7 +356,17 @@ void btrfs_subpage_set_dirty(const struct btrfs_fs_info *fs_info, > > spin_lock_irqsave(&bfs->lock, flags); > > bitmap_set(bfs->bitmaps, start_bit, len >> fs_info->sectorsize_bits); > > spin_unlock_irqrestore(&bfs->lock, flags); > > + /* > > + * Every caller of this helper is a reserving write path dirtying > > + * exactly the range it reserved; the dirty_folio callback must not > > + * treat it as potentially unnotified and drag the folio's clean > > + * sibling blocks into COW (that breaks reflink sharing of the > > + * untouched blocks, among other things). Unnotified dirtiers call > > + * folio_mark_dirty() directly and never pass through here. > > + */ > > + WRITE_ONCE(bfs->reserving_dirty, true); > > folio_mark_dirty(folio); > > My solution in my initial patchset is to not call folio_mark_dirty() as it > will call back to the ->dirty_folio() call back. > > So I implemented an local version which call back to filemap_mark_dirty(), > other than ->dirty_folio() which sets the fixup bits. > > > + WRITE_ONCE(bfs->reserving_dirty, false); > > } > > static void folio_clear_tags(struct folio *folio) > > @@ -457,6 +467,88 @@ void btrfs_subpage_clear_writeback(const struct btrfs_fs_info *fs_info, > > spin_unlock_irqrestore(&bfs->lock, flags); > > } > > +void btrfs_subpage_clear_fixup(const struct btrfs_fs_info *fs_info, > > + struct folio *folio, u64 start, u32 len) > > +{ > > + struct btrfs_folio_state *bfs = folio_get_private(folio); > > + unsigned int start_bit = subpage_calc_start_bit(fs_info, folio, > > + fixup, start, len); > > + unsigned long flags; > > + > > + spin_lock_irqsave(&bfs->lock, flags); > > + bitmap_clear(bfs->bitmaps, start_bit, len >> fs_info->sectorsize_bits); > > + spin_unlock_irqrestore(&bfs->lock, flags); > > Shouldn't we also check if we have cleared the last fixup bit and clear the > folio fixup flag? > [...] > > +void btrfs_folio_cancel_fixup(const struct btrfs_fs_info *fs_info, > > + struct folio *folio, u64 start, u32 len) > > Why not let IMPLEMNT_BTRFS_PAGE_OPS() to implement all the fixup helpers? > I think the level 0 answer is it doesn't apply to metadata and the llm also went nuts messing with the macros when it tried to use them. > [...] > > @@ -50,6 +65,14 @@ enum { > > struct btrfs_folio_state { > > /* Common members for both data and metadata pages */ > > spinlock_t lock; > > + /* > > + * Set across the folio_mark_dirty() that a reserving write path > > + * issues from btrfs_subpage_set_dirty(), so that the dirty_folio > > + * callback does not treat the dirtying as potentially unnotified > > + * and mark the folio's still-clean sibling blocks. Written under > > + * the folio lock, which every reserving path holds. > > + */ > > + bool reserving_dirty; > > As I mentioned, this is only for dirty_folio() to skip setting fixup bit. > > We can get rid of it, if the btrfs_folio_set_dirty() helper doesn't go > through folio_mark_dirty(). > > Thanks, > Qu