From 39ca33807b936a46bbd491ab5ecff817a6a28144 Mon Sep 17 00:00:00 2001 From: Mattias Andrée Date: Sat, 24 Feb 2024 19:58:22 +0100 Subject: Add coreupdownd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Andrée --- .gitignore | 1 + Makefile | 20 +++- TODO | 2 - config.mk | 9 ++ coredown.1 | 5 +- coreup.1 | 5 +- coreupdown.c | 313 +++++++++++++++++++++++++++++++++++++++++++++++++++------- coreupdownd.1 | 74 ++++++++++++++ 8 files changed, 387 insertions(+), 42 deletions(-) delete mode 100644 TODO create mode 100644 coreupdownd.1 diff --git a/.gitignore b/.gitignore index e9dfb32..a7d0aa5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ /coreupdown /coreup /coredown +/coreupdownd diff --git a/Makefile b/Makefile index f0d383d..f9850e0 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,16 @@ CONFIGFILE = config.mk include $(CONFIGFILE) +CPPFLAGS_CONFS =\ + -D'COREUP_THRESHOLD_CPU=$(COREUP_THRESHOLD_CPU)'\ + -D'COREUP_THRESHOLD_TIME=$(COREUP_THRESHOLD_TIME)'\ + -D'COREUP_COOLDOWN=$(COREUP_COOLDOWN)'\ + -D'COREDOWN_THRESHOLD_CPU=$(COREDOWN_THRESHOLD_CPU)'\ + -D'COREDOWN_THRESHOLD_TIME=$(COREDOWN_THRESHOLD_TIME)'\ + -D'COREDOWN_COOLDOWN=$(COREDOWN_COOLDOWN)'\ + -D'DAEMON_INTERVAL_SEC=$(DAEMON_INTERVAL_SEC)'\ + -D'DAEMON_INTERVAL_NSEC=$(DAEMON_INTERVAL_NSEC)' + OBJ =\ coreupdown.o @@ -12,7 +22,7 @@ all: coreupdown $(OBJ): $(HDR) .c.o: - $(CC) -c -o $@ $< $(CFLAGS) $(CPPFLAGS) + $(CC) -c -o $@ $< $(CFLAGS) $(CPPFLAGS) $(CPPFLAGS_CONFS) coreupdown: $(OBJ) $(CC) -o $@ $(OBJ) $(LDFLAGS) @@ -22,9 +32,11 @@ install: coreupdown mkdir -p -- "$(DESTDIR)$(MANPREFIX)/man1" test -f "$(DESTDIR)$(PREFIX)/bin/coreup" || test ! -e "$(DESTDIR)$(PREFIX)/bin/coreup" test -f "$(DESTDIR)$(PREFIX)/bin/coredown" || test ! -e "$(DESTDIR)$(PREFIX)/bin/coredown" + test -f "$(DESTDIR)$(PREFIX)/bin/coreupdownd" || test ! -e "$(DESTDIR)$(PREFIX)/bin/coreupdownd" cp -- coreupdown "$(DESTDIR)$(PREFIX)/bin/coreup" - ln -sf coreup "$(DESTDIR)$(PREFIX)/bin/coredown" - cp -- coreup.1 coredown.1 "$(DESTDIR)$(MANPREFIX)/man1" + ln -sf -- coreup "$(DESTDIR)$(PREFIX)/bin/coredown" + ln -sf -- coreup "$(DESTDIR)$(PREFIX)/bin/coreupdownd" + cp -- coreup.1 coredown.1 coreupdownd.1 "$(DESTDIR)$(MANPREFIX)/man1" postinstall: chown '0:0' "$(DESTDIR)$(PREFIX)/bin/coreup" @@ -33,8 +45,10 @@ postinstall: uninstall: -rm -f -- "$(DESTDIR)$(PREFIX)/bin/coreup" -rm -f -- "$(DESTDIR)$(PREFIX)/bin/coredown" + -rm -f -- "$(DESTDIR)$(PREFIX)/bin/coreupdownd" -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/coreup.1" -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/coredown.1" + -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/coreupdownd.1" clean: -rm -f -- *.o *.a *.lo *.su *.so *.so.* *.gch *.gcov *.gcno *.gcda diff --git a/TODO b/TODO deleted file mode 100644 index a399993..0000000 --- a/TODO +++ /dev/null @@ -1,2 +0,0 @@ -Add coreupdownd to automatically run coreup and coredown when the -machine has high or low CPU usage diff --git a/config.mk b/config.mk index f1d6591..f833645 100644 --- a/config.mk +++ b/config.mk @@ -1,6 +1,15 @@ PREFIX = /usr MANPREFIX = $(PREFIX)/share/man +COREUP_THRESHOLD_CPU = 95 +COREUP_THRESHOLD_TIME = 2 +COREUP_COOLDOWN = 4 +COREDOWN_THRESHOLD_CPU = 50 +COREDOWN_THRESHOLD_TIME = 2 +COREDOWN_COOLDOWN = 4 +DAEMON_INTERVAL_SEC = 1 +DAEMON_INTERVAL_NSEC = 0 + CC = c99 CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_GNU_SOURCE diff --git a/coredown.1 b/coredown.1 index 666ea46..e07abe2 100644 --- a/coredown.1 +++ b/coredown.1 @@ -31,7 +31,7 @@ utility does not use the standard input. None. .SH ENVIRONMENT VARIABLES No environment variables affects the execution of -.B coredown +.BR coredown . .SH ASYNCHRONOUS EVENTS Default. .SH STDOUT @@ -75,4 +75,5 @@ None. .SH FUTURE DIRECTIONS None. .SH SEE ALSO -.BR coreup (1) +.BR coreup (1), +.BR coreupdownd (1) diff --git a/coreup.1 b/coreup.1 index 1dfe99f..b7fd8f2 100644 --- a/coreup.1 +++ b/coreup.1 @@ -25,7 +25,7 @@ utility does not use the standard input. None. .SH ENVIRONMENT VARIABLES No environment variables affects the execution of -.B coreup +.BR coreup . .SH ASYNCHRONOUS EVENTS Default. .SH STDOUT @@ -69,4 +69,5 @@ None. .SH FUTURE DIRECTIONS None. .SH SEE ALSO -.BR coredown (1) +.BR coredown (1), +.BR coreupdownd (1) diff --git a/coreupdown.c b/coreupdown.c index 1de5007..403bdf6 100644 --- a/coreupdown.c +++ b/coreupdown.c @@ -3,37 +3,46 @@ #include #include #include +#include #include #include #include +#include #include -int -main(int argc, char *argv[]) +static const char *argv0; + +#define coreup() coreupdown(ULONG_MAX) +#define coredown() coreupdown(1) + +static int +coreupdown(unsigned long int count) { - unsigned long i, count; - const char *command, *argv0; - char path[sizeof("/sys/devices/system/cpu/cpu/online") + 3 * sizeof(i)]; + char path[sizeof("/sys/devices/system/cpu/cpu/online") + 3 * sizeof(count)]; + unsigned long int i; int fd, ret = 0; - if (argc == 0) { - wrong_command: - fprintf(stderr, "usage: coreup [count]\nusage: coredown [count]\n"); - return 1; + for (i = 1; i; i++) { + sprintf(path, "/sys/devices/system/cpu/cpu%zu/online", i); + fd = open(path, O_WRONLY); + if (fd < 0) { + if (errno == ENOENT) + break; + fprintf(stderr, "%s: open %s O_WRONLY\n", argv0, path); + return -1; + } + if (write(fd, i < count ? "1\n" : "0\n", 2) < 0) { + fprintf(stderr, "%s: write %s\n", argv0, path); + ret = -1; + } + close(fd); } + return ret; +} - argv0 = *argv++; - argc--; - command = strrchr(argv0, '/'); - command = command ? &command[1] : argv0; - - if (!strcmp(command, "coreup")) - count = ULONG_MAX; - else if (!strcmp(command, "coredown")) - count = 1; - else - goto wrong_command; - +static int +oneshot_main(int argc, char **argv, unsigned long int count) +{ if (argc && !strcmp(argv[0], "--")) { argv++; argc--; @@ -45,7 +54,7 @@ main(int argc, char *argv[]) return 1; } - if (argc >= 1) { + if (argc) { char *end; errno = 0; if (!isdigit(**argv)) @@ -57,21 +66,259 @@ main(int argc, char *argv[]) argc--; } - for (i = 1; i; i++) { - sprintf(path, "/sys/devices/system/cpu/cpu%zu/online", i); - fd = open(path, O_WRONLY); - if (fd < 0) { - if (errno == ENOENT) + return -coreupdown(count); +} + +static int +getusage(uintmax_t *total_out, uintmax_t *idle_out, size_t *cpus_out) +{ +#define IDLE_FIELD 3 + + static uintmax_t last_total = 0, last_idle = 0; + uintmax_t cur_total, cur_idle; + char buf[512]; + int fd; + size_t off, len, field; + ssize_t r; + int line_incomplete = 0, beginning_of_line = 1; + uintmax_t amount[] = {0, 0}, value; + + *cpus_out = 0; + + fd = open("/proc/stat", O_RDONLY); + if (fd < 0) { + fprintf(stderr, "%s: open /proc/stat O_RDONLY\n", argv0); + return -1; + } + + len = 0; + for (;;) { + r = read(fd, &buf[len], sizeof(buf) - len); + if (!r) { + fprintf(stderr, "%s: /proc/stat did not contain \"cpu\" line\n", argv0); + close(fd); + return -1; + } else if (r < 0) { + if (errno == EINTR) + continue; + fprintf(stderr, "%s: read /proc/stat: %s\n", argv0, strerror(errno)); + close(fd); + return -1; + } + len += (size_t)r; + + if (line_incomplete) + goto skip_line; + + for (off = 0; off < len;) { + if (len - off < 4) { + memmove(&buf[0], &buf[off], len -= off); break; - fprintf(stderr, "%s: open %s O_WRONLY\n", argv0, path); - return 1; + } else if (!strncmp(&buf[off], "cpu", 3)) { + if (isspace(buf[off + 3])) + goto cpu_line_found; + else if (isdigit(buf[off + 3])) + *cpus_out += 1; + } + + skip_line: + while (off < len && buf[off] != '\n') + off++; + if (off == len) { + line_incomplete = 0; + break; + } else { + off++; + line_incomplete = 1; + } } - if (write(fd, i < count ? "1\n" : "0\n", 2) < 0) { - fprintf(stderr, "%s: write %s\n", argv0, path); - ret = 1; + } +cpu_line_found: + + off += 4; + field = 0; + value = 0; + for (;;) { + if (off == len) { + off = len = 0; + r = read(fd, buf, sizeof(buf)); + if (!r) { + goto line_done; + } else if (r < 0) { + if (errno == EINTR) + continue; + fprintf(stderr, "%s: read /proc/stat: %s\n", argv0, strerror(errno)); + close(fd); + return -1; + } + len = (size_t)r; } + while (off < len && isspace(buf[off])) + off++; + if (off == len) + continue; + while (off < len) { + if (buf[off] == '\n') { + amount[field++ == IDLE_FIELD] += value; + off++; + goto line_done; + } else if (isspace(buf[off])) { + amount[field++ == IDLE_FIELD] += value; + value = 0; + off++; + break; + } else { + value *= 10; + value += buf[off] & 15; + off++; + } + } + } +line_done: + + for (;;) { + if (off == len) { + off = len = 0; + read_more: + r = read(fd, &buf[len], sizeof(buf) - len); + if (!r) { + break; + } else if (r < 0) { + if (errno == EINTR) + continue; + fprintf(stderr, "%s: read /proc/stat: %s\n", argv0, strerror(errno)); + close(fd); + return -1; + } + len = (size_t)r; + } + for (; off < len; off++) { + if (beginning_of_line) { + if (len - off < 5) { + memmove(&buf[0], &buf[off], len -= off); + goto read_more; + } + if (!strncmp(&buf[off], "cpu", 3) && isdigit(buf[off + 3])) + *cpus_out += 1; + beginning_of_line = 0; + } + if (buf[off] == '\n') + beginning_of_line = 1; + } + } + + close(fd); + + if (field < IDLE_FIELD) { + fprintf(stderr, "%s: /proc/stat did not contain CPU idle time\n", argv0); close(fd); + return -1; } - return ret; + cur_total = amount[0] + amount[1]; + cur_idle = amount[1]; + *total_out = cur_total - last_total; + *idle_out = cur_idle - last_idle; + last_total = cur_total; + last_idle = cur_idle; + return 0; + +#undef IDLE_FIELD +} + +static int +daemon_main(int argc, char **argv) +{ + uintmax_t total, idle; + size_t cpus; + int up_time = 0, down_time = 0, cooldown = 0; + struct timespec interval, rem; + + if (argc && !strcmp(argv[0], "--")) { + argv++; + argc--; + } + + if (argc > 0) { + fprintf(stderr, "usage: %s\n", argv0); + return 1; + } + + /* TODO configurations should be configurable */ + + /* TODO add SIGHUP support */ + + /* TODO add proper daemonisation */ + + interval.tv_sec = DAEMON_INTERVAL_SEC; + interval.tv_nsec = DAEMON_INTERVAL_NSEC; + + if (getusage(&total, &idle, &cpus)) + return 1; + + close(STDIN_FILENO); + close(STDOUT_FILENO); + + for (;;) { + if (clock_nanosleep(CLOCK_REALTIME /* = monotonic */, 0, &interval, &rem)) { + do { + if (errno != EINTR) { + fprintf(stderr, "%s: clock_nanosleep CLOCK_REALTIME: %s\n", + argv0, strerror(errno)); + return 1; + } + } while (clock_nanosleep(CLOCK_REALTIME, 0, &rem, &rem)); + } + sleep(1); + if (getusage(&total, &idle, &cpus)) + return 1; + if (cooldown) { + cooldown--; + continue; + } + + if (cpus == 1 && 100 * (total - idle) > COREUP_THRESHOLD_CPU * total) { + down_time = 0; + if (up_time++ >= COREUP_THRESHOLD_TIME) { + cooldown = COREUP_COOLDOWN; + coreup(); + up_time = 0; + } + } else if (cpus > 1 && 100 * (total - idle) < COREDOWN_THRESHOLD_CPU * total * cpus) { + up_time = 0; + if (down_time++ >= COREDOWN_THRESHOLD_TIME) { + cooldown = COREDOWN_COOLDOWN; + coredown(); + down_time = 0; + } + } else { + up_time = 0; + down_time = 0; + } + } + + return 0; +} + +int +main(int argc, char **argv) +{ + const char *command; + + if (argc--) { + command = strrchr(argv0 = *argv++, '/'); + command = command ? &command[1] : argv0; + if (!strcmp(command, "coreup")) + return oneshot_main(argc, argv, ULONG_MAX); + if (!strcmp(command, "coredown")) + return oneshot_main(argc, argv, 1); + if (!strcmp(command, "coreupdownd")) + return daemon_main(argc, argv); + } + + fprintf(stderr, "usage: coreup [count]\n" + "usage: coredown [count]\n" + "usage: coreupdownd\n"); + + return 1; } diff --git a/coreupdownd.1 b/coreupdownd.1 new file mode 100644 index 0000000..9dde00b --- /dev/null +++ b/coreupdownd.1 @@ -0,0 +1,74 @@ +.TH COREUPDOWND 1 COREUPDOWN +.SH MAME +coreupdownd - Dynamically enable and disable CPU' to only use one during low CPU usage +.SH SYNPOSIS +coreupdownd +.SH DESCRIPTION +The +.B coreupdownd +daemon dynamically turns all CPU cores, except except +the main one, on and off depending on CPU usage. +.PP +The daemon periodically determines what the CPU usage +would be if only the main CPU core was enable. If it +would be saturated, the other cores are enabled, otherwise +the they are disabled. +.PP +This is designed as a workaround for virtual machines +that have input performance issues (keyboard input lag +and keys getting stuck) when more than one CPU core is +online. +.SH OPTIONS +No options are supported. +.SH OPERANDS +No operands are supported. +.SH STDIN +The +.B coreupdownd +daemon does not use the standard input, however +it will be closed by the daemon once initialisation +has completed. +.SH INPUT FILES +None. +.SH ENVIRONMENT VARIABLES +No environment variables affects the execution of +.BR coreupdownd . +.SH ASYNCHRONOUS EVENTS +Default. +.SH STDOUT +The +.B coreupdownd +daemon does not use the standard output, however +it will be closed by the daemon once initialisation +has completed. +.SH STDERR +The standard error is used for diagnostic messages. +.SH OUTPUT FILES +None. +.SH EXTENDED DESCRIPTION +None. +.SH EXIT STATUS +If the +.B coreupdownd +daemon only terminates on failure, in which case +the exit status will be: +.TP +1 +An error occurred. +.SH CONSEQUENCES OF ERRORS +Default. +.SH APPLICATION USAGE +None. +.SH EXAMPLES +None. +.SH RATIONALE +None. +.SH NOTES +None. +.SH BUGS +None. +.SH FUTURE DIRECTIONS +None. +.SH SEE ALSO +.BR coreup (1), +.BR coredown (1) -- cgit v1.2.3-70-g09d2