Example Project

Forking a process

One of the primary responsibilities of an operating system is managing processes that run in the operating system. In the Linux operating system the Linux kernel oversees launching and managing processes. In Linux every process that runs is launched by some other process. The prime example of a program that launches other programs is a shell program. When you open a terminal in Linux and type commands you are interacting with a shell program. A common task that users of shell programs perform is launching a program. To launch a program from the shell we type a command line in the shell program: the shell program reads our command and manages the process of launching the programs we ask it to launch. Shell programs are not the only programs in Linux that can launch another process. In fact, any program in Linux can launch other programs by making an appropriate system to the Linux kernel.

In this example and the next we are going to be looking at two common techniques used to create processes in Linux. In this example we will see how a process in Linux can launch a child process by cloning itself by forking. In the next example we will see how a program in Linux can launch another, different program.

A forking server

In our web server example we saw a couple of ways that a server application can use threads to interact with multiple clients at the same time. An older technique that can used as an alternative to launching a thread to deal with a client is to fork the server application. In Linux when an application forks it creates a second, identical copy of itself. A server can handle multiple clients simultaneously by making a new copy of itself for every client that arrives, so that each client gets its own private copy of the server application to interact with.

Here is the relevant code in the miniweb server application needed to implement the forking strategy. This is the loop in main() that waits for new clients to connect and serves the clients as they arrive:

while(1) {
  struct sockaddr_in client;
  int new_socket , c = sizeof(struct sockaddr_in);
  new_socket = accept(server_socket, (struct sockaddr *) &client, (socklen_t*)&c);
  if(new_socket != -1) {
    int pid;
    if((pid = fork()) == 0) {
      serveRequest(new_socket);
      exit(0);
    } else {
      printf("Spawned process %d\n",pid);
      close(new_socket);
      wait_pid(-1,NULL,WNOHANG);
    }
  }
}

As usual, we use the accept() function here to wait for new client connections. accept() returns a file descriptor for a socket when a new client arrives.

When a client arrives we call the fork() function to fork the server. Calling fork() in a running process in Linux creates an identical copy of that process. The produces two processes: the original process that called fork() is called the parent process, while the copy produced by fork() is called the child process.

The parent and child processes are identical except for one very tiny but important detail. In the parent process the call to fork() returns an integer process identifier for the newly created child process. In the child process the call to fork() instead returns the value 0. Given this, the usual programming convention is to place the call to fork() in an if statement that can take one branch in the parent process and a different branch in the child process. This will allow the child process to immediately differentiate itself from the parent process. In the case of a forking server we set up the if statement so that the branch that the child takes goes ahead and handles the client request, while the parent branch goes back to listening for new client connections.

Another convention in a forking server is for the child process to exit immediately after serving the current client. To exit, the child process calls exit() with a status code. A status code of 0 indicates successful completion.

When the server forks to handle the client, the client gets an exact copy of everything in the parent, including all of the server's data structures, open files, and open socket connections. In particular, this means that the client can use the socket to the client that the server set up. When the client is done working with that client it will close that socket as usual.

In the server side the server will also have a copy of that same client socket. Since the parent is not going to do anything further with that client, the parent can safely close its copy of the server socket. This has no adverse impact on the communication between the child process and the client.

When a parent process launches a child process and that child process eventually terminates, the child process will become a zombie process that leaves behind a few small data structures, including information about how the process terminated. It is then the responsibility of the parent process to reap the no longer active child process via a call to wait_pid(). As the name of the function suggests, wait_pid() will wait until the child process is no longer running before returning. In the case of a server, we probably don't want to block the parent process to wait for the child to finish running, since the server will want to go back to listening for new connections as soon as possible. Given this, the server uses a special configuration of the parameters to wait_pid(). The first parameter to wait_pid() is normally the process id number of the child process we are waiting on. In place of this, we are instead using a value of -1, which indicates that we want to reap any one child process that is available to reap. The third parameter to wait_pid() is where we specify options: in this case we use the WNOHANG option, which indicates that if no child process is currently ready to be reaped we will not wait around for one to be available.

Although a forking server is even easier to implement than a threaded server, threaded servers are still preferred for a number of reasons. The first reason to prefer threads to child processes is that a thread is a more light weight construct than a process. Creating multiple threads versus creating multiple child processes requires fewer system resources. A second, more critical reason to prefer threads is that threads all run in the same process and share the same memory space. This is especially important in cases where the running threads all need to share access to a common data structure. This is impossible to do easily in child processes, because each child process will inherit its own unique copy of that data structure: any changes that that client process makes to the data structure will not be visible to either the parent or the other child processes.