为啥 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 规范仅讨论了如何将初始方向分配给流。 它说只有freopen
和fwide
可以改变方向。 perror
规范说它不会改变方向,这与此一致。以上是关于为啥 perror() 在重定向时会改变流的方向?的主要内容,如果未能解决你的问题,请参考以下文章