c - 如何从管道正确读取和写入

标签 c linux pipe eof stdio

我有这段代码,它充当两个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()实现它。我试了三种方法。
第一个是用fgetc()&fputc()实现的,所以速度会很慢。但是它工作正常,因为它检查EOF,所以它将等待cat(或我使用的任何shell调用)完成它的工作。
第二个比较快,但是我担心我没有检查EOF所以如果有任何时刻管道是空的(但是shell调用还没有完成,所以将来可能不会是空的),它将关闭管道并结束。
第三个实现是我想做的,它相对有效(所有文本都由less接收),但由于某种原因,它卡住了,没有关闭管道(似乎它永远不会得到EOF)。
编辑:第三个实现是错误的。第四个尝试解决这个bug,但是现在less没有收到任何消息。
这应该怎么做?

最佳答案

首先,我认为你在缓冲方面的问题比效率方面的问题要多。这是第一次处理stdio包时的常见问题。
其次,从输入到输出的简单数据复印机的最佳(也是最简单)实现是以下代码片段(从K&R第一版复制)。

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

(好吧,不是字面上的复制,K&R使用stdinstdout作为FILE*描述符,它们使用更简单的getchar();putchar(c);调用。)当您试图做得更好时,通常会产生一些错误的假设,如缺少缓冲或系统调用数的谬误。
stdio在标准输出是管道时执行完全缓冲(事实上,它总是执行完全缓冲,除非文件描述符将true提供给isatty(3)函数调用),因此,如果希望在输出可用时立即看到它,至少在某个点上不执行输出缓冲(使用类似于setbuf(out, NULL);fflush()的方法),所以当您在输入中等待更多数据时,它不会在输出中得到缓冲。
似乎是您看到less(1)程序的输出不可见,因为它被缓冲在程序的内部。而这正是正在发生的。。。假设您的程序feed(尽管对单个字符进行了处理,但它正在执行完全缓冲)在完全输入缓冲区(BUFSIZ个字符)被提供给它之前不会得到任何输入。然后,许多单个fgetc()调用在一个循环中完成,许多fputc()调用在一个循环中完成(每个调用都是BUFSIZ调用),并且在输出端填充缓冲区。但是这个缓冲区没有被写入,因为它需要一个字符来强制刷新。因此,在获得前两个BUFSIZ数据块之前,不会有任何东西写入less(1)
一个简单而有效的方法是在fputc(c, out);之后检查char是否是\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);更高的性能。
顺便说一句(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)程序,但功能有所减少)

关于c - 如何从管道正确读取和写入,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55668731/

相关文章:

c - 向结构数组添加行,类型 char* 和 char 不匹配

C++ 管道错误,pipe_wait 永远

c - 当文件描述符关闭时,有什么方法可以执行回调(在 Linux 上)

linux - 如何单独构建ld.so?

linux - Bash - 将命令输出管道化到 while 循环中

Ruby 在文件和标准输入之间切换

C 按位或返回 "f"

c - 输入 EOF 时 Shell 无限循环 C

c++ - 有没有办法反编译 Linux .so?

linux - 服务器扩展如何在 X 中工作?