Kahibaro
Discord Login Register

Signals and IPC

Signals

Signals are a simple, asynchronous way for the kernel and processes to notify a process that “something happened.” They are not data channels; they are more like software interrupts.

Common Signals and Their Meanings

Linux defines many signals; a few are especially important:

The exact numeric values differ across architectures; use the symbolic names.

Signal Dispositions

Each signal has a disposition for a process:

Two special constants for setting dispositions:

Some signals (SIGKILL, SIGSTOP) cannot be caught, blocked, or ignored. The kernel enforces this.

Sending Signals

From the Shell

You can also send signals by job control:

From Code: `kill(2)` and `raise(3)`

To send a signal from one process to another:

#include <signal.h>
#include <unistd.h>
int kill(pid_t pid, int sig);

To send a signal to yourself:

#include <signal.h>
int raise(int sig);   // equivalent to kill(getpid(), sig)

Real‑time Signals

Traditional (standard) signals are non‑queued: multiple occurrences can coalesce into one. POSIX real‑time signals (numbered SIGRTMIN to SIGRTMAX) are queued and can carry more detailed info (see sigqueue()).

Use them carefully; they are more complex and less portable between systems.

Handling Signals in User Space

Installing a Simple Handler: `signal()` (Legacy)

The historical API:

#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);

Usage:

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void sigint_handler(int sig) {
    write(STDOUT_FILENO, "Caught SIGINT\n", 14);
}
int main(void) {
    signal(SIGINT, sigint_handler);
    while (1) {
        pause(); // sleep until a signal is received
    }
}

signal() has historical quirks and portability issues; in serious code, use sigaction().

Robust Handling: `sigaction()`

sigaction() provides fine‑grained control:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
              struct sigaction *oldact);

Key parts of struct sigaction:

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int      sa_flags;
};

Example:

#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static void handler(int sig, siginfo_t *info, void *ucontext) {
    (void)ucontext;
    printf("Received signal %d from PID %d\n", sig, info->si_pid);
}
int main(void) {
    struct sigaction sa = {0};
    sa.sa_sigaction = handler;
    sa.sa_flags = SA_SIGINFO;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);
    printf("My PID: %d\n", getpid());
    while (1) {
        pause();
    }
}

Async‑Signal‑Safe Functions

Signal handlers run asynchronously relative to the main flow. Many library functions are not safe to call from within a handler (they may use non‑reentrant state, global variables, etc.).

POSIX defines async‑signal‑safe functions, which can be safely called from handlers. Examples:

Avoid calling:

Use global volatile sig_atomic_t flags to communicate with the main code:

#include <signal.h>
#include <stdatomic.h>
volatile sig_atomic_t got_sigint = 0;
void handler(int sig) {
    got_sigint = 1;
}
int main(void) {
    signal(SIGINT, handler);
    for (;;) {
        if (got_sigint) {
            // handle it in main flow, using safe APIs
            got_sigint = 0;
        }
        // do regular work
    }
}

Signal Masking and Pending Signals

Each thread has a signal mask: a set of signals that are blocked and will not be delivered until unblocked.

Manipulating the Mask: `sigprocmask()`

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

Helpers:

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);

Example: momentarily block SIGINT:

sigset_t block, old;
sigemptyset(&block);
sigaddset(&block, SIGINT);
sigprocmask(SIG_BLOCK, &block, &old);
/* critical section: SIGINT won't be delivered here */
sigprocmask(SIG_SETMASK, &old, NULL);

Pending Signals and `sigpending()`

Signals sent to a process while blocked become pending until they can be delivered.

Check pending signals:

sigset_t pending;
sigpending(&pending);
if (sigismember(&pending, SIGINT)) {
    // signal is pending
}

Standard signals coalesce (only one instance pending per type); real‑time signals queue.

Synchronous Waiting: `sigsuspend()` / `sigwaitinfo()`

To atomically unblock some signals and pause:

#include <signal.h>
int sigsuspend(const sigset_t *mask);

sigwaitinfo() and sigtimedwait() can synchronously wait for specific signals and return detailed info (similar to messages).


Inter‑Process Communication (IPC)

Signals provide only event notifications. For data exchange and richer coordination, Linux offers multiple IPC mechanisms. At a high level they differ in:

Below is an overview tailored to understanding how they relate to signals and to each other.

Pipes and FIFOs

Anonymous Pipes

Anonymous pipes are the simplest IPC. They connect a writer and a reader through a unidirectional byte stream.

Characteristics:

Anonymous pipes are not named; they do not appear as filesystem nodes.

Named Pipes (FIFOs)

FIFOs extend pipes across unrelated processes via a name in the filesystem:

Characteristics:

Use FIFOs for simple local IPC with minimal setup, especially for glue scripts or legacy systems.

UNIX Domain Sockets

UNIX domain sockets are IPC endpoints that behave like network sockets but are confined to one host and addressable via filesystem paths or abstract names.

Advantages:

Typical uses:

Example topology:

UNIX sockets are flexible and a good general “default choice” for structured local IPC.

Message Queues

Linux supports two major message queue styles:

  1. System V message queues (msgget, msgsnd, msgrcv).
  2. POSIX message queues (mq_open, mq_send, mq_receive).

System V Message Queues

They’re powerful but considered somewhat old‑fashioned and cumbersome.

POSIX Message Queues

Characteristics:

Signal integration: POSIX message queues can be configured to send a signal (e.g., SIGEV_SIGNAL) when new messages arrive, connecting queues with signal‑driven I/O.

Shared Memory

Shared memory is the fastest IPC mechanism for large data volumes. Multiple processes map the same region of physical memory into their address spaces.

Variants:

Characteristics:

Common synchronization tools:

Typical pattern:

Shared memory is ideal when throughput matters, such as video buffers, large telemetry feeds, or high‑throughput IPC.

Semaphores and Synchronization Primitives

Semaphores are counters used to control access to shared resources or to signal events between processes/threads.

Variants:

Typical use patterns:

In multi‑process scenarios, semaphores and shared memory are often combined:

Other synchronization options:

Signals vs Other IPC Mechanisms

Signals are distinct from other IPC in purpose and semantics:

A common pattern:

For example, a daemon might:

  1. Wait for clients on a UNIX domain socket.
  2. Get SIGHUP when it should reload config.
  3. Use shared memory + semaphores to efficiently share large data with worker processes.

IPC Design Considerations

When choosing an IPC method, consider:

Selecting the right combination is part of advanced Linux system design; signals are one piece of this broader IPC toolkit, mainly for control flow and notifications rather than bulk data transfer.

Views: 18

Comments

Please login to add a comment.

Don't have an account? Register now!