pax_global_header00006660000000000000000000000064132333530520014511gustar00rootroot0000000000000052 comment=6e990606e9f4bf89e911ebcb8772057cfd959555 earlyoom-1.0/000077500000000000000000000000001323335305200132005ustar00rootroot00000000000000earlyoom-1.0/.clang-format000066400000000000000000000000251323335305200155500ustar00rootroot00000000000000BasedOnStyle: WebKit earlyoom-1.0/.gitignore000066400000000000000000000001171323335305200151670ustar00rootroot00000000000000*~ /earlyoom # generated service files /earlyoom.service /earlyoom.initscript earlyoom-1.0/.travis.yml000066400000000000000000000000731323335305200153110ustar00rootroot00000000000000language: c script: - make - make format - git diff earlyoom-1.0/Dockerfile000066400000000000000000000002351323335305200151720ustar00rootroot00000000000000FROM gcc as build WORKDIR /usr/src COPY . . ENV CFLAGS -static RUN make ### FROM scratch COPY --from=build /usr/src/earlyoom / ENTRYPOINT ["/earlyoom"] earlyoom-1.0/LICENSE000066400000000000000000000020771323335305200142130ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 Jakob Unterwurzacher Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. earlyoom-1.0/Makefile000066400000000000000000000034741323335305200146500ustar00rootroot00000000000000VERSION ?= $(shell git describe --tags --dirty 2> /dev/null) CFLAGS += -Wall -Wextra -DVERSION=\"$(VERSION)\" -g DESTDIR ?= PREFIX ?= /usr/local BINDIR ?= /bin SYSCONFDIR ?= /etc SYSTEMDDIR ?= $(SYSCONFDIR)/systemd ifeq ($(VERSION),) VERSION := "(unknown version)" endif .PHONY: all clean install uninstall format all: earlyoom earlyoom: $(wildcard *.c *.h) $(CC) $(LDFLAGS) $(CFLAGS) -o $@ $(wildcard *.c) clean: rm -f earlyoom earlyoom.service earlyoom.initscript earlyoom.1.gz install: earlyoom.service install-bin install-default install-man install -d $(DESTDIR)$(SYSTEMDDIR)/system/ install -m 644 $< $(DESTDIR)$(SYSTEMDDIR)/system/ -systemctl enable earlyoom install-initscript: earlyoom.initscript install-bin install-default install -d $(DESTDIR)$(SYSCONFDIR)/init.d/ install -m 755 $< $(DESTDIR)$(SYSCONFDIR)/init.d/earlyoom -update-rc.d earlyoom start 18 2 3 4 5 . stop 20 0 1 6 . earlyoom.%: earlyoom.%.in sed "s|:TARGET:|$(PREFIX)$(BINDIR)|g;s|:SYSCONFDIR:|$(SYSCONFDIR)|g" $< > $@ install-default: earlyoom.default install-man install -d $(DESTDIR)$(SYSCONFDIR)/default/ install -m 644 $< $(DESTDIR)$(SYSCONFDIR)/default/earlyoom install-bin: earlyoom install -d $(DESTDIR)$(PREFIX)$(BINDIR)/ install -m 755 $< $(DESTDIR)$(PREFIX)$(BINDIR)/ install-man: earlyoom.1.gz install -d $(DESTDIR)$(PREFIX)/share/man/man1/ install -m 644 $< $(DESTDIR)$(PREFIX)/share/man/man1/ earlyoom.1.gz: earlyoom.1 gzip -k $< uninstall: uninstall-bin uninstall-man systemctl disable earlyoom rm -f $(DESTDIR)$(SYSTEMDDIR)/system/earlyoom.service uninstall-man: rm -f $(DESTDIR)$(PREFIX)/share/man/man1/earlyoom.1.gz uninstall-initscript: uninstall-bin rm -f $(DESTDIR)$(SYSCONFDIR)/init.d/earlyoom update-rc.d earlyoom remove uninstall-bin: rm -f $(DESTDIR)$(PREFIX)$(BINDIR)/earlyoom format: clang-format -i *.h *.c earlyoom-1.0/README.md000066400000000000000000000222231323335305200144600ustar00rootroot00000000000000The Early OOM Daemon ==================== [![Build Status](https://api.travis-ci.org/rfjakob/earlyoom.svg)](https://travis-ci.org/rfjakob/earlyoom) The oom-killer generally has a bad reputation among Linux users. This may be part of the reason Linux invokes it only when it has absolutely no other choice. It will swap out the desktop environment, drop the whole page cache and empty every buffer before it will ultimately kill a process. At least that's what I think what it will do. I have yet to be patient enough to wait for it. Instead of sitting in front of an unresponsive system, listening to the grinding disk for minutes, I usually press the reset button and get back to what I was doing quickly. If you want to see what I mean, open something like the Epic Citatel HTML5 Demo in a few Firefox windows (the demo is now offline, it looked like [this](https://blog.mozilla.org/blog/2014/03/12/mozilla-and-epic-preview-unreal-engine-4-running-in-firefox/)). Save your work to disk beforehand, though. The downside of the reset button is that it kills all processes, whereas it would probably have been enough to kill a single one. This made people wonder if the oom-killer could be configured to step in earlier: [superuser.com][2] , [unix.stackexchange.com][3]. As it turns out, no, it can't. At least using the in-kernel oom killer. In the user space however, we can do whatever we want. What does it do --------------- earlyoom checks the amount of available memory and (since version 0.5) free swap 10 times a second. If both are below 10%, it will kill the largest process. The percentage value is configurable via command line arguments. In the `free -m` output below, the available memory is 2170 MiB and the free swap is 231 MiB. total used free shared buff/cache available Mem: 7842 4523 137 841 3182 2170 Swap: 1023 792 231 Why is "available" memory checked as opposed to "free" memory? On a healthy Linux system, "free" memory is supposed to be close to zero, because Linux uses all available physical memory to cache disk access. These caches can be dropped any time the memory is needed for something else. The "available" memory accounts for that. It sums up all memory that is unused or can be freed immediately. Note that you need a recent version of `free` and Linux kernel 3.14+ to see the "available" column. If you have a recent kernel, but an old version of `free`, you can get the value from `cat /proc/meminfo | grep MemAvailable`. When both your available memory and free swap drop below 10% of the total, it will `kill -9` the process that uses the most memory in the opinion of the kernel (`/proc/*/oom_score`). It can optionally (`-i` option) ignore any positive adjustments set in `/proc/*/oom_score_adj` to protect innocent victims (see below). Why not trigger the kernel oom killer? -------------------------------------- Earlyoom does not use `echo f > /proc/sysrq-trigger` because [the Chrome people made their browser always be the first (innocent!) victim by setting `oom_score_adj` very high]( https://code.google.com/p/chromium/issues/detail?id=333617). Instead, earlyoom finds out itself by reading through `/proc/*/status` (actually `/proc/*/statm`, which contains the same information but is easier to parse programmatically). Additionally, in recent kernels (tested on 4.0.5), triggering the kernel oom killer manually may not work at all. That is, it may only free some graphics memory (that will be allocated immediately again) and not actually kill any process. [Here](https://gist.github.com/rfjakob/346b7dc611fc3cdf4011) you can see how this looks like on my machine (Intel integrated graphics). How much memory does earlyoom use? ---------------------------------- About `0.6MB RSS`. All memory is locked using `mlockall()` to make sure earlyoom does not slow down in low memory situations. Download and compile -------------------- Easy: ```bash git clone https://github.com/rfjakob/earlyoom.git cd earlyoom make sudo make install # Optional, if you want earlyoom to start # automatically as a service (works on Fedora) ``` For Arch Linux, there's an [AUR package](https://aur.archlinux.org/packages/earlyoom/): ```bash yaourt -S earlyoom sudo systemctl enable earlyoom sudo systemctl start earlyoom ``` Use --- Just start the executable you have just compiled: ```bash ./earlyoom ``` It will inform you how much memory and swap you have, what the minimum is, how much memory is available and how much swap is free. ``` earlyoom v0.10 mem total: 7842 MiB, min: 784 MiB (10 %) swap total: 1023 MiB, min: 102 MiB (10 %) mem avail: 5115 MiB (65 %), swap free: 1023 MiB (100 %) mem avail: 5115 MiB (65 %), swap free: 1023 MiB (100 %) mem avail: 5115 MiB (65 %), swap free: 1023 MiB (100 %) [...] ``` If the values drop below the minimum, processes are killed until it is above the minimum again. Every action is logged to stderr. If you are on running earlyoom as a systemd service, you can view the last 10 lines using ```bash systemctl status earlyoom ``` ### Notifications The command-line flag `-n` enables notifications via `notify-send`. However, if earlyoom is being run by a user other than the one running your desktop environment (e.g. if it's run as a service or cron job) then `notify-send` will not work on its own, as DBUS, X, and/or display information may required. In this case, you can use `-N` to supply environment variables or another command. The exact value will vary depending on your desktop environment, but the following command may work. `YOUR_USER` should be replaced with output of `whoami` and `YOUR_USER_ID` with output of `echo $UID`. Your `DISPLAY` value may also be different (check `echo $DISPLAY`). ```bash earlyoom -N 'sudo -u YOUR_USER DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/YOUR_USER_ID/bus notify-send' ``` Other options are discussed in [this thread](https://unix.stackexchange.com/questions/111188/using-notify-send-with-cron). Note that if you choose to use a command other than `notify-send`, it must support the same arguments. Example invocation earlyoom uses: ```bash NOTIFY_COMMAND -i dialog-warning 'notif title' 'notif text' ``` ### Preferred Processes The command-line flag `--prefer` specifies processes to prefer killing; likewise, `--avoid` specifies processes to avoid killing. The list of processes is specified by a regex expression. For instance, to avoid having `foo` and `bar` be killed: ```bash earlyoom --avoid '^(foo|bar)$' ``` The regex is matched against the basename of the process as shown in `/proc/PID/stat`. Command line options -------------------- ``` ./earlyoom -h earlyoom v0.12 Usage: earlyoom [OPTION]... -m PERCENT set available memory minimum to PERCENT of total (default 10 %) -s PERCENT set free swap minimum to PERCENT of total (default 10 %) -M SIZE set available memory minimum to SIZE KiB -S SIZE set free swap minimum to SIZE KiB -k use kernel oom killer instead of own user-space implementation -i user-space oom killer should ignore positive oom_score_adj values -n enable notifications using "notify-send" -N COMMAND enable notifications using COMMAND -d enable debugging messages -v print version information and exit -r INTERVAL memory report interval in seconds (default 1), set to 0 to disable completely -p set niceness of earlyoom to -20 and oom_score_adj to -1000 --prefer REGEX prefer killing processes matching REGEX --avoid REGEX avoid killing processes matching REGEX -h this help text ``` Contribute ---------- Bug reports and pull requests are welcome via github. In particular, I am glad to accept * Use case reports and feedback Changelog --------- * v1.0, 2018-01-28: Add `--prefer` and `--avoid` options (@TomJohnZ) * v0.12: Add `-M` and `-S` options (@nailgun); add man page, parameterize Makefile (@yangfl) * v0.11: Fix undefined behavoir in get_entry_fatal (missing return, [commit](https://github.com/rfjakob/earlyoom/commit/9251d25618946723eb8a829404ebf1a65d99dbb0)) * v0.10: Allow to override Makefile's VERSION variable to make packaging easier, add `-v` command-line option * v0.9: If oom_score of all processes is 0, use VmRss to find a victim * v0.8: Use a guesstimate if the kernel does not provide MemAvailable * v0.7: Select victim by oom_score instead of VmRSS, add options `-i` and `-d` * v0.6: Add command-line options `-m`, `-s`, `-k` * v0.5: Add swap support * v0.4: Add SysV init script (thanks [@joeytwiddle](https://github.com/joeytwiddle)), use the new `MemAvailable` from `/proc/meminfo` (needs Linux 3.14+, [commit][4]) * v0.2: Add systemd unit file * v0.1: Initial release [1]: http://www.freelists.org/post/procps/library-properly-handle-memory-used-by-tmpfs [2]: http://superuser.com/questions/406101/is-it-possible-to-make-the-oom-killer-intervent-earlier [3]: http://unix.stackexchange.com/questions/38507/is-it-possible-to-trigger-oom-killer-on-forced-swapping [4]: https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773 earlyoom-1.0/earlyoom.1000066400000000000000000000061261323335305200151160ustar00rootroot00000000000000.TH earlyoom 1 .SH NAME earlyoom \- Early OOM Daemon .SH SYNOPSIS .B earlyoom .RB [ OPTION ]... .SH DESCRIPTION The oom\-killer generally has a bad reputation among Linux users. One may have to sit in front of an unresponsive system, listening to the grinding disk for minutes, and press the reset button and get back to what was doing quickly after running out of patience. .B earlyoom checks the amount of available memory and free swap 10 times a second. If both are below 10%, it will kill the largest process. The percentage value is configurable via command line arguments. .SH OPTIONS .TP .BI \-m " PERCENT" set available memory minimum to .I PERCENT of total (default 10 %) .TP .BI \-s " PERCENT" set free swap minimum to .I PERCENT of total (default 10 %) .TP .BI \-M " SIZE" set available memory minimum to .I SIZE KiB .TP .BI \-S " SIZE" set free swap minimum to .I SIZE KiB .TP .B \-k use kernel oom killer instead of own user\-space implementation .TP .B \-i user\-space oom killer should ignore positive .I oom_score_adj values .TP .B \-d enable debugging messages .TP .B \-v print version information and exit .TP .BI \-r " INTERVAL" memory report interval in seconds (default 1), set to 0 to disable completely .TP .B \-p set niceness of earlyoom to -20 and .I oom_score_adj to -1000 .TP .BI \-\-prefer " REGEX" prefer killing processes matching .I REGEX (adds 300 to oom_score) .TP .BI \-\-avoid " REGEX" avoid killing processes matching .I REGEX (subtracts 300 from oom_score) .TP .B \-h this help text .SH EXIT STATUS .TP .B 0 Successful program execution. .TP .B 1 Usage printed (using .BR \-h ). .TP .B 2 Switch conflict. .TP .B 4 Could not cd to .I /proc .TP .B 5 Could not open .I proc .TP .B 7 Could not open .I /proc/sysrq-trigger .TP .B 13 Unknown options. .TP .B 14 Wrong parameters for other options. .TP .B 15 Wrong parameters for memory threshold. .TP .B 16 Wrong parameters for swap threshold. .TP .B 102 Could not open .I /proc/meminfo .TP .B 103 Could not read .I /proc/meminfo .TP .B 104 Could not find a specific entry in .I /proc/meminfo .TP .B 105 Could not convert number when parse the contents of .I /proc/meminfo .SH Why not trigger the kernel oom killer? Earlyoom does not use .I echo f > /proc/sysrq-trigger because the Chrome people made their browser always be the first (innocent!) victim by setting .I oom_score_adj very high. Instead, earlyoom finds out itself by reading through .I /proc/*/status (actually .I /proc/*/statm , which contains the same information but is easier to parse programmatically). Additionally, in recent kernels (tested on 4.0.5), triggering the kernel oom killer manually may not work at all. That is, it may only free some graphics memory (that will be allocated immediately again) and not actually kill any process. .SH MEMORY USAGE About 0.6MB RSS. All memory is locked using .I mlockall() to make sure earlyoom does not slow down in low memory situations. .SH AUTHOR The author of earlyoom is .MT jakobunt@gmail.com Jakob Unterwurzacher .ME . This manual page was written by .MT mmyangfl@gmail.com Yangfl .ME , for the Debian project (and may be used by others). earlyoom-1.0/earlyoom.default000066400000000000000000000007441323335305200164020ustar00rootroot00000000000000# Default settings for earlyoom. This file is sourced by /bin/sh from # /etc/init.d/earlyoom. # Options to pass to earlyoom EARLYOOM_ARGS="" # Examples: # Available minimum memory 5% # EARLYOOM_ARGS="-m 5" # Available minimum memory 15% and free minimum swap 5% # EARLYOOM_ARGS="-m 15 -s 5" # Use kernel oom killer # EARLYOOM_ARGS="-k" # Avoid killing processes whose name matches this regexp # EARLYOOM_ARGS="--avoid '(^|/)(init|X|sshd|firefox)$'" # See more at `earlyoom -h' earlyoom-1.0/earlyoom.initscript.in000077500000000000000000000106641323335305200175600ustar00rootroot00000000000000#! /bin/sh ### BEGIN INIT INFO # Provides: earlyoom # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Early OOM Killer # Description: A userspace service that will kill the largest process # (by VmRSS residential size) when free RAM drops below 10%. ### END INIT INFO # Author: https://github.com/rfjakob # Do NOT "set -e" # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="Early OOM Daemon" NAME=earlyoom DAEMON=:TARGET:/$NAME #DAEMON_ARGS="--options args" LOGFILE=/var/log/$NAME.log PIDFILE=/var/run/$NAME.pid SCRIPTNAME=:SYSCONFDIR:/init.d/$NAME # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Read configuration variable file if it is present [ -r :SYSCONFDIR:/default/$NAME ] && . :SYSCONFDIR:/default/$NAME # Load the VERBOSE setting and other rcS variables . /lib/init/vars.sh # Define LSB log_* functions. # Depend on lsb-base (>= 3.2-14) to ensure that this file is present # and status_of_proc is working. . /lib/lsb/init-functions # # Function that starts the daemon/service # do_start() { # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started start-stop-daemon --start --quiet --background --pidfile $PIDFILE --exec /bin/bash --test > /dev/null \ || return 1 start-stop-daemon --start --quiet --background --pidfile $PIDFILE --exec /bin/bash -- -c "exec $DAEMON $EARLYOOM_ARGS 2> \"$LOGFILE\"" \ || return 2 # Add code here, if necessary, that waits for the process to be ready # to handle requests from services started subsequently which depend # on this one. As a last resort, sleep for some time. } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 # Wait for children to finish too if this is a daemon that forks # and if the daemon is only ever run from this initscript. # If the above conditions are not satisfied then add some other code # that waits for the process to drop all resources that could be # needed by services started subsequently. A last resort is to # sleep for some time. # # We may be able to skip this, if we are not concerned about being attached # to any resources. # This was blocking until the timeout, so I replaced 0/ with TERM/ and now # it completes immediately. start-stop-daemon --stop --quiet --oknodo --retry=TERM/30/KILL/5 --exec $DAEMON [ "$?" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE return "$RETVAL" } # # Function that sends a SIGHUP to the daemon/service # #do_reload() { # # If the daemon can reload its configuration without # restarting (for example, when it is sent a SIGHUP), # then implement that here. # #start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME #return 0 #} case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" do_stop case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; status) status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? ;; #reload|force-reload) # # If do_reload() is not implemented then leave this commented out # and leave 'force-reload' as an alias for 'restart'. # #log_daemon_msg "Reloading $DESC" "$NAME" #do_reload #log_end_msg $? #;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # log_daemon_msg "Restarting $DESC" "$NAME" do_stop case "$?" in 0|1) do_start case "$?" in 0) log_end_msg 0 ;; 1) log_end_msg 1 ;; # Old process is still running *) log_end_msg 1 ;; # Failed to start esac ;; *) # Failed to stop log_end_msg 1 ;; esac ;; *) #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 exit 3 ;; esac : earlyoom-1.0/earlyoom.service.in000066400000000000000000000004321323335305200170150ustar00rootroot00000000000000[Unit] Description=Early OOM Daemon Documentation=man:earlyoom(1) https://github.com/rfjakob/earlyoom [Service] EnvironmentFile=-:SYSCONFDIR:/default/earlyoom StandardOutput=null StandardError=syslog ExecStart=:TARGET:/earlyoom $EARLYOOM_ARGS [Install] WantedBy=multi-user.target earlyoom-1.0/kill.c000066400000000000000000000163331323335305200143050ustar00rootroot00000000000000// SPDX-License-Identifier: MIT /* Kill the most memory-hungy process */ #include #include #include #include // for PATH_MAX #include #include #include #include #include #include #include "kill.h" #define BADNESS_PREFER 300 #define BADNESS_AVOID -300 extern int enable_debug; struct procinfo { int oom_score; int oom_score_adj; unsigned long vm_rss; int exited; }; static int isnumeric(char* str) { int i = 0; // Empty string is not numeric if (str[0] == 0) return 0; while (1) { if (str[i] == 0) // End of string return 1; if (isdigit(str[i]) == 0) return 0; i++; } } static void maybe_notify(char* notif_command, char* notif_args) { if (!notif_command) return; char notif[600]; snprintf(notif, 600, "%s %s", notif_command, notif_args); system(notif); } const char* const fopen_msg = "fopen %s failed: %s\n"; /* Read /proc/pid/{oom_score, oom_score_adj, statm} * Caller must ensure that we are already in the /proc/ directory */ static struct procinfo get_process_stats(int pid) { char buf[256]; FILE* f; struct procinfo p = { 0, 0, 0, 0 }; // Read /proc/[pid]/oom_score snprintf(buf, sizeof(buf), "%d/oom_score", pid); f = fopen(buf, "r"); if (f == NULL) { printf(fopen_msg, buf, strerror(errno)); p.exited = 1; return p; } fscanf(f, "%d", &(p.oom_score)); fclose(f); // Read /proc/[pid]/oom_score_adj snprintf(buf, sizeof(buf), "%d/oom_score_adj", pid); f = fopen(buf, "r"); if (f == NULL) { printf(fopen_msg, buf, strerror(errno)); p.exited = 1; return p; } fscanf(f, "%d", &(p.oom_score_adj)); fclose(f); // Read VmRss from /proc/[pid]/statm snprintf(buf, sizeof(buf), "%d/statm", pid); f = fopen(buf, "r"); if (f == NULL) { printf(fopen_msg, buf, strerror(errno)); p.exited = 1; return p; } fscanf(f, "%*u %lu", &(p.vm_rss)); fclose(f); return p; } /* * Find the process with the largest oom_score and kill it. * See trigger_kernel_oom() for the reason why this is done in userspace. */ static void userspace_kill(DIR* procdir, int sig, int ignore_oom_score_adj, char* notif_command, regex_t* prefer_regex, regex_t* avoid_regex) { struct dirent* d; char buf[256]; int pid; int victim_pid = 0; int victim_badness = 0; unsigned long victim_vm_rss = 0; char name[PATH_MAX]; struct procinfo p; int badness; rewinddir(procdir); while (1) { errno = 0; d = readdir(procdir); if (d == NULL) { if (errno != 0) perror("readdir returned error"); break; } // proc contains lots of directories not related to processes, // skip them if (!isnumeric(d->d_name)) continue; pid = strtoul(d->d_name, NULL, 10); if (pid == 1) // Let's not kill init. continue; p = get_process_stats(pid); if (p.exited == 1) // Process may have died in the meantime continue; badness = p.oom_score; if (ignore_oom_score_adj && p.oom_score_adj > 0) badness -= p.oom_score_adj; name[0] = 0; snprintf(buf, sizeof(buf), "%d/stat", pid); FILE* stat = fopen(buf, "r"); if (stat) { fscanf(stat, "%*d (%[^)]s", name); fclose(stat); } else { perror("could not read process name"); } if (prefer_regex && regexec(prefer_regex, name, (size_t)0, NULL, 0) == 0) { badness += BADNESS_PREFER; } if (avoid_regex && regexec(avoid_regex, name, (size_t)0, NULL, 0) == 0) { badness += BADNESS_AVOID; } if (enable_debug) printf("pid %5d: badness %3d vm_rss %6lu %s\n", pid, badness, p.vm_rss, name); if (badness > victim_badness) { victim_pid = pid; victim_badness = badness; victim_vm_rss = p.vm_rss; if (enable_debug) printf(" ^ new victim (higher badness)\n"); } else if (badness == victim_badness && p.vm_rss > victim_vm_rss) { victim_pid = pid; victim_vm_rss = p.vm_rss; if (enable_debug) printf(" ^ new victim (higher vm_rss)\n"); } } if (victim_pid == 0) { fprintf(stderr, "Error: Could not find a process to kill. Sleeping 10 seconds.\n"); maybe_notify(notif_command, "-i dialog-error 'earlyoom' 'Error: Could not find a process to kill'"); sleep(10); return; } if (sig != 0) { fprintf(stderr, "Killing process %d %s\n", victim_pid, name); char notif_args[200]; snprintf(notif_args, 200, "-i dialog-warning 'earlyoom' 'Killing process %d %s'", victim_pid, name); maybe_notify(notif_command, notif_args); } if (kill(victim_pid, sig) != 0) { perror("Could not kill process"); // Killing the process may have failed because we are not running as root. // In that case, trying again in 100ms will just yield the same error. // Throttle ourselves to not spam the log. fprintf(stderr, "Sleeping 10 seconds\n"); maybe_notify(notif_command, "-i dialog-error 'earlyoom' 'Error: Failed to kill process'"); sleep(10); } } /* * Invoke the kernel oom killer by writing "f" into /proc/sysrq-trigger * * This approach has a few problems: * 1) It is disallowed by default (even for root) on Fedora 20. * You have to first write "1" into /proc/sys/kernel/sysrq to enable the "f" * trigger. * 2) The Chrome web browser assigns a penalty of 300 onto its own tab renderer * processes. On an 8GB RAM machine, this means 2400MB, and will lead to every * tab being killed before the actual memory hog * See https://code.google.com/p/chromium/issues/detail?id=333617 for more info * 3) It is broken in 4.0.5 - see * https://github.com/rfjakob/earlyoom/commit/f7e2cedce8e9605c688d0c6d7dc26b7e81817f02 * Because of these issues, kill_by_rss() is used instead by default. */ void trigger_kernel_oom(int sig, char* notif_command) { FILE* trig_fd; trig_fd = fopen("sysrq-trigger", "w"); if (trig_fd == NULL) { perror("Could not open /proc/sysrq-trigger"); exit(7); } if (sig == 9) { fprintf(stderr, "Invoking oom killer: "); maybe_notify(notif_command, "-i dialog-warning 'earlyoom' 'Invoking OOM killer'"); if (fprintf(trig_fd, "f\n") != 2) { perror("failed"); maybe_notify(notif_command, "-i dialog-error 'earlyoom' 'Error: Failed to invoke OOM killer'"); } else fprintf(stderr, "done\n"); } fclose(trig_fd); } void handle_oom(DIR* procdir, int sig, int kernel_oom_killer, int ignore_oom_score_adj, char* notif_command, regex_t* prefer_regex, regex_t* avoid_regex) { if (kernel_oom_killer) trigger_kernel_oom(sig, notif_command); else userspace_kill(procdir, sig, ignore_oom_score_adj, notif_command, prefer_regex, avoid_regex); } earlyoom-1.0/kill.h000066400000000000000000000001451323335305200143040ustar00rootroot00000000000000/* SPDX-License-Identifier: MIT */ void handle_oom(DIR*, int, int, int, char*, regex_t*, regex_t*); earlyoom-1.0/main.c000066400000000000000000000223051323335305200142720ustar00rootroot00000000000000// SPDX-License-Identifier: MIT /* Check available memory and swap in a loop and start killing * processes if they get too low */ #include #include #include #include #include #include #include #include #include #include #include "kill.h" #include "meminfo.h" /* Arbitrary identifiers for long options that do not have a short * version */ #define LONG_OPT_PREFER 513 #define LONG_OPT_AVOID 514 int set_oom_score_adj(int); int enable_debug = 0; int main(int argc, char* argv[]) { int kernel_oom_killer = 0; unsigned long oom_cnt = 0; /* If the available memory goes below this percentage, we start killing * processes. 10 is a good start. */ int mem_min_percent = 0, swap_min_percent = 0; long mem_min = 0, swap_min = 0; /* Same thing in KiB */ int ignore_oom_score_adj = 0; char* notif_command = NULL; int report_interval = 1; int set_my_priority = 0; char* prefer_cmds = NULL; char* avoid_cmds = NULL; regex_t _prefer_regex; regex_t _avoid_regex; regex_t* prefer_regex = NULL; regex_t* avoid_regex = NULL; /* request line buffering for stdout - otherwise the output * may lag behind stderr */ setlinebuf(stdout); fprintf(stderr, "earlyoom " VERSION "\n"); if (chdir("/proc") != 0) { perror("Could not cd to /proc"); exit(4); } DIR* procdir = opendir("."); if (procdir == NULL) { perror("Could not open /proc"); exit(5); } int c; const char* short_opt = "m:s:M:S:kinN:dvr:ph"; struct option long_opt[] = { { "prefer", required_argument, NULL, LONG_OPT_PREFER }, { "avoid", required_argument, NULL, LONG_OPT_AVOID }, { 0, required_argument, NULL, 0 } }; while ((c = getopt_long(argc, argv, short_opt, long_opt, NULL)) != -1) { switch (c) { case -1: /* no more arguments */ case 0: /* long option toggles */ break; case 'm': mem_min_percent = strtol(optarg, NULL, 10); if (mem_min_percent <= 0) { fprintf(stderr, "-m: Invalid percentage\n"); exit(15); } break; case 's': swap_min_percent = strtol(optarg, NULL, 10); if (swap_min_percent <= 0 || swap_min_percent > 100) { fprintf(stderr, "-s: Invalid percentage\n"); exit(16); } break; case 'M': mem_min = strtol(optarg, NULL, 10); if (mem_min <= 0) { fprintf(stderr, "-M: Invalid KiB value\n"); exit(15); } break; case 'S': swap_min = strtol(optarg, NULL, 10); if (swap_min <= 0) { fprintf(stderr, "-S: Invalid KiB value\n"); exit(16); } break; case 'k': kernel_oom_killer = 1; fprintf(stderr, "Using kernel oom killer\n"); break; case 'i': ignore_oom_score_adj = 1; break; case 'n': notif_command = "notify-send"; break; case 'N': notif_command = optarg; break; case 'd': enable_debug = 1; break; case 'v': // The version has already been printed above exit(0); case 'r': report_interval = strtol(optarg, NULL, 10); if (report_interval < 0) { fprintf(stderr, "-r: Invalid interval\n"); exit(14); } break; case 'p': set_my_priority = 1; break; case LONG_OPT_PREFER: prefer_cmds = optarg; break; case LONG_OPT_AVOID: avoid_cmds = optarg; break; case 'h': fprintf(stderr, "Usage: earlyoom [OPTION]...\n" "\n" " -m PERCENT set available memory minimum to PERCENT of total (default 10 %%)\n" " -s PERCENT set free swap minimum to PERCENT of total (default 10 %%)\n" " -M SIZE set available memory minimum to SIZE KiB\n" " -S SIZE set free swap minimum to SIZE KiB\n" " -k use kernel oom killer instead of own user-space implementation\n" " -i user-space oom killer should ignore positive oom_score_adj values\n" " -n enable notifications using \"notify-send\"\n" " -N COMMAND enable notifications using COMMAND\n" " -d enable debugging messages\n" " -v print version information and exit\n" " -r INTERVAL memory report interval in seconds (default 1), set to 0 to\n" " disable completely\n" " -p set niceness of earlyoom to -20 and oom_score_adj to -1000\n" " --prefer REGEX prefer killing processes matching REGEX\n" " --avoid REGEX avoid killing processes matching REGEX\n" " -h this help text\n"); exit(1); case '?': exit(13); } } if (mem_min_percent && mem_min) { fprintf(stderr, "Can't use -m with -M\n"); exit(2); } if (swap_min_percent && swap_min) { fprintf(stderr, "Can't use -s with -S\n"); exit(2); } if (kernel_oom_killer && ignore_oom_score_adj) { fprintf(stderr, "Kernel oom killer does not support -i\n"); exit(2); } if (kernel_oom_killer && (prefer_cmds || avoid_cmds)) { fprintf(stderr, "Kernel oom killer does not support --prefer/--avoid\n"); exit(2); } if (prefer_cmds) { prefer_regex = &_prefer_regex; if (regcomp(prefer_regex, prefer_cmds, REG_EXTENDED | REG_NOSUB) != 0) { fprintf(stderr, "Could not compile regexp: %s\n", prefer_cmds); exit(6); } } if (avoid_cmds) { avoid_regex = &_avoid_regex; if (regcomp(avoid_regex, avoid_cmds, REG_EXTENDED | REG_NOSUB) != 0) { fprintf(stderr, "Could not compile regexp: %s\n", avoid_cmds); exit(6); } } struct meminfo m = parse_meminfo(); if (mem_min) { mem_min_percent = 100 * mem_min / m.MemTotal; } else { if (!mem_min_percent) { mem_min_percent = 10; } mem_min = m.MemTotal * mem_min_percent / 100; } if (swap_min) { swap_min_percent = 100 * swap_min / m.SwapTotal; } else { if (!swap_min_percent) { swap_min_percent = 10; } swap_min = m.SwapTotal * swap_min_percent / 100; } fprintf(stderr, "mem total: %lu MiB, min: %lu MiB (%d %%)\n", m.MemTotal / 1024, mem_min / 1024, mem_min_percent); fprintf(stderr, "swap total: %lu MiB, min: %lu MiB (%d %%)\n", m.SwapTotal / 1024, swap_min / 1024, swap_min_percent); if (notif_command) fprintf(stderr, "notifications enabled using command: %s\n", notif_command); /* Dry-run oom kill to make sure stack grows to maximum size before * calling mlockall() */ handle_oom(procdir, 0, kernel_oom_killer, ignore_oom_score_adj, notif_command, prefer_regex, avoid_regex); if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0) perror("Could not lock memory - continuing anyway"); if (set_my_priority) { if (setpriority(PRIO_PROCESS, 0, -20) != 0) perror("Could not set priority - continuing anyway"); if (set_oom_score_adj(-1000) != 0) perror("Could not set oom_score_adj to -1000 for earlyoom process - continuing anyway"); } c = 1; // Start at 1 so we do not print another status line immediately report_interval = report_interval * 10; // convert seconds to tenth of second while (1) { m = parse_meminfo(); if (report_interval && c % report_interval == 0) { int swap_free_percent = 0; if (m.SwapTotal > 0) swap_free_percent = m.SwapFree * 100 / m.SwapTotal; printf("mem avail: %lu MiB (%ld %%), swap free: %lu MiB (%d %%)\n", m.MemAvailable / 1024, m.MemAvailable * 100 / m.MemTotal, m.SwapFree / 1024, swap_free_percent); c = 0; } c++; if (m.MemAvailable <= mem_min && m.SwapFree <= swap_min) { fprintf(stderr, "Out of memory! avail: %lu MiB < min: %lu MiB\n", m.MemAvailable / 1024, mem_min / 1024); handle_oom(procdir, 9, kernel_oom_killer, ignore_oom_score_adj, notif_command, prefer_regex, avoid_regex); oom_cnt++; } usleep(100000); // 100ms } return 0; } int set_oom_score_adj(int oom_score_adj) { char buf[256]; pid_t pid = getpid(); snprintf(buf, sizeof(buf), "%d/oom_score_adj", pid); FILE* f = fopen(buf, "w"); if (f == NULL) { return -1; } fprintf(f, "%d", oom_score_adj); fclose(f); return 0; } earlyoom-1.0/meminfo.c000066400000000000000000000046141323335305200150030ustar00rootroot00000000000000// SPDX-License-Identifier: MIT /* Parse /proc/meminfo * Returned values are in kiB */ #include #include // for size_t #include #include #include #include "meminfo.h" /* Parse the contents of /proc/meminfo (in buf), return value of "*name" */ static long get_entry(const char* name, const char* buf) { char* hit = strstr(buf, name); if (hit == NULL) { return -1; } errno = 0; long val = strtol(hit + strlen(name), NULL, 10); if (errno != 0) { perror("Could not convert number"); exit(105); } return val; } /* Like get_entry(), but exit if the value cannot not be found */ static long get_entry_fatal(const char* name, const char* buf) { long val = get_entry(name, buf); if (val == -1) { fprintf(stderr, "Could not find \"%s\"\n", name); exit(104); } return val; } /* If the kernel does not provide MemAvailable (introduced in Linux 3.14), * approximate it using other data we can get */ static long available_guesstimate(const char* buf) { long Cached = get_entry_fatal("Cached:", buf); long MemFree = get_entry_fatal("MemFree:", buf); long Buffers = get_entry_fatal("Buffers:", buf); long Shmem = get_entry_fatal("Shmem:", buf); return MemFree + Cached + Buffers - Shmem; } struct meminfo parse_meminfo() { static FILE* fd; static char buf[8192]; static int guesstimate_warned = 0; struct meminfo m; if (fd == NULL) fd = fopen("/proc/meminfo", "r"); if (fd == NULL) { perror("Could not open /proc/meminfo"); exit(102); } rewind(fd); size_t len = fread(buf, 1, sizeof(buf) - 1, fd); if (len == 0) { perror("Could not read /proc/meminfo"); exit(103); } buf[len] = 0; // Make sure buf is zero-terminated m.MemTotal = get_entry_fatal("MemTotal:", buf); m.SwapTotal = get_entry_fatal("SwapTotal:", buf); m.SwapFree = get_entry_fatal("SwapFree:", buf); m.MemAvailable = get_entry("MemAvailable:", buf); if (m.MemAvailable == -1) { m.MemAvailable = available_guesstimate(buf); if (guesstimate_warned == 0) { fprintf(stderr, "Warning: Your kernel does not provide MemAvailable data (needs 3.14+)\n" " Falling back to guesstimate\n"); guesstimate_warned = 1; } } return m; } earlyoom-1.0/meminfo.h000066400000000000000000000003171323335305200150040ustar00rootroot00000000000000/* SPDX-License-Identifier: MIT */ struct meminfo { long MemTotal; long MemAvailable; long SwapTotal; long SwapFree; /* -1 means no data available */ }; struct meminfo parse_meminfo();