Process Management in Unix/Linux: Process Termination

In an operating system, a process may terminate or die, which is a common term of process termina­tion. As mentioned in Chap. 2, a process may terminate in two possible ways:

Normal termination: The process calls exit(value), which issues _exit(value) system call to execute kexit(value) in the OS kernel, which is the case we are discussing here.

Abnormal termination: The process terminates abnormally due to a signal. Signals and signal handling will be covered later in Chap. 6.

In either case, when a process terminates, it eventually calls kexit() in the OS kernel. The general algorithm of kexit() is as follows.

1. Algorithm of kexit()

/**************** Algorithm of kexit(int exitValue) *****************/

    1. Erase process user-mode context, e.g. close file descriptors,

release resources, deallocate user-mode image memory, etc.

    1. Dispose of children processes, if any
    2. Record exitValue in PROC.exitCode for parent to get
    1. Become a ZOMBIE (but do not free the PROC)
    2. Wakeup parent and, if needed, also the INIT process P1

All processes in the MT system run in the simulated kernel mode of an OS. As such they do not have any user mode context. So we begin by discussing Step 2 of kexit(). In some OS, the execution environment of a process may depend on that of its parent. For example, the child’s memory area may be within that of the parent, so that the parent process can not die unless all of its children have died. In Unix/Linux, processes only have the very loose parent-child relation but their execution environments are all independent. Thus, in Unix/Linux a process may die any time. If a process with children dies first, all the children processes would have no parent anymore, i.e. they become orphans. Then the question is: what to do with such orphans? In human society, they would be sent to grandma’s house. But what if grandma already died? Following this reasoning, it immediately becomes clear that there must be a process which should not die if there are other processes still existing. Otherwise, the parent- child process relation would soon break down. In all Unix-like systems, the process P1, which is also known as the INIT process, is chosen to play this role. When a process dies, it sends all the orphaned children, dead or alive, to P1, i.e. become P1’s children. Following suit, we shall also designate P1 in the MT system as such a process. Thus, P1 should not die if there are other processes still existing. The remaining problem is how to implement Step 2 of kexit() efficiently. In order for a dying process to dispose of orphan children, the process must be able to determine whether it has any child and, if it has children, find all the children quickly. If the number of processes is small, e.g. only a few as in the MT system, both questions can be answered effectively by searching all the PROC structures. For example, to determine whether a process has any child, simply search the PROCs for any one that is not FREE and its ppid matches the process pid. If the number of processes is large, e.g. in the order of hundreds or even thousands, this simple search scheme would be too slow to be acceptable. For this reason, most large OS kernels keep track of process relations by maintaining a process family tree.

2. Process Family Tree

Typically, the process family tree is implemented as a binary tree by a pair of child and sibling pointers in each PROC, as in

PROC *child, *sibling, *parent;

where child points to the first child of a process and sibling points to a list of other children of the same parent. For convenience, each PROC also uses a parent pointer to point at its parent. As an example, the process tree shown on the left-hand side of Fig. 3.2 can be implemented as the binary tree shown on the right-hand side, in which each vertical link is a child pointer and each horizontal link is a sibling pointer. For the sake of clarity, parent and null pointers are not shown.

With a process tree, it is much easier to find the children of a process. First, follow the child pointer to the first child PROC. Then follow the sibling pointers to traverse the sibling PROCs. To send all children to P1, simply detach the children list and append it to the children list of P1 (and change their ppid and parent pointer also).

Each PROC has an exitCode field, which is the process exitValue when it terminates. After recording exitValue in PROC.exitCode, the process changes its status to ZOMBIE but does not free the PROC structure. Then the process calls kwakeup(event) to wake up its parent, where event must be the same unique value used by both the parent and child processes, e.g. the address of the parent PROC structure or the parent pid. It also wakes up P1 if it has sent any orphans to P1. The final act of a dying process is to call tswitch() for the last time. After these, the process is essentially dead but still has a dead body in the form of a ZOMBIE PROC, which will be buried (set FREE) by the parent process through the wait operation.

3. Wait for Child Process Termination

At any time, a process may call the kernel function

pid = kwait(int *status)

to wait for a ZOMBIE child process. If successful, the returned pid is the ZOMBIE child’s pid and status contains the exitCode of the ZOMBIE child. In addition, kwait() also releases the ZOMBIE child PROC back to the freeList for reuse. The algorithm of kwait is

/******* Algorithm of kwait() *******/ int kwait(int *status)

{

if (caller has no child) return -1 for error;

while(1){ // caller has children

search for a (any) ZOMBIE child;

if (found a ZOMBIE child){

get ZOMBIE child pid

copy ZOMBIE child exitCode to *status;

bury the ZOMBIE child (put its PROC back to freeList)

return ZOMBIE child pid;

}

//**** has children but none dead yet ****

ksleep(running); // sleep on its PROC address

}

}

In the kwait algorithm, the process returns -1 for error if it has no child. Otherwise, it searches for a ZOMBIE child. If it finds a ZOMBIE child, it collects the ZOMBIE child’s pid and exitCode, releases the ZOMBIE PROC to freeList and returns the ZOMBIE child’s pid. Otherwise, it goes to sleep on its own PROC address, waiting for a child to terminate. Since each PROC address is a unique value, which is also known to all children processes, a waiting parent may sleep on its own PROC address for child to wake it up later. Correspondingly, when a process terminates, it must issue

kwakeup(running->parent);

to wake up the parent. Instead of the parent PROC address, the reader may verify that using the parent pid should also work. In the kwait() algorithm, when the process wakes up, it will find a dead child when it executes the while loop again. Note that each kwait() call handles only one ZOMBIE child, if any. If a process has many children, it may have to call kwait() multiple times to dispose of all the dead children. Alternatively, a process may terminate first without waiting for any dead child. When a process dies, all of its children become children of P1. In a real system, P1 executes in an infinite loop, in which it repeatedly waits for dead children, including adopted orphans. Therefore, in a Unix-like system, the INIT process P1 wears many hats.

. It is the ancestor of all processes except P0. In particular, it is the grand daddy of all user processes since all login processes are children of P1.

. It is the head of an orphanage since all orphans are sent to his house and call him Papa.

. It is the manager of a morgue since it keeps looking for ZOMBIEs to bury their dead bodies.

So, in a Unix-like system if the INIT process P1 dies or gets stuck, the system would stop functioning because no user can login again and the system will soon be full of rotten corpses.

Source: Wang K.C. (2018), Systems Programming in Unix/Linux, Springer; 1st ed. 2018 edition.

Leave a Reply

Your email address will not be published. Required fields are marked *