非阻塞式获取字符

11
  • 平台: Linux 3.2.0 x86 (Debian 7)
  • 编译器: GCC 4.7.2 (Debian 4.7.2-5)

我正在编写一个函数,该函数从标准输入(stdin)读取单个字符,如果stdin中已经存在字符,则读取该字符。如果stdin为空,则函数不执行任何操作并返回-1。我搜索了非阻塞输入,并被指向poll()select()。首先我尝试使用select(),但无法使其正常工作,因此我尝试了poll()并得出了同样的结论。我不确定这些函数具体做什么,但从我对poll()文档的理解来看,如果我像这样调用它:

struct pollfd pollfds;
pollfds = STDIN_FILENO;
pollfds.events = POLLIN;
poll(pollfds, 1, 0);

如果(pollfds.revents & POLLIN)为真,则表示“可以读取低优先级数据而不会阻塞”。但在我的测试情况下,poll()总是超时。可能是我测试函数的方式有问题,但我想要的功能恰好是我正在测试的功能。这是当前的函数和测试情况。

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

int ngetc(char *c)
{       
    struct pollfd pollfds;
    pollfds.fd = STDIN_FILENO;
    pollfds.events = POLLIN;

    poll(&pollfds, 1, 0);

    if(pollfds.revents & POLLIN)
    {
            //Bonus points to the persons that can tell me if
            //read() will change the value of '*c' if an error
            //occurs during the read
        read(STDIN_FILENO, c, 1);
            return 0;
    }
    else return -1;
}

//Test Situation:
//Try to read a character left in stdin by an fgets() call
int main()
{
    int ret = 0;
    char c = 0;
    char str[256];

    //Make sure to enter more than 2 characters so that the excess
    //is left in stdin by fgets()
    fgets(str, 2, stdin);

    ret = ngetc(&c);

    printf("ret = %i\nc = %c\n", ret, c);

    return 0;
}

1
https://dev59.com/IVPZs4cB2Jgan1znx5zy - ninjalj
你的函数永远不会返回0:如果(...)返回errno;否则返回EWOULDBLOCK; 那么最后一个返回有什么用处? - philippe lhardy
如果在读取过程中发生错误,该函数将返回errno,但如果在读取过程中没有发生错误,则返回0。该语句首先检查stdin中是否有数据,如果没有数据,则在调用read()之前会短路终止执行。但如果stdin中有数据,则调用read(),如果read()返回-1,则表示发生了读取错误,因此返回errno;否则,如果没有发生读取错误,则条件为假,所以返回0。 - John Vulconshinz
我的意思是:最后的 return 0 看起来只是一个无法到达的代码,因为它遵循了 if() return A; else return B; 的模式。 - philippe lhardy
哦,是的,我知道你的意思,它看起来有点尴尬。 - John Vulconshinz
3个回答

12
您的IO操作有误,POSIX手册和所有相关文档明确指出,永远不要混合使用FILE *和文件描述符进行IO操作。您明显违反了这个规则。这个规则的存在是因为FILE *使用缓冲,这意味着在调用fgets后,由于fgets已经将所有挂起的数据读入保存在FILE *结构中的缓冲区中,所以read函数将无法获取任何数据。
因此,由于无法检查ISO C IO方法是否会阻塞,我们必须仅使用文件描述符。
由于我们知道STDIN_FILENO只是数字0,因此我们可以使用
fcntl (0, F_SETFL, O_NONBLOCK);

这将把文件描述符0上的所有read转换为非阻塞模式,如果您想使用不同的文件描述符以便保留0,则只需使用dup进行复制即可。
这样,您可以完全避免使用poll并实现ngetc
ssize_t 
ngetc (char *c)
{
  return read (0, c, 1);
}

或者更好的是,一个宏

#define ngetc(c) (read (0, (c), 1))

因此,您可以得到一个简单的实现,以满足您的需求。
编辑:如果您仍然担心终端会缓冲输入,您可以随时更改终端的设置,请参见如何在程序中禁用xterm的输入行缓冲?了解更多信息。
编辑:不能使用fgetc代替read的原因与使用fgets不起作用的原因相同。当运行其中一个FILE * IO函数时,它会从关联的文件描述符读取所有数据。但是一旦发生这种情况,poll将永远不会返回,因为它正在等待一个始终为空的文件描述符,并且使用read也会发生同样的事情。因此,我建议您遵循文档的建议,永远不要混合使用流(使用fgets,fgetc等进行IO)和文件描述符(使用read,write等进行IO)。

除了终端会缓冲整行文本之外,还有其他部分。 - ninjalj
如果stdin被管道输入,那么预计会出现EOF。是否可以使用类似于if (ngetc(c) == -1) Handle_EOF_or_IOError();这样的语句? - chux - Reinstate Monica
@chux 不是的,你应该使用 if (ngetc (&c) == 0) handle_EOF (); 因为 read 在遇到 EOF 时返回 0,但你应该使用 if (ngetc (&c) < 0) handle_other_error (); 来检查其他任何类型的错误。 - randomusername
@randomusername感谢您的回复。我有一些问题,您是说我永远无法以我所述的方式读取fgets()调用后在stdin中留下的数据吗?此外,我已尝试使用fgetc()代替read(),但也没有起作用,您能解释一下为什么fgetc()也无法起作用吗? - John Vulconshinz
@JohnVulconshinz 是的,我的意思是你永远无法按照你所说的方式使用 fgets。ISO C 标准没有给你提供这样的方法,使用 POSIX 标准来绕过它需要你使用 文件描述符 - randomusername

0

你的代码存在两个问题。

  1. 根据 poll手册,将 timeout 赋值为 0 将会立即返回结果。

    如果 timeout 的值为 0,则 poll() 将立即返回。如果 timeout 的值为 -1,则 poll() 将一直阻塞,直到发生请求的事件或调用被中断。

  2. fgets 并不像你期望的那样工作,它来自于 stdio 库并且会缓存读取的内容。假设你输入了 3 个字母并按下回车,在使用 fgets 后,第三个字母将无法被 poll 获取。

因此,请注释掉 fgets 行并将 timeout 赋值为 -1 在 poll 中,然后再次运行以查看是否符合您的要求。


@dlutxx 谢谢您的回复。您所说的“读取缓冲区”是什么意思?我认为fgets()只需要处理终端行缓冲区。 - John Vulconshinz
@JohnVulconshinz fgets 会读取整行(将其存储在缓冲区中,并在后续读取时返回),而不仅仅是2个字符。 - adamsmith

0

我没有得到上面答案中预期的行为,实际上我还不得不考虑这个答案,因为它将TTY设置为非规范模式。

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

int main(int argc, char *argv[]) 
{
    struct termios t;
    tcgetattr(0, &t);
    t.c_lflag &= ~ICANON;
    tcsetattr(0, TCSANOW, &t);

    fcntl(0, F_SETFL, fcntl(0, F_GETFL) | O_NONBLOCK);

    printf("Starting loop (press i or q)...\n");

    for (int i = 0; ; i++) {
        char c = 0;
        read (0, &c, 1);

        switch (c) {
        case 'i':
            printf("\niteration: %d\n", i);
            break;

        case 'q':
            printf("\n");
            exit(0);
        }
    }

    return 0; 
}

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