]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
mm: limit filemap_fault readahead to VMA boundaries
authorFrederick Mayle <fmayle@google.com>
Mon, 27 Apr 2026 03:01:47 +0000 (20:01 -0700)
committerAndrew Morton <akpm@linux-foundation.org>
Fri, 29 May 2026 04:04:57 +0000 (21:04 -0700)
When a file mapping covers a strict subset of a file, an access to the
mapping can trigger readahead of file pages outside the mapped region.
Readahead is meant to prefetch pages likely to be accessed soon, but these
pages aren't accessible via the same means, so it fair to say we don't
have a good indicator they'll be accessed soon.  Take an ELF file for
example: an access to the end of a program's read-only segment isn't a
sign that nearby file contents will be accessed next (they are likely to
be mapped discontiguously, or not at all).  The pressure from loading
these pages into the cache can evict more useful pages.

To improve the behavior, make three changes:

* Introduce a new readahead_control field, max_index, as a hard limit on
  the readahead. The existing file_ra_state->size can't be used as a
  limit, it is more of a hint and can be increased by various
  heuristics.
* Set readahead_control->max_index to the end of the VMA in all of the
  readahead paths that can be triggered from a fault on a file mapping
  (both "sync" and "async" readahead).
* Limit the read-around range start to the VMA's start.

Note that these changes only affect readahead triggered in the context of
a fault, they do not affect readahead triggered by read syscalls.  If a
user mixes the two types of accesses, the behavior is expected to be the
following: if a fault causes readahead and places a PG_readahead marker
and then a read(2) syscall hits the PG_readahead marker, the resulting
async readahead *will not* be limited to the VMA end.  Conversely, if a
read(2) syscall places a PG_readahead marker and then a fault hits the
marker, the async readahead *will* be limited to the VMA end.

There is an edge case that the above motivation glosses over: A single
file mapping might be backed by multiple VMAs.  For example, a whole file
could be mapped RW, then part of the mapping made RO using mprotect.  This
patch would hurt performance of a sequential faulted read of such a
mapping, the degree depending on how fragmented the VMAs are.  A usage
pattern like that is likely rare and already suffering from sub-optimal
performance because, e.g., the fragmented VMAs limit the fault-around, so
each VMA boundary in a sequential faulted read would cause a minor fault.
Still, this patch would make it worse.  See a previous discussion of this
topic at [1].

Tested by mapping and reading a small subset of a large file, then using
the cachestat syscall to verify the number of cached pages didn't exceed
the mapping size.

In practical scenarios, the effect depends on the specific file and usage.
Sometimes there is no effect at all, but, for some ELF files in Android,
we see ~20% fewer pages pulled into the cache.

A comprehensive performance evaluation hasn't been done, but, in addition
to the anecdontal memory savings mentioned above, a benchmark was run with
fio 3.38, showing neutral looking results:

    /data/local/tmp/fio --version

    fio --name=mmap_test --ioengine=mmap --rw=read --bs=4k \
        --offset=1G --size=1G --filesize=3G --numjobs=1 \
        --filename=testfile.bin

        Before: 4366.6 MiB/s (avg of 3459, 4592, 4613, 4697, 4472)
        After:  4444.0 MiB/s (avg of 4633, 4655, 4511, 4571, 3850)
                +1.7%

    Same, with --ioengine=mmap --rw=randread

        Before: 445.6 MiB/s  (avg of 446, 447, 442, 452, 441)
        After:  447.0 MiB/s  (avg of 447, 446, 446, 451, 445)
                +0.3%

    Same, with --ioengine=psync --rw=read

        Before: 3086.6 MiB/s (avg of 3122, 3094, 3066, 3094, 3057)
        After:  3084.6 MiB/s (avg of 3039, 3103, 3103, 3084, 3094)
                -0.06%

    Same, with --ioengine=psync --rw=randread

        Before: 2226.4 MiB/s (avg of 2256, 2183, 2207, 2265, 2221)
        After:  2231.4 MiB/s (avg of 2236, 2241, 2236, 2193, 2251)
                +0.2%

Link: https://lore.kernel.org/20260427030148.653228-1-fmayle@google.com
Link: https://lore.kernel.org/all/ivnv2crd3et76p2nx7oszuqhzzah756oecn5yuykzqfkqzoygw@yvnlkhjjssoz/
Signed-off-by: Frederick Mayle <fmayle@google.com>
Reviewed-by: Jan Kara <jack@suse.cz>
Reviewed-by: Kalesh Singh <kaleshsingh@google.com>
Cc: David Hildenbrand <david@kernel.org>
Cc: Lorenzo Stoakes <ljs@kernel.org>
Cc: Matthew Wilcox <willy@infradead.org>
Cc: Suren Baghdasaryan <surenb@google.com>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
include/linux/pagemap.h
mm/filemap.c
mm/readahead.c

index 31a848485ad9d9850d37185418349b89e6efe420..1f50991b43e3b3b2d0875417103a38498fb1c343 100644 (file)
@@ -1350,6 +1350,7 @@ struct readahead_control {
        struct file_ra_state *ra;
 /* private: use the readahead_* accessors instead */
        pgoff_t _index;
+       pgoff_t _max_index; /* limit readahead to _max_index, inclusive */
        unsigned int _nr_pages;
        unsigned int _batch_count;
        bool dropbehind;
@@ -1363,6 +1364,7 @@ struct readahead_control {
                .mapping = m,                                           \
                .ra = r,                                                \
                ._index = i,                                            \
+               ._max_index = ULONG_MAX,                                \
        }
 
 #define VM_READAHEAD_PAGES     (SZ_128K / PAGE_SIZE)
index 4e636647100c1dddab3b319a1946510089105dbe..97772a05a18e26084b9276e0a18692612a0fa6a9 100644 (file)
@@ -3314,6 +3314,8 @@ static struct file *do_sync_mmap_readahead(struct vm_fault *vmf)
        bool force_thp_readahead = false;
        unsigned short mmap_miss;
 
+       ractl._max_index = vmf->vma->vm_pgoff + vma_pages(vmf->vma) - 1;
+
        /* Use the readahead code, even if readahead is disabled */
        if (IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE) &&
            (vm_flags & VM_HUGEPAGE) && HPAGE_PMD_ORDER <= MAX_PAGECACHE_ORDER)
@@ -3396,6 +3398,7 @@ static struct file *do_sync_mmap_readahead(struct vm_fault *vmf)
                 * mmap read-around
                 */
                ra->start = max_t(long, 0, vmf->pgoff - ra->ra_pages / 2);
+               ra->start = max(ra->start, vmf->vma->vm_pgoff);
                ra->size = ra->ra_pages;
                ra->async_size = ra->ra_pages / 4;
                ra->order = 0;
@@ -3438,6 +3441,7 @@ static struct file *do_async_mmap_readahead(struct vm_fault *vmf,
        }
 
        if (folio_test_readahead(folio)) {
+               ractl._max_index = vmf->vma->vm_pgoff + vma_pages(vmf->vma) - 1;
                fpin = maybe_unlock_mmap_for_io(vmf, fpin);
                page_cache_async_ra(&ractl, folio, ra->ra_pages);
        }
index 7b05082c89ea2b2e8408a3c9a10b0f2ba850255f..8c12b63ccd4a2e2f61b351ffb71ddf564d7c8f41 100644 (file)
@@ -324,6 +324,8 @@ static void do_page_cache_ra(struct readahead_control *ractl,
                return;
 
        end_index = (isize - 1) >> PAGE_SHIFT;
+       if (end_index > ractl->_max_index)
+               end_index = ractl->_max_index;
        if (index > end_index)
                return;
        /* Don't read past the page containing the last byte of the file */
@@ -471,7 +473,7 @@ void page_cache_ra_order(struct readahead_control *ractl,
        pgoff_t start = readahead_index(ractl);
        pgoff_t index = start;
        unsigned int min_order = mapping_min_folio_order(mapping);
-       pgoff_t limit = (i_size_read(mapping->host) - 1) >> PAGE_SHIFT;
+       pgoff_t limit;
        pgoff_t mark = index + ra->size - ra->async_size;
        unsigned int nofs;
        int err = 0;
@@ -484,6 +486,8 @@ void page_cache_ra_order(struct readahead_control *ractl,
                goto fallback;
        }
 
+       limit = (i_size_read(mapping->host) - 1) >> PAGE_SHIFT;
+       limit = min(limit, ractl->_max_index);
        limit = min(limit, index + ra->size - 1);
 
        new_order = min(mapping_max_folio_order(mapping), new_order);