从C程序中检测关闭的Unix管道

3

我正在编写一个程序(A),其输出结果将被管道传递给另一个程序(B)。如果B没有读取完A的所有数据,我希望A能够处理错误。

为了简化下面的示例,我删除了不需要的代码。

int main(int argc, char *argv[])
{
    signal(SIGPIPE, SIG_IGN);

    FILE *file = fopen("test.txt" , "r");

    if (!file)
        ... Error handling ...

    int next;
    while ((next = fgetc(file)) != EOF)
        if (fputc(next, stdout) < 0 || fflush(stdout) != 0)
            ... print error message to stderr ...
}

这个程序编译没问题,但它只是间歇性地工作。我最初认为这是由于stdout中的某种缓冲区引起的,所以我添加了flush。

我知道这可能不是做这件事的最佳方式,但这仅仅是一个边角项目。有没有办法关闭缓冲?我应该使用其他传输方式吗?我在寻找解决方案时看到有人使用mkfifo。

编辑:我的问题不是检查stdout是否关闭。它是要移除缓冲区,使得管道表现正常。


我认为_这个答案_解决了你的问题。请确保阅读到最后一句话。 - ryyker
您可以使用setbuf()或(更好的)setvbuf()来控制新打开文件(或管道,或stdout只要它是您对stdout进行的第一件事)的缓冲。逐个字符刷新很昂贵。最好使用write()系统调用。如果您写入了一个破损的管道,它将返回错误(-1)并将errno设置为EPIPE。然后,您可以一次写入多个字符,而不需要逐个写入。 - Jonathan Leffler
@JonathanLeffler 我尝试过了,但仍然卡在间歇性错误上。有时它可以工作,而有时又不能。 - Joshua Katz
通过查看我最初发布的代码示例,您可以看到这不是我的问题。我的问题是缓冲区,而不是查看管道是否为空。我已经在我的代码中应用了主要修复程序。如果您查看我的代码中的 signal(SIGPIPE, SIG_IGN);,您将会看到它。 - Joshua Katz
我猜我是根据你的标题:从C程序检测关闭的Unix管道。无论如何,你不需要大声喊叫。我可以在没有大写字母的情况下很好地阅读。 - ryyker
2个回答

1

这里有两个版本的代码,已转换为完整的工作程序。最基本的版本是cp41.c

#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static inline void putch(char c)
{
    if (write(STDOUT_FILENO, &c, sizeof(c)) != sizeof(c))
    {
        int errnum = errno;
        fprintf(stderr, "Attempt to write '%c' failed (%d: %s)\n", c, errnum, strerror(errnum));
        exit(EXIT_FAILURE);
    }
}

int main(void)
{
    signal(SIGPIPE, SIG_IGN);

    int c;
    while ((c = getchar()) != EOF)
    {
        putch(c);
    }
    return 0;
}

更加华丽版本为cp43.c。您可以在GitHub上找到stderr.hstderr.c的代码,网址为https://github.com/jleffler/soq/tree/master/src/libsoq。我使用它是因为它使报告错误变得更加容易(单行函数调用而不是多行)。我可以在putch()函数中使用err_syserr(),但出于对简明版和华丽版的一致性考虑,我选择不这样做。
#include "stderr.h"
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static inline void putch(char c)
{
    if (write(STDOUT_FILENO, &c, sizeof(c)) != sizeof(c))
    {
        int errnum = errno;
        fprintf(stderr, "Attempt to write '%c' failed (%d: %s)\n", c, errnum, strerror(errnum));
        exit(EXIT_FAILURE);
    }
}

static const char usestr[] = "[-l logfile] [-e]";

int main(int argc, char **argv)
{
    err_setarg0(argv[0]);
    signal(SIGPIPE, SIG_IGN);
    const char *filename = NULL;
    int to_stderr = 0;

    int opt;
    while ((opt = getopt(argc, argv, "l:e")) != -1)
    {
        switch (opt)
        {
        case 'l':
            filename = optarg;
            break;
        case 'e':
            to_stderr = 1;
            break;
        default:
            err_usage(usestr);
            /*NOTREACHED*/
        }
    }

    if (optind != argc)
        err_usage(usestr);

    FILE *fp = NULL;
    if (filename != NULL)
        fp = fopen(filename, "w");

    int c;
    while ((c = getchar()) != EOF)
    {
        if (fp)
            putc(c, fp);
        if (to_stderr)
            putc(c, stderr);
        putch(c);
    }
    return 0;
}

这允许用户将正在处理的内容从标准输入复制到标准错误和/或日志文件中。这很有用。
裸骨代码结果证明是“不可靠”的。例如:
$ cp41 < cp41.c | sed 1q
#include <errno.h>
$

然而,如果输入足够多,则变得可靠:
$ cp41 < <(cat cp41.c cp41.c cp41.c) | sed 1q
#include <errno.h>
Attempt to write 'n' failed (32: Broken pipe)
$

原因在使用 cp43 时变得更加清晰:
$ ./cp43 <cp43.c | sed 3q
#include "stderr.h"
#include <errno.h>
#include <signal.h>
Attempt to write 'e' failed (32: Broken pipe)
$

使用-e选项:
$ ./cp43 -e <cp43.c | sed 3q
#include "stderr.h"
#include <errno.h>
#include <signal.h>
#in#include "stderr.h"
cl#include <errno.h>
u#include <signal.h>
de <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static inline void putch(char c)
{
    if (write(STDOUT_FILENO, &c, sizeof(c)) != sizeof(c))
    {
        int erAttempt to write 'r' failed (32: Broken pipe)
$

或者使用-l cp43.log选项:
$ ./cp43 -l cp43.log <cp43.c | sed 3q
#include "stderr.h"
#include <errno.h>
#include <signal.h>
Attempt to write ' ' failed (32: Broken pipe)
$ cat cp43.log
#include "stderr.h"
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static inline void putch(char c)
{
    if (write(STDOUT_FILENO, &c, sizeof(c)) != sizeof(c))
    {
        int errnum = errno;
        fprintf(stderr, "Attempt to write '%c' failed (%d: %s)\n", c, errnum, strerror(errnum));
        exit(EXIT_FAILURE);
    }
}

static const char usestr[] = "[-l logfile] [-e]";

int main(int argc, char **argv)
{
    err_setarg0(argv[0]);
    signal(SIGPIPE, SIG_IGN);
    const char *filename = NULL;
    int to_stderr = $

注意到在sed 3q命令读取之前,日志中已经写入了大量内容。最终的$提示符位于日志文件的部分行末尾。
问题在于,如果写入程序足够超前于sed 3q,它可能在sed终止之前完成读写,因此它永远不会收到SIGPIPE信号,因为当它写入时,管道从未“无读者”。
您可以通过使用其他程序而不是sed来加速该问题的发生。例如:
$ cp41 < cp41.c | echo

Attempt to write '#' failed (32: Broken pipe)
$

0

fgetc()fputc()是带缓冲的,使用read()write()系统调用可以实现无缓冲I/O。


1
好的建议,但是这如何解决检测关闭管道的问题? - ryyker
你在问题中提到“程序间歇性工作”,我给出了解决方案,因为首先不建议使用fgetc()和fputc()。很抱歉,但从问题中并不完全明显你是在问如何检测管道是否关闭。 - Pbd
@Pbd 我不是ryyker,但即使使用read()和write(),我仍然遇到了类似之前的问题。 - Joshua Katz
我认为你需要再加入一些代码,如果你试图向已关闭的另一端写入数据,就会引发SIGPIPE错误;同样地,如果写入端被关闭,那么读取操作将得到EOF。可能可以在A中安装一个信号处理程序,如果它是写入端并且收到了SIGPIPE信号,则表示B中的读取端已关闭;同样地,如果A是读取端,则读取EOF将表示B中的写入端已关闭。 - Pbd
@Pdb A 是写入端,B 是读取端。当 B 退出时,A 不会停止写入,也不会检测到它已经完成了写入。在这里发布帖子是因为 SIGPIPE 从未发生过。我已经将信号处理程序附加到了 SIGPIPE 上,但是我遇到了相同的问题。SIGPIPE 有时会执行,有时不会执行。当它执行/写入 EOF 发生时,我得到了错误的信息。打印的信息是从 B 退出后的位置开始的。这就是为什么我提到了缓存问题。我相当确定这不是 SIGPIPE,而是缓存问题。 - Joshua Katz

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