Linux下C++开发不阻塞循环检测键盘按键输入的最优方法
Linux下C++结合popen实现运行时键盘检测的最优方案
核心实现逻辑
Linux终端默认处于规范模式,所有键盘输入会缓存到行缓冲区,直到用户按下回车才会提交给程序,要实现实时按键检测,首先要修改终端属性为非规范模式,再通过IO多路复用同时监听popen子进程的输出和标准输入的按键事件,避免阻塞等待。
具体实现步骤
- 保存原始终端属性,修改终端为非规范模式,关闭回显(可选,根据是否需要显示用户输入决定),设置输入不需要按回车直接上报
- 调用
popen启动目标程序,通过fileno()接口获取popen返回的FILE指针对应的文件描述符 - 初始化
select/poll的监听集合,同时添加popen的文件描述符和STDIN_FILENO(标准输入文件描述符)到监听队列 - 循环监听事件:
- 若popen的文件描述符可读:读取子进程输出内容,读到EOF则说明子进程运行结束,退出循环
- 若标准输入可读:读取用户按键,存储用户选择标记(继续/暂停),可根据需求选择是否丢弃输入内容
- 调用
pclose回收子进程资源,根据之前存储的用户选择标记,执行后续逻辑:继续处理文件/暂停程序等待二次确认 - 程序退出前必须恢复原始终端属性,避免终端出现输入不显示、回车无效等异常
示例代码
#include <iostream> #include <cstdio> #include <unistd.h> #include <termios.h> #include <sys/select.h> int main() { // 保存原始终端属性 termios old_tio, new_tio; tcgetattr(STDIN_FILENO, &old_tio); new_tio = old_tio; // 关闭规范模式和回显 new_tio.c_lflag &= ~(ICANON | ECHO); new_tio.c_cc[VMIN] = 0; new_tio.c_cc[VTIME] = 0; tcsetattr(STDIN_FILENO, TCSANOW, &new_tio); // 启动子进程 FILE* fp = popen("sleep 5 && echo 'sub process done'", "r"); if (!fp) { tcsetattr(STDIN_FILENO, TCSANOW, &old_tio); perror("popen failed"); return 1; } int sub_fd = fileno(fp); bool user_want_pause = false; char buf[1024]; bool sub_running = true; while (sub_running) { fd_set read_fds; FD_ZERO(&read_fds); FD_SET(sub_fd, &read_fds); FD_SET(STDIN_FILENO, &read_fds); // 阻塞等待事件,也可以设置超时时间避免卡死 int ret = select(std::max(sub_fd, STDIN_FILENO) + 1, &read_fds, nullptr, nullptr, nullptr); if (ret < 0) break; if (FD_ISSET(sub_fd, &read_fds)) { ssize_t n = read(sub_fd, buf, sizeof(buf)-1); if (n <= 0) { sub_running = false; } else { buf[n] = '\0'; // 这里处理子进程输出,不需要可以丢弃 printf("sub process output: %s", buf); } } if (FD_ISSET(STDIN_FILENO, &read_fds)) { char key; ssize_t n = read(STDIN_FILENO, &key, 1); if (n > 0) { // 示例:按p标记需要暂停,其他键继续 if (key == 'p' || key == 'P') { user_want_pause = true; printf("\nuser select pause after process done\n"); } } } } pclose(fp); // 处理后续逻辑 if (user_want_pause) { printf("\nprocess done, press any key to continue...\n"); // 等用户按任意键 while (read(STDIN_FILENO, buf, 1) <= 0); } else { printf("\nprocess done, continue handling file...\n"); } // 恢复终端属性 tcsetattr(STDIN_FILENO, TCSANOW, &old_tio); return 0; }
注意事项
- 建议用RAII封装终端属性的修改和恢复,避免程序异常退出时终端属性无法复原导致终端异常
- 如果不需要读取子进程的输出,也可以将标准输入设为非阻塞模式,循环调用
waitpid的非阻塞接口检测子进程状态,同时检测按键输入,但是IO多路复用的方案CPU占用更低、响应更及时 select的最后一个参数可以设置为timeval结构体实现超时检测,避免子进程卡死导致主程序一直阻塞
内容的提问来源于stack exchange,提问作者Chronigan
相关产品推荐
相关产品推荐

