Linux 终端:正确处理 Ctrl+C 输入避免 stdin 阻塞
2024-12-31 00:40:10
Linux:处理 Ctrl+C 中断 stdin 问题
在 Linux 系统上开发涉及终端输入输出的应用时,捕获 Ctrl+C 信号中断程序的运行是一种常见需求。 同样,对于一些特定的应用场景, 我们希望应用程序可以接收和处理包括 Ctrl+C 在内的各种键盘输入组合,而不是直接被系统中断。 本文将探讨一种在 Linux 中处理 Ctrl+C
输入时,可能导致标准输入(stdin)无法正常工作的状况, 并提供可行的解决方案。
问题分析
问题通常出现在以下情景:程序为了读取原始的、非规范的键盘输入而禁用了终端的规范模式 (canonical mode) 以及一些信号处理功能。 常见的代码片段如下所示,其中通过 tcsetattr
系统调用修改了终端的属性。 核心操作是禁用 ICANON
(启用规范模式),ISIG
(启用信号生成),IEXTEN
(扩展输入处理)以及回显。 IXON
和IXOFF
负责软件流控,在此也被禁用。
termios tOld, tNew;
tcgetattr(STDIN_FILENO, &tOld);
tNew = tOld;
tNew.c_lflag &= ~(ICANON | ISIG | IEXTEN | IXON | ECHO);
tNew.c_iflag &= ~(IXON | IXOFF);
tcsetattr(STDIN_FILENO, TCSANOW, &tNew);
// 读取字符...
// 还原之前的终端设置
tcsetattr(STDIN_FILENO, TCSANOW, &tOld);
在这个模式下,程序不再按行读取输入,而是可以立即读取每个按键事件。 这使得读取 Ctrl 组合键成为可能。但是,禁用 ISIG
标记虽然允许程序捕获Ctrl+C
等组合键的输入,但 Ctrl+C
也会导致终端驱动程序发送 SIGINT
信号到前台进程组(我们的应用)。 这会导致当前输入通道阻塞,之后的输入行为都依赖于 SIGINT
的后续处理,而我们的程序恰好没有设置。 于是产生了 “Ctrl+C 导致 stdin 输入停止工作” 现象。
这表明应用程序既需要处理 Ctrl+C 输入,又要防止因发送信号而终止或者挂起输入。
另外,使用 select
检查文件符是否可读的方法,其在某些场景也可能与 SIGINT
信号存在竞争关系,可能出现无法捕获的状况。
std::function<bool(unsigned)> fnWaitForNextKey =
[](unsigned iMillis) -> bool
{
struct timeval tv;
fd_set fds;
tv.tv_sec = 0;
tv.tv_usec = iMillis * 1000;
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
return select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv);
};
解决方案
为了解决这个问题,需在禁用信号生成的同时,注册一个自定义的 SIGINT
信号处理函数。当接收到 SIGINT
信号, 既不结束程序, 也不让它影响 stdin
的正常运作。 步骤如下:
1. 安装信号处理函数
使用 signal()
函数, 安装一个自定义的信号处理器,当程序接收到 SIGINT
信号(即按下 Ctrl+C
)时, 运行该函数。此函数什么都不做, 可以是一个空函数。
#include <signal.h>
void sigint_handler(int signum){
// 空函数
}
int main(){
signal(SIGINT, sigint_handler);
//...
}
2. 修改终端属性
按照之前的代码禁用终端的规范模式以及信号产生。 这能使 Ctrl+C
变成一个正常的字符输入,而不再触发进程中断。
termios tOld, tNew;
tcgetattr(STDIN_FILENO, &tOld);
tNew = tOld;
tNew.c_lflag &= ~(ICANON | ISIG | IEXTEN | ECHO);
tNew.c_iflag &= ~(IXON | IXOFF);
tcsetattr(STDIN_FILENO, TCSANOW, &tNew);
3. 读取字符
程序可以使用 read
函数来读取按键, 或者使用 getchar
。 此时 Ctrl+C
将作为一个普通字符读取, 而不再会中断程序。
char buffer[1];
while (true) {
ssize_t bytesRead = read(STDIN_FILENO, buffer, 1);
if (bytesRead > 0) {
// process the input
if (buffer[0] == '\x03') { // '\x03' 是 Ctrl+C 的字符代码
std::cout << "Ctrl+C pressed.\n";
break; // 程序退出,也可做其他的逻辑
}else{
printf("Character: 0x%X \n", (unsigned char) buffer[0]);
}
}
}
4. 恢复终端设置
退出读取循环后,不要忘记恢复终端原来的属性,这样避免对终端的后续操作产生副作用。
tcsetattr(STDIN_FILENO, TCSANOW, &tOld);
安全建议
在禁用终端的 ISIG
功能时需要格外注意。如果信号处理逻辑没有得到正确实施,程序可能无法响应系统信号,从而导致进程的终止不可控。建议在程序退出时及时恢复终端属性,或是在必要时使用默认的信号处理行为, 这能有效地避免在一些复杂程序环境下因未处理 SIGINT
而导致的退出错误。
以下为完整的代码示例
#include <iostream>
#include <termios.h>
#include <unistd.h>
#include <signal.h>
void sigint_handler(int signum){
//do nothing
}
int main() {
signal(SIGINT, sigint_handler);
termios tOld, tNew;
tcgetattr(STDIN_FILENO, &tOld);
tNew = tOld;
tNew.c_lflag &= ~(ICANON | ISIG | IEXTEN | ECHO);
tNew.c_iflag &= ~(IXON | IXOFF);
tcsetattr(STDIN_FILENO, TCSANOW, &tNew);
char buffer[1];
while (true) {
ssize_t bytesRead = read(STDIN_FILENO, buffer, 1);
if (bytesRead > 0) {
if (buffer[0] == '\x03') {
std::cout << "Ctrl+C pressed.\n";
break;
}else{
printf("Character: 0x%X \n", (unsigned char) buffer[0]);
}
}
}
tcsetattr(STDIN_FILENO, TCSANOW, &tOld);
return 0;
}
通过结合信号处理函数,终端属性控制和正确的读取操作,应用可以既处理 Ctrl+C
输入, 又能避免其造成的输入流中断。