DEV Community

David Cantrell
David Cantrell

Posted on

cpulimit annoyed me so I improved it

Previously | more previously

A few days ago I discovered cpulimit. It's a great tool that nicely (haha) complements nice. Where nice is normally used to reduce the amount of CPU a process uses by changing it priority, a niced process can still end up using more CPU than you want, and will of course use all that it wants if nothing with a higher priority comes along.

But sometimes you want to restrict a process to using no more than some particular fraction of CPU time regardless of priority. A good example is when you don't want those noisy PC fans to kick in and you don't care how long a job takes because whether it finishes in 15 minutes or 8 hours it's still going to finish while you're asleep.

cpulimit is perfect for this, and is simple to use. An invocation like this:

cpulimit -l 50 somecommand ...
Enter fullscreen mode Exit fullscreen mode

will run somecommand but restrict it to only use 50% of a CPU. It does this by forking a watchdog process that periodically checks to see how much work somecommand is doing, and if it's used too much briefly pauses it by sending the STOP signal. After a little while it will un-pause it with the CONT signal. Of course, because the load on your machine from other processes will never quite be constant, cpulimit rarely hits the target exactly, but it gets close enough.

But I wanted to tweak it a bit. I wanted to be able to interactively "turn the volume knob" so that I could give somecommand more or less CPU whenever I fancied. The result is a pull request which unfortunately is unlikely to ever get merged, as the original author hasn't touched the project in years, but if any of you want the nifty new feature applying the patch and building your own custom cpulimit is pretty easy.

How the patch works is simple. It installs signal handlers for SIGUSR1 and SIGUSR2 which respectively increase and decrease the CPU allocation by 1%. Want to turn it up by 50%? Just write a little shell loop to send the signal 50 times:

for i in $(seq 1 50); do kill -SIGUSR1 $pid; done
Enter fullscreen mode Exit fullscreen mode

Determining which process to send the signal to is a bit tricky, as there are two cpulimit processes running. There's the first one, which is just waiting in the background for somecommand to finish, then there's the watchdog that got forked off. It's the watchdog you want to send the signals to. You can tell which is the watchdog as it will generally have a higher PID and be using a little bit of CPU. If you are cpulimiting multiple processes then you can tell which watchdog is related to which process because the watchdog will have the command and its arguments on its command line. For example:

$ ps aux|grep ffmpeg|grep -v grep
david   90311 103.3  0.7 36105472 485448 s011  T     6:42pm   1:07.89 ffmpeg ...
david   90312   5.6  0.0 34221044    828 s011  S     6:42pm   0:02.82 cpulimit -l 100 ffmpeg ...
david   90310   0.0  0.0 34122740    796 s011  S     6:42pm   0:00.01 cpulimit -l 100 ffmpeg ...
Enter fullscreen mode Exit fullscreen mode

You can see here that I asked cpulimit to allow ffmpeg to only use 1 CPU of the several available on this machine (ie to use 100% of a CPU - on modern machines the maximum allowed is the number of CPU cores * 100%). My shell accordingly started process 90310 which forked and execed ffmpeg with pid 90311 and forked the watchdog process as pid 90312. The watchdog is using a little bit of CPU. It is therefore to process 90312 that I should send signals.

$ for i in $(seq 1 400); do kill -SIGUSR1 90312; done
Enter fullscreen mode Exit fullscreen mode

That will send the "turn it up a bit" signal 400 times, so ffmpeg is now limited to at most 500% of a CPU, and a few moments later:

$ ps aux|grep ffmpeg|grep -v grep
david   90311 497.0  0.8 36105472 507160 s011  T     6:42pm  12:42.37 ffmpeg ...
Enter fullscreen mode Exit fullscreen mode

we can see that ffmpeg is running a lot harder, now taking just under 500% of the CPU.

Top comments (2)

drhyde profile image
David Cantrell

Update: it annoyed me again. Exiting with code 0, which normally means "yay, I succeeded", when the process died because of a signal is a pretty serious bug. So with that second patch it will exit with code 124 instead.

Thankfully it already Did The Right Thing if the process it spawned exited of its own volition with a non-zero code.

phlash profile image
Phil Ashby

Nice addition, I might pop a few comments on your PR ;-)

There is of course an alternative approach, which is to not start the managed process through cpulimit but to connect it using the -p <pid> option. This would then allow disconnecting and reconnecting with a different priority... I've used cpulimit mostly in this fashion to turn down the volume on a process that's already started, I don't want to restart and is using too much CPU.. YYMV 😁