如何正确地 fread & fwrite from & to a pipe

How to properly fread & fwrite from & to a pipe

我有这段代码充当两个 shell 调用之间的管道。

它从一个管道读取,然后写入另一个管道。

#include <stdio.h>
#include <stdlib.h>


#define BUFF_SIZE (0xFFF)

/*
 *  $ cat /tmp/redirect.txt |less
 */
int main(void)
{
    FILE    *input;
    FILE    *output;
    int     c;
    char    buff[BUFF_SIZE];
    size_t  nmemb;

    input   = popen("cat /tmp/redirect.txt", "r");
    output  = popen("less", "w");
    if (!input || !output)
        exit(EXIT_FAILURE);

#if 01
    while ((c = fgetc(input))  !=  EOF)
        fputc(c, output);
#elif 01
    do {
        nmemb   = fread(buff, 1, sizeof(buff), input);
        fwrite(buff, 1, nmemb, output);
    } while (nmemb);
#elif 01
    while (feof(input) != EOF) {
        nmemb   = fread(buff, 1, sizeof(buff), input);
        fwrite(buff, 1, nmemb, output);
    }
#endif
/*
 * EDIT: The previous implementation is incorrect:
 * feof() return non-zero if EOF is set
 * EDIT2:  Forgot the !.  This solved the problem.
 */
#elif 01
    while (feof(input)) {
        nmemb   = fread(buff, 1, sizeof(buff), input);
        fwrite(buff, 1, nmemb, output);
    }
#endif

    pclose(input);
    pclose(output);

    return  0;
}

我希望它高效,所以我想用fread()&fwrite()来实现它。我尝试了3种方法。

第一个是用fgetc()&fputc()实现的,所以会很慢。但是它工作正常,因为它检查 EOF 所以它会等到 cat (或我使用的任何 shell 调用)完成它的工作。

第二个更快,但我担心我不检查 EOF 所以如果有任何时候管道是空的(但 shell 调用没有' t完成,所以以后可能不会为空),它会关闭管道并结束。

第三个实现是我想做的,它相对有效(所有文本都由 less 接收),但由于某种原因它卡住了并且没有关闭管道(似乎就像它永远不会得到 EOF)。

编辑: 第三个实现有问题。第四次尝试解决错误,但现在 less 没有收到任何东西。

这应该如何正确完成?

最简单的解决方案:


while (1) {
    nmemb = fread(buff, 1, sizeof buff, input);
    if (nmemb < 1) break; 
    fwrite(buff, 1, nmemb, output);
}

同样,对于 getc() 案例:


while (1) {
    c = getc(input);
    if (c == EOF) break;
    putc(c, output);
}

fgetc() 替换为 getc() 将提供与 fread() 情况相同的性能。 (getc() 将(通常)是一个宏,避免函数调用开销)。 [只看生成的程序集。

首先,我认为您在缓冲方面遇到的问题比效率方面的问题更多。这是第一次处理 stdio 包时的常见问题。

其次,从输入到输出的简单数据复制器的最佳(也是最简单)实现是以下代码段(从 K&R 第一版复制。)。

while((c = fgetc(input)) != EOF) 
    fputc(c, output);

(好吧,不是文字副本,因为在那里,K&R 使用 stdinstdout 作为 FILE* 描述符,并且他们使用更简单的 getchar();putchar(c); 调用。)当你试图做得比这更好时,通常你会产生一些错误的假设,如缺乏缓冲或系统调用次数的谬误。

stdio 当标准输出是一个管道 时进行完全缓冲(事实上,它总是进行完全缓冲,除非文件描述符给出 trueisatty(3) 函数调用),所以你应该这样做,如果你想尽快看到输出,至少,没有输出缓冲(像 setbuf(out, NULL);fflush()) 你的输出在某个时候,所以当你在输入中等待更多数据时它不会在输出中缓冲。

您似乎看到 less(1) 程序的输出不可见,因为它在您的程序内部进行了缓冲。而这正是正在发生的事情......假设你提供你的程序(尽管处理了单个字符,它正在做完整的缓冲)在完整的输入缓冲区(BUFSIZ 字符)之前没有得到任何输入已经喂给它了。然后,很多单个 fgetc() 调用在一个循环中完成,很多 fputc() 调用在一个循环中完成(每个 BUFSIZ 调用)并且缓冲区在输出。但是这个缓冲区没有被写入,因为它还需要一个字符来强制刷新。因此,在您获得前两个 BUFSIZ 数据块之前,您不会将任何内容写入 less(1).

一个简单有效的方法是在 fputc(c, out); 之后检查字符是否为 \n,并在这种情况下使用 fflush(out); 刷​​新输出,因此您将编写一次输出一行。

fputc(c, out);
if (c == '\n') fflush(out);

如果您不做某事,缓冲将以 BUFSIZ 块的形式进行,通常不会在输出端有如此多的数据之前进行。并始终记住 fclose() 事情(好吧,这是由 stdio 处理的),否则如果您的过程被中断,您可能会丢失输出。

恕我直言,您应该使用的代码是:

while ((c = fgetc(input))  !=  EOF) {
    fputc(c, output);
    if (c == '\n') fflush(output);
}
fclose(input);
fclose(output);

为了获得最佳性能,同时不会不必要地阻塞缓冲区中的输出数据。

顺便说一句,对一个字符执行 fread()fwrite() 是浪费时间,也是一种使事情复杂化(并且容易出错)的方法。一个字符的 fwrite() 不会避免使用缓冲区,因此您不会获得比使用 fputc(c, output);.

更高的性能

BTW(bis) 如果您想自己进行缓冲,请不要调用 stdio 函数,只需对普通系统文件描述符使用 read(2)write(2) 调用即可。一个好的方法是:

int input_fd = fileno(input); /* input is your old FILE * given by popen() */
int output_fd = fileno(output);

while ((n = read(input_fd, your_buffer, sizeof your_buffer)) > 0) {
    write(output_fd, your_buffer, n);
}
switch (n) {
case 0: /* we got EOF */
    ...
    break;
default: /* we got an error */
    fprintf(stderr, "error: read(): %s\n", strerror(errno));
    ...
    break;
} /* switch */

但这只会在缓冲区填满数据或没有更多数据时唤醒您的程序。

如果你想在少一行的情况下将数据提供给less(1),那么你可以完全禁用输入缓冲区:

setbuf(input, NULL);
int c; /* int, never char, see manual page */
while((c == fgetc(input)) != EOF) {
    putc(c, output);
    if (c == '\n') fflush(output);
}

一旦您生成一行输出文本,您就会 less(1) 开始工作。

你到底想做什么? (很高兴知道,因为您似乎正在重新发明 cat(1) 程序,但功能有所减少)