├── .gitignore ├── tools ├── .gitignore └── mem-eater.c ├── zig ├── .gitignore ├── src │ ├── pressure.zig │ ├── main.zig │ ├── memory.zig │ ├── missing_syscalls.zig │ ├── config.zig │ ├── daemonize.zig │ ├── monitor.zig │ └── process.zig ├── LICENSE ├── build.zig └── README.md ├── rust ├── build.rs ├── cc │ └── helper.c ├── src │ ├── memory │ │ ├── mod.rs │ │ ├── pressure.rs │ │ ├── mem_lock.rs │ │ └── mem_info.rs │ ├── errno.rs │ ├── daemon.rs │ ├── cli.rs │ ├── uname.rs │ ├── error.rs │ ├── main.rs │ ├── linux_version.rs │ ├── utils.rs │ ├── monitor.rs │ ├── kill.rs │ └── process.rs ├── Cargo.toml └── Cargo.lock ├── LICENSE ├── README.md └── .github └── workflows └── build.yml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /tools/.gitignore: -------------------------------------------------------------------------------- 1 | mem-eater 2 | -------------------------------------------------------------------------------- /zig/.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | -------------------------------------------------------------------------------- /rust/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | cc::Build::new().file("cc/helper.c").compile("helper"); 3 | 4 | println!("cargo:rerun-if-changed=cc/helper.c"); 5 | } 6 | -------------------------------------------------------------------------------- /rust/cc/helper.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #ifdef MCL_ONFAULT 4 | const int _MCL_ONFAULT = MCL_ONFAULT; 5 | #else 6 | const int _MCL_ONFAULT = -1; 7 | #endif -------------------------------------------------------------------------------- /rust/src/memory/mod.rs: -------------------------------------------------------------------------------- 1 | mod mem_info; 2 | mod mem_lock; 3 | pub mod pressure; 4 | 5 | pub use mem_info::MemoryInfo; 6 | pub use mem_lock::lock_memory_pages; 7 | -------------------------------------------------------------------------------- /rust/src/errno.rs: -------------------------------------------------------------------------------- 1 | use cfg_if::cfg_if; 2 | use libc::{self, c_int}; 3 | 4 | cfg_if! { 5 | if #[cfg(target_os = "android")] { 6 | unsafe fn _errno() -> *mut c_int { 7 | libc::__errno() 8 | } 9 | } else if #[cfg(target_os = "linux")] { 10 | unsafe fn _errno() -> *mut c_int { 11 | libc::__errno_location() 12 | } 13 | } 14 | } 15 | 16 | #[allow(clippy::unnecessary_cast)] 17 | pub fn errno() -> i32 { 18 | unsafe { (*_errno()) as i32 } 19 | } 20 | -------------------------------------------------------------------------------- /zig/src/pressure.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const os = std.os; 3 | const fmt = std.fmt; 4 | const fs = std.fs; 5 | const mem = std.mem; 6 | const assert = std.debug.assert; 7 | 8 | pub fn pressureSomeAvg10(buffer: []u8) !f32 { 9 | assert(buffer.len >= 128); 10 | 11 | const memory_pressure_file = try fs.cwd().openFile("/proc/pressure/memory", .{}); 12 | defer memory_pressure_file.close(); 13 | 14 | var memory_pressure_reader = memory_pressure_file.reader(); 15 | 16 | // Read "some" 17 | const some = try memory_pressure_reader.readUntilDelimiter(buffer, ' '); 18 | assert(mem.eql(u8, some, "some")); 19 | 20 | // Read "avg10=" (`readUntilDelimiter` will eat the '=') 21 | const avg10 = try memory_pressure_reader.readUntilDelimiter(buffer, '='); 22 | assert(mem.eql(u8, avg10, "avg10")); 23 | 24 | // Next up is the value we want 25 | const avg10_value = try memory_pressure_reader.readUntilDelimiter(buffer, ' '); 26 | std.log.info("avg10: {s}", .{avg10_value}); 27 | 28 | return try fmt.parseFloat(f32, avg10_value); 29 | } 30 | -------------------------------------------------------------------------------- /zig/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Vinícius Miguel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vinícius R. Miguel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bustd" 3 | authors = ["Vinícius R. Miguel "] 4 | version = "0.1.1" 5 | edition = "2018" 6 | readme = "README.md" 7 | repository = "https://github.com/vrmiguel/bustd" 8 | description = "Lightweight process killer daemon for out-of-memory scenarios" 9 | categories = ["command-line-utilities", "memory-management"] 10 | license = "MIT" 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | glob = { version = "0.3.1", optional = true } 15 | libc = "0.2.144" 16 | cfg-if = "1.0.0" 17 | daemonize = "0.5.0" 18 | argh = "0.1.10" 19 | memchr = "2.5.0" 20 | 21 | [build-dependencies] 22 | cc = "1.0.68" 23 | libc = "0.2.97" 24 | 25 | [dev-dependencies] 26 | # Using a somewhat popular crate, `procfs`, to test our own 27 | # implementation of proc-fs reads. 28 | # Probably not the best decision possible but OK for now 29 | procfs = { version = "0.14.2", default-features = false } 30 | 31 | [features] 32 | glob-ignore = ["glob"] 33 | 34 | [profile.release] 35 | lto = true 36 | codegen-units = 1 37 | opt-level = 3 38 | strip = true 39 | -------------------------------------------------------------------------------- /zig/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.build.Builder) void { 4 | // Standard target options allows the person running `zig build` to choose 5 | // what target to build for. Here we do not override the defaults, which 6 | // means any target is allowed, and the default is native. Other options 7 | // for restricting supported target set are available. 8 | const target = b.standardTargetOptions(.{}); 9 | 10 | // Standard release options allow the person running `zig build` to select 11 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. 12 | const mode = b.standardReleaseOptions(); 13 | 14 | const exe = b.addExecutable("buztd", "src/main.zig"); 15 | exe.setTarget(target); 16 | exe.setBuildMode(mode); 17 | exe.linkLibC(); 18 | exe.install(); 19 | 20 | const run_cmd = exe.run(); 21 | run_cmd.step.dependOn(b.getInstallStep()); 22 | if (b.args) |args| { 23 | run_cmd.addArgs(args); 24 | } 25 | 26 | const run_step = b.step("run", "Run the app"); 27 | run_step.dependOn(&run_cmd.step); 28 | 29 | const exe_tests = b.addTest("src/main.zig"); 30 | exe_tests.setTarget(target); 31 | exe_tests.setBuildMode(mode); 32 | 33 | const test_step = b.step("test", "Run unit tests"); 34 | test_step.dependOn(&exe_tests.step); 35 | } 36 | -------------------------------------------------------------------------------- /rust/src/daemon.rs: -------------------------------------------------------------------------------- 1 | use std::fs::OpenOptions; 2 | 3 | use daemonize::Daemonize; 4 | 5 | use crate::{error::Result, utils}; 6 | 7 | pub fn daemonize() -> Result<()> { 8 | let running_as_sudo = utils::running_as_sudo(); 9 | 10 | let username = if running_as_sudo { 11 | "root".into() 12 | } else { 13 | utils::get_username().unwrap_or_else(|| "nobody".into()) 14 | }; 15 | 16 | let open_opts = OpenOptions::new() 17 | .truncate(false) 18 | .create(true) 19 | .write(true) 20 | .to_owned(); 21 | 22 | let (stdout_path, stderr_path, pidfile_path) = if running_as_sudo { 23 | ( 24 | "/var/log/bustd.out", 25 | "/var/log/bustd.err", 26 | "/var/run/bustd.pid", 27 | ) 28 | } else { 29 | ("/tmp/bustd.out", "/tmp/bustd.err", "/tmp/bustd.pid") 30 | }; 31 | 32 | let stdout = open_opts.open(stdout_path)?; 33 | let stderr = open_opts.open(stderr_path)?; 34 | 35 | let daemonize = Daemonize::new() 36 | .user(&*username) 37 | .pid_file(pidfile_path) 38 | .chown_pid_file(false) 39 | .working_directory("/tmp") 40 | .stdout(stdout) 41 | .stderr(stderr); 42 | 43 | daemonize.start()?; 44 | 45 | println!( 46 | "[LOG] User {} has started the daemon successfully.", 47 | username 48 | ); 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /rust/src/cli.rs: -------------------------------------------------------------------------------- 1 | use argh::FromArgs; 2 | 3 | #[derive(FromArgs)] 4 | /// Lightweight process killer daemon for out-of-memory scenarios 5 | pub struct CommandLineArgs { 6 | /// toggles on verbose output 7 | #[argh(switch, short = 'V')] 8 | pub verbose: bool, 9 | 10 | /// when set, the process will not be daemonized 11 | #[argh(switch, short = 'n')] 12 | pub no_daemon: bool, 13 | 14 | /// when set, the victim's entire process group will be killed 15 | #[argh(switch, short = 'g')] 16 | pub kill_pgroup: bool, 17 | 18 | /// sets the PSI value on which, if surpassed, a process will be killed 19 | #[argh(option, short = 'p', long = "psi", default = "25.0")] 20 | pub cutoff_psi: f32, // TODO: responsitivity multiplier? 21 | 22 | #[cfg(feature = "glob-ignore")] 23 | /// all processes whose names match any of the supplied vertical bar-separated glob patterns will never be chosen to be killed 24 | #[argh( 25 | option, 26 | short = 'u', 27 | long = "unkillables", 28 | from_str_fn(parse_unkillables) 29 | )] 30 | pub ignored: Option>, 31 | } 32 | 33 | #[cfg(feature = "glob-ignore")] 34 | fn parse_unkillables(arg: &str) -> Result, String> { 35 | let unkillables: Result, _> = arg.split('|').map(glob::Pattern::new).collect(); 36 | 37 | unkillables.map_err(|err| err.to_string()) 38 | } 39 | -------------------------------------------------------------------------------- /zig/src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | test "imports" { 4 | _ = @import("pressure.zig"); 5 | _ = @import("daemonize.zig"); 6 | _ = @import("process.zig"); 7 | _ = @import("memory.zig"); 8 | _ = @import("monitor.zig"); 9 | _ = @import("config.zig"); 10 | } 11 | 12 | const pressure = @import("pressure.zig"); 13 | const daemon = @import("daemonize.zig"); 14 | const process = @import("process.zig"); 15 | const memory = @import("memory.zig"); 16 | const monitor = @import("monitor.zig"); 17 | const config = @import("config.zig"); 18 | const syscalls = @import("missing_syscalls.zig"); 19 | const MCL = syscalls.MCL; 20 | 21 | pub fn startMonitoring() anyerror!void { 22 | if (config.should_daemonize) { 23 | try daemon.daemonize(); 24 | } 25 | 26 | var buffer: [128]u8 = undefined; 27 | 28 | if (syscalls.mlockall(MCL.CURRENT | MCL.FUTURE | MCL.ONFAULT)) { 29 | std.log.warn("Memory pages locked.", .{}); 30 | } else |err| { 31 | std.log.warn("Failed to lock memory pages: {}. Continuing.", .{err}); 32 | } 33 | 34 | var m = try monitor.Monitor.new(&buffer); 35 | try m.poll(); 36 | } 37 | 38 | pub fn main() anyerror!void { 39 | startMonitoring() catch |err| { 40 | // If config.retry is set, get back up and running 41 | if (config.retry) { 42 | std.log.err("{s}. Continuing.", .{err}); 43 | try main(); 44 | } else { 45 | return err; 46 | } 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /rust/src/memory/pressure.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::Read; 3 | 4 | use crate::error::{Error, Result}; 5 | use crate::utils::str_from_bytes; 6 | 7 | macro_rules! malformed { 8 | () => { 9 | Error::MalformedPressureFile 10 | }; 11 | } 12 | 13 | /// Returns the avg10 value in the `some` row of `/proc/pressure/memory`, which 14 | /// indicates the absolute stall time (in us) in which at least some tasks were stalled. 15 | /// 16 | /// The data we're reading looks like: 17 | /// ```some avg10=0.00 avg60=0.00 avg300=0.00 total=0``` 18 | /// 19 | pub fn pressure_some_avg10(buf: &mut [u8]) -> Result { 20 | let mut file = File::open("/proc/pressure/memory")?; 21 | buf.fill(0); 22 | 23 | // `buf` won't be large enough to fit all of `/proc/pressure/memory` 24 | // but will be large enough to hold at least the first line, which has the data we want 25 | file.read(buf)?; 26 | let contents = str_from_bytes(buf)?; 27 | 28 | let line = contents.lines().next().ok_or(malformed!())?; 29 | let mut words = line.split_ascii_whitespace(); 30 | if let Some(indicator) = words.next() { 31 | // This has to be the case but checking to be sure 32 | if indicator == "some" { 33 | let entry = words.next().ok_or(malformed!())?; 34 | 35 | // The entry is of the form `avg10=0.00` 36 | // We'll break this string in two in order to parse the value on the right-hand side 37 | let equals_pos = entry.find('=').ok_or(malformed!())?; 38 | let avg10 = entry.get(equals_pos + 1..).ok_or(malformed!())?; 39 | let avg10: f32 = avg10.trim().parse()?; 40 | return Ok(avg10); 41 | } 42 | } 43 | 44 | Err(malformed!()) 45 | } 46 | -------------------------------------------------------------------------------- /rust/src/memory/mem_lock.rs: -------------------------------------------------------------------------------- 1 | use libc::{c_int, mlockall}; 2 | use libc::{EAGAIN, EINVAL, ENOMEM, EPERM}; 3 | use libc::{MCL_CURRENT, MCL_FUTURE}; 4 | 5 | use crate::errno::errno; 6 | use crate::error::{Error, Result}; 7 | 8 | extern "C" { 9 | pub static _MCL_ONFAULT: libc::c_int; 10 | } 11 | 12 | pub fn _mlockall_wrapper(flags: c_int) -> Result<()> { 13 | let err = unsafe { mlockall(flags) }; 14 | if err == 0 { 15 | return Ok(()); 16 | } 17 | 18 | // If err != 0, errno was set to describe the error that mlockall had 19 | Err(match errno() { 20 | // Some or all of the memory identified by the operation could not be locked when the call was made. 21 | EAGAIN => Error::CouldNotLockMemory, 22 | // The flags argument is zero, or includes unimplemented flags. 23 | EINVAL => Error::InvalidFlags, 24 | 25 | // Locking all of the pages currently mapped into the address space of the process 26 | // would exceed an implementation-defined limit on the amount of memory 27 | // that the process may lock. 28 | ENOMEM => Error::TooMuchMemoryToLock, 29 | // The calling process does not have appropriate privileges to perform the requested operation 30 | EPERM => Error::NoPermission, 31 | // Should not happen 32 | _ => Error::UnknownMlockall, 33 | }) 34 | } 35 | 36 | pub fn lock_memory_pages() -> Result<()> { 37 | // TODO: check for _MCL_ONFAULT == -1 38 | 39 | #[allow(non_snake_case)] 40 | let MCL_ONFAULT: c_int = unsafe { _MCL_ONFAULT }; 41 | match _mlockall_wrapper(MCL_CURRENT | MCL_FUTURE | MCL_ONFAULT) { 42 | Err(err) => { 43 | eprintln!("First try at mlockall failed: {:?}", err); 44 | } 45 | Ok(_) => return Ok(()), 46 | } 47 | 48 | _mlockall_wrapper(MCL_CURRENT | MCL_FUTURE) 49 | } 50 | -------------------------------------------------------------------------------- /zig/src/memory.zig: -------------------------------------------------------------------------------- 1 | const syscalls = @import("missing_syscalls.zig"); 2 | 3 | pub const MemoryInfo = struct { 4 | const Self = @This(); 5 | 6 | total_ram_mb: u64, 7 | total_swap_mb: u64, 8 | available_ram_mb: u64, 9 | available_swap_mb: u64, 10 | available_ram_percent: u8, 11 | available_swap_percent: u8, 12 | 13 | fn bytes_to_megabytes(bytes: u64, mem_unit: u64) u64 { 14 | const B_TO_MB: u64 = 1000 * 1000; 15 | return bytes / B_TO_MB * mem_unit; 16 | } 17 | 18 | fn ratio(x: u64, y: u64) u8 { 19 | const xf = @intToFloat(f32, x); 20 | const yf = @intToFloat(f32, y); 21 | 22 | const _ratio = (xf / yf) * 100.0; 23 | 24 | return @floatToInt(u8, _ratio); 25 | } 26 | 27 | pub fn new() !Self { 28 | var si: syscalls.SysInfo = undefined; 29 | try syscalls.sysinfo(&si); 30 | 31 | const mem_unit = @intCast(u64, si.mem_unit); 32 | 33 | const available_ram_mb = bytes_to_megabytes(si.freeram, mem_unit); 34 | const total_ram_mb = bytes_to_megabytes(si.totalram, mem_unit); 35 | const total_swap_mb = bytes_to_megabytes(si.totalswap, mem_unit); 36 | const available_swap_mb = bytes_to_megabytes(si.freeswap, mem_unit); 37 | const available_ram_percent = ratio(available_ram_mb, total_ram_mb); 38 | const available_swap_percent = blk: { 39 | if (total_swap_mb != 0) { 40 | break :blk ratio(available_swap_mb, total_swap_mb); 41 | } else { 42 | break :blk 0; 43 | } 44 | }; 45 | 46 | return Self{ 47 | .available_ram_mb = available_ram_mb, 48 | .total_ram_mb = total_ram_mb, 49 | .total_swap_mb = total_swap_mb, 50 | .available_swap_mb = available_swap_mb, 51 | .available_ram_percent = available_ram_percent, 52 | .available_swap_percent = available_swap_percent, 53 | }; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /zig/src/missing_syscalls.zig: -------------------------------------------------------------------------------- 1 | // Until mlockall and sysinfo are added to zig's stdlib 2 | 3 | const std = @import("std"); 4 | const os = std.os; 5 | const l = std.os.linux; 6 | 7 | // Flag magic numbers 8 | pub const MCL = struct { 9 | pub const CURRENT = 1; 10 | pub const FUTURE = 2; 11 | pub const ONFAULT = 4; 12 | }; 13 | 14 | // TODO: test if this works outside of x86_64 15 | /// Contains certain statistics on memory and swap usage, as well as the load average 16 | pub const SysInfo = extern struct { 17 | uptime: c_long, 18 | loads: [3]c_ulong, 19 | totalram: c_ulong, 20 | freeram: c_ulong, 21 | sharedram: c_ulong, 22 | bufferram: c_ulong, 23 | totalswap: c_ulong, 24 | freeswap: c_ulong, 25 | procs: c_ushort, 26 | totalhigh: c_ulong, 27 | freehigh: c_ulong, 28 | mem_unit: c_int, 29 | // pad 30 | _f: [20 - 2 * @sizeOf(c_long) - @sizeOf(c_int)]u8, 31 | }; 32 | 33 | pub const MLockError = error{ CouldNotLock, SystemResources, PermissionDenied } || os.UnexpectedError; 34 | 35 | fn syscall_mlockall(flags: i32) usize { 36 | return l.syscall1(.mlockall, @bitCast(usize, @as(isize, flags))); 37 | } 38 | 39 | pub fn mlockall(flags: i32) MLockError!void { 40 | const rc = l.getErrno(syscall_mlockall(flags)); 41 | switch (rc) { 42 | .SUCCESS => return, 43 | .AGAIN => return error.CouldNotLock, 44 | .PERM => return error.PermissionDenied, 45 | .NOMEM => return error.SystemResources, 46 | .INVAL => unreachable, 47 | else => |err| return os.unexpectedErrno(err), 48 | } 49 | } 50 | 51 | fn syscall_sysinfo(info: *SysInfo) usize { 52 | return l.syscall1(.sysinfo, @ptrToInt(info)); 53 | } 54 | 55 | pub fn sysinfo(info: *SysInfo) os.UnexpectedError!void { 56 | const rc = l.getErrno(syscall_sysinfo(info)); 57 | switch (rc) { 58 | .SUCCESS => return, 59 | .FAULT => unreachable, 60 | else => |err| return os.unexpectedErrno(err), 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /rust/src/uname.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | use std::mem; 3 | 4 | use crate::error::{Error, Result}; 5 | use crate::linux_version::LinuxVersion; 6 | use libc::{uname, utsname}; 7 | 8 | pub struct Uname { 9 | uts_struct: utsname, 10 | } 11 | 12 | impl Uname { 13 | pub fn new() -> Result { 14 | // Safety: libc::utsname is a bunch of char arrays and therefore 15 | // can be safely zeroed. 16 | let mut uts_struct: utsname = unsafe { mem::zeroed() }; 17 | 18 | let ret_val = unsafe { uname(&mut uts_struct) }; 19 | 20 | // uname returns a negative number upon failure 21 | if ret_val < 0 { 22 | return Err(Error::UnameFailed); 23 | } 24 | 25 | Ok(Self { uts_struct }) 26 | } 27 | 28 | pub fn print_info(&self) -> Result<()> { 29 | // Safety: dereference of these raw pointers are safe since we know they're not NULL, since 30 | // the buffers in struct utsname are all correctly allocated in the stack at this moment 31 | let sysname = unsafe { CStr::from_ptr(self.uts_struct.sysname.as_ptr()) }; 32 | let hostname = unsafe { CStr::from_ptr(self.uts_struct.nodename.as_ptr()) }; 33 | let release = unsafe { CStr::from_ptr(self.uts_struct.release.as_ptr()) }; 34 | let arch = unsafe { CStr::from_ptr(self.uts_struct.machine.as_ptr()) }; 35 | 36 | let sysname = sysname.to_str()?; 37 | let hostname = hostname.to_str()?; 38 | let release = release.to_str()?; 39 | let arch = arch.to_str()?; 40 | 41 | println!("OS: {}", sysname); 42 | println!("Hostname: {}", hostname); 43 | println!("Version: {}", release); 44 | println!("Architecture: {}", arch); 45 | 46 | Ok(()) 47 | } 48 | 49 | pub fn parse_version(&self) -> Result { 50 | let release = unsafe { CStr::from_ptr(self.uts_struct.release.as_ptr()) }; 51 | let release = release.to_str()?; 52 | 53 | LinuxVersion::from_str(release).ok_or(Error::InvalidLinuxVersion) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rust/src/error.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::{any::Any, str::Utf8Error}; 4 | 5 | #[derive(Debug)] 6 | pub enum Error { 7 | // Only possible uname error: "buf is invalid" 8 | UnameFailed, 9 | ProcessNotFound(&'static str), 10 | InvalidPidSupplied, 11 | ProcessGroupNotFound, 12 | InvalidSignal, 13 | Io { 14 | reason: String, 15 | }, 16 | Daemonize { 17 | error: daemonize::Error, 18 | }, 19 | #[allow(unused)] 20 | Unicode { 21 | error: Utf8Error, 22 | }, 23 | NoPermission, 24 | 25 | // mlockall-specific errors 26 | CouldNotLockMemory, 27 | TooMuchMemoryToLock, 28 | InvalidFlags, 29 | // Should not happen but better safe than sorry 30 | UnknownMlockall, 31 | UnknownKill, 32 | UnknownGetpguid, 33 | 34 | #[cfg(feature = "glob-ignore")] 35 | GlobPattern { 36 | error: glob::PatternError, 37 | }, 38 | 39 | // Errors that are likely impossible to happen 40 | InvalidLinuxVersion, 41 | MalformedStatm, 42 | MalformedPressureFile, 43 | ParseInt, 44 | ParseFloat, 45 | SysConfFailed, 46 | SysInfoFailed, 47 | } 48 | 49 | pub type Result = std::result::Result; 50 | 51 | impl From for Error { 52 | fn from(err: std::io::Error) -> Self { 53 | Self::Io { 54 | reason: err.to_string(), 55 | } 56 | } 57 | } 58 | 59 | impl From for Error { 60 | fn from(_: std::num::ParseIntError) -> Self { 61 | Self::ParseInt 62 | } 63 | } 64 | 65 | impl From for Error { 66 | fn from(_: std::num::ParseFloatError) -> Self { 67 | Self::ParseFloat 68 | } 69 | } 70 | 71 | impl From for Error { 72 | fn from(error: daemonize::Error) -> Self { 73 | Self::Daemonize { error } 74 | } 75 | } 76 | 77 | impl From for Error { 78 | fn from(error: std::str::Utf8Error) -> Self { 79 | Self::Unicode { error } 80 | } 81 | } 82 | 83 | #[cfg(feature = "glob-ignore")] 84 | impl From for Error { 85 | fn from(error: glob::PatternError) -> Self { 86 | Self::GlobPattern { error } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /zig/src/config.zig: -------------------------------------------------------------------------------- 1 | //! The configuration file for buztd 2 | const std = @import("std"); 3 | 4 | /// Sets whether or not buztd should daemonize 5 | /// itself. Don't use this if running buztd as a systemd 6 | /// service or something of the sort. 7 | pub const should_daemonize: bool = false; 8 | 9 | /// Free RAM percentage figures below this threshold are considered to be near terminal, meaning 10 | /// that buztd will start to check for Pressure Stall Information whenever the 11 | /// free RAM figures go below this. 12 | /// However, this free RAM amount is what the sysinfo syscall gives us, which does not take in consideration 13 | /// reclaimable or cached pages. The true free RAM amount available to the OS is bigger than what it indicates. 14 | pub const free_ram_threshold: u8 = 15; 15 | 16 | /// The Linux kernel presents canonical pressure metrics for memory, found in `/proc/pressure/memory`. 17 | /// Example: 18 | /// some avg10=0.00 avg60=0.00 avg300=0.00 total=11220657 19 | /// full avg10=0.00 avg60=0.00 avg300=0.00 total=10947429 20 | /// These ratios are percentages of recent trends over ten, sixty, and 21 | /// three hundred second windows. The `some` row indicates the percentage of time 22 | // in that given time frame in which _any_ process has stalled due to memory thrashing. 23 | /// 24 | /// This value configured here is the value of `some avg10` in which, if surpassed, some 25 | /// process will be killed. 26 | /// 27 | /// The ideal value for this cutoff varies a lot between systems. 28 | /// Try messing around with `tools/mem-eater.c` to guesstimate a value that works well for you. 29 | pub const cutoff_psi: f32 = 0.05; 30 | 31 | /// Sets processes that buztd must never kill. 32 | /// The values expected here are the `comm` values of the process you don't want to have terminated. 33 | /// A comm-value is the filename of the executable truncated to 16 characters.. 34 | /// 35 | /// Example: 36 | /// pub const unkillables = std.ComptimeStringMap(void, .{ 37 | /// .{ "firefox", void }, 38 | /// .{ "rustc", void }, 39 | /// .{ "electron", void }, 40 | /// }); 41 | pub const unkillables = std.ComptimeStringMap(void, .{ 42 | // Ideally, don't kill the oomkiller 43 | .{ "buztd", void }, 44 | }); 45 | 46 | /// If any error occurs, restarts the monitoring instead of exiting with an unsuccesful status code 47 | pub const retry: bool = true; 48 | -------------------------------------------------------------------------------- /rust/src/main.rs: -------------------------------------------------------------------------------- 1 | // use uname::Uname; 2 | 3 | use std::ops::Not; 4 | 5 | use linux_version::LinuxVersion; 6 | use uname::Uname; 7 | 8 | use crate::{error::Error, memory::lock_memory_pages, monitor::Monitor}; 9 | 10 | mod cli; 11 | mod daemon; 12 | mod errno; 13 | mod error; 14 | mod kill; 15 | mod linux_version; 16 | mod memory; 17 | mod monitor; 18 | mod process; 19 | mod uname; 20 | mod utils; 21 | 22 | /// The first Linux version in which PSI information became available 23 | const LINUX_4_20: LinuxVersion = LinuxVersion { 24 | major: 4, 25 | minor: 20, 26 | }; 27 | 28 | fn main() -> error::Result<()> { 29 | let args: cli::CommandLineArgs = argh::from_env(); 30 | let should_daemonize = args.no_daemon.not(); 31 | 32 | // Show uname info and return the Linux version running 33 | { 34 | let ensure_msg = "Ensure you're running at least Linux 4.20"; 35 | let uname = Uname::new()?; 36 | uname.print_info()?; 37 | 38 | match uname.parse_version() { 39 | Ok(version) => { 40 | if version < LINUX_4_20 { 41 | eprintln!( 42 | "{version} does not meet minimum requirements for bustd!\n{ensure_msg}" 43 | ); 44 | return Err(Error::InvalidLinuxVersion); 45 | } 46 | } 47 | Err(_) => { 48 | eprintln!("Failed to parse Linux version!\n{ensure_msg}"); 49 | } 50 | } 51 | 52 | if let Ok(version) = uname.parse_version() { 53 | if version < LINUX_4_20 { 54 | eprintln!("{version} does not meet minimum requirements for bustd!\n{ensure_msg}"); 55 | return Err(Error::InvalidLinuxVersion); 56 | } 57 | } else { 58 | eprintln!("Failed to parse Linux version!\n{ensure_msg}"); 59 | } 60 | }; 61 | 62 | // In order to correctly use `mlockall`, we'll try our best to avoid heap allocations and 63 | // reuse these buffers right here, even though it makes the code less readable. 64 | // Buffer specific to process creation 65 | let proc_buf = [0_u8; 50]; 66 | 67 | // Buffer for anything else 68 | let buf = [0_u8; 100]; 69 | 70 | if should_daemonize { 71 | // Daemonize current process 72 | println!("\nStarting daemonization process!"); 73 | daemon::daemonize()?; 74 | } 75 | 76 | // Attempt to lock the memory pages mapped to the daemon 77 | // in order to avoid being sent to swap when the system 78 | // memory is stressed 79 | if let Err(err) = lock_memory_pages() { 80 | eprintln!("Failed to lock memory pages: {:?}. Continuing anyway.", err); 81 | } else { 82 | // Save this on both bustd.out and bustd.err 83 | println!("Memory pages locked!"); 84 | eprintln!("Memory pages locked!"); 85 | } 86 | 87 | Monitor::new(proc_buf, buf, args)?.poll() 88 | } 89 | -------------------------------------------------------------------------------- /rust/src/memory/mem_info.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, mem}; 2 | 3 | use libc::sysinfo; 4 | 5 | use crate::{ 6 | error::{Error, Result}, 7 | utils::bytes_to_megabytes, 8 | }; 9 | 10 | #[derive(Debug, Default)] 11 | pub struct MemoryInfo { 12 | pub total_ram_mb: u64, 13 | pub total_swap_mb: u64, 14 | pub available_ram_mb: u64, 15 | pub available_swap_mb: u64, 16 | pub available_ram_percent: u8, 17 | pub available_swap_percent: u8, 18 | } 19 | 20 | /// Simple wrapper over libc's sysinfo 21 | fn sys_info() -> Result { 22 | // Safety: the all-zero byte pattern is a valid sysinfo struct 23 | let mut sys_info: sysinfo = unsafe { mem::zeroed() }; 24 | 25 | // Safety: sysinfo() is safe and must not fail when passed a valid reference 26 | let ret_val = unsafe { libc::sysinfo(&mut sys_info) }; 27 | 28 | if ret_val != 0 { 29 | // The only error that sysinfo() can have happens when 30 | // it is supplied an invalid struct sysinfo pointer 31 | // 32 | // This error should really not happen during this function 33 | return Err(Error::SysInfoFailed); 34 | } 35 | 36 | Ok(sys_info) 37 | } 38 | 39 | impl MemoryInfo { 40 | pub fn new() -> Result { 41 | let sysinfo { 42 | mem_unit, 43 | freeram, 44 | totalram, 45 | totalswap, 46 | freeswap, 47 | .. 48 | } = sys_info()?; 49 | 50 | let ratio = |x, y| ((x as f32 / y as f32) * 100.0) as u8; 51 | 52 | let available_ram_mb = bytes_to_megabytes(freeram, mem_unit); 53 | let total_ram_mb = bytes_to_megabytes(totalram, mem_unit); 54 | let total_swap_mb = bytes_to_megabytes(totalswap, mem_unit); 55 | let available_swap_mb = bytes_to_megabytes(freeswap, mem_unit); 56 | 57 | let available_ram_percent = ratio(available_ram_mb, total_ram_mb); 58 | let available_swap_percent = if total_swap_mb != 0 { 59 | ratio(available_swap_mb, total_swap_mb) 60 | } else { 61 | 0 62 | }; 63 | 64 | Ok(MemoryInfo { 65 | total_ram_mb, 66 | available_ram_mb, 67 | total_swap_mb, 68 | available_swap_mb, 69 | available_ram_percent, 70 | available_swap_percent, 71 | }) 72 | } 73 | } 74 | 75 | impl fmt::Display for MemoryInfo { 76 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 77 | writeln!(f, "Total RAM: {} MB", self.total_ram_mb)?; 78 | writeln!( 79 | f, 80 | "Available RAM: {} MB ({}%)", 81 | self.available_ram_mb, self.available_ram_percent 82 | )?; 83 | writeln!(f, "Total swap: {} MB", self.total_swap_mb)?; 84 | writeln!( 85 | f, 86 | "Available swap: {} MB ({} %)", 87 | self.available_swap_mb, self.available_swap_percent 88 | ) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /rust/src/linux_version.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::Ordering, fmt::Display}; 2 | 3 | #[derive(Debug, PartialEq, Eq)] 4 | pub struct LinuxVersion { 5 | pub major: u8, 6 | pub minor: u8, 7 | } 8 | 9 | impl LinuxVersion { 10 | /// Given a release string (e.g. as given by `uname -r`), attempt 11 | /// to extract the major and minor values of the Linux version 12 | pub fn from_str(release: &str) -> Option { 13 | // The position of the first dot in the 'release' string 14 | let dot_idx = release.find('.')?; 15 | 16 | let (major, minor): (&str, &str) = release.split_at(dot_idx); 17 | 18 | let major: u8 = major.parse().ok()?; 19 | 20 | // Eat the leading dot in front of minor 21 | let minor = &minor[1..]; 22 | let dot_idx = minor.find('.')?; 23 | 24 | let minor: u8 = minor[0..dot_idx].parse().ok()?; 25 | 26 | Some(Self { major, minor }) 27 | } 28 | } 29 | 30 | impl Display for LinuxVersion { 31 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 | let (major, minor) = (self.major, self.minor); 33 | write!(f, "Linux {major}.{minor}") 34 | } 35 | } 36 | 37 | impl PartialOrd for LinuxVersion { 38 | fn partial_cmp(&self, other: &Self) -> Option { 39 | match self.major.partial_cmp(&other.major) { 40 | Some(Ordering::Equal) => {} 41 | ord => return ord, 42 | } 43 | 44 | self.minor.partial_cmp(&other.minor) 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use crate::linux_version::LinuxVersion; 51 | 52 | #[test] 53 | fn should_be_able_to_parse_linux_versions() { 54 | assert_eq!( 55 | LinuxVersion::from_str("5.16.18-1-MANJARO").unwrap(), 56 | LinuxVersion { 57 | major: 5, 58 | minor: 16 59 | } 60 | ); 61 | 62 | assert_eq!( 63 | LinuxVersion::from_str("3.8.3-Fedora").unwrap(), 64 | LinuxVersion { major: 3, minor: 8 } 65 | ); 66 | } 67 | 68 | #[test] 69 | fn should_be_able_to_compare_linux_versions() { 70 | assert!(LinuxVersion::from_str("3.8.3") >= LinuxVersion::from_str("3.6.9")); 71 | 72 | // We do not require PATCH accuracy 73 | assert!( 74 | LinuxVersion::from_str("3.8.3").unwrap() == LinuxVersion::from_str("3.8.9").unwrap() 75 | ); 76 | 77 | assert!( 78 | LinuxVersion::from_str("5.8.3").unwrap() > LinuxVersion::from_str("3.13.9").unwrap() 79 | ); 80 | assert!( 81 | LinuxVersion::from_str("5.8.3").unwrap() < LinuxVersion::from_str("5.13.9").unwrap() 82 | ); 83 | assert!( 84 | LinuxVersion::from_str("5.8.3").unwrap() > LinuxVersion::from_str("4.20.0").unwrap() 85 | ); 86 | assert!( 87 | LinuxVersion::from_str("4.21.0").unwrap() > LinuxVersion::from_str("4.20.0").unwrap() 88 | ); 89 | assert!( 90 | LinuxVersion::from_str("4.15.0").unwrap() < LinuxVersion::from_str("4.20.0").unwrap() 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /zig/src/daemonize.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const os = std.os; 3 | 4 | const unistd = @cImport({ 5 | @cInclude("unistd.h"); 6 | }); 7 | 8 | const signal = @cImport({ 9 | @cInclude("signal.h"); 10 | }); 11 | 12 | const stat = @cImport({ 13 | @cInclude("sys/stat.h"); 14 | }); 15 | 16 | /// Any error that might come up during process daemonization 17 | const DaemonizeError = error{FailedToSetSessionId} || os.ForkError; 18 | 19 | const SignalHandler = struct { 20 | fn ignore(sig: i32, info: *const os.siginfo_t, ctx_ptr: ?*const anyopaque) callconv(.C) void { 21 | // Ignore the signal received 22 | _ = sig; 23 | _ = ctx_ptr; 24 | _ = info; 25 | _ = ctx_ptr; 26 | } 27 | }; 28 | 29 | /// Forks the current process and makes 30 | /// the parent process quit 31 | fn fork_and_keep_child() os.ForkError!void { 32 | const is_parent_proc = (try os.fork()) != 0; 33 | // Exit off of the parent process 34 | if (is_parent_proc) { 35 | os.exit(0); 36 | } 37 | } 38 | 39 | // TODO: 40 | // * Add logging 41 | // * Chdir 42 | /// Daemonizes the calling process 43 | pub fn daemonize() DaemonizeError!void { 44 | try fork_and_keep_child(); 45 | 46 | if (unistd.setsid() < 0) { 47 | return error.FailedToSetSessionId; 48 | } 49 | 50 | // Setup signal handling 51 | var act = os.Sigaction{ 52 | .handler = .{ .sigaction = SignalHandler.ignore }, 53 | .mask = os.empty_sigset, 54 | .flags = (os.SA.SIGINFO | os.SA.RESTART | os.SA.RESETHAND), 55 | }; 56 | os.sigaction(signal.SIGCHLD, &act, null); 57 | os.sigaction(signal.SIGHUP, &act, null); 58 | 59 | // Fork yet again and keep only the child process 60 | try fork_and_keep_child(); 61 | 62 | // Set new file permissions 63 | _ = stat.umask(0); 64 | 65 | var fd: u8 = 0; 66 | // The maximum number of files a process can have open 67 | // at any time 68 | const max_files_opened = unistd.sysconf(unistd._SC_OPEN_MAX); 69 | while (fd < max_files_opened) : (fd += 1) { 70 | _ = unistd.close(fd); 71 | } 72 | } 73 | 74 | test "fork_and_keep_child works" { 75 | const getpid = os.linux.getpid; 76 | const expect = std.testing.expect; 77 | const linux = std.os.linux; 78 | const fmt = std.fmt; 79 | 80 | const first_pid = getpid(); 81 | try fork_and_keep_child(); 82 | 83 | const new_pid = getpid(); 84 | // We should now be running on a new process 85 | try expect(first_pid != new_pid); 86 | 87 | var stat_buf: linux.Stat = undefined; 88 | var buf = [_:0]u8{0} ** 128; 89 | 90 | // Current process is alive (obviously) 91 | _ = try fmt.bufPrint(&buf, "/proc/{}/stat", .{new_pid}); 92 | 93 | try expect(linux.stat(&buf, &stat_buf) == 0); 94 | 95 | // Old process should now be dead 96 | _ = try fmt.bufPrint(&buf, "/proc/{}/stat", .{first_pid}); 97 | 98 | // Give the OS some time to reap the old process 99 | std.time.sleep(250_000); 100 | 101 | try expect( 102 | // Stat should now fail 103 | linux.stat(&buf, &stat_buf) != 0); 104 | } 105 | -------------------------------------------------------------------------------- /tools/mem-eater.c: -------------------------------------------------------------------------------- 1 | // `bustd`'s memory eater 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | struct free_mem_s { 11 | unsigned available_mem_mib; 12 | unsigned available_swap_mib; 13 | }; 14 | 15 | typedef struct free_mem_s free_mem_t; 16 | 17 | void display(free_mem_t * mem, float psi) { 18 | printf("\rFree RAM: %d MiB. Free swap: %d MiB. PSI: %0.2f", mem->available_mem_mib, mem->available_swap_mib, psi); 19 | fflush(stdout); 20 | } 21 | 22 | float memory_pressure_some_avg_10(void) { 23 | FILE * memory_pressure = fopen("/proc/pressure/memory", "r"); 24 | if(!memory_pressure) { 25 | perror("/proc/pressure/memory. Exiting.\n"); 26 | fclose(memory_pressure); 27 | _exit(1); 28 | } 29 | 30 | float psi; 31 | 32 | if (EOF == fscanf(memory_pressure, "some avg10=%f", &psi)) { 33 | perror("Failed to read memory pressure values. Exiting.\n"); 34 | fclose(memory_pressure); 35 | _exit(1); 36 | } 37 | 38 | fclose(memory_pressure); 39 | return psi; 40 | } 41 | 42 | free_mem_t poll_free_mem(void) { 43 | FILE * meminfo = fopen("/proc/meminfo", "r"); 44 | if(!meminfo) { 45 | fprintf(stderr, "/proc/meminfo not found. Exiting.\n"); 46 | fclose(meminfo); 47 | _exit(1); 48 | } 49 | 50 | char line[256]; 51 | bool avail_mem_read = false; 52 | bool avail_swap_read = false; 53 | free_mem_t free_mem; 54 | 55 | while((!avail_mem_read || !avail_swap_read) && fgets(line, sizeof(line), meminfo)) 56 | { 57 | int val; 58 | if(sscanf(line, "MemAvailable: %d kB", &val) == 1) 59 | { 60 | avail_mem_read = true; 61 | free_mem.available_mem_mib = (unsigned) val / 1024; 62 | } 63 | 64 | if(sscanf(line, "SwapFree: %d kB", &val) == 1) 65 | { 66 | avail_swap_read = true; 67 | free_mem.available_swap_mib = (unsigned) val / 1024; 68 | } 69 | } 70 | 71 | for (int i = 0; i < 100; i++) { 72 | putchar(' '); 73 | } 74 | 75 | if (!avail_swap_read || !avail_mem_read) { 76 | fprintf(stderr, "failed to read available system memory or swap amounts. Exiting.\n"); 77 | fclose(meminfo); 78 | _exit(1); 79 | } 80 | 81 | fclose(meminfo); 82 | return free_mem; 83 | } 84 | 85 | int main(void) { 86 | time_t start, now; 87 | float time_left = 4.0; 88 | 89 | time(&start); 90 | 91 | while(time_left > 0.0) { 92 | time(&now); 93 | time_left = 4.0 - difftime(now, start); 94 | 95 | printf("\rmem-eater will start consuming system memory in: %.2f secs. Press Ctrl+C if you don't want that to happen.", time_left); 96 | fflush(stdout); 97 | usleep(20); 98 | } 99 | 100 | while(1) 101 | { 102 | free_mem_t free_mem = poll_free_mem(); 103 | float psi = memory_pressure_some_avg_10(); 104 | display(&free_mem, psi); 105 | void *m = malloc(1024*1024); 106 | memset(m,0,1024*1024); 107 | } 108 | 109 | 110 | return 0; 111 | } 112 | -------------------------------------------------------------------------------- /rust/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::fs::File; 3 | use std::os::unix::prelude::OsStrExt; 4 | use std::path::Path; 5 | use std::{ffi::CStr, mem, ptr, str}; 6 | 7 | use libc::_SC_PAGESIZE; 8 | use libc::{getpgid, sysconf, EINVAL, EPERM, ESRCH}; 9 | use libc::{getpwuid_r, passwd}; 10 | use memchr::memchr; 11 | 12 | use crate::errno::errno; 13 | use crate::error::{Error, Result}; 14 | 15 | /// Gets the effective user ID of the calling process 16 | fn effective_user_id() -> u32 { 17 | // Safety: the POSIX Programmer's Manual states that 18 | // geteuid will always be successful. 19 | unsafe { libc::geteuid() } 20 | } 21 | 22 | /// Gets the process group of the process 23 | /// with the given PID. 24 | pub fn get_process_group(pid: i32) -> Result { 25 | let pgid = unsafe { getpgid(pid) }; 26 | if pgid == -1 { 27 | return Err(match errno() { 28 | EPERM => Error::NoPermission, 29 | ESRCH => Error::ProcessGroupNotFound, 30 | EINVAL => Error::InvalidPidSupplied, 31 | _ => Error::UnknownGetpguid, 32 | }); 33 | } 34 | 35 | Ok(pgid) 36 | } 37 | 38 | /// Checks if the program is running with sudo permissions. 39 | pub fn running_as_sudo() -> bool { 40 | effective_user_id() == 0 41 | } 42 | 43 | /// Get the size of the system's memory page in bytes. 44 | pub fn page_size() -> Result { 45 | // _SC_PAGESIZE is defined in POSIX.1 46 | // Safety: no memory unsafety can arise from `sysconf` 47 | let page_size = unsafe { sysconf(_SC_PAGESIZE) }; 48 | if page_size == -1 { 49 | return Err(Error::SysConfFailed); 50 | } 51 | 52 | #[allow(clippy::useless_conversion)] 53 | // The type of page_size differs between architectures 54 | // so we use .into() to convert to i64 if necessary 55 | Ok(page_size.into()) 56 | } 57 | 58 | /// Attempt to get the user's username from the system's password bank 59 | pub fn get_username() -> Option { 60 | let mut buf = [0; 2048]; 61 | let mut result = ptr::null_mut(); 62 | let mut passwd: passwd = unsafe { mem::zeroed() }; 63 | 64 | let uid = effective_user_id(); 65 | 66 | let getpwuid_r_code = 67 | unsafe { getpwuid_r(uid, &mut passwd, buf.as_mut_ptr(), buf.len(), &mut result) }; 68 | 69 | if getpwuid_r_code == 0 && !result.is_null() { 70 | // If getpwuid_r succeeded, let's get the username from it 71 | let username = unsafe { CStr::from_ptr(passwd.pw_name) }; 72 | let username = String::from_utf8_lossy(username.to_bytes()); 73 | 74 | return Some(username.into()); 75 | } 76 | 77 | None 78 | } 79 | 80 | fn bytes_until_first_nil(buf: &[u8]) -> &[u8] { 81 | let first_nul_idx = memchr(0, buf).unwrap_or(buf.len()); 82 | 83 | &buf[0..first_nul_idx] 84 | } 85 | 86 | /// Construct a string slice ranging from the first position to the position of the first nul byte 87 | pub fn str_from_bytes(buf: &[u8]) -> Result<&str> { 88 | let bytes = bytes_until_first_nil(buf); 89 | 90 | Ok(str::from_utf8(bytes)?) 91 | } 92 | 93 | fn path_from_bytes(buf: &[u8]) -> &Path { 94 | let bytes = bytes_until_first_nil(buf); 95 | 96 | Path::new(OsStr::from_bytes(bytes)) 97 | } 98 | 99 | /// Given a slice of bytes, try to interpret it as a file path and open the corresponding file. 100 | pub fn file_from_buffer(buf: &[u8]) -> Result { 101 | let path = path_from_bytes(buf); 102 | let file = File::open(path)?; 103 | Ok(file) 104 | } 105 | 106 | pub fn bytes_to_megabytes(bytes: impl Into, mem_unit: impl Into) -> u64 { 107 | const B_TO_MB: u64 = 1000 * 1000; 108 | bytes.into() / B_TO_MB * mem_unit.into() 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use super::str_from_bytes; 114 | 115 | #[test] 116 | fn should_construct_string_slice_from_bytes() { 117 | assert_eq!(str_from_bytes(b"ABC\0").unwrap(), "ABC"); 118 | assert_eq!(str_from_bytes(b"ABC\0abc").unwrap(), "ABC"); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `bustd`: Available memory or bust! 2 | 3 | `bustd` is a lightweight process killer daemon for out-of-memory scenarios for Linux! 4 | 5 | ## Features 6 | 7 | ### Small memory usage! 8 | 9 | `bustd` seems to use less memory than some other lean daemons such as `earlyoom`: 10 | 11 | ```console 12 | $ ps -F -C bustd 13 | UID PID PPID C SZ RSS PSR STIME TTY TIME CMD 14 | vrmiguel 353609 187407 5 151 8 2 01:20 pts/2 00:00:00 target/x86_64-unknown-linux-musl/release/bustd -V -n 15 | 16 | $ ps -F -C earlyoom 17 | UID PID PPID C SZ RSS PSR STIME TTY TIME CMD 18 | vrmiguel 350497 9498 0 597 688 6 01:12 pts/1 00:00:00 ./earlyoom/ 19 | ``` 20 | 21 | ¹: RSS stands for resident set size and represents the portion of RAM occupied by a process. 22 | 23 | ²: Compared when bustd was in [this commit](https://github.com/vrmiguel/bustd/commit/61beb097b3631afb231a76bb9187b802c9818793) and earlyoom in [this one](https://github.com/rfjakob/earlyoom/commit/509df072be79b3be2a1de6581499e360ab0180be). 24 | `bustd` compiled with musl libc and earlyoom with glibc through GCC 11.1. Different configurations would likely change these figures. 25 | 26 | 27 | ### Small CPU usage 28 | 29 | Much like `earlyoom` and `nohang`, `bustd` uses adaptive sleep times during its memory polling. Unlike these two, however, `bustd` does not read from `/proc/meminfo`, instead opting for the `sysinfo` syscall. 30 | 31 | This approach has its up- and downsides. The amount of free RAM that `sysinfo` reads does not account for cached memory, while `MemAvailable` in `/proc/meminfo` does. 32 | 33 | The `sysinfo` syscall is one order of magnitude faster, at least according to [this kernel patch](https://sourceware.org/legacy-ml/libc-alpha/2015-08/msg00512.html) (granted, from 2015). 34 | 35 | As `bustd` can't solely rely on the free RAM readings of `sysinfo`, we check for memory stress through [Pressure Stall Information](https://www.kernel.org/doc/html/v5.8/accounting/psi.html). 36 | 37 | ### `bustd` will try to lock all pages mapped into its address space 38 | 39 | Much like `earlyoom`, `bustd` uses [`mlockall`](https://www.ibm.com/docs/en/aix/7.2?topic=m-mlockall-munlockall-subroutine) to avoid being sent to swap, which allows the daemon to remain responsive even when the system memory is under heavy load and susceptible to [thrashing](https://en.wikipedia.org/wiki/Thrashing_(computer_science)). 40 | 41 | ### Checks for Pressure Stall Information 42 | 43 | The Linux kernel, since version 4.20 (and built with `CONFIG_PSI=y`), presents canonical new pressure metrics for memory, CPU, and IO. 44 | In the words of [Facebook Incubator](https://facebookmicrosites.github.io/psi/docs/overview): 45 | 46 | ``` 47 | PSI stats are like barometers that provide fair warning of impending resource 48 | shortages, enabling you to take more proactive, granular, and nuanced steps 49 | when resources start becoming scarce. 50 | ``` 51 | 52 | More specifically, `bustd` checks for how long, in microseconds, processes have stalled in the last 10 seconds. By default, `bustd` will kill a process when processes have stalled for 25 microseconds in the last ten seconds. 53 | 54 | ## Packaging 55 | 56 | ### Arch Linux 57 | 58 | Available on the Arch User Repository 59 | 60 | ### Gentoo 61 | 62 | Available on the [GURU project](https://gitweb.gentoo.org/repo/proj/guru.git) 63 | 64 | ### Pop!_OS 65 | 66 | Available on the [Pop!_OS PPA](https://launchpad.net/~system76/+archive/ubuntu/pop) (outdated) 67 | 68 | 69 | ## Building 70 | 71 | Requirements: 72 | * [Rust toolchain](https://rustup.rs/) 73 | * Any C compiler 74 | * Linux 4.20+ built with `CONFIG_PSI=y` 75 | 76 | ```shell 77 | git clone https://github.com/vrmiguel/bustd 78 | cd bustd && cargo run --release 79 | ``` 80 | 81 | The `-n, --no-daemon` flag is useful for running `bustd` through an init system such as `systemd`. 82 | 83 | ## Prebuilt binaries 84 | 85 | Binaries are generated at every commit through [GitHub Actions](https://github.com/vrmiguel/bustd/actions) 86 | 87 | ## TODO 88 | 89 | - [x] Allow for customization of the critical scenario (PSI cutoff) 90 | - [x] Command-line argument for disabling daemonization (useful for runnning `bustd` as a systemd service) 91 | - [x] Command-line argument to enable killing the entire process group, not just the chosen process itself 92 | - [x] Allow the user to setup a list of software that `bustd` should never kill 93 | - [ ] Notification sending and general notification customization settings 94 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: build-and-test 4 | 5 | jobs: 6 | armv7-glibc: 7 | name: Ubuntu 18.04 (for ARMv7 - glibc) 8 | runs-on: ubuntu-18.04 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | toolchain: stable 14 | target: armv7-unknown-linux-gnueabihf 15 | override: true 16 | 17 | - name: Install binutils-arm-none-eabi 18 | run: | 19 | sudo apt-get update 20 | sudo apt-get install binutils-arm-none-eabi 21 | 22 | - uses: actions-rs/cargo@v1 23 | with: 24 | use-cross: true 25 | command: build 26 | args: --release --target=armv7-unknown-linux-gnueabihf 27 | 28 | - name: Strip binary 29 | run: arm-none-eabi-strip target/armv7-unknown-linux-gnueabihf/release/bustd 30 | 31 | - name: Upload binary 32 | uses: actions/upload-artifact@v2 33 | with: 34 | name: 'bustd-linux-armv7-glibc' 35 | path: target/armv7-unknown-linux-gnueabihf/release/bustd 36 | 37 | armv7-musl: 38 | name: Ubuntu 20.01 (for ARMv7 - musl) 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v2 42 | - uses: actions-rs/toolchain@v1 43 | with: 44 | toolchain: stable 45 | target: armv7-unknown-linux-musleabihf 46 | override: true 47 | 48 | - name: Install binutils-arm-none-eabi 49 | run: | 50 | sudo apt-get update 51 | sudo apt-get install binutils-arm-none-eabi 52 | 53 | - name: Run cargo build 54 | uses: actions-rs/cargo@v1 55 | with: 56 | use-cross: true 57 | command: build 58 | args: --release --target=armv7-unknown-linux-musleabihf 59 | 60 | - name: Strip binary 61 | run: arm-none-eabi-strip target/armv7-unknown-linux-musleabihf/release/bustd 62 | 63 | - name: Upload binary 64 | uses: actions/upload-artifact@v2 65 | with: 66 | name: 'bustd-linux-armv7-musl' 67 | path: target/armv7-unknown-linux-musleabihf/release/bustd 68 | 69 | ubuntu: 70 | name: Ubuntu 20.04 71 | runs-on: ubuntu-latest 72 | strategy: 73 | matrix: 74 | rust: 75 | - stable 76 | steps: 77 | - name: Checkout sources 78 | uses: actions/checkout@v2 79 | 80 | - name: Install toolchain 81 | uses: actions-rs/toolchain@v1 82 | with: 83 | toolchain: stable 84 | target: x86_64-unknown-linux-musl 85 | override: true 86 | 87 | - name: Install dependencies for musl libc 88 | run: | 89 | sudo apt-get update 90 | sudo apt-get install musl-tools 91 | - name: Run cargo build 92 | uses: actions-rs/cargo@v1 93 | with: 94 | command: build 95 | args: --release --target x86_64-unknown-linux-musl 96 | 97 | - name: Run cargo test 98 | uses: actions-rs/cargo@v1 99 | with: 100 | command: test 101 | args: --release --target x86_64-unknown-linux-musl 102 | 103 | - name: Strip binary 104 | run: strip target/x86_64-unknown-linux-musl/release/bustd 105 | 106 | - name: Upload binary 107 | uses: actions/upload-artifact@v2 108 | with: 109 | name: 'bustd-x86-64-musl' 110 | path: target/x86_64-unknown-linux-musl/release/bustd 111 | 112 | ubuntu-glibc: 113 | name: Ubuntu 18.04 - glibc 114 | runs-on: ubuntu-18.04 115 | strategy: 116 | matrix: 117 | rust: 118 | - stable 119 | steps: 120 | - name: Checkout sources 121 | uses: actions/checkout@v2 122 | 123 | - name: Install toolchain 124 | uses: actions-rs/toolchain@v1 125 | with: 126 | toolchain: stable 127 | 128 | - name: Run cargo build 129 | uses: actions-rs/cargo@v1 130 | with: 131 | command: build 132 | args: --release 133 | 134 | - name: Run cargo test 135 | uses: actions-rs/cargo@v1 136 | with: 137 | command: test 138 | args: --release 139 | 140 | - name: Strip binary 141 | run: strip target/release/bustd 142 | 143 | - name: Upload binary 144 | uses: actions/upload-artifact@v2 145 | with: 146 | name: 'bustd-x86-64-glibc' 147 | path: target/release/bustd 148 | -------------------------------------------------------------------------------- /zig/src/monitor.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const time = std.time; 3 | const math = std.math; 4 | 5 | const memory = @import("memory.zig"); 6 | const pressure = @import("pressure.zig"); 7 | const process = @import("process.zig"); 8 | const config = @import("config.zig"); 9 | 10 | const MemoryStatusTag = enum { 11 | ok, 12 | near_terminal, 13 | }; 14 | 15 | const MemoryStatus = union(MemoryStatusTag) { 16 | /// Memory is "okay": basically no risk of memory thrashing 17 | ok: void, 18 | /// Nearing the terminal PSI cutoff: memory thrashing is occurring or close to it. Holds the current PSI value. 19 | near_terminal: f32, 20 | }; 21 | 22 | pub const Monitor = struct { 23 | mem_info: memory.MemoryInfo, 24 | /// Memory status as of last checked 25 | status: MemoryStatus, 26 | /// A pointer to a buffer of at least 128 bytes 27 | buffer: []u8, 28 | const Self = @This(); 29 | 30 | pub fn new(buffer: []u8) !Self { 31 | var self = Self{ 32 | .mem_info = undefined, 33 | .status = undefined, 34 | .buffer = buffer, 35 | }; 36 | 37 | try self.updateMemoryStats(); 38 | 39 | return self; 40 | } 41 | 42 | pub fn updateMemoryStats(self: *Self) !void { 43 | self.mem_info = try memory.MemoryInfo.new(); 44 | self.status = blk: { 45 | if (self.mem_info.available_ram_percent <= config.free_ram_threshold) { 46 | const psi = try pressure.pressureSomeAvg10(self.buffer); 47 | std.log.warn("read avg10: {}", .{psi}); 48 | break :blk MemoryStatus{ .near_terminal = psi }; 49 | } else { 50 | break :blk MemoryStatus.ok; 51 | } 52 | }; 53 | } 54 | 55 | fn freeUpMemory(self: *Self) !void { 56 | const victim_process = try process.findVictimProcess(self.buffer); 57 | 58 | // Check for memory stats again to see if the 59 | // low-memory situation was solved while 60 | // we were searching for our victim 61 | try self.updateMemoryStats(); 62 | if (self.isMemoryLow()) { 63 | try victim_process.terminateSelf(); 64 | } 65 | } 66 | 67 | pub fn poll(self: *Self) !void { 68 | while (true) { 69 | if (self.isMemoryLow()) { 70 | try self.freeUpMemory(); 71 | } 72 | 73 | try self.updateMemoryStats(); 74 | const sleep_time = self.sleepTimeNs(); 75 | std.log.warn("sleeping for {}ms, {}% of RAM is free", .{ sleep_time, self.mem_info.available_ram_percent }); 76 | 77 | // Convert ms to ns 78 | time.sleep(sleep_time * 1000000); 79 | } 80 | } 81 | 82 | /// Determines for how long buztd should sleep 83 | /// This function is essentially a copy of how earlyoom calculates its sleep time 84 | /// 85 | /// Credits: https://github.com/rfjakob/earlyoom/blob/dea92ae67997fcb1a0664489c13d49d09d472d40/main.c#L365 86 | /// MIT Licensed 87 | fn sleepTimeNs(self: *const Self) u64 { 88 | // Maximum expected memory fill rate as seen 89 | // with `stress -m 4 --vm-bytes 4G` 90 | const ram_fill_rate: i64 = 6000; 91 | // Maximum expected swap fill rate as seen 92 | // with membomb on zRAM 93 | const swap_fill_rate: i64 = 800; 94 | 95 | // Maximum and minimum sleep times (in ms) 96 | const min_sleep: i64 = 100; 97 | const max_sleep: i64 = 1000; 98 | 99 | // TODO: make these percentages configurable by args./config. file 100 | const ram_terminal_percent: f64 = 10.0; 101 | const swap_terminal_percent: f64 = 10.0; 102 | 103 | const f_ram_headroom_kib = (@intToFloat(f64, self.mem_info.available_ram_percent) - ram_terminal_percent) * (@intToFloat(f64, self.mem_info.total_ram_mb) * 10.0); 104 | const f_swap_headroom_kib = (@intToFloat(f64, self.mem_info.available_swap_percent) - swap_terminal_percent) * (@intToFloat(f64, self.mem_info.total_swap_mb) * 10.0); 105 | 106 | const i_ram_headroom_kib = math.max(0, @floatToInt(i64, f_ram_headroom_kib)); 107 | const i_swap_headroom_kib = math.max(0, @floatToInt(i64, f_swap_headroom_kib)); 108 | 109 | var time_to_sleep = @divFloor(i_ram_headroom_kib, ram_fill_rate) + @divFloor(i_swap_headroom_kib, swap_fill_rate); 110 | time_to_sleep = math.min(time_to_sleep, max_sleep); 111 | time_to_sleep = math.max(time_to_sleep, min_sleep); 112 | 113 | return @intCast(u64, time_to_sleep); 114 | } 115 | 116 | fn isMemoryLow(self: *const Self) bool { 117 | return switch (self.status) { 118 | MemoryStatusTag.ok => false, 119 | MemoryStatusTag.near_terminal => |psi| psi >= config.cutoff_psi, 120 | }; 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /rust/src/monitor.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::cli::CommandLineArgs; 4 | use crate::error::Result; 5 | use crate::kill; 6 | use crate::memory; 7 | use crate::memory::MemoryInfo; 8 | use crate::process::Process; 9 | 10 | enum MemoryStatus { 11 | NearTerminal(f32), 12 | Okay, 13 | } 14 | 15 | pub struct Monitor { 16 | memory_info: MemoryInfo, 17 | proc_buf: [u8; 50], 18 | buf: [u8; 100], 19 | status: MemoryStatus, 20 | args: CommandLineArgs, 21 | } 22 | 23 | impl Monitor { 24 | /// Determines how much oomf should sleep 25 | /// This function is essentially a copy of how earlyoom calculates its sleep time 26 | /// 27 | /// Credits: https://github.com/rfjakob/earlyoom/blob/dea92ae67997fcb1a0664489c13d49d09d472d40/main.c#L365 28 | /// MIT Licensed 29 | pub fn sleep_time_ms(&self) -> Duration { 30 | // Maximum expected memory fill rate as seen 31 | // with `stress -m 4 --vm-bytes 4G` 32 | const RAM_FILL_RATE: i64 = 6000; 33 | // Maximum expected swap fill rate as seen 34 | // with membomb on zRAM 35 | const SWAP_FILL_RATE: i64 = 800; 36 | 37 | // Maximum and minimum time to sleep, in ms. 38 | const MIN_SLEEP: i64 = 100; 39 | const MAX_SLEEP: i64 = 1000; 40 | 41 | // TODO: make these percentages configurable by args./config. file 42 | const RAM_TERMINAL_PERCENT: f64 = 10.; 43 | const SWAP_TERMINAL_PERCENT: f64 = 10.; 44 | 45 | let ram_headroom_kib = (self.memory_info.available_ram_percent as f64 46 | - RAM_TERMINAL_PERCENT) 47 | * (self.memory_info.total_ram_mb as f64 * 10.0); 48 | let swap_headroom_kib = (self.memory_info.available_swap_percent as f64 49 | - SWAP_TERMINAL_PERCENT) 50 | * (self.memory_info.total_swap_mb as f64 * 10.0); 51 | 52 | let ram_headroom_kib = i64::max(ram_headroom_kib as i64, 0); 53 | let swap_headroom_kib = i64::max(swap_headroom_kib as i64, 0); 54 | 55 | let time_to_sleep = ram_headroom_kib / RAM_FILL_RATE + swap_headroom_kib / SWAP_FILL_RATE; 56 | let time_to_sleep = i64::min(time_to_sleep, MAX_SLEEP); 57 | let time_to_sleep = i64::max(time_to_sleep, MIN_SLEEP); 58 | 59 | Duration::from_millis(time_to_sleep as u64) 60 | } 61 | 62 | pub fn new(proc_buf: [u8; 50], mut buf: [u8; 100], args: CommandLineArgs) -> Result { 63 | let memory_info = MemoryInfo::new()?; 64 | let status = if memory_info.available_ram_percent <= 15 { 65 | MemoryStatus::NearTerminal(memory::pressure::pressure_some_avg10(&mut buf)?) 66 | } else { 67 | MemoryStatus::Okay 68 | }; 69 | 70 | Ok(Self { 71 | memory_info, 72 | proc_buf, 73 | buf, 74 | status, 75 | args, 76 | }) 77 | } 78 | 79 | fn memory_is_low(&self) -> bool { 80 | let terminal_psi = self.args.cutoff_psi; 81 | matches!(self.status, MemoryStatus::NearTerminal(psi) if psi >= terminal_psi) 82 | } 83 | 84 | fn get_victim(&mut self) -> Result { 85 | kill::choose_victim(&mut self.proc_buf, &mut self.buf, &self.args) 86 | } 87 | 88 | fn update_memory_stats(&mut self) -> Result<()> { 89 | self.memory_info = memory::MemoryInfo::new()?; 90 | self.status = if self.memory_info.available_ram_percent <= 15 { 91 | let psi = memory::pressure::pressure_some_avg10(&mut self.buf)?; 92 | MemoryStatus::NearTerminal(psi) 93 | } else { 94 | MemoryStatus::Okay 95 | }; 96 | Ok(()) 97 | } 98 | 99 | fn free_up_memory(&mut self) -> Result<()> { 100 | let victim = self.get_victim()?; 101 | 102 | // TODO: is this necessary? 103 | // 104 | // Check for memory stats again to see if the 105 | // low-memory situation was solved while 106 | // we were searching for our victim 107 | self.update_memory_stats()?; 108 | if self.memory_is_low() { 109 | if self.args.kill_pgroup { 110 | kill::kill_process_group(victim)?; 111 | } else { 112 | kill::kill_and_wait(victim)?; 113 | } 114 | } 115 | Ok(()) 116 | } 117 | 118 | // Use the never type here whenever it reaches stable 119 | #[allow(unreachable_code)] 120 | pub fn poll(&mut self) -> Result<()> { 121 | loop { 122 | // Update our memory readings 123 | self.update_memory_stats()?; 124 | if self.memory_is_low() { 125 | self.free_up_memory()?; 126 | } 127 | 128 | // Calculating the adaptive sleep time 129 | let sleep_time = self.sleep_time_ms(); 130 | if self.args.verbose { 131 | eprintln!("[adaptive-sleep] {}ms", sleep_time.as_millis()); 132 | } 133 | 134 | std::thread::sleep(sleep_time); 135 | } 136 | Ok(()) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /rust/src/kill.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::time::Duration; 3 | use std::time::Instant; 4 | 5 | use libc::kill; 6 | use libc::{EINVAL, EPERM, ESRCH, SIGKILL, SIGTERM}; 7 | 8 | use crate::errno::errno; 9 | use crate::error::{Error, Result}; 10 | use crate::process::Process; 11 | use crate::{cli, utils}; 12 | 13 | pub fn choose_victim( 14 | proc_buf: &mut [u8], 15 | buf: &mut [u8], 16 | args: &cli::CommandLineArgs, 17 | ) -> Result { 18 | let now = Instant::now(); 19 | 20 | // `args` is currently only used when checking for unkillable patterns 21 | #[cfg(not(feature = "glob-ignore"))] 22 | let _ = args; 23 | 24 | let mut processes = fs::read_dir("/proc/")? 25 | .filter_map(|e| e.ok()) 26 | .filter_map(|entry| entry.file_name().to_str()?.trim().parse::().ok()) 27 | .filter(|pid| *pid > 1) 28 | .filter_map(|pid| Process::from_pid(pid, proc_buf).ok()); 29 | 30 | let first_process = processes.next(); 31 | if first_process.is_none() { 32 | // Likely an impossible scenario but we found no process to kill! 33 | return Err(Error::ProcessNotFound("choose_victim")); 34 | } 35 | 36 | let mut victim = first_process.unwrap(); 37 | // TODO: find another victim if victim.vm_rss_kib() fails here 38 | let mut victim_vm_rss_kib = victim.vm_rss_kib(buf)?; 39 | 40 | for process in processes { 41 | if victim.oom_score > process.oom_score { 42 | // Our current victim is less innocent than the process being analysed 43 | continue; 44 | } 45 | 46 | #[cfg(feature = "glob-ignore")] 47 | { 48 | if let Some(patterns) = &args.ignored { 49 | if matches!(process.is_unkillable(buf, patterns), Ok(true)) { 50 | continue; 51 | } 52 | } 53 | } 54 | 55 | let cur_vm_rss_kib = process.vm_rss_kib(buf)?; 56 | if cur_vm_rss_kib == 0 { 57 | // Current process is a kernel thread 58 | continue; 59 | } 60 | 61 | if process.oom_score == victim.oom_score && cur_vm_rss_kib <= victim_vm_rss_kib { 62 | continue; 63 | } 64 | 65 | let cur_oom_score_adj = match process.oom_score_adj(buf) { 66 | Ok(oom_score_adj) => oom_score_adj, 67 | // TODO: warn that this error happened 68 | Err(_) => continue, 69 | }; 70 | 71 | if cur_oom_score_adj == -1000 { 72 | // Follow the behaviour of the standard OOM killer: don't kill processes with oom_score_adj equals to -1000 73 | continue; 74 | } 75 | 76 | // eprintln!("[DBG] New victim with PID={}!", process.pid); 77 | victim = process; 78 | victim_vm_rss_kib = cur_vm_rss_kib; 79 | } 80 | 81 | println!("[LOG] Found victim in {} secs.", now.elapsed().as_secs()); 82 | println!( 83 | "[LOG] Victim => pid: {}, comm: {}, oom_score: {}", 84 | victim.pid, 85 | victim.comm(buf).unwrap_or("unknown").trim(), 86 | victim.oom_score 87 | ); 88 | 89 | Ok(victim) 90 | } 91 | 92 | pub fn kill_process(pid: i32, signal: i32) -> Result<()> { 93 | let res = unsafe { kill(pid, signal) }; 94 | 95 | if res == -1 { 96 | return Err(match errno() { 97 | // An invalid signal was specified 98 | EINVAL => Error::InvalidSignal, 99 | // Calling process doesn't have permission to send signals to any 100 | // of the target processes 101 | EPERM => Error::NoPermission, 102 | // The target process or process group does not exist. 103 | ESRCH => Error::ProcessNotFound("kill"), 104 | _ => Error::UnknownKill, 105 | }); 106 | } 107 | 108 | Ok(()) 109 | } 110 | 111 | pub fn kill_process_group(process: Process) -> Result<()> { 112 | let pid = process.pid; 113 | 114 | let pgid = utils::get_process_group(pid as i32)?; 115 | 116 | // TODO: kill and wait 117 | let _ = kill_process(-pgid, SIGTERM); 118 | 119 | Ok(()) 120 | } 121 | 122 | /// Tries to kill a process and wait for it to exit 123 | /// Will first send the victim a SIGTERM and escalate to SIGKILL if necessary 124 | /// Returns Ok(true) if the victim was successfully terminated 125 | pub fn kill_and_wait(process: Process) -> Result { 126 | let pid = process.pid; 127 | let now = Instant::now(); 128 | 129 | let _ = kill_process(pid as i32, SIGTERM); 130 | 131 | let half_a_sec = Duration::from_secs_f32(0.5); 132 | let mut sigkill_sent = false; 133 | 134 | for _ in 0..20 { 135 | std::thread::sleep(half_a_sec); 136 | if !process.is_alive() { 137 | println!("[LOG] Process with PID {} has exited.\n", pid); 138 | return Ok(true); 139 | } 140 | if !sigkill_sent { 141 | let _ = kill_process(pid as i32, SIGKILL); 142 | sigkill_sent = true; 143 | println!( 144 | "[LOG] Escalated to SIGKILL after {} nanosecs", 145 | now.elapsed().as_nanos() 146 | ); 147 | } 148 | } 149 | 150 | Ok(false) 151 | } 152 | -------------------------------------------------------------------------------- /rust/src/process.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | use std::io::Write; 3 | 4 | use libc::getpgid; 5 | 6 | use crate::{ 7 | error::{Error, Result}, 8 | utils::{self, str_from_bytes}, 9 | }; 10 | 11 | #[derive(Debug, Default)] 12 | pub struct Process { 13 | pub pid: u32, 14 | pub oom_score: i16, 15 | } 16 | 17 | impl Process { 18 | pub fn from_pid(pid: u32, buf: &mut [u8]) -> Result { 19 | let oom_score = 20 | Self::oom_score_from_pid(pid, buf).or(Err(Error::ProcessNotFound("from_pid")))?; 21 | Ok(Self { pid, oom_score }) 22 | } 23 | 24 | #[allow(dead_code)] 25 | /// Returns the current process represented as a Process struct 26 | /// Unused in the actual code but very often used when debugging 27 | pub fn this(buf: &mut [u8]) -> Result { 28 | let pid = unsafe { libc::getpid() } as u32; 29 | 30 | Self::from_pid(pid, buf) 31 | } 32 | 33 | /// Return true if the process is alive 34 | /// Could still return true if the process has exited but hasn't yet been reaped. 35 | /// TODO: would it be better to check for /proc// in here? 36 | pub fn is_alive_from_pid(pid: u32) -> bool { 37 | // Safety: `getpgid` is memory safe 38 | let group_id = unsafe { getpgid(pid as i32) }; 39 | 40 | group_id > 0 41 | } 42 | 43 | pub fn is_alive(&self) -> bool { 44 | Self::is_alive_from_pid(self.pid) 45 | } 46 | 47 | pub fn comm<'a>(&self, buf: &'a mut [u8]) -> Result<&'a str> { 48 | write!(&mut *buf, "/proc/{}/comm\0", self.pid)?; 49 | { 50 | let mut file = utils::file_from_buffer(buf)?; 51 | buf.fill(0); 52 | let _ = file.read(buf)?; 53 | } 54 | 55 | str_from_bytes(buf) 56 | } 57 | 58 | pub fn oom_score_from_pid(pid: u32, buf: &mut [u8]) -> Result { 59 | write!(&mut *buf, "/proc/{}/oom_score\0", pid)?; 60 | let contents = { 61 | let mut file = utils::file_from_buffer(buf)?; 62 | buf.fill(0); 63 | let _ = file.read(buf)?; 64 | 65 | str_from_bytes(buf)?.trim() 66 | }; 67 | 68 | Ok(contents.parse()?) 69 | } 70 | 71 | /// Reads VmRSS from /proc//statm 72 | /// In order to match the VmRSS value in /proc//status, we'll 73 | /// multiply the number of pages in `statm` by the page size of our system and then convert 74 | /// that value to KiB 75 | pub fn vm_rss_kib(&self, buf: &mut [u8]) -> Result { 76 | write!(&mut *buf, "/proc/{}/statm\0", self.pid)?; 77 | let mut columns = { 78 | let mut file = utils::file_from_buffer(buf)?; 79 | buf.fill(0); 80 | let _ = file.read(buf)?; 81 | 82 | str_from_bytes(buf)?.split_ascii_whitespace() 83 | }; 84 | let vm_rss: i64 = columns.nth(1).ok_or(Error::MalformedStatm)?.parse()?; 85 | 86 | let page_size = utils::page_size()?; 87 | 88 | // Converting VM RSS to KiB 89 | let vm_rss_kib = vm_rss * page_size / 1024; 90 | Ok(vm_rss_kib) 91 | } 92 | 93 | #[cfg(feature = "glob-ignore")] 94 | /// Checks if the process' name matches any of the given glob patterns 95 | pub fn is_unkillable(&self, buf: &mut [u8], patterns: &[glob::Pattern]) -> Result { 96 | let comm = self.comm(buf)?.trim(); 97 | for pattern in patterns { 98 | if pattern.matches(comm) { 99 | println!( 100 | "Skipping \"{}\" since it matches an unkillable pattern", 101 | comm 102 | ); 103 | return Ok(true); 104 | } 105 | } 106 | 107 | Ok(false) 108 | } 109 | 110 | pub fn oom_score_adj(&self, buf: &mut [u8]) -> Result { 111 | write!(&mut *buf, "/proc/{}/oom_score_adj\0", self.pid)?; 112 | let contents = { 113 | let mut file = utils::file_from_buffer(buf)?; 114 | buf.fill(0); 115 | let _ = file.read(buf)?; 116 | 117 | str_from_bytes(buf)?.trim() 118 | }; 119 | 120 | Ok(contents.parse()?) 121 | } 122 | } 123 | 124 | #[cfg(test)] 125 | mod tests { 126 | // We'll use the Process struct from procfs 127 | // in order to test our own Process struct. 128 | // 129 | // The reason we don't use `procfs` directly is 130 | // because our implementation is considerably leaner. 131 | use procfs; 132 | 133 | // Returns the Process representing the 134 | // process of the caller test 135 | fn this() -> ([u8; 100], crate::process::Process) { 136 | let mut buf = [0_u8; 100]; 137 | (buf, crate::process::Process::this(&mut buf).unwrap()) 138 | } 139 | 140 | #[test] 141 | fn comm() { 142 | let (mut buf, this) = this(); 143 | let comm = this.comm(&mut buf).unwrap(); 144 | 145 | // We'll now represent the current process using 146 | // the external `procfs` crate as well 147 | let _this = procfs::process::Process::myself().unwrap(); 148 | let _stat = _this.stat().unwrap(); 149 | let _comm = _stat.comm; 150 | 151 | assert_eq!(comm.trim(), _comm) 152 | } 153 | 154 | #[test] 155 | fn oom_score() { 156 | let (_, this) = this(); 157 | 158 | let _this = procfs::process::Process::myself().unwrap(); 159 | 160 | assert_eq!(this.oom_score, _this.oom_score().unwrap() as i16); 161 | } 162 | 163 | #[test] 164 | fn pid() { 165 | let (_, this) = this(); 166 | 167 | let _this = procfs::process::Process::myself().unwrap(); 168 | 169 | assert_eq!(this.pid as i32, _this.pid); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /zig/README.md: -------------------------------------------------------------------------------- 1 | :warning: This will be marged with the original [bustd](https://github.com/vrmiguel/bustd) repository. 2 | 3 | # `buztd`: Available memory or bust! 4 | 5 | `buztd` is a lightweight process killer daemon for out-of-memory scenarios for Linux! 6 | 7 | This particular project is a Zig version of the [original `bustd` project](https://github.com/vrmiguel/bustd). 8 | 9 | ## Features 10 | 11 | ### Extremely thin memory usage 12 | 13 | The Zig version of `bustd` makes no heap allocations and relies solely on a single 128-byte buffer in the stack for all its allocation needs. 14 | 15 | ### Small CPU usage 16 | 17 | Much like `earlyoom` and `nohang`, `buztd` uses adaptive sleep times during its memory polling. 18 | 19 | Unlike these two, however, `buztd` does not read from `/proc/meminfo`, instead opting for the `sysinfo` syscall. 20 | 21 | This approach has its up- and downsides. The amount of free RAM that `sysinfo` reads does not account for cached memory, while `MemAvailable` in `/proc/meminfo` does. 22 | 23 | However, the `sysinfo` syscall is one order of magnitude faster than parsing `/proc/meminfo`, at least according to [this kernel patch](https://sourceware.org/legacy-ml/libc-alpha/2015-08/msg00512.html) (granted, from 2015). 24 | 25 | As `buztd` can't solely rely on the free RAM readings of `sysinfo`, we check for memory stress through [Pressure Stall Information](https://www.kernel.org/doc/html/v5.8/accounting/psi.html). 26 | 27 | More on that below. 28 | 29 | ### `buztd` will try to lock all pages mapped into its address space 30 | 31 | Much like `earlyoom`, `buztd` uses [`mlockall`](https://www.ibm.com/docs/en/aix/7.2?topic=m-mlockall-munlockall-subroutine) to avoid being sent to swap, which allows the daemon to remain responsive even when the system memory is under heavy load and susceptible to [thrashing](https://en.wikipedia.org/wiki/Thrashing_(computer_science)). 32 | 33 | ### Checks for Pressure Stall Information 34 | 35 | The Linux kernel, since version 4.20 (and built with `CONFIG_PSI=y`), presents canonical new pressure metrics for memory, CPU, and IO. 36 | In the words of [Facebook Incubator](https://facebookmicrosites.github.io/psi/docs/overview): 37 | 38 | ``` 39 | PSI stats are like barometers that provide fair warning of impending resource 40 | shortages, enabling you to take more proactive, granular, and nuanced steps 41 | when resources start becoming scarce. 42 | ``` 43 | 44 | More specifically, `buztd` checks for how long, in microseconds, processes have stalled in the last 10 seconds. By default, `buztd` will kill a process when processes have stalled for 25 microseconds in the last ten seconds. 45 | 46 | Example: 47 | ``` 48 | some avg10=0.00 avg60=0.00 avg300=0.00 total=11220657 49 | full avg10=0.00 avg60=0.00 avg300=0.00 total=10947429 50 | ``` 51 | 52 | These ratios are percentages of recent trends over ten, sixty, and three hundred second windows. 53 | 54 | The `some` row indicates the percentage of time n that given time frame in which _any_ process has stalled due to memory thrashing. 55 | 56 | `buztd` allows you to configure the value of `some avg10` in which, if surpassed, some process will be killed. 57 | 58 | The ideal value for this cutoff varies a lot between systems. 59 | 60 | Try messing around with `tools/mem-eater.c` to guesstimate a value that works well for you. 61 | 62 | ## Building 63 | 64 | Requirements: 65 | * [Zig 0.10](https://ziglang.org/) 66 | * Linux 4.20+ built with `CONFIG_PSI=y` 67 | 68 | ```shell 69 | git clone https://github.com/vrmiguel/buztd 70 | cd buztd 71 | 72 | # Choose which compilation mode you'd like: 73 | zig build -Drelease-fast # Turns on optimization and disables safety checks 74 | zig build -Drelease-safe # Turns on optimization and keeps safety checks 75 | zig build -Drelease-small # Turns on size optimizations and disables safety checks 76 | ``` 77 | 78 | ## Configuration 79 | 80 | As of the time of writing, this version of `buztd` offers no command-line argument parsing, but allows easy configuration through the `src/config.zig` file. 81 | 82 | 83 | ```zig 84 | /// Sets whether or not buztd should daemonize 85 | /// itself. Don't use this if running buztd as a systemd 86 | /// service or something of the sort. 87 | pub const should_daemonize: bool = false; 88 | 89 | /// Free RAM percentage figures below this threshold are considered to be near terminal, meaning 90 | /// that buztd will start to check for Pressure Stall Information whenever the 91 | /// free RAM figures go below this. 92 | /// However, this free RAM amount is what the sysinfo syscall gives us, which does not take in consideration 93 | /// reclaimable or cached pages. The true free RAM amount available to the OS is bigger than what it indicates. 94 | pub const free_ram_threshold: u8 = 15; 95 | 96 | /// The Linux kernel presents canonical pressure metrics for memory, found in `/proc/pressure/memory`. 97 | /// Example: 98 | /// some avg10=0.00 avg60=0.00 avg300=0.00 total=11220657 99 | /// full avg10=0.00 avg60=0.00 avg300=0.00 total=10947429 100 | /// These ratios are percentages of recent trends over ten, sixty, and 101 | /// three hundred second windows. The `some` row indicates the percentage of time 102 | // in that given time frame in which _any_ process has stalled due to memory thrashing. 103 | /// 104 | /// This value configured here is the value of `some avg10` in which, if surpassed, some 105 | /// process will be killed. 106 | /// 107 | /// The ideal value for this cutoff varies a lot between systems. 108 | /// Try messing around with `tools/mem-eater.c` to guesstimate a value that works well for you. 109 | pub const cutoff_psi: f32 = 0.05; 110 | 111 | /// Sets processes that buztd must never kill. 112 | /// The values expected here are the `comm` values of the process you don't want to have terminated. 113 | /// A comm-value is the filename of the executable truncated to 16 characters.. 114 | pub const unkillables = std.ComptimeStringMap(void, .{ 115 | .{ "firefox", void }, 116 | .{ "rustc", void }, 117 | .{ "electron", void }, 118 | }); 119 | 120 | 121 | /// If any error occurs, restarts the monitoring instead of exiting with an unsuccesful status code 122 | pub const retry: bool = true; 123 | ``` 124 | 125 | 126 | -------------------------------------------------------------------------------- /zig/src/process.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const csig = @cImport({ 3 | @cInclude("signal.h"); 4 | }); 5 | const unistd = @cImport({ 6 | @cInclude("unistd.h"); 7 | }); 8 | const config = @import("config.zig"); 9 | const fs = std.fs; 10 | const fmt = std.fmt; 11 | const mem = std.mem; 12 | const os = std.os; 13 | const libc = std.c; 14 | const time = std.time; 15 | 16 | fn signalToString(signal: u8) []const u8 { 17 | return switch (signal) { 18 | csig.SIGTERM => "SIGTERM", 19 | csig.SIGKILL => "SIGKILL", 20 | else => "unknown", 21 | }; 22 | } 23 | 24 | pub const Process = struct { 25 | pid: u32, 26 | oom_score: i16, 27 | buffer: []u8, 28 | 29 | const Self = @This(); 30 | 31 | const ProcessError = error{ MalformedOomScore, MalformedOomScoreAdj, MalformedVmRss }; 32 | 33 | fn fromPid(pid: u32, buffer: []u8) !Self { 34 | const oom_score = try oomScoreFromPid(pid, buffer); 35 | 36 | return Self{ .pid = pid, .oom_score = oom_score, .buffer = buffer }; 37 | } 38 | 39 | fn oomScoreFromPid(pid: u32, buffer: []u8) !i16 { 40 | const path = try fmt.bufPrint(buffer, "/proc/{}/oom_score", .{pid}); 41 | 42 | // The file descriptor for the oom_score file of this process 43 | const oom_score_fd = try os.open(path, os.O.RDONLY, 0); 44 | defer os.close(oom_score_fd); 45 | 46 | const bytes_read = try os.read(oom_score_fd, buffer); 47 | 48 | const oom_score = parse(i16, buffer[0 .. bytes_read - 1]) orelse return error.MalformedOomScore; 49 | 50 | return oom_score; 51 | } 52 | 53 | pub fn oomScoreAdj(self: *const Self) !i16 { 54 | const path = try fmt.bufPrint(self.buffer, "/proc/{}/oom_score_adj", .{self.pid}); 55 | 56 | // The file descriptor for the oom_score file of this process 57 | const oom_score_adj_fd = try os.open(path, os.O.RDONLY, 0); 58 | defer os.close(oom_score_adj_fd); 59 | 60 | const bytes_read = try os.read(oom_score_adj_fd, self.buffer); 61 | 62 | const oom_score_adj = parse(i16, self.buffer[0 .. bytes_read - 1]) orelse return error.MalformedOomScoreAdj; 63 | 64 | return oom_score_adj; 65 | } 66 | 67 | pub fn comm(self: *const Self) ![]u8 { 68 | const path = try fmt.bufPrint(self.buffer, "/proc/{}/comm", .{self.pid}); 69 | 70 | // The file descriptor for the oom_score file of this process 71 | const comm_fd = try os.open(path, os.O.RDONLY, 0); 72 | defer os.close(comm_fd); 73 | 74 | const bytes_read = try os.read(comm_fd, self.buffer); 75 | 76 | return self.buffer[0 .. bytes_read - 1]; 77 | } 78 | 79 | pub fn isAlive(self: *const Self) bool { 80 | const group_id = unistd.getpgid(@intCast(c_int, self.pid)); 81 | 82 | return group_id > 0; 83 | } 84 | 85 | pub fn vmRss(self: *const Self) !usize { 86 | var filename = try fmt.bufPrint(self.buffer, "/proc/{}/statm", .{self.pid}); 87 | 88 | var statm_file = try fs.cwd().openFile(filename, .{}); 89 | defer statm_file.close(); 90 | var statm_reader = statm_file.reader(); 91 | 92 | // Skip first field (total program size) 93 | try statm_reader.skipUntilDelimiterOrEof(' '); 94 | var rss_str = try statm_reader.readUntilDelimiter(self.buffer, ' '); 95 | 96 | var ret = parse(usize, rss_str) orelse return error.MalformedVmRss; 97 | return (ret * std.mem.page_size) / 1024; 98 | } 99 | 100 | pub fn signalSelf(self: *const Self, signal: u8) void { 101 | // Don't warn `kill` failure if the process is no longer alive 102 | if (0 != libc.kill(@intCast(i32, self.pid), signal) and self.isAlive()) { 103 | std.log.warn("Failed to send {s} to process {}", .{ signalToString(signal), self.pid }); 104 | } else { 105 | std.log.warn("Successfully sent {s} to process {}", .{ signalToString(signal), self.pid }); 106 | } 107 | } 108 | 109 | pub fn terminateSelf(self: Self) !void { 110 | const quarter_sec_in_ns: u64 = 250000000; 111 | 112 | self.signalSelf(csig.SIGTERM); 113 | 114 | var attempt: u8 = 0; 115 | 116 | while (attempt < 20) : (attempt += 1) { 117 | time.sleep(quarter_sec_in_ns); 118 | if (!self.isAlive()) { 119 | std.log.warn("Process {} has exited.", .{self.pid}); 120 | return; 121 | } 122 | // Escalate to sigkill 123 | self.signalSelf(csig.SIGKILL); 124 | } 125 | } 126 | }; 127 | 128 | /// Wrapper over fmt.parseInt which returns null 129 | /// in failure instead of an error 130 | fn parse(comptime T: type, buf: []const u8) ?T { 131 | return fmt.parseInt(T, buf, 10) catch null; 132 | } 133 | 134 | /// Used to try to give LLVM hints on branch prediction. 135 | /// 136 | /// I have no idea how effective this is in practice. 137 | fn coldNoOp() void { 138 | @setCold(true); 139 | } 140 | 141 | /// Searches for a process to kill in order to 142 | /// free up memory 143 | pub fn findVictimProcess(buffer: []u8) !Process { 144 | var victim: Process = undefined; 145 | var victim_vm_rss: usize = undefined; 146 | var victim_is_undefined = true; 147 | 148 | var timer = try time.Timer.start(); 149 | 150 | var proc_dir = try fs.cwd().openIterableDir("/proc", .{ .access_sub_paths = false }); 151 | var proc_it = proc_dir.iterate(); 152 | 153 | while (try proc_it.next()) |proc_entry| { 154 | // We're only interested in directories of /proc 155 | if (proc_entry.kind != .Directory) { 156 | continue; 157 | } else { 158 | // `/proc` usually has much more directories than it has files 159 | coldNoOp(); 160 | } 161 | 162 | // But we're not interested in files that don't relate to a PID 163 | const pid = parse(u32, proc_entry.name) orelse continue; 164 | 165 | // Don't consider killing the init system 166 | if (pid <= 1) { 167 | coldNoOp(); 168 | continue; 169 | } 170 | 171 | const process = try Process.fromPid(pid, buffer); 172 | 173 | if (victim_is_undefined) { 174 | // We're still reading the first process so a victim hasn't been chosen 175 | coldNoOp(); 176 | victim = process; 177 | victim_vm_rss = try victim.vmRss(); 178 | victim_is_undefined = false; 179 | std.log.info("First victim set", .{}); 180 | } 181 | 182 | if (victim.oom_score > process.oom_score) { 183 | // Our current victim is less innocent than the process being analysed 184 | continue; 185 | } 186 | 187 | const victim_comm = try victim.comm(); 188 | if (config.unkillables.get(victim_comm) != null) { 189 | // The current process was set as unkillable 190 | continue; 191 | } 192 | 193 | const current_vm_rss = try process.vmRss(); 194 | if (current_vm_rss == 0) { 195 | // Current process is a kernel thread 196 | continue; 197 | } 198 | 199 | // TODO: recheck this 200 | if (process.oom_score == victim.oom_score and current_vm_rss <= victim_vm_rss) { 201 | continue; 202 | } 203 | 204 | const current_oom_score_adj = process.oomScoreAdj() catch { 205 | std.log.warn("Failed to read adj. OOM score for PID {}. Continuing.", .{process.pid}); 206 | continue; 207 | }; 208 | 209 | if (current_oom_score_adj == -1000) { 210 | // Follow the behaviour of the standard OOM killer: don't kill processes with oom_score_adj equals to -1000 211 | continue; 212 | } 213 | 214 | victim = process; 215 | victim_vm_rss = current_vm_rss; 216 | } 217 | 218 | const ns_elapsed = timer.read(); 219 | std.debug.print("Victim found in {} ns.: {s} with PID {} and OOM score {}\n", .{ ns_elapsed, try victim.comm(), victim.pid, victim.oom_score }); 220 | 221 | return victim; 222 | } 223 | -------------------------------------------------------------------------------- /rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "argh" 7 | version = "0.1.10" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ab257697eb9496bf75526f0217b5ed64636a9cfafa78b8365c71bd283fcef93e" 10 | dependencies = [ 11 | "argh_derive", 12 | "argh_shared", 13 | ] 14 | 15 | [[package]] 16 | name = "argh_derive" 17 | version = "0.1.10" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "b382dbd3288e053331f03399e1db106c9fb0d8562ad62cb04859ae926f324fa6" 20 | dependencies = [ 21 | "argh_shared", 22 | "proc-macro2", 23 | "quote", 24 | "syn", 25 | ] 26 | 27 | [[package]] 28 | name = "argh_shared" 29 | version = "0.1.10" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "64cb94155d965e3d37ffbbe7cc5b82c3dd79dd33bd48e536f73d2cfb8d85506f" 32 | 33 | [[package]] 34 | name = "bitflags" 35 | version = "1.2.1" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 38 | 39 | [[package]] 40 | name = "bustd" 41 | version = "0.1.1" 42 | dependencies = [ 43 | "argh", 44 | "cc", 45 | "cfg-if", 46 | "daemonize", 47 | "glob", 48 | "libc", 49 | "memchr", 50 | "procfs", 51 | ] 52 | 53 | [[package]] 54 | name = "byteorder" 55 | version = "1.4.3" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 58 | 59 | [[package]] 60 | name = "cc" 61 | version = "1.0.68" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" 64 | 65 | [[package]] 66 | name = "cfg-if" 67 | version = "1.0.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 70 | 71 | [[package]] 72 | name = "daemonize" 73 | version = "0.5.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" 76 | dependencies = [ 77 | "libc", 78 | ] 79 | 80 | [[package]] 81 | name = "errno" 82 | version = "0.2.8" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 85 | dependencies = [ 86 | "errno-dragonfly", 87 | "libc", 88 | "winapi", 89 | ] 90 | 91 | [[package]] 92 | name = "errno-dragonfly" 93 | version = "0.1.2" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 96 | dependencies = [ 97 | "cc", 98 | "libc", 99 | ] 100 | 101 | [[package]] 102 | name = "glob" 103 | version = "0.3.1" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 106 | 107 | [[package]] 108 | name = "hex" 109 | version = "0.4.3" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 112 | 113 | [[package]] 114 | name = "io-lifetimes" 115 | version = "1.0.3" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" 118 | dependencies = [ 119 | "libc", 120 | "windows-sys", 121 | ] 122 | 123 | [[package]] 124 | name = "lazy_static" 125 | version = "1.4.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 128 | 129 | [[package]] 130 | name = "libc" 131 | version = "0.2.144" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" 134 | 135 | [[package]] 136 | name = "linux-raw-sys" 137 | version = "0.1.3" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" 140 | 141 | [[package]] 142 | name = "memchr" 143 | version = "2.5.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 146 | 147 | [[package]] 148 | name = "proc-macro2" 149 | version = "1.0.27" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" 152 | dependencies = [ 153 | "unicode-xid", 154 | ] 155 | 156 | [[package]] 157 | name = "procfs" 158 | version = "0.14.2" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "b1de8dacb0873f77e6aefc6d71e044761fcc68060290f5b1089fcdf84626bb69" 161 | dependencies = [ 162 | "bitflags", 163 | "byteorder", 164 | "hex", 165 | "lazy_static", 166 | "rustix", 167 | ] 168 | 169 | [[package]] 170 | name = "quote" 171 | version = "1.0.9" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 174 | dependencies = [ 175 | "proc-macro2", 176 | ] 177 | 178 | [[package]] 179 | name = "rustix" 180 | version = "0.36.4" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23" 183 | dependencies = [ 184 | "bitflags", 185 | "errno", 186 | "io-lifetimes", 187 | "libc", 188 | "linux-raw-sys", 189 | "windows-sys", 190 | ] 191 | 192 | [[package]] 193 | name = "syn" 194 | version = "1.0.73" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" 197 | dependencies = [ 198 | "proc-macro2", 199 | "quote", 200 | "unicode-xid", 201 | ] 202 | 203 | [[package]] 204 | name = "unicode-xid" 205 | version = "0.2.2" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 208 | 209 | [[package]] 210 | name = "winapi" 211 | version = "0.3.9" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 214 | dependencies = [ 215 | "winapi-i686-pc-windows-gnu", 216 | "winapi-x86_64-pc-windows-gnu", 217 | ] 218 | 219 | [[package]] 220 | name = "winapi-i686-pc-windows-gnu" 221 | version = "0.4.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 224 | 225 | [[package]] 226 | name = "winapi-x86_64-pc-windows-gnu" 227 | version = "0.4.0" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 230 | 231 | [[package]] 232 | name = "windows-sys" 233 | version = "0.42.0" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 236 | dependencies = [ 237 | "windows_aarch64_gnullvm", 238 | "windows_aarch64_msvc", 239 | "windows_i686_gnu", 240 | "windows_i686_msvc", 241 | "windows_x86_64_gnu", 242 | "windows_x86_64_gnullvm", 243 | "windows_x86_64_msvc", 244 | ] 245 | 246 | [[package]] 247 | name = "windows_aarch64_gnullvm" 248 | version = "0.42.0" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" 251 | 252 | [[package]] 253 | name = "windows_aarch64_msvc" 254 | version = "0.42.0" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" 257 | 258 | [[package]] 259 | name = "windows_i686_gnu" 260 | version = "0.42.0" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" 263 | 264 | [[package]] 265 | name = "windows_i686_msvc" 266 | version = "0.42.0" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" 269 | 270 | [[package]] 271 | name = "windows_x86_64_gnu" 272 | version = "0.42.0" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" 275 | 276 | [[package]] 277 | name = "windows_x86_64_gnullvm" 278 | version = "0.42.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" 281 | 282 | [[package]] 283 | name = "windows_x86_64_msvc" 284 | version = "0.42.0" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" 287 | --------------------------------------------------------------------------------