From 97fe86837c9a75048494273ed7ec7272e7f76730 Mon Sep 17 00:00:00 2001 From: Rob Norris Date: Fri, 1 Aug 2025 13:51:46 +1000 Subject: [PATCH] ZTS: mmap_ftruncate test to confirm async writeback behaviour Sponsored-by: Klara, Inc. Sponsored-by: Wasabi Technology, Inc. Reviewed-by: Brian Behlendorf Reviewed-by: Alexander Motin Signed-off-by: Rob Norris Closes #17584 --- tests/runfiles/common.run | 2 +- tests/zfs-tests/cmd/.gitignore | 1 + tests/zfs-tests/cmd/Makefile.am | 4 +- tests/zfs-tests/cmd/mmap_ftruncate.c | 85 +++++++++++++++++++ tests/zfs-tests/include/commands.cfg | 1 + tests/zfs-tests/tests/Makefile.am | 1 + .../tests/functional/mmap/mmap_ftruncate.ksh | 80 +++++++++++++++++ 7 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 tests/zfs-tests/cmd/mmap_ftruncate.c create mode 100755 tests/zfs-tests/tests/functional/mmap/mmap_ftruncate.ksh diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run index bbe17b073..6d96ec435 100644 --- a/tests/runfiles/common.run +++ b/tests/runfiles/common.run @@ -788,7 +788,7 @@ tags = ['functional', 'migration'] [tests/functional/mmap] tests = ['mmap_mixed', 'mmap_read_001_pos', 'mmap_seek_001_pos', - 'mmap_sync_001_pos', 'mmap_write_001_pos'] + 'mmap_sync_001_pos', 'mmap_write_001_pos', 'mmap_ftruncate'] tags = ['functional', 'mmap'] [tests/functional/mount] diff --git a/tests/zfs-tests/cmd/.gitignore b/tests/zfs-tests/cmd/.gitignore index e9a6f8f0a..1cd90024e 100644 --- a/tests/zfs-tests/cmd/.gitignore +++ b/tests/zfs-tests/cmd/.gitignore @@ -23,6 +23,7 @@ /mkfiles /mktree /mmap_exec +/mmap_ftruncate /mmap_libaio /mmap_seek /mmap_sync diff --git a/tests/zfs-tests/cmd/Makefile.am b/tests/zfs-tests/cmd/Makefile.am index 4498c9a73..12107278c 100644 --- a/tests/zfs-tests/cmd/Makefile.am +++ b/tests/zfs-tests/cmd/Makefile.am @@ -72,7 +72,9 @@ scripts_zfs_tests_bin_PROGRAMS += %D%/mkbusy %D%/mkfile %D%/mkfiles %D%/mktree %C%_mkfile_LDADD = $(LTLIBINTL) -scripts_zfs_tests_bin_PROGRAMS += %D%/mmap_exec %D%/mmap_seek %D%/mmap_sync %D%/mmapwrite %D%/readmmap +scripts_zfs_tests_bin_PROGRAMS += \ + %D%/mmap_exec %D%/mmap_ftruncate %D%/mmap_seek \ + %D%/mmap_sync %D%/mmapwrite %D%/readmmap %C%_mmapwrite_LDADD = -lpthread if WANT_MMAP_LIBAIO diff --git a/tests/zfs-tests/cmd/mmap_ftruncate.c b/tests/zfs-tests/cmd/mmap_ftruncate.c new file mode 100644 index 000000000..91cdfe371 --- /dev/null +++ b/tests/zfs-tests/cmd/mmap_ftruncate.c @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: CDDL-1.0 +/* + * CDDL HEADER START + * + * The contents of this file are subject to the terms of the + * Common Development and Distribution License (the "License"). + * You may not use this file except in compliance with the License. + * + * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE + * or http://opensource.org/licenses/CDDL-1.0. + * See the License for the specific language governing permissions + * and limitations under the License. + * + * When distributing Covered Code, include this CDDL HEADER in each + * file and include the License file at usr/src/OPENSOLARIS.LICENSE. + * If applicable, add the following below this CDDL HEADER, with the + * fields enclosed by brackets "[]" replaced with your own identifying + * information: Portions Copyright [yyyy] [name of copyright owner] + * + * CDDL HEADER END + */ + +/* + * Copyright (c) 2025, Klara, Inc. + */ + +/* + * Tests async writeback behaviour. Creates a file, maps it into memory, and + * dirties every page within it. Then, calls ftruncate() to collapse the file + * back down to 0. This causes the kernel to begin writeback on the dirty + * pages so they can be freed, before it can complete the ftruncate() call. + * None of these are sync operations, so they should avoid the various "force + * flush" codepaths. + */ + +#include +#include +#include +#include +#include +#include + +#define _pdfail(f, l, s) \ + do { perror("[" f "#" #l "] " s); exit(2); } while (0) +#define pdfail(str) _pdfail(__FILE__, __LINE__, str) + +int +main(int argc, char **argv) { + if (argc != 3) { + printf("usage: mmap_ftruncate \n"); + exit(2); + } + + const char *file = argv[1]; + + char *end; + off_t sz = strtoull(argv[2], &end, 0); + if (end == argv[2] || *end != '\0' || sz == 0) { + fprintf(stderr, "E: invalid size"); + exit(2); + } + + int fd = open(file, O_CREAT|O_TRUNC|O_RDWR, S_IRUSR|S_IWUSR); + if (fd < 0) + pdfail("open"); + + if (ftruncate(fd, sz) < 0) + pdfail("ftruncate"); + + char *p = mmap(NULL, sz, PROT_WRITE, MAP_SHARED, fd, 0); + if (p == MAP_FAILED) + pdfail("mmap"); + + for (off_t off = 0; off < sz; off += 4096) + p[off] = 1; + + if (ftruncate(fd, 0) < 0) + pdfail("ftruncate"); + + if (munmap(p, sz) < 0) + pdfail("munmap"); + + close(fd); + return (0); +} diff --git a/tests/zfs-tests/include/commands.cfg b/tests/zfs-tests/include/commands.cfg index 1c7e42a06..bbaa8665e 100644 --- a/tests/zfs-tests/include/commands.cfg +++ b/tests/zfs-tests/include/commands.cfg @@ -205,6 +205,7 @@ export ZFSTEST_FILES='badsend mkfiles mktree mmap_exec + mmap_ftruncate mmap_libaio mmap_seek mmap_sync diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am index dc33258b1..d9ea50ebe 100644 --- a/tests/zfs-tests/tests/Makefile.am +++ b/tests/zfs-tests/tests/Makefile.am @@ -1660,6 +1660,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \ functional/mmap/mmap_seek_001_pos.ksh \ functional/mmap/mmap_sync_001_pos.ksh \ functional/mmap/mmap_write_001_pos.ksh \ + functional/mmap/mmap_ftruncate.ksh \ functional/mmap/setup.ksh \ functional/mmp/cleanup.ksh \ functional/mmp/mmp_active_import.ksh \ diff --git a/tests/zfs-tests/tests/functional/mmap/mmap_ftruncate.ksh b/tests/zfs-tests/tests/functional/mmap/mmap_ftruncate.ksh new file mode 100755 index 000000000..63ebf95de --- /dev/null +++ b/tests/zfs-tests/tests/functional/mmap/mmap_ftruncate.ksh @@ -0,0 +1,80 @@ +#!/bin/ksh -p +# SPDX-License-Identifier: CDDL-1.0 +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or https://opensource.org/licenses/CDDL-1.0. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +# +# Copyright (c) 2025, Klara, Inc. +# + +. $STF_SUITE/include/libtest.shlib + +# +# This verifies that async writeback of dirty mmap()'d pages completes quickly. +# ftruncate() is an operation that will trigger async writeback, but is not +# itself a syncing operation, making it a useful proxy for any way the kernel +# might trigger async writeback. +# +# The guts of this test is in the mmap_ftruncate program. This driver sets a +# larger zfs_txg_timeout. Test failure occurs ftruncate() blocks waiting for +# the writeback until the txg timeout is reached and the changes are forcibly +# written out. Success means the DMU has accepted the changes and cleared the +# page dirty flags. +# + +TIMEOUT=180 +TESTFILE=/$TESTPOOL/truncfile +TESTSIZE=$((2*1024*1024*1024)) # 2G + +verify_runnable "global" + +typeset claim="async writeback of dirty mmap()'d pages completes quickly" + +log_assert $claim + +log_must save_tunable TXG_TIMEOUT + +function cleanup +{ + log_must restore_tunable TXG_TIMEOUT + rm -f $TESTFILE +} +log_onexit cleanup + +log_must set_tunable32 TXG_TIMEOUT $TIMEOUT +log_must zpool sync -f + +# run mmap_ftruncate and record the run time +typeset -i start=$(date +%s) +log_must mmap_ftruncate $TESTFILE $TESTSIZE +typeset -i end=$(date +%s) +typeset -i delta=$((end - start)) + +# in practice, mmap_ftruncate needs a few seconds to dirty all the pages, and +# when this test passes, the ftruncate() call itself should be near-instant. +# when it fails, then its only the txg sync that allows ftruncate() to +# complete, in that case, the run time will be extremely close to the timeout, +# so to avoid any confusion at the edges, we require that it complets within +# half the transaction time. for any timeout higher than ~30s that should be a +# very bright line down the middle. +log_must test $delta -lt $((TIMEOUT / 2)) + +log_pass $claim