×

Search anything:

Linux processes: signals, termination, zombies, cleanup

Binary Tree book by OpenGenus

Open-Source Internship opportunity by OpenGenus for programmers. Apply now.

A process is a running instance of a program, it starts when a command is executed. In this article we discuss various process management concepts such as signals and how they are utilized, how a process is terminated, wait system calls, zombie processes and how they are cleaned.

Table of contents.

  1. Introduction.
  2. Signals.
  3. Process termination.
  4. Waiting for process termination
  5. Zombie processes.
  6. Asynchronous cleanup.
  7. Summary.
  8. References.

Prerequisites.

  1. Linux processes: listing, creation, scheduling

Introduction.

We continue to discuss Linux processes, this is a continuation of the prerequisite article we go over signals, process termination, waiting for a process to terminate, wait system calls, zombie processes and how they are cleaned up to avoid a lagging system.

Signals.

A signal is a special message sent to a process.

Signals are used for communicating with and manipulating processes.
They are asynchronous in nature, in that, when a process receives a signal it processes it immediately ignoring what it is currently doing.

Each signal in Linux is specified with a signal number, this is defined by the /usr/include/signal.h header file.

A program can specify a behavior e.g ignore the signal or call a signal handler and if the handler is in use, the currently executing program is paused, the handler is executed, when it returns the program proceeds with execution.
A default disposition defines what will happen if a program doesn't specify a behavior.

Signals are sent in response to conditions such as floating point exception(SIGFPE), bus error(SIGBUS) or segmentation violation(SIGSEGV), for such the default disposition is to terminate the process and produce a core file.
Processes can also send signals to other processes e.g SIGTERM or SIGKILL used for ending another process, or SUGUSR1 or SIGUSR2 sent to a running program.

SIGHUP is used to wake up an idling program or cause it to reread its config files.
sigaction function is used to set a signal disposition, it takes the signal number and two pointers to sigaction structures as its parameters.
Sigaction structures can take either of the following three values,

  • SIG_DFL: specifying the signal's default disposition.
  • SIG_ING: to ignore a signal.
  • A pointer to signal-handler function that takes the signal number and returns void.

The signal handler should perform the minimum amount of work to respond to a signal then give back control to the main program and thus we should avoid calling I/O operations, library or system functions from signal handlers since the main program may be in a fragile state when a signal is processed.

A race condition can occur when a signal is interrupted by another, This can also happen when we assign a value to a global variable whereby the assignment is carried out by two or more machine instructions and a second signal occurs between them.
If we use a global variable to flag a signal, it should be of sig_attomic_t special type and thus assignment to variables of this types are performed in a single instruction hence no midway interruptions.

signal handler

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

sig_atomic_t sigusr1_count = 0;

void sigHandler(int signalNumber){
    ++sigusr1_count;
}

int main(){
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = &sigHandler;
    sigaction(SIGUSR1, &sa, NULL);
    
    // lengthy opertion here
    
    printf(“SIGUSR1 was raised %d times\n”, sigusr1_count);
    return 0;
}

Process termination.

A process can either terminate when a program calls an exit function or the main program returns.

A process may also terminate in response to a signal(SIGBUS, SIGSEGV, SIGFPE).
When a user presses Ctrl+C in the terminal, the SIGINT signal is sent to the process.

The kill command sends the SIGTERM signal.
The abort() function sends the SIGABRT signal which terminates the process and produces a core file.
The SIGKILL signal is the most powerful, it cannot be blocked, it ends a process immediately and cannot be handled by program.
All these signals can be sent by the kill command in the terminal by specifying options.
We can also send a signal from a program by using the kill function which takes the target PID and the signal number as its parameters.
The <sys/types.h> and <signal.h> header files must be included.
Exit codes are used as indicators to determine if a program execution was successful. An zero exit code indicates success while a non-zero an error.

An example.

bogus command

Execute a bogus command and to check the exit status use the $?.

echo $?

Waiting for process termination.

Child processes are scheduled independently of parent processes as can be seen from the prerequisite article fork-exec.c program where the main program executes first then the output of the ping command is displayed.

It is sometimes desirable for the parent process to wait until one or more child process have completed. In such cases we use the wait family of system calls which allow waiting for a process to finish then enable the parent process to retrieve information about the child process termination.

Wait system calls.

The wait call blocks the calling process until one of its child processes exits or an error occurs then returns a status code from which we can extract information about how the child processes exited.

WIFEXITED macro is used to determine an if an exit was through an exit function or main returning.
WTERMSIG extracts the signal number by which a process died from the exit status.

An example fork-exec.c

int main(){
    int childStatus;
    // The argument list to pass to the "ping" command.
    char* argList[] = {"ping", "8.8.8.8", "-c3", NULL};
    
    // Spawn a child process running the "ping" command.
    // Ignore returned child PID
    spawn("ping", argList);
    
    // Wait for the child process to complete.
    wait(&childStatus);
    
    if(WIFEXITED(childStatus))
        printf("Child process exited normally, exit code: %d \n", WEXITSTATUS (childStatus));
    else
        printf("Child process exited abnormally \n");
    return 0;
}

The parent process calls wait and waits until the child process completes then the ping command is called.

Other wait family system calls are.
waitpid: is used to wait for a specific child process to finish
wait3: gets CPU statistics
wait4: used to specify additional options about processes to wait for.

Zombie processes.

A zombie process is a process that has terminated but not cleaned up. The parent process is responsible for this task. wait functions can also be used for these tasks.

An example

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

int main(){
    pid_t childPID;
    // Create a child process.
    childPID = fork();
    if(childPID > 0)
        // Parent process. Sleep for one minute.
        sleep(60);
    else
        // Child process. Exit immediately
        exit(0);
    return 0;
}

The program above forks a child process that terminates immediately and then it sleeps for a minute without ever cleaning up the child process.

After compiling and running the program, type in the following command.

ps -e -o pid,ppid,stat,cmd

From the output, in addition to the parent zombie process, there exists another zombie process listed. This is the child process, note its PPID is the PID of zombie process. Also note the status code Z for zombie.

When we run ps command again, the zombie process disappears, i.e, when a program exits, its children are inherited by special processes.
The init program - the first process when Linux boots always with PID of 1, cleans up any zombie child processes it inherits.

Asynchronous cleanup.

While using a child process to exec another program, we can use wait to block parent process until child process completes, however, oftenly we want don't want to block the parent process and want it to proceed while one or more children execute synchronously. To be sure that we clean up completed child processes we can user parent process to call wait3 or wait4 to periodically clean up zombie children.
Another solution would be to notify the parent process when a child terminates through signals e.g SIGCHLD whose default disposition it to do nothing.
It is also wise to store the termination status during cleanup.

An example

#include<signal.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>

sig_atomic_t childExitStatus;

void cleanUpChildProcess(int signalNumber){
    //Clean up child process.
    int status;
    wait(&status);
    // Store exit status in global variable
    childExitStatus = status;
}

int main(){
    // Handling SIGCHLD signal by calling cleanUpChildProcess()
    struct sigaction sigchldAction;
    memset(&sigchldAction, 0, sizeof(sigchldAction));
    sigchldAction.sa_handler = &cleanUpChildProcess;
    sigaction(SIGCHLD, &sigchldAction, NULL);
    
    // Other things, e.g forking a child process.
    
    return 0;
}

From the above program, the signal handler stores child process' exit status in a global variable that can be accessed by the main program.

Since the variable is assigned in a signal handler it is of type sig_atomic_int.

Summary.

Processes can be leveraged by applications so as to allow them to multi-task thereby increasing their robustness. This can be done by making use of already existing processes.

In this article at OpenGenus, we have discuss how various signals are used by processes, how a process is terminated, how to wait for process termination, system calls, zombie processes and clean up.

References.

  1. Process management commands.
  2. Advanced Programming in the UNIX Environment 3rd Edition W. Richard Stevens
Linux processes: signals, termination, zombies, cleanup
Share this