如何在Linux上使用C语言进行非阻塞控制台输入输出?

46

如何在Linux/OS X上使用C语言进行非阻塞控制台IO操作?


这个讨论(“C非阻塞键盘输入”)为我解决了问题:https://dev59.com/RXRB5IYBdhLWcg3w-8A9 - Ken Shirriff
8个回答

31

我想添加一个例子:

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char const *argv[]) {
    char buf[20];
    fcntl(0, F_SETFL, fcntl(0, F_GETFL) | O_NONBLOCK);
    sleep(4);
    int numRead = read(0, buf, 4);
    if (numRead > 0) {
        printf("You said: %s", buf);
    }
}

当您运行此程序时,您有4秒钟的时间向标准输入提供输入。如果没有找到输入,则会返回。
2个示例执行:
$ ./a.out
fda 
You said: fda
$ ./a.out
$ 

4
这是正确答案。它涵盖了所请求的两个操作系统(在GNU/Linux上进行了测试)。请记住,此解决方案仍将缓冲输入。(例如,您需要按Enter键将输入传递给程序)。 - Doe
7
为了更清晰易懂,我会使用STDIN_FILENO代替文件描述符0。 - Micah
1
请注意,这可能会破坏终端,因为stdin可以与未来的进程共享,这些进程可能不希望stdin是非阻塞的。在运行程序后尝试使用cat。并且,请使用一致的缩进风格。 - 12431234123412341234123

24

Pete Kirkham一样,我在cc.byexamples.com中找到了对我的问题有很好解释的方法。这里还提供了ncurses版本。

我的代码需要从标准输入或文件中读取一个初始命令,然后在处理该命令的同时等待取消命令。我的代码是用C++编写的,但您可以使用scanf()和其他函数来替代我的C++输入函数getline()

主要的函数是检查是否有任何输入可用:

#include <unistd.h>
#include <stdio.h>
#include <sys/select.h>

// cc.byexamples.com calls this int kbhit(), to mirror the Windows console
//  function of the same name.  Otherwise, the code is the same.
bool inputAvailable()  
{
  struct timeval tv;
  fd_set fds;
  tv.tv_sec = 0;
  tv.tv_usec = 0;
  FD_ZERO(&fds);
  FD_SET(STDIN_FILENO, &fds);
  select(STDIN_FILENO+1, &fds, NULL, NULL, &tv);
  return (FD_ISSET(0, &fds));
}

在使用任何stdin输入函数之前,必须调用此函数。当我在使用这个函数之前使用std :: cin时,它再也不会返回true了。例如,main()有一个类似于以下内容的循环:

int main(int argc, char* argv[])
{ 
   std::string initialCommand;
   if (argc > 1) {
      // Code to get the initial command from a file
   } else {
     while (!inputAvailable()) {
       std::cout << "Waiting for input (Ctrl-C to cancel)..." << std::endl;
       sleep(1);
     }
     std::getline(std::cin, initialCommand);
   }

   // Start a thread class instance 'jobThread' to run the command
   // Start a thread class instance 'inputThread' to look for further commands
   return 0;
}

在输入线程中,新的命令被添加到队列中,这些命令由jobThread定期处理。 inputThread 的代码大致如下:

THREAD_RETURN inputThread()
{
  while( !cancelled() ) {
    if (inputAvailable()) {
      std::string nextCommand;
      getline(std::cin, nextCommand);
      commandQueue.lock();
      commandQueue.add(nextCommand);
      commandQueue.unlock();
    } else {
        sleep(1);
    }
  }
  return 0;
}

这个函数本来可能可以放在main()中,但我是在一个现有的代码库中工作,而不是反对它。

对于我的系统来说,在发送换行符之前没有可用输入,这正是我想要的。如果你想在键入每个字符时读取输入,你需要关闭标准输入的"规范模式"。cc.byexamples.com有一些建议,我还没有尝试过,但其余的方法都可以工作,所以应该也可以。


看起来你把链接反了。cc.byexamples... 链接到 Pete Kirkham 的个人资料,反之亦然。 - Nick Meyer
很棒的示例源代码,正好满足我的需求。 - vmrob

7
你不需要真正地使用非阻塞 I/O,因为TTY(控制台)是一个非常有限的设备,几乎无法进行非阻塞I/O。当你在 curses/ncurses 应用程序中看到类似非阻塞I/O的东西时,你所做的就是称之为“原始I/O”。在原始I/O中,没有字符的解释、擦除处理等。相反,你需要编写自己的代码,在进行其他操作时检查数据。

在现代C程序中,你可以通过将控制台I/O放入线程或轻量级进程中来简化此过程。然后,I/O可以按照通常的阻塞方式进行,但数据可以被插入队列以在另一个线程中进行处理。

更新

这里有一个curses教程,更详细地介绍了它。


9
当然你可以在tty上进行非阻塞IO,这是大多数单线程文本界面程序的工作方式。 - MarkR
25
回答事实上是错误的。即使fd 0与终端tty相关联,你仍然可以使用fcntl(0, F_SETFL, fcntl(0, GETFL) | O_NONBLOCK)命令——尽管通常不建议这样做,因为它会困惑其他使用标准输入的人,但它确实能够工作。 - ephemient
6
在评论之前,我已经读了三遍,并且一直读成“替换/do/为/ca/”。尽管我并不完全同意这个答案,但它是连贯的... - ephemient
@eph,你不讨厌这种情况吗?没关系。就像你所说的,你可以使它非阻塞。然而,考虑到问题,我相当确定提问者是在询问原始I/O。 - Charlie Martin
2
@ephemient 我认为应该是 F_GETFL。 - Koray Tugay

7
我本月早些时候收藏了 "不使用ncurses在循环中进行非阻塞用户输入",当时我认为我可能需要非阻塞、非缓冲的控制台输入,但事实上并没有用到,因此无法保证它是否有效。对于我的用途,我并不在意它只有在用户按下回车键后才能获得输入,所以我只是使用 aio 读取标准输入。

4
另一种使用ncurses或线程的替代方案是使用GNU Readline,具体来说,是它允许你注册回调函数的部分。模式如下:
  1. 在STDIN(以及任何其他描述符)上使用select()
  2. 当select()告诉你可以从STDIN读取时,调用readline的rl_callback_read_char()
  3. 如果用户输入了完整的行,则rl_callback_read_char将调用您的回调。否则,它会立即返回,您的其他代码可以继续执行。


3

让我们看看Linux工具中的一个例子。例如,perf/builtin-top.c源码(简化):

static void *display_thread(void *arg)
{
    struct pollfd stdin_poll = { .fd = 0, .events = POLLIN };
    struct termios save;
    set_term_quiet_input(&save);
    while (!done) {
      switch (poll(&stdin_poll, 1, delay_msecs)) {
        ...
      }
    }
    tcsetattr(0, TCSAFLUSH, &save);
}

因此,如果您想检查是否有任何可用数据,可以像这样使用poll()或select():

#include <sys/poll.h>
...
struct pollfd pfd = { .fd = 0, .events = POLLIN };
while (...) {
  if (poll(&pfd, 1, 0)>0) {
    // data available, read it
  }
  ...
}

在这种情况下,您将收到事件,而不是在每个键上,而是在整行后按[RETURN]键。这是因为终端在规范模式(输入流被缓冲,并且缓冲区在按[RETURN]键时刷新)下运行:

在规范输入处理模式中,终端输入以由换行符('\n'),EOF或EOL字符终止的行进行处理。在用户键入整行之前,无法读取任何输入,并且read函数(请参阅输入和输出基元)返回最多一个输入行,无论请求多少字节。

如果您想立即读取字符,则可以使用非规范模式。使用tcsetattr()进行切换:
#include <termios.h>

void set_term_quiet_input()
{
  struct termios tc;
  tcgetattr(0, &tc);
  tc.c_lflag &= ~(ICANON | ECHO);
  tc.c_cc[VMIN] = 0;
  tc.c_cc[VTIME] = 0;
  tcsetattr(0, TCSANOW, &tc);
}

简单程序(链接到游乐场):
#include <stdio.h>
#include <unistd.h>
#include <sys/poll.h>
#include <termios.h>

void set_term_quiet_input()
{
  struct termios tc;
  tcgetattr(0, &tc);
  tc.c_lflag &= ~(ICANON | ECHO);
  tc.c_cc[VMIN] = 0;
  tc.c_cc[VTIME] = 0;
  tcsetattr(0, TCSANOW, &tc);
}

int main() { 
  struct pollfd pfd = { .fd = 0, .events = POLLIN };
  set_term_quiet_input();
  while (1) {
    if (poll(&pfd, 1, 0)>0) {
      int c = getchar();
      printf("Key pressed: %c \n", c);
      if (c=='q') break;
    }
    usleep(1000); // Some work 
  }
}

很好的答案,包含了使用select()poll()进行异步I/O,并且还涵盖了终端模式问题。点赞+1。 - reichhart

0

我不太确定你所说的“控制台IO”是什么意思——你是从STDIN读取,还是这是一个从其他来源读取的控制台应用程序?

如果你正在从STDIN读取,你需要跳过fread()并使用read()和write(),并使用poll()或select()来防止调用被阻塞。你可以尝试禁用输入缓冲区,这应该会导致fread返回EOF,使用setbuf()即可,但我从未尝试过。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接