Skip to content

Add -a DELAY_TIME, SIGTERM delayment feature #253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions MANPAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ To actually see the notifications in your GUI session, you need to have
[systembus-notify](https://github.com/rfjakob/systembus-notify)
running as your user.

#### -a DELAY_TIME
When SIGTERM conditions are met, send a notification to the user and
wait at least DELAY_TIME seconds before issuing SIGTERM, unless situation
improves. SIGTERM is cancelled and a separate notification is sent
when SIGTERM conditions have not been met, continuously, for at least
DELAY_TIME seconds. Countdown updates only when SIGTERM conditions
are satisfied. This provides a chance for a user to gracefully close
an important application and/or consciously select and close
the least needed application.

`-n` must be explicitly enabled.

#### -g
Kill all processes that have same process group id (PGID) as the process
with excessive memory usage.
Expand Down
23 changes: 1 addition & 22 deletions kill.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,6 @@ static int isnumeric(char* str)
}
}

static void notify(const char* summary, const char* body)
{
int pid = fork();
if (pid > 0) {
// parent
return;
}
char summary2[1024] = { 0 };
snprintf(summary2, sizeof(summary2), "string:%s", summary);
char body2[1024] = "string:";
if (body != NULL) {
snprintf(body2, sizeof(body2), "string:%s", body);
}
// Complete command line looks like this:
// dbus-send --system / net.nuetzlich.SystemNotifications.Notify 'string:summary text' 'string:and body text'
execl("/usr/bin/dbus-send", "dbus-send", "--system", "/", "net.nuetzlich.SystemNotifications.Notify",
summary2, body2, NULL);
warn("notify: exec failed: %s\n", strerror(errno));
exit(1);
}

/*
* Send the selected signal to "pid" and wait for the process to exit
* (max 10 seconds)
Expand Down Expand Up @@ -310,7 +289,7 @@ void kill_process(const poll_loop_args_t* args, int sig, const procinfo_t victim
snprintf(notif_args, sizeof(notif_args),
"Low memory! Killing process %d %s", victim.pid, victim.name);
if (args->notify) {
notify("earlyoom", notif_args);
notify("earlyoom", "%s", notif_args);
}
}

Expand Down
2 changes: 2 additions & 0 deletions kill.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ typedef struct {
double swap_kill_percent;
/* send d-bus notifications? */
bool notify;
/* minimum time to wait before sending SIGTERM */
int sigterm_delay_ms;
/* kill all processes within a process group */
bool kill_process_group;
/* prefer/avoid killing these processes. NULL = no-op. */
Expand Down
99 changes: 80 additions & 19 deletions main.c
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ int main(int argc, char* argv[])
meminfo_t m = parse_meminfo();

int c;
const char* short_opt = "m:s:M:S:kingN:dvr:ph";
const char* short_opt = "m:s:M:S:kina:gN:dvr:ph";
struct option long_opt[] = {
{ "prefer", required_argument, NULL, LONG_OPT_PREFER },
{ "avoid", required_argument, NULL, LONG_OPT_AVOID },
Expand All @@ -113,7 +113,7 @@ int main(int argc, char* argv[])
double mem_term_kib = 0, mem_kill_kib = 0, swap_term_kib = 0, swap_kill_kib = 0;

while ((c = getopt_long(argc, argv, short_opt, long_opt, NULL)) != -1) {
float report_interval_f = 0;
float interval_f = 0;
term_kill_tuple_t tuple;

switch (c) {
Expand Down Expand Up @@ -172,6 +172,13 @@ int main(int argc, char* argv[])
args.notify = true;
fprintf(stderr, "Notifying through D-Bus\n");
break;
case 'a':
interval_f = strtof(optarg, NULL);
if (interval_f < 0) {
fatal(14, "-a: invalid interval '%s'\n", optarg);
}
args.sigterm_delay_ms = (int)(interval_f * 1000);
break;
case 'g':
args.kill_process_group = true;
break;
Expand All @@ -186,11 +193,11 @@ int main(int argc, char* argv[])
// The version has already been printed above
exit(0);
case 'r':
report_interval_f = strtof(optarg, NULL);
if (report_interval_f < 0) {
interval_f = strtof(optarg, NULL);
if (interval_f < 0) {
fatal(14, "-r: invalid interval '%s'\n", optarg);
}
args.report_interval_ms = (int)(report_interval_f * 1000);
args.report_interval_ms = (int)(interval_f * 1000);
break;
case 'p':
set_my_priority = 1;
Expand Down Expand Up @@ -220,6 +227,9 @@ int main(int argc, char* argv[])
" -M SIZE[,KILL_SIZE] set available memory minimum to SIZE KiB\n"
" -S SIZE[,KILL_SIZE] set free swap minimum to SIZE KiB\n"
" -n enable d-bus notifications\n"
" -a DELAY_TIME send notification and wait at least DELAY_TIME seconds\n"
" before issuing SIGTERM unless situation improves;\n"
" -n has to be explicitly enabled\n"
" -g kill all processes within a process group\n"
" -d enable debugging messages\n"
" -v print version information and exit\n"
Expand All @@ -239,6 +249,9 @@ int main(int argc, char* argv[])
}
} /* while getopt */

if (args.sigterm_delay_ms && !args.notify) {
fatal(1, "-a requires -n to be explicitly enabled\n");
}
if (optind < argc) {
fatal(13, "extra argument not understood: '%s'\n", argv[optind]);
}
Expand Down Expand Up @@ -409,6 +422,11 @@ static void poll_loop(const poll_loop_args_t* args)
// Print a a memory report when this reaches zero. We start at zero so
// we print the first report immediately.
int report_countdown_ms = 0;
// SIGTERM countdown is happening when
// sigterm_countdown_ms < args->sigterm_delay_ms
int sigterm_countdown_ms = args->sigterm_delay_ms,
no_sigterm_streak_ms = 0, last_sig = 0;
procinfo_t victim;

while (1) {
meminfo_t m = parse_meminfo();
Expand All @@ -417,25 +435,47 @@ static void poll_loop(const poll_loop_args_t* args)
print_mem_stats(warn, m);
warn("low memory! at or below SIGKILL limits: mem " PRIPCT ", swap " PRIPCT "\n",
args->mem_kill_percent, args->swap_kill_percent);
} else if (sig == SIGTERM) {
} else if (sig == SIGTERM && sigterm_countdown_ms == args->sigterm_delay_ms) {
print_mem_stats(warn, m);
warn("low memory! at or below SIGTERM limits: mem " PRIPCT ", swap " PRIPCT "\n",
args->mem_term_percent, args->swap_term_percent);

if(args->sigterm_delay_ms) {
// kickstart the countdown
victim = find_largest_process(args);
notify("earlyoom",
"Low memory! Will send SIGTERM, likely to %s (%d), "
"in %g or more seconds unless situation improves",
victim.name, victim.pid,
(double)args->sigterm_delay_ms / 1000);
sigterm_countdown_ms -= 1;
}
}
if (sig) {
procinfo_t victim = find_largest_process(args);
/* The run time of find_largest_process is proportional to the number
* of processes, and takes 2.5ms on my box with a running Gnome desktop (try "make bench").
* This is long enough that the situation may have changed in the meantime,
* so we double-check if we still need to kill anything.
* The run time of parse_meminfo is only 6us on my box and independent of the number
* of processes (try "make bench").
*/
m = parse_meminfo();
if (lowmem_sig(args, &m) == 0) {
warn("memory situation has recovered while selecting victim\n");

if (sig && (sig != SIGTERM || sigterm_countdown_ms <= 0)) {
victim = find_largest_process(args);

if (!args->sigterm_delay_ms) {
/* The run time of find_largest_process is proportional to the number
* of processes, and takes 2.5ms on my box with a running Gnome desktop (try "make bench").
* This is long enough that the situation may have changed in the meantime,
* so we double-check if we still need to kill anything.
* The run time of parse_meminfo is only 6us on my box and independent of the number
* of processes (try "make bench").
*/
m = parse_meminfo();
if (lowmem_sig(args, &m) == 0) {
warn("memory situation has recovered while selecting victim\n");
} else {
kill_process(args, sig, victim);
}
} else {
kill_process(args, sig, victim);
// given that SIGTERM window can be passed rather quickly,
// make one small attempt to terminate process gracefully;
// kill_wait() will quickly escalate to SIGKILL anyway
kill_process(args, SIGTERM, victim);
Copy link
Author

@qirlib qirlib Jul 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I glitched out and have had misread kill_wait(): this does not really make much sense. This line should either be just kill_process(args, sig, victim); or I'll possibly add a single adaptive sleep to not dip too deep into SIGKILL territory. I'm a bit busy now, so it might take a while on my end.

sigterm_countdown_ms = args->sigterm_delay_ms;
no_sigterm_streak_ms = 0;
}
} else if (args->report_interval_ms && report_countdown_ms <= 0) {
print_mem_stats(printf, m);
Expand All @@ -446,5 +486,26 @@ static void poll_loop(const poll_loop_args_t* args)
struct timespec req = { .tv_sec = (time_t)(sleep_ms / 1000), .tv_nsec = (sleep_ms % 1000) * 1000000 };
nanosleep(&req, NULL);
report_countdown_ms -= (int)sleep_ms;
// if counting down
if (sigterm_countdown_ms < args->sigterm_delay_ms) {
if (sig == SIGTERM) {
sigterm_countdown_ms -= (int)sleep_ms;
}
if (sig == 0 && last_sig == 0) {
no_sigterm_streak_ms += (int)sleep_ms;

if (no_sigterm_streak_ms >= args->sigterm_delay_ms) {
notify("earlyoom",
"The memory situation has improved, SIGTERM "
"cancelled. (mem " PRIPCT ", swap " PRIPCT ")",
m.MemAvailablePercent, m.SwapFreePercent);
sigterm_countdown_ms = args->sigterm_delay_ms;
no_sigterm_streak_ms = 0;
}
} else {
no_sigterm_streak_ms = 0;
}
last_sig = sig;
}
}
}
26 changes: 26 additions & 0 deletions msg.c
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,32 @@ int debug(const char* fmt, ...)
return 0;
}

void notify(const char* summary_text, const char* fmt, ...)
{
int pid = fork();
if (pid > 0) {
// parent
return;
}

const size_t stringc_len = strlen("string:");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would alarm SCA because of the \0.


char summary[1024] = { 0 };
snprintf(summary, sizeof(summary), "string:%s", summary_text);
char body[1024] = "string:";
va_list vl;
va_start(vl, fmt);
vsnprintf(body + stringc_len, sizeof(body) - stringc_len, fmt, vl);
va_end(vl);

// Complete command line looks like this:
// dbus-send --system / net.nuetzlich.SystemNotifications.Notify 'string:summary text' 'string:and body text'
execl("/usr/bin/dbus-send", "dbus-send", "--system", "/", "net.nuetzlich.SystemNotifications.Notify",
summary, body, NULL);
warn("notify: exec failed: %s\n", strerror(errno));
exit(1);
}

// Parse a floating point value, check conversion errors and allowed range.
// Guaranteed value range: 0 <= val <= upper_limit.
// An error is indicated by storing an error message in tuple->err and returning 0.
Expand Down
2 changes: 2 additions & 0 deletions msg.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ int fatal(int code, char* fmt, ...) __attribute__((noreturn, format(printf, 2, 3
int warn(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
int debug(const char* fmt, ...) __attribute__((format(printf, 1, 2)));

void notify(const char* summary, const char* fmt, ...) __attribute__((format(printf, 2, 3)));

typedef struct {
// If the conversion failed, err contains the error message.
char err[255];
Expand Down