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).
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:
Sets the PF_EXITING
flag in the
flag
field of the process descriptor to indicate
that the process is being eliminated.
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).
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.
Decrements the resource counters of the modules used by the process.
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).
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.
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.
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:
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.
Invokes the free_uid( )
function to decrement by 1
the resource counter of the user_struct
structure.
Invokes unhash_process( )
, which in turn:
Decrements by 1 the nr_threads
variable
Invokes unhash_pid( )
to remove the process
descriptor from the pidhash
hash table
Uses the REMOVE_LINKS
macro to unlink the process
descriptor from the process list
Removes the process from its thread group, if any
Invokes the free_task_struct( )
function to
release the 8-KB memory area used to contain the process descriptor
and the Kernel Mode stack.