pax_global_header00006660000000000000000000000064134725312710014517gustar00rootroot0000000000000052 comment=b785b32c6722e249386283eb6dc27ca714e0d6b1 earlyoom-1.3/000077500000000000000000000000001347253127100132115ustar00rootroot00000000000000earlyoom-1.3/.clang-format000066400000000000000000000000251347253127100155610ustar00rootroot00000000000000BasedOnStyle: WebKit earlyoom-1.3/.gitignore000066400000000000000000000002071347253127100152000ustar00rootroot00000000000000*~ /earlyoom # generated from MANPAGE.md /earlyoom.1 /earlyoom.1.gz # generated service files /earlyoom.service /earlyoom.initscript earlyoom-1.3/.travis.yml000066400000000000000000000007171347253127100153270ustar00rootroot00000000000000# earlyoom is written in C, but the test suite is written in Go. # The "go" build environment in Travis CI includes gcc, so just # pretend we are a Go project. language: go # Travis by default only pulls the last 50 commits, which means # "git describe" will fail once we are more than 50 commits # away from the last tag. git: depth: 100 addons: apt: packages: - pandoc script: - cc --version - make - make test - make format - git diff earlyoom-1.3/CODE_OF_CONDUCT.md000066400000000000000000000064261347253127100160200ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at jakobunt@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq earlyoom-1.3/Dockerfile000066400000000000000000000002351347253127100152030ustar00rootroot00000000000000FROM gcc as build WORKDIR /usr/src COPY . . ENV CFLAGS -static RUN make ### FROM scratch COPY --from=build /usr/src/earlyoom / ENTRYPOINT ["/earlyoom"] earlyoom-1.3/LICENSE000066400000000000000000000020771347253127100142240ustar00rootroot00000000000000The 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.3/MANPAGE-render.sh000077500000000000000000000000561347253127100160760ustar00rootroot00000000000000#!/bin/bash make earlyoom.1 man ./earlyoom.1 earlyoom-1.3/MANPAGE.md000066400000000000000000000115761347253127100146150ustar00rootroot00000000000000% earlyoom(1) | General Commands Manual # NAME earlyoom - Early OOM Daemon # SYNOPSIS **earlyoom** [**OPTION**]... # 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 to quickly get back to what one was doing after running out of patience. **earlyoom** checks the amount of available memory and free swap up to 10 times a second (less often if there is a lot of free memory). If **both** memory **and** swap are below 10%, it will kill the largest process (highest `oom_score`). The percentage value is configurable via command line arguments. If there is a failure when trying to kill a process, **earlyoom** sleeps for 1 second to limit log spam due to recurring errors. # OPTIONS #### -m PERCENT[,KILL_PERCENT] set available memory minimum to PERCENT of total (default 10 %). earlyoom starts sending SIGTERM once **both** memory **and** swap are below their respective PERCENT setting. It sends SIGKILL once **both** are below their respective KILL_PERCENT setting (default PERCENT/2). Use the same value for PERCENT and KILL_PERCENT if you always want to use SIGKILL. Examples: earlyoom # sets PERCENT=10, KILL_PERCENT=5 earlyoom -m 30 # sets PERCENT=30, KILL_PERCENT=15 earlyoom -m 20,18 # sets PERCENT=20, KILL_PERCENT=18 #### -s PERCENT[,KILL_PERCENT] set free swap minimum to PERCENT of total (default 10 %). Send SIGKILL if at or below KILL_PERCENT (default PERCENT/2), otherwise SIGTERM. You can use `-s 100` to have earlyoom effectively ignore swap usage: Processes are killed once available memory drops below the configured minimum, no matter how much swap is free. Use the same value for PERCENT and KILL_PERCENT if you always want to use SIGKILL. #### -M SIZE[,KILL_SIZE] As an alternative to specifying a percentage of total memory, `-M` sets the available memory minimum to SIZE KiB. The value is internally converted to a percentage. You can only use **either** `-m` **or** `-M`. Send SIGKILL if at or below KILL_SIZE (default SIZE/2), otherwise SIGTERM. #### -S SIZE[,KILL_SIZE] As an alternative to specifying a percentage of total swap, `-S` sets the free swap minimum to SIZE KiB. The value is internally converted to a percentage. You can only use **either** `-s` **or** `-S`. Send SIGKILL if at or below KILL_SIZE (default SIZE/2), otherwise SIGTERM. #### -k removed in earlyoom v1.2, ignored for compatibility #### -i user-space oom killer should ignore positive oom_score_adj values #### -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. With earlyoom v1.2 and higher, floating point numbers are accepted. Due to the adaptive poll rate, when there is a lot of free memory, the actual interval may be up to 1 second longer than the setting. #### -p Increase earlyoom's priority: set niceness of earlyoom to -20 and oom_score_adj to -1000 #### \-\-prefer REGEX prefer killing processes matching REGEX (adds 300 to oom_score) #### \-\-avoid REGEX avoid killing processes matching REGEX (subtracts 300 from oom_score) #### -h, \-\-help this help text # EXIT STATUS 0: Successful program execution. 1: Usage printed (using -h). 2: Switch conflict. 4: Could not cd to /proc 5: Could not open proc 7: Could not open /proc/sysrq-trigger 13: Unknown options. 14: Wrong parameters for other options. 15: Wrong parameters for memory threshold. 16: Wrong parameters for swap threshold. 102: Could not open /proc/meminfo 103: Could not read /proc/meminfo 104: Could not find a specific entry in /proc/meminfo 105: Could not convert number when parse the contents of /proc/meminfo # 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. 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. # MEMORY USAGE About 2 MiB VmRSS. All memory is locked using mlockall() to make sure earlyoom does not slow down in low memory situations. # BUGS If there is zero total swap on earlyoom startup, any `-S` (uppercase "S") values are ignored, a warning is printed, and default swap percentages are used. # AUTHOR The author of earlyoom is Jakob Unterwurzacher ⟨jakobunt@gmail.com⟩. This manual page was written by Yangfl ⟨mmyangfl@gmail.com⟩, for the Debian project (and may be used by others). earlyoom-1.3/Makefile000066400000000000000000000044561347253127100146620ustar00rootroot00000000000000VERSION ?= $(shell git describe --tags --dirty 2> /dev/null) CFLAGS += -Wall -Wextra -DVERSION=\"$(VERSION)\" -g -fstack-protector-all -std=gnu99 DESTDIR ?= PREFIX ?= /usr/local BINDIR ?= /bin SYSCONFDIR ?= /etc SYSTEMDUNITDIR ?= $(SYSCONFDIR)/systemd/system PANDOC := $(shell command -v pandoc 2> /dev/null) ifeq ($(VERSION),) VERSION := "(unknown version)" endif .PHONY: all clean install uninstall format test all: earlyoom earlyoom.1 earlyoom: $(wildcard *.c *.h) Makefile $(CC) $(LDFLAGS) $(CFLAGS) -o $@ $(wildcard *.c) earlyoom.1: MANPAGE.md ifdef PANDOC pandoc MANPAGE.md -s -t man > earlyoom.1 else @echo "pandoc is not installed, skipping earlyoom.1 manpage generation" endif clean: rm -f earlyoom earlyoom.service earlyoom.initscript earlyoom.1 earlyoom.1.gz install: earlyoom.service install-bin install-default install-man install -d $(DESTDIR)$(SYSTEMDUNITDIR) install -m 644 $< $(DESTDIR)$(SYSTEMDUNITDIR) -chcon -t systemd_unit_file_t $(DESTDIR)$(SYSTEMDUNITDIR)/$< -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 ifdef PANDOC install -d $(DESTDIR)$(PREFIX)/share/man/man1/ install -m 644 $< $(DESTDIR)$(PREFIX)/share/man/man1/ endif earlyoom.1.gz: earlyoom.1 ifdef PANDOC gzip -f -k $< endif uninstall: uninstall-bin uninstall-man systemctl disable earlyoom rm -f $(DESTDIR)$(SYSTEMDUNITDIR)/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 # Depends on earlyoom compilation to make sure the syntax is ok. format: earlyoom clang-format -i *.h *.c test: earlyoom cd tests ; go test -v earlyoom-1.3/README.md000066400000000000000000000330321347253127100144710ustar00rootroot00000000000000The Early OOM Daemon ==================== [![Build Status](https://api.travis-ci.org/rfjakob/earlyoom.svg)](https://travis-ci.org/rfjakob/earlyoom) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 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 that it will do. I have yet to be patient enough to wait for it, sitting in front of an unresponsive system. This made me and other people wonder if the oom-killer could be configured to step in earlier: [reddit r/linux][5], [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 free swap up to 10 times a second (less often if there is a lot of free memory). By default if both are below 10%, it will kill the largest process (highest `oom_score`). 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 send the `SIGTERM` signal to 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). #### See also * [nohang](https://github.com/hakavlad/nohang), a similar project like earlyoom, written in Python and with additional features and configuration options. * facebooks's pressure stall information (psi) [kernel patches](http://git.cmpxchg.org/cgit.cgi/linux-psi.git/) and the accompanying [oomd](https://github.com/facebookincubator/oomd) userspace helper. The patches are merged in Linux 4.20. Why not trigger the kernel oom killer? -------------------------------------- Earlyoom does not use `echo f > /proc/sysrq-trigger` because [the Chrome people made their browser (and all electron-based apps - vscode, skype, discord etc) 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 `2 MiB` (`VmRSS`), though only `220 kiB` is private memory (`RssAnon`). The rest is the libc library (`RssFile`) that is shared with other processes. All memory is locked using `mlockall()` to make sure earlyoom does not slow down in low memory situations. Download and compile -------------------- Compiling yourself is easy: ```bash git clone https://github.com/rfjakob/earlyoom.git cd earlyoom make ``` Optional: Run the integrated self-tests: ```bash make test ``` Start earlyoom automatically by registering it as a service: ```bash sudo make install # systemd sudo make install-initscript # non-systemd ``` For Debian 10+ and Ubuntu 18.04+, there's a [Debian package](https://packages.debian.org/search?keywords=earlyoom): ```bash apt install earlyoom ``` For Arch Linux, there's an [AUR package](https://aur.archlinux.org/packages/earlyoom/). Use your favorite AUR helper. For example: ```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 earlyoom v1.2-10-ga8f30d7 mem total: 7834 MiB, swap total: 0 MiB Sending SIGTERM when mem <= 10 % and swap <= 10 %, SIGKILL when mem <= 5 % and swap <= 5 % mem avail: 4667 of 7834 MiB (59 %), swap free: 0 of 0 MiB ( 0 %) mem avail: 4704 of 7834 MiB (60 %), swap free: 0 of 0 MiB ( 0 %) mem avail: 4704 of 7834 MiB (60 %), swap free: 0 of 0 MiB ( 0 %) [...] ``` 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 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`. Configuration file ------------------ If you are running earlyoom as a system service (through systemd or init.d), you can adjust its configuration via the file provided in `/etc/default/earlyoom`. The file already contains some examples in the comments, which you can use to build your own set of configuration based on the supported command line options, for example: ``` EARLYOOM_ARGS="-m 5 -r 60 --avoid '(^|/)(init|Xorg|ssh)$' --prefer '(^|/)(java|chromium)$'" ``` After adjusting the file, simply restart the service to apply the changes. For example, for systemd: ```bash systemctl restart earlyoom ``` Please note that this configuration file has no effect on earlyoom instances outside of systemd/init.d. Command line options -------------------- ``` ./earlyoom -h earlyoom v1.2-10-ga8f30d7 Usage: earlyoom [OPTION]... -m PERCENT[,KILL_PERCENT] set available memory minimum to PERCENT of total (default 10 %). earlyoom sends SIGTERM once below PERCENT, then SIGKILL once below KILL_PERCENT (default PERCENT/2). -s PERCENT[,KILL_PERCENT] set free swap minimum to PERCENT of total (default 10 %). Note: both memory and swap must be below minimum for earlyoom to act. -M SIZE[,KILL_SIZE] set available memory minimum to SIZE KiB -S SIZE[,KILL_SIZE] set free swap minimum to SIZE KiB -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, --help this help text ``` See the [man page](MANPAGE.md) for details. Contribute ---------- Bug reports and pull requests are welcome via github. In particular, I am glad to accept * Use case reports and feedback Changelog --------- * v1.3, 2019-05-26 * Wait for processes to actually exit when sending a signal * This fixes the problem that earlyoom sometimes kills more than one process when one would be enough ([issue #121](https://github.com/rfjakob/earlyoom/issues/121)) * Be more liberal in what limits to accepts for SIGTERM and SIGKILL ([issue #97](https://github.com/rfjakob/earlyoom/issues/97)) * Don't exit with a fatal error if SIGTERM limit < SIGKILL limit * Allow zero SIGKILL limit * Reformat startup output to make it clear that BOTH swap and mem must be <= limit * Add [notify_all_users.py](contrib/notify_all_users.py) helper script * Add [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) (Contributor Covenant 1.4) ([#102](https://github.com/rfjakob/earlyoom/issues/102)) * Fix possibly truncated UTF8 app names in log output ([#110](https://github.com/rfjakob/earlyoom/issues/110)) * v1.2, 2018-10-28 * Implement adaptive sleep time (= adaptive poll rate) to lower CPU usage further ([issue #61](https://github.com/rfjakob/earlyoom/issues/61)) * Remove option to use kernel oom-killer (`-k`, now ignored for compatibility) ([issue #80](https://github.com/rfjakob/earlyoom/issues/80)) * Gracefully handle the case of swap being added or removed after earlyoom was started ([issue 62](https://github.com/rfjakob/earlyoom/issues/62), [commit](https://github.com/rfjakob/earlyoom/commit/88e58903fec70b105aebba39cd584add5e1d1532)) * Implement staged kill: first SIGTERM, then SIGKILL, with configurable limits ([issue #67](https://github.com/rfjakob/earlyoom/issues/67)) * v1.1, 2018-07-07 * Fix possible shell code injection through GUI notifications ([commit](https://github.com/rfjakob/earlyoom/commit/ab79aa3895077676f50120f15e2bb22915446db9)) * On failure to kill any process, only sleep 1 second instead of 10 ([issue #74](https://github.com/rfjakob/earlyoom/issues/74)) * Send the GUI notification *after* killing, not before ([issue #73](https://github.com/rfjakob/earlyoom/issues/73)) * Accept `--help` in addition to `-h` * Fix wrong process name displayed in kill notification ([commit](https://github.com/rfjakob/earlyoom/commit/1466e9c8f7997108758d9585442a96c6806c040e)) * Fix possible division by zero with `-S` ([commit](https://github.com/rfjakob/earlyoom/commit/a0c4b26dfef8b38ef81c7b0b907442f344a3e115)) * v1.0, 2018-01-28 * Add `--prefer` and `--avoid` options (@TomJohnZ) * Add support for GUI notifications, add options `-n` and `-N` * 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 [5]: https://www.reddit.com/r/linux/comments/56r4xj/why_are_low_memory_conditions_handled_so_badly/ earlyoom-1.3/contrib/000077500000000000000000000000001347253127100146515ustar00rootroot00000000000000earlyoom-1.3/contrib/.gitignore000066400000000000000000000000071347253127100166360ustar00rootroot00000000000000zombie earlyoom-1.3/contrib/Makefile000066400000000000000000000001551347253127100163120ustar00rootroot00000000000000.PHONY: format format: autopep8 -i *.py clang-format -i *.c zombie: gcc -Wall -Wextra -o zombie zombie.c earlyoom-1.3/contrib/mon.sh000077500000000000000000000035211347253127100160020ustar00rootroot00000000000000#!/bin/bash # # Monitor the memory usage and state of a PID in # a 0.1 second loop. # # Example with "sudo memtester 10G" running in the background: # # $ ./mon.sh $(pgrep memtester) # 0 MemAvailable: 10158052 kB VmRSS: 10487200 kB statm: 2622018 2621800 346 4 0 2621518 0 stat: 9067 (memtester) R 9065 9065 4 # 0 MemAvailable: 10153776 kB VmRSS: 10487200 kB statm: 2622018 2621800 346 4 0 2621518 0 stat: 9067 (memtester) R 9065 9065 4 # 0 MemAvailable: 10153532 kB VmRSS: 10487200 kB statm: 2622018 2621800 346 4 0 2621518 0 stat: 9067 (memtester) R 9065 9065 4 # ***sudo pkill memtester*** # 0 MemAvailable: 10154260 kB statm: 0 0 0 0 0 0 0 stat: 9067 (memtester) R 9065 9065 4 # 0 MemAvailable: 10154296 kB statm: 0 0 0 0 0 0 0 stat: 9067 (memtester) R 9065 9065 4 # 0 MemAvailable: 10154280 kB statm: 0 0 0 0 0 0 0 stat: 9067 (memtester) R 9065 9065 4 # 0 MemAvailable: 10154256 kB statm: 0 0 0 0 0 0 0 stat: 9067 (memtester) R 9065 9065 4 # 0 MemAvailable: 10146932 kB statm: 0 0 0 0 0 0 0 stat: 9067 (memtester) R 9065 9065 4 # 0 MemAvailable: 11038764 kB statm: 0 0 0 0 0 0 0 stat: 9067 (memtester) R 9065 9065 4 # 0 MemAvailable: 13773280 kB statm: 0 0 0 0 0 0 0 stat: 9067 (memtester) R 9065 9065 4 # 0 MemAvailable: 16593848 kB statm: 0 0 0 0 0 0 0 stat: 9067 (memtester) R 9065 9065 4 # 0 MemAvailable: 19330180 kB statm: 0 0 0 0 0 0 0 stat: 9067 (memtester) R 9065 9065 4 # ***actual exit*** # 1 MemAvailable: 20632628 kB statm: stat: # 1 MemAvailable: 20632992 kB statm: stat: # 1 MemAvailable: 20633244 kB statm: stat: set -u while sleep 0.1; do test -e /proc/$1 PROC=$? AVAIL=$(grep MemAvailable /proc/meminfo) RSS=$(grep VmRSS /proc/$1/status 2> /dev/null) STATM=$(cat /proc/$1/statm 2> /dev/null) STAT=$(head -c 30 /proc/$1/stat 2> /dev/null) echo "$PROC $AVAIL $RSS statm: $STATM stat: $STAT" done earlyoom-1.3/contrib/notify_all_users.py000077500000000000000000000072321347253127100206130ustar00rootroot00000000000000#!/usr/bin/env python3 # # Send a GUI notification to all logged-in users. # Written by https://github.com/hakavlad . # # Example: # ./notify_all_users.py earlyoom "Low memory! Killing/Terminating process 2233 tail" # # Notification that should pop up: # earlyoom # Low memory! Killing/Terminating process 2233 tail from sys import argv from os import listdir from subprocess import Popen, TimeoutExpired if len(argv) < 2 or argv[1] == "-h" or argv[1] == "--help": print("Usage:") print(" %s [notify-send options] summary [body text]" % (argv[0])) print("Examples:") print(" %s mytitle mytext" % (argv[0])) print(" %s -i dialog-warning earlyoom \"killing process X\"" % (argv[0])) exit(1) wait_time = 10 display_env = 'DISPLAY=' dbus_env = 'DBUS_SESSION_BUS_ADDRESS=' user_env = 'USER=' def rline1(path): """read 1st line from path.""" with open(path) as f: for line in f: return line def re_pid_environ(pid): """ read environ of 1 process returns tuple with USER, DBUS, DISPLAY like follow: ('user', 'DISPLAY=:0', 'DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus') returns None if these vars is not in /proc/[pid]/environ """ try: env = str(rline1('/proc/' + pid + '/environ')) if display_env in env and dbus_env in env and user_env in env: env_list = env.split('\x00') # iterating over a list of process environment variables for i in env_list: if i.startswith(user_env): user = i continue if i.startswith(display_env): display = i[:10] continue if i.startswith(dbus_env): dbus = i continue if i.startswith('HOME='): # exclude Display Manager's user if i.startswith('HOME=/var'): return None env = user.partition('USER=')[2], display, dbus return env except FileNotFoundError: return None except ProcessLookupError: return None def root_notify_env(): """return set(user, display, dbus)""" unsorted_envs_list = [] # iterates over processes, find processes with suitable env for pid in listdir('/proc'): if pid[0].isdecimal() is False: continue one_env = re_pid_environ(pid) unsorted_envs_list.append(one_env) env = set(unsorted_envs_list) env.discard(None) # deduplicate dbus new_env = [] end = [] for i in env: key = i[0] + i[1] if key not in end: end.append(key) new_env.append(i) else: continue return new_env list_with_envs = root_notify_env() # if somebody logged in with GUI if len(list_with_envs) > 0: # iterating over logged-in users for i in list_with_envs: username, display_env, dbus_env = i[0], i[1], i[2] display_tuple = display_env.partition('=') dbus_tuple = dbus_env.partition('=') display_value = display_tuple[2] dbus_value = dbus_tuple[2] with Popen([ 'sudo', '-u', username, 'env', 'DISPLAY=' + display_value, 'DBUS_SESSION_BUS_ADDRESS=' + dbus_value, 'notify-send', '--icon=dialog-warning', argv[1], argv[2] ]) as proc: try: proc.wait(timeout=wait_time) except TimeoutExpired: proc.kill() print('TimeoutExpired: notify user:' + username) else: print('Nobody logged-in with GUI. Nothing to do.') earlyoom-1.3/contrib/oomstat/000077500000000000000000000000001347253127100163375ustar00rootroot00000000000000earlyoom-1.3/contrib/oomstat/.gitignore000066400000000000000000000000101347253127100203160ustar00rootroot00000000000000oomstat earlyoom-1.3/contrib/oomstat/oomstat.go000066400000000000000000000045411347253127100203600ustar00rootroot00000000000000package main import ( "fmt" "io/ioutil" "log" "strconv" "strings" "time" "golang.org/x/sys/unix" ) func main() { t0 := time.Now() err := unix.Mlockall(unix.MCL_CURRENT | unix.MCL_FUTURE | unix.MCL_ONFAULT) if err != nil { fmt.Printf("warning: mlockall: %v. Run as root?\n", err) } fmt.Println("Time MemAvail SwapFree Some Full") some2, full2 := pressure() const interval = 100 for { t1 := time.Now() t := t1.Sub(t0).Seconds() some, full := pressure() m := meminfo() fmt.Printf("%4.1f %5d %4d %3d %2d\n", t, m.memAvailableMiB, m.swapFreeMiB, (some-some2)/interval/10, (full-full2)/interval/10) some2, full2 = some, full time.Sleep(interval * time.Millisecond) } } func pressure() (some int, full int) { /* $ cat /proc/pressure/memory some avg10=0.00 avg60=0.03 avg300=0.65 total=28851712 full avg10=0.00 avg60=0.01 avg300=0.27 total=12963374 */ buf, err := ioutil.ReadFile("/proc/pressure/memory") if err != nil { log.Fatal(err) } fields := strings.Fields(string(buf)) some, err = strconv.Atoi(fields[4][6:]) if err != nil { log.Fatal(err) } full, err = strconv.Atoi(fields[9][6:]) if err != nil { log.Fatal(err) } return } type meminfoStruct struct { memAvailableMiB int memTotalMiB int memAvailablePercent int swapFreeMiB int swapTotalMiB int swapFreePercent int } func atoi(s string) int { val, err := strconv.Atoi(s) if err != nil { log.Fatal(err) } return val } func meminfo() (m meminfoStruct) { /* $ cat /proc/meminfo MemTotal: 24537156 kB MemFree: 19759616 kB MemAvailable: 19891772 kB Buffers: 20564 kB Cached: 1029436 kB [...] SwapTotal: 1049596 kB SwapFree: 201864 kB [...] */ buf, err := ioutil.ReadFile("/proc/meminfo") if err != nil { log.Fatal(err) } fields := strings.Fields(string(buf)) for i, v := range fields { switch v { case "MemAvailable:": m.memAvailableMiB = atoi(fields[i+1]) / 1024 case "SwapFree:": m.swapFreeMiB = atoi(fields[i+1]) / 1024 case "MemTotal:": m.memTotalMiB = atoi(fields[i+1]) / 1024 case "SwapTotal:": m.swapTotalMiB = atoi(fields[i+1]) / 1024 } } m.memAvailablePercent = m.memAvailableMiB * 100 / m.memTotalMiB if m.swapTotalMiB > 0 { m.swapFreePercent = m.swapFreeMiB * 100 / m.swapTotalMiB } return } earlyoom-1.3/contrib/utf8_membomb.sh000077500000000000000000000007161347253127100176000ustar00rootroot00000000000000#!/bin/bash # Testcase for https://github.com/rfjakob/earlyoom/issues/110 # # earlyoom output should look like this: # # sending SIGTERM to process 28570 "tail_😀😀": badness 629, VmRSS 15048 MiB # # and not like this: # # sending SIGTERM to process 28491 "tail_😀😀�": badness 630, VmRSS 15076 MiB set -eu cd $(mktemp -d) TAIL=$(which tail) ln -s $TAIL tail_😀😀😀😀😀😀😀😀 exec ./tail_😀😀😀😀😀😀😀😀 /dev/zero earlyoom-1.3/contrib/zombie.c000066400000000000000000000012601347253127100163010ustar00rootroot00000000000000/* Creates a zombie child process. Automatically exits after 10 minutes. Should look like this in ps: $ ps auxwwww | grep zombie jakob 7513 0.0 0.0 2172 820 pts/1 S+ 13:44 0:00 ./zombie jakob 7514 0.0 0.0 0 0 pts/1 Z+ 13:44 0:00 [zombie] */ #include #include #include #include #include int main() { pid_t pid = fork(); if (pid < 0) { perror("fork failed"); exit(1); } if (pid > 0) { /* parent */ sleep(600); int wstatus; wait(&wstatus); exit(0); } else { /* child */ exit(0); } } earlyoom-1.3/earlyoom.default000066400000000000000000000010621347253127100164050ustar00rootroot00000000000000# Default settings for earlyoom. This file is sourced by /bin/sh from # /etc/init.d/earlyoom or by systemd from earlyoom.service. # Options to pass to earlyoom EARLYOOM_ARGS="-r 60" # Examples: # Print memory report every second instead of every minute # EARLYOOM_ARGS="-r 1" # Available minimum memory 5% # EARLYOOM_ARGS="-m 5" # Available minimum memory 15% and free minimum swap 5% # EARLYOOM_ARGS="-m 15 -s 5" # Avoid killing processes whose name matches this regexp # EARLYOOM_ARGS="--avoid '(^|/)(init|X|sshd|firefox)$'" # See more at `earlyoom -h' earlyoom-1.3/earlyoom.initscript.in000077500000000000000000000106641347253127100175710ustar00rootroot00000000000000#! /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.3/earlyoom.service.in000066400000000000000000000003611347253127100170270ustar00rootroot00000000000000[Unit] Description=Early OOM Daemon Documentation=man:earlyoom(1) https://github.com/rfjakob/earlyoom [Service] EnvironmentFile=-:SYSCONFDIR:/default/earlyoom ExecStart=:TARGET:/earlyoom $EARLYOOM_ARGS [Install] WantedBy=multi-user.target earlyoom-1.3/globals.c000066400000000000000000000000521347253127100147750ustar00rootroot00000000000000int enable_debug = 0; long page_size = 0; earlyoom-1.3/globals.h000066400000000000000000000000601347253127100150010ustar00rootroot00000000000000extern int enable_debug; extern long page_size; earlyoom-1.3/kill.c000066400000000000000000000175371347253127100143250ustar00rootroot00000000000000// SPDX-License-Identifier: MIT /* Kill the most memory-hungy process */ #include #include #include #include // for PATH_MAX #include #include #include #include #include #include #include #include "kill.h" #include "meminfo.h" #include "msg.h" #define BADNESS_PREFER 300 #define BADNESS_AVOID -300 extern int enable_debug; extern long page_size; void sanitize(char* s); 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[PATH_MAX + 2000]; snprintf(notif, sizeof(notif), "%s %s", notif_command, notif_args); if (system(notif) != 0) warn("system('%s') failed: %s\n", notif, strerror(errno)); } /* * Send the selected signal to "pid" and wait for the process to exit * (max 10 seconds) */ int kill_wait(const poll_loop_args_t args, pid_t pid, int sig) { meminfo_t m = { 0 }; const int poll_ms = 100; int res = kill(pid, sig); if (res != 0) { return res; } /* signal 0 does not kill the process. Don't wait for it to exit */ if (sig == 0) { return 0; } for (int i = 0; i < 100; i++) { float secs = ((float)i) * poll_ms / 1000; // We have sent SIGTERM but now have dropped below SIGKILL limits. // Escalate to SIGKILL. if (sig != SIGKILL) { m = parse_meminfo(); if (enable_debug) { print_mem_stats(0, m); } if (m.MemAvailablePercent <= args.mem_kill_percent && m.SwapFreePercent <= args.swap_kill_percent) { sig = SIGKILL; res = kill(pid, sig); // kill first, print after warn("escalating to SIGKILL after %.1f seconds\n", secs); if (res != 0) { return res; } } } else if (enable_debug) { m = parse_meminfo(); print_mem_stats(0, m); } if (!is_alive(pid)) { warn("process exited after %.1f seconds\n", secs); return 0; } usleep(poll_ms * 1000); } errno = ETIME; return -1; } /* * Find the process with the largest oom_score and kill it. */ void kill_largest_process(const poll_loop_args_t args, int sig) { struct dirent* d; char buf[256]; int pid; int victim_pid = 0; int victim_badness = 0; unsigned long victim_vm_rss = 0; char victim_name[256] = { 0 }; struct procinfo p; int badness; struct timespec t0 = { 0 }, t1 = { 0 }; if (enable_debug) { clock_gettime(CLOCK_MONOTONIC, &t0); } // main() makes sure that we are in /proc DIR* procdir = opendir("."); if (procdir == NULL) { fatal(5, "Could not open /proc: %s", strerror(errno)); } while (1) { errno = 0; d = readdir(procdir); if (d == NULL) { if (errno != 0) warn("userspace_kill: readdir error: %s", strerror(errno)); 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; if (p.VmRSSkiB == 0) // Skip kernel threads continue; badness = p.oom_score; if (args.ignore_oom_score_adj && p.oom_score_adj > 0) badness -= p.oom_score_adj; char name[256] = { 0 }; snprintf(buf, sizeof(buf), "%d/comm", pid); FILE* comm = fopen(buf, "r"); if (comm) { const int TASK_COMM_LEN = 16; int n = fread(name, 1, TASK_COMM_LEN, comm); // Strip trailing newline if (n > 1) { name[n - 1] = 0; } else { warn("reading %s failed: %s", buf, strerror(errno)); } fclose(comm); } else { warn("could not open %s: %s", buf, strerror(errno)); } // The kernel truncates /proc/[pid]/comm at 16 bytes. This // may result in broken utf8, which causes problems when // viewing the logs. Fix it. fix_truncated_utf8(name); if (args.prefer_regex && regexec(args.prefer_regex, name, (size_t)0, NULL, 0) == 0) { badness += BADNESS_PREFER; } if (args.avoid_regex && regexec(args.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.VmRSSkiB, name); if (badness > victim_badness) { victim_pid = pid; victim_badness = badness; victim_vm_rss = p.VmRSSkiB; strncpy(victim_name, name, sizeof(victim_name)); if (enable_debug) printf(" ^ new victim (higher badness)\n"); } else if (badness == victim_badness && p.VmRSSkiB > victim_vm_rss) { victim_pid = pid; victim_vm_rss = p.VmRSSkiB; strncpy(victim_name, name, sizeof(victim_name)); if (enable_debug) printf(" ^ new victim (higher vm_rss)\n"); } } // end of while(1) loop closedir(procdir); if (victim_pid == 0) { warn("Could not find a process to kill. Sleeping 1 second.\n"); maybe_notify(args.notif_command, "-i dialog-error 'earlyoom' 'Error: Could not find a process to kill. Sleeping 1 second.'"); sleep(1); return; } if (enable_debug) { clock_gettime(CLOCK_MONOTONIC, &t1); long delta = (t1.tv_sec - t0.tv_sec) * 1000000 + (t1.tv_nsec - t0.tv_nsec) / 1000; printf("selecting victim took %ld.%03ld ms\n", delta / 1000, delta % 1000); } char* sig_name = "?"; if (sig == SIGTERM) { sig_name = "SIGTERM"; } else if (sig == SIGKILL) { sig_name = "SIGKILL"; } // sig == 0 is used as a self-test during startup. Don't notifiy the user. if (sig != 0) { warn("sending %s to process %d \"%s\": badness %d, VmRSS %lu MiB\n", sig_name, victim_pid, victim_name, victim_badness, victim_vm_rss / 1024); } int res = kill_wait(args, victim_pid, sig); int saved_errno = errno; // Send the GUI notification AFTER killing a process. This makes it more likely // that there is enough memory to spawn the notification helper. if (sig != 0) { char notif_args[PATH_MAX + 1000]; // maybe_notify() calls system(). We must sanitize the strings we pass. sanitize(victim_name); snprintf(notif_args, sizeof(notif_args), "-i dialog-warning 'earlyoom' 'Low memory! Killing process %d %s'", victim_pid, victim_name); maybe_notify(args.notif_command, notif_args); } if (sig == 0) { return; } if (res != 0) { warn("kill failed: %s\n", strerror(saved_errno)); maybe_notify(args.notif_command, "-i dialog-error 'earlyoom' 'Error: Failed to 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. if (saved_errno == EPERM) { warn("sleeping 1 second\n"); sleep(1); } } } earlyoom-1.3/kill.h000066400000000000000000000014071347253127100143170ustar00rootroot00000000000000/* SPDX-License-Identifier: MIT */ #ifndef KILL_H #define KILL_H #include typedef struct { /* if the available memory AND swap goes below these percentages, * we start killing processes */ int mem_term_percent; int mem_kill_percent; int swap_term_percent; int swap_kill_percent; /* ignore /proc/PID/oom_score_adj? */ bool ignore_oom_score_adj; /* notifcation command to launch when killing something. NULL = no-op. */ char* notif_command; /* prefer/avoid killing these processes. NULL = no-op. */ regex_t* prefer_regex; regex_t* avoid_regex; /* memory report interval, in milliseconds */ int report_interval_ms; } poll_loop_args_t; void kill_largest_process(poll_loop_args_t args, int sig); #endif earlyoom-1.3/main.c000066400000000000000000000316571347253127100143150ustar00rootroot00000000000000// 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 #include "globals.h" #include "kill.h" #include "meminfo.h" #include "msg.h" /* Don't fail compilation if the user has an old glibc that * does not define MCL_ONFAULT. The kernel may still be recent * enough to support the flag. */ #ifndef MCL_ONFAULT #define MCL_ONFAULT 4 #endif /* Arbitrary identifiers for long options that do not have a short * version */ enum { LONG_OPT_PREFER = 513, LONG_OPT_AVOID, }; static int set_oom_score_adj(int); static void poll_loop(const poll_loop_args_t args); int main(int argc, char* argv[]) { poll_loop_args_t args = { .mem_term_percent = 10, .swap_term_percent = 10, .mem_kill_percent = 5, .swap_kill_percent = 5, .report_interval_ms = 1000, /* omitted fields are set to zero */ }; int set_my_priority = 0; char* prefer_cmds = NULL; char* avoid_cmds = NULL; regex_t _prefer_regex; regex_t _avoid_regex; page_size = sysconf(_SC_PAGESIZE); /* request line buffering for stdout - otherwise the output * may lag behind stderr */ setlinebuf(stdout); fprintf(stderr, "earlyoom " VERSION "\n"); if (chdir("/proc") != 0) { fatal(4, "Could not cd to /proc: %s", strerror(errno)); } meminfo_t m = parse_meminfo(); 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 }, { "help", no_argument, NULL, 'h' }, { 0, 0, NULL, 0 } /* end-of-array marker */ }; bool have_m = 0, have_M = 0, have_s = 0, have_S = 0; while ((c = getopt_long(argc, argv, short_opt, long_opt, NULL)) != -1) { float report_interval_f = 0; term_kill_tuple_t tuple; switch (c) { case -1: /* no more arguments */ case 0: /* long option toggles */ break; case 'm': // Use 99 as upper limit. Passing "-m 100" makes no sense. tuple = parse_term_kill_tuple(optarg, 99); if (strlen(tuple.err)) { fatal(15, "-m: %s", tuple.err); } args.mem_term_percent = tuple.term; args.mem_kill_percent = tuple.kill; have_m = 1; break; case 's': // Using "-s 100" is a valid way to ignore swap usage tuple = parse_term_kill_tuple(optarg, 100); if (strlen(tuple.err)) { fatal(16, "-s: %s", tuple.err); } args.swap_term_percent = tuple.term; args.swap_kill_percent = tuple.kill; have_s = 1; break; case 'M': tuple = parse_term_kill_tuple(optarg, m.MemTotalKiB * 100 / 99); if (strlen(tuple.err)) { fatal(15, "-M: %s", tuple.err); } args.mem_term_percent = 100 * tuple.term / m.MemTotalKiB; args.mem_kill_percent = 100 * tuple.kill / m.MemTotalKiB; have_M = 1; break; case 'S': if (m.SwapTotalKiB == 0) { warn("warning: -S: total swap is zero, using default percentages\n"); break; } tuple = parse_term_kill_tuple(optarg, m.SwapTotalKiB * 100 / 99); if (strlen(tuple.err)) { fatal(16, "-S: %s", tuple.err); } args.swap_term_percent = 100 * tuple.term / m.SwapTotalKiB; args.swap_kill_percent = 100 * tuple.kill / m.SwapTotalKiB; have_S = 1; break; case 'k': fprintf(stderr, "Option -k is ignored since earlyoom v1.2\n"); break; case 'i': args.ignore_oom_score_adj = 1; fprintf(stderr, "Ignoring oom_score_adj\n"); break; case 'n': args.notif_command = "notify-send"; fprintf(stderr, "Notifying using '%s'\n", args.notif_command); break; case 'N': args.notif_command = optarg; fprintf(stderr, "Notifying using '%s'\n", args.notif_command); break; case 'd': enable_debug = 1; break; case 'v': // The version has already been printed above exit(0); case 'r': report_interval_f = strtof(optarg, NULL); if (report_interval_f < 0) { fatal(14, "-r: invalid interval '%s'\n", optarg); } args.report_interval_ms = report_interval_f * 1000; 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: %s [OPTION]...\n" "\n" " -m PERCENT[,KILL_PERCENT] set available memory minimum to PERCENT of total\n" " (default 10 %%).\n" " earlyoom sends SIGTERM once below PERCENT, then\n" " SIGKILL once below KILL_PERCENT (default PERCENT/2).\n" " -s PERCENT[,KILL_PERCENT] set free swap minimum to PERCENT of total (default\n" " 10 %%).\n" " Note: both memory and swap must be below minimum for\n" " earlyoom to act.\n" " -M SIZE[,KILL_SIZE] set available memory minimum to SIZE KiB\n" " -S SIZE[,KILL_SIZE] set free swap minimum to SIZE KiB\n" " -i user-space oom killer should ignore positive\n" " 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\n" " to 0 to disable completely\n" " -p set niceness of earlyoom to -20 and oom_score_adj to\n" " -1000\n" " --prefer REGEX prefer killing processes matching REGEX\n" " --avoid REGEX avoid killing processes matching REGEX\n" " -h, --help this help text\n", argv[0]); exit(0); case '?': fprintf(stderr, "Try 'earlyoom --help' for more information.\n"); exit(13); } } /* while getopt */ if (optind < argc) { fatal(13, "extra argument not understood: '%s'\n", argv[optind]); } if (have_m && have_M) { fatal(2, "can't use both -m and -M\n"); } if (have_s && have_S) { fatal(2, "can't use both -s and -S\n"); } if (prefer_cmds) { args.prefer_regex = &_prefer_regex; if (regcomp(args.prefer_regex, prefer_cmds, REG_EXTENDED | REG_NOSUB) != 0) { fatal(6, "could not compile regexp '%s'\n", prefer_cmds); } fprintf(stderr, "Prefering to kill process names that match regex '%s'\n", prefer_cmds); } if (avoid_cmds) { args.avoid_regex = &_avoid_regex; if (regcomp(args.avoid_regex, avoid_cmds, REG_EXTENDED | REG_NOSUB) != 0) { fatal(6, "could not compile regexp '%s'\n", avoid_cmds); } fprintf(stderr, "Avoiding to kill process names that match regex '%s'\n", avoid_cmds); } if (set_my_priority) { bool fail = 0; if (setpriority(PRIO_PROCESS, 0, -20) != 0) { warn("Could not set priority: %s. Continuing anyway\n", strerror(errno)); fail = 1; } int ret = set_oom_score_adj(-1000); if (ret != 0) { warn("Could not set oom_score_adj: %s. Continuing anyway\n", strerror(ret)); fail = 1; } if (!fail) { fprintf(stderr, "Priority was raised successfully\n"); } } // Print memory limits fprintf(stderr, "mem total: %4d MiB, swap total: %4d MiB\n", m.MemTotalMiB, m.SwapTotalMiB); fprintf(stderr, "sending SIGTERM when mem <= %2d %% and swap <= %2d %%,\n", args.mem_term_percent, args.swap_term_percent); fprintf(stderr, " SIGKILL when mem <= %2d %% and swap <= %2d %%\n", args.mem_kill_percent, args.swap_kill_percent); /* Dry-run oom kill to make sure stack grows to maximum size before * calling mlockall() */ if (enable_debug) printf("dry-running kill_largest_process()...\n"); kill_largest_process(args, 0); int err = mlockall(MCL_CURRENT | MCL_FUTURE | MCL_ONFAULT); // kernels older than 4.4 don't support MCL_ONFAULT. Retry without it. if (err != 0) { err = mlockall(MCL_CURRENT | MCL_FUTURE); } if (err != 0) { perror("Could not lock memory - continuing anyway"); } // Jump into main poll loop poll_loop(args); return 0; } // Returns errno (success = 0) static 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 returns a negative error code on failure int ret1 = fprintf(f, "%d", oom_score_adj); // fclose returns a non-zero value on failure and errno contains the error code int ret2 = fclose(f); if (ret1 < 0) { return -ret1; } if (ret2) { return errno; } return 0; } /* Calculate the time we should sleep based upon how far away from the memory and swap * limits we are (headroom). Returns a millisecond value between 100 and 1000 (inclusive). * The idea is simple: if memory and swap can only fill up so fast, we know how long we can sleep * without risking to miss a low memory event. */ static int sleep_time_ms(const poll_loop_args_t* args, const meminfo_t* m) { // Maximum expected memory/swap fill rate. In kiB per millisecond ==~ MiB per second. const int mem_fill_rate = 6000; // 6000MiB/s seen with "stress -m 4 --vm-bytes 4G" const int swap_fill_rate = 800; // 800MiB/s seen with membomb on ZRAM // Clamp calculated value to this range (milliseconds) const int min_sleep = 100; const int max_sleep = 1000; int mem_headroom_kib = (m->MemAvailablePercent - args->mem_term_percent) * 10 * m->MemTotalMiB; if (mem_headroom_kib < 0) { mem_headroom_kib = 0; } int swap_headroom_kib = (m->SwapFreePercent - args->swap_term_percent) * 10 * m->SwapTotalMiB; if (swap_headroom_kib < 0) { swap_headroom_kib = 0; } int ms = mem_headroom_kib / mem_fill_rate + swap_headroom_kib / swap_fill_rate; if (ms < min_sleep) { return min_sleep; } if (ms > max_sleep) { return max_sleep; } return ms; } static void poll_loop(const poll_loop_args_t args) { meminfo_t m = { 0 }; // Print a a memory report when this reaches zero. We start at zero so // we print the first report immediately. int report_countdown_ms = 0; while (1) { int sig = 0; m = parse_meminfo(); if (m.MemAvailablePercent <= args.mem_kill_percent && m.SwapFreePercent <= args.swap_kill_percent) { print_mem_stats(1, m); warn("low memory! at or below SIGKILL limits: mem %d %%, swap %d %%\n", args.mem_kill_percent, args.swap_kill_percent); sig = SIGKILL; } else if (m.MemAvailablePercent <= args.mem_term_percent && m.SwapFreePercent <= args.swap_term_percent) { print_mem_stats(1, m); warn("low memory! at or below SIGTERM limits: mem %d %%, swap %d %%\n", args.mem_term_percent, args.swap_term_percent); sig = SIGTERM; } if (sig) { kill_largest_process(args, sig); } else if (args.report_interval_ms && report_countdown_ms <= 0) { print_mem_stats(0, m); report_countdown_ms = args.report_interval_ms; } int sleep_ms = sleep_time_ms(&args, &m); if (enable_debug) { printf("adaptive sleep time: %d ms\n", sleep_ms); } usleep(sleep_ms * 1000); report_countdown_ms -= sleep_ms; } } earlyoom-1.3/meminfo.c000066400000000000000000000133511347253127100150120ustar00rootroot00000000000000// SPDX-License-Identifier: MIT /* Parse /proc/meminfo * Returned values are in kiB */ #include #include // for size_t #include #include #include #include "globals.h" #include "meminfo.h" #include "msg.h" /* Parse the contents of /proc/meminfo (in buf), return value of "name" * (example: MemTotal) */ 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("get_entry: strtol() failed"); return -1; } return val; } /* Like get_entry(), but exit if the value cannot be found */ static long get_entry_fatal(const char* name, const char* buf) { long val = get_entry(name, buf); if (val == -1) { fatal(104, "could not find entry '%s' in /proc/meminfo\n"); } 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; } meminfo_t parse_meminfo() { static FILE* fd; static char buf[8192]; static int guesstimate_warned = 0; meminfo_t m; if (fd == NULL) fd = fopen("/proc/meminfo", "r"); if (fd == NULL) { fatal(102, "could not open /proc/meminfo: %s\n", strerror(errno)); } rewind(fd); size_t len = fread(buf, 1, sizeof(buf) - 1, fd); if (len == 0) { fatal(102, "could not read /proc/meminfo: %s\n", strerror(errno)); } buf[len] = 0; // Make sure buf is zero-terminated m.MemTotalKiB = get_entry_fatal("MemTotal:", buf); m.SwapTotalKiB = get_entry_fatal("SwapTotal:", buf); long SwapFree = get_entry_fatal("SwapFree:", buf); long MemAvailable = get_entry("MemAvailable:", buf); if (MemAvailable == -1) { 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; } } // Calculate percentages m.MemAvailablePercent = MemAvailable * 100 / m.MemTotalKiB; if (m.SwapTotalKiB > 0) { m.SwapFreePercent = SwapFree * 100 / m.SwapTotalKiB; } else { m.SwapFreePercent = 0; } // Convert kiB to MiB m.MemTotalMiB = m.MemTotalKiB / 1024; m.MemAvailableMiB = MemAvailable / 1024; m.SwapTotalMiB = m.SwapTotalKiB / 1024; m.SwapFreeMiB = SwapFree / 1024; return m; } bool is_alive(int pid) { char buf[256]; // Read /proc/[pid]/stat snprintf(buf, sizeof(buf), "/proc/%d/stat", pid); FILE* f = fopen(buf, "r"); if (f == NULL) { // Process is gone - good. return false; } // File content looks like this: // 10751 (cat) R 2663 10751 2663[...] char state; if (fscanf(f, "%*d %*s %c", &state) < 1) { warn("is_alive: fscanf() failed: %s\n", strerror(errno)); return false; } fclose(f); if (enable_debug) printf("process state: %c\n", state); if (state == 'Z') { // A zombie process does not use any memory. Consider it dead. return false; } return true; } /* Read /proc/pid/{oom_score, oom_score_adj, statm} * Caller must ensure that we are already in the /proc/ directory */ struct procinfo get_process_stats(int pid) { const char* const fopen_msg = "fopen %s failed: %s\n"; char buf[256]; FILE* f; struct procinfo p = { 0 }; // Read /proc/[pid]/oom_score snprintf(buf, sizeof(buf), "%d/oom_score", pid); f = fopen(buf, "r"); if (f == NULL) { // ENOENT just means that process has already exited. // Not need to bug the user. if (errno != ENOENT) { printf(fopen_msg, buf, strerror(errno)); } p.exited = 1; return p; } if (fscanf(f, "%d", &(p.oom_score)) < 1) warn("fscanf() oom_score failed: %s\n", strerror(errno)); 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; } if (fscanf(f, "%d", &(p.oom_score_adj)) < 1) warn("fscanf() oom_score_adj failed: %s\n", strerror(errno)); fclose(f); // Read VmRSS from /proc/[pid]/statm (in pages) 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; } if (fscanf(f, "%*u %lu", &(p.VmRSSkiB)) < 1) { warn("fscanf() vm_rss failed: %s\n", strerror(errno)); } // Value is in pages. Convert to kiB. p.VmRSSkiB = p.VmRSSkiB * page_size / 1024; fclose(f); return p; } /* Print a status line like * mem avail: 5259 MiB (67 %), swap free: 0 MiB (0 %)" * as an informational message to stdout (default), or * as a warning to stderr. */ void print_mem_stats(bool urgent, const meminfo_t m) { int (*out_func)(const char* fmt, ...) = &printf; if (urgent) { out_func = &warn; } out_func("mem avail: %5d of %5d MiB (%2d %%), swap free: %4d of %4d MiB (%2d %%)\n", m.MemAvailableMiB, m.MemTotalMiB, m.MemAvailablePercent, m.SwapFreeMiB, m.SwapTotalMiB, m.SwapFreePercent); } earlyoom-1.3/meminfo.h000066400000000000000000000014171347253127100150170ustar00rootroot00000000000000/* SPDX-License-Identifier: MIT */ #ifndef MEMINFO_H #define MEMINFO_H #include typedef struct { // Values from /proc/meminfo, in KiB or converted to MiB. long MemTotalKiB; int MemTotalMiB; int MemAvailableMiB; // -1 means no data available int SwapTotalMiB; long SwapTotalKiB; int SwapFreeMiB; // Calculated percentages int MemAvailablePercent; // percent of total memory that is available int SwapFreePercent; // percent of total swap that is free } meminfo_t; struct procinfo { int oom_score; int oom_score_adj; unsigned long VmRSSkiB; int exited; }; meminfo_t parse_meminfo(); bool is_alive(int pid); struct procinfo get_process_stats(int pid); void print_mem_stats(bool urgent, const meminfo_t m); #endif earlyoom-1.3/msg.c000066400000000000000000000103521347253127100141440ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #include // need isdigit() #include #include #include #include // need strlen() #include #include "msg.h" // Print message, prefixed with "fatal: ", to stderr and exit with "code". // Example: fatal(6, "could not compile regexp '%s'\n", regex_str); void fatal(int code, char* fmt, ...) { char* red = ""; char* reset = ""; if (isatty(fileno(stderr))) { red = "\033[31m"; reset = "\033[0m"; } char fmt2[100]; snprintf(fmt2, sizeof(fmt2), "%sfatal: %s%s", red, fmt, reset); va_list args; va_start(args, fmt); // yes fmt, NOT fmt2! vfprintf(stderr, fmt2, args); va_end(args); exit(code); } // Print a yellow warning message to stderr. No "warning" prefix is added. int warn(const char* fmt, ...) { int ret = 0; char* yellow = ""; char* reset = ""; if (isatty(fileno(stderr))) { yellow = "\033[33m"; reset = "\033[0m"; } char fmt2[100]; snprintf(fmt2, sizeof(fmt2), "%s%s%s", yellow, fmt, reset); va_list args; va_start(args, fmt); // yes fmt, NOT fmt2! ret = vfprintf(stderr, fmt2, args); va_end(args); return ret; } // Parse the "123[,456]" tuple in optarg. term_kill_tuple_t parse_term_kill_tuple(char* optarg, long upper_limit) { term_kill_tuple_t tuple = { 0 }; int n = 0; // Arbitrary limit of 100 bytes to prevent snprintf truncation if (strlen(optarg) > 100) { snprintf(tuple.err, sizeof(tuple.err), "argument too long (%d bytes)\n", (int)strlen(optarg)); return tuple; } for (size_t i = 0; i < strlen(optarg); i++) { if (isdigit(optarg[i])) { continue; } if (optarg[i] == ',') { n++; if (n == 1) { continue; } snprintf(tuple.err, sizeof(tuple.err), "found multiple ','\n"); return tuple; } snprintf(tuple.err, sizeof(tuple.err), "found non-digit '%c'\n", optarg[i]); return tuple; } n = sscanf(optarg, "%ld,%ld", &tuple.term, &tuple.kill); if (n == 0) { snprintf(tuple.err, sizeof(tuple.err), "could not parse '%s'\n", optarg); return tuple; } // User passed only the SIGTERM value: the SIGKILL value is calculated as // SIGTERM/2. if (n == 1) { tuple.kill = tuple.term / 2; } // Would setting SIGTERM below SIGKILL ever make sense? if (tuple.term < tuple.kill) { warn("warning: SIGTERM value %ld is below SIGKILL value %ld, setting SIGTERM = SIGKILL = %ld\n", tuple.term, tuple.kill, tuple.kill); tuple.term = tuple.kill; } // Sanity checks if (tuple.term < 0) { snprintf(tuple.err, sizeof(tuple.err), "negative SIGTERM value in '%s'\n", optarg); return tuple; } if (tuple.term > upper_limit) { snprintf(tuple.err, sizeof(tuple.err), "SIGTERM value %ld exceeds limit %ld\n", tuple.term, upper_limit); return tuple; } if (tuple.kill < 0) { snprintf(tuple.err, sizeof(tuple.err), "negative SIGKILL value in '%s'\n", optarg); return tuple; } if (tuple.kill == 0 && tuple.term == 0) { snprintf(tuple.err, sizeof(tuple.err), "both SIGTERM and SIGKILL values are zero\n"); return tuple; } return tuple; } // Credit to https://gist.github.com/w-vi/67fe49106c62421992a2 // Only works for string of length 3 and up. This is good enough // for our use case, which is fixing the 16-byte value we get // from /proc/[pid]/comm. // // Tested in unit_test.go: Test_fix_truncated_utf8() void fix_truncated_utf8(char* str) { int len = strlen(str); if (len < 3) { return; } // We only need to look at the last three bytes char* b = str + len - 3; // Last byte is ascii if ((b[2] & 0x80) == 0) { return; } // Last byte is multi-byte sequence start if (b[2] & 0x40) { b[2] = 0; } // Truncated 3-byte sequence else if ((b[1] & 0xe0) == 0xe0) { b[1] = 0; // Truncated 4-byte sequence } else if ((b[0] & 0xf0) == 0xf0) { b[0] = 0; } } earlyoom-1.3/msg.h000066400000000000000000000007011347253127100141460ustar00rootroot00000000000000/* SPDX-License-Identifier: MIT */ #ifndef MSG_H #define MSG_H #include void fatal(int code, char* fmt, ...); int warn(const char* fmt, ...); typedef struct { // If the conversion failed, err contains the error message. char err[255]; // Parsed values. long term; long kill; } term_kill_tuple_t; term_kill_tuple_t parse_term_kill_tuple(char* optarg, long upper_limit); void fix_truncated_utf8(char* str); #endif earlyoom-1.3/out000066400000000000000000000072231347253127100137470ustar00rootroot00000000000000.\" Automatically generated by Pandoc 2.0.6 .\" .TH "earlyoom" "1" "" "" "" .hy .SH NAME .PP earlyoom \- Early OOM Daemon .SH SYNOPSIS .PP \f[B]earlyoom\f[] [\f[B]OPTION\f[]]\&... .SH DESCRIPTION .PP The oom\-killer generally has a bad reputation among Linux users. One may have to sit in front of an unre‐ sponsive 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. .PP \f[B]earlyoom\f[] 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 .SS \-m PERCENT .PP set available memory minimum to PERCENT of total (default 10 %) .SS \-s PERCENT .PP set free swap minimum to PERCENT of total (default 10 %) .SS \-M SIZE .PP set available memory minimum to SIZE KiB .IP .nf \f[C] \ \ \ \-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 \ \ \ \-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\ (adds\ 300\ to\ oom_score) \ \ \ \-\-avoid\ REGEX \ \ \ \ \ \ \ \ \ \ avoid\ killing\ processes\ matching\ REGEX\ (subtracts\ 300\ from\ oom_score) \ \ \ \-h,\ \-\-help \ \ \ \ \ \ \ \ \ \ this\ help\ text \f[] .fi .PP EXIT STATUS 0 Successful program execution. .IP .nf \f[C] \ \ \ 1\ \ \ \ \ \ Usage\ printed\ (using\ \-h). \ \ \ 2\ \ \ \ \ \ Switch\ conflict. \ \ \ 4\ \ \ \ \ \ Could\ not\ cd\ to\ /proc \ \ \ 5\ \ \ \ \ \ Could\ not\ open\ proc \ \ \ 7\ \ \ \ \ \ Could\ not\ open\ /proc/sysrq\-trigger \ \ \ 13\ \ \ \ \ Unknown\ options. \ \ \ 14\ \ \ \ \ Wrong\ parameters\ for\ other\ options. \ \ \ 15\ \ \ \ \ Wrong\ parameters\ for\ memory\ threshold. \ \ \ 16\ \ \ \ \ Wrong\ parameters\ for\ swap\ threshold. \ \ \ 102\ \ \ \ Could\ not\ open\ /proc/meminfo \ \ \ 103\ \ \ \ Could\ not\ read\ /proc/meminfo \ \ \ 104\ \ \ \ Could\ not\ find\ a\ specific\ entry\ in\ /proc/meminfo \ \ \ 105\ \ \ \ Could\ not\ convert\ number\ when\ parse\ the\ contents\ of\ /proc/meminfo \f[] .fi .PP 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. Instead, earlyoom finds out itself by reading through /proc/\f[I]/status (actually /proc/\f[]/statm , which contains the same information but is easier to parse programmatically). .IP .nf \f[C] \ \ \ 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\ actu‐ \ \ \ ally\ kill\ any\ process. \f[] .fi .PP MEMORY USAGE About 2 MiB VmRSS. All memory is locked using mlockall() to make sure earlyoom does not slow down in low memory situations. .PP AUTHOR The author of earlyoom is Jakob Unterwurzacher ⟨jakobunt\@gmail.com⟩. .IP .nf \f[C] \ \ \ This\ manual\ page\ was\ written\ by\ Yangfl\ ⟨mmyangfl\@gmail.com⟩,\ for\ the\ Debian\ project\ (and\ may\ be\ used\ by\ oth‐ \ \ \ ers). \f[] .fi earlyoom-1.3/out.1000066400000000000000000000062431347253127100141070ustar00rootroot00000000000000.\" Automatically generated by Pandoc 2.0.6 .\" .TH "earlyoom" "1" "line3" "" "" .hy .SH NAME .PP earlyoom \- Early OOM Daemon .SH SYNOPSIS .PP \f[B]earlyoom\f[] [\f[B]OPTION\f[]]\&... .SH DESCRIPTION .PP 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 to quickly get back to what one was doing after running out of patience. .PP \f[B]earlyoom\f[] 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 .SS \-m PERCENT .PP set available memory minimum to PERCENT of total (default 10 %) .SS \-s PERCENT .PP set free swap minimum to PERCENT of total (default 10 %) .SS \-M SIZE .PP set available memory minimum to SIZE KiB .SS \-S SIZE .PP set free swap minimum to SIZE KiB .SS \-k .PP use kernel oom killer instead of own user\-space implementation .SS \-i .PP user\-space oom killer should ignore positive oom_score_adj values .SS \-d .PP enable debugging messages .SS \-v .PP print version information and exit .SS \-r INTERVAL .PP memory report interval in seconds (default 1), set to 0 to disable completely .SS \-p .PP set niceness of earlyoom to \-20 and oom_score_adj to \-1000 .SS \[en]prefer REGEX .PP prefer killing processes matching REGEX (adds 300 to oom_score) .SS \[en]avoid REGEX .PP avoid killing processes matching REGEX (subtracts 300 from oom_score) .SS \-h, \[en]help .PP this help text .SH EXIT STATUS .PP 0: Successful program execution. .PP 1: Usage printed (using \-h). .PP 2: Switch conflict. .PP 4: Could not cd to /proc .PP 5: Could not open proc .PP 7: Could not open /proc/sysrq\-trigger .PP 13: Unknown options. .PP 14: Wrong parameters for other options. .PP 15: Wrong parameters for memory threshold. .PP 16: Wrong parameters for swap threshold. .PP 102: Could not open /proc/meminfo .PP 103: Could not read /proc/meminfo .PP 104: Could not find a specific entry in /proc/meminfo .PP 105: Could not convert number when parse the contents of /proc/meminfo .SH Why not trigger the kernel oom killer? .PP Earlyoom does not use \f[C]echo\ f\ >\ /proc/sysrq\-trigger\f[] because the Chrome people made their browser always be the first (innocent!) victim by setting \f[C]oom_score_adj\f[] very high. Instead, earlyoom finds out itself by reading through \f[C]/proc/*/status\f[] (actually \f[C]/proc/*/statm\f[], which contains the same information but is easier to parse programmatically). .PP 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 actu‐ ally kill any process. .SH MEMORY USAGE .PP About 2 MiB VmRSS. All memory is locked using mlockall() to make sure earlyoom does not slow down in low memory situations. .SH AUTHOR .PP The author of earlyoom is Jakob Unterwurzacher ⟨jakobunt\@gmail.com⟩. .PP This manual page was written by Yangfl ⟨mmyangfl\@gmail.com⟩, for the Debian project (and may be used by others). .SH AUTHORS line2. earlyoom-1.3/sanitize.c000066400000000000000000000006461347253127100152110ustar00rootroot00000000000000// SPDX-License-Identifier: MIT /* sanitize replaces everything in string "s" that is not [a-zA-Z0-9] * with an underscore. The resulting string is safe to pass to a shell. */ void sanitize(char* s) { char c; for (int i = 0; s[i] != 0; i++) { c = s[i]; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { continue; } s[i] = '_'; } } earlyoom-1.3/tests/000077500000000000000000000000001347253127100143535ustar00rootroot00000000000000earlyoom-1.3/tests/c_wrappers.go000066400000000000000000000014251347253127100170510ustar00rootroot00000000000000package tests import ( "fmt" ) // #cgo CFLAGS: -std=gnu99 // #include "../globals.c" // #include "../sanitize.c" // #include "../msg.c" // #include "../meminfo.c" import "C" func sanitize(s string) string { cs := C.CString(s) C.sanitize(cs) return C.GoString(cs) } func parse_term_kill_tuple(optarg string, upper_limit int) (error, int, int) { cs := C.CString(optarg) tuple := C.parse_term_kill_tuple(cs, C.long(upper_limit)) errmsg := C.GoString(&(tuple.err[0])) if len(errmsg) > 0 { return fmt.Errorf(errmsg), 0, 0 } return nil, int(tuple.term), int(tuple.kill) } func is_alive(pid int) bool { res := C.is_alive(C.int(pid)) return bool(res) } func fix_truncated_utf8(str string) string { cstr := C.CString(str) C.fix_truncated_utf8(cstr) return C.GoString(cstr) } earlyoom-1.3/tests/cli_test.go000066400000000000000000000136101347253127100165110ustar00rootroot00000000000000package tests import ( "fmt" "io/ioutil" "strconv" "strings" "testing" ) type cliTestCase struct { args []string code int stdoutContains string stdoutEmpty bool stderrContains string stderrEmpty bool } func paseMeminfoLine(l string) int64 { fields := strings.Split(l, " ") asciiVal := fields[len(fields)-2] val, err := strconv.ParseInt(asciiVal, 10, 64) if err != nil { panic(err) } return val } func parseMeminfo() (memTotal int64, swapTotal int64) { /* /proc/meminfo looks like this: MemTotal: 8024108 kB [...] SwapTotal: 102396 kB [...] */ content, err := ioutil.ReadFile("/proc/meminfo") if err != nil { panic(err) } lines := strings.Split(string(content), "\n") for _, l := range lines { if strings.HasPrefix(l, "MemTotal:") { memTotal = paseMeminfoLine(l) } if strings.HasPrefix(l, "SwapTotal:") { swapTotal = paseMeminfoLine(l) } } return } func TestCli(t *testing.T) { memTotal, swapTotal := parseMeminfo() mem1percent := fmt.Sprintf("%d", memTotal*2/101) // slightly below 2 percent swap2percent := fmt.Sprintf("%d", swapTotal*3/101) // slightly below 3 percent // The periodic memory report looks like this: // mem avail: 4998 MiB (63 %), swap free: 0 MiB (0 %) const memReport = "mem avail: " // earlyoom startup looks like this: // earlyoom v1.1-5-g74a364b-dirty // mem total: 7836 MiB, min: 783 MiB (10 %) // swap total: 0 MiB, min: 0 MiB (10 %) // startupMsg matches the last line of the startup output. const startupMsg = "swap total: " testcases := []cliTestCase{ // Both -h and --help should show the help text {args: []string{"-h"}, code: 0, stderrContains: "this help text", stdoutEmpty: true}, {args: []string{"--help"}, code: 0, stderrContains: "this help text", stdoutEmpty: true}, {args: nil, code: -1, stderrContains: startupMsg, stdoutContains: memReport}, {args: []string{"-p"}, code: -1, stdoutContains: memReport}, {args: []string{"-v"}, code: 0, stderrContains: "earlyoom v", stdoutEmpty: true}, {args: []string{"-d"}, code: -1, stdoutContains: "^ new victim (higher badness)"}, {args: []string{"-m", "1"}, code: -1, stderrContains: " 1 %", stdoutContains: memReport}, {args: []string{"-m", "0"}, code: 15, stderrContains: "fatal", stdoutEmpty: true}, {args: []string{"-m", "-10"}, code: 15, stderrContains: "fatal", stdoutEmpty: true}, // Using "-m 100" makes no sense {args: []string{"-m", "100"}, code: 15, stderrContains: "fatal", stdoutEmpty: true}, {args: []string{"-s", "2"}, code: -1, stderrContains: " 2 %", stdoutContains: memReport}, // Using "-s 100" is a valid way to ignore swap usage {args: []string{"-s", "100"}, code: -1, stderrContains: " 100 %", stdoutContains: memReport}, {args: []string{"-s", "101"}, code: 16, stderrContains: "fatal", stdoutEmpty: true}, {args: []string{"-s", "0"}, code: 16, stderrContains: "fatal", stdoutEmpty: true}, {args: []string{"-s", "-10"}, code: 16, stderrContains: "fatal", stdoutEmpty: true}, {args: []string{"-M", mem1percent}, code: -1, stderrContains: " 1 %", stdoutContains: memReport}, {args: []string{"-M", "9999999999999999"}, code: 15, stderrContains: "fatal", stdoutEmpty: true}, {args: []string{"-r", "0"}, code: -1, stderrContains: startupMsg, stdoutEmpty: true}, {args: []string{"-r", "0.1"}, code: -1, stderrContains: startupMsg, stdoutContains: memReport}, // Test --avoid and --prefer {args: []string{"--avoid", "MyProcess1"}, code: -1, stderrContains: "Avoiding to kill", stdoutContains: memReport}, {args: []string{"--prefer", "MyProcess2"}, code: -1, stderrContains: "Prefering to kill", stdoutContains: memReport}, // Extra arguments should error out {args: []string{"xyz"}, code: 13, stderrContains: "extra argument not understood", stdoutEmpty: true}, {args: []string{"-i", "1"}, code: 13, stderrContains: "extra argument not understood", stdoutEmpty: true}, // Tuples {args: []string{"-m", "2,1"}, code: -1, stderrContains: "sending SIGTERM when mem <= 2 % and swap <= 10 %", stdoutContains: memReport}, {args: []string{"-m", "1,2"}, code: -1, stdoutContains: memReport}, {args: []string{"-m", "1,-1"}, code: 15, stderrContains: "fatal", stdoutEmpty: true}, {args: []string{"-m", "1000,-1000"}, code: 15, stderrContains: "fatal", stdoutEmpty: true}, {args: []string{"-s", "2,1"}, code: -1, stderrContains: "sending SIGTERM when mem <= 10 % and swap <= 2 %", stdoutContains: memReport}, {args: []string{"-s", "1,2"}, code: -1, stdoutContains: memReport}, // https://github.com/rfjakob/earlyoom/issues/97 {args: []string{"-m", "5,0"}, code: -1, stdoutContains: memReport}, {args: []string{"-m", "5,9"}, code: -1, stdoutContains: memReport}, } if swapTotal > 0 { // Tests that cannot work when there is no swap enabled tc := []cliTestCase{ cliTestCase{args: []string{"-S", swap2percent}, code: -1, stderrContains: " 2 %", stdoutContains: memReport}, } testcases = append(testcases, tc...) } for i, tc := range testcases { t.Logf("Testcase #%d: earlyoom %s", i, strings.Join(tc.args, " ")) pass := true res := runEarlyoom(t, tc.args...) if res.code != tc.code { t.Errorf("wrong exit code: have=%d want=%d", res.code, tc.code) pass = false } if tc.stdoutEmpty && res.stdout != "" { t.Errorf("stdout should be empty but is not") pass = false } if !strings.Contains(res.stdout, tc.stdoutContains) { t.Errorf("stdout should contain %q, but does not", tc.stdoutContains) pass = false } if tc.stderrEmpty && res.stderr != "" { t.Errorf("stderr should be empty, but is not") pass = false } if !strings.Contains(res.stderr, tc.stderrContains) { t.Errorf("stderr should contain %q, but does not", tc.stderrContains) pass = false } if !pass { const empty = "(empty)" if res.stderr == "" { res.stderr = empty } if res.stdout == "" { res.stdout = empty } t.Logf("stderr:\n%s", res.stderr) t.Logf("stdout:\n%s", res.stdout) } } } earlyoom-1.3/tests/helpers.go000066400000000000000000000022101347253127100163370ustar00rootroot00000000000000package tests import ( "bytes" "log" "os" "os/exec" "syscall" "testing" "time" ) type exitVals struct { code int stdout string stderr string } // runEarlyoom runs earlyoom with a timeout func runEarlyoom(t *testing.T, args ...string) exitVals { var stdoutBuf, stderrBuf bytes.Buffer cmd := exec.Command("../earlyoom", args...) cmd.Stdout = &stdoutBuf cmd.Stderr = &stderrBuf // Start with 100 ms timeout var timer *time.Timer timer = time.AfterFunc(100*time.Millisecond, func() { timer.Stop() cmd.Process.Kill() }) err := cmd.Run() timer.Stop() return exitVals{ code: extractCmdExitCode(err), stdout: string(stdoutBuf.Bytes()), stderr: string(stderrBuf.Bytes()), } } // extractCmdExitCode extracts the exit code from an error value that was // returned from exec / cmd.Run() func extractCmdExitCode(err error) int { if err == nil { return 0 } // OMG this is convoluted if err2, ok := err.(*exec.ExitError); ok { return err2.Sys().(syscall.WaitStatus).ExitStatus() } if err2, ok := err.(*os.PathError); ok { return int(err2.Err.(syscall.Errno)) } log.Panicf("could not decode error %#v", err) return 0 } earlyoom-1.3/tests/membomb/000077500000000000000000000000001347253127100157715ustar00rootroot00000000000000earlyoom-1.3/tests/membomb/.gitignore000066400000000000000000000000111347253127100177510ustar00rootroot00000000000000/membomb earlyoom-1.3/tests/membomb/Makefile000066400000000000000000000002121347253127100174240ustar00rootroot00000000000000CFLAGS += -Wall -Wextra -g -fstack-protector-all -std=gnu99 membomb: $(wildcard *.c *.h) Makefile $(CC) $(CFLAGS) -o $@ $(wildcard *.c) earlyoom-1.3/tests/membomb/membomb.c000066400000000000000000000031301347253127100175500ustar00rootroot00000000000000// SPDX-License-Identifier: MIT /* Use up all memory that we can get, as fast as we can. * As progress information, prints how much memory we already * have. * * This file is part of the earlyoom project: https://github.com/rfjakob/earlyoom */ #include #include #include #include #include #include #define NUM_PAGES 10 void handle_sigterm(int sig) { printf("blocking SIGTERM %d\n", sig); } int main() { long page_size = sysconf(_SC_PAGESIZE); long bs = page_size * NUM_PAGES; long cnt = 0, last_sum = 0; struct timeval tv1; char* p; signal(SIGTERM, handle_sigterm); gettimeofday(&tv1, NULL); while (1) { p = malloc(bs); if (!p) { printf("malloc failed\n"); continue; } for (int i = 0; i < NUM_PAGES; i++) { // Write to each page so the kernel really has to allocate it. p[i * page_size] = 0xab; } cnt++; if (cnt % 1000 == 0) { long sum = bs * cnt / 1024 / 1024; struct timeval tv2; gettimeofday(&tv2, NULL); long delta = tv2.tv_sec - tv1.tv_sec; // Convert to microseconds delta *= 1000000; // Add microsecond delta delta = delta + tv2.tv_usec - tv1.tv_usec; // Micro-MB-per-Microsecond = MB/s long mbps = (sum - last_sum) * 1000000 / delta; printf("%4ld MiB (%4ld MiB/s)\n", sum, mbps); last_sum = sum; gettimeofday(&tv1, NULL); } } } earlyoom-1.3/tests/unit_test.go000066400000000000000000000050451347253127100167240ustar00rootroot00000000000000package tests import ( "os" "testing" "unicode/utf8" ) func TestSanitize(t *testing.T) { type testCase struct { in string out string } tcs := []testCase{ {in: "", out: ""}, {in: "foo", out: "foo"}, {in: "foo bar", out: "foo_bar"}, {in: "foo\\", out: "foo_"}, {in: "foo234", out: "foo234"}, {in: "foo$", out: "foo_"}, {in: "foo\"bar", out: "foo_bar"}, {in: "foo\x00bar", out: "foo"}, {in: "foo!§$%&/()=?`'bar", out: "foo_____________bar"}, } for _, tc := range tcs { out := sanitize(tc.in) if out != tc.out { t.Errorf("wrong result: in=%q want=%q have=%q ", tc.in, tc.out, out) } } } func TestParseTuple(t *testing.T) { tcs := []struct { arg string limit int shouldFail bool term int kill int }{ {arg: "2,1", limit: 100, term: 2, kill: 1}, {arg: "20,10", limit: 100, term: 20, kill: 10}, {arg: "30", limit: 100, term: 30, kill: 15}, {arg: "30", limit: 20, shouldFail: true}, // https://github.com/rfjakob/earlyoom/issues/97 {arg: "22[,20]", limit: 100, shouldFail: true}, {arg: "220[,160]", limit: 300, shouldFail: true}, {arg: "180[,170]", limit: 300, shouldFail: true}, {arg: "5,0", limit: 100, term: 5, kill: 0}, {arg: "5,9", limit: 100, term: 9, kill: 9}, {arg: "0,5", limit: 100, term: 5, kill: 5}, // SIGTERM value is set to zero when it is below SIGKILL {arg: "4,5", limit: 100, term: 5, kill: 5}, {arg: "0", limit: 100, shouldFail: true}, {arg: "0,0", limit: 100, shouldFail: true}, } for _, tc := range tcs { err, term, kill := parse_term_kill_tuple(tc.arg, tc.limit) hasFailed := (err != nil) if tc.shouldFail != hasFailed { t.Errorf("case %v: hasFailed=%v", tc, hasFailed) continue } if term != tc.term { t.Errorf("case %v: term=%d", tc, term) } if kill != tc.kill { t.Errorf("case %v: kill=%d", tc, kill) } } } func TestIsAlive(t *testing.T) { tcs := []struct { pid int res bool }{ {os.Getpid(), true}, {1, true}, {999999, false}, {0, false}, } for _, tc := range tcs { if res := is_alive(tc.pid); res != tc.res { t.Errorf("pid %d: expected %v, got %v", tc.pid, tc.res, res) } } } func Test_fix_truncated_utf8(t *testing.T) { // From https://gist.github.com/w-vi/67fe49106c62421992a2 str := "___😀∮ E⋅da = Q, n → ∞, 𐍈∑ f(i) = ∏ g(i)" // a range loop will split at runes - we *want* broken utf8 so use raw // counter. for i := 3; i < len(str); i++ { truncated := str[:i] fixed := fix_truncated_utf8(truncated) if !utf8.Valid([]byte(fixed)) { t.Errorf("Invalid utf8: %q", fixed) } } }