这里有两个版本的代码,已转换为完整的工作程序。最基本的版本是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.h
和
stderr.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);
}
}
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
$
然而,如果输入足够多,则变得可靠:
$ 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)
$
setbuf()
或(更好的)setvbuf()
来控制新打开文件(或管道,或stdout
只要它是您对stdout
进行的第一件事)的缓冲。逐个字符刷新很昂贵。最好使用write()
系统调用。如果您写入了一个破损的管道,它将返回错误(-1
)并将errno
设置为EPIPE
。然后,您可以一次写入多个字符,而不需要逐个写入。 - Jonathan Lefflersignal(SIGPIPE, SIG_IGN);
,您将会看到它。 - Joshua Katz