Skip to content

Instantly share code, notes, and snippets.

@zevweiss
Last active November 19, 2021 17:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zevweiss/2cba14070b62c979f2ceaf0ca3b66a13 to your computer and use it in GitHub Desktop.
Save zevweiss/2cba14070b62c979f2ceaf0ca3b66a13 to your computer and use it in GitHub Desktop.
/*
* cheesegrater -- reduce a filesystem's free space to shreds.
*
* Basic strategy: fill the FS with one big file (write until ENOSPC), then hole-punch every
* other block in the file. Assuming the filesystem allocates space reasonably contiguously
* (and doesn't reshuffle anything during hole-punching), this should produce ~50% free space
* in a minimally contiguous form.
*
* I wrote this a few years ago (circa 2016 I think) basically just to see how different
* filesystems would handle it. From what I can recall:
*
* - ext4 did about as well as could be expected
* - btrfs failed (don't remember exactly how) before the hole-punching phase even completed
* - xfs ended up in the expected 50%-free state, but then failed all subsequent file-creation
* attempts with ENOSPC (because it wants to allocate inodes in 16KB clusters, and there weren't
* any contiguous chunks that large remaining)
*
* (Caveats on the above: (a) I may be misremembering some details, and (b) filesystems are
* still in active development and their behavior may have changed in the last few years.)
*
* Options to tweak the default behavior described above:
* - -b BLKSIZE: override the blocksize retrieved via fstatvfs(2)
* - -p NBLKS: number of blocks in each hole-punch (default 1)
* - -s NBLKS: number of blocks to skip between hole-punches (default 1)
* - -z SIZE: restrict file-size to SIZE instead of filling all available space
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <getopt.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <linux/falloc.h>
static unsigned long blksize = 0;
static long punch_len = 1, skip_len = 1;
static off_t filesize = -1;
#define fperror(fmt, ...) do { \
fprintf(stderr, fmt, ##__VA_ARGS__); \
fprintf(stderr, ": %m\n"); \
} while (0)
static int strict_strtol(const char* str, int base, long* val)
{
char* end;
*val = strtol(str, &end, base);
return (*str && !*end) ? 0 : -1;
}
static long parse_number(const char* s)
{
long tmp;
if (strict_strtol(s, 0, &tmp)) {
fprintf(stderr, "'%s' is not a valid number\n", s);
exit(1);
}
return tmp;
}
static off_t parse_size(const char* s)
{
char* end;
long shift, n = strtol(s, &end, 0);
if (!*s || (end[0] && end[1])) {
fprintf(stderr, "'%s' is not a valid size\n", s);
exit(1);
}
switch (toupper(*end)) {
case '\0':
shift = 0;
break;
case 'K':
shift = 10;
break;
case 'M':
shift = 20;
break;
case 'G':
shift = 30;
break;
case 'T':
shift = 40;
break;
default:
fprintf(stderr, "unrecognized unit suffix '%c'\n", *end);
exit(1);
}
return n << shift;
}
static char iobuf[1 << 19];
static void cheesegrate(int fd)
{
ssize_t status;
size_t towrite;
unsigned long p;
off_t written = 0;
memset(iobuf, 'x', sizeof(iobuf));
for (;;) {
if (filesize >= 0 && sizeof(iobuf) > (filesize - written))
towrite = filesize - written;
else
towrite = sizeof(iobuf);
if (!towrite)
break;
status = write(fd, iobuf, towrite);
if (status < 0) {
if (errno == ENOSPC)
break;
else
perror("write");
}
written += status;
}
if (!punch_len)
return;
for (p = 0; p < (written / blksize); p += punch_len + skip_len) {
if (fallocate(fd, FALLOC_FL_KEEP_SIZE|FALLOC_FL_PUNCH_HOLE,
p * blksize, punch_len * blksize)) {
perror("fallocate(FALLOC_FL_KEEP_SIZE|FALLOC_FL_PUNCH_HOLE)");
exit(1);
}
}
}
static const char* progname;
static void usage(FILE* out)
{
fprintf(out, "Usage: %s [-b FORCEBLKSIZE | -p PUNCHBLKS | -s SKIPBLKS | -z FILESIZE] FILLFILE\n", progname);
}
int main(int argc, char** argv)
{
int opt;
int fd;
const char* fillpath;
struct statvfs st;
progname = argv[0];
while ((opt = getopt(argc, argv, "b:p:s:z:")) != -1) {
switch (opt) {
case 'b':
blksize = parse_size(optarg);
if (blksize <= 0) {
fprintf(stderr, "Block size must be positive\n");
exit(1);
}
break;
case 'p':
punch_len = parse_number(optarg);
if (punch_len < 0) {
fprintf(stderr, "Punch length must be >= 0\n");
exit(1);
}
break;
case 's':
skip_len = parse_number(optarg);
if (skip_len < 0) {
fprintf(stderr, "Skip length must be >= 0\n");
exit(1);
}
break;
case 'z':
filesize = parse_size(optarg);
if (filesize < 0) {
fprintf(stderr, "File size must be >= 0\n");
exit(1);
}
break;
default:
fprintf(stderr, "Error: unrecognized flag '%c'\n", opt);
usage(stderr);
exit(1);
}
}
argc -= optind;
argv += optind;
if (argc != 1) {
usage(stderr);
exit(1);
}
fillpath = argv[0];
if (unlink(fillpath) && errno != ENOENT) {
fperror("unlink(%s)", fillpath);
exit(1);
}
fd = open(fillpath, O_CREAT|O_TRUNC|O_EXCL|O_WRONLY, 0644);
if (fd < 0) {
fperror("open(%s)", fillpath);
exit(1);
}
if (blksize == 0) {
if (fstatvfs(fd, &st)) {
fperror("statvfs(%s)", fillpath);
exit(1);
}
blksize = st.f_bsize;
}
cheesegrate(fd);
if (close(fd))
perror("close");
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment