[PATCH] erofs-utils: tests: add a splice() test for inode_share mode

Zhan Xusheng <[email protected]>
Newsgroups org.ozlabs.lists.linux-erofs
Message-ID <[email protected]>
From: Zhan Xusheng <[email protected]>

From: Zhan Xusheng <[email protected]>

splice() and sendfile() went through filemap_splice_read() on the user
file, so they populated the per-inode page cache instead of the shared one
that read() and mmap() use.  The content was correct either way, which is
why nothing caught it: only which mapping the pages land in changes.

Observe that instead.  cachestat() works on the file's own mapping, while
fincore's mmap()+mincore() fallback goes through erofs_ishare_mmap(), which
redirects the vma to the backing file and so reports the shared mapping.
After a splice-only read the first must be empty and the second must not,
which is the same pair of observations erofs/028 already relies on.

Measured on 7.2 with two identical 8 MiB files under inode_share, before
and after the kernel change, as pages of the 8 MiB file:

  before   own 2048   shared 0
  after    own 0      shared 2048

spliceread is a small helper because no common tool reads a file purely
with splice(2); dd and cat both use read(2).

Signed-off-by: Zhan Xusheng <[email protected]>
---
 tests/Makefile.am      |  3 ++
 tests/common/rc        |  6 ++++
 tests/erofs/032        | 82 ++++++++++++++++++++++++++++++++++++++++++
 tests/erofs/032.out    |  2 ++
 tests/src/Makefile.am  |  5 ++-
 tests/src/spliceread.c | 61 +++++++++++++++++++++++++++++++
 6 files changed, 158 insertions(+), 1 deletion(-)
 create mode 100755 tests/erofs/032
 create mode 100644 tests/erofs/032.out
 create mode 100644 tests/src/spliceread.c

diff --git a/tests/Makefile.am b/tests/Makefile.am
index c0291ac..d7674fe 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -132,6 +132,9 @@ TESTS += erofs/030
 # 031 - test chunk-based mapping with shared chunks across inodes
 TESTS += erofs/031
 
+# 032 - test that splice() uses the shared page cache in inode_share mode
+TESTS += erofs/032
+
 # NEW TEST CASE HERE
 # TESTS += erofs/999
 
diff --git a/tests/common/rc b/tests/common/rc
index aa7ef99..32484ee 100644
--- a/tests/common/rc
+++ b/tests/common/rc
@@ -357,6 +357,12 @@ _require_fssum()
 	[ -x $FSSUM_PROG ] || _notrun "fssum not built"
 }
 
+_require_spliceread()
+{
+	SPLICEREAD_PROG=${PWD}/src/spliceread
+	[ -x $SPLICEREAD_PROG ] || _notrun "spliceread not built"
+}
+
 # generate random string with maximum $1 length
 _random()
 {
diff --git a/tests/erofs/032 b/tests/erofs/032
new file mode 100755
index 0000000..aef2164
--- /dev/null
+++ b/tests/erofs/032
@@ -0,0 +1,82 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0+
+#
+# Test that splice() reads through the shared page cache in inode_share mode
+#
+seq=`basename $0`
+seqres=$RESULT_DIR/$(echo $0 | awk '{print $((NF-1))"/"$NF}' FS="/")
+
+# get standard environment, filters and checks
+. "${srcdir}/common/rc"
+
+cleanup()
+{
+	cd /
+	rm -f $tmp.*
+}
+
+# Resident bytes as reported through cachestat(), which works on the file's
+# own mapping.  In inode_share mode that is the per-inode one.
+own_cached()
+{
+	fincore -oRES -bnr "$1"
+}
+
+# The same, with cachestat() made to fail so that fincore falls back to
+# mmap()+mincore().  mmap is redirected to the backing file, so this reports
+# the shared mapping instead.
+shared_cached()
+{
+	strace -efault=cachestat -o/dev/null fincore -oRES -bnr "$1"
+}
+
+_require_erofs
+_require_erofs_inode_sharing
+_require_spliceread
+command -v strace > /dev/null 2>&1 || _notrun "strace is not found"
+command -v fincore > /dev/null 2>&1 || _notrun "fincore is not found"
+command -v fadvise > /dev/null 2>&1 || _notrun "fadvise is not found"
+
+# remove previous $seqres.full before test
+rm -f $seqres.full
+
+# real QA test starts here
+echo "QA output created by $seq"
+
+if [ -z $SCRATCH_DEV ]; then
+	SCRATCH_DEV=$tmp/erofs_$seq.img
+	rm -f $SCRATCH_DEV
+fi
+
+localdir="$tmp/$seq"
+rm -rf $localdir
+mkdir -p $localdir
+
+# two files with identical content, so that they end up sharing one inode
+dd if=/dev/urandom of=$localdir/a bs=1M count=8 2>/dev/null
+cp $localdir/a $localdir/b
+
+_scratch_mkfs --xattr-inode-digest=system.erofs.fingerprint $localdir \
+	>> $seqres.full 2>&1 || _fail "failed to mkfs"
+_scratch_mount -oinode_share,domain_id=share_$seq 2>> $seqres.full
+
+f=$SCRATCH_MNT/b
+
+# fadvise is redirected to the backing file, so this empties the shared
+# mapping.  The per-inode mapping is empty anyway on a fresh mount.
+fadvise -a dontneed $f
+
+$SPLICEREAD_PROG $f >> $seqres.full 2>&1 || _fail "splice failed"
+
+echo "own $(own_cached $f) shared $(shared_cached $f)" >> $seqres.full
+
+[ "$(own_cached $f)" -eq 0 ] || \
+	_fail "splice populated the per-inode page cache"
+[ "$(shared_cached $f)" -gt 0 ] || \
+	_fail "splice did not populate the shared page cache"
+
+_scratch_unmount
+
+echo Silence is golden
+status=0
+exit 0
diff --git a/tests/erofs/032.out b/tests/erofs/032.out
new file mode 100644
index 0000000..34e059f
--- /dev/null
+++ b/tests/erofs/032.out
@@ -0,0 +1,2 @@
+QA output created by 032
+Silence is golden
diff --git a/tests/src/Makefile.am b/tests/src/Makefile.am
index 16de41a..f51be82 100644
--- a/tests/src/Makefile.am
+++ b/tests/src/Makefile.am
@@ -3,12 +3,15 @@
 
 AUTOMAKE_OPTIONS	= foreign
 check_PROGRAMS		=
-noinst_PROGRAMS		= fssum
+noinst_PROGRAMS		= fssum spliceread
 
 fssum_CFLAGS = -Wall -I$(top_srcdir)/include
 fssum_LDADD = $(top_builddir)/lib/liberofs.la
 fssum_SOURCES = fssum.c
 
+spliceread_CFLAGS = -Wall
+spliceread_SOURCES = spliceread.c
+
 if ENABLE_LZ4
 check_PROGRAMS += badlz4
 badlz4_SOURCES = badlz4.c
diff --git a/tests/src/spliceread.c b/tests/src/spliceread.c
new file mode 100644
index 0000000..cb9f09d
--- /dev/null
+++ b/tests/src/spliceread.c
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * spliceread.c
+ *
+ * Read a file into a pipe with splice(2) and discard it, so that a test can
+ * exercise ->splice_read without any read(2) touching the file.
+ */
+#define _GNU_SOURCE
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#define CHUNK	65536
+
+int main(int argc, char *argv[])
+{
+	static char buf[CHUNK];
+	int fd, p[2];
+
+	if (argc != 2) {
+		fprintf(stderr, "usage: %s FILE\n", argv[0]);
+		return 2;
+	}
+
+	fd = open(argv[1], O_RDONLY);
+	if (fd < 0) {
+		perror("open");
+		return 1;
+	}
+	if (pipe(p)) {
+		perror("pipe");
+		return 1;
+	}
+
+	for (;;) {
+		ssize_t n = splice(fd, NULL, p[1], NULL, CHUNK, 0);
+
+		if (n < 0) {
+			perror("splice");
+			return 1;
+		}
+		if (!n)
+			break;
+
+		/* drain the pipe so the next splice() has room */
+		while (n > 0) {
+			ssize_t m = read(p[0], buf, n > CHUNK ? CHUNK : n);
+
+			if (m <= 0) {
+				perror("read");
+				return 1;
+			}
+			n -= m;
+		}
+	}
+
+	close(p[0]);
+	close(p[1]);
+	close(fd);
+	return 0;
+}
-- 
2.43.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.