3.5 Destroying Processes

Most processes "die" in the sense that they terminate the execution of the code they were supposed to run. When this occurs, the kernel must be notified so that it can release the resources owned by the process; this includes memory, open files, and any other odds and ends that we will encounter in this book, such as semaphores.

The usual way for a process to terminate is to invoke the exit( ) library function, which releases the resources allocated by the C library, executes each function registered by the programmer, and ends up invoking the _exit( ) system call. The exit( ) function may be inserted by the programmer explicitly. Additionally, the C compiler always inserts an exit( ) function call right after the last statement of the main( ) function.

Alternatively, the kernel may force a process to die. This typically occurs when the process has received a signal that it cannot handle or ignore (see Chapter 10) or when an unrecoverable CPU exception has been raised in Kernel Mode while the kernel was running on behalf of the process (see Chapter 4).

3.5.1 Process Termination

All process terminations are handled by the do_exit( ) function, which removes most references to the terminating process from kernel data structures. The do_exit( ) function executes the following actions:

1.       Sets the PF_EXITING flag in the flag field of the process descriptor to indicate that the process is being eliminated.

2.       Removes, if necessary, the process descriptor from an IPC semaphore queue via the sem_exit( ) function (see Chapter 19) or from a dynamic timer queue via the del_timer_sync( ) function (see Chapter 6).

3.       Examines the process's data structures related to paging, filesystem, open file descriptors, and signal handling, respectively, with the _ _exit_mm( ), _ _exit_files( ), _ _exit_fs( ), and exit_sighand( ) functions. These functions also remove each of these data structures if no other process are sharing them.

4.       Decrements the resource counters of the modules used by the process.

5.       Sets the exit_code field of the process descriptor to the process termination code. This value is either the _exit( ) system call parameter (normal termination), or an error code supplied by the kernel (abnormal termination).

6.       Invokes the exit_notify( ) function to update the parenthood relationships of both the parent process and the child processes. All child processes created by the terminating process become children of another process in the same thread group, if any, or of the init process. Moreover, exit_notify( ) sets the state field of the process descriptor to TASK_ZOMBIE. We shall see what happens to zombie processes in the following section.

7.       Invokes the schedule( ) function (see Chapter 11) to select a new process to run. Since a process in a TASK_ZOMBIE state is ignored by the scheduler, the process stops executing right after the switch_to macro in schedule( ) is invoked.

3.5.2 Process Removal

The Unix operating system allows a process to query the kernel to obtain the PID of its parent process or the execution state of any of its children. A process may, for instance, create a child process to perform a specific task and then invoke a wait( )-like system call to check whether the child has terminated. If the child has terminated, its termination code will tell the parent process if the task has been carried out successfully.

To comply with these design choices, Unix kernels are not allowed to discard data included in a process descriptor field right after the process terminates. They are allowed to do so only after the parent process has issued a wait( )-like system call that refers to the terminated process. This is why the TASK_ZOMBIE state has been introduced: although the process is technically dead, its descriptor must be saved until the parent process is notified.

What happens if parent processes terminate before their children? In such a case, the system could be flooded with zombie processes that might end up using all the available task entries. As mentioned earlier, this problem is solved by forcing all orphan processes to become children of the init process. In this way, the init process will destroy the zombies while checking for the termination of one of its legitimate children through a wait( )-like system call.

The release_task( ) function releases the process descriptor of a zombie process by executing the following steps:

1.       Decrements by 1 the number of processes created up to now by the user owner of the terminated process. This value is stored in the user_struct structure mentioned earlier in the chapter.

2.       Invokes the free_uid( ) function to decrement by 1 the resource counter of the user_struct structure.

3.       Invokes unhash_process( ), which in turn:

a.       Decrements by 1 the nr_threads variable

b.       Invokes unhash_pid( ) to remove the process descriptor from the pidhash hash table

c.       Uses the REMOVE_LINKS macro to unlink the process descriptor from the process list

d.       Removes the process from its thread group, if any

4.       Invokes the free_task_struct( ) function to release the 8-KB memory area used to contain the process descriptor and the Kernel Mode stack.