All of 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.