Signals - Fundamental Concepts

  • The various different signals and their purposes
  • the circumstances in which the kernel may generate a signal for a process, and the syscall that one process may use to send a signal to another process
  • how a process responds to a signal by default, and the means by which a process can change its response to a signal
  • the use of a process signal mask to block signals, and the associated notion of pending signals
  • how a process can suspend execution and wait for the delivery of a signal

Concepts and Overview

A signal is a notification to a process that an event has occurred. Signals are sometimes described as software interrupts. Signals are analogous to hardware interrupts in that they interrupt the normal flow of execution of a program.

Cases that kernel send signals to process:

  • A hardware exception occurred, meaning that the hardware detected a fault condition that was notified to the kernel
  • The user typed on of the terminal special characters that generate signals (ctrl+c, crtl+z)
  • A software event occurred

Each signal is defined as a unique integer, starting sequentially from 1. (<signal.h>, SIGxxxx)

A pending signal is not always delivered to a process as soon as it is next scheduled to run.

Upon delivery of a signal, a process carries out one of the following default actions:

  • The signal is ignored
  • The process is terminated
  • A core dump file is generated, and the process is terminated
  • The process is stopped-execution of the process is suspended
  • Execution of the process is resumed after previously being stopped

Signal types and default actions

Signal Serial number desc default
SIGABRT 6 Abort process core
SIGALRM 14 Real-time timer expired term
SIGBUS 7 Memory access error core
SIGCHLD 17 Child terminated or stopped ignore
SIGCONT 18 Continue if stopped cont
SIGEMT undef Hardware fault term
SIGFPE 8 Arithmetic exception core
SIGHUG 1 Hangup term
SIGILL 4 Illegal instruction core
SIGINT 2 Terminal interrupt term
SIGIO / SIGPOLL 29 I/O possible term
SIGKILL 9 Sure Kill term
SIGPIPE 13 Broken pipe term
SIGPROF 27 Profiling timer expired term
SIGPWR 30 Power about to fail term
SIGQUIT 3 Terminal quit core
SIGSEGV 11 Invalid memory reference core
SIGTKFLT 16 Stack fault on coprocessor term
SIGSTOP 19 Sure stop stop
SIGSYS 31 Invalid system call core
SIGTERM 15 Terminate process term
SIGTRAP 5 Trace/breakpoint trap core
SIGTSTP 20 Terminal stop stop
SIGTTIN 21 Terminal read from BG stop
SIGTTOU 22 Terminal write from BG stop
SIGTURG 23 Urgent data on socket ignore
SIGUSR1 10 User-defined signal 1 term
SIGUSR2 12 User-defined signal 2 term
SIGVTALRM 26 Virtual timer expired term
SIGWINCH 28 Terminal window size change ignore
SIGXCPU 24 CPU time limit exceeded core
SIGXFSZ 25 File size limit exceeded core

Changing Signal Dispositions: signal()

1
2
3
#include <signal.h>

void (*signal(int sig, void(*handler)(int)))(int); // returns previous signal on success, or SIG_ERR on error

Simple Sampler

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void (*oldHandler)(int);

oldHandler = signal(SIGINT, newHandler);

if (oldHandler == SIG_ERR) {
    errExit("signal");
}
// do some work here
if (signal(SIGINT, oldHandler) == SIG_ERR) {
    errExit("signal");
}

Introduction to Signal Handlers

调用信号处理程序可能会随时打断主程序流程;内核代表进程来调用信号处理程序,当程序返回是,主程序会在被打断的地方恢复执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <signal.h>
#include "tlpi_hdr.h"

static void sigHandler(int sig)
{
    printf("Ouch!\n"); /* UNSAFE (see Section 21.1.2) */
}

int main(int argc, char *argv[])
{
    int j;

    /* Establish handler for SIGINT. Here we use the simpler signal()
       API to establish a signal handler, but for the reasons described in
       Section 22.7 of TLPI, sigaction() is the (strongly) preferred API
       for this task. */
    if (signal(SIGINT, sigHandler) == SIG_ERR)
        errExit("signal");

    /* Loop continuously waiting for signals to be delivered */
    for (j = 0;; j++)
    {
        printf("%d\n", j);
        sleep(3); /* Loop slowly... */
    }
}

Sending signals: kill()

1
2
3
#include <signal.h>

int kill(pid_t pid, int sig);

pid 的处理逻辑

  1. 如果 pid 大于 0,发送至 pid 指定的进程
  2. 如果 pid 等于 0,发送至与调用进程同组的每个进程
  3. 如果小于-1,那么会向组 id 等于该 pid 绝对值的进程组内所有子进程发送信号
  4. 如果 pid 等于-1,那么信号的发送范围:调用进程有权将信号发往每个目标进程,出去 init 和调用者本身

kill 所需权限

  1. 特权级(CAP_KILL)进程可以向任何进程发送信号
  2. init 是特例,仅能接收已配置 SigHandler 的信号
  3. 如果发送者的实际或有效用户 ID 匹配于接收者的实际用户 ID 活着保存设置用户 ID,那么非特权进程也可以向另一进程发送信号

Checking for the existence of a process

可以通过 kill(pid, 0) 来检查进程是否存在。

  • 发送空信号失败,且 errno 为 ESRCH,则证明目标进程不存在
  • 发送空信号失败,且 errno 为 EPERM,或调用成功,则表示进程存在

Other ways of sending signals: raise() and killpg()

1
2
3
#include <signal.h>

int raise(int sig);
  • single-threaded program: kill(getpid(), sig);
  • multi-threaded program: pthread_kill(pthread_self(), sig);

killpg() sends a signal to all of the members of a process group. kill(-pgrp, sig)

1
2
3
#include <signal.h>

int killpg(pid_t pgrp, int sig);

Displaying signal descriptions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#define _BSD_SOURCE
#include <signal.h>

extern const char* const sys_siglist[];

#define _GNU_SOURCE
#include <string.h>

char *strsignal(int sig);

void psignal(int sig, const char* msg);

Each signal has an associated printable description, those are listed in the array sys_siglist.

  • strsignal performs bounds checking on the sig argument, and then returns a pointer to a printable description of the signal, or a pointer to an error string if the signal number was invalid.
  • psignal displays the string given in its argument msg.

Signal Sets

Multiple signals are represented using a data structure called a signal set, provided by the system data type sigset_t.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <signal.h>

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set);
int sigdelset(sigset_t *set);
int sigismember(const sigset_t *set, int sig);

#define _GNU_SOURCE
int sigandset(sigset_t *dest, sigset_t *left, sigset_t *right); // intersection
int sigorset(sigset_t *dest, sigset_t *left, sigset_t *right); // union
int sigisemptyset(const sigset_t *set); // check is empty
  • sigemptyset initializes a signal set to contain no members
  • sigfillset initializes a set to contain all signals (including all realtime signals)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/* signal_functions.c

   Various useful functions for working with signals.
*/
#define _GNU_SOURCE
#include <string.h>
#include <signal.h>
#include "signal_functions.h" /* Declares functions defined here */
#include "tlpi_hdr.h"

/* NOTE: All of the following functions employ fprintf(), which
   is not async-signal-safe (see Section 21.1.2). As such, these
   functions are also not async-signal-safe (i.e., beware of
   indiscriminately calling them from signal handlers). */

void /* Print list of signals within a signal set */ printSigset(FILE *of, const char *prefix, const sigset_t *sigset)
{
    int sig, cnt;

    cnt = 0;
    for (sig = 1; sig < NSIG; sig++)
    {
        if (sigismember(sigset, sig))
        {
            cnt++;
            fprintf(of, "%s%d (%s)\n", prefix, sig, strsignal(sig));
        }
    }

    if (cnt == 0)
        fprintf(of, "%s<empty signal set>\n", prefix);
}

int /* Print mask of blocked signals for this process */ printSigMask(FILE *of, const char *msg)
{
    sigset_t currMask;

    if (msg != NULL)
        fprintf(of, "%s", msg);

    if (sigprocmask(SIG_BLOCK, NULL, &currMask) == -1)
        return -1;

    printSigset(of, "\t\t", &currMask);

    return 0;
}

int /* Print signals currently pending for this process */ printPendingSigs(FILE *of, const char *msg)
{
    sigset_t pendingSigs;

    if (msg != NULL)
        fprintf(of, "%s", msg);

    if (sigpending(&pendingSigs) == -1)
        return -1;

    printSigset(of, "\t\t", &pendingSigs);

    return 0;
}

The signal mask (Blocking Signal Delivery)

For each process, the kernel maintains a signal mask- a set of signals whose delivery to the process is currently blocked. If a signal that is blocked is sent to a process, delivery of that signal is delayed untial it is unblocked by being removed from the process signal mask.

add signal to the signal mask:

  • When a signal handler is invoked, the signal that caused its invocation can be automatically added to the signal mask
  • When a signal handler is established with sigaction, it is possible to specify an additional set of signals that are to be blocked when the handler is invoked
1
2
3
#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

how determines the changes that sigprocmask makes to the signal mask:

  • SIG_BLOCK -> the signal mask is set to the union of its current value and set
  • SIG_UNBLOCK -> remove the signal set from the signal mask
  • SIT_SETMASK -> assign the set to signal mask

Temporarily blocking delivery of a signal

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
sigset_t blockSet, prevMask;

sigemptyset(&blockSet);
sigaddset(&blockSet, SIGINT);

if (sigprocmask(SIG_BLOCK, &blockSet, &prevMask) == -1) {
    errExit("sigprocmask1");
}

// do your own work

// restore previous signal mask
if (sigprocmask(SIG_SETMASK, &prevMask, NULL) == -1) {
    errExit("sigprocmask2");
}

Pending signals

If a process receives a signal that it is currently blocking, that signal is added to the process’s set of pending signals. When (and if) the signal is later unblocked, it is then delivered to the proess.

sigpending determines which signals are pending.

1
2
3
#include <signal.h>

int sigpending(sigset_t *set);

Signals are not queued

The set of pending signals is only a mask; it indicates whether or not a signal has occurred, but not how many times it has occurred. (Receive Many, Deliver Once)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <signal.h>
#include "tlpi_hdr.h"
int main(int argc, char *argv[])
{
    int numSigs, sig, j;
    pid_t pid;

    if (argc < 4 || strcmp(argv[1], "--help") == 0)
        usageErr("%s pid num-sigs sig-num [sig-num-2]\n", argv[0]);
    pid = getLong(argv[1], 0, "pid]");
    numSigs = getInt(argv[2], GN_GT_0, "num-sigs");
    sig = getInt(argv[3], 0, "sig-num");

    printf("%s: sending signal %d to process %ld %d times\n",
           argv[0], sig, (long)pid, numSigs);
    for (j = 0; j < numSigs; j++)
    {
        if (kill(pid, sig) == -1)
        {
            errExit("kill");
        }
    }

    /* If a fourth command-line argument was specified, send that signal */

    if (argc > 4)
        if (kill(pid, getInt(argv[4], 0, "sig-num-2")) == -1)
            errExit("kill");

    printf("%s: exiting\n", argv[0]);
    exit(EXIT_SUCCESS);
}

Changing signal dispositions: sigaction()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <signal.h>

struct sigaction {
    void (*sa_handler)(int); // address of handelr
    sigset_t sa_mask; // signals blocked during handler invocation
    int sa_flags; // flags controlling handler invocation
    void (*sa_resotrer)(void); // not for application use
}

int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);

The sig argument identifies the signal whose disposition we want to retrieve or change. This argument can be any signal except SIGKILL or SIGSTOP.

Waiting for a signal: pause()

Calls pause() suspend execution of the process until the call is interrupted by a signal handler (or until an unhandled signal terminates the process)

1
2
3
#include <unistd.h>

int pause(void);

Summary

A signal is a notification that some kind of event has occurred, and may be sent to a process by the kernel, by another process, or by itself. There is a range of standard signal types, each of which has a unique number and purpose.

Signal delivery is typically asynchronous, meaning that the point at which the signal interrupts execution of the process is unpredictable. In some cases (hardware-generated signals), signals are delivered synchronously, meaning that delivery occurs predictably and reproducibly at a certain point in the execution of a program.

By default, a signal either is ignored, terminated a process (optional core dump), stops a running process, or restarts a stopped process. The particular default action depends on the signal type. Alternatively, a program can use signal() or sigaction() to explicitly ignore a signal or to establish a programmer-defined signal handelr function that is invoked when the signal is delivered. For portability reasons, establishing a signal handler is best performed using sigaction().

A process (with suitable permissions) can send signal to another process using kill(). Sending the num signal(0) is a way of determining if a particular process ID is in use.

Each process has a signal mask, which is the set of signals whose delivery is currently blocked. Signals can be aded to and removed from the signal mask using sigprocmask().

If a signal is received while it is blocked, then it remains pending until it is unblocked. Standard signals can’t be queued; that is, a signal can be marked as pending (and thus later delivered) only once. A process can use the sigpending() syscall to retrieve a signal set (a data structure used to represent multiple different signals) identifying the signals that it has pending.

The sigaction() syscall provides more control and flexibility than signal() when setting the disposition of a signal. First, we can specify a set of additional signals to be blocked when a handler is invoked. In addition, various flags can be used to control the actions that occur when a signal handler is invoked. For example, there are flags that select the older unreliable signal semantics (not blocking the signal causing invocation of a handler, and having the disposition of the signal reset to its default before the handler is called).

Using pause(), a process can suspend execution until a signal arrives.

Licensed under CC BY-NC-SA 4.0
Get Things Done
Built with Hugo
Theme Stack designed by Jimmy