Skip to content

Instantly share code, notes, and snippets.

@TheProjectsGuy
Last active April 22, 2023 07:41
Show Gist options
  • Save TheProjectsGuy/83dcb9ea06cf014822dfe5911dc1143b to your computer and use it in GitHub Desktop.
Save TheProjectsGuy/83dcb9ea06cf014822dfe5911dc1143b to your computer and use it in GitHub Desktop.
Monitor the RAM utilization of a process and kill it if it is above a threshold. Can be put in a cron job. The kills are logged. If manually invoked (on Linux Mint), a system notification is also sent.
#!/bin/bash
readonly VERSION_MAJOR=1
readonly VERSION_MINOR=2
VERSION="${VERSION_MAJOR}.${VERSION_MINOR}"
# Program properties
readonly ARGS="$@" # Reset using https://stackoverflow.com/a/4827707
readonly PROGNAME=$(basename $0)
readonly PROGPATH=$(realpath $(dirname $0))
readonly DEF_THRESH=2048 # Default threshold in MB
readonly DEF_LOGFILE=$HOME/proc_kill.logs
function usage () {
cat <<-EOF
Monitors the RAM utilization of a process and kills it if the RAM utilization is above
a certain threshold.
VERSION: $VERSION
USAGE: $PROGNAME -ARG VAL [-OPTARG [OPTVAL]]
WARNINGS:
1. You could unknowingly kill many processes
- Runs a grep search for matching processes. The process will be killed (if run by
the user) if it takes more pss RAM than specified threshold.
2. You could lose unsaved work
3. Tremendous potential for MISUSE. Please use wisely.
Arguments:
-p | --pattern PAT Search for processes containing "PAT" in their name
Optional arguments:
-d | --dry-run Don't kill, just print processes (if killed). Also enables
verbose mode (print utilization and stuff)
-h | --help Print the help menu (this output)
-l | --kill-log LOG_F A log file for saving the kill logs (time stamped). If ''
(blank), then no logs are saved (I sugguest to NOT do this).
(Default is: $DEF_LOGFILE)
-m | --multi-sum If multiple PIDs are found, use the sum total. By default,
it uses the max utilization (for comparison).
-n | --nofity If passed, a system GUI notification (popup) is sent.
Currently only the following distributions are supported
- Linux Mint
-s | --strict Throw error even when there is a minor issue (>=127 code)
-t | --thresh TH_MB The process should have a maximum of "TH_MB" (in MB) memory
on RAM. If it exceeds that, it could get killed. See the
kill policy.
(Default is: $DEF_THRESH MB)
Kill policy:
1. The program finds PIDs containing the pattern 'PAT'
2. If there are no PIDs, the process exits (code 0 or 127, depending on '-s' option)
3. If there is only one PID found, the RAM utilization is noted. If it uses more
than TH_MB of RAM, it is killed
4. If there are multiple PIDs found, the RAM utilization of each of the processes is
found (as well as their total). Depending on the '-m' setting
- If '-m' is passed: If the total RAM usage exceeds TH_MB, all PIDs are killed
- If '-m' is not passed: The PID using the max RAM is killed, if it is using more
RAM than TH_MB
EXAMPLES:
- Kill firefox if it uses more than 5120 MB RAM (combined)
$PROGNAME -m -p firefox -t 5120
- Kill a firefox process if it uses more than 1024 MB RAM (individual)
$PROGNAME -p firefox -t 1024
- Kill all barrier processes if they use more than 500 MB RAM (combined)
$PROGNAME -m -p barrier -t 500
CRON SUGGESTIONS:
You can enable CRON jobs to periodically check for memory utilizations
- Eg: Limit barrier to only 200 MB RAM (check every 2 minutes)
*/2 * * * * $PROGPATH/$PROGNAME -m -p barrier -t 200
- Eg: Limit firefox to only 6144 MB (6 GB) RAM (check every 15 minutes)
*/15 * * * * $PROGPATH/$PROGNAME -m -p firefox -t 6144
Exit codes:
0 Normal exit
1 Argument error (needed argument missing)
2 Argument error (not recognized, NaN, etc.)
127 Minor: No matching processes found
References:
- Types of memory representations: https://stackoverflow.com/q/22372960/5836037
- Periodic CRON jobs: https://www.ibm.com/support/pages/how-run-cron-job-every-5-minutes
EOF
}
# Environment variables
PROC=""
THRESH_USAGE="$DEF_THRESH"
STRICT_EXEC="false" # "true" | "false"
LOGFILE=$DEF_LOGFILE
MULTI_KILLSTYLE="single" # "single" | "all"
DRY_RUN="false" # "true" | "false"
NOTIFY_GUI="false" # "true" | "false"
# ================ Functions ================
function parse_options () {
# Set the passed bash options
set -- $ARGS
while (( $# )); do
arg=$1 # Read option
shift # Shift to the next argument
case "$arg" in
# Help options
"--help" | "-h")
usage
exit 0
;;
# Process name (pattern)
"--pattern" | "-p")
pattern=$1
shift
PROC=$pattern
;;
# Threshold usage
"--thresh" | "-t")
thresh=$1
# From: https://stackoverflow.com/a/806923/5836037
re='^[+-]?[0-9]+([.][0-9]+)?$'
if ! [[ $thresh =~ $re ]]; then
echo "Not a number: $thresh"
exit 2
fi
shift
THRESH_USAGE=$thresh
;;
# Strict usage
"--strict" | "-s")
STRICT_EXEC="true"
;;
# Log file
"--kill-log" | "-l")
log_file=$1
shift
LOGFILE=$log_file
;;
# Kill strategy for multiple processes
"--multi-sum" | "-m")
MULTI_KILLSTYLE="all"
;;
# Dry run
"--dry-run" | "-d")
DRY_RUN="true"
;;
# Notify GUI
"--notify" | "-n")
NOTIFY_GUI="true"
;;
*)
echo "Unrecognized option: $1"
exit 2
;;
esac
done
}
function proc_util() {
# Given a PID as argument, return (echo) the process usage on RAM (in MB)
# From: https://www.golinuxcloud.com/check-memory-usage-per-process-linux/
pid=$1
proc_usage=$(cat /proc/$pid/smaps | grep -i pss | awk '{Total+=$2} END {print Total/1024}')
echo $proc_usage
}
function notify_kill() {
# Gives a GUI notification (if enabled). Pass the process IDs as argument
# Also uses the process name PROC
if [[ "$NOTIFY_GUI" = "false" ]]; then
return 0
fi
distro_id=$(cat /etc/*-release 2> /dev/null | awk '/^DISTRIB_ID/ {print $1}' | sed "s/DISTRIB_ID\=//")
if [[ $distro_id == "LinuxMint" ]]; then
notify-send -u critical "Process $PROC killed" "Was consuming more than $THRESH_USAGE MB RAM"
fi
}
function kill_pid() {
# Function handles killing (even dry run) and notification
kpid=$1
if [[ $DRY_RUN = "true" ]]; then
echo "Would kill PID: $kpid (but in dry run)"
else
kill -9 $kpid
fi
notify_kill
}
# ===================== Main entrypoint =====================
parse_options
# Check if mandatory arguments are set
if [[ -z $PROC ]]; then
echo "Process pattern not passed (-p)"
exit 1
fi
if [[ -z $THRESH_USAGE ]]; then
echo "RAM threshold not specified"
exit 1
fi
# Get process ID(s)
self_pids=$(ps aux | egrep "$0" | awk 'BEGIN {ORS=" "} {print $2}')
pid_uf=$(pgrep -i ".*$PROC.*" -d ' ' --full)
for pid_self in $self_pids; do # Do not kill self
pid_uf=$(echo $pid_uf | sed "s/$pid_self//" | xargs)
done
pid=$pid_uf
# Verbose
if [[ "$DRY_RUN" = "true" ]]; then
echo "Date: `date`"
echo "PID(s) after filtering self: $pid"
fi
if [[ $(wc -w <<< $pid) == 0 ]]; then # If no processes found
# Verbose
if [[ "$DRY_RUN" = "true" ]]; then
echo "No process found to kill"
fi
if [[ "$STRICT_EXEC" = "true" ]]; then
exit 127
else
exit 0
fi
elif [[ $(wc -w <<< $pid) == 1 ]]; then # If a single matching process is found
ps_util=$(proc_util $pid)
# Verbose
if [[ "$DRY_RUN" = "true" ]]; then
echo "Process $PROC (pid: $pid) uses $ps_util MB RAM, threshold is $THRESH_USAGE MB"
fi
if (( $(echo "$ps_util > $THRESH_USAGE" | bc -l) )); then
if [[ ( ! -z $LOGFILE ) && ( "$DRY_RUN" = "false" ) ]]; then
date >> $LOGFILE
echo "Process '$PROC' (PID: $pid) using $ps_util (> $THRESH_USAGE) MB RAM! I'm closing it" >> $LOGFILE
echo "-----------------------------------------------------------------------------" >> $LOGFILE
elif [[ "$DRY_RUN" = "true" ]]; then
echo "No logs since dry run"
fi
kill_pid "$pid"
fi
else # If multiple matching processes are found
pidArray=($pid)
# Verbose
if [[ "$DRY_RUN" = "true" ]]; then
echo "Process $PROC has the following processes (PID: Usage in MB)"
fi
# Maximum utilization process
max_pid="0"
max_mem="0"
total_util="0" # Total RAM utilization
for i_pid in ${pidArray[*]}; do
usage_ipid=$(proc_util $i_pid)
total_util=$(echo "$total_util + $usage_ipid" | bc -l)
# Verbose
if [[ "$DRY_RUN" = "true" ]]; then
echo -e " - $i_pid: $usage_ipid MB"
fi
if (( $(echo "$usage_ipid > $max_mem" | bc -l) )); then
max_mem=$usage_ipid
max_pid=$i_pid
fi
done
# Verbose
if [[ "$DRY_RUN" = "true" ]]; then
echo "Total: $total_util MB"
echo "Multi-kill style: $MULTI_KILLSTYLE, threshold: $THRESH_USAGE MB"
fi
# Kill strategy for multiple PIDs
if [[ "$MULTI_KILLSTYLE" = "all" ]]; then
if (( $(echo "$total_util > $THRESH_USAGE" | bc -l) )); then # Kill all pid
if [[ ( ! -z $LOGFILE ) && ( "$DRY_RUN" = "false" ) ]]; then
date >> $LOGFILE
echo "Processes matching '$PROC': $pid" >> $LOGFILE
echo "Total RAM usage is $total_util (> $THRESH_USAGE) MB! I'm killing all of them" >> $LOGFILE
echo "-----------------------------------------------------------------------------" >> $LOGFILE
elif [[ "$DRY_RUN" = "true" ]]; then
echo "No logs since dry run"
fi
kill_pid "$pid"
fi
elif [[ "$MULTI_KILLSTYLE" = "single" ]]; then
if (( $(echo "$max_mem > $THRESH_USAGE" | bc -l) )); then # Kill max usage PID
if [[ ( ! -z $LOGFILE ) && ( "$DRY_RUN" = "false" ) ]]; then
date >> $LOGFILE
echo "Processes matching '$PROC': $pid" >> $LOGFILE
echo "Max utilization is by PID $max_pid, which is $max_mem (> $THRESH_USAGE) MB! I'm killing it" >> $LOGFILE
echo "-----------------------------------------------------------------------------" >> $LOGFILE
elif [[ "$DRY_RUN" = "true" ]]; then
echo "No logs since dry run"
fi
kill_pid "$max_pid"
fi
fi
fi
@TheProjectsGuy
Copy link
Author

Currently, the script doesn't check for child processes. I think a future version could check for sum over child processes as well.

@TheProjectsGuy
Copy link
Author

The new version is more professional

  • Arguments are taken in a more human way
  • More control on killing processes with multiple children

I have removed the GUI notification part. I'll probably add it in the future.

@TheProjectsGuy
Copy link
Author

It can now notify a kill on Linux Mint

@TheProjectsGuy
Copy link
Author

New version (version 1.2) gives more verbose output on a dry run (with process utilization). Also, fixed a bug due to which it wouldn't work in a CRON job. However, the user functionality -u is gone.

@TheProjectsGuy
Copy link
Author

Moved the main script to my DotFiles

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment