From: Mike Snitzer <snitzer@kernel.org>
To: Mike Snitzer <snitzer@hammerspace.com>
Cc: Chuck Lever <cel@kernel.org>, Jeff Layton <jlayton@kernel.org>,
linux-nfs@vger.kernel.org, NeilBrown <neil@brown.name>,
Olga Kornievskaia <okorniev@redhat.com>,
Dai Ngo <Dai.Ngo@oracle.com>, Tom Talpey <tom@talpey.com>,
Christoph Hellwig <hch@lst.de>, Jens Axboe <axboe@kernel.dk>,
Matthew Wilcox <willy@infradead.org>,
Jonathan Corbet <corbet@lwn.net>,
linux-mm@kvack.org, linux-fsdevel@vger.kernel.org
Subject: Re: [PATCH v7 0/5] mm, iomap, NFSD: stop uncached writes reading back shared pages
Date: Thu, 22 Oct 2026 18:07:01 -0400 [thread overview]
Message-ID: <atqJBS8MlYfeUU-t@kernel.org> (raw)
In-Reply-To: <20261020220346.28020-1-snitzer@kernel.org>
On Tue, Oct 20, 2026 at 06:03:41PM -0400, Mike Snitzer wrote:
> Hi,
>
> Please consider this v7 for the v7.5 development cycle, so that it can
> go upstream in the v7.5 merge window.
>
> An uncached (RWF_DONTCACHE) buffered write drops each page it writes
> once that page is written back. A page the write covers only partly is
> shared with the neighbouring write, and once the first of the two has
> been written back the second has to read it back from disk before it
> can write its part. 32 threads writing interleaved records to one file
> on XFS (emulated 4Kn NVMe, 4 KiB pages), before and after 3/5:
>
> record pwritev2() flags 4 KiB device MiB/s
> bytes reads per write
> before after before after
> 47008 RWF_DONTCACHE 0.97 0.000 831 2281
> 47008 RWF_DONTCACHE|RWF_DSYNC 0.99 0.000 601 1227
> 6000 RWF_DONTCACHE 0.97 0.000 121 488
> 6000 RWF_DONTCACHE|RWF_DSYNC 0.99 0.000 90 206
For anyone who wants to reproduce the table, the two inlined files
(below) are what produced the data, with no NFS involved:
dcwrite.c N threads writing interleaved records to one file, thread
r's record s at offset (s*N + r)*RECSZ, one pwritev2() per
record with the given RWF_ flags. Each record shares its
first and last page with the records of two other threads.
dc-ab.sh for each record size and flag set: a fresh XFS on an
emulated 4Kn NVMe namespace, dcwrite, then the device's
4 KiB reads per write (from /sys/block/<dev>/stat around
the write and a final sync), the file's resident pages
after the sync (fincore), and the write rate.
The device is the in-kernel NVMe target's file backend on a tmpfs
image, attached over nvme-loop, so it has NVMe's queue limits and a
4096-byte logical block on a 4 KiB-page kernel, and every read the
page cache sends it is counted. It needs root, CONFIG_NVME_TARGET and
CONFIG_NVME_TARGET_LOOP, nvme-cli, xfsprogs, util-linux's fincore, and
4 GiB free in /dev/shm:
cc -O2 -pthread -o dcwrite dcwrite.c
./dc-ab.sh /tmp/dc-ab ./dcwrite
On kernel with this v7 patchset's patches 1-4:
rs flags | writes 4K-reads reads/write READ-MiB | resident-pages | MiB/s
49152 none | 21824 15 0.0007 0.1 | 261888/261888 | 318.6
49152 dontcache | 21824 15 0.0007 0.1 | 0/261888 | 4433.3
49152 dsync | 21824 15 0.0007 0.1 | 261888/261888 | 1074.9
49152 dontcache,dsync | 21824 15 0.0007 0.1 | 0/261888 | 1475.3
47008 none | 22816 15 0.0007 0.1 | 261850/261850 | 3379.0
47008 dontcache | 22816 15 0.0007 0.1 | 1/261850 | 3053.8
47008 dsync | 22816 15 0.0007 0.1 | 261850/261850 | 1205.3
47008 dontcache,dsync | 22816 15 0.0007 0.1 | 1/261850 | 1173.4
6000 none | 178944 15 0.0001 0.1 | 262125/262125 | 1923.5
6000 dontcache | 178944 15 0.0001 0.1 | 0/262125 | 457.5
6000 dsync | 178944 15 0.0001 0.1 | 262125/262125 | 204.8
6000 dontcache,dsync | 178944 26 0.0001 0.1 | 11/262125 | 200.0
(The rates are from a 4 KiB-page arm64 VM and will differ elsewhere;
the reads per write should not.)
The defaults are the cover letter's: 32 threads, 1 GiB per run, record
sizes 49152, 47008 and 6000 bytes, and flags none, dontcache, dsync and
dontcache,dsync (NT, TOTAL, RSS and FLAGSETS override them). Run it
once on a kernel without the series and once with it. What to look
for:
- 47008 and 6000 with dontcache, with or without dsync: about one read
per write without the series, none with it. These are the table's
rows.
- 49152 with dontcache: the control. Its records cover only whole
pages, so it reads nothing either way.
- none and dsync: plain buffered writes. They read nothing either way,
and leave the whole file resident; the dontcache rows leave next to
none.
Thanks,
Mike
/* dcwrite: interleaved misaligned buffered writers, no NFS. N threads; thread
* r's record s lands at (s*N + r)*RECSZ, the iorhard geometry, so each record
* shares its first and last page with the records of two other threads.
* Each record is one pwritev2() with FLAGS (RWF_DONTCACHE, RWF_DSYNC, ...).
* With "direct" the file is opened O_DIRECT and the buffer is page aligned.
* Reports the write phase alone and, after a final fsync(), "to stable".
*
* usage: dcwrite FILE NTHREADS RECSZ TOTALBYTES FLAGS
* FLAGS: comma list of none,dontcache,dsync,direct
* Exit 1 at the first failed or short write, or a failed fsync(). */
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/uio.h>
#include <time.h>
#include <unistd.h>
#ifndef RWF_DONTCACHE
#define RWF_DONTCACHE 0x00000080
#endif
static int N, fd, flags, odirect; static long RECSZ, NREC; static volatile int failed;
static void *writer(void *arg)
{
long r = (long)arg;
char *buf = NULL;
if (posix_memalign((void **)&buf, 4096, RECSZ)) { failed = 1; return NULL; }
memset(buf, 'A' + r % 26, RECSZ);
for (long s = 0; s < NREC && !failed; s++) {
off_t off = (off_t)(s * N + r) * RECSZ;
struct iovec iov = { buf, RECSZ };
ssize_t n = pwritev2(fd, &iov, 1, off, flags);
if (n != RECSZ) {
fprintf(stderr, "thread %ld: pwritev2 at %lld: %s\n", r, (long long)off,
n < 0 ? strerror(errno) : "short write");
failed = 1;
}
}
free(buf);
return NULL;
}
int main(int argc, char **argv)
{
if (argc != 6) { fprintf(stderr, "usage: %s FILE NTHREADS RECSZ TOTALBYTES FLAGS\n", argv[0]); return 2; }
N = atoi(argv[2]); RECSZ = atol(argv[3]);
if (N < 1 || N > 1024 || RECSZ < 1) { fprintf(stderr, "bad NTHREADS or RECSZ\n"); return 2; }
NREC = atol(argv[4]) / RECSZ / N;
for (char *t = strtok(argv[5], ","); t; t = strtok(NULL, ","))
if (!strcmp(t, "dontcache")) flags |= RWF_DONTCACHE;
else if (!strcmp(t, "dsync")) flags |= RWF_DSYNC;
else if (!strcmp(t, "direct")) odirect = O_DIRECT;
else if (strcmp(t, "none")) { fprintf(stderr, "bad flag %s\n", t); return 2; }
fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC | odirect, 0644);
if (fd < 0) { perror(argv[1]); return 1; }
pthread_t *t = calloc(N, sizeof(*t));
struct timespec a, b; clock_gettime(CLOCK_MONOTONIC, &a);
for (long r = 0; r < N; r++)
if (pthread_create(&t[r], NULL, writer, (void *)r)) { perror("pthread_create"); return 1; }
for (int r = 0; r < N; r++) pthread_join(t[r], NULL);
clock_gettime(CLOCK_MONOTONIC, &b);
int frc = fsync(fd);
struct timespec c; clock_gettime(CLOCK_MONOTONIC, &c);
double sec = (b.tv_sec - a.tv_sec) + (b.tv_nsec - a.tv_nsec) / 1e9;
double ssec = (c.tv_sec - a.tv_sec) + (c.tv_nsec - a.tv_nsec) / 1e9;
double mib = (double)NREC * N * RECSZ / 1048576;
printf("dcwrite: %d threads x %ld records x %ld bytes = %.1f MiB in %.2fs (%.1f MiB/s) flags=0x%x%s; to stable %.2fs (%.1f MiB/s)\n",
N, NREC, RECSZ, mib, sec, mib / sec, flags, odirect ? " O_DIRECT" : "", ssec, mib / ssec);
if (frc) perror("fsync");
if (close(fd)) { perror("close"); frc = -1; }
return (failed || frc) ? 1 : 0;
}
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# dc-ab.sh OUTDIR [DCWRITE] - RWF_DONTCACHE shared-page test, no NFS.
#
# For each record size and flag set: a fresh XFS on an emulated 4Kn NVMe
# namespace (the in-kernel NVMe target's file backend on a tmpfs image,
# attached over nvme-loop, so the device has NVMe's queue limits and a
# 4096-byte logical block), then NT dcwrite threads writing TOTAL bytes as
# interleaved records. It reports the device's 4 KiB reads per write, from
# /sys/block/<dev>/stat around the write and a final sync, the file's
# resident pages after the sync (fincore), and the write rate.
#
# Needs root, the nvmet and nvme-loop modules, nvme-cli, xfsprogs, util-linux
# (fincore), and SIZE (default 4G) free in /dev/shm. Build dcwrite first:
# cc -O2 -pthread -o dcwrite dcwrite.c
#
# Environment: NT (32), TOTAL (1 GiB), RSS ("49152 47008 6000"),
# FLAGSETS ("none dontcache dsync dontcache,dsync"), SIZE (4G),
# IMG (/dev/shm/dc-ab.img), MNT (/mnt/dc-ab), PORT (nvmet port id, 9).
set -u
OUT=${1:?usage: dc-ab.sh OUTDIR [DCWRITE]}; DCW=$(realpath "${2:-./dcwrite}")
NT=${NT:-32}; TOTAL=${TOTAL:-1073741824}; SIZE=${SIZE:-4G}
RSS=${RSS:-"49152 47008 6000"}
FLAGSETS=${FLAGSETS:-"none dontcache dsync dontcache,dsync"}
IMG=${IMG:-/dev/shm/dc-ab.img}; MNT=${MNT:-/mnt/dc-ab}; PORT=${PORT:-9}
NQN=dc-ab-test; CFS=/sys/kernel/config/nvmet
SUB=$CFS/subsystems/$NQN; PRT=$CFS/ports/$PORT
die() { echo "dc-ab.sh: $*" >&2; down; exit 2; }
nsdev() {
local d
for d in /sys/block/nvme*n*; do
# not a multipath path node (nvmeXcYnZ), which has no /dev entry
[[ $(basename "$d") =~ ^nvme[0-9]+n[0-9]+$ ]] || continue
[ "$(cat "$d/device/subsysnqn" 2>/dev/null)" = $NQN ] &&
{ basename "$d"; return; }
done
}
up() {
truncate -s "$SIZE" "$IMG" || die "cannot create $IMG"
mkdir "$SUB" || die "cannot create nvmet subsystem $NQN"
echo 1 > "$SUB/attr_allow_any_host"
mkdir "$SUB/namespaces/1"
echo -n "$IMG" > "$SUB/namespaces/1/device_path"
# the image is in RAM already: no O_DIRECT below the emulated device
echo 1 > "$SUB/namespaces/1/buffered_io"
echo 1 > "$SUB/namespaces/1/enable" || die "cannot enable the namespace"
mkdir "$PRT" || die "nvmet port $PORT is in use: set PORT"
echo -n loop > "$PRT/addr_trtype"
# nvme-loop sends a connect without an address to the first loop port,
# which need not be this one
echo -n $NQN > "$PRT/addr_traddr"
ln -s "$SUB" "$PRT/subsystems/$NQN"
nvme connect -t loop -a $NQN -n $NQN > /dev/null || die "nvme connect failed"
local i
for i in $(seq 20); do DEV=$(nsdev); [ -n "$DEV" ] && break; sleep 0.5; done
[ -n "$DEV" ] || die "the namespace did not appear"
udevadm settle 2> /dev/null
mkfs.xfs -q -f "/dev/$DEV" || die "mkfs.xfs failed"
mkdir -p "$MNT"
mount -t xfs "/dev/$DEV" "$MNT" || die "mount failed"
}
down() {
umount "$MNT" 2> /dev/null
nvme disconnect -n $NQN > /dev/null 2>&1
rm -f "$PRT/subsystems/$NQN" 2> /dev/null
rmdir "$PRT" 2> /dev/null
[ -d "$SUB" ] && echo 0 > "$SUB/namespaces/1/enable" 2> /dev/null
rmdir "$SUB/namespaces/1" "$SUB" 2> /dev/null
rm -f "$IMG"
}
[ "$(id -u)" = 0 ] || { echo "dc-ab.sh: run as root" >&2; exit 2; }
[ -x "$DCW" ] || { echo "dc-ab.sh: build dcwrite first: cc -O2 -pthread -o dcwrite dcwrite.c" >&2; exit 2; }
modprobe nvmet && modprobe nvme-loop || exit 2
[ -d "$SUB" ] && { echo "dc-ab.sh: $SUB exists: a run is in progress or was left behind" >&2; exit 2; }
mkdir -p "$OUT"
echo "kernel $(uname -r)" > "$OUT/env.txt"
printf '%-6s %-16s | %-9s %-10s %-11s %-9s | %-15s | %s\n' \
rs flags writes 4K-reads reads/write READ-MiB resident-pages MiB/s > "$OUT/table.txt"
for rs in $RSS; do for fl in $FLAGSETS; do
d=$OUT/$rs-${fl//,/+}; mkdir -p "$d"
DEV=; up
F=$MNT/dc.bin
sync; echo 3 > /proc/sys/vm/drop_caches
read -r _ _ s0 _ < "/sys/block/$DEV/stat"
"$DCW" "$F" "$NT" "$rs" "$TOTAL" "$fl" > "$d/dcwrite.txt" 2>&1; rc=$?
sync
read -r _ _ s1 _ < "/sys/block/$DEV/stat"
res=$(fincore -b -n -r -o RES "$F"); pages=$(( ($(stat -c %s "$F") + 4095) / 4096 ))
nrec=$(( TOTAL / rs / NT * NT ))
mbs=$(grep -o '([0-9.]* MiB/s' "$d/dcwrite.txt" | head -1 | tr -d '(' | awk '{print $1}')
# data reads in 4 KiB blocks, from sectors read (a read request that
# moves no sectors, as a flush can show up, adds nothing here)
rd=$(( (s1 - s0) * 512 / 4096 ))
rmib=$(awk -v s=$((s1 - s0)) 'BEGIN { printf "%.1f", s * 512 / 1048576 }')
rpw=$(awk -v r=$rd -v w=$nrec 'BEGIN { printf "%.4f", r / w }')
[ $rc -ne 0 ] && mbs="rc=$rc"
printf '%-6s %-16s | %-9s %-10s %-11s %-9s | %-15s | %s\n' $rs $fl $nrec $rd \
$rpw $rmib "$(( (res + 4095) / 4096 ))/$pages" "${mbs:-?}" >> "$OUT/table.txt"
down
done; done
cat "$OUT/table.txt"
prev parent reply other threads:[~2026-10-22 22:07 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-20 22:03 [PATCH v7 0/5] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
2026-10-20 22:03 ` [PATCH v7 1/5] mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats Mike Snitzer
2026-10-20 22:11 ` Mike Snitzer
2026-10-21 10:50 ` Jeff Layton
2026-10-22 22:31 ` Mike Snitzer
2026-10-20 22:03 ` [PATCH v7 2/5] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it Mike Snitzer
2026-10-21 10:52 ` Jeff Layton
2026-10-22 23:15 ` Mike Snitzer
2026-10-20 22:03 ` [PATCH v7 3/5] mm, iomap: keep a partly covered page until an uncached write completes it Mike Snitzer
2026-10-21 14:36 ` Jeff Layton
2026-10-22 21:38 ` Mike Snitzer
2026-10-20 22:03 ` [PATCH v7 4/5] mm, iomap: let an uncached writer complete its shared pages itself Mike Snitzer
2026-10-21 10:54 ` Jeff Layton
2026-10-23 0:54 ` Mike Snitzer
2026-10-20 22:03 ` [PATCH v7 5/5] NFSD: drop a shared boundary page only once both WRITEs have written it Mike Snitzer
2026-10-22 22:07 ` Mike Snitzer [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=atqJBS8MlYfeUU-t@kernel.org \
--to=snitzer@kernel.org \
--cc=Dai.Ngo@oracle.com \
--cc=axboe@kernel.dk \
--cc=cel@kernel.org \
--cc=corbet@lwn.net \
--cc=hch@lst.de \
--cc=jlayton@kernel.org \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=linux-nfs@vger.kernel.org \
--cc=neil@brown.name \
--cc=okorniev@redhat.com \
--cc=snitzer@hammerspace.com \
--cc=tom@talpey.com \
--cc=willy@infradead.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox