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

Why perror() changes orientation of stream when it is redirected?

The standard 表示:

The perror() function shall not change the orientation of the standard error stream.

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 把戏到底是什么?

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

Each stream has an orientation. After a stream is associated with an external file, but before any operations are performed on it, the stream is without orientation. Once a wide character input/output function has been applied to a stream without orientation, the stream becomes a wide-oriented stream. Similarly, once a byte input/output function has been applied to a stream without orientation, the stream becomes a byte-oriented stream. Only a call to the freopen function or the fwide function can otherwise alter the orientation of a stream. (A successful call to freopen removes any orientation.)

此外,perror 似乎符合 "byte I/O function" 的条件,因为它需要 char * 并且根据 7.21.10.4 ¶2,"writes a sequence of characters".

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

至于题中的实际例子:

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

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

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

The perror(), psiginfo(), and psignal() functions shall behave as described above for the byte output functions if the stream is already byte-oriented, and shall behave as described above for the wide-character output functions if the stream is already wide-oriented. If the stream has no orientation, they shall behave as described for the byte output functions except that they shall not change the orientation of the stream.

并且 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,复制它,returns 复制的数量。 fdopen 从 fd 创建标准 C 库流。简而言之,那不会重新打开stderr;相反,它创建(或尝试创建)stderr 的副本,可以在不影响 stderr.

方向的情况下写入该副本

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

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

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

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

您正在传递一个仅为输出而打开的 fd 可执行文件。 fdopen 不会让你逍遥法外的:

The application shall ensure that the mode of the stream as expressed by the mode argument is allowed by the file access mode of the open file description to which fildes refers.

fdopen 的 glibc 实现实际检查,并且 returns NULL 且 errno 设置为 EINVAL 如果您指定的模式要求访问权限不可用fd.

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

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

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

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

据我所知,bash 没有提供 read/append 重定向的方法。

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