1

I have found a problem where it asks to explain the behaviour of the following program:

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

void sig_alrm(int n) {
    write(2, "ALARM!\n", 7);
    return;
}

int main() {
    int fd[2], n;
    char message[6], *s;

    s = "HELLO\n";
    signal(SIGALRM, sig_alrm);
    pipe(fd);
    
    if (fork() == 0) {
        close(fd[1]);
        alarm(3);
        while ((n = read(fd[0], message, 6)) > 0);
        alarm(0);
        exit(0);
    }
    
    close(1);
    dup(fd[1]);
    close(fd[0]);
    close(fd[1]);
    
    while (1)
        write(1, s, 6);
}

It's basically a parent process with a shared pipe sending HELLO\n constantly via pipe to a child. The child sets up a SIGALRM in three seconds which will be caught by the sig_alrm function, and then proceeds to read indefinitely from the pipe.

If I understand correctly, SIGALRM should interrupt the read() system call, causing it to error out upon arriving, which would in turn cause the child to exit with an instant alarm with default behavior, and the parent to end too due to a SIGPIPE.

The problem is that I attempted to run the code, and both processes continue to read and write from the pipe happily after the SIGALRM arrives to the child.

Is there something I misunderstood from signal behaviors?

Lightsong
  • 312
  • 2
  • 8
  • Probably it's not sleeping in the read because the write is faster and there's always something to be read. Try slowing down the write side. –  Jan 15 '21 at 19:17
  • @dratenik thanks, but no luck. I tried writing every second or couple of seconds, and the same thing happens. – Lightsong Jan 15 '21 at 19:29
  • 1
    I bet your OS's `signal()` implementation installs a restartable handler. Use `sigaction()` instead. – Shawn Jan 15 '21 at 20:05
  • See https://stackoverflow.com/a/65478753/9952196 for details. – Shawn Jan 15 '21 at 20:10

1 Answers1

2

The behavior of signal varies from platform to platform, and you should instead use its successor sigaction, which was standardized in POSIX-1.1988.

On your platform and with your build instructions, signal() installs user handlers in a "restartable" fashion, meaning that interruptable system calls will simply be restarted rather than fail with EINTR. Your read is interrupted by the handler, but then resumes. Some platforms and/or build semantics do not do this.

sigaction resolves this ambiguity by providing a flag, SA_RESTART, which controls whether or not interrupted syscalls are, in fact, restarted. It standardizes other historically divergent behaviors, too.

For what it's worth, when I gcc-compile your code on an older Linux system without specifying any feature macros, an strace reveals that signal is, in fact, implemented in terms of a (real-time) sigaction call with SA_RESTART, which explains the behavior you see:

$ strace -fe trace=\!write,read ./so65742182
....
munmap(0x7f8ddffaf000, 70990)           = 0
rt_sigaction(SIGALRM, {0x013370, [ALRM], SA_RESTORER|SA_RESTART, ...
#                                                    ^^^^^^^^^^
pipe([3, 4])
....
pilcrow
  • 56,591
  • 13
  • 94
  • 135
  • *`sigaction` resolves this ambiguity by providing a flag, `SA_RESTART`, which controls whether or not interrupted syscalls are, in fact, restarted.* Not all system calls will be restarted. In general, only system calls specified by the implementation will be restarted. On Linux, those are listed on the [`signal.7` man page](https://man7.org/linux/man-pages/man7/signal.7.html). – Andrew Henle Jan 15 '21 at 22:36
  • @AndrewHenle, [yup](https://stackoverflow.com/questions/13357019/how-to-know-if-a-linux-system-call-is-restartable-or-not). – pilcrow Jan 15 '21 at 23:13
  • I see, that explains it, thank you! Indeed sigaction is better and should be used, but we are studying both in case we find legacy code. – Lightsong Jan 16 '21 at 07:59