In Progress
Unit 1, Lesson 21
In Progress

Subprocesses Part 6: Limits

Video transcript & code

As we continue our series on starting processes from Ruby programs, today we come to the topic of setting limits.

Sometimes when we start a process, we want to set some boundaries for it. Now, I want to make one thing very clear before we get started: the techniques I'm going to show you today will not help you isolate an untrusted process from the rest of the system. Secure sandboxing is a whole different ballgame.

However, sometimes we do want to put some limits around a process even when it's one from a trusted source. For instance:

  • We're writing a job queue, and we want to make sure that a poorly-written job doesn't accidentally hog all of the system resources.
  • We're currently working on some code that, by its nature, has a tendency to eat unlimited CPU or memory whenever we get it wrong. We want a way to safely test out our changes without our development machine slowing to a crawl every time we mess something up.

As a simplified example, here's some code that will happily monopolize the CPU if left unchecked.

require "prime"
Prime.each do |n|
  puts n
end

All it does is sit and generate prime numbers, forever.

We'll save our greedy CPU-devouring code into its own script.

If we run this code as it is,

it spikes the CPU and keeps it spiked as it churns out prime numbers.

But there's a way to keep this from happening.

We'll write some new code that uses Process.spawn to start this script.

And then we'll add an option, :rlimit_cpu.

This is part of a family of spawn options, all of which start with :rlimit. rlimit is short for "resource limit". This one takes a number of seconds as an argument.

We give it the value 1. This means that the process is allowed a total of one full second of CPU time.

We add a waitpid call to pause until the subprocess finishes.

pid = Process.spawn "./cpu_asplode.rb", rlimit_cpu: 1
Process.waitpid pid

Then we drop into a shell and execute this wrapper script.

This time, we can see that it cranks out primes for a moment, and then the system terminates it.

Another resource we might want to set limits on is memory. For the sake of example, here's another pathological little program. It just keeps endlessly adding more characters to a string in memory.

string = ""
loop do
  string << "x" * 1024
  puts "#{string.size / 1024}K"
end

I'm not even going to demonstrate running this program without any limits. It would just bog the machine down in virtual memory thrashing until the system got around to telling it no more memory can be found.

Instead, let's write another wrapper.

This time, we use the rlimit_as option.

"AS" stands for "addresseable space". This option sets a bound on the total virtual memory address space made available to the program.

The value of this limit is given in bytes. We give it a fairly large number, because even a basic Ruby script loads a lot of shared libraries into memory just to operate normally.

pid = Process.spawn "./rss_asplode.rb", rlimit_as: 2**28
Process.waitpid pid

Then we run our wrapper.

We see the misbehaving script crank away for a bit, and then BOOM, it fails with a NoMemoryError.

These are just two of the process limits that are available to us. There are many others. Check out the Ruby Process module documentation for a full list. There are limits for number of files open at a time, file size, the max CPU priority of a process, and many more. Pay careful attention to the compatibility information you find there; many limits are specific to certain operating system types and are unavailable elsewhere.

As I said at the beginning of this episode, these process limitations won't give you a way to put unsafe processes into lockdown. But they can provide a rudimentary "safety net" to keep poorly-behaved processes from bringing a system to its knees. Happy hacking!

Responses