Type kill -L and you’ll see all the standard signals available on Linux — 31 in total.

$ kill -L
 1 HUP      2 INT      3 QUIT     4 ILL      5 TRAP     6 ABRT     7 BUS
 8 FPE      9 KILL    10 USR1    11 SEGV    12 USR2    13 PIPE    14 ALRM
15 TERM    16 STKFLT  17 CHLD    18 CONT    19 STOP    20 TSTP    21 TTIN
22 TTOU    23 URG     24 XCPU    25 XFSZ    26 VTALRM  27 PROF    28 WINCH
29 POLL    30 PWR     31 SYS

This time, let’s try to trigger these signals in their intended scenarios.

Classifying Signals

Signals are a process-level asynchronous notification mechanism — an OS-level encapsulation of hardware and software interrupts. A signal’s asynchronous nature derives from the asynchronous nature of interrupts. Based on their source and purpose, we can classify the 31 signals into three categories:

  • Job Control: Signals for controlling process execution states. These originate from user input or other processes.
  • Error Handling: Signals generated when a process performs improper operations. These originate from the process itself.
  • Asynchronous Operations: Signals generated to notify the completion of certain asynchronous operations. These originate from external devices.

glibc provides a more detailed classification, but three categories is simpler.

Here’s how the signals map to these categories:

  • Job Control
    • HUP (hang up): Session terminated, requesting process termination
    • INT (interrupt): User requests the program to stop the current operation
    • QUIT (quit): User requests program termination due to abnormality
    • TRAP (trap): Program trapped by debugger control
    • KILL (kill): User forces program termination
    • USR1/USR2 (user): User-defined requests
    • TERM (terminate): User requests program termination
    • CHLD (child): Notifies parent that a child process has terminated or stopped
    • CONT (continue): Requests the current process to continue
    • STOP (stop): Requests the current process to pause
    • TSTP (terminal stop): Keyboard request to pause the current process
    • TTIN (tty input): Background process attempted to read from terminal
    • TTOU (tty output): Background process attempted to write to terminal
  • Error Handling
    • ILL (illegal instruction): Invalid instruction
    • ABRT (abort): Program called abort() to terminate due to error
    • BUS (bus): Bus error while accessing a device
    • FPE (float point exception): Floating-point error
    • SEGV (segment violation): Accessed an inaccessible address
    • PIPE (pipe): Wrote to a closed pipe/socket
    • SIGSTKFLT: “Coprocessor stack error” (unused)
    • XCPU (exceeds cpu limit): Process exceeded CPU time limit
    • XFSZ (exceeds file size): Process exceeded writable file size limit
    • PWR (power): Power about to be exhausted
    • SYS (system call): Invalid system call
  • Asynchronous Operations
    • ALRM (alarm): Timer expired
    • URG (urgent): Urgent data on socket
    • VTALRM (virtual alarm): Virtual timer expired
    • PROF (profile): Profiling timer expired
    • WINCH: Terminal window size changed
    • POLL: A file descriptor is ready for I/O

Job Control

(1) HUP

Opening a terminal emulator window, logging in via SSH, etc., all create a terminal session on the target host. Typically, a shell acts as the controlling process for this session. When we disconnect the session, the kernel sends SIGHUP to the controlling shell. The shell then forwards SIGHUP to all other processes it manages. The default disposition for SIGHUP is to terminate the process — so by default, disconnecting a session terminates all processes within it.

Here’s a program to receive SIGHUP:

// handle_hup.c
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

void handle_hup(int sig) {
    write(1, "HUP\n", 4);
    exit(1);
}

int main() {
    signal(SIGHUP, handle_hup);
    while (1) {
        pause();
    }
}

Start a session, run this program, then exit. Check out.txt — the process received SIGHUP.

$ ./handle_hup >out.txt &
$ exit
...
$ cat out.txt
HUP

(2) INT

When a process runs in the foreground, pressing Ctrl+C sends it a SIGINT signal. This signal means “stop the current operation” so the user can provide new input. For non-interactive programs, this is equivalent to terminating the process; but for interactive programs, it only means the current partial operation is interrupted.

An example is Python’s REPL mode. Enter an infinite loop in interactive mode, then press Ctrl+C. Notice that only the infinite loop is interrupted — the Python interpreter itself does not exit.

$ python3
Python 3.10.12 (main, Feb  4 2025, 14:57:36) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> while True:
...   pass
... 
^CTraceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyboardInterrupt
>>> 

(3) QUIT

When a process runs in the foreground, pressing Ctrl+\ sends it a SIGQUIT signal. The default disposition terminates the current process and generates a core dump. A scenario for using this signal might be when the user notices clearly abnormal behavior in a running program, such as an infinite loop. Terminating the process and inspecting the core dump is a good approach.

Take a simple infinite loop program as an example:

// loop.c
#include <stdio.h>

int main() {
    while (1) {
    }
    printf("Hello World\n");
    return 0;
}

Compile, run, then press Ctrl+\ to send SIGQUIT.

$ clang loop.c -o loop -g
$ ./loop 
^\Quit (core dumped)

Since my environment uses systemd-coredump, I can use coredumpctl to load core dump info directly in gdb:

$ coredumpctl gdb loop
...
#0  main () at main.c:4
4           while (1) {
(gdb) 

If you don’t have a core dump file, remember to adjust ulimit.

(4) TRAP

SIGTRAP is used by ptrace for implementing breakpoint debugging and system call tracing. However, I don’t know much about ptrace yet, so we’ll skip this one.

(5) KILL

When a process receives SIGKILL, it must die. Apart from using kill -9 <pid> or similar, there’s no other scenario that produces this signal. Note that SIGKILL is not the default signal sent by the kill command (SIGTERM is). Because SIGKILL cannot be caught, the process cannot perform any cleanup — it’s simply killed. This may leave resources unreleased.

The following program attempts to catch SIGKILL:

// try_handle_kill.c
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

void handle_kill(int signum) {
    write(1, "KILL\n", 5);
    exit(0);
}

int main() {
    signal(SIGKILL, handle_kill);

    while (1) {
        pause();
    }
    return 0;
}

Run it and send SIGKILL. The handle_kill() function is never executed.

$ ./try_handle_kill &
[1] 12345
$ kill -KILL $!
$ 
[1]+  Killed                  ./try_handle_kill

(6) USR1/USR2

SIGUSR1 and SIGUSR2 are reserved for user-defined purposes. You can assign custom meanings to these signals. However, real-time signals (32–63) might be a better choice for this.

The default disposition for SIGUSR1 and SIGUSR2 is to terminate the process — likely to prevent accidental use of unhandled SIGUSR1/SIGUSR2 signals.

$ ./loop &
[1] 12345
$ kill -USR1 14475
$ 
[1]+  User defined signal 1   ./loop

(7) TERM

SIGTERM is the default signal sent by kill. Unlike SIGKILL, a program can catch SIGTERM and perform additional cleanup before exiting; it can also temporarily block SIGTERM to prevent critical operations from being interrupted. This is very important — for instance, a database must maintain data integrity while writing, and interrupting this with SIGKILL could cause data corruption.

The following program blocks SIGTERM (and attempts to block SIGKILL) before a critical operation:

// test_mask.c
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void do_something_important() { sleep(2); }

int main() {
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGTERM);
    sigaddset(&mask, SIGKILL);
    sigprocmask(SIG_BLOCK, &mask, NULL);

    do_something_important();
    printf("Done\n");

    sigprocmask(SIG_UNBLOCK, &mask, NULL);

    while (1) {
        pause();
    }

    return 0;
}

If SIGTERM is sent while the program is running, the program handles it only after completing the critical operation:

$ ./test_mask & sleep 1; kill $! & wait
[1] 123456
[2] 654321
Done
[1]-  Terminated              ./test_mask
[2]+  Done                    kill $!

But if SIGKILL is sent, the program terminates immediately, despite our “blocking” of SIGKILL:

$ ./test_mask & sleep 1; kill -9 $! & wait
[1] 123456
[2] 654321
[1]-  Killed                  ./test_mask

(8) CHLD

When a child process terminates, stops, or continues, the parent receives a SIGCHLD signal.

This is useful in scenarios where a child process’s lifetime is unpredictable. The child could terminate at any time, but the parent, busy with its own work, can’t constantly call wait() to reap zombie children. By delivering child termination notifications via a signal, the parent can handle child process cleanup outside its main program flow.

This is similar to detached threads. However, threads can automatically release their own resources upon completion, whereas processes require the parent for complete cleanup.

When using SIGCHLD, you don’t need to worry about the parent terminating first. If the parent terminates first, the child’s parent automatically becomes the init process (pid=1). And init always reaps zombies.

This is similar to memory leaks — it’s only a problem while the process is running. Once the process terminates, its resources are released along with it.

The following program creates two child processes and registers a SIGCHLD handler. Inside the handler, we call waitpid() in a loop until no more zombie children remain.

// test_child.c
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>

void handle_child(int sig) {
    write(1, "CHLD\n", 5);
    while (waitpid(-1, NULL, WNOHANG) > 0) {
        write(1, "WAIT\n", 5);
    }
}

int main() {
    signal(SIGCHLD, handle_child);

    for (int i = 0; i < 2; i++) {
        int pid = fork();
        if (pid == 0) {
            while (1) {
                pause();
            }
            return 0;
        }
    }

    while (1) {
        pause();
    }

    return 0;
}

Run it. For the two child processes, we kill the first, stop the second, then resume it:

$ ./test_child &
$ ps
    PID TTY          TIME CMD
  12345 pts/0    00:00:00 bash
 123001 pts/0    00:00:00 test_child
 123002 pts/0    00:00:00 test_child
 123003 pts/0    00:00:00 test_child
 123456 pts/0    00:00:00 ps
$ kill 123002
CHLD
WAIT
$ ps
    PID TTY          TIME CMD
  12345 pts/0    00:00:00 bash
 123001 pts/0    00:00:00 test_child
 123003 pts/0    00:00:00 test_child
 123456 pts/0    00:00:00 ps
$ kill -STOP 123003
CHLD
$ kill -CONT 123003
CHLD

(9) CONT

SIGCONT is used to make a stopped process resume. If the process isn’t stopped, SIGCONT has no effect. You can register a handler for this signal — when the process resumes, it will first execute the handler code.

To resume, you first need to stop. So SIGCONT is used in conjunction with SIGSTOP or SIGTSTP.

The following program handles SIGCONT:

// test_cont.c
#include <signal.h>
#include <unistd.h>

void handle_cont(int signum) { write(1, "CONT\n", 5); }

int main() {
    signal(SIGCONT, handle_cont);

    while (1) {
        pause();
    }
    return 0;
}

First stop the process, then send SIGCONT to resume it. Note: if the process is already running, sending SIGCONT will still invoke the signal handler.

$ ./test_cont &
[1] 123456
$ kill -STOP $!

[1]+  Stopped                 ./test_cont
$ kill -CONT $!
CONT
$ jobs
[1]+  Running                 ./test_cont &
$ kill -CONT $!
CONT

(10) STOP

SIGSTOP causes a process to stop. Like SIGKILL, SIGSTOP cannot be ignored or blocked — once received, the process will definitely stop.

SIGSTOP can only be triggered via kill and similar. We’ve used it several times already, so we’ll skip further demonstration.

(11) TSTP

SIGTSTP’s default disposition is the same as SIGSTOP — the process stops upon receiving it. However, this signal can be ignored or blocked. Its relationship to SIGSTOP is similar to SIGTERM and SIGKILL.

For a foreground process, pressing Ctrl+Z sends a SIGTSTP signal. Normally this stops the foreground process and returns the shell to the foreground. This foreground/background switching is an important feature of job control, allowing the shell to run many different jobs concurrently.

Let’s use the infinite loop program from earlier. Run it, then press Ctrl+Z to stop it:

$ ./loop 
^Z
[1]+  Stopped                 ./loop
$ jobs 
[1]+  Stopped                 ./loop

Then start another program in the background:

$ ./loop &
[2] 257314
$ jobs 
[1]+  Stopped                 ./loop
[2]-  Running                 ./loop &

Use bg to make the stopped process run in the background:

$ bg %1
[1]+ ./loop &
$ jobs 
[1]-  Running                 ./loop &
[2]+  Running                 ./loop &

Use fg to bring a background process to the foreground, then stop it with SIGINT:

$ fg %2
./loop
^C

Finally, kill the remaining background process:

$ kill %1
$ 
[1]+  Terminated              ./loop

(12) TTIN

If a process is in the background and attempts to read from the terminal, SIGTTIN is triggered. This is because user input at that moment is intended for the foreground process. The default disposition for TTIN (and TTOU) is to stop the current process — because while the user doesn’t need to interact with the SIGTTIN-triggering process right now, they may later bring it to the foreground.

The following program simply reads a number and echoes it:

// iecho.c
#include <stdio.h>

int main() {
    int n;
    scanf("%d", &n);
    printf("%d\n", n);
    return 0;
}

When run in the background, the process quickly stops. We can then bring it to the foreground with fg and interact with it:

$ ./iecho &
[1] 123456
$ 
[1]+  Stopped                 ./iecho
$ jobs 
[1]+  Stopped                 ./iecho
$ fg
./iecho
1
1

(13) TTOU

SIGTTOU is similar to SIGTTIN. It’s generated when a background process attempts to write to the terminal. By default, this signal is not generated — background processes can output to the terminal. It’s only enabled after explicitly running stty tostop to disallow background output.

Let’s test with the iecho program. Without tostop, the background process still outputs to the foreground:

$ echo 1 | ./a.out & wait
[1] 123456
1
[1]+  Done                    echo 123 | ./a.out

But with tostop, the same program now causes the process to stop:

$ stty tostop
$ echo 123 | ./a.out & wait
[1] 524975

[1]+  Stopped                 echo 123 | ./a.out
bash: wait: warning: job 1[524975] stopped
$ fg
echo 123 | ./a.out
123

Error Handling

(1) ILL

When the processor encounters an unrecognized instruction during program execution, SIGILL is triggered. Producing this signal is simple — just use the following code:

// test_ill.c
int main() {
    asm(".byte 0x0f, 0x00");
    return 0;
}

This compiles fine, but triggers SIGILL at runtime. Since this signal indicates a program error, the default disposition generates a core dump.

$ ./test_ill
Illegal instruction (core dumped)

Since machine code is usually generated by a compiler or assembler, this signal is rare. Even if the PC mistakenly points to a data segment, SIGILL won’t occur because the data segment lacks execute permission. For example:

// invalid_call.c
typedef void (*func_t)(void);

const char a = 0;

int main() {
    func_t f = (func_t)&a;
    f();
    return 0;
}

This triggers SIGSEGV, not SIGILL:

$ ./invalid_call
Segmentation fault (core dumped)

(2) ABRT

When a program detects an unrecoverable error, it can call abort() to terminate abnormally. The process then receives SIGABRT. The default disposition terminates the process and generates a core dump. Example:

// test_abort.c
#include <stdlib.h>
#include <unistd.h>

int factorial(int n) {
    if (n < 0) {
        abort();
    } else if (n == 0) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}

int main() {
    int n = -1;
    return factorial(n);
}

Running it, abort() triggers SIGABRT and the process terminates:

$ ./test_abort
Aborted (core dumped)

The standard library’s assert also calls abort() internally.

(3) BUS

SIGBUS is generated when accessing a device in an incorrect way. A common scenario is accessing addresses beyond a file’s bounds when using mmap(). For example:

// test_bus.c
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

int main() {
    int fd = open("test.txt", O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    void *addr = mmap(NULL, 1, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    *(char *)addr = 0;

    close(fd);
    return 0;
}

We create a new file test.txt with size 0, then mmap() its first byte. Since the file has size 0, the data at that address should be invalid. The result:

$ ./test_bus
Bus error (core dumped)

The location we accessed has no corresponding on-disk data, so SIGBUS is triggered.

(4) FPE

SIGFPE signals a floating-point exception. It occurs on integer division by zero and similar operations. Producing it is simple:

// test_fpe.c
int main() {
    int x = 1;
    int y = 0;
    return x / y;
}

Output:

$ ./test_fpe
Floating point exception (core dumped)

Note that floating-point division by zero does not produce SIGFPE, because IEEE 754 defines inf.

(5) SEGV

SIGSEGV is probably the most common error signal. It occurs when a process accesses an address whose corresponding page doesn’t exist or lacks the required permissions. For example:

// test_segv.c
int main() {
    *(char*)1 = 0;
    return 0;
}
$ ./test_segv
Segmentation fault (core dumped)

Note that SIGSEGV doesn’t only occur on invalid addresses. In the ILL section, even though the accessed address was valid, the lack of execute permission still produced SIGSEGV.

(6) PIPE

When a process attempts to write to a pipe or socket whose read end is already closed, SIGPIPE is generated. The default disposition terminates the process.

The following program creates a pipe and closes the read end before the second write:

// test_pipe.c
#include <stdio.h>
#include <unistd.h>

int main() {
    int pipefd[2];
    pipe(pipefd);

    printf("Before write (1)\n");
    write(pipefd[1], "Hello, World!", 13);
    printf("After write (1)\n");

    close(pipefd[0]);

    printf("Before write (2)\n");
    write(pipefd[1], "Hello, World!", 13);
    printf("After write (2)\n");

    return 0;
}

Running it, only the first three lines are output:

$ ./test_pipe
Before write (1)
After write (1)
Before write (2)

The exit status is 141 (128 + 13), indicating the process was terminated by signal 13, SIGPIPE:

$ echo $?
141

We can also demonstrate this with sockets. Here’s a server that accepts a connection and writes data every second:

// server.c
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }

    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = 0;

    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) <
        0) {
        perror("bind");
        close(sockfd);
        exit(1);
    }

    socklen_t addr_len = sizeof(server_addr);
    if (getsockname(sockfd, (struct sockaddr *)&server_addr, &addr_len) < 0) {
        perror("getsockname");
        close(sockfd);
        exit(1);
    }
    printf("Socket bound to port %d\n", ntohs(server_addr.sin_port));

    if (listen(sockfd, 5) < 0) {
        perror("listen");
        close(sockfd);
        exit(1);
    }

    int client_sockfd = accept(sockfd, NULL, NULL);
    if (client_sockfd < 0) {
        perror("accept");
        close(sockfd);
        exit(1);
    }
    printf("Accepted a connection\n");

    char buffer[] = "Hello, World!\n";

    while (1) {
        if (send(client_sockfd, buffer, sizeof(buffer), 0) < 0) {
            perror("send");
            close(client_sockfd);
            close(sockfd);
            exit(1);
        }
        printf("Sent message to client\n");
        sleep(1);
    }

    return 0;
}

Run this server and use netcat as the client. After a while, terminate netcat with SIGINT:

Server:

$ ./server 
Socket bound to port 54321
Accepted a connection
Sent message to client
Sent message to client
Sent message to client
Sent message to client
Sent message to client
Sent message to client
$ echo $?
141

Client:

$ netcat 0.0.0.0 54321
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
^C

Notice what happened: the client closed the connection, and as a result the server was terminated. For a server that requires high availability, this behavior is unacceptable.

For socket-based programs, especially servers, you must handle SIGPIPE explicitly. Options include ignoring SIGPIPE or adding MSG_NOSIGNAL to send() calls to avoid the issue. With these measures in place, a closed read end will cause write(), send(), etc., to return an error with errno set to EPIPE.

There’s a dual scenario: the write end closes while reading. This is detected when read() returns normally with 0 bytes read. Since this is considered normal, no signal or error code is needed.

(7) XCPU

setrlimit() can limit various resources for a single process — virtual memory size, CPU time, file size, and more. When a process’s resource request exceeds its limit, an exception is raised. Some limits are checked synchronously when the API is called, and the exception is delivered via error codes. Others are checked asynchronously as the program executes — these asynchronous exceptions are delivered via signals.

rlimit applies to a single process. A related concept is cgroup, which applies to a process group.

SIGXCPU signals that “the process has exceeded its preset CPU execution time limit.” The following code sets the CPU time limit to 1 second, then runs an infinite loop to trigger the timeout:

// test_xcpu.c
#include <stdio.h>
#include <sys/resource.h>

int main() {
    // change the CPU time limit to 1 second
    struct rlimit rl = {.rlim_cur = 1, .rlim_max = RLIM_INFINITY};
    if (setrlimit(RLIMIT_CPU, &rl) == -1) {
        perror("setrlimit");
        return 1;
    }
    printf("CPU time limit: %ld seconds\n", rl.rlim_cur);

    while (1)
        ;
    return 0;
}

Run it and measure the time. After roughly one second, SIGXCPU is triggered. The default disposition terminates the process and generates a core dump.

$ time ./test_xcpu
CPU time limit: 1 seconds
CPU time limit exceeded (core dumped)

real    0m1.087s
user    0m0.993s
sys     0m0.005s

(8) XFSZ

Another resource-limit-related signal is SIGXFSZ. setrlimit() can set the maximum file size the current process can create. Exceeding this limit triggers SIGXFSZ. The default disposition terminates the program and generates a core dump.

The following code sets the limit to 1 byte, then tries to create a file and write more than 1 byte:

// test_xfsz.c
#include <stdio.h>
#include <sys/resource.h>

int main() {
    struct rlimit rl = {.rlim_cur = 1, .rlim_max = RLIM_INFINITY};
    if (setrlimit(RLIMIT_FSIZE, &rl) == -1) {
        perror("setrlimit");
        return 1;
    }
    printf("File size limit changed to: %ld bytes\n", rl.rlim_cur);

    // try to create a file larger than the limit
    FILE *fp = fopen("test.txt", "w");
    if (fp == NULL) {
        perror("fopen");
        return 1;
    }
    printf("Writing to file...\n");
    fprintf(fp, "Hello, World!\n");
    fclose(fp);

    return 0;
}

Output: SIGXFSZ is triggered when attempting to write data:

$ ./test_xfsz
File size limit changed to: 1 bytes
Writing to file...
File size limit exceeded (core dumped)

(9) PWR

SIGPWR is sent to the init process when power is about to be exhausted. The init process is responsible for orchestrating an orderly system shutdown at this point. The trigger conditions are rather strict, so we’ll skip this one.

(10) SYS

SIGSYS is triggered when a process makes an invalid system call. However, I wasn’t able to construct such a program — using an invalid syscall number or wrong arguments simply causes the process to exit directly.

(11) How to Actually Handle These?

Although the default disposition for all error-handling signals is to terminate the process and generate a core dump, they can still be caught and handled by user-defined handlers. But you’re probably wondering how to handle signals like SIGILL and SIGSEGV in practice. After all, even if you return from the handler, you’ll still execute the same instruction that caused the error. The following program illustrates this situation:

// infinity.c
#include <signal.h>
#include <unistd.h>

void handle_segv(int sig) {
    write(1, "SEGV\n", 5);
    sleep(1); // Just to slow down the output
}

int main() {
    signal(SIGSEGV, handle_segv);
    *(char *)1 = 1;
    return 0;
}

Running this, the process keeps triggering SIGSEGV:

$ ./infinity 
SEGV
SEGV
SEGV
SEGV
SEGV
^C

The key to solving this is to avoid re-executing the same instruction when the signal handler returns. This requires longjmp — or more precisely, siglongjmp.

In the following program, we use sigsetjmp() to create a checkpoint. When SIGSEGV is caught, we use siglongjmp() to jump back to the sigsetjmp() location for a second return, and execute the error-handling code:

// catch.c
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

static sigjmp_buf senv;

void handle_segv(int sig) {
    write(1, "SEGV\n", 5);
    siglongjmp(senv, 1);
}

int main() {
    signal(SIGSEGV, handle_segv);
    int snd = sigsetjmp(senv, 1);
    if (!snd) {
        *(char *)1 = 1;
    } else {
        printf("Catch SEGV\n");
    }
    return 0;
}

The modified program exits successfully:

$ ./catch 
SEGV
Catch SEGV

The above program is similar to the following pseudocode:

int main() {
    try {
        *(char *)1 = 1;
    } catch (const SEGVException &ex) {
        printf("Catch SEGV\n");
    }
    return 0;
}

Asynchronous Operations

(1) ALRM/VTALRM/PROF

SIGALRM, SIGVTALRM, and SIGPROF all originate from the same system call: setitimer. This call sets an asynchronous timer that triggers the corresponding signal when it expires. The default disposition terminates the process.

The difference between SIGALRM, SIGVTALRM, and SIGPROF lies in what time they measure. Setting setitimer’s which parameter to ITIMER_REAL measures wall-clock time (real time), and generates SIGALRM on expiry. Setting it to ITIMER_VIRTUAL measures CPU time in user mode, generating SIGVTALRM. Setting it to ITIMER_PROF measures CPU time in both user and kernel mode, generating SIGPROF.

Note that a process can only use one of these three timers at a time, and can only set a single expiry time. The following program uses setitimer() to set an ITIMER_REAL timer to expire after 1 second, and registers a SIGALRM handler:

// test_alarm.c
#include <signal.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>

void handle_alarm(int sig) {
    write(1, "ALRM\n", 5);
    exit(0);
}

int main() {
    signal(SIGALRM, handle_alarm);

    struct itimerval timer = {
        .it_value = {.tv_sec = 1, .tv_usec = 0},   // First alarm after 1 second
        .it_interval = {.tv_sec = 0, .tv_usec = 0} // No repeat
    };

    setitimer(ITIMER_REAL, &timer, NULL);

    while (1) {
        pause();
    }
}

Output:

$ time ./test_alarm
ALRM

real    0m1.002s
user    0m0.002s
sys     0m0.000s

The other two signals are triggered similarly, so we’ll skip them.

(2) URG

SIGURG is generated when urgent data is transmitted over a socket. However, urgent data is hardly used nowadays, so we’ll skip this.

(3) WINCH

SIGWINCH is triggered when the user changes the terminal window size. The window size can affect the formatting of terminal applications (like vim). When SIGWINCH is received, such programs can redraw their display based on the new window size.

The following program catches SIGWINCH:

// test_winch.c
#include <signal.h>
#include <unistd.h>

void handle_winch(int sig) { write(1, "WINCH\n", 6); }

int main() {
    signal(SIGWINCH, handle_winch);
    while (1) {
        pause();
    }
}

Open a new terminal, run this program, then resize the window. You’ll see a stream of SIGWINCH signals:

$ ./test_winch
WINCH
WINCH
WINCH
WINCH
WINCH
WINCH
WINCH
WINCH

(4) POLL

SIGPOLL is used for signal-driven I/O. However, implementing I/O on top of signal handling is overly complex and offers no clear performance advantage over multiplexing, so it seems rarely useful. We’ll skip this.