In last week's post, I discussed using
cpulimit on multiple processes in the special case of
web browsers, but I wanted a more general solution.
cpulimit-all.sh is a wrapper around
which will call
cpulimit many times to cover multiple processes of
the same name and subprocesses.
Using that script, the follow is the equivalent of the script from last week to limit all browser processes to 10% CPU:
cpulimit-all.sh --limit=10 --max-depth=1 \ -e firefox -e firefox-esr -e chromium -e chrome
But also, we can add a couple options to include any grandchild processes and check for new processes to limit every minute:
cpulimit-all.sh --limit=10 --max-depth=2 \ -e firefox -e firefox-esr -e chromium -e chrome \ --watch-interval=1m
The script is mostly argument parsing and bookkeeping. It collects a set
of targets which may be any combination of PIDs, program names, program
paths, and a program to execute, with syntax matching
except for allowing multiple to be specified. Additionally, it accepts
cpulimit's arguments to be passed on to the instances of
cpulimit that it runs.
In order to keep track of what processes it is limiting, it keeps
$watched_pids which is a list where each line is of the format
pid is the PID of the process being
depth is the depth in the process tree from one of the
targets listed on the command line (
0 for the explicit targets), and
cpulimit_pid is the PID of the
cpulimit process limiting that
limit_pid() function is where
cpulimit is actually
run and that list is updated.
To start, the PIDs specified on the command line are limited. Then
the rest of the time is spent in a loop that checks for any new
processes of the executable names or paths given on the command
line or any subprocesses of the currently limited processes. If the
--watch-interval option is not given on the command line, then once
this loop stops finding new processes to limit, it will just wait.
Otherwise, that option gives the interval at which new processes are
searched for. In practice, that interval should be fairly long because
with enough processes being limited, searching for all of their
subprocesses starts taking a non-trivial amount of processor time,
possibly cancelling any gains from doing the limiting.
Watching for processes
Optimally, the script would subscribe to notifications about new
processes and determine if they should be limited instead of having to
constantly poll for processes. Unfortunately, no such API exists.
polls for processes with a 2 second wait when there is
no process running matching the target specified, which suggests there
really isn't a better way to do it.
Repeated arguments in
I originally followed these instructions to use GNU
getopt for option handling... but I could not figure out a way to
make it accept multiple copies of the same option (e.g.
-e firefox -e
chrome), which I want to allow to specify multiple programs to limit.
My solution was to handle my own argument parsing and therefore lose out
on some of
getopt's features. Most noticeably, I did not implement
handling multiple short arguments after a single dash (e.g.,
Checking if an argument is a number
Part of argument parsing is verifying that the argument values are
valid. To check if the numeric argument are actually numbers, I used
this snippet, which does not depend on
Accidental subshell in loop body
The basic structure of the code is that there's a loop over the
processes to limit and inside the loop body is a call to a function
cpulimit and records the processes as being watched
by writing its PID into a variable along with some other bookkeeping
information. But outside the loop, that variable was always empty.
At first I thought it had to do with variable scoping, but the shell
semantics on scoping are that a variable is global unless explicitly
defined as local using the
local keyword. Trying to figure
out what was going on, I made another variable and set it in various
places which caused ShellCheck (which I've written about
before) to show me the following warning:
var was modified in a subshell. That change might be lost. [SC2031]
which led me to this StackOverflow answer which linked to this page of various solutions. As I generally try to avoid bashisms, I went with the heredoc solution: instead of
echo "$foo" | while read -r line; do ... done
while read -r line; do ... done <<EOF $foo EOF