无阻塞键盘输入

我正在尝试用 C 语言编写一个程序(在 Linux 上) ,循环直到用户按下一个键,但不应该要求按键来继续每个循环。

有什么简单的方法吗?我认为我可以做到这一点与 select()但似乎有很多工作。

或者,是否有一种方法可以捕获 ctrl-c按键在程序关闭之前进行清理,而不是非阻塞输入输出?

127887 次浏览

On UNIX systems, you can use sigaction call to register a signal handler for SIGINT signal which represents the Control+C key sequence. The signal handler can set a flag which will be checked in the loop making it to break appropriately.

There is no portable way to do this, but select() might be a good way. See http://c-faq.com/osdep/readavail.html for more possible solutions.

The curses library can be used for this purpose. Of course, select() and signal handlers can be used too to a certain extent.

You probably want kbhit();

//Example will loop until a key is pressed
#include <conio.h>
#include <iostream>


using namespace std;


int main()
{
while(1)
{
if(kbhit())
{
break;
}
}
}

this may not work on all environments. A portable way would be to create a monitoring thread and set some flag on getch();

As already stated, you can use sigaction to trap ctrl-c, or select to trap any standard input.

Note however that with the latter method you also need to set the TTY so that it's in character-at-a-time rather than line-at-a-time mode. The latter is the default - if you type in a line of text it doesn't get sent to the running program's stdin until you press enter.

You'd need to use the tcsetattr() function to turn off ICANON mode, and probably also disable ECHO too. From memory, you also have to set the terminal back into ICANON mode when the program exits!

Just for completeness, here's some code I've just knocked up (nb: no error checking!) which sets up a Unix TTY and emulates the DOS <conio.h> functions kbhit() and 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 */
}

select() is a bit too low-level for convenience. I suggest you use the ncurses library to put the terminal in cbreak mode and delay mode, then call getch(), which will return ERR if no character is ready:

WINDOW *w = initscr();
cbreak();
nodelay(w, TRUE);

At that point you can call getch without blocking.

If you are happy just catching Control-C, it's a done deal. If you really want non-blocking I/O but you don't want the curses library, another alternative is to move lock, stock, and barrel to the AT&T sfio library. It's nice library patterned on C stdio but more flexible, thread-safe, and performs better. (sfio stands for safe, fast I/O.)

Another way to get non-blocking keyboard input is to open the device file and read it!

You have to know the device file you are looking for, one of /dev/input/event*. You can run cat /proc/bus/input/devices to find the device you want.

This code works for me (run as an administrator).

  #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;
}

You can do that using select as follow:

  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;
}
}
}

Not too much work :D

Here's a function to do this for you. You need termios.h which comes with POSIX systems.

#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);
}

Breaking this down: tcgetattr gets the current terminal information and stores it in t. If cmd is 1, the local input flag in t is set to non-blocking input. Otherwise it is reset. Then tcsetattr changes standard input to t.

If you don't reset standard input at the end of your program you will have problems in your shell.

In C++, I did this:

#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);
}
}

This code would be platform-independent.