CSC 357 Lecture Notes Week 8
Introduction to Signals and Pipes



  1. Relevant reading.
    1. Stevens Chapter 10; Chapter 15, Sections 15.1 and 15.2
    2. Cited man pages, in particular for sigaction and pages referenced therein.
    3. /usr/include/{signal.h,sys/signal.h,sys/iso/signal_iso.h}

  2. Introduction (Section 10.1).
    1. A signal is a program interrupt.
    2. Such an interrupt is an asynchronous event.
    3. Often, an interrupting signal occurs at a time not known in advance to the program being interrupted.
      1. Hence, the program cannot simply test some variable or call some other function to know when a signal has occurred.
      2. Rather, the program has to tell the kernel "When this signal occurs, do the following."

  3. Signal concepts (Section 10.2).
    1. In UNIX, all signals have names.
      1. The names begin with the prefix "SIG", followed by a reasonably mnemonic suffix.
      2. E.g., SIGALRM is an alarm signal generated by a timer created with the alarm or setitimer functions.
    2. Signals can be generated by a wide variety of sources, including:
      1. the user typing certain terminal-interrupt keys, such as ^C or ^Z, which generate a SIGINT and SIGTSTP respectively 1
      2. hardware exceptions, such as divide by 0, invalid memory reference, and the like
      3. the kill(2) function 2 that allows a process to send a signal to another process
      4. the kill(1) shell command, which is an end-user interface to kill(2)
      5. software conditions, such as SIGALRM generated when a timer or alarm goes off

  4. A practical example of signal use (and starter kit for Programming Assignment 5).
    1. The following program illustrates the use of the setitimer and sigaction functions, in the generation and handling of SIGARLM signals.
      1. setitimer initiates an interval timer, that generates a SIGARLM as each time interval elapses
      2. sigaction sets up the handler for SIGALRM
    2. Here is the simple-timer program:
   1  #include <stdio.h>
   2  #include <stdlib.h>
   3  #include <unistd.h>
   4  #include <termios.h>
   5  #include <signal.h>
   6  #include <sys/time.h>
   7
   8
   9  /****
  10   *
  11   * This program is a simple example of using setitimer and sigaction to
  12   * generate and handle SIGALRM signals.  The program sets up an interval timer
  13   * that generates a SIGALRM once a second.  The SIGALRM handler simply
  14   * increments a signal counter and prints out its value.
  15   *
  16   * Before starting the timer, the program puts the terminal in "raw mode".
  17   * This means that it does not echo input characters, wait for a newline to
  18   * complete an input, or respond to interrupt characters.
  19   *
  20   * After starting the timer and setting up the handler, the main function waits
  21   * for the user to type the character 'q' to quit.  Since the terminal is in
  22   * raw mode, typing 'q' is the only way to stop, since all other printable
  23   * characters are ignored, and terminal interrupt characters are disabled.
  24   *
  25   */
  26
  27
  28  /**
  29   * The ticks variable counts the number of one-second ticks since the program
  30   * started.
  31   */
  32  static int ticks;
  33
  34  /**
  35   * The tick function is the handler for SIGALRM.
  36   */
  37  void tick(int sig) {
  38      printf("%d\r", ++ticks);
  39  }
  40
  41  /**
  42   * Set the terminal to "raw" input mode.  This is defined by setting terminal
  43   * control flags to noncanonical mode, turning off character echoing, and
  44   * ignoring signaling characters.  Before setting to raw mode, save the
  45   * current mode so it can be restored later.  After setting, return the
  46   * saved mode.
  47   *
  48   * For explanatory details, see Sections 18.10, 18.11 of Stevens, the
  49   * termio(7I) man page, and the tcsetattr(3C) man page.  (To see a particular
  50   * man page section, use the "-s" argument, e.g., "man -s 7I termio" on
  51   * falcon/hornet.)
  52   */
  53  struct termios set_raw_term_mode() {
  54      struct termios cur_term_mode, raw_term_mode;
  55
  56      tcgetattr(STDIN_FILENO, &cur_term_mode);
  57      raw_term_mode = cur_term_mode;
  58      raw_term_mode.c_lflag &= ~(ICANON | ECHO | ISIG) ;
  59      raw_term_mode.c_cc[VMIN] = 1 ;
  60      raw_term_mode.c_cc[VTIME] = 0;
  61      tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw_term_mode);
  62
  63      return cur_term_mode;
  64  }
  65
  66  /**
  67   * Restore the terminal mode to that saved by set_raw_term_mode.
  68   */
  69  void restore_term_mode(struct termios saved_term_mode) {
  70      tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_term_mode);
  71  }
  72
  73  /**
  74   * Set the terminal to raw mode, set up a one-second timer, set up the SIGARLM
  75   * handler, and then wait for the user to type 'q'.
  76   *
  77   * For details of timer setup, see Stevens Section 6.1 and the man pages for
  78   * setitimer(2) and gettimeofday(3C).
  79   *
  80   * For details of signal setup, see Stevens Section 10.14, and the man page for
  81   * sigaction.
  82   */
  83  int main(int argc, char** argv) {
  84
  85      struct termios saved_term_mode;      /* saved entering terminal mode */
  86      struct itimerval tbuf;               /* interval timer structure */
  87      struct sigaction action;             /* signal action structure */
  88
  89      /*
  90       * Explain to the user how to quit.
  91       */
  92      printf("Type 'q' to quit.\n\n");
  93
  94      /*
  95       * Initialize ticks to 0.
  96       */
  97      ticks = 0;
  98
  99      /*
 100       * Set up the SIGALRM handler.
 101       */
 102      action.sa_handler = tick;     /* set tick to be the handler function */
 103      sigemptyset(&action.sa_mask); /* clear out masked functions */
 104      action.sa_flags   = 0;        /* no special handling */
 105
 106      /*
 107       * Use the sigaction function to associate the signal action with SIGALRM.
 108       */
 109      if (sigaction(SIGALRM, &action, NULL) < 0 ) {
 110          perror("SIGALRM");
 111          exit(-1);
 112      }
 113
 114      /*
 115       * Define a one-second timer.
 116       */
 117      tbuf.it_interval.tv_sec  = 1;
 118      tbuf.it_interval.tv_usec = 0;
 119      tbuf.it_value.tv_sec  = 1;
 120      tbuf.it_value.tv_usec = 0;
 121
 122      /*
 123       * Use the setitimer function to start the timer.
 124       */
 125      if ( setitimer(ITIMER_REAL, &tbuf, NULL) == -1 ) {
 126          perror("setitimer");
 127          exit(-1);                   /* should only fail for serious reasons */
 128      }
 129
 130      /*
 131       * Set the terminal to raw mode.
 132       */
 133      saved_term_mode = set_raw_term_mode();
 134
 135      /*
 136       * Busy wait until the user types 'q'.
 137       */
 138      while (getchar() != 'q') {}
 139
 140      /*
 141       * Restore the terminal to the mode it was in at program entry.
 142       */
 143      restore_term_mode(saved_term_mode);
 144
 145  }

  1. There are three actions a process can take when a signal occurs.
    1. Ignore the signal.
      1. This works for most signals.
      2. Two that can never be ignored are SIGKILL and SIGSTOP.
      3. This allows the super user always to be able to terminate or stop any process.
    2. Catch the signal.
      1. This is done by associating a function as the handler of a signal.
      2. The general way to do this is with the sigaction function, illustrated in the example above.
      3. There is also a function named "signal" to set up a handler, but sigaction supersedes it.
      4. Note that SIGKILL and SIGSTOP cannot be caught.
    3. Let the default action apply.
      1. All signals have a default action.
      2. For most it's to terminate the signaled process.
      3. Table 10.1 on Page 292 defines the default action for all signals.

  2. A compendium of UNIX signals.
    1. Different variants of UNIX define different signals.
    2. A good deal of them are common to all UNIX implementations.
    3. Table 10.1 on Page 292 lists all the POSIX signals, plus those defined in the four reference OS implementations covered in Stevens (i.e., FreeBSD, Linux, Mac OS, Solaris).
    4. Pages 293-298 provide a very nice summary of all the signals, including what they are used for.

  3. Details of the sigaction function (Section 10.14).
    1. Signature:
      int sigaction(int sig, const struct sigaction *restrict act,
                    struct sigaction *restrict oact);
      
    2. Arguments:
      1. The first argument sig is the signal whose action is being examined or modified.
      2. If the second act argument is non-null, the action is being modified.
      3. If the third oact argument is non-null, the system returns the previous action for the signal.
    3. The struct definition is:
      struct sigaction {
          union {                              /* handling function */
              void (*sa_handler)(int);         /*
              void (*sa_sigaction)(
                    int, siginfo_t*, void*);
          } funcptr;
          int sa_flags;                        /* options flags */
          sigset_t sa_mask;                    /* signals to block */
      }
      

  4. Noteworthy signals (Section 10.2).
    1. SIGKILL -- kill a process; cannot be caught or ignored
    2. SIGSTOP -- stop a process; cannot be caught or ignored
    3. SIGSEGV -- invalid memory reference, leading to "Segmentation fault"
    4. SIGBUS -- implementation-defined hardware fault, leading to "Bus error"
    5. SIGTERM -- generated by kill(1), by default
    6. SIGINT -- sent by terminal driver in response to Control-C
    7. SIGTSTP -- sent by terminal driver in response to Control-Z
    8. SIGCHLD -- sent to parent when child terminates
    9. SIGALRM -- generated by calling alarm or setitimer
    10. SIGUSR1 and SIGUSR2 -- user-defined, for use in application programs

  5. The signal function and unreliable signals (Sections 10.3 and 10.4).
    1. signal is an older interface to signal features, superseded by sigaction.
    2. The signature is
      typedef void Sigfunc(int);
      Sigfunc* signal(int, Sigfunc*);
      
    3. Using signal in its original form leads to unreliable signal handling, as discussed in Section 10.4.
    4. The sigaction function, as standardized by POSIX, provides reliable signal handling.
    5. Some UNIX systems implement signal using sigaction, making signal just a simpler interface to reliable signal processing (e.g., Mac OS X).
    6. Other UNIX systems implement signal in its original unreliable form, presumably for backwards compatibility with older code (e.g., Solaris).
    7. To avoid any possibility of unreliable signal handling, sigaction should always be used, either directly or in a re-implementation like the one in Section 10.14, Figure 10.18 (Page 328).

    We'll return to some additional signal topics from chapter 10 in a later lecture; in the meantime, we'll introduce the important concept of pipes, which you will use in Lab 7, and the upcoming programming assignment.

  6. Using pipes for interprocess communication (IPC) (Chapter 15).
    1. So we've seen how to start multiple processes, and have them communicate with signals.
    2. However, we've seen no way for processes to share data, other than through files that remain open across a fork or exec.
    3. Chapter 15 of Stevens discusses a number of ways for processes to communicate data by other means than the file system, i.e., to perform interprocess communication.
    4. We'll start our look at IPC with the oldest and simplest form -- pipes.

  7. Pipe basics (Section 15.2).
    1. At the shell level, we've seen the use of pipes with the '|' operator, as in
      ps -fe | grep "gfisher"
      
      which pipes the output of "ps" through grep to find all processes owned by "gfisher".
    2. At the program level, the same kind of pipe can be created between two processes.
    3. A pipe is created using the pipe function:
      int pipe(int filedes[2]);
      
      1. The function takes a two-element array of file descriptors.
      2. It returns two file descriptors in the array, such that
        1. filedes[0] is open for reading, as the read end of the pipe
        2. filedes[1] is open for writing, as the write end of the pipe
        3. I.e., the output of filedes[1] is the input for filedes[0].
    4. Pipes have two limitations as a data communication channel:
      1. they're half-duplex, meaning data only flow in one direction
      2. pipes can only be used by processes that have a common ancestor, most typically between a parent and child process
    5. The book has some helpful illustrations and examples on pages 497-503.
    6. The following is a "hello world" example of using pipes, where a parent process sends data down a pipe that is read by a child process; it's in Figure 15.5 on Page 499.

         1  #include <unistd.h>
         2  #include <stdlib.h>
         3  #include <stdio.h>
         4  #include <string.h>
         5
         6  #define MAXLINE 100
         7  #define err_sys(msg) perror(msg); exit(-1)
         8
         9  int main() {
        10      int n;              /* number of bytes read from the pipe */
        11      int fd[2];          /* the pipe read/write file descriptors */
        12      pid_t pid;          /* pid of the child process */
        13      char line[MAXLINE]; /* line of input read from the pipe */
        14
        15      /*
        16       * Create a pipe.
        17       */
        18      if (pipe(fd) < 0) {
        19          err_sys("pipe");
        20      }
        21
        22      /*
        23       * Fork a child process.
        24       */
        25      if ((pid = fork()) < 0) {
        26          err_sys("fork");
        27      }
        28
        29      /*
        30       * Parent writes down the pipe, closing the unneeded read end of the pipe.
        31       */
        32      else if (pid > 0) {
        33          close(fd[0]);
        34          write(fd[1], "Hello world0, strlen("Hello world0));
        35      }
        36
        37      /*
        38       * Child reads the pipe, closing the unneded write end.  It then writes the
        39       * read data to stdout, just for confirmation.
        40       */
        41      else {
        42          close(fd[1]);
        43          n = read(fd[0], line, MAXLINE);
        44          write(STDOUT_FILENO, line, n);
        45      }
        46      exit(0);
        47  }
      
    7. The following example shows how to use a pipe to read the standard output of a program that is exec'd in a child; this example is directly relevant to Lab 7.

         1  /**
         2   * Exec a program and read the first 80 chars of its stdout.
         3   */
         4
         5  #include <stdio.h>
         6  #include <unistd.h>
         7  #include <stdlib.h>
         8  #include <sys/wait.h>
         9
        10  #define READ_SIZE 80
        11
        12  int main(int argc, char** argv) {
        13
        14      int fd[2];
        15      int pid;
        16      char read_buf[READ_SIZE + 1];
        17      int chars_read;
        18
        19      /*
        20       * Check for adequate args.
        21       */
        22      if (argc < 1) {
        23          fprintf(stderr, "usage: grab-stdout prog arg ...");
        24          exit(-1);
        25      }
        26
        27      /*
        28       * Create a pipe that will have its output written to by the executed
        29       * program.
        30       */
        31      pipe(fd);
        32
        33      /*
        34       * Fork off a process to exec the program.
        35       */
        36      if ((pid = fork()) < 0) {
        37          perror("fork");
        38    exit(-1);
        39      }
        40
        41      /*
        42       * Forked child has its stdout be the write end of the pipe.
        43       */
        44      else if (pid == 0) {
        45
        46          /*
        47           * Don't need read end of pipe in child.
        48           */
        49          close(fd[0]);
        50
        51          /*
        52           * This use of dup2 makes the output end of the pipe be stdout.
        53           */
        54          dup2(fd[1], STDOUT_FILENO);
        55
        56          /*
        57           * Don't need fd[1] after the dup2.
        58           */
        59          close(fd[1]);
        60
        61          /*
        62           * Exec the program given in the command line, including any args.
        63           */
        64          execvp(argv[1], &(argv[1]));
        65          perror("exec");
        66          exit(-1);
        67      }
        68
        69      /*
        70       * Parent takes its input from the read end of the pipe.
        71       */
        72      else {
        73          /*
        74           * Don't need write end of pipe in parent.
        75           */
        76          close(fd[1]);
        77
        78          /*
        79           * Read the data coming in on the read end of the pipe.
        80           */
        81          chars_read = read(fd[0], read_buf, READ_SIZE);
        82
        83          /*
        84           * Print data, if any read, otherwise print "no output" message.
        85           */
        86          if (chars_read > 0) {
        87
        88              /*
        89               * Null terminate the read string.
        90               */
        91              read_buf[chars_read] = '\0';
        92
        93              /*
        94               * Print the string out, for confirmation.
        95               */
        96              printf("read_buf:\n%s\n", read_buf);
        97          }
        98
        99          else {
       100              fprintf(stderr, "Program produced no output.\n");
       101              exit(-1);
       102          }
       103      }
       104
       105      exit(0);
       106  }
      

    8. The preceding two examples are in the 357 examples directory, in the files hello-pipe.c and grab-stdout.c.



index | lectures | labs | programs | handouts | solutions | examples | documentation | bin

    

Footnotes:

1 These two signals are good examples of how "reasonably" mnemonic signal names are; SIGINT is the INTerrupt signal, that interrupts and typically terminates a process started from a shell terminal; SIGTSTP is the Terminal SToP signal, that suspends a shell process, that can subsequently be restarted.

2 The name "kill" for the system function is a bit of a misnomer, since it does not explicitly kill a process, but rather sends it any signal; hence, "sigsend" might be a better name.