为何poll()返回就绪状态后getchar()仍出现阻塞?
问题
我用C语言开发文本编辑器,用poll()判断输入是否就绪(未就绪时执行其他功能)。当有字符可用时(if (!poll(...))为假),调用getchar()读取处理字符。代码如下:
#include <poll.h> #include <stdio.h> int main(void) { input_set_tty_raw(); char input_history[1000]; int curr = 0; struct pollfd in = {.fd = 0, .events = POLLIN}; while (1) { if (!poll(&in, 1, 20)) { // arbitrary timeout for poll function continue; } char c = getchar(); /* problem code */ if (input_history[curr - 1] == '\033' && c == '[') { while (poll(&in, 1, 0)) { getchar(); } return input_restore_tty(); } // handle inputs input_history[curr++] = c; } return input_restore_tty(); }
input_set_tty_raw()等效于执行$ stty raw,支持逐字符处理原始输入;input_restore_tty()用于恢复终端初始状态。
按下箭头键等按键时,stdin会传入特殊转义序列(比如箭头键对应27-91-6x字节序列),这类序列和删除键的转义序列共享\033[前缀。
因此在标记为problem code的条件块中,我检查前一个字符是否为\033且当前字符为[,若是则通过while循环读取剩余输入。测试发现\033和[之间有延迟,后续字符即时传入,所以内层poll()超时设为0,并加入return语句验证输入处理完成时机。
但代码出现阻塞:按下箭头键后程序未按预期返回,按下其他键才返回,说明阻塞发生在problem code块内的getchar()中。
已知poll()仅在有输入可用时返回非零值,为何会出现阻塞?该如何修复?
辅助函数代码:
#include <termios.h> static struct termios tbufsave; int input_set_tty_raw(void) { struct termios tbuf; if (tcgetattr(0, &tbufsave) == -1) { return 1; } tbuf = tbufsave; tbuf.c_iflag &= ~(INLCR | ICRNL | ISTRIP | IXON | BRKINT); tbuf.c_oflag &= ~OPOST; tbuf.c_lflag &= ~(ICANON | ISIG | ECHO); tbuf.c_cc[VMIN] = 1; tbuf.c_cc[VTIME] = 0; return tcsetattr(0, TCSANOW, &tbuf) == -1; // 1 for error } int input_restore_tty(void) { return tcsetattr(0, TCSANOW, &tbufsave) == -1; // 1 for error }
原因分析
阻塞的核心原因有两点:
- 终端属性设置导致
getchar()阻塞:你在input_set_tty_raw()中把VMIN设为1、VTIME设为0,这意味着getchar()会一直阻塞,直到至少有1个字符可读。内层poll(&in,1,0)是非阻塞检查,当转义序列的剩余字符还没全部到达终端缓冲区时,poll()返回0,循环终止,但此时如果后续再调用getchar(),就会因为VMIN=1的设置卡死,直到有新输入。 - 数组越界的未定义行为:
curr初始值为0,第一次读取字符时input_history[curr-1]会访问input_history[-1],属于越界访问,可能干扰程序逻辑。
修复方案
1. 调整终端属性,消除getchar()阻塞
修改input_set_tty_raw()中的VMIN和VTIME,让getchar()变为非阻塞或超时返回:
// 在input_set_tty_raw()中替换原有设置 tbuf.c_cc[VMIN] = 0; // 允许无字符时立即返回 tbuf.c_cc[VTIME] = 1; // 超时时间0.1秒,可按需调整
此时getchar()在无输入时会返回EOF,不会持续阻塞。
2. 修正转义序列处理逻辑
- 先判断
curr > 0,避免数组越界; - 读取剩余转义字符时,直接循环调用
getchar()直到EOF,结合调整后的终端属性,不会阻塞:
修改后的问题代码块:
/* fixed code */ if (curr > 0 && input_history[curr - 1] == '\033' && c == '[') { char next; // 读取所有剩余转义字符,直到无输入 while ((next = getchar()) != EOF) { // 可在此添加转义序列的具体处理逻辑 } return input_restore_tty(); }
3. 优化外层输入读取逻辑
因为VMIN设为0,poll()返回有输入后,可能存在多个待读字符,需要循环读取避免遗漏,同时添加数组越界防护:
修改后的外层循环:
while (1) { if (!poll(&in, 1, 20)) { continue; } // 读取所有可用字符 char c; while ((c = getchar()) != EOF) { /* fixed code */ if (curr > 0 && input_history[curr - 1] == '\033' && c == '[') { char next; while ((next = getchar()) != EOF) { // 处理转义序列后续字符 } return input_restore_tty(); } input_history[curr++] = c; // 防止数组越界 if (curr >= (int)(sizeof(input_history)/sizeof(input_history[0]))) { curr = (int)(sizeof(input_history)/sizeof(input_history[0])) - 1; } } }
完整修正代码
#include <poll.h> #include <stdio.h> #include <termios.h> static struct termios tbufsave; int input_set_tty_raw(void) { struct termios tbuf; if (tcgetattr(0, &tbufsave) == -1) { return 1; } tbuf = tbufsave; tbuf.c_iflag &= ~(INLCR | ICRNL | ISTRIP | IXON | BRKINT); tbuf.c_oflag &= ~OPOST; tbuf.c_lflag &= ~(ICANON | ISIG | ECHO); tbuf.c_cc[VMIN] = 0; // 修改为0,允许非阻塞读取 tbuf.c_cc[VTIME] = 1; // 超时0.1秒 return tcsetattr(0, TCSANOW, &tbuf) == -1; } int input_restore_tty(void) { return tcsetattr(0, TCSANOW, &tbufsave) == -1; } int main(void) { input_set_tty_raw(); char input_history[1000]; int curr = 0; struct pollfd in = {.fd = 0, .events = POLLIN}; while (1) { if (!poll(&in, 1, 20)) { continue; } char c; while ((c = getchar()) != EOF) { /* fixed code */ if (curr > 0 && input_history[curr - 1] == '\033' && c == '[') { char next; while ((next = getchar()) != EOF) { // 可在此添加转义序列的具体处理逻辑 } return input_restore_tty(); } input_history[curr++] = c; if (curr >= (int)(sizeof(input_history)/sizeof(input_history[0]))) { curr = (int)(sizeof(input_history)/sizeof(input_history[0])) - 1; } } } return input_restore_tty(); }
内容的提问来源于stack exchange,提问作者vs07
相关产品推荐
相关产品推荐

