Mr. Peabody Explains fork()
- Introduction
- How Windows Does It
- How Unix Does It
- So Why Do People Want the Unix Way?
- How does it work in Perl?
Introduction
UNIX – Fork Explained
The thing you must understand, see, is that Perl is an effort to take the Unix Way of doing things to other platforms. Many of the functions in Perl correspond directly to Unix functionality.
The fork() function is all about process creation. That is, how do I start a new program?
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.
Process Creation Under Microsoft Windows
The originating process (Bullwinkle, in this case) called Create Process which then constructs a new running program image out of whole cloth. Some attributes are “inherited” of course from the creating process (the user ID) but this is all handled by Windows, not really the Process::Create call.
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();
The program rocky.exe, once created, starts at the beginning and runs normally as though you had typed rocky.exe at the C:\> prompt. The rocky.exe process goes along its merry way not knowing (or caring) who its creator was.
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.
Process Creation Under Unix
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().
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.
The designers of Unix weren’t fools. They developed a scheme called copy on write which would prevent this slowness from happening.
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.
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: !$"; }
Immediately after forking, the child goes and overlays his own code with the code of another program (/usr/bin/rocky, in this case). The new program inherits many of the attributes of its parent.
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
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.
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.
Why Do Things the Unix Way?
- Open filehandles (this includes sockets)
- Environment
- Process’ user ID
- …and a few other items.
Having the environment and user ID of your parent is a nice security feature. This ensures that as processes are created that new processes have only as much “stuff” in them as their parents. Thus, parents can carefully control what kind of environment that their children receive.
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.
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”.
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.
This queuing and polling kind of scheme makes for very complicated and buggy code.
Following me so far Sherman?
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. |
That’s it. And if you don’t want the code for processing the connection in the same physical program as the server port-listener, simply have the child exec() the process you DO want to handle the connection–it’ll inherit the socket as well.
The Unix utility inetd works just like this, and runs almost every interesting server that runs on a Unix system.
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.
How Perl Does It
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 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.
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.
- perlfork
- perlfunc (fork, exec, wait, waitpid)
- perlipc (signal handling, reaping children)