在按 ENTER 之前捕获 SIGINT 后程序不会结束?

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

为什么我的程序在终端中按下 Ctrl+C[=26 后直到按下 ENTER 才结束=]?

这是我的代码:

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

我已经设置了我的 while 循环以从 stdin 和 运行 读取字符,直到 SIGINT 被捕获。之后 keepRunning 将设置为 0 并且循环应该结束并终止程序。但是,当我按下 Ctrl+C 时,我的程序不再接受任何输入,但它不允许我在终端中键入任何命令,直到我按 ENTER 键。这是为什么?

这是因为 fgetc() 正在阻止执行,并且您选择处理 SIGINT 的方式 - fgetc() 不会被 EINTR 中断(请参阅@AnttiHaapala 的回答以获取更多解释)。因此只有在您按下 enter 后,释放 fgetc(),keepRunning 才会被评估。

终端也有缓冲,所以只有当你按下回车键时,它才会将字符发送到 FILE * 缓冲区,并由 fgetc() 一个接一个地读取。这就是为什么它只在按下回车后才存在,而不是其他键。

"solve" 的几个选项之一是使用非阻塞标准输入、signalfd 和 epoll(如果您使用 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);
}

另请注意,我没有使用 fgetc()。由于 FILE * 的缓冲特性,它不适用于非阻塞 IO。

以上程序仅用于教育目的,不得 "production" 使用。有几个问题需要注意,例如:

  • 所有 libc / 系统调用都需要进行错误测试。
  • 如果输出比输入慢(printf()可能很容易慢),可能会导致饥饿,信号不会被捕获(内循环只有在输入over/slower后才会退出) .
  • 系统调用的性能/减少:
    • read() 可以填充更大的缓冲区。
    • epoll_wait 可以 return 多个事件而不是 1 个。

通常系统调用 return 和 errno == EINTR 如果在它们阻塞时传递了信号,这将导致 fgetc 提前到 return 并出现错误情况Control-C 被击中后。问题是 signal 设置的信号将被设置为自动重启模式,即底层 read 系统调用将在信号处理程序完成后立即重新启动。

正确的解决方法是删除自动重启,但它确实使正确使用稍微有些棘手。在这里,我们查看 return 值是否是 fgetc 中的 EOF,然后它是否是由 EINTR 引起的,如果布尔值不正确,则重新启动循环。

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