summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-06-15 03:59:45 +0530
committerLinus Torvalds <torvalds@linux-foundation.org>2026-06-15 03:59:45 +0530
commit7e0e7bd60d4a812b694c477716597fcb038b00cb (patch)
tree4ff61d47485803e7dacab1c8ddef0a4c11b512da /tools
parentff8747aacaff8266dd751b8a8648fb728dcc3b21 (diff)
parentaa5c4fe3ba0cb2af90bbcfa7a8ef4fefcd5c2370 (diff)
Merge tag 'vfs-7.2-rc1.misc' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs
Pull misc vfs updates from Christian Brauner: "Features: - Reduce pipe->mutex contention by pre-allocating pages outside the lock in anon_pipe_write(). anon_pipe_write() called alloc_page() once per page while holding pipe->mutex. The allocation can sleep doing direct reclaim and runs memcg charging, which extends the critical section and stalls any concurrent reader on the same mutex. Now up to 8 pages are pre-allocated before the mutex is taken, leftovers are recycled into the per-pipe tmp_page[] cache before unlock, and any remainder is released after unlock, keeping the allocator out of the critical section on both sides. On a writers x readers sweep with 64KB writes against a 1 MB pipe throughput improves 6-28% and average write latency drops 5-22%; under memory pressure - when the cost of holding the mutex across reclaim is highest - throughput improves 21-48% and latency drops 17-33%. The microbenchmark is added to selftests. - uaccess/sockptr: fix the ignored_trailing logic in copy_struct_to_user() to behave as documented and the usize check in copy_struct_from_sockptr() for user pointers, and add copy_struct_{from,to}_bounce_buffer() and copy_struct_to_sockptr() helpers for upcoming users (IPPROTO_SMBDIRECT, IPPROTO_QUIC). - bpf: add a sleepable bpf_real_inode() kfunc that resolves the real inode backing a dentry via d_real_inode(). On overlayfs the inode attached to the dentry doesn't carry the underlying device information; this is used by the filesystem restriction BPF program that was merged into systemd. - docs: add guidelines for submitting new filesystems, motivated by the maintenance burden abandoned and untestable filesystems impose on VFS developers, blocking infrastructure work like folio conversions and iomap migration. Fixes: - libfs: set SB_I_NOEXEC and SB_I_NODEV by default in init_pseudo() and drop the now-redundant assignments in callers. This began as a one-line dma-buf fix for a path_noexec() warning; a pseudo filesystem has no reason not to set SB_I_NOEXEC. All init_pseudo() callers were audited: the only visible effect is on dma-buf where SB_I_NOEXEC silences the warning. - Handle set_blocksize() failures in legacy filesystems (bfs, hpfs, qnx4, jfs, befs, affs, isofs, minix, ntfs3, omfs). Mounting a device with a sector size > PAGE_SIZE crashed roughly half of them; the rest had the same missing error handling pattern. Plus a follow-up releasing the superblock buffer_head when setting the minix v3 block size fails. - mount: honour SB_NOUSER in the new mount API. - fs/fcntl: fix a SOFTIRQ-unsafe lock order in fasync signaling by switching the process-group paths of send_sigio() and send_sigurg() from read_lock(&tasklist_lock) to RCU, matching the single-PID path. - vfs: add an FS_USERNS_DELEGATABLE flag and set it for NFS, fixing delegated NFS mounts (fsopen() in a container with the mount performed by a privileged daemon) that broke when non-init s_user_ns was tied to FS_USERNS_MOUNT. - selftests/namespaces: fix a hang in nsid_test where an unreaped grandchild kept the TAP pipe write-end open, a waitpid(-1) race in listns_efault_test, and a false FAIL on kernels without listns() where the tests should SKIP. - filelock: fix the break_lease() stub signature for CONFIG_FILE_LOCKING=n. - init/initramfs_test: wait for the async initramfs unpacking before running; the test and do_populate_rootfs() share the parser state. - fs/coredump: reduce redundant log noise in validate_coredump_safety(). - iomap: pass the correct length to fserror_report_io() in __iomap_write_begin(). - backing-file: fix the backing_file_open() kerneldoc. Cleanups: - initramfs: refactor the cpio hex header parsing to use hex2bin() instead of the hand-rolled simple_strntoul() which is reverted, and extend the initramfs KUnit tests to cover header fields with 0x prefixes. - Replace __get_free_pages() and friends with kmalloc()/kzalloc() across quota, proc, ocfs2/dlm, nilfs2, nfs, nfsd, libfs, jfs, jbd2, isofs, fuse, select, namespace, configfs, binfmt_misc, bfs, and the do_mounts init code - part of the larger work of replacing page allocator calls with kmalloc(). - Use clear_and_wake_up_bit() in unlock_buffer() and journal_end_buffer_io_sync() instead of open-coding the sequence. - Drop unused VFS exports: unexport drop_super_exclusive(), remove start_removing_user_path_at(), and fold __start_removing_path() into start_removing_path(). - fs/read_write: narrow the __kernel_write() export with EXPORT_SYMBOL_FOR_MODULES(). - vfs: uapi: retire octal and hex constants in favor of (1 << n) for the O_ flags. Finding a free bit for a new flag across the architectures was needlessly hard with the mixed bases. - dcache: add extra sanity checks of dead dentries in dentry_free() via a new DENTRY_WARN_ONCE() that also prints d_flags. - iov_iter: use kmemdup_array() in dup_iter() to harden the allocation against multiplication overflow. - fs/pipe: write to ->poll_usage only once. - vfs: remove an always-taken if-branch in find_next_fd(). - dcache: use kmalloc_flex() for struct external_name in __d_alloc(). - namei: use QSTR() instead of QSTR_INIT() in path_pts(). - sync_file_range: delete dead S_ISLNK code. - Comment fixes: retire a stale comment in fget_task_next() and fix assorted spelling mistakes" * tag 'vfs-7.2-rc1.misc' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs: (73 commits) backing-file: fix backing_file_open() kerneldoc parameter iomap: pass the correct len to fserror_report_io in __iomap_write_begin vfs: add FS_USERNS_DELEGATABLE flag and set it for NFS filelock: fix break_lease() stub signature for CONFIG_FILE_LOCKING=n vfs: uapi: retire octal and hex numbers in favor of (1 << n) for O_ flags bpf: add bpf_real_inode() kfunc fs/read_write: Do not export __kernel_write() to the entire world libfs: drop redundant SB_I_NOEXEC/SB_I_NODEV in init_pseudo() callers libfs: set SB_I_NOEXEC and SB_I_NODEV by default in init_pseudo() mount: honour SB_NOUSER in the new mount API fs/fcntl: fix SOFTIRQ-unsafe lock order in fasync signaling selftests/pipe: add pipe_bench microbenchmark fs/pipe: pre-allocate pages outside pipe->mutex in anon_pipe_write fs: retire stale comment in fget_task_next() fs: fix spelling mistakes in comment bfs: replace get_zeroed_page() with kzalloc() binfmt_misc: replace __get_free_page() with kmalloc() configfs: replace __get_free_pages() with kzalloc() fs/namespace: use __getname() to allocate mntpath buffer fs/select: replace __get_free_page() with kmalloc() ...
Diffstat (limited to 'tools')
-rw-r--r--tools/testing/selftests/Makefile1
-rw-r--r--tools/testing/selftests/namespaces/listns_efault_test.c33
-rw-r--r--tools/testing/selftests/namespaces/nsid_test.c14
-rw-r--r--tools/testing/selftests/pipe/.gitignore1
-rw-r--r--tools/testing/selftests/pipe/Makefile9
-rw-r--r--tools/testing/selftests/pipe/pipe_bench.c616
6 files changed, 667 insertions, 7 deletions
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 641a180fb35f..8d4db2241cc2 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -91,6 +91,7 @@ TARGETS += pcie_bwctrl
TARGETS += perf_events
TARGETS += pidfd
TARGETS += pid_namespace
+TARGETS += pipe
TARGETS += power_supply
TARGETS += powerpc
TARGETS += prctl
diff --git a/tools/testing/selftests/namespaces/listns_efault_test.c b/tools/testing/selftests/namespaces/listns_efault_test.c
index b570746e917c..26b452c98c66 100644
--- a/tools/testing/selftests/namespaces/listns_efault_test.c
+++ b/tools/testing/selftests/namespaces/listns_efault_test.c
@@ -38,7 +38,7 @@ TEST(listns_partial_fault_with_ns_cleanup)
__u64 *ns_ids;
ssize_t ret;
long page_size;
- pid_t pid, iter_pid;
+ pid_t pid, iter_pid, ns_pids[5];
int pidfds[5];
int sv[5][2];
int iter_pidfd;
@@ -114,6 +114,7 @@ TEST(listns_partial_fault_with_ns_cleanup)
pid = create_child(&pidfds[i], CLONE_NEWNS);
ASSERT_NE(pid, -1);
+ ns_pids[i] = pid;
if (pid == 0) {
close(sv[i][0]); /* Close parent end */
@@ -164,7 +165,7 @@ TEST(listns_partial_fault_with_ns_cleanup)
/* Wait for all mount namespace children to exit and cleanup */
for (i = 0; i < 5; i++) {
- waitpid(-1, NULL, 0);
+ waitpid(ns_pids[i], NULL, 0);
close(sv[i][0]);
close(pidfds[i]);
}
@@ -175,6 +176,12 @@ TEST(listns_partial_fault_with_ns_cleanup)
ASSERT_EQ(ret, iter_pid);
close(iter_pidfd);
+ /* If listns() is not supported the iterator exits cleanly via ENOSYS */
+ if (WIFEXITED(status) && WEXITSTATUS(status) == PIDFD_SKIP) {
+ munmap(map, page_size);
+ SKIP(return, "listns() not supported");
+ }
+
/* Should have been killed */
ASSERT_TRUE(WIFSIGNALED(status));
ASSERT_EQ(WTERMSIG(status), SIGKILL);
@@ -250,7 +257,7 @@ TEST(listns_late_fault_with_ns_cleanup)
__u64 *ns_ids;
ssize_t ret;
long page_size;
- pid_t pid, iter_pid;
+ pid_t pid, iter_pid, ns_pids[10];
int pidfds[10];
int sv[10][2];
int iter_pidfd;
@@ -320,6 +327,7 @@ TEST(listns_late_fault_with_ns_cleanup)
pid = create_child(&pidfds[i], CLONE_NEWNS);
ASSERT_NE(pid, -1);
+ ns_pids[i] = pid;
if (pid == 0) {
close(sv[i][0]); /* Close parent end */
@@ -373,7 +381,7 @@ TEST(listns_late_fault_with_ns_cleanup)
/* Wait for all children and cleanup */
for (i = 0; i < 10; i++) {
- waitpid(-1, NULL, 0);
+ waitpid(ns_pids[i], NULL, 0);
close(sv[i][0]);
close(pidfds[i]);
}
@@ -384,6 +392,12 @@ TEST(listns_late_fault_with_ns_cleanup)
ASSERT_EQ(ret, iter_pid);
close(iter_pidfd);
+ /* If listns() is not supported the iterator exits cleanly via ENOSYS */
+ if (WIFEXITED(status) && WEXITSTATUS(status) == PIDFD_SKIP) {
+ munmap(map, page_size);
+ SKIP(return, "listns() not supported");
+ }
+
/* Should have been killed */
ASSERT_TRUE(WIFSIGNALED(status));
ASSERT_EQ(WTERMSIG(status), SIGKILL);
@@ -402,7 +416,7 @@ TEST(listns_mnt_ns_cleanup_on_fault)
__u64 *ns_ids;
ssize_t ret;
long page_size;
- pid_t pid, iter_pid;
+ pid_t pid, iter_pid, ns_pids[8];
int pidfds[8];
int sv[8][2];
int iter_pidfd;
@@ -462,6 +476,7 @@ TEST(listns_mnt_ns_cleanup_on_fault)
pid = create_child(&pidfds[i], CLONE_NEWNS);
ASSERT_NE(pid, -1);
+ ns_pids[i] = pid;
if (pid == 0) {
close(sv[i][0]); /* Close parent end */
@@ -508,7 +523,7 @@ TEST(listns_mnt_ns_cleanup_on_fault)
/* Wait for children and cleanup */
for (i = 0; i < 8; i++) {
- waitpid(-1, NULL, 0);
+ waitpid(ns_pids[i], NULL, 0);
close(sv[i][0]);
close(pidfds[i]);
}
@@ -519,6 +534,12 @@ TEST(listns_mnt_ns_cleanup_on_fault)
ASSERT_EQ(ret, iter_pid);
close(iter_pidfd);
+ /* If listns() is not supported the iterator exits cleanly via ENOSYS */
+ if (WIFEXITED(status) && WEXITSTATUS(status) == PIDFD_SKIP) {
+ munmap(map, page_size);
+ SKIP(return, "listns() not supported");
+ }
+
/* Should have been killed */
ASSERT_TRUE(WIFSIGNALED(status));
ASSERT_EQ(WTERMSIG(status), SIGKILL);
diff --git a/tools/testing/selftests/namespaces/nsid_test.c b/tools/testing/selftests/namespaces/nsid_test.c
index b4a14c6693a5..46dc838cba82 100644
--- a/tools/testing/selftests/namespaces/nsid_test.c
+++ b/tools/testing/selftests/namespaces/nsid_test.c
@@ -25,14 +25,24 @@
/* Fixture for tests that create child processes */
FIXTURE(nsid) {
pid_t child_pid;
+ pid_t grandchild_pid;
};
FIXTURE_SETUP(nsid) {
self->child_pid = 0;
+ self->grandchild_pid = 0;
}
FIXTURE_TEARDOWN(nsid) {
- /* Clean up any child process that may still be running */
+ /*
+ * Kill grandchild first: timens_separate and pidns_separate fork a
+ * grandchild that calls pause(). It is reparented to init on child
+ * exit and keeps the test runner's tap pipe open, hanging the runner.
+ */
+ if (self->grandchild_pid > 0) {
+ kill(self->grandchild_pid, SIGKILL);
+ waitpid(self->grandchild_pid, NULL, 0);
+ }
if (self->child_pid > 0) {
kill(self->child_pid, SIGKILL);
waitpid(self->child_pid, NULL, 0);
@@ -676,6 +686,7 @@ TEST_F(nsid, timens_separate)
pid_t grandchild_pid;
ASSERT_EQ(read(pipefd[0], &grandchild_pid, sizeof(grandchild_pid)), sizeof(grandchild_pid));
+ self->grandchild_pid = grandchild_pid;
close(pipefd[0]);
/* Open grandchild's time namespace */
@@ -797,6 +808,7 @@ TEST_F(nsid, pidns_separate)
pid_t grandchild_pid;
ASSERT_EQ(read(pipefd[0], &grandchild_pid, sizeof(grandchild_pid)), sizeof(grandchild_pid));
+ self->grandchild_pid = grandchild_pid;
close(pipefd[0]);
/* Open grandchild's PID namespace */
diff --git a/tools/testing/selftests/pipe/.gitignore b/tools/testing/selftests/pipe/.gitignore
new file mode 100644
index 000000000000..20b549361a15
--- /dev/null
+++ b/tools/testing/selftests/pipe/.gitignore
@@ -0,0 +1 @@
+pipe_bench
diff --git a/tools/testing/selftests/pipe/Makefile b/tools/testing/selftests/pipe/Makefile
new file mode 100644
index 000000000000..1810c680117b
--- /dev/null
+++ b/tools/testing/selftests/pipe/Makefile
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Meta Platforms, Inc. and affiliates
+# Copyright (c) 2026 Breno Leitao <leitao@debian.org>
+
+CFLAGS += -O2 -Wall -Wextra -pthread
+
+TEST_GEN_PROGS := pipe_bench
+
+include ../lib.mk
diff --git a/tools/testing/selftests/pipe/pipe_bench.c b/tools/testing/selftests/pipe/pipe_bench.c
new file mode 100644
index 000000000000..7e96429b8fb4
--- /dev/null
+++ b/tools/testing/selftests/pipe/pipe_bench.c
@@ -0,0 +1,616 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * pipe_bench - exercise concurrent pipe operation
+ *
+ * N writer threads hammer a single pipe with multi-page writes; M reader
+ * threads drain it. Each writer records its own write() latency histogram.
+ * Multi-page writes (msgsize >= PAGE_SIZE) force the loop in
+ * anon_pipe_write() to call alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT) under
+ * pipe->mutex, which is the critical section the patch shrinks.
+ *
+ * By default the benchmark sweeps writers in {1, 2, 5} x readers in
+ * {1, 5, 10} and prints one block per configuration so two runs (e.g.
+ * baseline vs patched) can be diffed directly. Pass -w and -r to run a
+ * single configuration instead. Pass --memory-pressure to spawn stress-ng
+ * alongside the sweep so the per-page alloc_page() path under pipe->mutex
+ * has to dip into reclaim.
+ *
+ * Copyright (c) 2026 Meta Platforms, Inc. and affiliates
+ * Copyright (c) 2026 Breno Leitao <leitao@debian.org>
+ */
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
+#define HIST_BUCKETS 32
+
+static size_t g_msgsize = 16 * 4096;
+static int g_duration = 3;
+static int g_pipe_size = 1024 * 1024;
+static int g_memory_pressure;
+
+static atomic_int g_stop;
+static int g_pipe[2];
+
+struct wstats {
+ uint64_t writes;
+ uint64_t bytes;
+ uint64_t lat_sum_ns;
+ uint64_t lat_max_ns;
+ uint64_t lat_hist[HIST_BUCKETS];
+ char *buf;
+};
+
+struct rstats {
+ char *buf;
+};
+
+struct hist_totals {
+ uint64_t writes;
+ uint64_t bytes;
+ uint64_t lat_sum;
+ uint64_t lat_max;
+};
+
+static inline uint64_t now_ns(void)
+{
+ struct timespec ts;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
+}
+
+static inline int log2_bucket(uint64_t v)
+{
+ int b = 0;
+
+ if (!v)
+ return 0;
+ while (v >>= 1)
+ b++;
+ return b < HIST_BUCKETS ? b : HIST_BUCKETS - 1;
+}
+
+static void *writer(void *arg)
+{
+ struct wstats *s = arg;
+
+ while (!atomic_load_explicit(&g_stop, memory_order_relaxed)) {
+ uint64_t t0 = now_ns();
+ ssize_t n = write(g_pipe[1], s->buf, g_msgsize);
+ uint64_t dt = now_ns() - t0;
+
+ if (n > 0) {
+ s->writes++;
+ s->bytes += (uint64_t)n;
+ s->lat_sum_ns += dt;
+ if (dt > s->lat_max_ns)
+ s->lat_max_ns = dt;
+ s->lat_hist[log2_bucket(dt)]++;
+ } else if (n < 0 && (errno == EPIPE || errno == EBADF)) {
+ break;
+ }
+ }
+ return NULL;
+}
+
+static void *reader(void *arg)
+{
+ struct rstats *s = arg;
+
+ /*
+ * Drain until EOF (write end closed by main). g_stop is not checked
+ * here on purpose: writers may be blocked in write() with the pipe
+ * full when g_stop is set, so the reader must keep draining until
+ * main closes the write end.
+ */
+ for (;;) {
+ ssize_t n = read(g_pipe[0], s->buf, g_msgsize);
+
+ if (n <= 0)
+ break;
+ }
+ return NULL;
+}
+
+/* Sum per-writer stats and per-bucket counts into the caller's aggregates. */
+static void aggregate_wstats(struct wstats *all, int nw,
+ uint64_t agg[HIST_BUCKETS],
+ struct hist_totals *t)
+{
+ memset(t, 0, sizeof(*t));
+ for (int i = 0; i < nw; i++) {
+ t->writes += all[i].writes;
+ t->bytes += all[i].bytes;
+ t->lat_sum += all[i].lat_sum_ns;
+ if (all[i].lat_max_ns > t->lat_max)
+ t->lat_max = all[i].lat_max_ns;
+ for (int b = 0; b < HIST_BUCKETS; b++)
+ agg[b] += all[i].lat_hist[b];
+ }
+}
+
+/*
+ * Walk @agg in order, returning the inclusive upper bound (in ns) of the
+ * log2 bucket where the running sum first reaches @target.
+ *
+ * A percentile is undefined with zero samples, and with very low sample
+ * counts integer truncation could make @target zero -- then "cum >= 0"
+ * would latch on the first (possibly empty) bucket. Callers must pass
+ * @target >= 1.
+ */
+static uint64_t bucket_at(const uint64_t agg[HIST_BUCKETS], uint64_t target)
+{
+ uint64_t cum = 0;
+
+ for (int b = 0; b < HIST_BUCKETS; b++) {
+ /* HIST_BUCKETS <= 63, so (b + 1) is always a safe shift. */
+ uint64_t upper = (1ULL << (b + 1)) - 1;
+
+ cum += agg[b];
+ if (cum >= target)
+ return upper;
+ }
+ return 0;
+}
+
+static void compute_p50_p99(const uint64_t agg[HIST_BUCKETS], uint64_t writes,
+ uint64_t *p50, uint64_t *p99)
+{
+ uint64_t p50_target, p99_target;
+
+ *p50 = *p99 = 0;
+ if (!writes)
+ return;
+
+ p50_target = writes * 50 / 100;
+ p99_target = writes * 99 / 100;
+ if (!p50_target)
+ p50_target = 1;
+ if (!p99_target)
+ p99_target = 1;
+
+ *p50 = bucket_at(agg, p50_target);
+ *p99 = bucket_at(agg, p99_target);
+}
+
+static void print_summary(int nw, int nr, const struct hist_totals *t,
+ uint64_t p50, uint64_t p99)
+{
+ double sec = g_duration;
+ uint64_t avg_ns = t->writes ? t->lat_sum / t->writes : 0;
+
+ printf("config: writers=%d readers=%d msgsize=%zu duration=%d pipe_size=%d memory_pressure=%s\n",
+ nw, nr, g_msgsize, g_duration, g_pipe_size,
+ g_memory_pressure ? "yes" : "no");
+ printf("writes: total=%llu rate=%.0f/s\n",
+ (unsigned long long)t->writes, (double)t->writes / sec);
+ printf("throughput_MBps: %.2f\n",
+ ((double)t->bytes / sec) / (1024.0 * 1024.0));
+ printf("lat_avg_ns: %llu\n", (unsigned long long)avg_ns);
+ printf("lat_p50_ns_upper: %llu\n", (unsigned long long)p50);
+ printf("lat_p99_ns_upper: %llu\n", (unsigned long long)p99);
+ printf("lat_max_ns: %llu\n", (unsigned long long)t->lat_max);
+}
+
+static void summarize(struct wstats *all, int nw, int nr)
+{
+ uint64_t agg[HIST_BUCKETS] = {0};
+ struct hist_totals t;
+ uint64_t p50, p99;
+
+ aggregate_wstats(all, nw, agg, &t);
+ compute_p50_p99(agg, t.writes, &p50, &p99);
+ print_summary(nw, nr, &t, p50, p99);
+}
+
+/*
+ * Child branch of fork(): restore SIGPIPE to default (parent ignores it),
+ * exec stress-ng, and on failure write the reason into @hs_wr before
+ * exiting. The parent observes EOF on hs_wr (closed via O_CLOEXEC) when
+ * exec succeeds.
+ */
+static void stress_ng_child(int hs_wr) __attribute__((noreturn));
+static void stress_ng_child(int hs_wr)
+{
+ char errbuf[256];
+
+ signal(SIGPIPE, SIG_DFL);
+ execlp("stress-ng", "stress-ng",
+ "--vm", "4", "--vm-bytes", "80%",
+ "--vm-method", "all",
+ (char *)NULL);
+ snprintf(errbuf, sizeof(errbuf),
+ "exec stress-ng failed: %s\n", strerror(errno));
+ (void)!write(hs_wr, errbuf, strlen(errbuf));
+ _exit(127);
+}
+
+/*
+ * Read from the O_CLOEXEC handshake pipe. Anything readable means the
+ * child wrote an error before exec; EOF (n == 0) means the write-end
+ * closed because exec succeeded. Returns 0 on exec success, -1 if the
+ * child failed and was reaped.
+ */
+static int stress_ng_wait_handshake(int hs_rd, pid_t pid)
+{
+ struct pollfd pfd = { .fd = hs_rd, .events = POLLIN };
+ char errbuf[256];
+ int status;
+ int ret;
+
+ ret = poll(&pfd, 1, 500);
+ if (ret <= 0)
+ return 0;
+
+ ssize_t n = read(hs_rd, errbuf, sizeof(errbuf) - 1);
+
+ if (n > 0) {
+ errbuf[n] = '\0';
+ fputs(errbuf, stderr);
+ waitpid(pid, &status, 0);
+ return -1;
+ }
+ return 0;
+}
+
+static pid_t spawn_stress_ng(void)
+{
+ int hs[2];
+ pid_t pid;
+
+ /*
+ * Handshake pipe: child writes one byte and _exit()s on exec
+ * failure. On exec success the O_CLOEXEC flag closes the write
+ * end, which the parent observes as EOF. This makes the "is
+ * stress-ng on $PATH?" check fail fast rather than silently.
+ */
+ if (pipe2(hs, O_CLOEXEC) < 0) {
+ perror("pipe2");
+ return -1;
+ }
+
+ pid = fork();
+ if (pid < 0) {
+ perror("fork");
+ close(hs[0]);
+ close(hs[1]);
+ return -1;
+ }
+ if (pid == 0) {
+ close(hs[0]);
+ stress_ng_child(hs[1]);
+ }
+
+ close(hs[1]);
+ if (stress_ng_wait_handshake(hs[0], pid) < 0) {
+ close(hs[0]);
+ return -1;
+ }
+ close(hs[0]);
+
+ /* Give stress-ng a moment to map its VM regions before measuring. */
+ sleep(1);
+ return pid;
+}
+
+static void kill_stress_ng(pid_t pid)
+{
+ int status;
+
+ if (pid <= 0)
+ return;
+ kill(pid, SIGTERM);
+ for (int i = 0; i < 20; i++) {
+ if (waitpid(pid, &status, WNOHANG) > 0)
+ return;
+ usleep(100 * 1000);
+ }
+ kill(pid, SIGKILL);
+ waitpid(pid, &status, 0);
+}
+
+/*
+ * Allocate per-thread page-aligned buffers in main so a failed
+ * aligned_alloc() aborts the run before any thread starts. Workers used
+ * to allocate their own buffer and return NULL on failure, which left
+ * peers blocked in write()/read() with nobody to unblock them.
+ */
+static int alloc_thread_bufs(struct wstats *ws, int nw,
+ struct rstats *rs, int nr)
+{
+ for (int i = 0; i < nw; i++) {
+ ws[i].buf = aligned_alloc(4096, g_msgsize);
+ if (!ws[i].buf) {
+ fprintf(stderr, "writer %d: aligned_alloc(%zu) failed\n",
+ i, g_msgsize);
+ return -1;
+ }
+ memset(ws[i].buf, 0xAA, g_msgsize);
+ }
+ for (int i = 0; i < nr; i++) {
+ rs[i].buf = aligned_alloc(4096, g_msgsize);
+ if (!rs[i].buf) {
+ fprintf(stderr, "reader %d: aligned_alloc(%zu) failed\n",
+ i, g_msgsize);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static void free_thread_bufs(struct wstats *ws, int nw,
+ struct rstats *rs, int nr)
+{
+ if (ws)
+ for (int i = 0; i < nw; i++)
+ free(ws[i].buf);
+ if (rs)
+ for (int i = 0; i < nr; i++)
+ free(rs[i].buf);
+}
+
+static int start_readers(pthread_t *rt, struct rstats *rs, int nr,
+ int *created)
+{
+ for (int i = 0; i < nr; i++) {
+ int err = pthread_create(&rt[i], NULL, reader, &rs[i]);
+
+ if (err) {
+ fprintf(stderr, "pthread_create reader %d: %s\n",
+ i, strerror(err));
+ return -1;
+ }
+ (*created)++;
+ }
+ return 0;
+}
+
+static int start_writers(pthread_t *wt, struct wstats *ws, int nw,
+ int *created)
+{
+ for (int i = 0; i < nw; i++) {
+ int err = pthread_create(&wt[i], NULL, writer, &ws[i]);
+
+ if (err) {
+ fprintf(stderr, "pthread_create writer %d: %s\n",
+ i, strerror(err));
+ return -1;
+ }
+ (*created)++;
+ }
+ return 0;
+}
+
+static int open_bench_pipe(void)
+{
+ if (pipe(g_pipe) < 0) {
+ perror("pipe");
+ return -1;
+ }
+ if (fcntl(g_pipe[1], F_SETPIPE_SZ, g_pipe_size) < 0)
+ perror("F_SETPIPE_SZ (continuing)");
+ return 0;
+}
+
+/*
+ * Normal termination: g_stop tells writers to leave the loop after the
+ * current write() returns. Closing the shared write-end fd means once
+ * the in-flight writes drain, readers see EOF and exit. Writers are not
+ * unblocked by EPIPE here -- g_pipe[0] stays open so readers can keep
+ * draining.
+ *
+ * Error path: some threads may have been created and others skipped, so
+ * writers could be blocked in write() with no reader making progress.
+ * Close both ends -- closing the read end is what delivers EPIPE to a
+ * blocked writer.
+ */
+static void stop_and_join(pthread_t *wt, int nw_created,
+ pthread_t *rt, int nr_created, int rc)
+{
+ atomic_store(&g_stop, 1);
+ close(g_pipe[1]);
+ if (rc < 0)
+ close(g_pipe[0]);
+ for (int i = 0; i < nw_created; i++)
+ pthread_join(wt[i], NULL);
+ for (int i = 0; i < nr_created; i++)
+ pthread_join(rt[i], NULL);
+ if (rc == 0)
+ close(g_pipe[0]);
+}
+
+static int run_one(int nw, int nr)
+{
+ pthread_t *wt = NULL, *rt = NULL;
+ struct wstats *ws = NULL;
+ struct rstats *rs = NULL;
+ int nw_created = 0, nr_created = 0;
+ int rc = 0;
+
+ atomic_store(&g_stop, 0);
+
+ if (open_bench_pipe() < 0)
+ return -1;
+
+ wt = calloc((size_t)nw, sizeof(*wt));
+ rt = calloc((size_t)nr, sizeof(*rt));
+ ws = calloc((size_t)nw, sizeof(*ws));
+ rs = calloc((size_t)nr, sizeof(*rs));
+ if (!wt || !rt || !ws || !rs) {
+ fprintf(stderr, "alloc failed\n");
+ rc = -1;
+ goto teardown;
+ }
+
+ if (alloc_thread_bufs(ws, nw, rs, nr) < 0) {
+ rc = -1;
+ goto teardown;
+ }
+
+ if (start_readers(rt, rs, nr, &nr_created) < 0 ||
+ start_writers(wt, ws, nw, &nw_created) < 0) {
+ rc = -1;
+ goto teardown;
+ }
+
+ sleep((unsigned int)g_duration);
+
+teardown:
+ stop_and_join(wt, nw_created, rt, nr_created, rc);
+
+ if (rc == 0) {
+ summarize(ws, nw, nr);
+ fflush(stdout);
+ }
+
+ free_thread_bufs(ws, nw, rs, nr);
+ free(wt);
+ free(rt);
+ free(ws);
+ free(rs);
+ return rc;
+}
+
+static void usage(const char *prog)
+{
+ fprintf(stderr,
+ "usage: %s [-w writers] [-r readers] [-s msgsize] [-d secs] [-p pipe_size] [--memory-pressure]\n"
+ " default: sweep writers={1,2,5} x readers={1,5,10}\n"
+ " --memory-pressure: spawn stress-ng (--vm 4 --vm-bytes 80%% --vm-method all) for the run\n",
+ prog);
+}
+
+static int parse_args(int argc, char **argv,
+ int *writers_override, int *readers_override)
+{
+ static const struct option long_opts[] = {
+ {"memory-pressure", no_argument, NULL, 'M'},
+ {0, 0, 0, 0},
+ };
+ int opt;
+
+ while ((opt = getopt_long(argc, argv, "w:r:s:d:p:",
+ long_opts, NULL)) != -1) {
+ switch (opt) {
+ case 'w':
+ *writers_override = atoi(optarg);
+ break;
+ case 'r':
+ *readers_override = atoi(optarg);
+ break;
+ case 's':
+ g_msgsize = (size_t)atol(optarg);
+ break;
+ case 'd':
+ g_duration = atoi(optarg);
+ break;
+ case 'p':
+ g_pipe_size = atoi(optarg);
+ break;
+ case 'M':
+ g_memory_pressure = 1;
+ break;
+ default:
+ usage(argv[0]);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+/*
+ * aligned_alloc(4096, size) requires size to be a multiple of the
+ * alignment (C11); glibc returns NULL otherwise, which would make
+ * writer/reader threads silently exit and the run report zero writes.
+ * Validate up front instead.
+ */
+static int validate_args(void)
+{
+ if (g_msgsize == 0 || g_msgsize % 4096 != 0) {
+ fprintf(stderr,
+ "msgsize must be a positive multiple of 4096 (got %zu)\n",
+ g_msgsize);
+ return -1;
+ }
+ if (g_duration <= 0) {
+ fprintf(stderr, "duration must be > 0 seconds (got %d)\n",
+ g_duration);
+ return -1;
+ }
+ if (g_pipe_size <= 0) {
+ fprintf(stderr, "pipe_size must be > 0 bytes (got %d)\n",
+ g_pipe_size);
+ return -1;
+ }
+ return 0;
+}
+
+static int run_sweep(void)
+{
+ static const int writers_sweep[] = {1, 2, 5};
+ static const int readers_sweep[] = {1, 5, 10};
+
+ for (size_t i = 0; i < ARRAY_SIZE(writers_sweep); i++) {
+ for (size_t j = 0; j < ARRAY_SIZE(readers_sweep); j++) {
+ printf("---\n");
+ if (run_one(writers_sweep[i], readers_sweep[j]) < 0)
+ return -1;
+ }
+ }
+ return 0;
+}
+
+int main(int argc, char **argv)
+{
+ int writers_override = 0, readers_override = 0;
+ pid_t stress_pid = -1;
+ int rc = 0;
+
+ if (parse_args(argc, argv, &writers_override, &readers_override) < 0)
+ return 1;
+ if (validate_args() < 0)
+ return 1;
+
+ signal(SIGPIPE, SIG_IGN);
+ setvbuf(stdout, NULL, _IOLBF, 0);
+ setvbuf(stderr, NULL, _IOLBF, 0);
+
+ fprintf(stderr, "pid=%d\n", getpid());
+ fflush(stderr);
+
+ if (g_memory_pressure) {
+ stress_pid = spawn_stress_ng();
+ if (stress_pid < 0) {
+ fprintf(stderr,
+ "memory_pressure requested but stress-ng could not be spawned\n");
+ return 1;
+ }
+ }
+
+ if (writers_override > 0 || readers_override > 0) {
+ int nw = writers_override > 0 ? writers_override : 1;
+ int nr = readers_override > 0 ? readers_override : 1;
+
+ rc = run_one(nw, nr) < 0 ? 1 : 0;
+ } else {
+ rc = run_sweep() < 0 ? 1 : 0;
+ }
+
+ kill_stress_ng(stress_pid);
+ return rc;
+}