Search code examples
cvolatilesigintsignal-handling

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?


Solution

  • 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.