如何为C stdio输入流实现行缓冲?

时间:2016-01-15 07:53:48

标签: unix stdio buffering

据我所知,完全缓冲的输入可以通过为可能大于应用程序所需的数据块发出单个read系统调用来实现。但我不明白在没有内核支持的情况下如何将行缓冲应用于输入。我想有人必须读取一个数据块然后查找换行符,但如果是这样,那么完全缓冲有什么不同?

更具体一点:

假设我有一个输入流FILE* in。关于stdio库如何从操作系统中检索字节以填充其缓冲区,以下是否有任何区别?

  • 线路缓冲:setvbuf(in, NULL, _IOLBF, BUFSIZ)
  • 完全缓冲:setvbuf(in, NULL, _IOFBF, BUFSIZ)

如果是这样,那有什么区别?

3 个答案:

答案 0 :(得分:7)

FILE结构有一个默认的内部缓冲区。在fopen之后,在freadfgets等之后,缓冲区将由read(2)调用的stdio图层填充。

执行fgets时,它会将数据复制到您的缓冲区,将其从内部缓冲区中拉出[直到找到换行符]。如果未找到换行符,则会使用另一个read(2)调用补充流内部缓冲区。然后,继续扫描换行符并填充缓冲区。

这可以重复多次[尤其如果你正在做fread]。剩下的任何内容都可用于下一个流读取操作(例如freadfgetsfgetc)。

您可以使用setlinebuffer设置流缓冲区的大小。为了提高效率,典型的默认大小是机器页面大小[IIRC]。

因此,流缓冲区"可以说比你早一步,#34;可以这么说。它的运作方式就像一个响铃队列[实际上,如果不是现实]。

Dunno肯定,但行缓冲[或任何缓冲模式]通常用于输出文件(例如默认设置为stdout)。它说,如果您看到换行符,请执行隐含的fflush。完全缓冲意味着当缓冲区已满时执行fflush。无缓冲意味着在每个字符上执行fflush

如果打开输出日志文件,则会获得完全缓冲[效率最高],因此如果程序崩溃,则可能无法获得最后N行输出(即它们在缓冲区中仍未处理)。您可以设置行缓冲,以便在程序崩溃后获得最后一条跟踪线。

在输入时,行缓冲对文件[AFAICT]没有任何意义。它只是尝试使用尽可能最有效的大小(例如流缓冲区大小)。

我认为重要的一点是,在输入时,您事先不知道换行符的位置,因此_IOLBF的操作方式与其他任何模式一样 - 因为它到。 (即)您确实阅读(2)流式传输buf大小(或实现未完成fread所需的金额)。换句话说,唯一重要的是内部缓冲区大小和fread的大小/计数参数,而不是缓冲模式。

对于TTY设备(例如stdin),流将等待换行[除非您在底层fildes上使用TIOC * ioctl(例如0)来设置char-at-a-time aka raw mode],无论的流模式。那是因为TTY设备规范处理层[在内核中]将阻止读取(例如,为什么你可以键入退格等,而不需要应用程序处理它)。

然而,在TTY设备/流上执行fgets将在内部得到特殊处理(例如)它将进行选择/轮询并获取待处理字符的数量并且只读取该数量,因此它赢得了#39; t阻止阅读。然后它会查找换行符,如果找不到换行符,则重新发出select / poll。但是,如果找到换行符,则会从fgets返回。换句话说,它将执行任何必要的操作以允许stdin上的预期行为。如果用户输入10个字符+换行符,它就不会阻止4096字节的读取。

<强>更新

回答你的第二轮后续问题

  

我认为tty子系统和在进程中运行的stdio代码是完全独立的。它们接口的唯一方法是通过发出read syscalls的进程;这些可能会阻止或不阻止,这取决于tty设置。

通常,这是事实。大多数应用程序尝试调整TTY图层设置。但是,应用可以如果愿意的话,但通过任何流/ stdio函数。

  

但是这个过程完全没有意识到这些设置,并且无法改变它们。

再次,通常是真的。但是,再次,进程可以更改它们。

  

如果我们在同一页面上,你所说的意味着setvbuf调用将改变tty设备的缓冲策略,我发现难以与我对Unix的理解I / O操作。

setvbuf仅设置缓冲区大小和策略。它根本与内核无关。内核只看到read(2)并且没有知道应用是否是原始的,或者流是通过fread [或fgets]来做的。 以任何方式影响TTY图层。

在正在fgetc循环且用户输入abcdef\n的应用中,fgetc会阻止[在驱动程序中] ,直到换行符为进入。这是执行此操作的TTY规范处理层。然后,当输入换行符时,read(2)完成的fgetc将返回值7。第一个fgetc将返回,其余六个将快速发生,从流的内部缓冲区中完成。

  

然而......

更复杂的应用可能会通过ioctl(fileno(stdin),TIOC*,...)更改TTY图层策略。流将意识到这一点。因此,在这样做时,必须要小心。因此,如果进程需要,它可以完全控制文件单元后面的TTY层,但必须通过ioctl

手动完成。

使用ioctl来修改[甚至禁用] TTY规范处理[aka&#34; TTY原始模式&#34;]可以由需要真正按字母一次输入的应用程序使用。例如,vimemacsgetkey

虽然应用程序可以混合原始模式和stdio流[并且有效地执行],但正常用法是在正常模式/使用中使用流或 完全绕过 stdio图层,执行ioctl(0,TIOC*,...)然后直接执行read(2)

这是一个示例getkey计划:

// getkey -- wait for user input

#include <stdio.h>
#include <fcntl.h>
#include <termios.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <errno.h>

#define sysfault(_fmt...) \
    do { \
        printf(_fmt); \
        exit(1); \
    } while (0)

int
main(int argc,char **argv)
{
    int fd;
    int remain;
    int err;
    int oflag;
    int stdflg;
    char *cp;
    struct termios tiold;
    struct termios tinew;
    int len;
    int flag;
    char buf[1];
    int code;

    --argc;
    ++argv;

    stdflg = 0;

    for (;  argc > 0;  --argc, ++argv) {
        cp = *argv;
        if (*cp != '-')
            break;

        switch (cp[1]) {
        case 's':
            stdflg = 1;
            break;
        }
    }

    printf("using %s\n",stdflg ? "fgetc" : "read");

    fd = fileno(stdin);

    oflag = fcntl(fd,F_GETFL);
    fcntl(fd,F_SETFL,oflag | O_NONBLOCK);

    err = tcgetattr(fd,&tiold);
    if (err < 0)
        sysfault("getkey: tcgetattr failure -- %s\n",strerror(errno));

    tinew = tiold;

#if 1
    tinew.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP |
        INLCR | IGNCR | ICRNL | IXON);
    tinew.c_oflag &= ~OPOST;
    tinew.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    tinew.c_cflag &= ~(CSIZE | PARENB);
    tinew.c_cflag |= CS8;

#else
    cfmakeraw(&tinew);
#endif

#if 0
    tinew.c_cc[VMIN] = 0;
    tinew.c_cc[VTIME] = 0;
#endif

    err = tcsetattr(fd,TCSAFLUSH,&tinew);
    if (err < 0)
        sysfault("getkey: tcsetattr failure -- %s\n",strerror(errno));

    for (remain = 9;  remain > 0;  --remain) {
        printf("\rHit any key within %d seconds to abort ...",remain);
        fflush(stdout);

        sleep(1);

        if (stdflg) {
            len = fgetc(stdin);
            if (len != EOF)
                break;
        }
        else {
            len = read(fd,buf,sizeof(buf));
            if (len > 0)
                break;
        }
    }

    tcsetattr(fd,TCSAFLUSH,&tiold);
    fcntl(fd,F_SETFL,oflag);

    code = (remain > 0);

    printf("\n");
    printf("%s (%d remaining) ...\n",code ? "abort" : "normal",remain);

    return code;
}

答案 1 :(得分:2)

您正在谈论终端输入的线路规则。终端,至少在Unix系统上,是特殊的内核子系统,它们在字符提供者和内核之间提供连接。行缓冲,完全缓冲或原始输入是指字符传递到内核并可供用户进程使用的方式。因此,进程在读取tty或文件之间没有任何区别;它只是子系统对I / O子系统可用的内容产生语义差异。您可以阅读TTY demystified以了解TTY I / O会发生什么。

答案 2 :(得分:0)

你是对的,就STDIN而言,行和完全缓冲之间没有区别,因为libc仍然需要读取更大的块来查找其中的换行符。不支持从内核管道缓冲区读取单个行。请考虑以下示例:

printf "a\nb\nc\n" | (sed 1q ; sed 1q ; sed 1q)
a

正如您所看到的,第一个sed实例在尝试读取单行时获取了所有数据。无论STDIN是完全缓冲还是行缓冲,结果都是相同的。例如,选中stdbuf手册页:

  

如果MODE是&#39; L&#39;相应的流将被行缓冲。标准输入时此选项无效。

完全缓冲和线缓冲之间的差异在输出流中变得可见,控制它们何时被刷新。