The problem#
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.
The solution#
cpulimit-all.sh
is a wrapper around cpulimit
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 details#
Script summary#
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 cpulimit
except for allowing multiple to be specified. Additionally, it accepts
many of 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:depth:cpulimit_pid
where pid
is the PID of the process being
limited, 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
process. The 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.
In fact, cpulimit
internally
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 getopt
#
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., -vkr
).
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 bash
.
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
which starts 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
use
while read -r line; do ... done <<EOF
$foo
EOF
Comments
Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.
There are no comments yet.