Operations on Processes

We can perform various operations on a process: spawning child processes, terminate the process, set up inter-process communication channels, change the process priority, and many more. All of these operations require a system call (switching to Kernel Mode). In this example, we use the C API to make the system call.

Process Creation

We can create new processes using fork() system call.

  1. The process creator is called a parent process, the new processes are called the children of that process.
  2. Each of these new processes may in turn create more child processes, forming a tree of processes.

Process Tree

We can illustrate multiple process creation as a process tree:

In the example above, there are 5 processes in total. Process 2, 3, and 4 are direct children of Process 1. Process 5 is created by Process 2.

Process id

Each process is identified by an integer called the process id (pid). Pid is unique in the system.

You can type the command ps [options] to observe all running processes in your system, along with the pid of each process. For instance,

Child Process vs Parent Process

The new process consists of the entire copy of the address space (code, stack, process of execution, etc) of the original parent process at the point of fork().

In other words, the child process inherits the parent process’ state at the point of fork().

Parent and child processes operate in different address space (isolation). Since they are different processes, parent and children processes execute concurrently.

Practically, a parent process waits for its children to terminate (using wait() system call) to read the child process’ exit status and only then its PCB entry in the process table can be removed.

Child processes cannot wait for their parents to terminate. Since children processes are a duplicate of their parents (inherits the whole address space), they can either

  1. Execute the same instructions as their parents concurrently, or
  2. Load a new program into its address space

Program: How fork works

It is best to explain how fork() process creation works by example.

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

int main(int argc, char const *argv[])
{
   pid_t pid;

   pid = fork();
   printf("pid: %d\n", pid);

   if (pid < 0)
   {
       fprintf(stderr, "Fork has failed. Exiting now");
       return 1; // exit error
   }
   else if (pid == 0)
   {
       execlp("/bin/ls", "ls", NULL);
   }
   else
   {
       wait(NULL);
       printf("Child has exited.\n");
   }
   return 0;
}

The simple C program above is executed and when the execution system call fork() returns, two processes are present.

Both have the same copy of the text (code) and resources (any opened files, etc). The parent process is cloned, resulting in the child process. They’re at a different address space, executed concurrently by the system.

fork return value

fork()returns 0 in the child process while in the parent process it returns the pid of the child (>0).

We can write just one instruction for both parent and child process but each will take a different branch of the if clause. In the example code above:

  • The child executes the line if-clause: execlp
  • The parent process executes the else clause where it wait for the child process to exit

execlp

execlp is a system call that loads a new program called ls onto the child process’ address space, effectively replacing its text (code), data, and stack content.

wait

Concurrently, the parent process executes wait(NULL), which is a system call that suspends the parents’ execution until this child process that is executing ls has returned.

Program: The fork tree

Compile and run the C program below.

How many processes are created in total? (excluding the parent process). Can you draw the process tree?

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

int main(int argc, char const *argv[])
{
   int level = 3;
   pid_t pid[level];

   for (int i = 0; i < level; i++)
   {
       pid[i] = fork();

       if (pid[i] < 0)
       {
           fprintf(stderr, "Fork has failed. Exiting now");
           return 1; // exit error
       }
       else if (pid[i] == 0)
       {
           printf("Hello from child %d \n", i);
       }
   }

   return 0;
}

Process Termination

A process needs certain resources (CPU time, memory, files, I/O devices) to run and accomplish its task. These resources are limited.

A process can terminate itself using exit() system call. A process can also terminate other processes using the kill(pid, SIGKILL) system call.

Once a process is terminated, these resources are freed by the kernel for other processes. Parent processes may terminate or abort its children as it knows the pid of its children.

Orphaned processes

If a parent process with live children is terminated, the children processes become orphaned processes:

  • Some operating system is designed to either abort all of its orphaned children (cascading termination) or
  • Adopt the orphaned children processes (init process usually will adopt orphaned processes)

About init

In UNIX-like OS, init is the first process started by the kernel during booting of the computer system. Init is a daemon process that continues running until the system is shut down (see Appendix). It is the direct or indirect ancestor of all other processes, and automatically adopts all orphaned processes.

Init is a user process like any other processes, and hence it is using virtual memory. The only special thing about init is that it is one of the two processes that the kernel started initially. When init is started by the kernel, it goes into user mode. When init calls system call fork(), it traps into the kernel mode, and the kernel does certain things to create the new process, and the new process will be scheduled in the future. When the fork() returns, the original process is back to user mode. The equivalent of init in macOS is launchd.

Zombie Processes

Zombie processes are processes that are ALREADY TERMINATED, and memory as well as other resources are freed, but its exit status is not read by their parents, hence its entry (PCB) in the process table remains.

A parent process must call wait or waitpid to read their children’s exit status. A call to wait or waitpid blocks the calling process until one of its child processes exits or a signal is received. Otherwise, their child process becomes a zombie process.

  • Children processes can terminate themselves after they have finished executing their tasks using exit(int status) system call.
  • The kernel will free the memory and other resources from this process, but not the PCB entry.
  • Parent processes are supposed to call wait or waitpid to obtain the exit status of a child.
  • Only after wait or waitpid in the parent process returns, the kernel can remove the child PCB entry from the system wide process table.
  • If the parents didn’t call wait or waitpid and instead continue execution of other things, then children’s entry in the pcb remains; a zombie process remains.
    • A zombie process generally takes up very little memory space, but pid of the child remains
    • Recall that pid is unique, hence in a 32-bit system, there’re only 32768 available pids.

Having too many zombie processes might result in inability to create new processes in the system, simply because we may run out of pid.

Note: if a parent process has died as well, then all the zombie children will be cleared by the kernel. To observe zombie children, you need to artifically suspend the parent process after the children have terminated.

All processes transition to this zombie state when they terminate, but generally they exist as zombies only briefly. Once the parent calls wait, waitpid the pid of the zombie process and its entry in the process table are released.

Program: Zombie making

Compile and run the C program below. It will suspend itself at scanf, waiting for input at stdin. Do not type anything, leave it hanging there.

What’s the (possible) maximum number of zombies created by this process?

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

int main(int argc, char const *argv[])
{
    int level = 5;
    pid_t pid[level];

    for (int i = 0; i < level; i++)
    {
        pid[i] = fork();

        if (pid[i] < 0)
        {
            fprintf(stderr, "Fork has failed. Exiting now");
            return 1; // exit error
        }
        else if (pid[i] == 0)
        {
            printf("Hello from child %d \n", i);
            return 0;
        }
    }

    int testInteger;
    printf("Enter an integer: "); // artificially blocking parent
    scanf("%d", &testInteger);

    return 0;
}

We can enter the ps aux | grep 'Z' command to list all zombie processes in the system caused by running the program above.