COMS 4995 Advanced Systems Programming

Signals

Process groups and job control

Consider the following shell session, where we start off a long-running pipeline:

$ proc1 | proc2  # we don't get the shell back while this runs

Let’s say that we know it’s going to take a very long time to complete and we want to do other work. What can we do?

After issuing the following commands:

$ proc1 | proc2 &  # send pipeline to background 
[1] 7106
$ proc3 | proc4 | proc5  # we have our shell, start another pipeline

You have:

Figure 9.7, APUE

A session with a controlling terminal – there is exactly one foreground process group and multiple background process groups.

[1] 7106 refers to the job# and leading pid of the backgrounded pipeline. More job control:

Signals and system calls

Sending signals

#include <signal.h>

int kill(pid_t pid, int signo);

int raise(int signo);

Alternatively, use terminal-generated signals:

Background process groups don’t receive these job control signals.

signal()

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

Sets disposition of signum to handler, where handler can be:

Portability (from man 2 signal):

The only portable use of signal() is to set a signal's disposition to SIG_DFL or
SIG_IGN.  The semantics when using signal() to establish a signal handler vary
across systems (and POSIX.1 explicitly permits this variation); do not use it
for this purpose.

Unreliable signals and read()

The following program, from APUE section 1.9, adds signal handling to the simple shell program we studied before:

#include "apue.h"
#include <sys/wait.h>

static void	sig_int(int);		/* our signal-catching function */

int main(void)
{
    char	buf[MAXLINE];	/* from apue.h */
    pid_t	pid;
    int		status;

    if (signal(SIGINT, sig_int) == SIG_ERR)
        err_sys("signal error");

    printf("%% ");	/* print prompt (printf requires %% to print %) */
    while (fgets(buf, MAXLINE, stdin) != NULL) {
        if (buf[strlen(buf) - 1] == '\n')
            buf[strlen(buf) - 1] = 0; /* replace newline with null */

        if ((pid = fork()) < 0) {
            err_sys("fork error");
        } else if (pid == 0) {		/* child */
            execlp(buf, buf, (char *)0);
            err_ret("couldn't execute: %s", buf);
            exit(127);
        }

        /* parent */
        if ((pid = waitpid(pid, &status, 0)) < 0)
            err_sys("waitpid error");
        printf("%% ");
    }
    exit(0);
}

void sig_int(int signo)
{
    printf("interrupt\n%% ");
}

Portability issues:

  1. “Slow” system calls may get interrupted on signals
    • Slow underlying read() syscall gets interrupted. errno set to EINTR, causes fgets() to return NULL.
      • errno is a global variable set to the ID of the last error
      • some OS kernels (e.g., FreeBSD, macOS) automatically restart the interrupted syscall
    • Hotfix: check EINTR and manually restart the syscall
      • ideally, specify restart semantics in handler installation instead of doing it manually
  2. Signals get lost
    • Disposition set with Linux signal() resets after each signal
      • some OS kernels (e.g., FreeBSD, macOS) do not reset disposition after each signal handling
    • Hotfix: Set disposition again after detecting EINTR
      • but now there’s a race condition: what if we get another signal before we set disposition?
      • ideally, specify disposition reset semantics in handler installation instead of doing it manually

Reentrancy issues: can’t call certain functions in signal handlers

Unreliable signals and alarm()/pause()

#include <unistd.h>

unsigned int alarm(unsigned int seconds);
        // Returns: 0 or number of seconds until previously set alarm

int pause(void);
        // Returns: –1 with errno set to EINTR

alarm(): generate SIGALRM after seconds

pause(): suspend program execution indefinitely

Can implement sleep() via alarm()/pause(), but there are many subtleties, see APUE 10.10.

We can use alarm() to implement a slow read() with timeout:

#include "apue.h"

static void sig_alrm(int);

int main(void)
{
    int     n;
    char    line[MAXLINE];

    if (signal(SIGALRM, sig_alrm) == SIG_ERR)
        err_sys("signal(SIGALRM) error");

    alarm(10);
    if ((n = read(STDIN_FILENO, line, MAXLINE)) < 0)
        err_sys("read error");
    alarm(0);

    write(STDOUT_FILENO, line, n);
    exit(0);
}

static void sig_alrm(int signo)
{
    /* nothing to do, just return to interrupt the read */
}

Two problems:

  1. This doesn’t work if slow system calls are automatically restarted
  2. A race condition: the alarm can be missed between alarm(10) and read()
    • One solution: select() – advanced I/O next week

Towards a portable solution

Signal sets

#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signo);

int sigdelset(sigset_t *set, int signo);
       // All four above return: 0 if OK, -1 on error

int sigismember(const sigset_t *set, int signo);
       // Returns: 1 if true, 0 if false, -1 on error

sigset_t is another opaque type – only manipulate using above functions.

A possible implementation – bit mask:

// sigset_t is just a typedef for some integer type, e.g., uint64_t

#define sigemptyset(ptr)  (*(ptr) = 0)
#define sigfillset(ptr)   (*(ptr) = ~(sigset_t)0, 0)

/*
* <signal.h> usually defines NSIG to include signal number 0.
*/
#define SIGBAD(signo)   ((signo) <= 0 || (signo) >= NSIG)

int sigaddset(sigset_t *set, int signo)
{
    if (SIGBAD(signo)) {
        errno = EINVAL;
        return(-1);
    }
    *set |= 1 << (signo - 1);       /* turn bit on */
    return(0);
}

int sigdelset(sigset_t *set, int signo)
{
    if (SIGBAD(signo)) {
        errno = EINVAL;
        return(-1);
    }
    *set &= ~(1 << (signo - 1));    /* turn bit off */
    return(0);
}

int sigismember(const sigset_t *set, int signo)
{
    if (SIGBAD(signo)) {
        errno = EINVAL;
        return(-1);
    }
    return((*set & (1 << (signo - 1))) != 0);
}

sigaction()

Portable (POSIX-compliant) version of signal():

#include <signal.h>

int sigaction(int signo, const struct sigaction *restrict act,
                struct sigaction *restrict oact);

        // Returns: 0 if OK, -1 on error

struct sigaction {
    void     (*sa_handler)(int);  /* addr of signal handler, */
                                  /* or SIG_IGN, or SIG_DFL */
    sigset_t sa_mask;             /* additional signals to block */
    int      sa_flags;            /* signal options, Figure 10.16 */
    /* alternate handler */
    void     (*sa_sigaction)(int, siginfo_t *, void *);
};

An installed action stays installed until otherwise changed with sigaction()

sigset_t sa_mask: additional signals to block while signo is being handled with sa_handler

int sa_flags: handling options – some notable ones:

APUE Figure 10.18 reimplements signal() via sigaction() with reasonable (and portable) semantics:

#include "apue.h"

Sigfunc *signal(int signo, Sigfunc *func)
{
    struct sigaction    act, oact;

    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    if (signo == SIGALRM) {
#ifdef  SA_INTERRUPT
        act.sa_flags |= SA_INTERRUPT;
#endif
    } else {
        act.sa_flags |= SA_RESTART;
    }
    if (sigaction(signo, &act, &oact) < 0)
        return(SIG_ERR);
    return(oact.sa_handler);
}

APUE Figure 10.19 implements a portable signal() that never restarts slow system calls:

#include "apue.h"

Sigfunc *signal_intr(int signo, Sigfunc *func)
{
    struct sigaction    act, oact;

    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
#ifdef SA_INTERRUPT
    act.sa_flags |= SA_INTERRUPT;
#endif
    if (sigaction(signo, &act, &oact) < 0)
        return(SIG_ERR);
    return(oact.sa_handler);
}

More signal management

Signal mask: set of signals currently blocked from delivery

#include <signal.h>

int sigprocmask(int how, const sigset_t *restrict set,
                sigset_t *restrict oset);

        // how: SIG_BLOCK, SIG_UNBLOCK, or SIG_SETMASK
        // Returns: 0 if OK, -1 on error

int sigpending(sigset_t *set);

        // Returns: 0 if OK, -1 on error

sigprocmask(): manipulate process’s signal mask

sigpending(): retrieve a set of pending signals that are blocked from delivery


Last updated: 2024-02-04