2

Why does my program not end until I press ENTER in terminal after pressing Ctrl+C?

Here is my code:

static volatile sig_atomic_t keepRunning = 1;

void intHandler(int sig) 
{
    keepRunning = 0;
}

int main(int argc, char *argv[])
{
    signal(SIGINT, intHandler);

    int ch; 
    while((ch = fgetc(stdin)) && keepRunning)
    {
      ...
    }
    exit(EXIT_SUCCESS);
}

I have setup my while loop to read chars from stdin and to run until the SIGINT is caught. After that the keepRunning will be set to 0 and loop should end and terminate the program. However when I hit Ctrl+C my program doesn't accept any input anymore but it doesn't let me type any command in terminal until I press ENTER key. Why is that?

jww
  • 97,681
  • 90
  • 411
  • 885
cheshire
  • 1,109
  • 3
  • 15
  • 37

2 Answers2

2

It is because fgetc() is blocking the execution, and the way you chose to handle SIGINT - fgetc() will NOT be interrupted with EINTR (see @AnttiHaapala's answer for further explanation). So only after you press enter, which releases fgetc(), keepRunning is being evaluated.

The terminal is also buffered, so only when you press enter it will send the chars to the FILE * buffer and will read by fgetc() one by one. This is why it exists only after pressing enter, and not other keys.

One of several options to "solve" it is to use nonblocking stdin, signalfd and epoll (if you use linux):

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/epoll.h>
#include <sys/signalfd.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <error.h>

int main(int argc, char *argv[])
{
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);

    /* Block signals so that they aren't handled
       according to their default dispositions */
    sigprocmask(SIG_BLOCK, &mask, NULL); // need check

    // let's treat signal as fd, so we could add to epoll
    int sfd = signalfd(-1, &mask, 0); // need check

    int epfd = epoll_create(1); // need check

    // add signal to epoll
    struct epoll_event ev = { .events = EPOLLIN, .data.fd = sfd };
    epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev); // need check

    // Make STDIN non-blocking 
    fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK);

    // add STDIN to epoll
    ev.data.fd = STDIN_FILENO;
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); // need check

    char ch;
    int keepRunning = 1; // no need to synchronize anymore
    while(keepRunning) {
        epoll_wait(epfd, &ev, 1, -1); // need check, must be always 1
        if (ev.data.fd == sfd) {
            printf("signal caught\n");
            keepRunning = 0;
        } else {
            ssize_t r;
            while(r = read(STDIN_FILENO, &ch, 1) > 0) {
                printf("%c", ch);
            }
            if (r == 0 && errno == 0) { 
                /* non-blocking non-eof will return 0 AND EAGAIN errno */
                printf("EOF reached\n");
                keepRunning = 0;
            } else if (errno != EAGAIN) {
                perror("read");
                keepRunning = 0;
            }
        }
    }
    fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) & ~O_NONBLOCK);
    exit(EXIT_SUCCESS);
}

Also note that I'm not using fgetc(). Because of buffering nature of FILE *, it will not work well with nonblocking IO.

The program above is intended for education purposes only and not for "production" use. There are several issue that need attention, for example:

  • All the libc / system calls need to tested for errors.
  • If output is slower than input (printf() may easily be slower), it may cause starvation and the signal will not get caught (the inner loop will exit only after input is over/slower).
  • Performance / reduction of system calls:
    • read() can fill much larger buffer.
    • epoll_wait can return multiple events instead of 1.
niry
  • 3,238
  • 22
  • 34
0

Usually system calls return with errno == EINTR if a signal was delivered when they're blocking, which would cause fgetc to return early with an error condition as soon as Control-C was hit. The problem is that the signal set by signal will be set to auto restarting mode, i.e. the underlying read system call would be restarted as soon as the signal handler completed.

The correct fix would be to remove the automatic restart but it does make it slightly trickier to use correctly. Here we see if the return value is EOF from fgetc and then if it is caused by EINTR and restart the loop if the boolean was not true.

struct sigaction action = {
    .sa_flags = 0,
    .sa_handler = intHandler
};
sigaction(SIGINT, &action, NULL);

int ch;
while (1) {
    ch = fgetc(stdin);
    if (ch == EOF) {
        if (errno == EINTR) {
            if (keepRunning) {
                continue;
            }
            break;
        }
        break;
    }
}
  • not explaining the behavior of the original problem therefore not really answering the question. Your suggestion is to stop blocking calls which defeats the purpose of handling the signal in the manner the question suggest - finish processing the loop and only then exit. – niry Nov 16 '19 at 22:16
  • If the loop has any other blocking calls it will get cancelled too. – niry Nov 16 '19 at 22:22
  • @niry and your approach does not explain how to stop `fgetc`, it explains how to replace it. – Antti Haapala -- Слава Україні Nov 16 '19 at 22:23
  • true. the question is why it is happening (not how to stop `fgetc()`, which you are answering) I'm suggesting completely different approach to achieve what would be presumed a desired behavior. – niry Nov 16 '19 at 22:27
  • 1
    @niry and I did answer *why* it is happening: because the `signal` function sets a handler that is *auto-restarting*. It does not have anything to do with `fgetc` being blocking. – Antti Haapala -- Слава Україні Nov 16 '19 at 22:29
  • as I said the first comment, your suggestion with interrupting calls will defeat the purpose of handling interrupts in that manner... the whole idea on handling interrupt and setting a variable somewhere is to *control* when to leave the program, not to interrupt calls. – niry Nov 16 '19 at 22:35
  • 1
    @niry incorrect. The purpose of `EINTR` is to interrupt blocking calls, for this very reason. It does not defeat the purpose, it is exactly why `EINTR` exists. – Antti Haapala -- Слава Україні Nov 16 '19 at 22:36
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/202486/discussion-between-niry-and-antti-haapala). – niry Nov 16 '19 at 22:37