I was recently writing a bash script where I wanted
to nice a particular
function. The function in question was doing multiple expensive operations (e.g.
gzip
), and I didn't want to have to put a nice
invocation in front of every
one of these operations.
It turns out that this is kind of tricky. The nice
command isn't a builtin,
and therefore requires a real command to run with the new niceness value. For
instance, the following bash will exit with an error due to foo
not being an
actual executable:
foo() {
gzip -d bigfile.gz
grep "hello world" /dev/urandom
}
# try to lower the priority of foo
nice foo
In this example the printed error message sill be something like nice: ‘foo’: No such file or directory
.
One idea I had for overcoming this obstacle is to renice the process twice. For instance, I was initially considering the following, where I lower the nice value at function entry and then raise it again upon function exit:
foo() {
renice 10 $$ # lower the niceness
gzip -d bigfile.gz
grep "hello world" /dev/urandom
renice -10 $$ # raise the niceness again
}
Unfortunately this doesn't work because only the root user can decrease
niceness. So if the script is being run as an unprivileged user the first
renice
command will work, but the second will fail.
The solution I came up with is to use renice
in conjunction with
bash subshells. The original
version I had looked like this:
foo() {
renice 10 $BASHPID # note $BASHPID, *not* $$
gzip -d bigfile.gz
grep "hello world" /dev/urandom
}
# invoke foo in a subshell
(foo)
This works because the subshell will execute as a separate process, which means
that there's no need to undo the renice
command. Note here that $BASHPID
must be used here, instead of $$
which is much more commonly used to locate
the current process id. The reason is that bash subshells actually return the
original shell's PID when using $$
, whereas $BASHPID
always returns the
actual PID for the process.
The downside to this approach---which I think is significant---is that if you
forget to invoke the function in a subshell then the entire script will end up
niced. For instance, given the previous definition for foo
we'll have the
following correct and incorrect invocations:
(foo) # CORRECT: uses a subshell
foo # INCORRECT: now the entire script is niced
This is pretty subtle. To understand what's happening here you need to know what a subshell is, what the syntax is for subshell invocation, and you need to understand why a subshell is needed in the first place.
My friend James pointed out that this can be fixed by invoking the subshell within the function. That looks like this:
foo() {
(renice 10 $BASHPID
gzip -d bigfile.gz
grep "hello world" /dev/urandom)
}
Now foo
can be invoked as normal. Nifty!