为什么我的程序在按下Ctrl+C后,直到我在终端中按下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的回答以获得进一步的解释(。因此,只有在按下回车键(释放fgetc()
(后,才会评估keepRunning。
终端也是缓冲的,所以只有当你按下回车键时,它才会将字符发送到FILE *
缓冲区,并由fgetc()
逐个读取。这就是为什么它只在按下回车键后才存在,而不是其他键。
"解决"它的几个选项之一是使用非阻塞的stdin、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 caughtn");
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 reachedn");
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一起工作
上述程序仅用于教育目的,不用于"生产"用途。有几个问题需要注意,例如:
- 所有libc/系统调用都需要测试错误
- 如果输出比输入慢(
printf()
可能很容易慢(,它可能会导致饥饿,并且信号不会被捕获(只有在输入过慢/过慢之后,内环才会退出( - 系统调用的性能/减少:
read()
可以填充更大的缓冲区epoll_wait
可以返回多个事件,而不是1个
通常,如果系统调用在阻塞时传递了信号,则系统调用会返回errno == EINTR
,这将导致一旦命中Control-C,fgetc
就会提前返回并出现错误。问题是signal
设置的信号将被设置为自动重新启动模式,即一旦信号处理程序完成,底层read
系统调用就会重新启动。
正确的解决方案是删除自动重启,但它确实使正确使用变得稍微困难。在这里,我们看看返回值是否是fgetc
中的EOF
,然后看看它是否是由EINTR
引起的,如果布尔值不为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;
}
}