From: Gao Xiang <xi...@kernel.org> Just import it as an in-tree component for tests.
Signed-off-by: Gao Xiang <xi...@kernel.org> --- changes since v1: - Limit to the linux platform for now. Makefile.am | 1 + configure.ac | 15 +- contrib/Makefile.am | 11 + contrib/stress.c | 776 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 802 insertions(+), 1 deletion(-) create mode 100644 contrib/Makefile.am create mode 100644 contrib/stress.c diff --git a/Makefile.am b/Makefile.am index fc464e8..f8a967f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -6,3 +6,4 @@ SUBDIRS = man lib mkfs dump fsck if ENABLE_FUSE SUBDIRS += fuse endif +SUBDIRS += contrib diff --git a/configure.ac b/configure.ac index 0a069c5..c922f73 100644 --- a/configure.ac +++ b/configure.ac @@ -20,6 +20,18 @@ AC_PROG_INSTALL LT_INIT +AC_CANONICAL_TARGET + +build_linux=no +case "${target_os}" in + linux*) + build_linux=yes + ;; +esac + +# OS-specific treatment +AM_CONDITIONAL([OS_LINUX], [test "$build_linux" = "yes"]) + # Test presence of pkg-config AC_MSG_CHECKING([pkg-config m4 macros]) if test m4_ifdef([PKG_CHECK_MODULES], [yes], [no]) = "yes"; then @@ -647,5 +659,6 @@ AC_CONFIG_FILES([Makefile mkfs/Makefile dump/Makefile fuse/Makefile - fsck/Makefile]) + fsck/Makefile + contrib/Makefile]) AC_OUTPUT diff --git a/contrib/Makefile.am b/contrib/Makefile.am new file mode 100644 index 0000000..4eb7abe --- /dev/null +++ b/contrib/Makefile.am @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Makefile.am + +AUTOMAKE_OPTIONS = foreign + +if OS_LINUX +noinst_PROGRAMS = stress + +stress_CFLAGS = -Wall -I$(top_srcdir)/include +stress_SOURCES = stress.c +endif diff --git a/contrib/stress.c b/contrib/stress.c new file mode 100644 index 0000000..eedd9b9 --- /dev/null +++ b/contrib/stress.c @@ -0,0 +1,776 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * stress test for EROFS filesystem + * + * Copyright (C) 2019-2025 Gao Xiang <xi...@kernel.org> + */ +#define _GNU_SOURCE +#include "erofs/defs.h" +#include <errno.h> +#include <sched.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <dirent.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <time.h> +#include <unistd.h> +#include <fcntl.h> + +#define MAX_CHUNKSIZE (2 * 1024 * 1024) +#define MAX_SCAN_CHUNKSIZE (256 * 1024) + +bool superuser; +unsigned int nprocs = 1, loops = 1, r_seed; +unsigned int procid; +volatile sig_atomic_t should_stop; + +enum { + DROP_PAGE_CACHE, + DROP_SLAB_CACHE, + COMPACT_MEMORY, +}; + +enum { + OP_GETDENTS, + OP_READLINK, + OP_SEQREAD_ALIGNED, + OP_SEQREAD_UNALIGNED, + OP_READ, + OP_FADVISE, + OP_DROP_CACHES, +}; + +struct opdesc { + char *name; + int (*func)(int op, unsigned int sn); + int freq; + bool requireroot; +}; + +extern struct opdesc ops[]; + +static int drop_caches_f(int op, unsigned int sn) +{ + static const char *procfile[] = { + [DROP_PAGE_CACHE] = "/proc/sys/vm/drop_caches", + [DROP_SLAB_CACHE] = "/proc/sys/vm/drop_caches", + [COMPACT_MEMORY] = "/proc/sys/vm/compact_memory", + }; + static const char *val[] = { + [DROP_PAGE_CACHE] = "1\n", + [DROP_SLAB_CACHE] = "2\n", + [COMPACT_MEMORY] = "1\n", + }; + int mode = random() % ARRAY_SIZE(val); + FILE *f; + clock_t start; + + if (!procfile[mode]) + return -EINVAL; + + printf("%d[%u]/%u %s: %s=%s", getpid(), procid, sn, __func__, + procfile[mode], val[mode]); + + f = fopen(procfile[mode], "w"); + if (!f) + return -errno; + + start = clock(); + while (clock() < start + CLOCKS_PER_SEC) { + fputs(val[mode], f); + (void)sched_yield(); + } + fclose(f); + return 0; +} + +struct fent { + char *subpath; + int fd, chkfd; +}; + +#define FT_DIR 0 +#define FT_DIRm (1 << FT_DIR) +#define FT_REG 1 +#define FT_REGm (1 << FT_REG) +#define FT_SYM 2 +#define FT_SYMm (1 << FT_SYM) +#define FT_DEV 3 +#define FT_DEVm (1 << FT_DEV) +#define FT_nft 4 +#define FT_ANYm ((1 << FT_nft) - 1) + +#define FLIST_SLOT_INCR 16 + +struct flist { + int nfiles, nslots; + struct fent *fents; +} flists[FT_nft]; + +static struct fent *add_to_flist(int type, char *subpath) +{ + struct fent *fep; + struct flist *ftp; + + ftp = &flists[type]; + if (ftp->nfiles >= ftp->nslots) { + ftp->nslots += FLIST_SLOT_INCR; + ftp->fents = realloc(ftp->fents, + ftp->nslots * sizeof(struct fent)); + if (!ftp->fents) + return NULL; + } + fep = &ftp->fents[ftp->nfiles++]; + fep->subpath = strdup(subpath); + fep->fd = -1; + fep->chkfd = -1; + return fep; +} + +static inline bool is_dot_dotdot(const char *name) +{ + if (name[0] != '.') + return false; + + return name[1] == '\0' || (name[1] == '.' && name[2] == '\0'); +} + +static int walkdir(struct fent *ent) +{ + const char *dirpath = ent->subpath; + int ret = 0; + struct dirent *dp; + DIR *_dir; + + _dir = opendir(dirpath); + if (!_dir) { + fprintf(stderr, "failed to opendir at %s: %s\n", + dirpath, strerror(errno)); + return -errno; + } + + while (1) { + char subpath[PATH_MAX]; + struct stat st; + + /* + * set errno to 0 before calling readdir() in order to + * distinguish end of stream and from an error. + */ + errno = 0; + dp = readdir(_dir); + if (!dp) + break; + + if (is_dot_dotdot(dp->d_name)) + continue; + + sprintf(subpath, "%s/%s", dirpath, dp->d_name); + + if (lstat(subpath, &st)) + continue; + + switch (st.st_mode & S_IFMT) { + case S_IFDIR: + ent = add_to_flist(FT_DIR, subpath); + if (ent == NULL) { + ret = -ENOMEM; + goto err_closedir; + } + ret = walkdir(ent); + if (ret) + goto err_closedir; + break; + case S_IFREG: + ent = add_to_flist(FT_REG, subpath); + if (ent == NULL) { + ret = -ENOMEM; + goto err_closedir; + } + break; + case S_IFLNK: + ent = add_to_flist(FT_SYM, subpath); + if (ent == NULL) { + ret = -ENOMEM; + goto err_closedir; + } + break; + default: + break; + } + } + if (errno) + ret = -errno; +err_closedir: + closedir(_dir); + return ret; +} + +static int init_filetable(int testdir_fd) +{ + struct fent *fent; + + fent = add_to_flist(FT_DIR, "."); + if (!fent) + return -ENOMEM; + if (fchdir(testdir_fd) < 0) { + perror("failed to fchdir"); + return -errno; + } + return walkdir(fent); +} + +static struct fent *getfent(int which, int r) +{ + int totalsum = 0; /* total number of matching files */ + int partialsum = 0; /* partial sum of matching files */ + struct flist *flp; + int i, x; + + totalsum = 0; + for (i = 0, flp = flists; i < FT_nft; ++i, ++flp) + if (which & (1 << i)) + totalsum += flp->nfiles; + + if (!totalsum) + return NULL; + + /* + * Now we have possible matches between 0..totalsum-1. + * And we use r to help us choose which one we want, + * which when bounded by totalsum becomes x. + */ + x = (int)(r % totalsum); + + for (i = 0, flp = flists; i < FT_nft; i++, flp++) { + if (which & (1 << i)) { + if (x < partialsum + flp->nfiles) + return &flp->fents[x - partialsum]; + partialsum += flp->nfiles; + } + } + fprintf(stderr, "%s failure\n", __func__); + return NULL; +} + +static int testdir_fd = -1, chkdir_fd = -1; + +static int __getdents_f(unsigned int sn, struct fent *fe) +{ + int dfd; + DIR *dir; + + dfd = openat(testdir_fd, fe->subpath, O_DIRECTORY); + if (dfd < 0) { + fprintf(stderr, "%d[%u]/%u getdents_f: failed to open directory %s", + getpid(), procid, sn, fe->subpath); + return -errno; + } + + dir = fdopendir(dfd); + while (readdir64(dir) != NULL) + continue; + closedir(dir); + return 0; +} + +static int getdents_f(int op, unsigned int sn) +{ + struct fent *fe; + + fe = getfent(FT_DIRm, random()); + if (!fe) + return 0; + printf("%d[%u]/%u %s: %s\n", getpid(), procid, sn, __func__, + fe->subpath); + return __getdents_f(sn, fe); +} + +static int readlink_f(int op, unsigned int sn) +{ + char buf1[PATH_MAX], buf2[PATH_MAX]; + struct fent *fe; + ssize_t sz; + + fe = getfent(FT_SYMm, random()); + if (!fe) + return 0; + + printf("%d[%u]/%u %s: %s\n", getpid(), procid, sn, __func__, + fe->subpath); + sz = readlinkat(testdir_fd, fe->subpath, buf1, PATH_MAX - 1); + if (sz < 0) { + fprintf(stderr, "%d[%u]/%u %s: failed to readlinkat %s: %d", + getpid(), procid, sn, __func__, fe->subpath, errno); + return -errno; + } + + if (chkdir_fd >= 0) { + if (sz != readlinkat(testdir_fd, fe->subpath, buf2, + PATH_MAX - 1)) { + fprintf(stderr, "%d[%u]/%u %s: symlink length mismatch @%s\n", + getpid(), procid, sn, __func__, fe->subpath); + return -E2BIG; + } + if (memcmp(buf1, buf2, sz)) { + fprintf(stderr, "%d[%u]/%u %s: symlink mismatch @%s\n", + getpid(), procid, sn, __func__, fe->subpath); + return -EBADMSG; + } + } + return 0; +} + +static int tryopen(unsigned int sn, const char *op, struct fent *fe) +{ + if (fe->fd < 0) { + fe->fd = openat(testdir_fd, fe->subpath, O_RDONLY); + if (fe->fd < 0) { + fprintf(stderr, "%d[%u]/%u %s: failed to open %s: %d", + getpid(), procid, sn, op, fe->subpath, errno); + return -errno; + } + /* use force_page_cache_readahead for every read request */ + posix_fadvise(fe->fd, 0, 0, POSIX_FADV_RANDOM); + } + + if (chkdir_fd >= 0 && fe->chkfd < 0) + fe->chkfd = openat(chkdir_fd, fe->subpath, O_RDONLY); + return 0; +} + +static int fadvise_f(int op, unsigned int sn) +{ + struct fent *fe; + int ret; + + fe = getfent(FT_REGm, random()); + if (!fe) + return 0; + ret = tryopen(sn, __func__, fe); + if (ret) + return ret; + + printf("%d[%u]/%u %s: %s\n", getpid(), procid, sn, + __func__, fe->subpath); + ret = posix_fadvise(fe->fd, 0, 0, POSIX_FADV_DONTNEED); + if (!ret) + return 0; + fprintf(stderr, "%d(%u)/%u %s: posix_fadvise %s failed %d\n", + getpid(), procid, sn, __func__, fe->subpath, errno); + return -errno; +} + +static int __read_f(unsigned int sn, struct fent *fe, uint64_t filesize) +{ + static char buf[MAX_CHUNKSIZE], chkbuf[MAX_CHUNKSIZE]; + uint64_t lr, off, len, trimmed; + size_t nread, nread2; + + lr = ((uint64_t) random() << 32) + random(); + off = lr % filesize; + len = (random() % MAX_CHUNKSIZE) + 1; + trimmed = len; + + if (off + len > filesize) { + uint64_t a = filesize - off + 16 * getpagesize(); + + if (len > a) + len %= a; + trimmed = len <= filesize - off ? len : filesize - off; + } + + printf("%d[%u]/%u read_f: %llu bytes @ %llu\n", getpid(), procid, sn, + len | 0ULL, off | 0ULL); + nread = pread64(fe->fd, buf, len, off); + if (nread != trimmed) { + fprintf(stderr, "%d[%u]/%u read_f: failed to read %llu bytes @ %llu of %s\n", + getpid(), procid, sn, len | 0ULL, off | 0ULL, + fe->subpath); + return -errno; + } + + if (fe->chkfd < 0) + return 0; + + nread2 = pread64(fe->chkfd, chkbuf, len, off); + if (nread2 <= 0) { + fprintf(stderr, "%d[%u]/%u read_f: failed to check %llu bytes @ %llu of %s\n", + getpid(), procid, sn, len | 0ULL, off | 0ULL, + fe->subpath); + return -errno; + } + + if (nread != nread2) { + fprintf(stderr, "%d[%u]/%u read_f: size mismatch %llu bytes @ %llu of %s\n", + getpid(), procid, sn, len | 0ULL, off | 0ULL, + fe->subpath); + return -EFBIG; + } + + if (memcmp(buf, chkbuf, nread)) { + fprintf(stderr, "%d[%u]/%u read_f: data mismatch %llu bytes @ %llu of %s\n", + getpid(), procid, sn, len | 0ULL, off | 0ULL, + fe->subpath); + return -EBADMSG; + } + return 0; +} + +static int read_f(int op, unsigned int sn) +{ + struct fent *fe; + ssize_t fsz; + int ret; + + fe = getfent(FT_REGm, random()); + if (!fe) + return 0; + ret = tryopen(sn, __func__, fe); + if (ret) + return ret; + + fsz = lseek64(fe->fd, 0, SEEK_END); + if (fsz <= 0) { + if (!fsz) { + printf("%d[%u]/%u %s: zero size @ %s\n", + getpid(), procid, sn, __func__, fe->subpath); + return 0; + } + fprintf(stderr, "%d[%u]/%u %s: lseek64 %s failed %d\n", + getpid(), procid, sn, __func__, fe->subpath, errno); + return -errno; + } + return __read_f(sn, fe, fsz); +} + +static int __doscan_f(unsigned int sn, const char *op, struct fent *fe, + uint64_t filesize, uint64_t chunksize) +{ + static char buf[MAX_SCAN_CHUNKSIZE], chkbuf[MAX_SCAN_CHUNKSIZE]; + uint64_t pos; + + printf("%d[%u]/%u %s: filesize %llu, chunksize %llu @ %s\n", + getpid(), procid, sn, op, (unsigned long long)filesize, + (unsigned long long)chunksize, fe->subpath); + + for (pos = 0; pos < filesize; pos += chunksize) { + ssize_t nread, nread2; + + nread = pread64(fe->fd, buf, chunksize, pos); + + if (nread <= 0) + return -errno; + + if (nread < chunksize && nread != filesize - pos) + return -ERANGE; + + if (fe->chkfd < 0) + continue; + + nread2 = pread64(fe->chkfd, chkbuf, chunksize, pos); + if (nread2 <= 0) + return -errno; + + if (nread != nread2) + return -EFBIG; + + if (memcmp(buf, chkbuf, nread)) { + fprintf(stderr, "%d[%u]/%u %s: %llu bytes mismatch @ %llu of %s\n", + getpid(), procid, sn, op, chunksize | 0ULL, + pos | 0ULL, fe->subpath); + return -EBADMSG; + } + } + return 0; +} + +static int doscan_f(int op, unsigned int sn) +{ + struct fent *fe; + uint64_t chunksize; + ssize_t fsz; + int ret; + + fe = getfent(FT_REGm, random()); + if (!fe) + return 0; + ret = tryopen(sn, __func__, fe); + if (ret) + return ret; + + fsz = lseek64(fe->fd, 0, SEEK_END); + if (fsz <= 0) { + if (!fsz) { + printf("%d[%u]/%u %s: zero size @ %s\n", + getpid(), procid, sn, __func__, fe->subpath); + return 0; + } + fprintf(stderr, "%d[%u]/%u %s: lseek64 %s failed %d\n", + getpid(), procid, sn, __func__, fe->subpath, errno); + return -errno; + } + chunksize = ((uint64_t)random() * random() % MAX_SCAN_CHUNKSIZE) + 1; + return __doscan_f(sn, __func__, fe, fsz, chunksize); +} + +static int doscan_aligned_f(int op, unsigned int sn) +{ + const int psz = getpagesize(); + struct fent *fe; + uint64_t chunksize, maxchunksize; + ssize_t fsz; + int ret; + + fe = getfent(FT_REGm, random()); + if (!fe) + return 0; + ret = tryopen(sn, __func__, fe); + if (ret) + return ret; + fsz = lseek64(fe->fd, 0, SEEK_END); + if (fsz <= psz) { + if (fsz >= 0) { + printf("%d[%u]/%u %s: size too small %lld @ %s\n", + getpid(), procid, sn, __func__, fsz | 0LL, + fe->subpath); + return 0; + } + fprintf(stderr, "%d[%u]/%u %s: lseek64 %s failed %d\n", + getpid(), procid, sn, __func__, fe->subpath, errno); + return -errno; + } + + maxchunksize = (fsz - psz > MAX_SCAN_CHUNKSIZE ? + MAX_SCAN_CHUNKSIZE : fsz - psz); + chunksize = random() * random() % maxchunksize; + chunksize = (((chunksize - 1) / psz) + 1) * psz; + if (!chunksize) + chunksize = psz; + return __doscan_f(sn, __func__, fe, fsz, chunksize); +} + +void randomdelay(void) +{ + uint64_t lr = ((uint64_t) random() << 32) + random(); + clock_t start; + clock_t length = (lr % CLOCKS_PER_SEC) >> 1; + + start = clock(); + while (clock() < start + length) + (void)sched_yield(); +} + +void sg_handler(int signum) +{ + switch (signum) { + case SIGTERM: + should_stop = 1; + break; + default: + break; + } +} + +struct opdesc ops[] = { + [OP_GETDENTS] = { "getdents", getdents_f, 5, false }, + [OP_READLINK] = { "readlink", readlink_f, 5, false }, + [OP_SEQREAD_ALIGNED] = { "readscan_aligned", doscan_aligned_f, 10, false }, + [OP_SEQREAD_UNALIGNED] = { "readscan_unaligned", doscan_f, 10, false }, + [OP_READ] = { "read", read_f, 30, false}, + [OP_FADVISE] = { "fadvise", fadvise_f, 3, false}, + [OP_DROP_CACHES] = { "drop_caches", drop_caches_f, 1, true}, +}; + +static int parse_options(int argc, char *argv[]) +{ + char *testdir, *chkdir; + int opt; + + while ((opt = getopt(argc, argv, "l:p:s:")) != -1) { + switch (opt) { + case 'l': + loops = atoi(optarg); + if (loops < 0) { + fprintf(stderr, "invalid loops %d\n", loops); + return -EINVAL; + } + break; + case 'p': + nprocs = atoi(optarg); + if (nprocs < 0) { + fprintf(stderr, "invalid workers %d\n", + nprocs); + return -EINVAL; + } + break; + case 's': + r_seed = atoi(optarg); + if (r_seed < 0) { + fprintf(stderr, "invalid random seed %d\n", + r_seed); + return -EINVAL; + } + break; + default: /* '?' */ + return -EINVAL; + } + } + + if (optind >= argc) + return -EINVAL; + + testdir = argv[optind++]; + if (testdir) { + testdir_fd = open(testdir, O_PATH); + if (testdir_fd < 0) { + fprintf(stderr, "cannot open testdir fd @ %s: %s\n", + testdir, strerror(errno)); + return 1; + } + } + + if (argc > optind) { + chkdir = argv[optind++]; + + chkdir_fd = open(chkdir, O_PATH); + if (chkdir_fd < 0) { + fprintf(stderr, "cannot open checkdir fd @ %s: %s\n", + chkdir, strerror(errno)); + return 1; + } + } + return 0; +} + +static void usage(void) +{ + fputs("usage: [options] TESTDIR [COMPRDIR]\n\n" + "Stress test for EROFS filesystem, where TESTDIR is the directory to test and\n" + "COMPRDIR (optional) serves as a directory for data comparison.\n" + " -l# Number of times each worker should loop (0 for infinite, default: 1)\n" + " -p# Number of parallel worker processes (default: 1)\n" + " -s# Seed for random generator (default: random)\n", + stderr); +} + +unsigned int *freq_table; +int freq_table_size; + +static void doproc(void) +{ + unsigned int sn; + + srandom(r_seed + procid); + for (sn = 0; !should_stop && (!loops || sn < loops); ++sn) { + int op, err; + + op = freq_table[random() % freq_table_size]; + if (op >= ARRAY_SIZE(ops)) { + fprintf(stderr, "%d[%u]/%u %s: internal error\n", + getpid(), procid, sn, __func__); + abort(); + } + + if (sn && op != OP_DROP_CACHES) + randomdelay(); + err = ops[op].func(op, sn); + if (err) { + fprintf(stderr, "%d[%u]/%u test failed (%d): %s\n", + getpid(), procid, sn, err, strerror(-err)); + exit(1); + } + } +} + +static void make_freq_table(void) +{ + int f, i; + struct opdesc *p; + + for (p = ops, f = 0; p < ops + ARRAY_SIZE(ops); p++) { + if (!superuser && p->requireroot) + continue; + f += p->freq; + } + freq_table = malloc(f * sizeof(*freq_table)); + freq_table_size = f; + for (p = ops, i = 0; p < ops + ARRAY_SIZE(ops); p++) { + if (!superuser && p->requireroot) + continue; + for (f = 0; f < p->freq; f++, i++) + freq_table[i] = p - ops; + } +} + +int main(int argc, char *argv[]) +{ + unsigned int i; + int err, stat; + struct sigaction action; + + err = parse_options(argc, argv); + if (err) { + if (err == -EINVAL) + usage(); + return 1; + } + + err = init_filetable(testdir_fd); + if (err) { + fprintf(stderr, "cannot initialize file table: %s\n", + strerror(errno)); + return 1; + } + + superuser = (geteuid() == 0); + setpgid(0, 0); + action.sa_handler = sg_handler; + action.sa_flags = 0; + + if (sigaction(SIGTERM, &action, 0)) { + perror("sigaction failed"); + exit(1); + } + + if (!r_seed) + r_seed = (time(NULL) ? : 1); + make_freq_table(); + + /* spawn nprocs processes */ + for (i = 0; i < nprocs; ++i) { + if (fork() == 0) { + action.sa_handler = SIG_DFL; + sigemptyset(&action.sa_mask); + if (sigaction(SIGTERM, &action, 0)) { + perror("sigaction failed"); + exit(1); + } + procid = i; + doproc(); + return 0; + } + } + + err = 0; + while (wait(&stat) > 0 && !should_stop) { + if (!WIFEXITED(stat)) { + err = 1; + break; + } + + if (WEXITSTATUS(stat)) { + err = WEXITSTATUS(stat); + break; + } + } + action.sa_flags = SA_RESTART; + sigaction(SIGTERM, &action, 0); + kill(-getpid(), SIGTERM); + /* wait until all children exit */ + while (wait(&stat) > 0) + continue; + return err; +} -- 2.43.5