Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

sigsuspend vs additional signals delivered during handler execution

sigsuspend changes the signal mask, suspends execution of the calling thread until it receives a "signal whose action is either to execute a signal-catching function or to terminate the process", and then (if the process is not terminated and the signal handler returns) restores the signal mask to its original state.

The linked page of POSIX.1-2008 doesn't say whether it's possible for multiple signals to be delivered within a single call to sigsuspend, nor does it say anything about the atomicity of the signal mask changes; i.e. it appears to me that this is a conforming implementation of sigsuspend, even though the whole point of sigsuspend is that it doesn't have the race condition that this code does:

int sigsuspend(const sigset_t *mask)
{
    sigset_t oldmask;
    if (sigprocmask(SIG_SETMASK, mask, &oldmask)) return -1;
    pause();
    if (sigprocmask(SIG_SETMASK, &oldmask, 0)) return -1;
    return -1;
}

The scenario I'm actually worried about is a program that uses SIGUSR1 to communicate with itself (it's a long story) and I need a way to be sure that the signal handler executes only once per internal call to sigsuspend, even if other processes on the same system send it signals.

So my questions are:

  1. Is there a requirement (in POSIX or any other relevant standard) to deliver at most one signal (of any kind) per call to sigsuspend?
  2. Is there a requirement (ditto) for sigsuspend to change the signal mask, suspend execution, and restore the signal mask atomically? That is, without any risk that a signal will be delivered "in between" the three system calls in the hypothetical user-space implementation above?

Since this is fairly abstract, below a test program that I would like always to print 1 and exit successfully, but am worried that under some circumstances it might print 2 or 0, hang until the alarm goes off, or crash. (C11 atomics used out of an overabundance of caution; technically you aren't allowed to read a volatile sig_atomic_t from a signal handler, only write to one.) It uses SIGUSR1 by default, and SIGRTMIN if you pass -r on the command line.

#define _XOPEN_SOURCE 700
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>

#ifndef ATOMIC_INT_LOCK_FREE
#error "This program uses atomic_uint from a signal handler."
#endif

static atomic_uint handler_call_count;
static pid_t self_pid;

static void perror_exit(const char *msg)
{
  perror(msg);
  exit(1);
}

static void handler(int signo)
{
  union sigval dummy;
  dummy.sival_int = 0;

  if (handler_call_count++ == 0)
    if (sigqueue(self_pid, signo, dummy))
      perror_exit("sigqueue");
}

int main(int argc, char **argv)
{
  sigset_t mask1, mask2;
  struct sigaction sa;
  int signo;
  union sigval dummy;

  if (argc > 1 && !strcmp(argv[1], "-r"))
    signo = SIGRTMIN;
  else
    signo = SIGUSR1;

  sigemptyset(&mask1);
  sigemptyset(&sa.sa_mask);

  sigaddset(&mask1, signo);
  sigaddset(&sa.sa_mask, signo);

  if (sigprocmask(SIG_BLOCK, &mask1, &mask2))
    perror_exit("sigprocmask");

  sigdelset(&mask2, SIGALRM);
  sigdelset(&mask2, signo);

  sa.sa_handler = handler;
  sa.sa_flags = SA_RESTART;
  if (sigaction(signo, &sa, 0))
    perror_exit("sigaction");

  self_pid = getpid();
  dummy.sival_int = 0;
  if (sigqueue(self_pid, signo, dummy))
    perror_exit("sigqueue");

  alarm(5);
  sigsuspend(&mask2);
  alarm(0);

  printf("%u\n", atomic_load(&handler_call_count));
  return 0;
}
like image 616
zwol Avatar asked Nov 14 '16 15:11

zwol


2 Answers

1) Is there a requirement for handling at most one signal?

No. In fact, the opposite is recommended. POSIX 2008's Volume 4: Rationale, §B.2.4.1 Signal Generation and Delivery states:

When there are multiple pending signals that are not blocked, implementations should arrange for the delivery of all signals at once, if possible. Some implementations stack calls to all pending signal-catching routines, making it appear that each signal-catcher was interrupted by the next signal. In this case, the implementation should ensure that this stacking of signals does not violate the semantics of the signal masks established by sigaction(). Other implementations process at most one signal when the operating system is entered, with remaining signals saved for later delivery. Although this practice is widespread, this behavior is neither standardized nor endorsed. In either case, implementations should attempt to deliver signals associated with the current state of the process (for example, SIGFPE) before other signals, if possible.

Moreover, it is possible for multiple signals with the same signal number to be queued simultaneously. POSIX 2008 Volume 2: System Interfaces, §2.4.1 Signal Generation and Delivery states

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. The order in which multiple, simultaneously pending signals outside the range SIGRTMIN to SIGRTMAX are delivered to or accepted by a process is unspecified.

This also applies to real-time signals. POSIX 2008 Volume 2: System Interfaces, §2.4.2 Realtime Signal Generation and Delivery states

[...] Multiple occurrences of signals so generated are queued in FIFO order. [...]

If, when a pending signal is delivered, there are additional signals queued to that signal number, the signal shall remain pending. Otherwise, the pending indication shall be reset.

2) Is there a requirement for atomicity?

Legitimately Debatable. It was probably intended, but it's not actually spelled out normatively. There are good arguments in favour and against:

The case for atomicity:

POSIX 2008 does strongly imply atomicity in Volume 2: System Interfaces, §3 pause():

APPLICATION USAGE

Many common uses of pause() have timing windows. The scenario involves checking a condition related to a signal and, if the signal has not occurred, calling pause(). When the signal occurs between the check and the call to pause(), the process often blocks indefinitely. The sigprocmask() and sigsuspend() functions can be used to avoid this type of problem.

A similar strong implication appears under the RATIONALE heading for sleep(). Additionally, the word can is defined in POSIX 2008 Volume 1: Base Definitions, §1.5 Terminology` as:

For the purposes of POSIX.1-2008, the following terminology definitions apply:

can

Describes a permissible optional feature or behavior available to the user or application. The feature or behavior is mandatory for an implementation that conforms to POSIX.1-2008. An application can rely on the existence of the feature or behavior.

And POSIX 2008's Volume 4: Rationale, §A.1.5 Terminology states:

may

The use of may has been limited as much as possible, due both to confusion stemming from its ordinary English meaning and to objections regarding the desirability of having as few options as possible and those as clearly specified as possible.

The usage of can and may were selected to contrast optional application behavior (can) against optional implementation behavior (may).

That is to say, pause()'s APPLICATION USAGE heading states that an application is not obliged to call sigsuspend() to avoid timing problems, but should it choose to, then POSIX.1-2008-conformant implementations of sigsuspend() are required to avoid said timing-window problems (e.g. be atomic, with the help of the kernel).

If sigsuspend() were not atomic, then

  • The statement that an application can avoid timing window problems by using sigsuspend() is false, and thus all versions of the POSIX.1 standard since the first one in 1988 are internally inconsistent.
  • sigsuspend() would be useless, since it would serve no purpose beyond pause(). One would then have to ask why would POSIX.1 include both since the very first version, along with a false recommendation to use sigsuspend() in preference to pause().

The case against atomicity:

POSIX 2008 Volume 2: System Interfaces, §1.2 Format of Entries states:

[...]

APPLICATION USAGE

This section is informative.

This section gives warnings and advice to application developers about the entry. In the event of conflict between warnings and advice and a normative part of this volume of POSIX.1-2008, the normative material is to be taken as correct.

RATIONALE

This section is informative.

This section contains historical information concerning the contents of this volume of POSIX.1-2008 and why features were included or discarded by the standard developers.

[...]

This means that these sections are informative, not normative, and only normative sections can directly impose requirements on the implementor. However, they can help in interpreting the Standard, and the statement that "The sigprocmask() and sigsuspend() functions can be used to avoid this type of problem" does not conflict with any normative portion of the Standard.

Neutral case

POSIX 2008's Volume 4: Rationale, §A.1.5 Terminology states:

implementation-defined

This definition is analogous to that of the ISO C standard and, together with ‘‘undefined’’ and ‘‘unspecified’’, provides a range of specification of freedom allowed to the interface implementor.

[...]

unspecified

See implementation-defined.

[...]

In many places POSIX.1-2008 is silent about the behavior of some possible construct. For example, a variable may be defined for a specified range of values and behaviors are described for those values; nothing is said about what happens if the variable has any other value. That kind of silence can imply an error in the standard, but it may also imply that the standard was intentionally silent and that any behavior is permitted. There is a natural tendency to infer that if the standard is silent, a behavior is prohibited. That is not the intent. Silence is intended to be equivalent to the term ‘‘unspecified’’.

The ISO C Standard (presently, C11) defines "implementation-defined behaviour" as behaviour where the implementation can choose, but must document its choice.

The absence of any normative statement on the atomicity of sigsuspend() is perhaps either an error or equivalent to an explicit statement that it is implementation-defined behaviour.

  • The application notes and rationale in pause() and sleep() suggest that it is an error in the Standard.
  • But conversely, an implicit normative statement that sigsuspend()'s atomicity is implementation-defined takes precedence over an informative statement to the contrary.

Implementations

So can we inspire ourselves from in-the-wild implementations? Perhaps. I know of no implementations that don't implement it atomically:

  • Linux kernels implement sigsuspend() atomically.
  • BSD kernels implement sigsuspend() atomically.
like image 59
Iwillnotexist Idonotexist Avatar answered Sep 21 '22 14:09

Iwillnotexist Idonotexist


This is not an answer, but an exploratory program. I'm hoping that a kind macOS and/or *BSD user might test it on their machines, and report their results.


Just out of interest, I wrote a crude program to assess how often more than one signal is delivered per call to sigsuspend().

The idea in the following program is to let the user specify the signal on the command line. The process will fork a child process.

The child process blocks that signal, then installs a signal handler that atomically (using GCC built-ins) increments a counter. It will then enter into a loop, where it stops itself (via raise(SIGSTOP)). The parent process will detect this, and send a cluster of the signals, then SIGCONT. When the child process wakes up, it calls sigsuspend() until no more signals are pending. For each sigsuspend() call, the number of delivered signals is counted. After there are no more signals pending, the child stops itself again.

The parent sends half the (copies of the) signal using kill(), the rest using sigqueue() with a varying payload.

This is an utter hack, may not at all follow the POSIX.1 standard, and probably contains lots of bugs. (If you notice any, please let me know in a comment, so I can fix it.)

#define  _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

#ifndef   CLUSTER
#define   CLUSTER 8
#endif

#ifndef   INTERVAL
#define   INTERVAL 100000
#endif

static const struct {
    const int  number;
    const char name[8];
} signal_list[] = {
    { SIGHUP,     "HUP"     },
    { SIGINT,     "INT"     },
    { SIGQUIT,    "QUIT"    },
    { SIGILL,     "ILL"     },
    { SIGABRT,    "ABRT"    },
    { SIGFPE,     "FPE"     },
    { SIGSEGV,    "SEGV"    },
    { SIGPIPE,    "PIPE"    },
    { SIGALRM,    "ALRM"    },
    { SIGTERM,    "TERM"    },
    { SIGUSR1,    "USR1"    },
    { SIGUSR2,    "USR2"    },
    { SIGTSTP,    "TSTP"    },
    { SIGTTIN,    "TTIN"    },
    { SIGTTOU,    "TTOU"    },
    { SIGBUS,     "BUS"     },
    { SIGPOLL,    "POLL"    },
    { SIGPROF,    "PROF"    },
    { SIGSYS,     "SYS"     },
    { SIGTRAP,    "TRAP"    },
    { SIGURG,     "URG"     },
    { SIGVTALRM,  "VTALRM"  },
    { SIGXCPU,    "XCPU"    },
    { SIGXFSZ,    "XFSZ"    },
    { SIGIO,      "IO"      },
    { SIGPWR,     "PWR"     },
    { SIGWINCH,   "WINCH"   },
    { -1,         ""        }
};


static volatile sig_atomic_t done = 0;

static void handle_done(int signum)
{
    done = 1;
}

static int install_done(int signum)
{
    struct sigaction act;

    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);
    act.sa_handler = handle_done;
    act.sa_flags = 0;
    if (sigaction(signum, &act, NULL) == -1)
        return errno;

    return 0;
}

static unsigned long counter = 0UL;
static void          increment_counter(void)     { __atomic_add_fetch(&counter, 1UL, __ATOMIC_SEQ_CST); }
static unsigned long get_and_clear_counter(void) { return __atomic_fetch_and(&counter, 0UL, __ATOMIC_SEQ_CST); }

static void handle_counter(int signum)
{
    increment_counter();
}

static int install_counter(int signum)
{
    struct sigaction  act;

    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, signum);
    act.sa_handler = handle_counter;
    act.sa_flags = 0;
    if (sigaction(signum, &act, NULL) == -1)
        return errno;

    return 0;
}

int child_process(int signum)
{
    sigset_t      signals, no_signals, pending;
    unsigned long count, corrects, incorrects, cluster, noncluster, clustercount, nonclustercount;
    int           result, exitcode;

    sigemptyset(&no_signals);
    sigemptyset(&signals);
    sigaddset(&signals, signum);

    if (sigprocmask(SIG_BLOCK, &signals, NULL) == -1) {
        fprintf(stderr, "Child: Cannot block the signal: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    if (install_done(SIGINT) ||
        install_done(SIGHUP) ||
        install_done(SIGTERM) ||
        install_counter(signum)) {
        fprintf(stderr, "Child: Cannot install signal handlers: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    /* Ready to wait for signals to become pending. */
    exitcode = EXIT_SUCCESS;
    corrects = 0UL;         incorrects = 0UL;
    cluster = 0UL;          clustercount = 0UL;
    noncluster = CLUSTER;   nonclustercount = 0UL;

    raise(SIGSTOP);

    while (1) {

        if (done)
            return exitcode;

        sigemptyset(&pending);
        if (sigpending(&pending) == -1) {
            fprintf(stderr, "Child: Sigpending failed: %s.\n", strerror(errno));
            return EXIT_FAILURE;
        }

        if (!sigismember(&pending, signum)) {
            if (cluster != CLUSTER) {
                if (cluster != noncluster) {
                    fprintf(stderr, "Child: Signals are delivered in clusters of %lu signals; expected %d.\n", cluster, CLUSTER);
                    noncluster = cluster;
                }
                nonclustercount++;
            } else
                clustercount++;
            if ((clustercount + nonclustercount) % INTERVAL == 0UL) {

                if (incorrects > 0UL)
                    printf("Child: %lu times out of %lu times only one signal is delivered per sigsuspend() call.\n", corrects, corrects + incorrects);
                else
                    printf("Child: All %lu times sigsuspend() was called, only one signal was delivered.\n", corrects);

                if (clustercount > 0UL && nonclustercount > 0UL)
                    printf("Child: In %lu of %lu sets of signals, all %d copies of the signal were delivered.\n", clustercount, clustercount + nonclustercount, CLUSTER);
                else
                if (clustercount > 0UL)
                    printf("Child: In all %lu sets of signals, all %d copies of the signal were delivered.\n", clustercount, CLUSTER);

                fflush(stdout);
            }
            cluster = 0UL;
            raise(SIGSTOP);
        }

        if (done)
            return exitcode;

        result = sigsuspend(&no_signals);
        if (result != -1 || errno != EINTR) {
            printf("Child: sigsuspend() returned %d, expected -1 with errno == EINTR!\n", result);
            return EXIT_FAILURE;
        }
        if (done)
            return exitcode;

        count = get_and_clear_counter();
        cluster += count;
        if (count != 1UL) {
            printf("Child: Received %lu signals on one sigsuspend() call!\n", count);
            fflush(stdout);
            exitcode = EXIT_FAILURE;
            ++incorrects;
        } else
            ++corrects;
    }
}

int parse_signum(const char *name)
{
    unsigned int u;
    int          i;
    char         c;

    if (!name || !*name) {
        errno = EINVAL;
        return -1;
    }

    if (name[0] == 'S' &&
        name[1] == 'I' &&
        name[2] == 'G' &&
        name[3] != '\0')
        for (i = 0; signal_list[i].number >= 0; i++)
            if (!strcmp(name + 3, signal_list[i].name))        
                return signal_list[i].number;

    for (i = 0; signal_list[i].number >= 0; i++)
        if (!strcmp(name, signal_list[i].name))        
            return signal_list[i].number;

    if ((sscanf(name, " SIGRT%u %c", &u, &c) == 1 ||
         sscanf(name, " RT%u %c", &u, &c) == 1 ||
         sscanf(name, " SIGRTMIN+%u %c", &u, &c) == 1 ||
         sscanf(name, " RTMIN+%u %c", &u, &c) == 1) &&
         u <= (unsigned int)(SIGRTMAX - SIGRTMIN))
        return SIGRTMIN + u;

    if ((sscanf(name, " SIGRTMAX-%u %c", &u, &c) == 1 ||
         sscanf(name, " RTMAX-%u %c", &u, &c) == 1) &&
         u <= (unsigned int)(SIGRTMAX - SIGRTMIN))
        return SIGRTMAX - u;

    errno = EINVAL;
    return -1;
}

int main(int argc, char *argv[])
{
    pid_t child, p;
    int   signum, i, status;

    if (argc != 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s [ -l | --list ]\n", argv[0]);
        fprintf(stderr, "       %s SIGNAL\n", argv[0]);
        fprintf(stderr, "\n");
        fprintf(stderr, "This program uses a stopped child process to see if\n");
        fprintf(stderr, "a single call to sigsuspend() can cause more than\n");
        fprintf(stderr, "one SIGNAL to be delivered.\n");
        fprintf(stderr, "\n");
        return EXIT_SUCCESS;
    }
    if (!strcmp(argv[1], "-l") || !strcmp(argv[1], "--list")) {
        fprintf(stderr, "List of known standard POSIX signals:\n");
        for (i = 0; signal_list[i].number >= 0; i++)
            fprintf(stderr, "\tSIG%-7s (%d)\n", signal_list[i].name, signal_list[i].number);
        fprintf(stderr, "POSIX realtime signals can be referred to as\n");
        fprintf(stderr, "\tSIGRTMIN+0      or SIGRTMAX-%d\n", SIGRTMAX-SIGRTMIN);
        fprintf(stderr, "\t                to\n");
        fprintf(stderr, "\tSIGRTMIN+%d     or SIGRTMAX-0\n", SIGRTMAX-SIGRTMIN);
        fprintf(stderr, "\n");
        return EXIT_SUCCESS;
    }

    if (install_done(SIGINT) ||
        install_done(SIGHUP) ||
        install_done(SIGTERM)) {
        fprintf(stderr, "Parent: Cannot install signal handlers: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    signum = parse_signum(argv[1]);
    if (signum < 0) {
        fprintf(stderr, "%s: Unknown signal.\n", argv[1]);
        return EXIT_FAILURE;
    }

    if (signum >= SIGRTMIN && signum <= SIGRTMAX)
        fprintf(stderr, "Using POSIX realtime signal number %d (SIGRTMIN%+d)\n", signum, signum-SIGRTMIN);
    else
        fprintf(stderr, "Using standard POSIX signal number %d.\n", signum);

    child = fork();
    if (child == (pid_t)-1) {
        fprintf(stderr, "Cannot fork a child process: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    } else
    if (!child)
        return child_process(signum);

    /* Parent process. */
    while (!done) {

        /* Wait for child to become stopped or continued. */
        while (!done) {

            do {
                p = waitpid(child, &status, WUNTRACED | WCONTINUED);
            } while (!done && p == (pid_t)-1 && errno == EINTR);
            if (done)
                break;

            if (p == (pid_t)-1) {
                if (errno == EINTR)
                    continue;
                fprintf(stderr, "Parent: Child process vanished: %s.\n", strerror(errno));
                return EXIT_FAILURE;
            } else
            if (p != child)
                continue;

            if (WIFSTOPPED(status) || WIFCONTINUED(status))
                break;

            if (WIFEXITED(status)) {
                if (WEXITSTATUS(status))
                    fprintf(stderr, "Parent: Child exited with exit status %d.\n", WEXITSTATUS(status));
                else
                    fprintf(stderr, "Parent: Child exited successfully.\n");
            } else
            if (WIFSIGNALED(status))
                fprintf(stderr, "Parent: Child died from signal %d.\n", WTERMSIG(status));
            else
                fprintf(stderr, "Parent: Lost child process.\n");
            return EXIT_FAILURE;
        }

        if (done)
            break;

        if (WIFSTOPPED(status)) {

            /* Send child a total of CLUSTER signals.
               Half of them using sigqueue(), half via kill().
            */
            i = 0;
            while (i < CLUSTER) {
                union sigval sv;
                sv.sival_int = ++i;
                sigqueue(child, signum, sv);
                if (i++ < CLUSTER)
                    kill(child, signum);
            }

            /* Wake child up. */
            kill(child, SIGCONT);
        }

    }

    /* Tell the child process to terminate. */
    kill(child, SIGCONT);
    kill(child, signum);
    kill(child, SIGTERM);

    while (1) {
        p = waitpid(child, &status, 0);
        if (p == (pid_t)-1) {
            if (errno == EINTR)
                continue;
            return EXIT_FAILURE;
        }

        if (p == child)
            return status; /* HACK to return the child process status as-is */
    }
}

Saving the above as e.g. hack.c, and compiling and running it using

gcc -Wall -O2 hack.c -o hack
./hack SIGUSR1

provides the output for the case the OP is worried about. In Linux on x86-64 architecture (kernel 4.4.0, GCC 5.4.0) it outputs something like

Using standard POSIX signal number 10.
Child: Signals are delivered in clusters of 1 signals; expected 8.
Child: All 100000 times sigsuspend() was called, only one signal was delivered.
Child: All 200000 times sigsuspend() was called, only one signal was delivered.
Child: All 300000 times sigsuspend() was called, only one signal was delivered.
Child: All 400000 times sigsuspend() was called, only one signal was delivered.

The above output shows that all 400,000 times sigsuspend() was called, only one signal was delivered. (However, only one instance of the signal was delivered, even if the parent sent it 8 times: four with different sigqueue() payloads, four with kill()`.)

As I mentioned in a comment, based on the Linux kernel sources, I do believe the Linux kernel delivers only one signal per sigsuspend() call, if the signal is blocked before (and thus after) the sigsuspend() call. The above output supports that belief.

Running the same with a realtime signal, say ./hack SIGRTMIN+0, outputs something like

Using POSIX realtime signal number 34 (SIGRTMIN+0)
Child: All 800000 times sigsuspend() was called, only one signal was delivered.
Child: In all 100000 sets of signals, all 8 copies of the signal were delivered.
Child: All 1600000 times sigsuspend() was called, only one signal was delivered.
Child: In all 200000 sets of signals, all 8 copies of the signal were delivered.
Child: All 2400000 times sigsuspend() was called, only one signal was delivered.
Child: In all 300000 sets of signals, all 8 copies of the signal were delivered.
Child: All 3200000 times sigsuspend() was called, only one signal was delivered.
Child: In all 400000 sets of signals, all 8 copies of the signal were delivered.

which shows that on this run, every single instance of the signal was delivered, exactly once per sigsuspend() call.

This program cannot provide any sort of proof that a system works as the OP hopes (or that Linux works how I believe it works based on the kernel sources). It can only be used to disprove it (and on Linux, my belief). If there is a system that reports

Child: Received # signals on one sigsuspend() call!

with # being 2 or greater, then we know that on that system, more than one (copy of the) signal is delivered per sigsuspend() call.

(The case of # being 0 should only occur if the signal chosen is SIGINT, SIGHUP, or SIGTERM, these also being caught, so that the user can stop the program.)

It is of course not guaranteed that even if run on such a system, this program will manage to stumble upon the multiple-delivery case.

However, having the child process stop, get multiple (copies of a) signal, and then continued, would be the optimal point for a system to deliver more than one (copy of the) signal per sigsuspend() call. If it does not do that in this case, what would be the case where it would?

like image 39
Nominal Animal Avatar answered Sep 21 '22 14:09

Nominal Animal