Skip to content

Commit 7d7dd9d

Browse files
committed
1 parent 9fe9ef9 commit 7d7dd9d

File tree

12 files changed

+872
-0
lines changed

12 files changed

+872
-0
lines changed

zig/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
zig-cache/
2+
zig-out/

zig/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.

zig/README.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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+

zig/build.zig

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
}

zig/src/config.zig

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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;

zig/src/daemonize.zig

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
}

zig/src/main.zig

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
}

0 commit comments

Comments
 (0)