[RFC PATCH 17/18] mm/shmem: read without folio references

From: Kiryl Shutsemau (Meta)

Date: Mon Oct 19 2026 - 13:03:04 EST


shmem_file_read_iter() takes a reference on every folio it reads, so
small reads of one tmpfs file from many CPUs bounce the folio's
refcount.

Copy with filemap_read_rcu() first, and continue with the existing loop
where it stops. Holes, swap entries and fallocated folios that are not
uptodate all end the copy. So does a hwpoisoned page, which shmem keeps
in the page cache and reports with -EIO.

This drops the folio lock as well as the reference. shmem_get_folio()
held it only for the lookup, and the copy ran after folio_unlock(), so a
read was never atomic against a write or a hole punch. Now a read also
does not wait for one that holds the lock, as filemap_read() does not.

shmem does no readahead, and on its folios the PG_readahead bit is
PG_reclaim, which drop_caches can leave set on dirty tmpfs folios. Do
not let it end the copy there.

Assisted-by: LLM
Signed-off-by: Kiryl Shutsemau (Meta) <kas@xxxxxxxxxx>
---
mm/filemap.c | 7 ++++---
mm/internal.h | 2 ++
mm/shmem.c | 11 +++++++++--
3 files changed, 15 insertions(+), 5 deletions(-)

diff --git a/mm/filemap.c b/mm/filemap.c
index 4b5b7ba78f39..cde7bc68b32d 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -2837,9 +2837,10 @@ static void filemap_end_dropbehind_read(struct folio *folio)
static bool filemap_read_rcu_can_copy(struct folio *folio, pgoff_t index,
pgoff_t last_index)
{
- if (!folio_test_uptodate(folio))
+ if (!folio_test_uptodate(folio) || folio_test_dropbehind(folio))
return false;
- if (folio_test_readahead(folio) || folio_test_dropbehind(folio))
+ /* shmem does no readahead: there the bit is PG_reclaim */
+ if (folio_test_readahead(folio) && !folio_test_swapbacked(folio))
return false;
/* memory_failure() waits only for readers that got past this */
if (folio_contain_hwpoisoned_page(folio))
@@ -3017,7 +3018,7 @@ static bool filemap_read_rcu_batch(struct kiocb *iocb, struct iov_iter *iter,
* Copy from the page cache without folio references, in batches, and
* return how much was copied. The slow path reads the rest.
*/
-static size_t filemap_read_rcu(struct kiocb *iocb, struct iov_iter *iter,
+size_t filemap_read_rcu(struct kiocb *iocb, struct iov_iter *iter,
loff_t last_pos)
{
/* The page the previous read ended in; ULONG_MAX if none */
diff --git a/mm/internal.h b/mm/internal.h
index 18391dc50418..ae9a4e511c82 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -1249,6 +1249,8 @@ struct migration_target_control {
size_t splice_folio_into_pipe(struct pipe_inode_info *pipe,
struct folio *folio, loff_t fpos, size_t size);
void filemap_read_accessed(struct folio *folio, loff_t pos, loff_t last_pos);
+size_t filemap_read_rcu(struct kiocb *iocb, struct iov_iter *iter,
+ loff_t last_pos);

static inline bool vma_is_single_threaded_private(struct vm_area_struct *vma)
{
diff --git a/mm/shmem.c b/mm/shmem.c
index 43ddb50974a5..e6c18e745f81 100644
--- a/mm/shmem.c
+++ b/mm/shmem.c
@@ -3313,7 +3313,14 @@ static ssize_t shmem_file_read_iter(struct kiocb *iocb, struct iov_iter *to)
pgoff_t index;
unsigned long offset;
int error = 0;
- ssize_t retval = 0;
+ ssize_t retval;
+
+ retval = filemap_read_rcu(iocb, to, last_pos);
+ if (retval) {
+ last_pos = iocb->ki_pos;
+ if (!iov_iter_count(to))
+ goto out;
+ }

for (;;) {
struct folio *folio = NULL;
@@ -3417,7 +3424,7 @@ static ssize_t shmem_file_read_iter(struct kiocb *iocb, struct iov_iter *to)
}
cond_resched();
}
-
+out:
file_accessed(file);
/* Readers sharing the file would bounce the line for nothing */
if (file->f_ra.prev_pos != last_pos)
--
2.54.0