0

The following code should call the signal handler once the pipes are closed by the parent. The signal handler will be sporned by the child, cause it waits one second. This wait cycle should be interrupted by the signal handler.

Only output:

n=10, 'Hello Worl'  // perfect line ;), but the next line is missing.
// "signal received: ...  MISSING

How ever, the signal handler is not invoked. Can someone tell, why?

hint: As no Signal is processed, the child is left behind. Terminate it manually.

#include <csignal>
#include <cstdio>

#include <sys/socket.h>
#include <unistd.h>

void child(int socket) {
    while (true) {
        char buf[100];
        int n = read(socket, buf, 10);
        printf("n=%d, '%s'\n", n, buf);
        sleep(1);
    }
}

void parent(int socket) {
    char buf[] = "Hello World\n";
    write(socket, buf, sizeof(buf));

    close(socket);
}

int main() {
    int socket[2];
    socketpair(AF_LOCAL, SOCK_DGRAM, 0, socket);

    signal(SIGHUP, [](int sig) {
        printf("signal received: %d\n", sig);
    });

    switch (fork()) {
        case 0:
            close(socket[1]);
            child(socket[0]);
            break;

        default:
            close(socket[0]);
            parent(socket[1]);
            break;

        case -1:
            return 1;
    }

    return 0;
}
Cutton Eye
  • 3,207
  • 3
  • 20
  • 39
  • Assuming the hint in your question came from your instructor, have you considered invoking the `kill` function from the parent process? – Botje Mar 31 '23 at 08:41
  • thx for the comment. This is a bit more complex than a simple "homework". The point is, that a signal which is emitted on 95% of all tested machines is not emitted or maybe not processed. Eventually such magically missing signal would, if emitted, cause an immediate termination of the system as desired. The signal handler is an indicator of such missing signal process which in term leads on my machine to a daemon and on all other machines to the handlers message. The info is for those who compile that code, having the same situation and in turn unknown daemons in the background. – Cutton Eye Mar 31 '23 at 09:04
  • 1
    `SIGHUP` isn't sent when the parent dies, only the controlling terminal (if the parent dies, the child's new parent will be `init`). Closing one side of a socket also doesn't generate a signal, especially since your child is only reading, it will simply receive an EOF. – Homer512 Mar 31 '23 at 09:16
  • 2
    @Homer512 it's a DGRAM socket so the child has no way of knowing the parent closed its end. – Botje Mar 31 '23 at 11:03
  • 2
    @Botje oh, right, I was thinking of stream sockets. In any case, I think OP expects the behavior of [`prctl(PR_SET_PDEATHSIG, SIGHUP)`](https://stackoverflow.com/questions/284325/how-to-make-child-process-die-after-parent-exits/284443#284443) With the caveats mentioned in that thread, namely a race condition if the parent dies before the call is made – Homer512 Mar 31 '23 at 11:22
  • yes, the intention is, that if one side dies, the socketpair in term is destroyed and this should cause some kind of signal which should lead to a collapse of all started processes. – Cutton Eye Mar 31 '23 at 12:02

1 Answers1

1

the intention is, that if one side dies, the socketpair in term is destroyed and this should cause some kind of signal which should lead to a collapse of all started processes

Well, in that case a datagram socket is just about the only thing you cannot use.

If you want to continue using file descriptors for this and/or stay fully POSIX-compatible, a stream socket or pipe is what you can use.

#include <poll.h>

#include <sys/types.h>
#include <sys/socket.h>
// using socketpair

#include <unistd.h>
// using fork, sleep, close

#include <cstdio>
// using std::puts


int main()
{
  int sockets[2];
  socketpair(AF_UNIX, SOCK_STREAM, 0, sockets);
  if(fork()) { // parent
    close(sockets[1]);
    sleep(1);
    std::puts("Parent exit");
  }
  else { // child
    close(sockets[0]);
    pollfd poll_fd = { sockets[1], POLLIN, 0 };
    poll(&poll_fd, 1, 2000 /*timeout*/);
    if(poll_fd.revents & POLLIN)
      std::puts("Parent died before child");
    else
      std::puts("Child exit before parent");
  }
}

Personally, if I don't have need for a socket, I just use a pipe, keeping the write-side open on the parent and the read-side in the child. When the write-side closes (or on a stream socket if either side closes), reading will unblock (indicating EOF).

This approach is most useful if you have some sort of event loop since you can usually just plug it into those like I did here with the poll. If you don't and you want a signal instead, you may use SIGIO and interrupt-driven input but I find this rather tedious, especially for larger process trees.

If you are fine with a Linux-specific solution, prctl(PR_SET_PDEATHSIG, SIGHUP) is what you want.

#include <sys/types.h>
#include <unistd.h>
// using getpid, getppid, pause, sleep
#include <sys/prctl.h>

#include <csignal>
#include <cstdio>
// using std::puts. std::printf
#include <cstring>
// using std::strlen


int main()
{
  pid_t parent_id = getpid();
  if(fork()) { // parent
    sleep(1);
    std::puts("Parent exit");
  }
  else { // child
    std::signal(SIGHUP, [](int sig) {
      const char* msg = "Child received SIGHUP\n";
      // puts or printf are not signal-safe
      write(1 /*stdout*/, msg, std::strlen(msg));
      _exit(1); // std::exit is not signal-safe!
    });
    prctl(PR_SET_PDEATHSIG, SIGHUP);
    if(getppid() != parent_id) {
      std::puts("Parent died during child setup");
      return 0;
    }
    while(true)
      pause();
  }
}

Note this warning from the man-page:

the "parent" in this case is considered to be the thread that created this process. In other words, the signal will be sent when that thread terminates (via, for example, pthread_exit(3)), rather than after all of the threads in the parent process terminate.

BTW: Your use of printf in a signal handler was erroneous. None of the C stdio functions are async-signal-safe.

Homer512
  • 9,144
  • 2
  • 8
  • 25