C 非阻塞键盘输入
Posted
技术标签:
【中文标题】C 非阻塞键盘输入【英文标题】:C non-blocking keyboard input 【发布时间】:2010-10-01 17:27:02 【问题描述】:我正在尝试用 C 语言(在 Linux 上)编写一个程序,该程序循环直到用户按下一个键,但不应该需要按键来继续每个循环。
有没有简单的方法来做到这一点?我想我可以用select()
来做这件事,但这似乎需要做很多工作。
或者,有没有办法在程序关闭而不是非阻塞 io 之前捕获 ctrl-c 按键以进行清理?
【问题讨论】:
开个话题怎么样? 我推荐一个单独的线程,它阻塞键盘输入并将输入传递给另一个主线程,每次按键进入时。Here is an example I wrote in Python。对于 C,请务必使用线程安全的消息传递队列,或根据需要阻止获取互斥锁。 Python 队列库已经是线程安全的,但在 C 或 C++ 中自然不存在队列的这种安全性。 另见:How to avoid pressing Enter with getchar() for reading a single character only? 【参考方案1】:如前所述,您可以使用 sigaction
捕获 ctrl-c,或使用 select
捕获任何标准输入。
但是请注意,使用后一种方法,您还需要设置 TTY,使其处于一次字符而不是一次一行模式。后者是默认设置 - 如果您输入一行文本,它不会发送到正在运行的程序的标准输入,直到您按下回车键。
您需要使用tcsetattr()
函数来关闭 ICANON 模式,并且可能还要禁用 ECHO。从内存中,您还必须在程序退出时将终端设置回 ICANON 模式!
为了完整起见,这是我刚刚敲出的一些代码(nb:没有错误检查!),它设置了一个 Unix TTY 并模拟了 DOS <conio.h>
函数 kbhit()
和 getch()
:
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <termios.h>
struct termios orig_termios;
void reset_terminal_mode()
tcsetattr(0, TCSANOW, &orig_termios);
void set_conio_terminal_mode()
struct termios new_termios;
/* take two copies - one for now, one for later */
tcgetattr(0, &orig_termios);
memcpy(&new_termios, &orig_termios, sizeof(new_termios));
/* register cleanup handler, and set the new terminal mode */
atexit(reset_terminal_mode);
cfmakeraw(&new_termios);
tcsetattr(0, TCSANOW, &new_termios);
int kbhit()
struct timeval tv = 0L, 0L ;
fd_set fds;
FD_ZERO(&fds);
FD_SET(0, &fds);
return select(1, &fds, NULL, NULL, &tv) > 0;
int getch()
int r;
unsigned char c;
if ((r = read(0, &c, sizeof(c))) < 0)
return r;
else
return c;
int main(int argc, char *argv[])
set_conio_terminal_mode();
while (!kbhit())
/* do some work */
(void)getch(); /* consume the character */
【讨论】:
getch() 应该返回哪个键被按下,还是只是应该消耗字符? @Salepate 它应该返回字符。是(void)
位获取该字符,然后将其丢弃。
哦,我明白了,也许我做错了,但是当我在 printf("%c"); 上使用 getch() 时它在屏幕上显示奇怪的字符。
@Max888 不可移植,但如果下一个检索到的字符是 ^c (0x03),您可以自己退出程序
感谢您提供优雅的解决方案...我有一个正在运行的中断计时器,这导致kbhit()
偶尔返回-1 == EINTR
...这意味着kbhit()
在应该返回true
时(可以说) return false
...我已通过将 kbhit()
中的 return
语句更改为 return (select(1, &fds, NULL, NULL, &tv) > 0);
来解决此问题【参考方案2】:
select()
为了方便起见有点太低级了。我建议你使用ncurses
库将终端置于cbreak模式和延迟模式,然后调用getch()
,如果没有准备好字符,它将返回ERR
:
WINDOW *w = initscr();
cbreak();
nodelay(w, TRUE);
此时您可以调用getch
而不会阻塞。
【讨论】:
我也想过使用 curses,但这样做的潜在问题是 initscr() 调用会清除屏幕,并且它也可能会妨碍使用普通 stdio 进行屏幕输出。跨度> 是的,curses 几乎是全有或全无。【参考方案3】:在 UNIX 系统上,您可以使用 sigaction
调用为 SIGINT
信号注册一个信号处理程序,该信号表示 Control+C 键序列。信号处理程序可以设置一个标志,该标志将在循环中进行检查,使其适当中断。
【讨论】:
我想我可能会为了方便而最终这样做,但我会接受另一个答案,因为它更接近标题的要求。【参考方案4】:你可能想要kbhit();
//Example will loop until a key is pressed
#include <conio.h>
#include <iostream>
using namespace std;
int main()
while(1)
if(kbhit())
break;
这可能不适用于所有环境。一种可移植的方法是创建一个监控线程并在getch();
上设置一些标志
【讨论】:
绝对不便携。 kbhit() 是一个 DOS/Windows 控制台 IO 函数。 它仍然是许多用途的有效答案。 OP 没有指定可移植性或任何特定平台。 Alnitak:我不知道它暗示了一个平台——等效的 Windows 术语是什么? @Alnitak 为什么“非阻塞”作为一个编程概念,在 Unix 环境中会更加熟悉? @Alnitak:我的意思是我在接触任何 *nix 之前很久就熟悉“非阻塞调用”这个术语了。所以就像 Zxaos 一样,我永远不会意识到它意味着一个平台。【参考方案5】:获得非阻塞键盘输入的另一种方法是打开设备文件并读取它!
您必须知道您要查找的设备文件,即 /dev/input/event* 之一。你可以运行 cat /proc/bus/input/devices 来找到你想要的设备。
此代码适用于我(以管理员身份运行)。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <linux/input.h>
int main(int argc, char** argv)
int fd, bytes;
struct input_event data;
const char *pDevice = "/dev/input/event2";
// Open Keyboard
fd = open(pDevice, O_RDONLY | O_NONBLOCK);
if(fd == -1)
printf("ERROR Opening %s\n", pDevice);
return -1;
while(1)
// Read Keyboard Data
bytes = read(fd, &data, sizeof(data));
if(bytes > 0)
printf("Keypress value=%x, type=%x, code=%x\n", data.value, data.type, data.code);
else
// Nothing read
sleep(1);
return 0;
【讨论】:
很棒的例子,但是我遇到了一个问题,即当事件设备在按住超过六个键时停止发射时,Xorg 会继续接收键事件:\ 很奇怪吧?【参考方案6】:curses 库可用于此目的。当然,select()
和信号处理程序也可以在一定程度上使用。
【讨论】:
curses 既是库的名称,也是您在使用它时会喃喃自语的内容;)但是,因为我要向您提及 +1。【参考方案7】:如果您对使用 Control-C 感到满意,那就大功告成了。如果你真的想要非阻塞 I/O 但又不想使用 curses 库,另一种选择是将锁、库存和桶移动到 AT&T sfio
library。这是一个很好的库,基于 C stdio
,但更灵活、线程安全且性能更好。 (sfio 代表安全、快速的 I/O。)
【讨论】:
【参考方案8】:没有可移植的方法来做到这一点,但 select() 可能是一个好方法。有关更多可能的解决方案,请参阅http://c-faq.com/osdep/readavail.html。
【讨论】:
这个答案不完整。如果没有使用 tcsetattr() 进行终端操作 [请参阅我的回答],在您按 Enter 之前,您不会在 fd 0 上得到任何东西。【参考方案9】:您可以使用 select 来做到这一点,如下所示:
int nfds = 0;
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(0, &readfds); /* set the stdin in the set of file descriptors to be selected */
while(1)
/* Do what you want */
int count = select(nfds, &readfds, NULL, NULL, NULL);
if (count > 0)
if (FD_ISSET(0, &readfds))
/* If a character was pressed then we get it and exit */
getchar();
break;
没有太多的工作:D
【讨论】:
这个答案不完整。您需要将终端设置为非规范模式。nfds
也应该设置为 1。
第一次读没问题,但后面读到两个字符。我想第一个是前一个的输入,第二个是键本身。【参考方案10】:
这是为您执行此操作的函数。您需要 POSIX 系统附带的termios.h
。
#include <termios.h>
void stdin_set(int cmd)
struct termios t;
tcgetattr(1,&t);
switch (cmd)
case 1:
t.c_lflag &= ~ICANON;
break;
default:
t.c_lflag |= ICANON;
break;
tcsetattr(1,0,&t);
分解:tcgetattr
获取当前终端信息并将其存储在t
中。如果cmd
为1,则t
中的本地输入标志设置为非阻塞输入。否则将被重置。然后tcsetattr
将标准输入更改为t
。
如果你没有在程序结束时重置标准输入,你的 shell 就会出现问题。
【讨论】:
switch语句少了一个括号:)【参考方案11】:在 C++ 中,我这样做了:
#include <chrono>
#include <thread>
using namespace std::chrono_literals;
void OnEnter()
while (true)
getchar();
// do something when enter is pressed
int main()
std::thread keyBoardCommands(OnEnter);
while(true)
// code for main loop
std::this_thread::sleep_for(16ms);
此代码将独立于平台。
【讨论】:
以上是关于C 非阻塞键盘输入的主要内容,如果未能解决你的问题,请参考以下文章