blob: b252d4360baa0c5848d09fbf5777aebe30def721 [file] [log] [blame]
// 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;
}