Unix Signals

Signals represent a very limited form of interprocess communication. They are easy to use (hard to use well) but they communicate very little information. In addition the sender (if it is a process) and receiver must belong to the same user id, or the sender must be the superuser. Signals are sent explicitly to a process from another process using the kill function. Signals can indicate some significant terminal action, such as Hang Up. Alternatively signals are sent to a process from the hardware (to indicate things like numeric overflow, or illegal address) through mediation of the OS. There are only a few possible signals (usually 32). A process can specify with a mask (1 bit per signal) what it wants to be done with a signal directed to it, whether to block it or to deliver it. Blocked signals remain pending, i.e. we may ask to have them delivered later. A process specifies with the signal function (obsolete) or with the sigaction function what it wants it to be done when signals are delivered to it. There are three things that can be done when a signal is delivered:

In writing a handler function one has to be conscious that it is executed as an asynchronous action during the execution of a process. The handler code may execute a routine that was interrupted by the signal, or access variables that were being used in the process. Thus one has to be careful about the code that is executed in the handler and in the variables it accesses. You may want to examine in C or C++ the use of the attribute volatile. During execution of the handler associated to a signal, that specific signal is automatically blocked thus preventing a race condition. After the handler function is completed, execution resumes at the statement being executed when the signal was received.

The following diagram describes how a signal is raised, possibly blocked before delivery, and then handled.

We may worry that signals may be lost while pending [if this happens, we talk of an unsafe signal mechanism], but recent Unix system use only reliable mechanisms. Multiple copies of the same signal may be pending to a process so the OS should have facilities to store this information. Multiple copies of an idempotent signal (i.e., two copies of the signal have the same effect as just one, like the termination request) are treated as single copies.

Here are some of the possible signals, with the number associated to them, and their default handling.


    SIGNAL      ID   DEFAULT  DESCRIPTION
    ======================================================================
    SIGHUP      1    Termin.  Hang up on controlling terminal
    SIGINT      2    Termin.  Interrupt. Generated when we enter CNRTL-C
    SIGQUIT     3    Core     Generated when at terminal we enter CNRTL-\
    SIGILL      4    Core     Generated when we executed an illegal instruction
    SIGTRAP     5    Core     Trace trap (not reset when caught)
    SIGABRT     6    Core     Generated by the abort function
    SIGFPE      8    Core     Floating Point error
    SIGKILL     9    Termin.  Termination (can't catch, block, ignore)
    SIGBUS     10    Core     Generated in case of hardware fault
    SIGSEGV    11    Core     Generated in case of illegal address
    SIGSYS     12    Core     Generated when we use a bad argument in a 
                              system service call
    SIGPIPE    13    Termin.  Generated when writing to a pipe or a socket
                              while no process is reading at other end
    SIGALRM    14    Termin.  Generated by clock when alarm expires
    SIGTERM    15    Termin.  Software termination signal
    SIGURG     16    Ignore   Urgent condition on IO channel
    SIGCHLD    20    Ignore   A child process has terminated or stopped
    SIGTTIN    21    Stop     Generated when a backgorund process reads
                              from terminal
    SIGTTOUT   22    Stop     Generated when a background process writes
                              to terminal
    SIGXCPU    24    Discard  CPU time has expired
    SIGUSR1    30    Termin.  User defiled signal 1
    SIGUSR2    31    Termin.  User defined signal 2

One can see the effect of these signal by executing in the shell the kill command. For example
    % kill -TERM pidid
You may see what are the defined signals with the shell command
    % kill -l

It is easy to see that some signals occur synchronously with the executing program (for example SIGSEGV), others asynchronously (for example SIGINT), and others are directed from one process to another (for example SIGKILL).

Here are some basic functions for signals:

    #include <signal.h>
    void (*signal(int sign, void(*function)(int)))(int);
         The signal function takes two parameters, an integer
	 and the address of a function of one integer argument which
	 gives no return. Signal returns the address of a function of
	 one integer argument that returns nothing. 
         sign identifies a signal
         the second argument is either SIG_IGN (ignore the signal)
	 or SIG_DFL (do the default action for this signal), or
	 the address of the function that will handle the signal.
	 It returns the previous handler to the sign signal.
         The signal function is still available in modern Unix
         systems, but only for compatibility reasons.
         It is better to use sigaction.


    #include <signal.h>
    void sigaction(int signo, const struct sigaction *action, 
                   struct sigaction *old_action);
    where
      struct sigaction {
        void (*sa-handler) (int); /*address of signal handler*/
        sigset_t  sa_mask;        /*signals to block in addition to the one
                                    being handled*/
        int  sa_flags;};          /*It specifies special handling for a 
                                    signal. For simplicity we will assume
                                    it is 0.*/
        /*The possible values of sa_handler are:
          SIG_IGN:    ignore the signal
          SIG_DFL:    do the default action for this signal
          or the address of the signal handler*/

    Objects of type sigset_t can be manipulated with the functions:

    #include <signal.h>
    int sigemptyset(sigset_t * sigmask);
    int sigaddset(sigset_t * sigmask, const int signal_num);
    int sigdelset(sigset_t * sigmask, const int signal_num);
    int sigfillset(sigset_t * sigmask);
    int sigismember(const sigset_t * sigmask, const int signal_num);

    that have the meaning implied by their names.

    We can determine, or change, the mask currently defined for
    blocking signals in the system, with the function

    #include <signal.h>
    int sigprocmask(int cmd, const sigset_t* new_mask, sigset_t* old_mask);
    where the parameter cmd can have the values
      SIG_SETMASK:  sets the system mask to new_mask
      SIG_BLOCK:    Adds the signals in new_mask to the system mask
      SIG_UNBLOCK:  Removes the signals in new_mask from system mask
    If old_mask is not null, it is set to the previous value of the system
    mask

    #include <sys/types.h>
    #include <signal.h>
    int kill(pid_t process-id, int sign);
	Sends the signal sign to the process process-id.
	[kill may also be used to send signals to groups of
	processes.]

Other useful functions are:

    #include <unistd.h>
    unsigned int alarm(unsigned int n);
        It requests the delivery in n seconds of a SIGALRM signal.
        If n is 0 it cancels a requested alarm.
        It returns the number of seconds left for the previous call to
        alarm (0 if none is pending).

    #include <unistd.h>
    int pause(void);
        It requests to be put to sleep until the process receives a signal.
        It always returns -1.
        
    #include <signal.h>
    int sigsuspend(const sigset_t *sigmask);
        It saves the current (blocking) signal mask and sets it to
	sigmask. Then it waits for a non-blocked signal to arrive.
	At which time it restores the old signal mask, returns -1,
        and sets errno to EINTR (since the system service was
        interrupted by a signal).
	It is used in place of pause when afraid of race conditions
	in the situation where we block some signals, then we unblock
        and would like to wait for one of them to occur.

The use of the function signal is being replaced in recent Unix versions by the use of the function sigaction which is more complex and uses signal sets. The functions signal and pause are still being supported for compatibility reasons.

Here is a very simple example of use of signals (modified from Stevens)

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

    static void	sig_cld();

    int
    main()
    {
	pid_t	pid;
	int     i;

	if ((int)signal(SIGCLD, sig_cld) == -1) {
		printf("signal error\n");
                exit(1);}
	for (i=0; i < 10; i++){
	  if ( (pid = fork()) < 0) {
	    printf("fork error\n");
	    exit(1);}
	  else if (pid == 0) {		/* child */
	    sleep(2);
	    exit(0);
	  }
	  pause();	/* parent */
	}
	exit(0);
    }

    static void
    sig_cld()
    {
	pid_t	pid;
	int	status;

	printf("SIGCLD received, ");
	if ( (pid = wait(&status)) < 0){	/* fetch child status */
		printf("wait error\n");
                exit(1);}
	printf("pid = %d\n", pid);
	return;		                        /* interrupts pause() */
    }
This program binds the signal SIGCLD to a handler sig_cld, then it loops ten times. In each iteration it creates a child and pauses. The handler is called when the child terminates. It collects and prints out information about the defunct child.
Notice that in this handler we do not need, as it was necessary in old Unix implementations, to reestablish the handler.

As we saw earlier, the programmer can control which signals are blocked [a blocked signal remains pending until the program unblocks that signal and the signal is delivered] with the sigprocmask function.

Here is an example of a program that uses sigaction and sigsuspend instead of signal and pause.


/* Process sets a handler for SIGILL, and suspends until any 
 * signal is received. If SIGILL is received the handler
 * is executed; for all other signals the handler is not executed.
 * You can run this program with "% a.out &". The program prints
 * its own processid. Then you can send a SIGILL signal to it with 
 *             % kill -ILL processid
 * Sending any other signal will not invoke the handler and will have
 * the usual behavior: will be ignored or will terminate the process.
 */

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

void handler(int a ); 

int main() {
  sigset_t  zeromask;
  struct sigaction  action, old_action;
  
  /* Block all signals when executing handler */
  sigfillset(&action.sa_mask); 
  action.sa_handler = handler;
  action.sa_flags = 0;
  if (sigaction(SIGILL,&action,&old_action)==-1) 
    perror( "sigaction");
  /* wait for any signal */
  sigemptyset(&zeromask);
  sigsuspend(&zeromask); 
  printf("After signal is received and perhaps handled\n");
  return 0;
}

void handler(int a){
  printf("\ncaught signal %d\n", a);
};

To use signals well is very tricky. Signals can interfere with system services. For example, what happens if we receive an exception while executing a system call? The answer depends on the specific call. Usually time consuming operations such as IO operations are interrupted by a signal. For example if we are reading from a file we may be interrupted. We find out about it by checking errno for the value EINTR. For example:

      bytesread = read(input_fd, buf, BLKSIZE);
      if (bytesread < 0) 
         if (errno == EINTR)
            printf("The read was interrupted. You can try to read again\n");
         else 
            printf("Error in read. Don't know what to do about it\n");

Here is another program that shows the effect of signals on system services, sleep in this case. The termination of the child will wake the parent from its sleep by delivering the signal SIGCHLD.

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

void handler(int a ); 

int main() {

  sigset_t  sigmask;
  struct sigaction  action, old_action;
  pid_t childpid;
  int remaining_time;

  /* Set up handler for SIGCHLD */
  sigfillset(&action.sa_mask); /*Block all signals while executing handler*/
  action.sa_handler = handler; 
  action.sa_flags = 0;
  if (sigaction(SIGCHLD, &action, &old_action)==-1) 
    perror( "sigaction");

  /* Create and run the child process */
  if((childpid=fork())<0){
    perror("Fork error");
    exit(0);
  }
  if (childpid==0){
    /* The child process executes the ls command and exits */
    sleep(3); /* Just to make sure that the parent will run first */
    if(execlp("ls","ls",(char *)0)!=0){
      perror("exec did not work");
      exit(1);
    }
    /* Note that the child at this point has terminated */
  }

  /* We are in the parent */
  printf("The child is %d\n", childpid);
  remaining_time = 10;
  do {
    remaining_time = sleep(remaining_time);
    printf("The remaining sleep time is %d\n", remaining_time);
  }while(remaining_time > 0);
  printf("Parent has slept, now it returns\n");
  return 0;
}

void handler(int a){  /* a is the signal being handled */
  pid_t pid;
  int statloc;

  printf("\nWe caught signal %d\n", a);
  pid = wait(&statloc);           
  printf("Terminated child was %d with exit code %d\n", 
	 pid, statloc);
};

ingargiola.cis.temple.edu