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

时间:2016-10-10 02:50:51

标签: file-io io posix wchar-t widechar

The standard说:

  

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

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

以下是在调用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技巧到底是什么?

2 个答案:

答案 0 :(得分:2)

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

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

  

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

glibc试图实现Posix语义。不幸的是,它并没有完全正确。

当然,如果不设置方向,就无法写入流。因此,为了遵守这个奇怪的要求,glibc尝试使用与OP结尾指向的代码基于与stderr相同的fd创建新流:

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

尝试打开允许读写的流。它将与原始的$ ./a.out 2>/dev/null 一起使用,它只是控制台fd的一个副本,最初是为读取和写入而打开的。但是当你使用重定向将stderr绑定到其他设备时:

fdopen

您正在传递可执行文件,仅为输出打开fd。并且fdopen不会让你逃脱:

  

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

errno的glibc实现实际检查,如果指定的模式需要fd不可用的访问权限,则返回NULL,EINVAL设置为$ ./a.out 2<>/dev/null

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

stderr

但您首先想要的是在追加模式中重定向$ ./a.out 2>>/dev/null

"w+"

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

我不知道为什么glibc代码使用stderr作为模式参数,因为它无意从"w"读取。 {{1}}应该可以正常工作,虽然它可能不会保留附加模式,这可能会产生不幸的后果。

答案 1 :(得分:1)

我不确定在没有询问glibc开发人员的情况下是否对“为什么”有一个很好的答案 - 它可能只是一个错误 - 但POSIX要求似乎与ISO C冲突,后者读入7.21.2, ¶4

  

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

此外,perror似乎有资格作为“字节I / O函数”,因为它需要char *,并且每7.21.10.4 ¶2,“写一个字符序列”。

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

至于问题中的实际例子:

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