cheshire
cheshire

Reputation: 1159

Program won't end after catching SIGINT before pressing ENTER?

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?

Upvotes: 2

Views: 1354

Answers (2)

niry
niry

Reputation: 3308

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.

Upvotes: 2

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;
    }
}

Upvotes: 0

Related Questions