Department of Engineering

IT Services

Unix Signals and Forking

What are signals?

A signal is a message (an integer) sent to a process. The receiving process can try to ignore the signal, block the signal, or call a routine (a so-called signal handler). After returning from the signal handler, the receiving process will resume execution at the point at which it was interrupted. The system calls to deal with signals vary between one Unix version and another (the main split is between BSD and System V styles of Unix), so beware! Even where different Unix versions have the same function names, the arguments may differ). On our CentrOS Teaching system there are POSIX, XOPEN, XOPEN_EXTENDED, and SVID flavours of signal support.

The signal values are made available by including the signal.h file. In the context of this document, SIGCLD or SIGCHLD (depending on the type of Unix) are relevant. They are sent when a child process terminates or is stopped.

Sending and receiving signals

Signals can be sent using the kill() routine. Non-superuser processes can only send signals to processes with the same uid and gid or to processes in the same process group. The signal() and sigaction() routines are used to control how a process should respond to a signal. The sigaction() function supersedes the signal() interface, so that's what's going to be used here. sigaction() was derived from the IEEE POSIX 1003.1-1988 Standard, so it should be widespead. If you use

 const struct sigaction act;
 sigaction (SIGCHLD, &act, NULL) 

with appropriate settings in the sigaction structure you can control the current process's response to receiving a SIGCHLD signal. As well as setting a signal handler, other behaviours can be set. If

  • act.sa_handler is SIG_DFL then the default behaviour will be restored
  • act.sa_handler is SIG_IGN then the signal will be ignored if possible (SIGSTOP and SIGKILL can't be ignored)
  • act.sa_flags is SA_NOCLDSTOP - SIGCHLD won't be generated when children stop.
  • act.sa_flags is SA_NOCLDWAIT - child processes of the calling process will not be transformed into zombie processes (processes that have exited, but haven't been acknowledged as much by their parent process) when they terminate.

Once an action is installed for a specific signal, it remains installed until another action is explicitly requested unless you're using a non-BSD style of signals. In that case you need to reset the signal handler each time a signal's dealt with.

Signals and processes

When a process dies, its parent is sent a SIGCHLD. So one way of avoiding zombies is to install a signal handler that waits for the children as and when they die:

/* this first line is needed on some systems to get POSIX behaviour */ 
#define _INCLUDE_POSIX_SOURCE
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
void waiter(int dummy) {
        int st;
        while (wait3(&st, WNOHANG, NULL) > 0);
}

int main(int argc, char *argv[]){
 struct sigaction act;
 act.sa_handler=waiter;
 sigaction (SIGCHLD, &act, NULL) ;
 ...
}

You also should make sure that when the parent exits, it waits for any children which have not yet finished.

Alternatively you can arrange for the process not to become a zombie in the first place. This condition needs to be set before any forks. Two ways of doing this (but they may not both work on your machine) are

  • If the action for the SIGCHLD signal is set to SIG_IGN, child processes of the calling processes will not be transformed into zombie processes when they terminate.
  • void MySigIgnore(int dummy)
    {
    }
    
    int main(int argc, char *argv[]){
      struct sigaction act;
    
      act.sa_handler = MySigIgnore;
      sigemptyset(&act.sa_mask); /* non-standard */
      act.sa_flags = SA_NOCLDWAIT;
    
      sigaction(SIGCHLD, &act, NULL);
    }
    

Multiple signals

Another problem is what to do if a signal arrives while you're already dealing with a signal - do you want certain signals to be ignored or blocked? The sigaction(), sigprocmask(), siginterrupt(), and sigsuspend() functions control the manipulation of the signal mask, which defines the set of signals currently blocked. The manual pages give details. The following code shows how the response to signals can be delayed.

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>

void divert (int sig) {
  printf("signal received=%d\n",sig);
}

int main() {
sigset_t mask, pending;

if (signal(SIGINT, divert)== SIG_ERR) {
   perror("signal(SIGINT, divert) failed");
   exit(1);
}
printf("going to sleep for 5 secs, during which Ctrl-C will wake up the process\n");
sleep(5);

sigemptyset(&mask);
sigaddset(&mask,SIGINT);
if(sigprocmask(SIG_BLOCK, &mask, 0) < 0) {
    perror("sigprocmask");
    exit(1);
}

printf("sleeping again for 5 secs, delaying the response to Ctrl-C\n");
sleep(5);

if(sigpending(&pending) <0) {
   perror("sigpending");
   exit(1);
}

if(sigismember(&pending, SIGINT))
   printf("SIGINT pending\n");

if(sigprocmask(SIG_UNBLOCK,&mask,0) < 0) {
  perror("sigblockmask");
  exit(1);
}

printf("SIGINT unblocked\n");
}

Signals and process groups

If you kill a process, how do you kill all its children? Processes can be put into groups, and a signal can be sent to the whole group with id GID if -GID is given to kill().

siginterrupt

Like most of these commands, siginterrupt's details might vary according to the Unix/Linux version you have. Its job is to change the restart behaviour when a system call is interrupted by a signal. By default on Linux, the system call will be restarted, but if you specify a new signal handler, the behaviour will change.

References