| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * Copyright (c) 2021 Oracle. All Rights Reserved. |
| * Author: Darrick J. Wong <djwong@kernel.org> |
| * |
| * Race pwrite/mwrite with dedupe to see if we got the locking right. |
| * |
| * File writes and mmap writes should not be able to change the src_fd's |
| * contents after dedupe prep has verified that the file contents are the same. |
| */ |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| #include <sys/mman.h> |
| #include <sys/ioctl.h> |
| #include <linux/fs.h> |
| #include <string.h> |
| #include <stdio.h> |
| #include <unistd.h> |
| #include <fcntl.h> |
| #include <pthread.h> |
| #include <stdlib.h> |
| #include <errno.h> |
| |
| #define GOOD_BYTE 0x58 |
| #define BAD_BYTE 0x66 |
| |
| #ifndef FIDEDUPERANGE |
| /* extent-same (dedupe) ioctls; these MUST match the btrfs ioctl definitions */ |
| #define FILE_DEDUPE_RANGE_SAME 0 |
| #define FILE_DEDUPE_RANGE_DIFFERS 1 |
| |
| /* from struct btrfs_ioctl_file_extent_same_info */ |
| struct file_dedupe_range_info { |
| __s64 dest_fd; /* in - destination file */ |
| __u64 dest_offset; /* in - start of extent in destination */ |
| __u64 bytes_deduped; /* out - total # of bytes we were able |
| * to dedupe from this file. */ |
| /* status of this dedupe operation: |
| * < 0 for error |
| * == FILE_DEDUPE_RANGE_SAME if dedupe succeeds |
| * == FILE_DEDUPE_RANGE_DIFFERS if data differs |
| */ |
| __s32 status; /* out - see above description */ |
| __u32 reserved; /* must be zero */ |
| }; |
| |
| /* from struct btrfs_ioctl_file_extent_same_args */ |
| struct file_dedupe_range { |
| __u64 src_offset; /* in - start of extent in source */ |
| __u64 src_length; /* in - length of extent */ |
| __u16 dest_count; /* in - total elements in info array */ |
| __u16 reserved1; /* must be zero */ |
| __u32 reserved2; /* must be zero */ |
| struct file_dedupe_range_info info[0]; |
| }; |
| #define FIDEDUPERANGE _IOWR(0x94, 54, struct file_dedupe_range) |
| #endif /* FIDEDUPERANGE */ |
| |
| static int fd1, fd2; |
| static loff_t offset = 37; /* Nice low offset to trick the compare */ |
| static loff_t blksz; |
| |
| /* Continuously dirty the pagecache for the region being dupe-tested. */ |
| void * |
| mwriter( |
| void *data) |
| { |
| volatile char *p; |
| |
| p = mmap(NULL, blksz, PROT_WRITE, MAP_SHARED, fd1, 0); |
| if (p == MAP_FAILED) { |
| perror("mmap"); |
| exit(2); |
| } |
| |
| while (1) { |
| *(p + offset) = BAD_BYTE; |
| *(p + offset) = GOOD_BYTE; |
| } |
| } |
| |
| /* Continuously write to the region being dupe-tested. */ |
| void * |
| pwriter( |
| void *data) |
| { |
| char v; |
| ssize_t sz; |
| |
| while (1) { |
| v = BAD_BYTE; |
| sz = pwrite(fd1, &v, sizeof(v), offset); |
| if (sz != sizeof(v)) { |
| perror("pwrite0"); |
| exit(2); |
| } |
| |
| v = GOOD_BYTE; |
| sz = pwrite(fd1, &v, sizeof(v), offset); |
| if (sz != sizeof(v)) { |
| perror("pwrite1"); |
| exit(2); |
| } |
| } |
| |
| return NULL; |
| } |
| |
| static inline void |
| complain( |
| loff_t offset, |
| char bad) |
| { |
| fprintf(stderr, "ASSERT: offset %llu should be 0x%x, got 0x%x!\n", |
| (unsigned long long)offset, GOOD_BYTE, bad); |
| abort(); |
| } |
| |
| /* Make sure the destination file pagecache never changes. */ |
| void * |
| mreader( |
| void *data) |
| { |
| volatile char *p; |
| |
| p = mmap(NULL, blksz, PROT_READ, MAP_SHARED, fd2, 0); |
| if (p == MAP_FAILED) { |
| perror("mmap"); |
| exit(2); |
| } |
| |
| while (1) { |
| if (*(p + offset) != GOOD_BYTE) |
| complain(offset, *(p + offset)); |
| } |
| } |
| |
| /* Make sure the destination file never changes. */ |
| void * |
| preader( |
| void *data) |
| { |
| char v; |
| ssize_t sz; |
| |
| while (1) { |
| sz = pread(fd2, &v, sizeof(v), offset); |
| if (sz != sizeof(v)) { |
| perror("pwrite0"); |
| exit(2); |
| } |
| |
| if (v != GOOD_BYTE) |
| complain(offset, v); |
| } |
| |
| return NULL; |
| } |
| |
| void |
| print_help(const char *progname) |
| { |
| printf("Usage: %s [-b blksz] [-c dir] [-n nr_ops] [-o offset] [-r] [-w] [-v]\n", |
| progname); |
| printf("-b sets the block size (default is autoconfigured)\n"); |
| printf("-c chdir to this path before starting\n"); |
| printf("-n controls the number of dedupe ops (default 10000)\n"); |
| printf("-o reads and writes to this offset (default 37)\n"); |
| printf("-r uses pread instead of mmap read.\n"); |
| printf("-v prints status updates.\n"); |
| printf("-w uses pwrite instead of mmap write.\n"); |
| } |
| |
| int |
| main( |
| int argc, |
| char *argv[]) |
| { |
| struct file_dedupe_range *fdr; |
| char *Xbuf; |
| void *(*reader_fn)(void *) = mreader; |
| void *(*writer_fn)(void *) = mwriter; |
| unsigned long same = 0; |
| unsigned long differs = 0; |
| unsigned long i, nr_ops = 10000; |
| ssize_t sz; |
| pthread_t reader, writer; |
| int verbose = 0; |
| int c; |
| int ret; |
| |
| while ((c = getopt(argc, argv, "b:c:n:o:rvw")) != -1) { |
| switch (c) { |
| case 'b': |
| errno = 0; |
| blksz = strtoul(optarg, NULL, 0); |
| if (errno) { |
| perror(optarg); |
| exit(1); |
| } |
| break; |
| case 'c': |
| ret = chdir(optarg); |
| if (ret) { |
| perror("chdir"); |
| exit(1); |
| } |
| break; |
| case 'n': |
| errno = 0; |
| nr_ops = strtoul(optarg, NULL, 0); |
| if (errno) { |
| perror(optarg); |
| exit(1); |
| } |
| break; |
| case 'o': |
| errno = 0; |
| offset = strtoul(optarg, NULL, 0); |
| if (errno) { |
| perror(optarg); |
| exit(1); |
| } |
| break; |
| case 'r': |
| reader_fn = preader; |
| break; |
| case 'v': |
| verbose = 1; |
| break; |
| case 'w': |
| writer_fn = pwriter; |
| break; |
| default: |
| print_help(argv[0]); |
| exit(1); |
| break; |
| } |
| } |
| |
| fdr = malloc(sizeof(struct file_dedupe_range) + |
| sizeof(struct file_dedupe_range_info)); |
| if (!fdr) { |
| perror("malloc"); |
| exit(1); |
| } |
| |
| /* Initialize both files. */ |
| fd1 = open("file1", O_RDWR | O_CREAT | O_TRUNC | O_NOATIME, 0600); |
| if (fd1 < 0) { |
| perror("file1"); |
| exit(1); |
| } |
| |
| fd2 = open("file2", O_RDWR | O_CREAT | O_TRUNC | O_NOATIME, 0600); |
| if (fd2 < 0) { |
| perror("file2"); |
| exit(1); |
| } |
| |
| if (blksz <= 0) { |
| struct stat statbuf; |
| |
| ret = fstat(fd1, &statbuf); |
| if (ret) { |
| perror("file1 stat"); |
| exit(1); |
| } |
| blksz = statbuf.st_blksize; |
| } |
| |
| if (offset >= blksz) { |
| fprintf(stderr, "offset (%llu) < blksz (%llu)?\n", |
| (unsigned long long)offset, |
| (unsigned long long)blksz); |
| exit(1); |
| } |
| |
| Xbuf = malloc(blksz); |
| if (!Xbuf) { |
| perror("malloc buffer"); |
| exit(1); |
| } |
| memset(Xbuf, GOOD_BYTE, blksz); |
| |
| sz = pwrite(fd1, Xbuf, blksz, 0); |
| if (sz != blksz) { |
| perror("file1 write"); |
| exit(1); |
| } |
| |
| sz = pwrite(fd2, Xbuf, blksz, 0); |
| if (sz != blksz) { |
| perror("file2 write"); |
| exit(1); |
| } |
| |
| ret = fsync(fd1); |
| if (ret) { |
| perror("file1 fsync"); |
| exit(1); |
| } |
| |
| ret = fsync(fd2); |
| if (ret) { |
| perror("file2 fsync"); |
| exit(1); |
| } |
| |
| /* Start our reader and writer threads. */ |
| ret = pthread_create(&reader, NULL, reader_fn, NULL); |
| if (ret) { |
| fprintf(stderr, "rthread: %s\n", strerror(ret)); |
| exit(1); |
| } |
| |
| ret = pthread_create(&writer, NULL, writer_fn, NULL); |
| if (ret) { |
| fprintf(stderr, "wthread: %s\n", strerror(ret)); |
| exit(1); |
| } |
| |
| /* |
| * Now start deduping. If the contents match, fd1's blocks will be |
| * remapped into fd2, which is why the writer thread targets fd1 and |
| * the reader checks fd2 to make sure that none of fd1's writes ever |
| * make it into fd2. |
| */ |
| for (i = 1; i <= nr_ops; i++) { |
| fdr->src_offset = 0; |
| fdr->src_length = blksz; |
| fdr->dest_count = 1; |
| fdr->reserved1 = 0; |
| fdr->reserved2 = 0; |
| fdr->info[0].dest_fd = fd2; |
| fdr->info[0].dest_offset = 0; |
| fdr->info[0].reserved = 0; |
| |
| ret = ioctl(fd1, FIDEDUPERANGE, fdr); |
| if (ret) { |
| perror("dedupe"); |
| exit(2); |
| } |
| |
| switch (fdr->info[0].status) { |
| case FILE_DEDUPE_RANGE_DIFFERS: |
| differs++; |
| break; |
| case FILE_DEDUPE_RANGE_SAME: |
| same++; |
| break; |
| default: |
| fprintf(stderr, "deduperange: %s\n", |
| strerror(-fdr->info[0].status)); |
| exit(2); |
| break; |
| } |
| |
| if (verbose && (i % 337) == 0) |
| printf("nr_ops: %lu; same: %lu; differs: %lu\n", |
| i, same, differs); |
| } |
| |
| if (verbose) |
| printf("nr_ops: %lu; same: %lu; differs: %lu\n", i - 1, same, |
| differs); |
| |
| /* Program termination will kill the threads and close the files. */ |
| return 0; |
| } |