[RFC PATCH 0/2] mm: continue using per-VMA lock when retrying page faults after I/O
wang lian
lianux.mm at gmail.com
Sat Apr 4 20:19:32 AEDT 2026
Hi Barry,
> If either you or Matthew have a reproducer for this issue, Iâd be
> happy to try it out.
Kunwu and I evaluated this series ("mm: continue using per-VMA lock when
retrying page faults after I/O") under a stress scenario specifically
designed to expose the retry behavior in filemap_fault(). This models
the exact situation described by Matthew Wilcox [1], where retries after
I/O fail to make forward progress under memory pressure.
The scenario targets the critical window between I/O completion and
mmap_lock reacquisition. This workload deliberately includes frequent
mmap/munmap operations to simulate a highly contended mmap_lock
environment alongside severe memory pressure (1GB memcg limit). Under
this pressure, folios instantiated by the I/O can be aggressively
reclaimed before the delayed task can re-acquire the lock and install
the PTE, forcing retries to repeat the entire work.
To make this behavior reproducible, we constructed a stress setup that
intentionally extends this interval:
* 256-core x86 system
* 1GB memory cgroup
* 500 threads continuously faulting on a 16MB file
The core reproducer and the execution command are provided below:
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <time.h>
#define THREADS 500
#define FILE_SIZE (16 * 1024 * 1024) /* 16MB */
static _Atomic int g_stop = 0;
#define RUN_SECONDS 600
struct worker_arg {
long id;
uint64_t *counts;
};
void *worker(void *arg)
{
struct worker_arg *wa = (struct worker_arg *)arg;
long id = wa->id;
char path[64];
uint64_t local_rounds = 0;
snprintf(path, sizeof(path), "./test_file_%d_%ld.dat",
getpid(), id);
int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd < 0) return NULL;
if (ftruncate(fd, FILE_SIZE) < 0) {
close(fd); return NULL;
}
while (!atomic_load_explicit(&g_stop, memory_order_relaxed)) {
char *f_map = mmap(NULL, FILE_SIZE, PROT_READ,
MAP_SHARED, fd, 0);
if (f_map != MAP_FAILED) {
/* Pure page cache thrashing */
for (int i = 0; i < FILE_SIZE; i += 4096) {
volatile unsigned char c =
(unsigned char)f_map[i];
(void)c;
}
munmap(f_map, FILE_SIZE);
local_rounds++;
}
}
wa->counts[id] = local_rounds;
close(fd);
unlink(path);
return NULL;
}
int main(void)
{
printf("Pure File Thrashing Started. PID: %d\n", getpid());
pthread_t t[THREADS];
uint64_t local_counts[THREADS];
memset(local_counts, 0, sizeof(local_counts));
struct worker_arg args[THREADS];
for (long i = 0; i < THREADS; i++) {
args[i].id = i;
args[i].counts = local_counts;
pthread_create(&t[i], NULL, worker, &args[i]);
}
sleep(RUN_SECONDS);
atomic_store_explicit(&g_stop, 1, memory_order_relaxed);
for (int i = 0; i < THREADS; i++) pthread_join(t[i], NULL);
uint64_t total = 0;
for (int i = 0; i < THREADS; i++) total += local_counts[i];
printf("Total rounds : %llu\n", (unsigned long long)total);
printf("Throughput : %.2f rounds/sec\n",
(double)total / RUN_SECONDS);
return 0;
}
Command line used for the test:
systemd-run --scope -p MemoryHigh=1G -p MemoryMax=1.2G -p MemorySwapMax=0 \
--unit=mmap-thrash-$$ ./mmap_lock & \
TEST_PID=$!
We also added temporary counters in page fault retries [2]:
- RETRY_IO_MISS : folio not present after I/O completion
- RETRY_MMAP_DROP : retry fallback due to waiting for I/O
We report representative runs from our 600-second test iterations
(kernel v7.0-rc3):
| Case | Total Rounds | Throughput | Miss/Drop(%) | RETRY_MMAP_DROP | RETRY_IO_MISS |
| ------------------- | ------------ | ---------- | ------------ | --------------- | ------------- |
| Baseline (Run 1) | 22,711 | 37.85 /s | 45.04 | 970,078 | 436,956 |
| Baseline (Run 2) | 23,530 | 39.22 /s | 44.96 | 972,043 | 437,077 |
| With Series (Run A) | 54,428 | 90.71 /s | 1.69 | 1,204,124 | 20,398 |
| With Series (Run B) | 35,949 | 59.91 /s | 0.03 | 327,023 | 99 |
Notes:
1. Throughput Improvement: During the 600-second testing window, overall
workload throughput can more than double (e.g., Run A jumped from ~38
to 90.71 rounds/sec).
2. Elimination of Race Condition: Without the patch, ~45% of retries
were invalid because newly fetched folios were evicted during the
mmap_lock reacquisition delay. With the per-VMA retry path, the
invalidation ratio plummeted to near zero (0.03% - 1.69%).
3. Counter Scaling and Variance: In Run A, because the I/O wait
bottleneck is eliminated, the threads advance much faster. Thus, the
absolute number of mmap_lock drops naturally scales up with the
increased throughput. In Run B, the primary bottleneck shifts to the
mmap write-lock contention (lock convoying), causing throughput and
total drops to fluctuate. Crucially, the Miss/Drop ratio remains near
zero regardless of this variance.
Without this series, almost half of the retries fail to observe
completed I/O results, causing severe CPU and I/O waste. With the
finer-grained VMA lock, the faulting threads bypass the heavily
contended mmap_lock entirely during retries, completing the fault
almost instantly.
This scenario perfectly aligns with the exact concern raised, and these
results show that the patch not only successfully eliminates the retry
inefficiency but also tangibly boosts macro-level system throughput.
[1] https://lore.kernel.org/linux-mm/aSip2mWX13sqPW_l@casper.infradead.org/
[2] https://github.com/lianux-mm/ioretry_test/
Tested-by: Wang Lian <wanglian at kylinos.cn>
Tested-by: Kunwu Chan <chentao at kylinos.cn>
Reviewed-by: Wang Lian <lianux.mm at gmail.com>
Reviewed-by: Kunwu Chan <kunwu.chan at gmail.com>
--
Best Regards,
wang lian
More information about the Linuxppc-dev
mailing list