Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
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"


      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