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
技巧到底是什么?
答案 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要求无效。
至于问题中的实际例子:
perror
的方向是正确的,并且没有因呼叫而改变。perror
将流定向到字节方向。这似乎是ISO C所要求的,但POSIX不允许这样做。