1

I am currently trying to write a program that calls fork() to spawn a child process which sends a random number of signals to the parent process. Both the child and the parent process should show the same number, but I have an issue with blocking the signals when incrementing the counter.

I tried multiple methods of blocking the signals but I have failed. Anybody with a suggestion? Thanks a lot.

int nreceived = 0;
void handler(int sig)
{
    nreceived++;
    signal(SIGUSR1,handler);
}
int main()
{
    int nsignals;
    pid_t pid;

    srand(time(NULL));
    nsignals = rand() % 256;
    signal(SIGUSR1,handler);

    if((pid = fork()) > 0)
    {
        wait(NULL);
        printf("Received %d signals from process %d\n",nreceived,pid);
    }
    else if (pid == 0)
    {
         for(int i = 0; i < nsignals; i++)
            kill(getppid(),SIGUSR1);
        printf("Sent %d signals to process %d\n", nsignals, getppid());
    }
    
    return 0;
}
  • "blocking" or "receiving"? It sounds like you are using the word "blocking" incorrectly. – William Pursell Apr 17 '22 at 12:32
  • Since you are on linux, you probably want to look at `signalfd` – William Pursell Apr 17 '22 at 12:33
  • 2
    Use `sigaction()` rather than `signal()`. – Jonathan Leffler Apr 17 '22 at 12:35
  • I tried with sigaction() as well as sigemptyset() and sigaddset() and sigblock(), but I can't figure out how to block the signals only when the counter is incremented. – Daniel Wagner Apr 17 '22 at 12:36
  • 1
    there is nothing to prevent the child from sending all signals before the parent has received the first one. – stark Apr 17 '22 at 12:37
  • 1
    @WilliamPursell — I think the problem is that multiple signals are sent by the child while the patent is responding to the first, so the desire is to block signal delivery while in the handler. – Jonathan Leffler Apr 17 '22 at 12:38
  • Are you constrained to use SIGUSR1? Could you use one of the “real-time” signals instead? – Jonathan Leffler Apr 17 '22 at 12:39
  • But if the signal is blocked, then multiple instances of the signal will coalesce. You cannot count signals accurately. – William Pursell Apr 17 '22 at 12:40
  • Is there any method to count the signals accurately? Yes, I am constrained to using SIGUSR1. – Daniel Wagner Apr 17 '22 at 12:41
  • 2
    @DanielWagner The non-real-time signals such as `SIGUSR1` are subject to being coalesced - if you receive multiple signals, the receiving process may only "see" one. There's no real way to reliably count signals like this. You'd need to implement something like two-way signaling - the child process won't send another signal until the parent signals that it's received the previous one. – Andrew Henle Apr 17 '22 at 12:46
  • 1
    See [**2.4.1 Signal Generation and Delivery**](https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_01): "If a subsequent occurrence of a pending signal is generated, it is implementation-defined as to whether the signal is delivered or accepted more than once in circumstances other than those in which queuing is required." – Andrew Henle Apr 17 '22 at 12:52
  • Are you constrained to send the signals as fast as possible? If you add a microsecond delay between signals (`nanosleep()`), does that give better results? – Jonathan Leffler Apr 17 '22 at 12:55
  • I should somehow use the standard signal library functions to block the signal while the handler is executing, using something like nanosleep() is not an option unfortunately. – Daniel Wagner Apr 17 '22 at 12:58
  • 3
    You are being set up to lose. You can use `sigaction()` to maximise your chances (by blocking SIGUSR1 while the signal is being processed), but unless your system reliably queues all SIGUSR1 signals sent to a process, some of them are going to be lost as your child batters the parent with signals. I don't know if real-time signals support a queue of length 255. That's a helluva lot of signals that could be sent by the child. – Jonathan Leffler Apr 17 '22 at 13:09
  • 1
    @DanielWagner Per the [Linux `signal(7)` man page](https://man7.org/linux/man-pages/man7/signal.7.html): "Standard signals do not queue. If multiple instances of a standard signal are generated while that signal is blocked, then only one instance of the signal is marked as pending (and the signal will be delivered just once when it is unblocked). In the case where a standard signal is already pending, the `siginfo_t` structure (see `sigaction(2)`) associated with that signal is not overwritten on arrival of subsequent instances of the same signal. ..." – Andrew Henle Apr 17 '22 at 13:14

1 Answers1

2

As discussed extensively in the comments, it is important to use POSIX function sigaction() rather than the standard C function signal() because there are many implementation-defined aspects to signal() (primarily because there were many divergent implementations before the C standard was created, and the standard tried to accommodate existing implementations without breaking any of them).

However, the system is not obligated to queue signals that are not real-time signals (signal numbers in the range SIGRTMIN..SIGRTMAX). SIGUSR1 is not a real-time signal. Frankly, even with signal queueing, I'm not sure whether implementations would handle up to 255 pending signals of a specific type for a process — it isn't an area I've experimented with.

This is the best code I was able to come up with:

#include <assert.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#ifndef SEND_SIGNAL
#define SEND_SIGNAL SIGUSR1
#endif

static const char *arg0;
static volatile sig_atomic_t nreceived = 0;

static _Noreturn void err_syserr(const char *syscall);

static void handler(int sig)
{
    assert(sig == SEND_SIGNAL);
    nreceived++;
}

int main(int argc, char **argv)
{
    if (argc != 1)
    {
        fprintf(stderr, "Usage: %s\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    arg0 = argv[0];

    struct sigaction sa = { .sa_handler = handler, .sa_flags = SA_RESTART };
    /* Block all blockable signals */
    if (sigfillset(&sa.sa_mask) != 0)
        err_syserr("sigfillset");
    if (sigaction(SEND_SIGNAL, &sa, 0) != 0)
        err_syserr("sigaction");

    pid_t pid = fork();
    if (pid > 0)
    {
        int status;
        int corpse = wait(&status);
        if (corpse != -1)
            printf("Child process %d exited with status 0x%.4X\n", corpse, status);
        else
            fprintf(stderr, "%s: wait() failed: (%d) %s\n", argv[0], errno, strerror(errno));
        printf("Caught %d signals from process %d\n", nreceived, pid);
    }
    else if (pid == 0)
    {
        srand(time(NULL));
        int nsignals = rand() % 256;
        for (int i = 0; i < nsignals; i++)
            kill(getppid(), SEND_SIGNAL);
        printf("Sent %d signals to process %d\n", nsignals, getppid());
    }
    else
        err_syserr("fork");

    return 0;
}

static _Noreturn void err_syserr(const char *syscall)
{
    fprintf(stderr, "%s: %s() failed: (%d) %s\n", arg0, syscall, errno, strerror(errno));
    exit(EXIT_FAILURE);
}

When run as program sig53 (source code sig53.c) on a Mac running macOS Monterey 12.3.1, I got variable numbers of signals received:

$ sig53
Sent 50 signals to process 37973
Child process 37974 exited with status 0x0000
Caught 14 signals from process 37974
$: sig53
Sent 39 signals to process 38442
Child process 38443 exited with status 0x0000
Caught 16 signals from process 38443
$: sig53
Sent 28 signals to process 38478
Child process 38479 exited with status 0x0000
Caught 6 signals from process 38479
$

Sometimes, the number received reached near 100, but never very near to all the signals sent.

YMMV on Linux. There may be alternative mechanisms for handling signals on Linux. But for portable code, sending a myriad signals to a single process at full tilt is not a reliable way of communicating between processes. Some of the signals will be delivered, but it may not be all of them.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278