0

I need to send SIGABRT to all children of my process in a signal handler of this process.

in this answer and in this answer and in some others, it is suggested to go through all processes' directories in /proc. UPD: I have just discovered even a better /proc way.

For this, one needs opendir, readdir, closedir, etc. But they are not async-signal-safe. In this answer, it is explained that this is because "opendir() calls malloc(), so you can't run it from within the handler". As far as I understand, the problem is that malloc() can be called by the same thread two times: in the handler and in the working code of the thread. So you get a deadlock, as it happens in this question: malloc inside linux signal handler cause deadlock.

To solve a similar problem Is there an async-signal-safe way of reading a directory listing on Linux?, it is suggested:

If you know in advance which directory you need to read, you could call opendir() outside the signal handler (opendir() calls malloc(), so you can't run it from within the handler) and keep the DIR* in a static variable somewhere.

But in my case I don't know the directories beforehand, as I don’t know which children processes with which pids will exist at the point when the signal handler is called.

Is there any way to safely find and kill all the children processes of my process in its signal handler? Thank you for attention.

JenyaKh
  • 2,040
  • 17
  • 25
  • 1
    Another approach that might be relevant: You can use the Linux-specific `prctl()` syscall in the child processes to arrange for them to get a signal when their parent process terminates, instead of having the parent explicitly send them a signal. – Shawn Mar 18 '23 at 06:48
  • Alas, I don't think that I can change the code of children. I also doubt that I would be able to add the process to a new program group, though I will have to investigate. I hoped for a more general solution but without the need to use opendir, etc. – JenyaKh Mar 18 '23 at 07:20
  • Is [killpg()](https://man7.org/linux/man-pages/man3/killpg.3.html) what you are looking for? – Jesper Juhl Mar 18 '23 at 16:08
  • Thank you for the comment, @JesperJuhl! However, I don't understand: how is it different from the answer of @Shawn? It looks like kill() may kill a process group, is not it? – JenyaKh Mar 18 '23 at 18:00

1 Answers1

3

The easy way is to use a process group for all the children and send a signal to all processes in it at once.

Example program:

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

// Called by the parent on a SIGUSR1
void parent_handler(int signo __attribute__((__unused__))) {
  const char msg[] = "SIGUSR1 received in parent.\n";
  write(STDOUT_FILENO, msg, sizeof msg);
  kill(0, SIGABRT); // Send signal to all processes in its group
}

// Called by the children on a SIGABRT
void child_abort_handler(int signo __attribute__((__unused__))) {
  const char msg[] = "SIGABRT received in child.\n";
  write(STDOUT_FILENO, msg, sizeof msg);
  _exit(0);
}

// Children processes set up a signal handler and wait for a signal
void do_child(void) {
  struct sigaction act;
  memset(&act, 0, sizeof act);
  act.sa_handler = child_abort_handler;
  sigaction(SIGABRT, &act, NULL);
  pause();
}

int main(void) {
  // Make sure this process is in a new process group
  if (setpgid(0, 0) < 0) {
    perror("setpgid");
    return EXIT_FAILURE;
  }

  // Create some child processes
  for (int i = 0; i < 5; i++) {
    pid_t child = fork();
    if (child < 0) {
      perror("fork");
    } else if (child > 0) {
      do_child();
    }
  }

  // Block SIGABRT in parent
  sigset_t mask;
  sigemptyset(&mask);
  sigaddset(&mask, SIGABRT);
  sigprocmask(SIG_BLOCK, &mask, NULL);

  // Set up parent SIGUSR1 handler
  struct sigaction act;
  memset(&act, 0, sizeof act);
  act.sa_handler = parent_handler;
  if (sigaction(SIGUSR1, &act, NULL) < 0) {
    perror("parent sigaction");
    kill(0, SIGTERM);
    return EXIT_FAILURE;
  }

  printf("Please, type: kill -10 %d\n", (int)getpid());
  fflush(stdout);

  pause();

  // Exit normally after the sighandler and pause return.
  return 0;
}

and usage:

$ gcc -Wall -Wextra -O -o sigtest sigtest.c
$ ./sigtest &
Please kill -USR1 12345
$ kill -USR1 12345
SIGUSR1 received in parent.
SIGABRT received in child.
SIGABRT received in child.
SIGABRT received in child.
SIGABRT received in child.
SIGABRT received in child.
$
JenyaKh
  • 2,040
  • 17
  • 25
Shawn
  • 47,241
  • 3
  • 26
  • 60
  • May I ask, if there is a possibility that the process would send SIGABRT also to it very self this way? This would end up in a cycle. I see in your output that it does not happen, but not sure why. (BTW, I read that while in handler the processed signal cannot trigger the handler again. But after the handler finishes, I think it could trigger it again.) – JenyaKh Mar 18 '23 at 09:40
  • Why would there be a cycle? The signal being handled is automatically masked while the handler is running, and this example exits in the handler. Maybe I should have had the parent catch a different signal if using SIGABRT for both is too confusing. – Shawn Mar 18 '23 at 11:03
  • This is just in my case, the process would get another signal (a custom signal from completely another process) and then in the handler it would need to abort its children. So I am afraid that its call to `kill(0, SIGABRT)` may send SIGABRT not only to its children but to the process itself too. – JenyaKh Mar 18 '23 at 11:13
  • Also here https://www.gnu.org/software/libc/manual/html_node/Blocking-for-Handler.html it's written: "When a handler function is invoked on a signal, that signal is automatically blocked (in addition to any other signals that are already in the process’s signal mask) during the time the handler is running. If you set up a handler for SIGTSTP, for instance, then the arrival of that signal forces further SIGTSTP signals to wait during the execution of the handler." But it looks like though the signal gets blocked, it may come again after the handler is done. – JenyaKh Mar 18 '23 at 11:16
  • So just use a different signal. Or mask it in the parent and use other means to tell when it's received (signalfd, sigtimedwait(), etc.) – Shawn Mar 18 '23 at 11:18
  • 1
    @JenyaKh: Note that this only happens if you do not specify `SA_NODEFER` in the sigaction call. However, in any case, blocking a signal does not cause it to be ignored -- it merely defers handling it until the signal is unblocked. In the example code, the signal is never unblocked, so won't be delivered, but if it is blocked do to handling a previous instance of the signal, when the signal handler completes, it will be unblocked and handled again. – Chris Dodd Mar 18 '23 at 21:03
  • 1
    Note that this will not send the signal to children who have moved themselves to a new process group. In addition it *will* send the signal to grandchildren and such that are in the process group. Both of these behaviors may actually be desirable. – Chris Dodd Mar 18 '23 at 21:06
  • Thank you for comments, @ChrisDodd! And thank you, @Shawn, for rewriting the example with SIGUSR1! Also, I understand now why in the previous variant SIGABRT would not come to the parent twice: I didn't notice the `exit()` in its handler. – JenyaKh Mar 19 '23 at 04:08