What do the system calls fork(), vfork(), exec(), wait(), waitpid() do? Whats a Zombie process? Whats the difference between fork() and vfork()?

The system call fork() is used to create new processes. It does not take any arguments and returns a process ID. The purpose of fork() is to create a new process, which becomes the child process of the caller (which is called the parent). After a new child process is created, both processes will execute the next instruction following the fork() system call. We can distinguish the parent from the child by testing the returned value of fork():  

If fork() returns a negative value, the creation of a child process was unsuccessful. A call to fork() returns a zero to the newly created child process and the same call to fork() returns a positive value (the process ID of the child process) to the parent. The returned process ID is of the type pid_t defined in sys/types.h. Normally, the process ID is an integer. Moreover, a process can use the function getpid() to retrieve the process ID assigned to this process. Unix will make an exact copy of the parent's address space and give it to the child. Therefore, the parent and child processes will have separate address spaces. Both processes start their execution right after the system call fork(). Since both processes have identical but separate address spaces, those variables initialized before the fork() call have the same values in both address spaces. Since every process has its own address space, any modifications will be independent of the others. In other words, if the parent changes the value of its variable, the modification will only affect the variable in the parent process's address space. Other address spaces created by fork() calls will not be affected even though they have identical variable names. Following is a tricky interview question:

main() {   
  fork();   
  fork(); 
} 

The answer is 2 raise to n when n is the number of calls to fork (2 in this case). 

Each process has certain information associated with it including: 

The UID(numeric user identity).
The GID(numeric group identity). 
A process ID number used to identify the process. 
A parent process ID. 
The execution status, e.g.active, runnable, waiting for input etc.
Environment variables and values.
The current directory.
Where the process currently resides in memory.
The relative process priority see nice(1).
Where it gets standard input from.
Where it sends standard output to.
Any other files currently open.

Certain process resources are unique to the individual process. A few among these are: 

  • Stack Space: This is where local variables, function calls, etc. are stored.  
  • Environment Space: This is used for the storage of specific environment variables.
  • Program Pointer (Counter): PC.
  • File Descriptors: fd
  • Variables.

Under UNIX, each subdirectory under /proc corresponds to a running process (PID #). A ps command provides detailed information about the processes running. A typical output of ps looks as follows:  

PID               TTY            STAT           TIME         COMMAND     
--------------------------------------------------------------------------------
8811              3                  SW             0:00         (login)
3466              3                  SW             0:00         (bash)      
8777              3                  SW             0:00         (startx)
1262              p7                 R              0:00          ps 

The columns refer to the following: 

PID      - The process id's (PID).
TTY      - The terminal the process was started from.
STAT     - The current status of all the processes. 
		   Info about the process status can be broken into more than 1 field. 
		   The first of these fields can contain the following entries:  
 				R - Runnable.             
				S - Sleeping.             
				D - Un-interruptable sleep.             
				T - Stopped or Traced.             
				Z - Zombie Process. 
 
The second field can contain the following entry:  
	W - If the process has no residual pages. 
 
And the third field:  
	N - If the process has a positive nice value.              
	TIME - The CPU time used by the process so far.             
	COMMAND - The actual command. 
 

The init process is the very first process run upon startup. It starts additional processes. When it runs it reads a file called /etc/inittab which specifies init how to set up the system, what processes it should start with respect to specific run levels. One crucial process which it starts is the getty program. A getty process is usually started for each terminal upon which a user can log into the system. The getty program produces the login: prompt on each terminal and then waits for activity. Once a getty process detects activity (at a user attempts to log in to the system), the getty program passes control over to the login program. There are two commands to set a process's priority nice and renice. One can start a process using the system() function call:

#include <stdio.h>  
#include <stdlib.h>

int main() {
  printf("Running ls.....\n");
  system("ls -lrt");
  printf("Done.\n");
  exit(0);
}

The exec() functions replace a current process with another created according to the arguments given. The syntax of these functions is as follows: 

#include <unistd.h>

char * env[];

int execl(const char * path, const char * arg0, ..., (char * ) 0);
int execv(const char * path, const char * argv[]);

int execlp(const char * path,const char * arg0, ..., (char * ) 0);
int execvp(const char * path,const char * argv[]);

int execle(const char * path,const char * arg0, ..., (char * ) 0,const char * env[]);
int execve(const char * path,const char * argv[], const char * env[]);

The program given by the path argument is used as the program to execute in place of what is currently running. In the case of the execl() the new program is passed arguments arg0, arg1, arg2,... up to a null pointer. By convention, the first argument supplied (i.e. arg0) should point to the file name of the file being executed. In the case of the execv() programs, the arguments can be given in the form of a pointer to an array of strings, i.e. the argv array. The new program starts with the given arguments appearing in the argv array passed to main. Again, by convention, the first argument listed should point to the file name of the file being executed. The function name suffixed with a p (execlp() and execvp())differ in that they will search the PATH environment variable to find the new program executable file. If the executable is not on the path, and absolute file name, including directories, will need to be passed to the function as a parameter. The global variable environ is available to pass a value for the new program environment. In addition, an additional argument to the exec() functions execle() and execve() is available for passing an array of strings to be used as the new program environment.  
 
Examples to run the ls command using exec are:  

const char *argv[] = ("ls", "-lrt", 0);  
const char *env[] = {"PATH=/bin:/usr/bin", "TERM=console", 0};  
execl("/bin/ls", "ls", "-lrt", 0); execv("/bin/ls", argv);  
execlp("ls", "ls", "-lrt", 0);    execle("/bin/ls", "ls", "-lrt", 0, env);   
execvp("ls", argv);  execve("/bin/ls", argv, env); 

A simple call to fork() would be something like this 

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main() {
  pid_t pid;
  pid = fork();
  switch (pid) {
  case -1:
    exit(1); // fork() error.      
  case 0: // Child process, can call exec here.                
    break;
  default: // Parent.               
    break;
  }
  exit(0);
}

The call wait() can be used to determine when a child process has completed its job and finished. We can arrange for the parent process to wait until the child finishes before continuing by calling wait(). wait() causes a parent process to pause until one of the child processes dies or is stopped. The call returns the PID of the child process for which status information is available. This will usually be a child process that has terminated. The status information allows the parent process to determine the exit status of the child process, the value returned from main or passed to exit. If it is not a null pointer the status information will be written to the location pointed to by stat_loc. We can interrogate the status information using macros defined in sys/wait.h

Macro                                 Definition  
----------------------------------------------------------------------------------- 
WIFEXITED(stat_val);        Nonzero if the child is terminated normally  
WEXITSTATUS(stat_val);      If WIFEXITED is nonzero, this returns child exit code.  
WIFSIGNALLED(stat_val);     Nonzero if the child is terminated on an uncaught signal.  
WTERMSIG(stat_val);         If WIFSIGNALLED is nonzero, this returns a signal number.  
WIFSTOPPED(stat_val);       Nonzero if the child stopped on a signal.  
WSTOPSIG(stat_val);         If WIFSTOPPED is nonzero, this returns a signal number.  

An example code which used wait() is shown below 

#include <sys/types.h> 
#include <sys/wait.h> 
#include <unistd.h>

int main(void) {
  pid_t child_pid;
  int * status = NULL;
  if (fork()) {
    /* wait for child, getting  PID */
    child_pid = wait(status);
    printf("I'm the parent.\n");
    printf("My child's PID was: %d\n", child_pid);
  } else {
    printf("I'm the child.\n");
  }
  return 0;
}

Or a more detailed program

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>

int main() {

  pid_t pid;
  int exit_code;

  pid = fork();
  switch (pid) {
  case -1:
    exit(1);
  case 0:
    exit_code = 11;
    //Set the child exit process              
    break;
  default:
    exit_code = 0;
    break;
  }

  if (pid) {
    // This is the parent process      
    int status;
    pid_t child_pid;

    child_pid = wait( & status);

    printf("Child process finished with PID [%d]\n", child_pid);
    if (WIFEXITED(status)) {
      printf("Child exited with code [%d]\n", WEXITSTATUS(status));
    } else {
      printf("Child terminated abnormally.\n");
    }
  }
  exit(exit_code);
}

So now, whats a Zombie process? 

When using fork() to create child processes it is important to keep track of these processes. For instance, when a child process terminates, an association with the parent survives until the parent either terminates normally or calls wait(). The child process entry in the process table is not freed up immediately. Although it is no longer active, the child process is still in the system because it's exit code needs to be stored in the even the parent process calls wait(). The child process is at that point referred to as a zombie process. Note, if the parent terminates abnormally then the child process gets the process with PID 1, (init) as a parent. (such a child process is often referred to as an orphan). The child process is now a zombie. It is no longer running, it's original parent process is gone, and it has been inherited by init. It will remain in the process table like a zombie until the next time the table is processed. If the process table is long this may take a while. till init cleans them up. As a general rule, program wisely and try to avoid zombie processes. When zombies accumulate they eat up valuable resources. The waitpid() system call is another call that can be used to wait for child processes. This system call, however, can be used to wait for a specific process to terminate.  

#include <sys/types.h>  
#include <sys/wait.h>  
pit_t waitpid(pid_t pid, int *status, int options);  

The pid argument specifies the PID of the particular child process to wait for. If it is a -1 then waitpid() it will return information to the child process. Status information will be written to the location pointed to by status. The options argument enables us to change the behavior of waitpid(). A very useful option is WNOHANG which prevents the call  waitpid() from suspending the execution of the caller. We can it to find out whether any child process has terminated and, if not, to continue.   

Synchronous and asynchronous process execution 
In some cases, for example, if the child process is a server or "daemon" ( a process expected to run all the time in the background to deliver services such as mail forwarding) the parent process would not wait for the child to finish. In other cases, e.g. running an interactive command where it is not a good design for the parent's and child's output to be mixed up into the same output stream, the parent process, e.g. a shell program, would normally wait for the child to exit before continuing. If you run a shell command with an ampersand as it's the last argument, e.g. sleep 60 & the parent shell doesn't wait for this child process to finish.  
 
So what's the difference between fork() and vfork()? 
The system call vfork(), is a low overhead version of fork(), as fork() involves copying the entire address space of the process and is therefore quite expensive. The basic difference between the two is that when a new process is created with vfork(), the parent process is temporarily suspended, and the child process might borrow the parent's address space. This strange state of affairs continues until the child process either exits, or calls execve(), at which point the parent process continues. This means that the child process of a vfork() must be careful to avoid unexpectedly modifying variables of the parent process. In particular, the child process must not return from the function containing the vfork() call, and it must not call exit() (if it needs to exit, it should use _exit(); actually, this is also true for the child of a normal fork()). However, since vfork() was created, the implementation of fork() has improved, most notably with the introduction of `copy-on-write', where the copying of the process address space is transparently faked by allowing both processes to refer to the same physical memory until either of them modify it. This largely removes the justification for vfork(); indeed, a large proportion of systems now lack the original functionality of vfork() completely. For compatibility, though, there may still be a vfork() call present, that simply calls fork() without attempting to emulate all of the vfork() semantics.  

 

C Programming Questions