为啥 perror() 在重定向时会改变流的方向?

Posted

技术标签:

【中文标题】为啥 perror() 在重定向时会改变流的方向?【英文标题】:Why perror() changes orientation of stream when it is redirected?为什么 perror() 在重定向时会改变流的方向? 【发布时间】:2017-02-18 10:14:26 【问题描述】:

The standard 说:

perror() 函数不应改变标准错误流的方向。

This 是perror() 在 GNU libc 中的实现。

以下是在调用perror() 之前,stderr 面向宽、面向多字节和不面向时的测试。 测试 1) 和 2) 均正常。问题在于测试 3)。

1) stderr 面向广泛:

#include <stdio.h>
#include <wchar.h>
#include <errno.h>
int main(void)

  fwide(stderr, 1);
  errno = EINVAL;
  perror("");
  int x = fwide(stderr, 0);
  printf("fwide: %d\n",x);
  return 0;

$ ./a.out
Invalid argument
fwide: 1
$ ./a.out 2>/dev/null
fwide: 1

2) stderr 是面向多字节的:

#include <stdio.h>
#include <wchar.h>
#include <errno.h>
int main(void)

  fwide(stderr, -1);
  errno = EINVAL;
  perror("");
  int x = fwide(stderr, 0);
  printf("fwide: %d\n",x);
  return 0;

$ ./a.out
Invalid argument
fwide: -1
$ ./a.out 2>/dev/null
fwide: -1

3) stderr 没有方向性:

#include <stdio.h>
#include <wchar.h>
#include <errno.h>
int main(void)

  printf("initial fwide: %d\n", fwide(stderr, 0));
  errno = EINVAL;
  perror("");
  int x = fwide(stderr, 0);
  printf("fwide: %d\n", x);
  return 0;

$ ./a.out
initial fwide: 0
Invalid argument
fwide: 0
$ ./a.out 2>/dev/null
initial fwide: 0
fwide: -1

如果perror() 被重定向,为什么它会改变流的方向?这是正确的行为吗?

this 代码如何工作?这个__dup 的诡计是什么?

【问题讨论】:

您是否考虑过在第一个示例中检查对fwide() 的初始调用?成功了吗?你怎么知道的? @RastaJedi 上面说的是宽字符函数和多字节函数,比如fprintf(stderr, "something"); vs. fwprintf(stderr,L"something");,但是这个例子使用了shell重定向,这是另一回事。 @JonathanLeffler 在示例 3 中调用 perror() 之前调用 fwide(stderr,0);)返回 0,正如预期的那样,因为尚未对 stderr 执行任何操作。请参阅编辑后的示例 3) - 测试表明一切都是正确的。 @RastaJedi 请参阅编辑后的示例 3) - 测试表明一切正常,因此问题不在 shell 中。 我问的是第一个例子,而不是第三个。但是,fwide() 的 POSIX 规范提到很难发现错误,并建议在调用函数之前设置errno = 0。但是,在我看来,示例 3 的非重定向变体是有问题的变体。 perror() 写入stderr 流,因此一旦perror() 返回,它应该被赋予一个方向,但它似乎没有发生。重定向 stderr 时的变化是我所期望的行为。 【参考方案1】:

TL;DR:是的,这是 glibc 中的一个错误。如果你关心它,你应该报告它。

perror 不改变流方向的引用要求在 Posix 中,但 C 标准本身似乎并不要求。然而,Posix 似乎非常坚持 stderr 的方向不能被 perror 改变,即使 stderr 尚未定向。 XSH 2.5 Standard I/O Streams:

如果流已经是面向字节的,则 perror()、psiginfo() 和 psignal() 函数的行为应与上述字节输出函数的行为相同,并且如果流已经是面向字节的,则其行为应与宽字符输出函数的行为相同,如果流已经是广泛的。如果流没有方向,则它们的行为应与字节输出函数的描述相同,但它们不应改变流的方向。

而 glibc 尝试实现 Posix 语义。不幸的是,它并不完全正确。

当然,不设置方向就不可能写入流。因此,为了满足这个奇怪的要求,glibc 尝试使用与 stderr 相同的 fd 创建一个新流,使用 OP 末尾指向的代码:

58    if (__builtin_expect (_IO_fwide (stderr, 0) != 0, 1)
59      || (fd = __fileno (stderr)) == -1
60      || (fd = __dup (fd)) == -1
61      || (fp = fdopen (fd, "w+")) == NULL)
62     ...

去掉内部符号后,基本上等同于:

if (fwide (stderr, 0) != 0
    || (fd = fileno (stderr)) == -1
    || (fd = dup (fd)) == -1
    || (fp = fdopen (fd, "w+")) == NULL)
  
    /* Either stderr has an orientation or the duplication failed,
     * so just write to stderr
     */
    if (fd != -1) close(fd);
    perror_internal(stderr, s, errnum);
  
else
  
    /* Write the message to fp instead of stderr */
    perror_internal(fp, s, errnum);
    fclose(fp);
  

fileno 从标准 C 库流中提取 fd。 dup 获取一个 fd,复制它,然后返回副本的编号。 fdopen 从 fd 创建标准 C 库流。简而言之,这不会重新打开stderr;相反,它创建(或尝试创建)stderr 的副本,可以在不影响stderr 的方向的情况下写入该副本。

不幸的是,由于模式,它不能可靠地工作:

fp = fdopen(fd, "w+");

这试图打开一个允许读写的流。并且它将与原始的stderr 一起使用,它只是控制台 fd 的副本,最初是为读写而打开的。但是,当您通过重定向将 stderr 绑定到其他设备时:

$ ./a.out 2>/dev/null

您正在向可执行文件传递一个仅用于输出的 fd。 fdopen 不会让你逃脱惩罚:

应用程序应确保 fildes 引用的打开文件描述的文件访问模式允许 mode 参数表示的流的模式。

fdopen 的 glibc 实现实际上会检查并返回 NULL,如果您指定的模式需要 fd 不可用的访问权限,则将 errno 设置为 EINVAL

因此,如果您重定向 stderr 以进行读取和写入,您可以通过测试:

$ ./a.out 2<>/dev/null

但您可能首先想要的是在附加模式下重定向stderr

$ ./a.out 2>>/dev/null

据我所知,bash 不提供读取/附加重定向的方法。

我不知道为什么 glibc 代码使用 "w+" 作为模式参数,因为它无意读取 stderr"w" 应该可以正常工作,尽管它可能不会保留附加模式,这可能会带来不幸的后果。

【讨论】:

如果在调用perror() 之前未定向stderr,则还有另一种可能使stderr 未定向。如果stderr 在调用perror() 之前没有定向(在以默认多字节方向输出所有内容之后),perror() 可以重新打开stderr,而不是复制stderr。这将有效地使stderr 不定向。这种方法可以安全使用吗? 这是错误报告的链接:sourceware.org/bugzilla/show_bug.cgi?id=20677 BTW,你知道如何重新打开以前打开的错误吗?我在这里打开了一个错误:sourceware.org/bugzilla/show_bug.cgi?id=20639 它被关闭为 UNCONFIRMED。但这确实是一个错误。我想把它修好。 @igor:UNCONFIRMED 并不意味着它已关闭。这只是意味着没有人确认这是一个错误。如果你要从 *** 复制答案,你真的应该提供一个讨论链接。 提供了讨论的链接 参见 OP 中的 EDIT【参考方案2】:

如果不询问 glibc 开发人员,我不确定“为什么”是否有一个好的答案 - 这可能只是一个错误 - 但 POSIX 要求似乎与 ISO C 冲突,其内容为7.21.2, ¶4:

每个流都有一个方向。在流与外部文件关联之后,但在对其执行任何操作之前,流是没有方向的。 一旦将宽字符输入/输出功能应用于无方向的流,该流就变成了宽方向的流。类似地,一旦一个字节输入/输出函数被应用到一个没有方向的流上,这个流就变成了一个面向字节的流。 只有调用 freopen 函数或 fwide 函数才能改变流的方向. (成功调用 freopen 会删除任何方向。)

此外,perror 似乎符合“字节 I/O 函数”的条件,因为它需要一个 char *,并且根据 7.21.10.4 ¶2,“写入一个字符序列”。

由于 POSIX 在发生冲突时遵循 ISO C,因此有理由认为此处的 POSIX 要求无效。

至于问题中的实际例子:

    未定义的行为。在面向宽的流上调用字节 I/O 函数。 完全没有争议。呼叫 perror 的方向是正确的,并且没有因呼叫而改变。 调用perror 将流定向到字节方向。这似乎是 ISO C 的要求,但 POSIX 不允许这样做。

【讨论】:

以上是对的,但也有这样的:perror() 函数不应该改变标准错误流的方向。 而且我认为它们并不矛盾彼此。在示例 3) 的非重定向情况下,行为是正确的。 @IgorLiferenko:该文本来自 POSIX,而不是 ISO C。我的观点是它似乎与 ISO C 的要求相冲突。 但是在 GNU libc 实现中,他们特别提到了这一点。所以,似乎没有冲突。示例 3) 证实了这一点。但只是部分...... 它们如何冲突?您引用的 ISO C 规范仅讨论了如何将初始方向分配给流。 它说只有freopenfwide 可以改变方向。 perror 规范说它不会改变方向,这与此一致。

以上是关于为啥 perror() 在重定向时会改变流的方向?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在 URL 中缺少尾部斜杠 (/) 时会发生重定向?

c++中重载输出操作符,为啥要返回引用

为啥 ErrorLevel 只在 || 之后设置重定向失败时的操作员?

在重定向期间切换协议

Passport.JS 卡在重定向

laravel 4.2在重定向时重新生成会话