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

From: Kiryl Shutsemau (Meta)

Date: Wed Nov 04 2026 - 08:10:23 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.

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 65d414ad9d85..e148490db4da 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -2831,9 +2831,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;
/* A split clears PG_head before it puts the new folios in their slots */
if (!folio_test_large(folio) && folio->index != index)
@@ -3020,7 +3021,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 1c700fa9666d..8700b03a8a5e 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);
file->f_ra.prev_pos = last_pos;
return retval ? retval : error;
--
2.54.0