diff options
| author | Linus Torvalds <torvalds@linux-foundation.org> | 2026-10-10 07:05:38 +0200 |
|---|---|---|
| committer | Linus Torvalds <torvalds@linux-foundation.org> | 2026-10-10 07:05:38 +0200 |
| commit | 4c0f3697118eb3a708ea79175f7f07c43339bfab (patch) | |
| tree | a678a9e3f2075cea30f10834d00461455ea096b0 | |
| parent | e69e58a909381aec305fc793625f52ec427f74e2 (diff) | |
| download | linux-noref-filemap-read-with-llm.tar.gz linux-noref-filemap-read-with-llm.zip | |
mm: read from the page cache without folio referencesnoref-filemap-read-with-llm
Problem
-------
Small reads of one file from many threads contend on the folio refcount:
filemap_get_read_batch() takes a reference (lock cmpxchg) and
filemap_read() drops it (lock decl), so every read writes the struct
page line twice. Matthew raised it in 2024 from a customer workload
[1]. On my laptop (i7-8565U, 4 cores / 8 threads), with 8 threads doing
16-byte pread() of one page, that line takes 99.92% of the load HITMs.
Idea
----
A folio removed from the page cache goes back to the page allocator
only after an RCU grace period.
Then a page that xas_load() finds in i_pages under rcu_read_lock() holds
that file's data until rcu_read_unlock(), and filemap_read() copies from
it straight to the user buffer with page faults disabled. No reference,
no on-stack bounce buffer, no seqcount, no check after the copy, and no
limit on the size of the read.
The earlier attempts ([2], and Kiryl's "fast short reads" [3]) copied
without a reference into a stack buffer and validated afterwards. They
stopped at what the validation cannot see: free, reuse and re-insert at
the same index [4], migration ("__folio_migrate_mapping() does not use
page_cache_delete()", Hugh [5]), and reading memory that is no longer
page cache at all: secretmem (Pedro), unaccepted memory (Kiryl), "or
even worse MMIO with undesired read-effects" (David [6]; all three in
the thread of [3]).
Deferring the free removes those cases instead of detecting them. It
has been suggested several times and not tried; Matthew's objection was
"it just feels too risky to have so much memory waiting to be reused"
[7], and that is the part of this series that is least solved (patches 7
and 8, and problem 1 below).
Design decisions
----------------
- The final free is deferred, not the removal and not the reference.
PG_rcu_free is set where a folio leaves i_pages (page_cache_delete(),
page_cache_delete_batch(), replace_page_cache_folio(),
__folio_migrate_mapping(), collapse_file(),
shmem_delete_from_page_cache()). __free_pages_prepare() tests it,
queues the page and returns false, so whoever drops the last
reference does not need to know.
So the guarantee is "the memory is not reused", not "the refcount
stays above zero". Reclaim, migration and split still freeze the
count under a reader, folio_try_get() stays conditional, and the
lockless lookup protocol is unchanged. Deferring the page cache's own
reference instead would be the stronger guarantee, but reclaim frees
from a frozen count without a put, so there is nothing to defer
there.
- Queueing: per CPU, a pending list and one batch whose grace period is
in flight (queue_rcu_work(), WQ_MEM_RECLAIM workqueue). A per-CPU
spinlock, irqsave, makes "cut pending into the batch" and "start the
grace period" one step; without it a page can join a batch whose
grace period has already started. A page waits one to two grace
periods. This is the shape of kfree_rcu() batching, without its
shrinker.
- A CPU that frees faster than grace periods end must not fill the
lists. Above 1024 pages on a CPU's pending list a second work item
takes the list, calls synchronize_rcu_expedited() and frees it
(patch 7); only that work waits. The loops that remove page cache
reschedule when the scheduler has asked for it, so that the work gets
the CPU before the next tick (patch 8).
- Not deferred: folios of an inode that has I_FREEING, so that rm of a
big file does not send all of it through the lists. The rule this
creates: a lockless reader that relies on the deferral must hold a
reference to mapping->host. An open file does. Also not covered:
hugetlb (frees to its pool) and the swap cache. filemap_read() reads
neither.
- Reuse without a free breaks the guarantee as well.
page_cache_pipe_buf_try_steal() takes a folio out of the page cache
and fuse puts it into another file. Patch 4 removes the steal; fuse
and virtio_console copy instead.
- Read path: per page, xas_load(), then uptodate, readahead mark,
dropbehind, i_size, copy. It never reads folio_size() or anything
else of the head that a split rewrites: a split leaves the bytes in
place, so the page at the looked-up index is right whatever the folio
looks like by then. Page faults are disabled for the whole RCU read
section, and the copy is plain copy_page_to_iter():
copy_page_to_iter_nofault() would run the hardened usercopy check,
which reads the compound order.
FOLIO_BATCH_SIZE pages per rcu_read_lock(). On any reason to stop
(user fault, folio missing or not uptodate, readahead mark, EOF,
fatal signal, flush_dcache_folio() needed) the existing path,
unchanged, continues the read from there.
folio_mark_accessed() needs a reference, so the fast path is taken
only where it would change nothing (a test of the flags, for both LRU
implementations). A hot folio gets there after a few reads through
the old path.
Numbers
-------
My laptop only, ext4, mitigations off, CONFIG_HARDENED_USERCOPY off.
The laptop throttles on temperature, so compare cost per read, not
reads per second. 16-byte pread() at offset 0 of a one-page file, one
fd per thread:
kernel insn kernel cycles reads/s
per read per read
base, 1 thread 973 452-454 7.9-8.1 M
base, 8 threads 988-989 2937-2975 9.9-10.4 M
base, 4 threads,
one per core 979-980 1318-1401 8.9-11.6 M
series, 1 thread 873 408-409 8.8-8.9 M
series, 8 threads 864 725-726 30-33 M
series, 4 threads,
one per core 873-876 417-438 25-26 M
With the series one thread per core costs 2-7% more than one thread
alone; the 8-thread figure is two SMT siblings sharing a core. Load
HITMs (perf c2c, 8 threads): 30538 -> 101.
All the cores of this laptop are on one die and share one L3. A
machine with more CPUs, or more than one socket, should gain more;
nothing bigger has been measured.
Kernel cycles per read, base -> series:
- 16 bytes, a file per thread, so nothing is shared: 8 threads
827 -> 730-732, 1 thread 449-456 -> 409-411. The base pays for SMT
too; the rest of its 2937-2975 comes from sharing the file.
- A read into a user page that is not present costs one extra failed
exception per read(); the old path then takes the real fault and
finishes the read. 16 bytes, 1 thread: 4117-4188 -> 6543-6593.
64 KiB with 16 faults: 57478-58073 -> 59648-59822.
- Large reads stay on the fast path. 64 KiB, 5% fewer instructions:
1 thread 7334-7346 -> 6917-6929; 8 threads, one file, 17948-18051
-> 10925-11146. 1 MiB, 6% more instructions: 1 thread
166544-171752 -> 161315-166941. 1 MiB at 8 threads is not
comparable in cycles (the clock differed between runs). In time
per read: with a file per thread the same, 637-721 us ->
616-660 us; with one file 257-280 us -> 215-230 us, two runs.
Memory waiting for a grace period. N processes each fallocate() and
hole-punch 64 MiB of a memfd in a loop; together they fill and free
about 22 GiB/s from N = 4 on. Mean of what waits, sampled every 2 ms,
in MiB:
N patches 1-6 patches 1-8
1 148 6.6
4 565 15
8 503 70
16 540 177
With patches 1-6 a page waits 20-25 ms and the peaks are 1.0 to
1.2 GiB from N = 4 on. With patches 7 and 8 the peaks are 59, 128,
249 and 870 MiB, the expedite work runs 4600 times a second at N = 4,
and the processes fill and free as fast as before. The same loop on
ext4 files (pwrite(), ftruncate(0), 7-9 GiB/s): 124 -> 12 MiB at N = 4.
Kiryl's reproducer (that loop in four processes next to one that
touches 1400 MiB of anonymous memory 20 times), in a 2 GiB VM without
swap: with patches 1-6 the OOM killer takes the large process with 4
and with 8 CPUs, and not with 1 or 2. With patches 1-8 it runs to the
end with 1, 2, 4 and 8 CPUs and never enters direct reclaim.
The series' HITM count and the figures below are from earlier builds
of the series, which copied with copy_page_to_iter_nofault().
- Slower frees: a loop of fadvise(DONTNEED) freeing 850 MiB/s had at
most 28 MiB waiting (sampled every 12 ms). Truncating a 256 MiB
file had 208 MiB waiting, gone within 25 ms. On this laptop from
boot until the test runs: 3323 pages deferred, 3.29 M not deferred
because their inode was being evicted.
Testing
-------
evict-race: 4 readers check every byte they read from a clean file
while one thread evicts it with fadvise(DONTNEED) in a loop and reuses
the freed pages at once, and two more recycle page cache and anonymous
memory. 0 bad reads in 30 s runs of about 10^8 reads each. With a
debug patch that frees PG_rcu_free pages immediately, the same test
gets bytes that are not the file's (12 and 138 bad reads in two runs),
some of them from other users of the freed page, so it does detect a
broken guarantee. That control was run on an earlier build of the
series; the debug patch has not been redone for this one.
partial-fault: reads of a cached file into buffers with some pages
zapped by MADV_DONTNEED, so that copies fault partway through a page,
with every byte checked. 0 bad in 1.15 M reads.
punch-wait and the VM: the hole-punch loop of "Numbers" with a sampler
of the vmstat counters, and Kiryl's reproducer as the only program of
an initramfs under qemu/KVM.
Those, a gcc build of one x86-64 config, and running the laptop on it,
are all the testing there is. That config has CONFIG_DEBUG_VM, lazy
preemption and preemptible RCU, and no lockdep, no
CONFIG_DEBUG_ATOMIC_SLEEP and no KASAN. The last commit builds
without warnings. An earlier form of the first six was compiled at
each commit, and with clang at the last; patch 7 has not been built
without patch 8.
Known problems, and what I'd like eyes on
-----------------------------------------
1. The allocator cannot see the waiting pages. They have refcount 0,
are not PageBuddy, are on no pcp list and in no counter.
- It ran a machine out of memory (Kiryl's reproducer, under
"Numbers"). should_reclaim_retry() computes "available" from
the LRU counters and NR_FREE_PAGES, the waiting pages are in
neither, and it decides for the OOM killer while hundreds of MiB
wait. Patches 7 and 8 keep the lists short; they do not make
the allocator see what is on them. The limit of 1024 pages is a
guess. With no idle CPU (N = 16 above) 177 MiB still waits on
average and 870 MiB at the peak. kswapd and direct reclaim
overshoot for the same reason, and compaction's source pages
reach the free lists late.
- alloc_contig_range() fails. The sources of its migration are
still waiting when test_pages_isolated() runs, so it returns
-EBUSY for a range that had page cache in it, and cma_alloc()
goes on to the next range. That is a regression for CMA as the
series stands. hugetlb's deferred free has the same shape, and
test_pages_isolated() calls wait_for_freed_hugetlb_folios() for
it.
- Nothing bounds what waits when a grace period is slow.
I would rather expedite than wait in the allocator: a
synchronize_rcu() there throttles page-out. alloc_contig_range()
is different, it needs those very pages: a
get_state_synchronize_rcu() cookie per batch, and
cond_synchronize_rcu_expedited() plus the free from
test_pages_isolated(). Not written. A zone counter for what
waits is not written either.
2. One more page flag. With MAXSMP, NUMA balancing and the page idle
flag it takes one of MGLRU's two refs bits (arithmetic from
page-flags-layout.h, not built). Is there a bit that is dead once
a folio has left the page cache?
3. splice. SPLICE_F_MOVE of page cache pages into fuse is gone; does
anyone depend on it? And one more way in is closed only by
accident: vmsplice(SPLICE_F_GIFT) of a mapped page cache page, then
truncate, leaves the pipe with the only reference,
generic_pipe_buf_try_steal() takes the page, and fuse_check_folio()
refuses it only because PG_rcu_free is not in its list of allowed
flags (with a dump_page()). That needs an explicit test.
4. s390: __make_folio_secure() freezes the refcount. A reference
blocks it, an RCU reader does not. What a reader copies from a
page that goes secure under it is not worked out.
5. Cost outside the read path is not measured. Every deferred free
takes an irqsave spinlock, and the free itself moves to a kworker
on whichever CPU ran the RCU callback: cold pages, CPU time not
charged to the task that freed, and bad-page reports and
page_owner free stacks that name the kworker. Reclaim-heavy and
truncate-heavy loads need numbers.
6. Arguments that are only arguments. The ordering from the i_pages
store to the start of the grace period (store; the remover's
reference drop; the final dec-and-test; the per-CPU lock; the cut;
queue_rcu_work()) is argued from the documented guarantees, with no
litmus test. Not every ->migrate_folio() was read for "the
destination is not uptodate before its data is there".
7. Not tested: xfstests (generic/095 and generic/741 failed with the
2024 patch [8]), anything that splits folios under the fast path,
memory pressure other than Kiryl's reproducer in a VM, lockdep,
KASAN, a kernel linked by clang, 32-bit, CONFIG_HARDENED_USERCOPY,
any machine but this one. And any preemption model but lazy with
preemptible RCU: without preemptible RCU a task that punches holes
in a loop reports no quiescent state until RCU asks for one, about
HZ/10 into the grace period, so more would wait (from the code,
not run).
8. trace_mm_filemap_get_pages and folio_mark_accessed() are not
reached by reads the fast path serves, so tools that count either
see fewer page cache hits.
9. For RCU. The deferral keeps its own per-CPU pending list and one
batch per grace period, started with queue_rcu_work(). That is
the shape of kfree_rcu(), and RCU's segmented callback lists
already do the same thing in general. call_rcu_hurry() per folio
would need no lock and no workqueue, and would lose the batched
free_unref_folios(); not tried. Is there a better way to hook
in?
Patch 7 calls synchronize_rcu_expedited() from a work item every
time a CPU has 1024 pages waiting, 4600 times a second with four
hole-punching processes. Its kernel-doc says not to use it in a
loop. What the IPIs cost a task that frees nothing is not
measured. Is there a way to get a short grace period without
them?
10. For the scheduler. With lazy preemption the kworker that runs the
expedite work waits for the tick while the task that queued it
keeps the CPU: 0.84 ms to start, against 20 us once that task
makes way. Patch 8 calls schedule() in the page cache removal
loops when TIF_NEED_RESCHED_LAZY is set, which is the
cond_resched() that lazy preemption took out, and it acts on any
lazy request. An unbound workqueue does as well while a CPU is
idle and nothing when none is. I am not sure this is the
direction; I would rather have the freeing task do something
synchronous that leaves the scheduler alone.
Tools
-----
The code, the test programs, the review behind the list above and the
first draft of this text were done with an LLM coding assistant (Claude
Code) under my direction. The measurements were run on my machine.
Patches
-------
1 mm: free PG_rcu_free folios only after an RCU grace period
2 mm: free folios removed from the page cache only after an RCU
grace period
3 mm: don't defer the free of page cache folios of an inode being
evicted
4 splice: don't steal page cache folios
5 mm/filemap: read from the page cache without folio references
6 mm: count pages whose free waits for an RCU grace period
7 mm: expedite the grace period when a CPU has many deferred frees
8 mm: make way for the expedite work in the page cache removal
loops
Base: 7.3-rc6 plus fixes, 0c2669a9f4a1.
[1] https://lore.kernel.org/all/Zduto30LUEqIHg4h@casper.infradead.org/
[2] https://lore.kernel.org/all/CAHk-=wjOogaW0yLoUqQ0WfQ=etPA4cOFLy56VYCnHVU_DOMLrg@mail.gmail.com/
[3] https://lore.kernel.org/all/20251017141536.577466-1-kirill@shutemov.name/
[4] https://lore.kernel.org/all/Zds8T9O4AYAmdS9d@casper.infradead.org/
[5] https://lore.kernel.org/all/b8e56515-3903-068c-e4bd-fc0ca5c30d94@google.com/
[6] https://lore.kernel.org/all/dcdfb58c-5ba7-4015-9446-09d98449f022@redhat.com/
[7] https://lore.kernel.org/all/Zdz9p_Kn0puI1KEL@casper.infradead.org/
[8] https://lore.kernel.org/all/ZkNQiWpTOZDMp3kS@bombadil.infradead.org/
0 files changed, 0 insertions, 0 deletions
