Lesson 13-C Language Signal Processing

Signals are a mechanism used by the operating system to communicate with processes, notifying them of specific events (such as user input, hardware exceptions, or software errors). C language provides signal processing capabilities through the POSIX signal interface (signal.h), suitable for process control, error handling, user interaction, and other scenarios.

Signal Basics

The Nature and Classification of Signals

A signal is an asynchronous event notification mechanism. When a process receives a signal, it pauses its current execution and executes the corresponding signal handler (if registered). The essence of a signal is a flag bit in the process control block (PCB), which is triggered and scheduled by the kernel.

  • Signal: A mechanism used by the operating system to notify a process that an event has occurred.
  • Signal Types: Different signals serve different purposes, such as SIGINT for interrupt signals and SIGTERM for termination signals.
  • Signal Handling: Custom handler functions can be registered to respond to specific signals.

Common Signal Types (Partial List):

Signal NameValueDescription
SIGINT2User pressed Ctrl+C (interrupt process)
SIGTERM15Graceful termination (default signal sent by kill)
SIGSEGV11Segmentation fault (invalid memory access)
SIGKILL9Force termination (cannot be caught or ignored)
SIGALRM14Alarm clock signal (timer expired)
SIGUSR110User-defined signal 1
SIGUSR212User-defined signal 2

Signal Generation and Delivery

  • Hardware-triggered: Such as division by zero (SIGFPE), invalid memory access (SIGSEGV).
  • Software-triggered: Such as user pressing Ctrl+C (SIGINT), calling kill() to send a signal.
  • Kernel-triggered: Such as timer expiration (SIGALRM), child process termination (SIGCHLD).

Signal delivery process:

  1. Event occurs → Kernel sets the signal flag in the process.
  2. During process scheduling, the kernel checks the signal flag → If there are pending signals, pause the current process.
  3. Execute the signal handler (if registered) → Resume process execution.

Signal Handlers: Basics and Limitations

signal() Function: Quick Handler Registration

signal() is the most basic signal registration interface, with the following prototype:

#include <signal.h>

typedef void (*sighandler_t)(int);  // Signal handler function type

sighandler_t signal(int sig, sighandler_t handler);

Parameter Explanation:

  • sig: The signal number to handle (e.g., SIGINT).
  • handler: Pointer to the signal handler function (or special values SIG_IGN to ignore, SIG_DFL to restore default behavior).

Example: Handling SIGINT (Ctrl+C)

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handle_sigint(int sig) {
    printf("\nReceived SIGINT (Ctrl+C), preparing to exit...\n");
    _exit(0);  // Safe exit (avoid calling non-async-safe functions)
}

int main() {
    // Register SIGINT handler
    if (signal(SIGINT, handle_sigint) == SIG_ERR) {
        perror("signal registration failed");
        return 1;
    }

    printf("Running... Press Ctrl+C to exit\n");
    while (1) {
        sleep(1);  // Simulate long-running process
    }
    return 0;
}

Compile and Run:

gcc -o signal_demo signal_demo.c
./signal_demo

Output:

Running... Press Ctrl+C to exit
^C
Received SIGINT (Ctrl+C), preparing to exit...

Limitations of signal()

  • Non-reentrant: If the same signal is received again during handler execution, it may cause recursion or state corruption.
  • Signal Masking: By default, the same signal is not automatically blocked during handling (requires manual setup).
  • Platform Differences: Different systems may implement signal() differently (e.g., BSD vs. System V).

Signal Handling Example

Here is a simple signal handling example that demonstrates how to set up a handler for the SIGINT signal (typically triggered by pressing Ctrl+C):

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void signal_handler(int signum) {
    if (signum == SIGINT) {
        printf("Caught SIGINT. Exiting...\n");
        exit(0);
    }
}

int main() {
    // Register signal handler
    signal(SIGINT, signal_handler);

    // Main loop
    while (1) {
        printf("Running...\n");
        sleep(1); // Pause for one second
    }

    return 0;
}

Advanced Signal Handling: sigaction

sigaction Function: More Flexible Signal Control

sigaction is the POSIX-standard signal registration interface that addresses the limitations of signal(), offering finer control (such as signal masking and flag settings). Its prototype is:

#include <signal.h>

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

struct sigaction Structure:

struct sigaction {
    void (*sa_handler)(int);       // Signal handler (same as signal())
    void (*sa_sigaction)(int, siginfo_t *, void *);  // Handler with extra info
    sigset_t sa_mask;              // Signals to block during handler execution
    int sa_flags;                  // Flags (e.g., SA_RESTART, SA_NODEFER)
    void (*sa_restorer)(void);     // Restoration function (deprecated, use sigreturn())
};

Key Field Explanations:

  • sa_mask: Signals to automatically block during handler execution (prevents race conditions).
  • sa_flags: Flags controlling signal handling behavior:
    • SA_RESTART: Automatically restart interrupted system calls (e.g., read, write).
    • SA_NODEFER: Do not block the current signal during handling (blocked by default).
    • SA_SIGINFO: Use sa_sigaction instead of sa_handler, passing additional signal info (e.g., sender PID, signal code).

Example: Using sigaction to Handle SIGSEGV (Segmentation Fault)

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/ucontext.h>  // For ucontext_t (optional)

void handle_segv(int sig, siginfo_t *info, void *ucontext) {
    printf("\nReceived SIGSEGV (segmentation fault), fault address: %p\n", info->si_addr);
    printf("Attempting recovery...\n");
    _exit(1);  // Force exit (segmentation faults are usually unrecoverable)
}

int main() {
    struct sigaction sa;
    sa.sa_sigaction = handle_segv;  // Use handler with extra info
    sigemptyset(&sa.sa_mask);       // Initially block no signals
    sa.sa_flags = SA_SIGINFO;       // Enable sa_sigaction

    // Register SIGSEGV handler
    if (sigaction(SIGSEGV, &sa, NULL) == -1) {
        perror("sigaction registration failed");
        return 1;
    }

    printf("Running... Attempting invalid memory access\n");
    int *ptr = NULL;
    *ptr = 10;  // Trigger segmentation fault (SIGSEGV)
    return 0;
}

Compile and Run:

gcc -o segv_demo segv_demo.c
./segv_demo

Output:

Running... Attempting invalid memory access

Received SIGSEGV (segmentation fault), fault address: (nil)
Attempting recovery...

Signal Sets

Purpose of Signal Sets

A signal set (sigset_t) is a collection of signals used for batch management of signal blocking, waiting, or handling. Common operations include:

  • Blocking a group of signals (to prevent interruption during critical sections).
  • Waiting for any signal in a group (sigsuspend).
  • Sending a group of signals to a process (kill supports this).

Signal Set Operation Functions

FunctionDescription
sigemptyset(sigset_t *set)Initialize an empty signal set
sigfillset(sigset_t *set)Initialize a full signal set (all signals)
sigaddset(sigset_t *set, int sig)Add signal sig to the set
sigdelset(sigset_t *set, int sig)Remove signal sig from the set
sigismember(const sigset_t *set, int sig)Check if sig is in the set

Using Signal Sets

Here is an example using signal sets to manage signal masks:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

void signal_handler(int signum) {
    if (signum == SIGINT) {
        printf("Caught SIGINT. Exiting...\n");
        exit(0);
    }
}

int main() {
    struct sigaction sa;
    sigset_t sigset;

    // Initialize signal set
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGINT);

    // Set signal handler
    sa.sa_handler = signal_handler;
    sa.sa_mask = sigset; // Set signal mask
    sa.sa_flags = 0;

    // Register signal handler
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    // Block SIGINT
    if (sigprocmask(SIG_BLOCK, &sigset, NULL) == -1) {
        perror("sigprocmask");
        exit(EXIT_FAILURE);
    }

    // Main loop
    while (1) {
        printf("Running...\n");
        sleep(1); // Pause for one second

        // Unblock SIGINT
        if (sigprocmask(SIG_UNBLOCK, &sigset, NULL) == -1) {
            perror("sigprocmask");
            exit(EXIT_FAILURE);
        }
    }

    return 0;
}

Blocking SIGINT During Critical Operations

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handle_sigint(int sig) {
    printf("\nReceived SIGINT, but operation cannot be interrupted\n");
}

int main() {
    struct sigaction sa;
    sa.sa_handler = handle_sigint;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);

    sigset_t block_set;
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGINT);  // Block SIGINT

    printf("Starting critical operation (SIGINT blocked)...\n");
    sigprocmask(SIG_BLOCK, &block_set, NULL);  // Block SIGINT

    // Simulate critical operation (e.g., file write)
    sleep(3);

    sigprocmask(SIG_UNBLOCK, &block_set, NULL);  // Unblock SIGINT
    printf("Critical operation complete, resuming SIGINT handling\n");

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

Output:

Starting critical operation (SIGINT blocked)...
^C  # Ctrl+C is blocked, no response
Critical operation complete, resuming SIGINT handling
^C
Received SIGINT, but operation cannot be interrupted

Advanced Usage of Signal Sets

Signal sets can be used to mask or handle multiple signals. The following example shows how to handle multiple signals:

Example: Handling Multiple Signals

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

void signal_handler(int signum) {
    switch (signum) {
        case SIGINT:
            printf("Caught SIGINT. Exiting...\n");
            break;
        case SIGTERM:
            printf("Caught SIGTERM. Exiting...\n");
            break;
        default:
            printf("Caught signal %d. Ignoring...\n", signum);
            break;
    }
    exit(0);
}

int main() {
    struct sigaction sa;
    sigset_t sigset;

    // Initialize signal set
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGINT);
    sigaddset(&sigset, SIGTERM);

    // Set signal handler
    sa.sa_handler = signal_handler;
    sa.sa_mask = sigset; // Set signal mask
    sa.sa_flags = 0;

    // Register signal handlers
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }
    if (sigaction(SIGTERM, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    // Main loop
    while (1) {
        printf("Running...\n");
        sleep(1); // Pause for one second
    }

    return 0;
}

Signal Stack

Why Use a Signal Stack?

By default, signal handlers use the process’s main stack. If the main stack is corrupted (e.g., due to stack overflow or segmentation fault), the handler may fail to execute. An alternative signal stack provides a backup stack for executing handlers when the main stack is unusable.

sigaltstack Function: Set Alternative Stack

#include <signal.h>

int sigaltstack(const stack_t *ss, stack_t *old_ss);

stack_t Structure:

typedef struct {
    void *ss_sp;     // Stack pointer (must point to writable memory)
    int ss_flags;    // Flags (e.g., SS_ONSTACK: currently using alt stack)
    size_t ss_size;  // Stack size (at least SIGSTKSZ, typically 8MB)
} stack_t;

Example: Setting a Signal Stack

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

void signal_handler(int signum) {
    if (signum == SIGINT) {
        printf("Caught SIGINT. Exiting...\n");
        exit(0);
    }
}

int main() {
    struct sigaction sa;
    stack_t ss;

    // Initialize signal stack
    ss.ss_sp = malloc(SIGSTKSZ); // Allocate stack space
    ss.ss_size = SIGSTKSZ;
    ss.ss_flags = 0;

    // Set signal stack
    if (sigaltstack(&ss, NULL) == -1) {
        perror("sigaltstack");
        exit(EXIT_FAILURE);
    }

    // Set signal handler
    sa.sa_handler = signal_handler;
    sa.sa_mask = sigemptyset(); // Set signal mask to empty
    sa.sa_flags = SA_ONSTACK; // Use signal stack

    // Register signal handler
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    // Main loop
    while (1) {
        printf("Running...\n");
        sleep(1); // Pause for one second
    }

    free(ss.ss_sp); // Free signal stack
    return 0;
}

Example: Using Alternative Stack for SIGSEGV

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

#define STACK_SIZE (SIGSTKSZ * 2)  // Alternative stack size (at least SIGSTKSZ)

void handle_segv(int sig) {
    printf("\nHandling SIGSEGV on alternative stack\n");
    _exit(1);
}

int main() {
    // Allocate alternative stack memory (must be writable and executable)
    char *alt_stack = malloc(STACK_SIZE);
    if (!alt_stack) {
        perror("malloc failed");
        return 1;
    }

    stack_t ss;
    ss.ss_sp = alt_stack;
    ss.ss_flags = 0;
    ss.ss_size = STACK_SIZE;

    // Set alternative stack
    if (sigaltstack(&ss, NULL) == -1) {
        perror("sigaltstack setup failed");
        free(alt_stack);
        return 1;
    }

    // Register SIGSEGV handler
    struct sigaction sa;
    sa.sa_handler = handle_segv;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGSEGV, &sa, NULL);

    printf("Running... Triggering segmentation fault (using alt stack)\n");
    int *ptr = NULL;
    *ptr = 10;  // Trigger SIGSEGV (handler runs on alt stack)

    free(alt_stack);
    return 0;
}

Compile and Run:

gcc -o alt_stack_demo alt_stack_demo.c
./alt_stack_demo

Output:

Running... Triggering segmentation fault (using alt stack)

Handling SIGSEGV on alternative stack

Signal-Safe Functions and Precautions

Async-Signal-Safe Functions

Signal handlers execute asynchronously (can interrupt the main program at any time), so only async-signal-safe functions should be called (they do not modify global state or call unsafe functions). Common safe functions include:

FunctionDescription
write()Write to a file descriptor (e.g., STDOUT_FILENO)
_exit()Terminate process (no cleanup)
sigprocmask()Modify signal mask
sigsuspend()Wait for signals
getpid()Get process ID

Prohibited unsafe functions: printf, malloc, free, sleep, system, etc. (may access shared resources or modify global state).

Signal-Safe Function Example

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

void signal_handler(int signum) {
    if (signum == SIGINT) {
        printf("Caught SIGINT. Exiting...\n");
        _exit(0); // Use _exit() instead of exit()
    }
}

int main() {
    struct sigaction sa;

    // Set signal handler
    sa.sa_handler = signal_handler;
    sa.sa_mask = sigemptyset(); // Set signal mask to empty
    sa.sa_flags = 0;

    // Register signal handler
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    // Main loop
    while (1) {
        printf("Running...\n");
        sleep(1); // Pause for one second
    }

    return 0;
}

Signal Handling Precautions

  1. Minimize handler logic: Handlers should only perform essential operations (e.g., set flags, clean resources), avoiding complex logic.
  2. Avoid race conditions: Use sa_mask to block signals that might interrupt the handler.
  3. Proper state restoration: If the handler modifies global variables, the main program must check and restore state.
  4. Handling SIGKILL and SIGSTOP: These cannot be caught or ignored (SIGKILL forces termination, SIGSTOP pauses the process).
  5. Multithreaded signal handling: Signals are delivered to all threads by default; use pthread_sigmask to control per-thread signal masks.

Signal Synchronization and Delivery

sigwait: Synchronous Signal Waiting

The sigwait function synchronously waits for signals, ideal for multithreaded environments to ensure signals are handled by a specific thread. Prototype:

#include <signal.h>

int sigwait(const sigset_t *set, int *sig);

Thread Synchronously Waiting for SIGUSR1

#include <stdio.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>

sigset_t sig_set;

void* wait_thread(void *arg) {
    int sig;
    while (1) {
        sigwait(&sig_set, &sig);  // Block and wait for signal
        if (sig == SIGUSR1) {
            printf("Thread %ld received SIGUSR1\n", (long)pthread_self());
        }
    }
    return NULL;
}

int main() {
    sigemptyset(&sig_set);
    sigaddset(&sig_set, SIGUSR1);

    // Block SIGUSR1 in main thread (prevent main thread from receiving it)
    pthread_sigmask(SIG_BLOCK, &sig_set, NULL);

    // Create waiting thread
    pthread_t tid;
    pthread_create(&tid, NULL, wait_thread, NULL);

    // Main thread sends SIGUSR1 to itself
    sleep(1);
    kill(getpid(), SIGUSR1);

    pthread_join(tid, NULL);
    return 0;
}

Output:

Thread 12345 received SIGUSR1  # Thread ID may vary

Signal Synchronization in Multithreading

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

void *thread_function(void *arg) {
    sigset_t *sigset = (sigset_t *)arg;

    // Set signal mask
    if (pthread_sigmask(SIG_SETMASK, sigset, NULL) == -1) {
        perror("pthread_sigmask");
        pthread_exit(NULL);
    }

    // Thread main loop
    while (1) {
        printf("Thread running...\n");
        sleep(1); // Pause for one second
    }

    pthread_exit(NULL);
}

int main() {
    struct sigaction sa;
    sigset_t sigset;

    // Initialize signal set
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGINT);

    // Set signal handler
    sa.sa_handler = signal_handler;
    sa.sa_mask = sigset; // Set signal mask
    sa.sa_flags = 0;

    // Register signal handler
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    pthread_t thread;
    if (pthread_create(&thread, NULL, thread_function, &sigset) != 0) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    // Main thread loop
    while (1) {
        printf("Main thread running...\n");
        sleep(1); // Pause for one second
    }

    pthread_join(thread, NULL);

    return 0;
}

Signal Delivery Mechanism

  • Inter-process signals: Use kill(pid, sig) to send a signal to a specific process.
  • Inter-thread signals: Signals are delivered to all threads by default, but pthread_sigmask can restrict handling to specific threads.
  • Real-time signals (SIGRTMIN to SIGRTMAX): Support queuing (no loss), while standard signals (e.g., SIGINT) may be lost (only the last one is kept).

Signal Delivery Example

If a signal arrives while another is being handled, it is pended until the current handler completes. Use sigpending to check for pending signals.

Example: Checking Pending Signals

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

void signal_handler(int signum) {
    if (signum == SIGINT) {
        printf("Caught SIGINT. Checking for pending signals...\n");

        sigset_t pending;
        if (sigpending(&pending) == -1) {
            perror("sigpending");
            exit(EXIT_FAILURE);
        }

        if (sigismember(&pending, SIGTERM)) {
            printf("SIGTERM is pending. Exiting...\n");
            exit(0);
        }

        printf("No pending signals. Continuing...\n");
    }
}

int main() {
    struct sigaction sa;
    sigset_t sigset;

    // Initialize signal set
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGINT);
    sigaddset(&sigset, SIGTERM);

    // Set signal handler
    sa.sa_handler = signal_handler;
    sa.sa_mask = sigset; // Set signal mask
    sa.sa_flags = 0;

    // Register signal handlers
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }
    if (sigaction(SIGTERM, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    // Main loop
    while (1) {
        printf("Running...\n");
        sleep(1); // Pause for one second
    }

    return 0;
}

Summary and Best Practices

Key Summary

  • Signal Basics: Signals are asynchronous event notifications; common ones include SIGINT, SIGSEGV.
  • Handlers: signal() is simple but limited; sigaction() is more flexible (supports masking and flags).
  • Signal Sets: Used for batch management of blocking and waiting (sigprocmask, sigsuspend).
  • Signal Stack: Prevents handler failure when main stack is corrupted (sigaltstack).
  • Safe Functions: Only call async-signal-safe functions (e.g., write, _exit).

Best Practices

  • Minimize handler logic: Handlers should execute quickly and avoid blocking.
  • Use sigaction over signal: Ensures portability and reliability.
  • Set signal masks: Block interfering signals during handling (e.g., SA_NODEFER).
  • Test signal handling: Use kill or raise() to manually trigger signals and verify logic.
  • Multithreaded environments: Use pthread_sigmask to control thread-specific signal masks.

By mastering these concepts, developers can efficiently handle signals in C, improving program robustness and reliability.

Share your love