Per-filesystem-block dirty bits inside larger folios, mmap write faults, and page-cache writeback.
docs.kernel.org · filesystem documentationSmall writes.
Large writeback.
The same 64-byte file update can lead to 4 KiB or 2 MiB of data written to disk. Follow write() and shared mmap() through the page cache to see where the difference appears.
A model of the behavior discussed in the linked kernel sources. Actual results depend on filesystem, kernel, cache folio sizes, and workload.
A read can set up
the next write.
Device readahead helps shape the cache before a small write arrives. A wider read window can accommodate larger folios—and a larger later mmap writeback footprint.
Geometry influences readahead
An array can advertise a large optimal I/O size. The block layer can raise readahead to at least twice that size.
optimal_io_size → read_ahead_kb
Earlier reads fill the cache
576 × 4 KiB pages fit in this window.
For example, a sequential scan before the updates.
A larger folio can fit
A 2 MiB folio can fit in 2304 KiB. Some allocations can reach this size; others can be smaller.
Whole-folio dirtying
2 MiB mmap writeback
versus 4 KiB with write().
Ratio of file-data bytes, given the selected folio size.
The reported observation: software RAID across many NVMe drives automatically obtained read_ahead_kb = 2304, and some folios reached 2 MiB. This is consistent with the source mechanisms; the array’s exact RAID level and geometry were not provided.
Why read_ahead_kb is influential, but not an absolute folio-size cap
Window size is one constraint
In page_cache_ra_order(), allocation order is constrained by the readahead window and the mapping’s supported orders. Alignment, file boundaries, and allocation success can reduce actual sizes. With an ordinary 128 KiB window, 128 KiB fits; 2 MiB does not.
A large read request can extend the window toward bdi->io_pages, derived from max_sectors_kb. POSIX_FADV_SEQUENTIAL also doubles a file’s readahead limit. Therefore, 128 KiB in sysfs is not a universal ceiling. Changing it does not split folios already resident in the cache.
Inspect the filesystem-facing device
Check the md or dm device backing the filesystem, as well as its members. Example read-only checks; replace the path and md0 with the correct names:
findmnt -no SOURCE,FSTYPE --target /path/to/journal cat /sys/class/block/md0/queue/read_ahead_kb cat /sys/class/block/md0/queue/optimal_io_size cat /sys/class/block/md0/queue/max_sectors_kb
The two _kb values use KiB; optimal_io_size uses bytes. A default of 128 KiB is a baseline, not a guarantee for every disk.
Same file. Same bytes.
Two paths to disk.
A 64-byte overwrite at 400 MiB + 128 bytes in a 1 GiB file. It falls inside one 4 KiB block of a 2 MiB folio, after earlier reads populated the cache. Compare separate runs from the same clean cache state.
One update through the stack
Existing, clean, cached file · no concurrent writes
pwrite(fd, data, 64, offset)Known range: offset + 64 bytes
memcpy(map + offset, data, 64)Fault if write-protected; then CPU stores.
The cache starts clean.
The cache starts clean.
Each square = 4 KiB. This 2 MiB region contains one 512-block folio.
dirty folio + cache tag→inode: I_DIRTY_PAGES→wb->b_dirtyBoth paths have one dirty folio. The inode is the kernel’s record for this file.
wb_workfn()→wb_do_writeback()→wb_check_old_data_flush()The worker wakes and calls the checker. If a periodic pass is due, wb_writeback() selects eligible dirty inodes.
do_writepages()→iomap_writepages()→writeback_iter()1 of 512 folios selected. Clean folios are skipped. Each square above is a 2 MiB region of the file; the pass is limited by its range and budget.
Step forward to submit writeback.—
The diagram retains the dirty footprint after submission for comparison. Successful completion ends writeback; it does not by itself promise power-loss durability.
Timing, dirty state, and what this example leaves out
dirty_writeback_centisecs controls periodic wake frequency; dirty_expire_centisecs supplies the age cutoff for old-data selection. The first dirty inode may schedule delayed work via wb_wakeup_delayed(). Age is an inode scheduling heuristic, not a timer attached to each modified byte. Dirty-pressure writeback, reclaim-related work, and explicit sync requests may cause I/O sooner.
A folio dirty flag and iomap’s per-block dirty bitmap are different things. Both paths mark the folio dirty; their block maps differ. Buffered writes track the range copied, not a comparison of old and new byte values. Counts here are file-data writeback bytes, excluding filesystem metadata and journal traffic, and SSD-internal writes. This model uses ordinary buffered I/O and shared mappings, not DAX, direct I/O, or MAP_PRIVATE.
A little here.
A little everywhere.
Scatter identical updates across a 1 GiB file. Both APIs receive the same offsets and payloads. Change the pattern to see how many cache folios become involved.
Readahead preset: 2304 KiB. Model uses 2 MiB folios; actual allocations can be mixed.
The whole file
Each tile is a 2 MiB region. Select one to inspect.
Uniform sample of distinct blocks across the whole file.
Inside one region
0–2 MiB · one update
Each square is a 4 KiB block. The highlighted blocks form the writeback footprint after all selected updates.
512 blocks × 4 KiB = 2 MiB317 folios × 2 MiB = 634 MiBModel: initially clean cache, fixed-size folios, 4 KiB blocks, unique block locations, and one final writeback after updates coalesce. A 64-byte edit stays within its block. Ratios compare file-data bytes; they are not latency or device-wear predictions.
Fixed sample · 20261009Why random and maximally scattered are different
With 512 full-block updates, the app changes only 2 MiB. The initial random sample touches 317 large folios: 634 MiB of mmap writeback versus 2 MiB with write(). Confining the same amount of work to 16 MiB touches eight 2 MiB folios. Placing one update in every 2 MiB region touches all 512: 1 GiB versus 2 MiB. That last pattern is a deliberately spread example, not the expected outcome of every random workload.
These counters are computed from the selected offsets. Repeated modifications before a flush can coalesce. A later modification after a completed flush can cause the same folio to be written again.
A folio is not
a page-table entry.
The page cache groups file data into folios. Page tables map virtual addresses to that memory. A large cache folio can be reached through either small or huge mappings.
A PMD dirty bit says that something in its 2 MiB mapping was written. It does not identify the modified 4 KiB block.
Mapping size changes what the CPU can report.
A 2 MiB folio does not guarantee a PMD mapping. The fault path checks size, alignment, page-table state, and mapping policy. Smaller PTE mappings are used when a huge mapping cannot be installed.
In the whole-folio dirtying behavior modeled here, PTE-mapped large folios can also suffer the amplification. Selecting PTEs alone does not fix the filesystem’s dirty tracking. Buffered write() gets its precision from the supplied offset and copied length, recorded in iomap’s dirty-block bitmap. It does not depend on PTE dirty bits.
Where finer tracking could help
PTE dirty bits provide 4 KiB information in this example. A September 2026 RFC proposed harvesting them for sub-folio dirty tracking. PMD mappings still expose only a coarse dirty bit. This page illustrates the behavior motivating that work; it does not claim the RFC is merged or enabled on your kernel.
Where the large folio came from
The readahead profiles above show the setup stage. A wider window can accommodate larger folios before the writes begin. This mapping diagram specifically shows a 2 MiB folio; the 128 KiB profile uses smaller cache folios, which are PTE-mapped in this x86-64 example. Real files can contain mixed sizes.
What tells writeback which blocks to send?
Same 2 MiB cache folio, same 64-byte edit within one 4 KiB block. Whole-folio mmap dirtying is the behavior modeled here.
| Update path | Information available | Modeled file-data writeback |
|---|---|---|
| mmap · PMD mapping | One hardware dirty bit covers the entire 2 MiB mapping. | 2 MiB · whole folio |
| mmap · PTE mappings | Hardware bits identify 4 KiB pages, but the modeled filesystem dirtying path still marks the whole folio. | 2 MiB · whole folio |
| Buffered write() / pwrite() | The kernel knows the offset and copied length. iomap records the overlapping filesystem blocks. | 4 KiB · one dirty block |
CPU page tables still translate memory addresses during write(), but those mappings are not the mechanism used to track the file’s dirty ranges. “Precise” here means filesystem-block granularity, not byte-sized disk writes.
From the picture
to the kernel.
Documentation for the mechanisms, source for the call path, and the discussions connecting them to sparse journal-file updates.
The distinction matters for workloads such as systemd-journald: small, scattered file updates can dirty much larger cache units. This is a conditional explanation of that mechanism, not a claim about every journal installation.
Kernel discussion of small mmap writes, whole-folio writeback, and the contrast with range-aware buffered writes.
LKML · Pedro Falcato · 20 Aug 2026Explains the PTE/PMD distinction and proposes finer tracking for PTE-mapped file writes.
LKML · Kiryl Shutsemau · 3 Sep 2026 · proposalDefinitions of dirty_writeback_centisecs and dirty_expire_centisecs, plus background dirty thresholds.
docs.kernel.org · VM sysctl documentationRange dirtying in buffered writes, whole-folio dirtying, and extraction of dirty ranges during writeback.
Linux source · fs/iomap/buffered-io.cPeriodic checks, old-inode selection, and dispatch into filesystem writeback.
Linux source · fs/fs-writeback.cTagged cache lookup, writeback iteration, and dirty-state preparation with folio_mkclean().
Linux source · mm/page-writeback.cThe fault path and the conditions used when attempting a huge page-table mapping.
Linux source · mm/memory.cpage_cache_ra_order() constrains order by window size. ractl_max_pages() shows the large-request exception.
Linux source · mm/readahead.cblk_apply_bdi_limits() uses the maximum of the existing setting, twice optimal I/O size, and the baseline.
Linux source · block/blk-settings.cRAID0 computes optimal I/O size from chunk size and disk count. This illustrates array-driven scaling, not the unknown layout of the reported test.
Linux source · drivers/md/raid0.cVM_READAHEAD_PAGES defines the baseline. It does not imply every device retains this setting.
Linux source · include/linux/pagemap.hThe exported value converts the backing-device page count to units of 1024 bytes.
Linux source · block/blk-sysfs.cPOSIX_FADV_SEQUENTIAL doubles the per-file readahead limit relative to the backing-device setting.
Linux source · mm/fadvise.cFunction-by-function route through the walkthrough
| Step | Source entry points | What to look for |
|---|---|---|
| Earlier cache population | page_cache_ra_order() | Allocation order is shaped by the current readahead window and filesystem support. |
| Range-aware buffered write | __iomap_write_end() | Dirty state for the range actually copied, rounded to blocks. |
| Shared mmap write | iomap_page_mkwrite() iomap_dirty_folio() | Write-fault preparation and the full-folio dirtying hook. |
| Queue dirty inode | filemap_dirty_folio() __mark_inode_dirty() | Cache tag, inode dirty flag, queue, and dirty timestamp. |
| Wake and check | wb_workfn() wb_check_old_data_flush() | Worker calls the periodic checker via wb_do_writeback(). |
| Choose eligible inodes | wb_writeback() move_expired_inodes() | Age cutoff and movement from b_dirty to b_io via queue_io(). |
| Find dirty folios | writeback_iter() filemap_get_folios_tag() | Dirty-tagged folios within the requested range and budget. |
| Prepare mapped memory | folio_clear_dirty_for_io() | folio_mkclean() write-protects mappings and reconciles mapped dirty state. |
| Submit dirty blocks | iomap_writeback_folio() iomap_find_dirty_range() | The block bitmap determines the file-data ranges submitted for I/O. |
Source walkthrough based on the Code Browser snapshot identified as v6.19-rc8-185-g2687c848e578. Online source pages and line numbers may change. The linked September 2026 RFC is evidence of a proposal, not of deployment status.