Processes Can Fork

Forking is one of the most powerful concepts in Unix programming. The fork(2) system call allows a running process to create new process programmatically. This new process is an exact copy of the original process.

Up until now we’ve talked about creating processes by launching them from the terminal. We’ve also mentioned low level operating system processes that create other processes: fork(2) is how they do it.

When forking, the process that initiates the fork(2) is called the “parent”, and the newly created process is called the “child”.

The child process inherits a copy of all of the memory in use by the parent process, as well as any open file descriptors belonging to the parent process. Let’s take a moment to review child processes from the eye of our first three chapters.

Since the child process is an entirely new process, it gets its own unique pid.

The parent of the child process is, obviously, its parent process. So its ppid is set to the pid of the process that initiated the fork(2).

The child process inherits any open file descriptors from the parent at the time of the fork(2). It’s given the same map of file descriptor numbers that the parent process has. In this way the two processes can share open files, sockets, etc.

The child process inherits a copy of everything that the parent process has in main memory. In this way a process could load up a large codebase, say a Rails app, that occupies 500MB of main memory. Then this process can fork 2 new child processes. Each of these child processes would effectively have their own copy of that codebase loaded in memory.

The call to fork returns near-instantly so we now have 3 processes with each using 500MB of memory. Perfect for when you want to have multiple instances of your application loaded in memory at the same time. Because only one process needs to load the app and forking is fast, this method is faster than loading the app 3 times in separate instances.

The child processes would be free to modify their copy of the memory without affecting what the parent process has in memory. See the next chapter for a discussion of copy-on-write and how it affects memory when forking.

Let’s get started with forking in Ruby by looking at a mind-bending example:

if fork
  puts "entered the if block"
else
  puts "entered the else block"
end

outputs:

entered the if block
entered the else block

WTF! What's going on here? A call to the fork method has taken the once-familiar if construct and turned it on its head. Somehow this piece of code is entering both the if and else block of the if construct!

It’s no mystery what’s happening here. One call to the fork method actually returns twice. Remember that fork creates a new process. So it returns once in the calling process (parent) and once in the newly created process (child).

The last example becomes more obvious if we print the pids.

puts "parent process pid is #{Process.pid}"

if fork
  puts "entered the if block from #{Process.pid}"
else
  puts "entered the else block from #{Process.pid}"
end

outputs:

parent process is 21268
entered the if block from 21268
entered the else block from 21282

Now it becomes clear that the code in the if block is being executed by the parent process, while the code in the else block is being executed by the child process. Both the child process and the parent process will carry on executing the code after the if construct.

Again, there’s a rhythm to this beat, and it has to do with the return value of the fork method. In the child process fork returns nil. Since nil is falsy it executes the code in the else block.

In the parent process fork returns the pid of the newly created child process. Since an integer is truthy it executes the code in the if block.

This concept is illustrated nicely by simply printing the return value of a fork call.

puts fork

outputs

21423
nil

Here we have the two different return values. The first value returned is the pid of the newly created child process; this comes from the parent. The second return value is the nil from the child process.

Multicore Programming?

In a roundabout way, yes. By making new processes it means that your code is able, but not guaranteed, to be distributed across multiple CPU cores.

Given a system with 4 CPUs, if you fork 4 new processes then those can be handled each by a separate CPU, giving you multicore concurrency.

However, there’s no guarantee that stuff will be happening in parallel. On a busy system it’s possible that all 4 of your processes are handled by the same CPU.

fork(2) creates a new process that's a copy of the old process. So if a process is using 500MB of main memory, then it forks, now you have 1GB in main memory.

Do this another ten times and you can quickly exhaust main memory. This is often called a fork bomb. Before you turn up the concurrency make sure that you know the consequences.

Using a Block

In the example above we’ve demonstrated fork with an if/else construct. It’s also possible, and more common in Ruby code, to use fork with a block.

When you pass a block to the fork method that block will be executed in the new child process, while the parent process simply skips over it. The child process exits when it’s done executing the block. It does not continue along the same code path as the parent.

fork do
  # Code here is only executed in the child process
end

# Code here is only executed in the parent process.

In the Real World

Have a look at either of the appendices, or the attached Spyglass project, to see some real-world examples of using fork(2).

System Calls

Ruby’s Kernel#fork maps to fork(2).