Notes on Signals

Notes on Signals, signal(), sigaction(), alarm()


Introduction

A signal is a notification to a process that an event has occurred. Signals are sometimes called "software interrupts". They usually occur asynchronously; a process does not know ahead of time exactly when a signal will occur.

There are 5 conditions that can generate signals:

  1. The kill() system call.
           Declaration:
     
           int kill(int pid, int sig);
           

    A process is not able to send a signal to any other arbitrary process. To send a signal, the sending process and the receiving process must both have the same effective user ID, or the sending process must be the superuser

  2. The kill command.

    This command is a program that takes its command line arguments and issues a kill system call, so all the restrictions and options of the above kill() system call apply here.

  3. Certain terminal characters generate signals.
           
           Ctrl-C  = SIGINT
           Ctrl-\  = SIGQUIT
           Ctrl-Z  = SIGTSTP
           

  4. Certain hardware conditions generate signals.

    Example 1: Floating point arithmetic error generate a SIGFPE error
    Example 2: Referencing an address outside a process' address space generates a SIGSEGV signal

  5. Certain software conditions that are noticed by the kernel cause signals to be generated

    Example: The SIGURG signal is generated when out-of-band data arrives on a socket

alarm()

Purpose:

The alarm function allows us to set a timer that will expire at a specified time in the future. When the timer expires, the SIGALRM signal is generated. If we ignore or don't catch this signal, its default action is to terminate the process.

Declaration:

       #include <unistd.h>
       
       /* returns 0 or the number of seconds until
        * previously set alarm
        */
       unsigned int alarm(unsigned int seconds);    

Parameters:

The seconds value is the number of clock seconds in the future when the signal should be generated. Be aware that when the time occurs, the signal is generated by the kernel, but there could be additional time before the process gets control to handle the signal because of processor scheduling delays

There is only one of these alarm clocks per process. If there is a previously registered alarm clock for the process that has not yet expired, and we call alarm with:

In both cases, the process returns the number of seconds left on the previous alarm.

signal()

Purpose:

The signal() system call installs a new signal handler for a specified signal.

Conceptually, for each process, the UNIX system maintains a table of the action that should be performed for each kind of signal. When the process calls the signal() function, the table entry for the signal type named as the first argument is changed to the value provided by the second argument.

signalfunction pointer for signal_handler (initially set to default settings)
SIGABRTfunc *
SIGALRMfunc *
SIGBUSfunc *
... 
... 

Declaration:

       /* Sigfunc = a function-type that takes an integer argument, 
        *           and returns nothing
        */
       typedef void Sigfunc(int); 
    
       Sigfunc *signal(int signum, Sigfunc *handler);

Parameters:

signum = the signal number. Typically, a constant is used for this (ie. SIGCONT, SIGQUIT, ...see p.266). This makes is possible to use one signal handler for several signals -- within the handler, the input parameter can be used to determine the invoking signal.

The signal handler is set to handler which may be a pointer to a user specified function, or one of the following:

Other details:

There are 3 ways for a signal handler to exit:

  1. Control can be returned to the place in the program which was executing when the signal occurred [this is the default]
  2. Control can be returned to some other point in the program [see sigsetjmp() and siglongjmp() for more info]
  3. The signal handler can terminate the program by calling the exit() or _exit() functions

sigaction()

Purpose:

The sigaction function allows us to examine or modify (or both) the action associated with a particular signal. This function supersedes the signal function.

Declaration:

       #include <signal.h>
       
       /* Returns: 0  if ok, 
        *          -1 on error 
        */
       int sigaction(int signo, const struct sigaction *act,
                                      struct sigaction *oact);

Parameters:

The argument signo is the signal number whose action we are examining or modifying. If the act pointer is nonnull, we are modifying the action. If the oact pointer is nonnull, the system returns the previous action for the signal.

This function uses the following structure:

       struct sigaction {
          void     (*sa_handler)(); /* addr of signal handler,
                                     * or SIG_IGN, or SIG_DL 
                                     */                                    
          sigset_t sa_mask;         /* additional signals to block */
          int      sa_flags;        /* signal options, p. 297      */
       };

sa_handler is parallel to the sole argument previously passed to signal

When changing the action for a signal, if the sa_handler points to a signal-catching function (as opposed to the constants SIG_IGN or SIG_DFL) then the sa_mask field specifies a set of signals that are added to the signal mask of the process before the signal-catching function is called. If and when the signal-catching function returns, the signal mask of the process is reset to its previous value. This way we are able to block certain signals whenever a signal handler is invoked. This new signal mask that is installed by the system when the signal handler is invoked automatically includes the signal being delivered. Hence, we are guaranteed that whenever we are processing a given signal, another occurrence of that same signal is blocked until we're finished processing the first occurrence.

The available options for sa_flags are listed on p. 297. The most interesting are:

SA_RESTARTautomatically restarts system calls that were interrupted by this signal
SA_NOCLDWAITif signo is SIGCHLD, this option causes the system not to create zombie processes when children of the calling process terminate
SA_SIGINFOAdditional information is provided to the signal handler (ie. the PID of the sending process)

sigaction() vs signal()

signalsigaction
By default, the signal being handled can occur again within the signal-handling function By default, sigaction blocks the same signal from occurring again while the signal-handler is executing
We are not able to determine the current disposition of a signal without changing the disposition.

For example, to have a process catch a signal only if the signal is not currently being ignored, common code resembles:

  int sig_int(), sig_quit();

  if (signal(SIGINT, SIG_IGN) != SIG_IGN)
      signal(SIGINT, sig_int);
  if (signal(SIGQUIT, SIG_IGN) != SIG_IGN)
      signal(SIGQUIT, siq_quit);

To retrieve the current disposition, we pass NULL as the 2nd parameter, and examine the results of the 3rd parameter.

The action for a signal is reset to its default each time a signal occurs. Typically, this is dealt with by code similar to:
main() 
{
  /* my signal handling function */
  int sig_int();

  ...
  /* establish handler */
  signal(SIGINT, sig_int); 
  ...
}
        
sig_int() 
{
  /* reestablish handler */
  signal(SIGINT, sig_int);
  ...
}

The problem with this code fragment is that there is a window of time after the signal has occurred, but before the call to signal in the signal handler, when the interrupt signal could occur another time. This second signal would cause the default action to occur, which for this signal terminates the process.

A signal handler setup by sigaction remains installed until it is explicitly changed

If a process catches a signal while being blocked in a "slow" system call (see p.276), then the system call is interrupted. The system call returns an error and errno is set to EINTR. This was done under the assumption that since a signal occurred and the process caught it, there is a good chance that something has happened that should wake up the blocked system call.

The problem with interrupted system calls is that we now have to handle the error return explicitly. The typical code sequence (assuming a read operation and assuming we want to restart the read even if it's interrupted) would be:

  again:
    if ((n = read(fd, buff, BUFFSIZE)) < 0) {
      if (errno = EINTR) {
        /* just interrupted sys. call */
        goto again; 
      }
     
      /* handle other errors */
    }
sigaction has the option SA_RESTART for automatically restarting interrupted system calls.

References

  1. "Unix Network Programming" by W. Richard Stevens
  2. "The Berkeley UNIX environment" 2nd edition by R. Nigel Horspool
  3. "Advanced Programming in the UNIX environment", by W. Richard Stevens
  4. Linux man pages