Mr. Peabody Explains fork()



Sherman, my boy, step over here to the WaBac(tm) machine and let me show you exactly what fork is about.

The fork() function is all about process creation. That is, how do I start a new program?

But Mr Peabody: I already know how to do that in Perl! I can use system() or even backticks.

Patience, Sherman!

The fork() function goes back to how Unix creates any new process. For example the system() call fundamentally needs to create a new process, and running programs with backticks needs to create new processes as well. And under Unix (and Perl) that's done with fork().

Let's distinguish between processes and threads for just a moment. A process (in unix terminology) is a program with its own address space, it's own slot in the scheduler, and resources that are all its own (except the filesystem and shared resources, of course). Processes are generally designed to run independantly of whatever else is going on on the system.

A thread, however, shares some resources with another thread. The first thread creates the second thread ususally for the purpose of taking a task and parallelizing it. For example, a web browser might create indepenendant threads for fetching network data and for running the browser itself. Under most architectures, each process can contain many threads.

We're mostly concerned with Processes. And since you're familar with Windows, let's see how Windows does process creation.





In Perl, it looks like this:

# Bullwinkle.perl # use Win32::Process; sub ErrorReport{ print Win32::FormatMessage( Win32::GetLastError() ); } Win32::Process::Create($ProcessObj, "D:\\winnt35\\system32\\rocky.exe", "rocky nuts", 0, NORMAL_PRIORITY_CLASS, ".")|| die ErrorReport();

Of course, in the parent you now have a process object ($ProcessObj) that you can use to do manipulations on the process, but otherwise they're unrelated.

You see, Sherman, under Unix process creation looks entirely different. In the guts of Unix (the kernel)) there's a function called fork(). This function is accessable to the user through the normal C library routines.

What fork() does is extrordinary! It takes the existing process and clones it. If you're familar with Star Trek, this is like a bad transporter accident. An exact copy is made of each process, and each process is almost unaware of the other.

The code is executed instruction-by-instruction until the fork() call is reached. At that point, the process is cloned and now there are two identical processes running the instruction right after the fork().

In fact, they're so identical that only one bit of information distinguishes between them: the return value of fork(). In the child process fork() appears to have returned 0. In the parent process, fork() appears to have returned a non-zero integer. This is how parent and child tell themselves apart.



Are they really identical?

Quite! If the parent had files open before the fork(), those files remain open...and the child will also have those files open complete with file pointers in the same position as before the fork(). Any data structures and memory allocated by the parent before the fork() are copied in the child. If the parent had network socket connections open before the fork(), the child will have a copy of that socket connection. The environment variables, user ID, and other associated baggage also is copied.

After the fork() however, the processes are independant. If the parent allocates new memory the child doesn't know (or care). Newly opened filehandles are only accessable in the process that opened them.

All that copying of data structures sounds slow.

Always the pessimist, Sherman?

The designers of Unix weren't fools. They developed a scheme called copy on write which would prevent this slowness from happening.

Essentially copy on write means that as the kernel executes the fork() all of the pages of memory in the parent are tagged to say that, "if this page of memory is ever written to, it needs to be copied first". Afterwards, if the CPU ever goes to write to that page of memory it's first copied for the other process and the tag removed.

So only those pages that need to be copied will be. So in the beginning, the child is mostly a "skeleton" of page-pointers back to the original pages of memory. After running for a while, those pages are copied into the child's own private memory space.



Ok I think I understand now. But how do new kinds of programs get created? This would eventually give me a system full of clones--not particularly useful.

The fork() system call is only the first shoe. The other shoe is exec(). And together they make a nice pair. The exec() function, like fork(), is a C library routine which directly accesses a kernel routine of similar name.

What exec() does is overlay a process with new code. The original process' baggage (environment variables, open file handles, user ID, etc...) are kept, but the code and the data associated with the process is dropped in place of a new program.

So very often in Unix code you'll see something like this:

# Perl $pid=fork(); die "Cannot fork: $!" if (! defined $pid); if (! $pid) { # Only the child does this exec("/usr/bin/rocky"); die "Could not exec: !$"; }



For example, if you log into a Unix system over a network: inetd forks, and the child execs telnetd to handle the telnet connection. When that connection is established, telnetd execs login. When your login is completed, login execs your shell. Every time you run a program at your shell prompt the shell itself forks and the child execs the program whose name you typed.

If you examine the output of the ps program on any Unix system, you'll note that each process has a process ID number (the value returned by fork()) and a parent process ID number (the child can see this with the getppid() function). Thus, the parentage of a process can be traced all the way back to process ID #1--init.

UID PID PPID C STIME TTY TIME CMD root 1 0 0 Mar31 ? 00:00:04 init [5] root 374 1 0 Mar31 ? 00:00:00 inetd root 11254 374 0 23:38 ? 00:00:00 in.telnetd root 11255 11254 0 23:38 pts/3 00:00:00 login -- clintp clintp 11256 11255 0 23:38 pts/3 00:00:00 -bash clintp 11288 11256 0 23:39 pts/3 00:00:00 ps -ef

Ahh! Like all questions of origin, the answer is complicated. The init process is typically constructed by the Unix kernel when the system boots. Here fork() isn't used: the process is simply created ex nihilo. The init process after creation, goes about creating all of the other processes that are required to get the system up and running using fork() and exec().

If you look at a ps listing on a Unix system, you'll see a LOT of processes have init as their parent. Some of these were actually created by init, and some were not. In addition to being the creator, init also inherits any orphaned processes: processes whose parent process dies and they continue running.

Parents forking, children, killing, dying... that's pretty gruesome, Mr. Peabody.

Oh it gets even more gruesome than that. Child processes which die become zombie processes (also called "defunct processses"). They go into a state where their memory is freed and they're no longer running any code--they're just hanging around to have their death acknowledged by their parent.

The parent process has to occasionally reap the dead children by using the wait() function. Doing this allows dead children to completely disappear.

Details of this exercise are in the Unix or Perl documentation for the wait function.

The beauty and elegance of this process creation technique isn't in the processes that are created, its in the baggage. You'll recall that when a process is forked and execed, the "baggage" that comes along consists of:

Open filehandles (this includes sockets)

Environment

Process' user ID

...and a few other items.

So, for example, a program which listens on a network port as root (the superuser) can fork() and carefully take away its own privelages and then exec() an insecure user-program. The user-program will have no more privelages than the parent wants it to have.

I guess that's clever. I'm still not impressed.

Patience, m'boy. Patience.

The one application for fork() which drove its creation under Win32 was for networking. In a network server application, a process is bound to a network port and listens for connections. For example, a web server is usually bound to port 80 on a computer and listens there for incoming connections.

Only one process may be bound to a port at a time. Thus, only one program can be listening to port 80 at a time. Also, if the port is unbound--even for a little bit of time--an incoming request will fail with "connection refused".

When a connection comes in, the code designer has to make a choice--should the incoming (and outgoing) stream of data be processed, or should we wait for new connections?

Both horns of this dilemma are dangerous. On the one hand, processing the request from the user quickly is important and all due speed should be given to that. But on the other hand, while processing the user's data we must not ignore new incoming connections for too long--or they'll time out.

What programmers traditionally do is take a middle ground. They'll process the user-request, and occasionally go back and listen for a new connection. When new connections are received, they are queued up until the first user request is satisfied, and then processed in order.

Following me so far Sherman?

Yup!

Now the Unix way of doing this is simple. Since network sockets (like filehandles) are inherited by children Unix network servers typically work like this:

Step 1: The server binds to a network port and listens for a connection.

Step 2: Upon receipt of a connection, the server and the client establish the socket.

Step 3: The server then immediately performs a fork(). Remember, the child inherits open file descriptors.

Step 4 (for the parent): The parent closes his copy of the socket and goes back and listens for another connection. Step 4 (for the child): The child inherits the socket connection and talks to the client on the other end. When the child is done processing, it simply exits.

The Unix utility inetd works just like this, and runs almost every interesting server that runs on a Unix system.

Gee! That's great! Are there other things I can do with fork()?

Almost any task that lends itself to parallelism can be tackled with fork() and other tools (Interprocess Communications, for example).

Be careful with fork() though. Remember to check to see if fork() worked by examining the return value. If fork() returns undef, then the fork failed. The reason for failure will be stored in $! (errno in C). Too much forking can cause a system to bog down creating processes instead of doing real work. These kinds of programs that fork indiscriminantly are known as fork bombs.

Well it really is worth a fuss. You see Sherman: Microsoft has one way of doing process creation, Unix has another. Anything which can bridge this gap is worth getting excited about.

Getting fork() to work at all in a Microsoft Windows environment is a feat because Windows simply doesn't work that way. There's no concept of "cloning" an entire running process as a new process and having both clones continue running where they left off, much less having the clones inherit file handles and such.

So how'd they do it?

Simply like this: when you run a fork() function in a Perl program, the Perl interpreter creates a new thread of itself. It's the same interpreter, it's just running in threads now.

So within that interpreter there are two "copies" of your perl script running. The process IDs returned to fork() for the parent are actually just handles on threads within Perl. Perl takes care of the nasty details so that your program "feels" like it's running as a different process.

When you exec() a new program, the thread which performed the exec() terminates and a new process is created normally.

Does fork() work on all versions of Perl?

Not quite. Under Unix fork() works as it always has using Unix's native fork() routines.

Under Microsoft Windows you have to compile Perl with this pseudo-fork enabled. You do this by selecting the "MULTIPLICITY" and "ITHREADS" options when compiling Perl. The 5.6 distribution of Perl from ActiveState is compiled with fork() enabled.

Most other architectures do not have fork() under Perl.

Where can I learn more about fork()?

From the following manual pages, distributed with Perl 5.6: