funwithlinux blog

Why Is recv() Not Interrupted by SIGINT in a Multithreaded Environment? Troubleshooting EINTR Signal Handling Issues

If you’ve worked with network programming in C/C++, you’re likely familiar with recv()—the system call used to read data from a socket. Under normal circumstances, recv() blocks until data arrives or an error occurs. One common expectation is that recv() should be interrupted by signals like SIGINT (e.g., when the user presses Ctrl+C), returning -1 with errno set to EINTR (interrupted system call). However, in multithreaded applications, this behavior often breaks: recv() may stubbornly block despite SIGINT being sent, leaving developers scratching their heads.

This blog dives into why recv() might not be interrupted by SIGINT in multithreaded environments, explores the nuances of signal handling in threads, and provides actionable troubleshooting steps to fix EINTR-related issues.

2025-11

Table of Contents#

  1. Understanding the Basics: recv(), SIGINT, and EINTR
  2. Signal Handling in Multithreaded Environments: Key Differences
  3. Why recv() Might Not Be Interrupted by SIGINT in Multithreaded Code
  4. Troubleshooting Steps: Diagnosing the Issue
  5. Practical Example: Fixing a Non-Interruptible recv()
  6. Best Practices for Signal Handling in Multithreaded Network Code
  7. Conclusion
  8. References

1. Understanding the Basics: recv(), SIGINT, and EINTR#

Before diving into multithreaded behavior, let’s recap the fundamentals:

What is recv()?#

recv() is a blocking system call that reads data from a socket. Its signature is:

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

If no data is available, recv() blocks until data arrives, the socket is closed, or an error occurs.

What is SIGINT?#

SIGINT (Interrupt Signal) is a POSIX signal sent to a process when the user presses Ctrl+C in the terminal. By default, SIGINT terminates the process, but developers often override this with a custom handler (e.g., to clean up resources before exiting).

What is EINTR?#

When a blocking system call (like recv()) is interrupted by a signal, it returns -1 and sets errno to EINTR (short for "interrupted system call"). This allows the program to handle the signal (e.g., exit gracefully) instead of remaining blocked indefinitely.

Single-Threaded Expectation#

In a single-threaded program, if recv() is blocked and SIGINT is delivered, the kernel interrupts recv(), and it returns EINTR. For example:

// Single-threaded example: recv() should return EINTR on SIGINT
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
 
void sigint_handler(int signum) {
    printf("Received SIGINT\n");
}
 
int main() {
    struct sigaction sa;
    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0; // No SA_RESTART
    sigaction(SIGINT, &sa, NULL);
 
    int sockfd = ...; // Assume a connected socket
    char buf[1024];
    ssize_t n = recv(sockfd, buf, sizeof(buf), 0);
    if (n == -1) {
        if (errno == EINTR) {
            printf("recv() interrupted by signal\n"); // Expected on Ctrl+C
        } else {
            perror("recv failed");
        }
    }
    return 0;
}

Here, pressing Ctrl+C triggers SIGINT, the handler runs, and recv() returns EINTR.

2. Signal Handling in Multithreaded Environments: Key Differences#

Multithreaded environments complicate signal handling. Unlike single-threaded programs, where the process has one thread to receive signals, multithreaded processes have multiple threads, each with its own signal mask and execution context. To understand why recv() might not interrupt, we first need to clarify how signals behave in threads.

Key Multithreaded Signal Rules#

  1. Signal Handlers Are Process-Wide: A signal handler installed with sigaction() applies to all threads in the process. However, threads can block signals via their signal mask (a per-thread attribute).
  2. Signal Delivery: Signals are delivered to the process, but the kernel routes them to a specific thread based on:
    • The signal’s type (e.g., SIGINT is a "general" signal, not tied to a specific thread).
    • Thread signal masks: A signal is delivered to any thread not blocking it (i.e., not in its signal mask).
  3. Default Signal Behavior: For most signals (including SIGINT), the kernel delivers the signal to an arbitrary thread that is not blocking it. If all threads block the signal, the signal remains pending until a thread unblocks it.

Critical Implication#

In a multithreaded program, SIGINT may not reach the thread blocked in recv() if:

  • The signal is delivered to a different thread (e.g., the main thread), leaving the recv() thread unaffected.
  • The recv() thread’s signal mask explicitly blocks SIGINT.

3. Why recv() Might Not Be Interrupted by SIGINT in Multithreaded Code#

Let’s break down the most common reasons recv() fails to interrupt with EINTR in multithreaded code.

Reason 1: SIGINT Is Delivered to a Different Thread#

By default, SIGINT is delivered to any thread not blocking it. If your program has a main thread and a worker thread (blocked in recv()), the kernel might send SIGINT to the main thread instead of the worker. Since the worker thread is not interrupted, recv() remains blocked.

Reason 2: The recv() Thread Blocks SIGINT#

Each thread has its own signal mask, which determines which signals it will block. If the worker thread (the one calling recv()) has SIGINT in its signal mask, it will never receive the signal, and recv() will not interrupt.

Threads inherit the signal mask of their parent thread at creation time. If the main thread blocks SIGINT before spawning workers, all workers will inherit this mask.

Reason 3: The SA_RESTART Flag Is Set#

The SA_RESTART flag, when passed to sigaction(), tells the kernel to automatically restart certain blocking system calls (including recv()) after a signal handler returns. If SA_RESTART is enabled, recv() will resume blocking instead of returning EINTR, even if the signal is delivered to the correct thread.

Reason 4: The Signal Handler Does Not Unblock recv()#

Even if SIGINT is delivered to the recv() thread, recv() may not return EINTR if the signal handler itself does not trigger the system call to exit. For example, if the handler performs a long-running task, the kernel may not unblock recv() until the handler completes (though this is rare).

4. Troubleshooting Steps: Diagnosing the Issue#

If recv() isn’t interrupting with SIGINT, follow these steps to identify the root cause.

Step 1: Verify Which Thread Receives SIGINT#

The first question is: Which thread is actually receiving the SIGINT signal? To check, modify your signal handler to print the thread ID of the thread executing it.

Example:

#include <pthread.h>
#include <stdio.h>
 
void sigint_handler(int signum) {
    printf("SIGINT received by thread %lu\n", pthread_self());
}

If the handler prints a thread ID different from the recv() thread’s ID, the signal is being delivered to the wrong thread.

Step 2: Check the recv() Thread’s Signal Mask#

Use pthread_sigmask() to inspect the signal mask of the thread blocked in recv(). A blocked SIGINT will prevent the thread from receiving the signal.

Example code to check the mask:

#include <signal.h>
#include <pthread.h>
 
void print_signal_mask() {
    sigset_t mask;
    pthread_sigmask(SIG_BLOCK, NULL, &mask); // Get current mask
    if (sigismember(&mask, SIGINT)) {
        printf("SIGINT is BLOCKED in this thread\n");
    } else {
        printf("SIGINT is UNBLOCKED in this thread\n");
    }
}
 
// Call this in the recv() thread before blocking

Step 3: Check for SA_RESTART in Signal Handler Setup#

Review how the SIGINT handler is installed. If SA_RESTART is set in sa_flags, recv() will restart instead of returning EINTR.

Example problematic setup:

struct sigaction sa;
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // Restarts recv() after signal handler
sigaction(SIGINT, &sa, NULL); // Bad for EINTR expectation!

Step 4: Ensure SIGINT Is Unblocked in the recv() Thread#

If the recv() thread’s mask blocks SIGINT, explicitly unblock it using pthread_sigmask():

// In the recv() thread: Unblock SIGINT
sigset_t unblock_mask;
sigemptyset(&unblock_mask);
sigaddset(&unblock_mask, SIGINT);
pthread_sigmask(SIG_UNBLOCK, &unblock_mask, NULL);

Step 5: Use Debugging Tools#

Tools like strace, gdb, or pstack can help trace signal delivery and thread behavior:

  • strace -p <pid>: Attaches to the process and logs system calls. Look for recvfrom() (the underlying syscall for recv()) and whether it’s interrupted.
  • gdb: Set breakpoints in the signal handler and recv() to see which thread triggers them.
  • pstack <pid>: Prints stack traces for all threads to confirm which thread is blocked in recv().

5. Practical Example: Fixing a Non-Interruptible recv()#

Let’s walk through a real-world scenario where recv() fails to interrupt in a multithreaded program and fix it.

Problematic Code#

Suppose we have a main thread that spawns a worker thread to handle recv(). Pressing Ctrl+C sends SIGINT, but recv() never returns EINTR:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
 
pthread_t worker_thread;
 
void sigint_handler(int signum) {
    printf("SIGINT received by thread %lu\n", pthread_self());
}
 
void *worker(void *arg) {
    int sockfd = *(int *)arg;
    char buf[1024];
    printf("Worker thread %lu entering recv()\n", pthread_self());
    ssize_t n = recv(sockfd, buf, sizeof(buf), 0); // Blocks here
    if (n == -1) {
        if (errno == EINTR) {
            printf("Worker thread: recv() interrupted by signal\n"); // Never printed!
        } else {
            perror("recv failed");
        }
    }
    return NULL;
}
 
int main() {
    // Install SIGINT handler (process-wide)
    struct sigaction sa;
    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0; // No SA_RESTART (good)
    sigaction(SIGINT, &sa, NULL);
 
    int sockfd = ...; // Assume a connected socket
    pthread_create(&worker_thread, NULL, worker, &sockfd);
 
    // Main thread waits indefinitely
    while (1) sleep(1);
    return 0;
}

Symptoms#

When Ctrl+C is pressed, the signal handler prints the main thread’s ID (e.g., SIGINT received by thread 1234), but the worker thread remains blocked in recv().

Root Cause#

The kernel is delivering SIGINT to the main thread (which is not blocked in recv()) instead of the worker thread. Since the main thread is not blocked, the signal handler runs, but the worker’s recv() is unaffected.

Fix: Direct SIGINT to the Worker Thread#

To ensure SIGINT is delivered to the worker thread, we need to:

  1. Block SIGINT in the main thread (so the kernel can’t deliver it there).
  2. Unblock SIGINT in the worker thread (so it becomes the only candidate to receive the signal).

Fixed Code#

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
 
pthread_t worker_thread;
 
void sigint_handler(int signum) {
    printf("SIGINT received by thread %lu\n", pthread_self());
}
 
void *worker(void *arg) {
    int sockfd = *(int *)arg;
 
    // Unblock SIGINT in the worker thread
    sigset_t unblock_mask;
    sigemptyset(&unblock_mask);
    sigaddset(&unblock_mask, SIGINT);
    pthread_sigmask(SIG_UNBLOCK, &unblock_mask, NULL);
 
    char buf[1024];
    printf("Worker thread %lu entering recv()\n", pthread_self());
    ssize_t n = recv(sockfd, buf, sizeof(buf), 0); // Now interruptible!
    if (n == -1) {
        if (errno == EINTR) {
            printf("Worker thread: recv() interrupted by signal\n"); // Now printed!
        } else {
            perror("recv failed");
        }
    }
    return NULL;
}
 
int main() {
    // Install SIGINT handler
    struct sigaction sa;
    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0; // No SA_RESTART
    sigaction(SIGINT, &sa, NULL);
 
    // Block SIGINT in the main thread (so it can't receive it)
    sigset_t block_mask;
    sigemptyset(&block_mask);
    sigaddset(&block_mask, SIGINT);
    pthread_sigmask(SIG_BLOCK, &block_mask, NULL);
 
    int sockfd = ...; // Connected socket
    pthread_create(&worker_thread, NULL, worker, &sockfd);
 
    // Main thread waits (but is blocked from receiving SIGINT)
    while (1) sleep(1);
    return 0;
}

Why It Works#

  • The main thread blocks SIGINT, so the kernel cannot deliver the signal to it.
  • The worker thread unblocks SIGINT, making it the only thread eligible to receive SIGINT.
  • When Ctrl+C is pressed, SIGINT is delivered to the worker thread, interrupting recv(), which returns EINTR.

6. Best Practices for Signal Handling in Multithreaded Network Code#

To avoid EINTR issues with recv() in multithreaded environments, follow these best practices:

1. Explicitly Manage Thread Signal Masks#

  • Block non-essential signals in all threads except the one responsible for handling them (e.g., a dedicated "signal thread").
  • Use pthread_sigmask() to ensure the thread blocked in recv() does not block the target signal (e.g., SIGINT).

2. Avoid SA_RESTART for Interruptible System Calls#

If you need recv() to interrupt on signals, never use SA_RESTART. Explicitly handle EINTR in your code:

ssize_t n;
do {
    n = recv(sockfd, buf, sizeof(buf), 0);
} while (n == -1 && errno == EINTR); // Retry on EINTR if needed

3. Use Dedicated Signal Threads#

For complex applications, create a dedicated thread to handle signals. This thread can block indefinitely in sigwait() (or sigwaitinfo()) to explicitly catch signals, avoiding race conditions with other threads.

Example:

void *signal_thread(void *arg) {
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    pthread_sigmask(SIG_BLOCK, &mask, NULL); // Block SIGINT in this thread
 
    int signum;
    while (1) {
        sigwait(&mask, &signum); // Wait for SIGINT
        if (signum == SIGINT) {
            printf("Signal thread received SIGINT; exiting...\n");
            // Trigger cleanup and exit
            exit(0);
        }
    }
}

4. Prefer Asynchronous I/O Over Blocking Calls#

For high-reliability applications, use asynchronous I/O (e.g., epoll, kqueue, or select()) instead of blocking recv(). Asynchronous models avoid blocking entirely, making signal handling more predictable.

7. Conclusion#

recv() not being interrupted by SIGINT in multithreaded environments is almost always due to misconfigured signal delivery or thread signal masks. By understanding that signals target threads based on their signal masks, ensuring the recv() thread is eligible to receive SIGINT, and avoiding SA_RESTART, you can reliably trigger EINTR and handle interruptions gracefully.

Remember: In threads, signals are routed to the first available unblocked thread. To interrupt recv(), ensure the thread blocked in recv() is the only one unblocked for SIGINT.

8. References#

  • recv(2): Linux manual page for recv() system call.
  • sigaction(2): Linux manual page for sigaction() (signal handler setup).
  • pthread_sigmask(3): Linux manual page for thread signal mask management.
  • signal(7): Linux manual page for signal concepts (includes multithreaded behavior).
  • pthreads(7): Linux manual page for POSIX threads (includes signal handling).
  • GNU C Library: Signal Handling in Threads