Test bio coalescing in blkdev_writepages() and buffer_head batching
within a folio, using buffered writes to a loop device with a block
size below PAGE_SIZE. Contiguous, gapped and scattered writes are
checked against the backing file, and a concurrent writer/fsync run
checks for kernel warnings and crashes.

Signed-off-by: Lukas Sichert <[email protected]>
---
 .../testing/selftests/filesystems/.gitignore  |   1 +
 tools/testing/selftests/filesystems/Makefile  |   2 +-
 .../filesystems/blkdev_writeback_test.c       | 259 ++++++++++++++++++
 3 files changed, 261 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/filesystems/blkdev_writeback_test.c

diff --git a/tools/testing/selftests/filesystems/.gitignore 
b/tools/testing/selftests/filesystems/.gitignore
index 9eb185fb2f9d..ed90ff2bf086 100644
--- a/tools/testing/selftests/filesystems/.gitignore
+++ b/tools/testing/selftests/filesystems/.gitignore
@@ -7,3 +7,4 @@ anon_inode_test
 kernfs_test
 idmapped_tmpfile
 ustat_test
+blkdev_writeback_test
diff --git a/tools/testing/selftests/filesystems/Makefile 
b/tools/testing/selftests/filesystems/Makefile
index 03be337c1f35..4236aa18a73b 100644
--- a/tools/testing/selftests/filesystems/Makefile
+++ b/tools/testing/selftests/filesystems/Makefile
@@ -2,7 +2,7 @@
 
 CFLAGS += $(KHDR_INCLUDES)
 TEST_GEN_PROGS := devpts_pts file_stressor anon_inode_test kernfs_test fclog 
ustat_test
-TEST_GEN_PROGS += idmapped_tmpfile
+TEST_GEN_PROGS += idmapped_tmpfile blkdev_writeback_test
 TEST_GEN_PROGS_EXTENDED := dnotify_test
 
 include ../lib.mk
diff --git a/tools/testing/selftests/filesystems/blkdev_writeback_test.c 
b/tools/testing/selftests/filesystems/blkdev_writeback_test.c
new file mode 100644
index 000000000000..307d67b150e6
--- /dev/null
+++ b/tools/testing/selftests/filesystems/blkdev_writeback_test.c
@@ -0,0 +1,259 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+
+#include <fcntl.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <linux/fs.h>
+#include <linux/loop.h>
+
+#include "kselftest_harness.h"
+
+#define DEV_SIZE       (4 * 1024 * 1024)
+#define SOFT_BLKSIZE   1024U   /* < PAGE_SIZE: several buffer_heads/folio */
+
+#define STRESS_DURATION_SEC    30
+#define STRESS_MAX_CHUNK_BLKS  8
+
+FIXTURE(blkdev_writeback) {
+       int fd_backing;
+       int fd_dev;
+       char dev_path[32];
+};
+
+static void fill_pattern(char *buf, size_t len, unsigned seed)
+{
+       size_t i;
+
+       for (i = 0; i < len; i++)
+               buf[i] = (char)(seed + i);
+}
+
+static void write_pattern(struct __test_metadata *_metadata,
+                         FIXTURE_DATA(blkdev_writeback) *self,
+                         off_t offset, size_t len, unsigned seed)
+{
+       char *buf = malloc(len);
+
+       ASSERT_NE(buf, NULL);
+       fill_pattern(buf, len, seed);
+       ASSERT_EQ(pwrite(self->fd_dev, buf, len, offset), (ssize_t)len);
+       free(buf);
+}
+
+static void verify_pattern(struct __test_metadata *_metadata,
+                          FIXTURE_DATA(blkdev_writeback) *self,
+                          off_t offset, size_t len, unsigned seed)
+{
+       char *expected = malloc(len);
+       char *actual = malloc(len);
+       size_t i;
+
+       ASSERT_NE(expected, NULL);
+       ASSERT_NE(actual, NULL);
+       fill_pattern(expected, len, seed);
+
+       ASSERT_EQ(fsync(self->fd_dev), 0);
+       ASSERT_EQ(pread(self->fd_backing, actual, len, offset), (ssize_t)len);
+
+       for (i = 0; i < len; i++)
+               ASSERT_EQ(actual[i], expected[i])
+                       TH_LOG("mismatch at backing file offset %zu",
+                              (size_t)offset + i);
+
+       free(expected);
+       free(actual);
+}
+
+static void verify_zero(struct __test_metadata *_metadata,
+                       FIXTURE_DATA(blkdev_writeback) *self,
+                       off_t offset, size_t len)
+{
+       char *actual = malloc(len);
+       size_t i;
+
+       ASSERT_NE(actual, NULL);
+       ASSERT_EQ(pread(self->fd_backing, actual, len, offset), (ssize_t)len);
+
+       for (i = 0; i < len; i++)
+               ASSERT_EQ(actual[i], 0)
+                       TH_LOG("unexpected write at backing file offset %zu",
+                              (size_t)offset + i);
+
+       free(actual);
+}
+
+FIXTURE_SETUP(blkdev_writeback)
+{
+       int fd_ctl, loop_nr, blksize = SOFT_BLKSIZE;
+       char template[] = "/tmp/blkdev_writeback_test_XXXXXX";
+       struct loop_config config = { 0 };
+
+       self->fd_backing = -1;
+       self->fd_dev = -1;
+
+       if (geteuid() != 0)
+               SKIP(return, "Test requires root to attach a loop device");
+
+       self->fd_backing = mkstemp(template);
+       ASSERT_GE(self->fd_backing, 0);
+       ASSERT_EQ(unlink(template), 0);
+       ASSERT_EQ(ftruncate(self->fd_backing, DEV_SIZE), 0);
+
+       fd_ctl = open("/dev/loop-control", O_RDWR);
+       if (fd_ctl < 0)
+               SKIP(return, "Loop device support is not available");
+       loop_nr = ioctl(fd_ctl, LOOP_CTL_GET_FREE);
+       ASSERT_GE(loop_nr, 0);
+       ASSERT_EQ(close(fd_ctl), 0);
+
+       snprintf(self->dev_path, sizeof(self->dev_path), "/dev/loop%d", 
loop_nr);
+       self->fd_dev = open(self->dev_path, O_RDWR);
+       ASSERT_GE(self->fd_dev, 0);
+
+       config.fd = self->fd_backing;
+       ASSERT_EQ(ioctl(self->fd_dev, LOOP_CONFIGURE, &config), 0);
+       ASSERT_EQ(ioctl(self->fd_dev, BLKBSZSET, &blksize), 0);
+}
+
+FIXTURE_TEARDOWN(blkdev_writeback)
+{
+       if (self->fd_dev >= 0) {
+               fsync(self->fd_dev);
+               ioctl(self->fd_dev, LOOP_CLR_FD);
+               close(self->fd_dev);
+       }
+       if (self->fd_backing >= 0)
+               close(self->fd_backing);
+}
+
+/*
+ * A single write spanning many contiguous folios should still read back
+ * correctly regardless of how many bios blkdev_writepages() coalesced it
+ * into.
+ */
+TEST_F(blkdev_writeback, contiguous_multi_folio)
+{
+       write_pattern(_metadata, self, 0, 512 * 1024, 0);
+       verify_pattern(_metadata, self, 0, 512 * 1024, 0);
+}
+
+/*
+ * Two writes separated by an untouched gap must each land correctly,this 
means the
+ * non-contiguous block run must close out the current bio/run rather than 
merge across
+ * the hole.
+ */
+TEST_F(blkdev_writeback, gapped_regions)
+{
+       const off_t region_a = 0;
+       const off_t region_b = 192 * 1024;
+       const size_t region_len = 64 * 1024;
+       const off_t gap = 64 * 1024;
+       const size_t gap_len = region_b - gap;
+
+       write_pattern(_metadata, self, region_a, region_len, 0xa5);
+       write_pattern(_metadata, self, region_b, region_len, 0x3c);
+
+       verify_pattern(_metadata, self, region_a, region_len, 0xa5);
+       verify_pattern(_metadata, self, region_b, region_len, 0x3c);
+       verify_zero(_metadata, self, gap, gap_len);
+}
+
+/*
+ * Many small, mutually non-adjacent writes force repeated run/bio splits
+ * in __bh_bio_append(), stressing the tracking of buffer runs within a
+ * coalesced bio.
+ */
+TEST_F(blkdev_writeback, scattered_writes)
+{
+       const size_t chunk_len = 4096;
+       const unsigned nr_chunks = 32;
+       const size_t stride = DEV_SIZE / nr_chunks;
+       unsigned i;
+
+       for (i = 0; i < nr_chunks; i++) {
+               /* write in reverse order so nothing is naturally contiguous */
+               unsigned idx = nr_chunks - 1 - i;
+
+               write_pattern(_metadata, self, idx * stride, chunk_len, idx);
+       }
+
+       for (i = 0; i < nr_chunks; i++)
+               verify_pattern(_metadata, self, i * stride, chunk_len, i);
+}
+
+static void stress_writer(FIXTURE_DATA(blkdev_writeback) *self)
+{
+       char buf[STRESS_MAX_CHUNK_BLKS * SOFT_BLKSIZE];
+       unsigned seed = getpid();
+
+       srand(seed);
+       for (;;) {
+               size_t blks = 1 + rand_r(&seed) % STRESS_MAX_CHUNK_BLKS;
+               size_t len = blks * SOFT_BLKSIZE;
+               size_t max_off_blks = (DEV_SIZE - len) / SOFT_BLKSIZE;
+               off_t off = (rand_r(&seed) % max_off_blks) * SOFT_BLKSIZE;
+
+               fill_pattern(buf, len, seed);
+               if (pwrite(self->fd_dev, buf, len, off) != (ssize_t)len)
+                       exit(1);
+       }
+}
+
+static void stress_syncer(FIXTURE_DATA(blkdev_writeback) *self)
+{
+       for (;;) {
+               if (fsync(self->fd_dev) < 0)
+                       exit(1);
+       }
+}
+
+/*
+ * Concurrent writers dirtying overlapping, adjacent and disjoint ranges
+ * while syncers repeatedly force writing the bio. Data content is not checked
+ * (writes overlap by design), the point is to survive the duration without a
+ * kernel-side warning or crash.
+ */
+TEST_F_TIMEOUT(blkdev_writeback, stress_concurrent_writeback, 
STRESS_DURATION_SEC + 30)
+{
+       long nr_procs = sysconf(_SC_NPROCESSORS_ONLN);
+       pid_t pids[2 * nr_procs];
+       int i;
+
+       for (i = 0; i < nr_procs; i++) {
+               pids[i] = fork();
+               ASSERT_GE(pids[i], 0);
+               if (pids[i] == 0)
+                       stress_writer(self);
+       }
+       for (i = 0; i < nr_procs; i++) {
+               pids[nr_procs + i] = fork();
+               ASSERT_GE(pids[nr_procs + i], 0);
+               if (pids[nr_procs + i] == 0)
+                       stress_syncer(self);
+       }
+
+       ASSERT_EQ(clock_nanosleep(CLOCK_MONOTONIC, 0,
+                       &(struct timespec){ .tv_sec = STRESS_DURATION_SEC }, 
NULL), 0);
+
+       for (i = 0; i < 2 * nr_procs; i++)
+               kill(pids[i], SIGKILL);
+
+       for (i = 0; i < 2 * nr_procs; i++) {
+               int wstatus;
+
+               ASSERT_EQ(waitpid(pids[i], &wstatus, 0), pids[i]);
+               /* an exit before we killed it means it hit a real error */
+               ASSERT_FALSE(WIFEXITED(wstatus) && WEXITSTATUS(wstatus) != 0);
+       }
+}
+
+TEST_HARNESS_MAIN


Reply via email to