Sleeping without a subprocess in Bash, and how to sleep forever

No subprocess, no sleep command, no coproc, no nothing? Yes.

Sleeping in bash script is traditionally done with the sleep (1) command, which is external to bash, residing in /bin/sleep. However, if you have a bunch of scripts running that all do the output of ps looks like a mess, pstree looks like a bigger mess, and every OCD sensor in my brain goes off.

Sample output of pstree:

$ sudo pstree -ps 2828
systemd(1)───urxvt(2826)───bash(2828)───bash(14252)───sleep(14255)

Here, my terminal (urxvt) runs a shell (bash, 2828), that runs a test script (bash, 14252), that runs sleep (14255).

Several bad ideas

This post on Stack Exchange contains several horrible proposed solutions, but do point out that several distributions of Linux ship a package with loadable bash modules. Among them is an internal sleep command. I didn’t want to rely on that, however.

Stack Overflow has a post on how to sleep forever. Again there are several horrendous ideas, but the answer by Tino is rather clever:

bash -c 'coproc { exec >&-; read; }; eval exec "${COPROC[0]}<&-"; wait'

coproc is a relatively new feature, however, and it uses eval, which, as wooledge.org points out, is a common misspelling of “evil”. We can do better.

Finally asleep

Waiting to read a file descriptor that will never output anything is a clever solution, but we can achieve that without using coproc. instead opting for good old fashioned process substitution.

So I wrote the following function:

snore()
{
    [[ -n "${_snore_fd:-}" ]] || exec {_snore_fd}<> >(:)
    read ${1:+-t "$1"} -u $_snore_fd || :
}

So what does that do? Well, this:

 

[[ -n “${snore_fd:-}” ]] Checks if the $_snore_fd variable has already been declared. If so, we are good to go.
exec {_snore_fd}<> Assigns the next available file descriptor to the “_snore_fd” variable. “_snore_fd” will be a number signifying the assigned file descriptor after this.
>(:) Process substituion into a subshell that simply runs “:”, or “true” if you will, and then exits
read Attempts to read input, though it won’t get any
${1:+-t “$1”} Parameter expansion: The snore() function was provided a parameter, it will pass -t (timeout) and the first parameter to read.
If no parameters were provided, -t will not be specified, and read will hang forever.
-u $_snore_fd Specifies that read should use the value of $_snore_fd as its input file descriptor
|| : Making sure read returns 0, for coding with -e set. This will run : if read fails, and : always returns 0.

Let’s test it!

Here’s a short script to compare the efficiency of snore() to that of /bin/sleep. It runs each operation 1000 times, for a total of what should be 10 seconds for each.

#!/usr/bin/env bash
set -u
set -e

snore()
{
	[[ -n "${_snore_fd:-}" ]] || exec {_snore_fd}<> >(:)
	read ${1:+-t "$1"} -u $_snore_fd || :
}

time for ((i=0; i<1000; i++)); do snore 0.01; done
time for ((i=0; i<1000; i++)); do sleep 0.01; done

The snore() function runs faster than /bin/sleep, at least on my system. That’s not to say it sleeps too quickly – one second is still one second – but if called in quick succession, one can see that the snoring loop is faster than the sleeping one:

$ /tmp/test </dev/null

real	0m10.545s
user	0m0.292s
sys	0m0.296s

real	0m11.674s
user	0m0.088s
sys	0m0.220s

As you can see, calling snore() 1000 times has a combined overhead of 0.545 seconds, while /bin/sleep measured 1.1674 seconds. This is of course utterly insignificant in real world applications, but it’s interesting none the less. Do note the increase in user CPU time, however.

No more sleep processes

Aside from the completely insignificant performance differences, my OCD was satisfied, as a script running snore() has no child process to wait for, and the subshell we spawn disappears immediately. Here’s pstree while I run a script that snores:

$ sudo pstree -ps 2828
systemd(1)───urxvt(2826)───bash(2828)───bash(19247)

So my terminal runs a shell, and that shell runs the script, but there’s no sleep call, and no other subprocess. Just the interactive shell waiting for the script. Excellent.

As an added bonus, there will no longer be any of the usual issues of various sleep processes hanging around after killing processes, or preventing them from being killed in the first place.

Halt and Catch Fire

Going back to the question on stack overflow, you may have noticed the parameter processing of snore() allowing for no parameters to be passed. This means that if you don’t pass any parameters to snore(), -t (timeout) will not be specified for the call to read, and read will hang forever. I don’t know why you’d want this, but now you can.

Leave a Reply

Your email address will not be published. Required fields are marked *