From 3b8d990b5d93be94cb838bec7594b9db3262731b Mon Sep 17 00:00:00 2001 From: Ole Tange Date: Sun, 29 Dec 2024 02:57:09 +0100 Subject: [PATCH] iothrottle: Initial version. --- Makefile | 21 +-- iothrottle/iothrottle.c | 291 +++++++++++++++++++++++++++++++++++++++ iothrottle/iothrottle.in | 174 +++++++++++++++++++++++ 3 files changed, 477 insertions(+), 9 deletions(-) create mode 100644 iothrottle/iothrottle.c create mode 100755 iothrottle/iothrottle.in diff --git a/Makefile b/Makefile index fa13c9d..12b43c6 100644 --- a/Makefile +++ b/Makefile @@ -2,11 +2,11 @@ CMD = 2grep 2search audioping blink burncpu bwlimit clipboard drac \ duplicate-packets em emoticons encdir fanspeed field \ find-first-fail find-optimal forever ft-udvalg fxkill G \ gitdiffdir gitedit gitnext gitundo goodpasswd histogram \ - Loffice mtrr mirrorpdf neno not off pdfman pidcmd pidtree \ - plotpipe puniq ramusage rand rclean rina rn rrm seekmaniac \ - shython sound-reload splitvideo stdout summer swapout T \ - teetime timestamp tracefile transpose twitsplit ctc upsidedown \ - vid w4it-for-port-open whitehash wifi-reload wssh \ + iothrottle Loffice mtrr mirrorpdf neno not off pdfman pidcmd \ + pidtree plotpipe puniq ramusage rand rclean rina rn rrm \ + seekmaniac shython sound-reload splitvideo stdout summer \ + swapout T teetime timestamp tracefile transpose twitsplit ctc \ + upsidedown vid w4it-for-port-open whitehash wifi-reload wssh \ youtube-lbry ytv yyyymmdd all: 2search/2grep.1 2search/2search.1 blink/blink.1 \ @@ -16,10 +16,10 @@ all: 2search/2grep.1 2search/2search.1 blink/blink.1 \ ft-udvalg/ft-udvalg.1 G/G.1 gitdiffdir/gitdiffdir.1 \ gitedit/gitedit.1 gitnext/gitnext.1 gitundo/gitundo.1 \ goodpasswd/goodpasswd.1 histogram/histogram.1 \ - mirrorpdf/mirrorpdf.1 neno/neno.1 off/off.1 pdfman/pdfman.1 \ - pidcmd/pidcmd.1 pidtree/pidtree.1 plotpipe/plotpipe.1 \ - puniq/puniq.1 rand/rand.1 rina/rina.1 rn/rn.1 rrm/rrm.1 \ - seekmaniac/seekmaniac.1 shython/shython.1 \ + iothrottle/iothrottle.1 mirrorpdf/mirrorpdf.1 neno/neno.1 \ + off/off.1 pdfman/pdfman.1 pidcmd/pidcmd.1 pidtree/pidtree.1 \ + plotpipe/plotpipe.1 puniq/puniq.1 rand/rand.1 rina/rina.1 \ + rn/rn.1 rrm/rrm.1 seekmaniac/seekmaniac.1 shython/shython.1 \ sound-reload/sound-reload.1 splitvideo/splitvideo.1 \ stdout/stdout.1 teetime/teetime.1 timestamp/timestamp.1 \ tracefile/tracefile.1 transpose/transpose.1 \ @@ -30,6 +30,9 @@ all: 2search/2grep.1 2search/2search.1 blink/blink.1 \ %.1: % pod2man $< > $@ +iothrottle/iothrottle: + cd iothrottle; make + install: mkdir -p /usr/local/bin parallel eval ln -sf `pwd`/*/{} /usr/local/bin/{} ::: $(CMD) diff --git a/iothrottle/iothrottle.c b/iothrottle/iothrottle.c new file mode 100644 index 0000000..e136316 --- /dev/null +++ b/iothrottle/iothrottle.c @@ -0,0 +1,291 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Function pointers for original read and write and other relevant calls +ssize_t (*original_read)(int fd, void *buf, size_t count); +ssize_t (*original_write)(int fd, const void *buf, size_t count); +size_t (*original_fwrite)(const void *ptr, size_t size, size_t nmemb, FILE *stream); +int (*original_fflush)(FILE *stream); +ssize_t (*original_pread)(int fd, void *buf, size_t count, off_t offset); +ssize_t (*original_pwrite)(int fd, const void *buf, size_t count, off_t offset); +ssize_t (*original_readv)(int fd, const struct iovec *iov, int iovcnt); +ssize_t (*original_writev)(int fd, const struct iovec *iov, int iovcnt); +off_t (*original_sendfile)(int out_fd, int in_fd, off_t *offset, size_t count); +ssize_t (*original_splice)(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags); +ssize_t (*original_copy_file_range)(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags); + +// Rate limits in bytes per second +static size_t read_limit = 0; // 0 = no limit +static size_t write_limit = 0; +static int debug_mode = 0; + +// Tracking variables +static size_t read_bytes = 0; +static size_t write_bytes = 0; +static struct timespec read_start, write_start; + +void throttle(size_t *bytes, size_t limit, struct timespec *start) { + if (limit == 0) { + return; // No limit applied + } + + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + + double elapsed = (now.tv_sec - start->tv_sec) + (now.tv_nsec - start->tv_nsec) / 1e9; + + double allowed_bytes = limit * elapsed; + if (debug_mode) fprintf(stderr, "allowed: %ld written: %ld\n", + (long)allowed_bytes, *bytes); + if (*bytes > allowed_bytes) { + *bytes -= allowed_bytes; + double sleep_time = *bytes / (double)limit; + + if (debug_mode) fprintf(stderr, "sleep: %f\n", sleep_time); + usleep((useconds_t)(sleep_time * 1e6)); + *start = now; + } +} + +size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) { + if (!original_fwrite) { + original_fwrite = dlsym(RTLD_NEXT, "fwrite"); + if (!original_fwrite) { + if (debug_mode) fprintf(stderr, "Error loading original fwrite: %s\n", dlerror()); + exit(EXIT_FAILURE); + } + } + + size_t total_bytes = size * nmemb; + throttle(&write_bytes, write_limit, &write_start); + + size_t result = original_fwrite(ptr, size, nmemb, stream); + if (result > 0) { + write_bytes += total_bytes; + } + return result; +} + +int fflush(FILE *stream) { + if (!original_fflush) { + original_fflush = dlsym(RTLD_NEXT, "fflush"); + if (!original_fflush) { + if (debug_mode) fprintf(stderr, "Error loading original fflush: %s\n", dlerror()); + exit(EXIT_FAILURE); + } + } + + throttle(&write_bytes, write_limit, &write_start); + + return original_fflush(stream); +} + +ssize_t read(int fd, void *buf, size_t count) { + if (!original_read) { + original_read = dlsym(RTLD_NEXT, "read"); + if (!original_read) { + if (debug_mode) fprintf(stderr, "Error loading original read: %s\n", dlerror()); + exit(EXIT_FAILURE); + } + } + + throttle(&read_bytes, read_limit, &read_start); + + ssize_t result = original_read(fd, buf, count); + if (result > 0) { + read_bytes += result; + } + return result; +} + +ssize_t write(int fd, const void *buf, size_t count) { + if (!original_write) { + original_write = dlsym(RTLD_NEXT, "write"); + if (!original_write) { + if (debug_mode) fprintf(stderr, "Error loading original write: %s\n", dlerror()); + exit(EXIT_FAILURE); + } + } + + throttle(&write_bytes, write_limit, &write_start); + + ssize_t result = original_write(fd, buf, count); + if (result > 0) { + write_bytes += result; + } + return result; +} + +ssize_t pread(int fd, void *buf, size_t count, off_t offset) { + if (!original_pread) { + original_pread = dlsym(RTLD_NEXT, "pread"); + if (!original_pread) { + if (debug_mode) fprintf(stderr, "Error loading original pread: %s\n", dlerror()); + exit(EXIT_FAILURE); + } + } + + throttle(&read_bytes, read_limit, &read_start); + + ssize_t result = original_pread(fd, buf, count, offset); + if (result > 0) { + read_bytes += result; + } + return result; +} + +ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset) { + if (!original_pwrite) { + original_pwrite = dlsym(RTLD_NEXT, "pwrite"); + if (!original_pwrite) { + if (debug_mode) fprintf(stderr, "Error loading original pwrite: %s\n", dlerror()); + exit(EXIT_FAILURE); + } + } + + throttle(&write_bytes, write_limit, &write_start); + + ssize_t result = original_pwrite(fd, buf, count, offset); + if (result > 0) { + write_bytes += result; + } + return result; +} + +ssize_t readv(int fd, const struct iovec *iov, int iovcnt) { + if (!original_readv) { + original_readv = dlsym(RTLD_NEXT, "readv"); + if (!original_readv) { + if (debug_mode) fprintf(stderr, "Error loading original readv: %s\n", dlerror()); + exit(EXIT_FAILURE); + } + } + + throttle(&read_bytes, read_limit, &read_start); + + ssize_t result = original_readv(fd, iov, iovcnt); + if (result > 0) { + read_bytes += result; + } + return result; +} + +ssize_t writev(int fd, const struct iovec *iov, int iovcnt) { + if (!original_writev) { + original_writev = dlsym(RTLD_NEXT, "writev"); + if (!original_writev) { + if (debug_mode) fprintf(stderr, "Error loading original writev: %s\n", dlerror()); + exit(EXIT_FAILURE); + } + } + + throttle(&write_bytes, write_limit, &write_start); + + ssize_t result = original_writev(fd, iov, iovcnt); + if (result > 0) { + write_bytes += result; + } + return result; +} + +off_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count) { + if (!original_sendfile) { + original_sendfile = dlsym(RTLD_NEXT, "sendfile"); + if (!original_sendfile) { + if (debug_mode) fprintf(stderr, "Error loading original sendfile: %s\n", dlerror()); + exit(EXIT_FAILURE); + } + } + + throttle(&read_bytes, read_limit, &read_start); + throttle(&write_bytes, write_limit, &write_start); + + off_t result = original_sendfile(out_fd, in_fd, offset, count); + if (result > 0) { + read_bytes += result; + write_bytes += result; + } + return result; +} + +ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags) { + if (!original_splice) { + original_splice = dlsym(RTLD_NEXT, "splice"); + if (!original_splice) { + if (debug_mode) fprintf(stderr, "Error loading original splice: %s\n", dlerror()); + exit(EXIT_FAILURE); + } + } + + throttle(&read_bytes, read_limit, &read_start); + throttle(&write_bytes, write_limit, &write_start); + + ssize_t result = original_splice(fd_in, off_in, fd_out, off_out, len, flags); + if (result > 0) { + read_bytes += result; + write_bytes += result; + } + return result; +} + +ssize_t copy_file_range(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags) { + if (!original_copy_file_range) { + original_copy_file_range = dlsym(RTLD_NEXT, "copy_file_range"); + if (!original_copy_file_range) { + if (debug_mode) fprintf(stderr, "Error loading original copy_file_range: %s\n", dlerror()); + exit(EXIT_FAILURE); + } + } + + throttle(&read_bytes, read_limit, &read_start); + throttle(&write_bytes, write_limit, &write_start); + + ssize_t result = original_copy_file_range(fd_in, off_in, fd_out, off_out, len, flags); + if (result > 0) { + read_bytes += result; + write_bytes += result; + } + return result; +} + +__attribute__((constructor)) +void init() { + clock_gettime(CLOCK_MONOTONIC, &read_start); + clock_gettime(CLOCK_MONOTONIC, &write_start); + + // Initialize limits from environment variables (bytes/s) + char *read_env = getenv("IOTHROTTLE_READ"); + char *write_env = getenv("IOTHROTTLE_WRITE"); + char *debug_env = getenv("IOTHROTTLE_DEBUG"); + + if (debug_env && atoi(debug_env) != 0) { + debug_mode = 1; + } + + if (debug_mode) fprintf(stderr, "init called: default read_limit=%zu, write_limit=%zu\n", read_limit, write_limit); + + if (read_env) { + if (debug_mode) fprintf(stderr, "IOTHROTTLE_READ=%s\n", read_env); + read_limit = strtoull(read_env, NULL, 10); + } else { + if (debug_mode) fprintf(stderr, "IOTHROTTLE_READ not set\n"); + } + + if (write_env) { + if (debug_mode) fprintf(stderr, "IOTHROTTLE_WRITE=%s\n", write_env); + write_limit = strtoull(write_env, NULL, 10); + } else { + if (debug_mode) fprintf(stderr, "IOTHROTTLE_WRITE not set\n"); + } + + if (debug_mode) fprintf(stderr, "Final read_limit=%zu, write_limit=%zu\n", read_limit, write_limit); +} diff --git a/iothrottle/iothrottle.in b/iothrottle/iothrottle.in new file mode 100755 index 0000000..f2e969d --- /dev/null +++ b/iothrottle/iothrottle.in @@ -0,0 +1,174 @@ +#!/bin/bash + +: <<=cut +=pod + +=head1 NAME + +iothrottle - Limit IO to a given speed + + +=head1 SYNOPSIS + +B [-i read-speed] [-o write-speed] I + + +=head1 DESCRIPTION + +B limits B(2) and B(2) to a given speed. + + +=head1 OPTIONS + +=over 4 + +=item B<-i> I + +Throttle B(2) to I bytes per second. + +To I you can append k, K, m, M, g, G to multiply by 1000, +1024, 1000000, 1048576 respectively. + + +=item B<-o> I + +Throttle B(2) to I bytes per second. + +To I you can append k, K, m, M, g, G to multiply by 1000, +1024, 1000000, 1048576 respectively. + +=back + + +=head1 EXAMPLE + +Copy mydir at 1MB/s: + + iothrottle -i 1M cp -a mydir/ /other/filesystem/ + + +=head1 BUGS + +Not all program use B(2) and B(2) for I/O. + +These all use other methods: cp (to same filesystem), seq + +File a bug report when you find other programs: +https://git.data.coop/tange/tangetools/issues + + +=head1 ENVIRONMENT VARIABLES + +=over 9 + +=item $IOTHROTTLE_DEBUG=1 + +Turn on debugging. + +=back + + +=head1 AUTHOR + +Copyright (C) 2024 Ole Tange, +http://ole.tange.dk and Free Software Foundation, Inc. + + +=head1 LICENSE + +Copyright (C) 2012 Free Software Foundation, Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +at your option any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + + +=head1 DEPENDENCIES + +B uses B. + + +=head1 SEE ALSO + +B, B + + +=cut + + +_hex() { + local hex=dummy + echo $hex +} + +_iothrottle.so() { + local iothrottleso=$(mktemp) + _hex | + LC_ALL=C awk '{for (i=1; i<=length($0); i+=2) printf "%c", strtonum("0x" substr($0, i, 2))}' > "$iothrottleso" + echo "$iothrottleso" +} + +# Default values for input and output limits +INPUT_LIMIT="" +OUTPUT_LIMIT="" + +# Parse command-line arguments +while [[ $# -gt 0 ]]; do + case "$1" in + -i) + INPUT_LIMIT="$2" + shift 2 + ;; + -o) + OUTPUT_LIMIT="$2" + shift 2 + ;; + *) + break + ;; + esac +done + +# Convert limits to bytes +convert_to_bytes() { + local value="$1" + if [[ "$value" =~ ^[0-9]+K$ ]]; then + value=$(( ${value%K*} * 1024 )) + fi + if [[ "$value" =~ ^[0-9]+k$ ]]; then + value=$(( ${value%k*} * 1000 )) + fi + if [[ "$value" =~ ^[0-9]+M$ ]]; then + value=$(( ${value%M*} * 1024 * 1024 )) + fi + if [[ "$value" =~ ^[0-9]+m$ ]]; then + value=$(( ${value%m*} * 1000 * 1000 )) + fi + echo "$value" +} + +IOTHROTTLE_READ=$(convert_to_bytes "$INPUT_LIMIT") +IOTHROTTLE_WRITE=$(convert_to_bytes "$OUTPUT_LIMIT") + +# Export environment variables +export IOTHROTTLE_READ=${IOTHROTTLE_READ:-0} +export IOTHROTTLE_WRITE=${IOTHROTTLE_WRITE:-0} + +# Preload the library and execute the remaining command +_so=$(_iothrottle.so) +if [ IOTHROTTLE_DEBUG = 1 ] ; then + echo LD_PRELOAD=$_so + LD_PRELOAD=$_so "$@" +else + LD_PRELOAD=$_so "$@" + rm $_so +fi