在STDOUT和STDIN的文件描述符上执行库函数的奇怪行为
作为一名C程序员,我一直对标准流文件描述符感到困惑。有些地方,像Wikipedia [1],说:在STDOUT和STDIN的文件描述符上执行库函数的奇怪行为
在C程序设计语言,该标准输入,输出和错误流附着到现有的Unix的文件描述符分别为0,1和2。
这是由unistd.h
备份:
/* Standard file descriptors. */
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */
然而,这个代码(在任何系统上):
write(0, "Hello, World!\n", 14);
将打印Hello, World!
(和换行符)来STDOUT
。这很奇怪,因为STDOUT
的文件描述符应该是1. write
-ing到文件描述符1 也打印到STDOUT
。
文件描述符0变化标准输入[2]执行ioctl
,并在文件描述符1个改变标准输出。但是,执行termios
functions 0或1更改标准输入[3][4]。
我对文件描述符的行为1和0有谁知道为什么很困惑:
-
write
荷兰国际集团以1或0写到标准输出? - 执行
ioctl
on 1修改标准输出,并在0修改标准输入,但在1或0上执行tcsetattr
/tcgetattr
作品用于标准输入?
首先让我们来回顾所涉及的一些关键概念:
-
文件描述
在操作系统内核,每一个文件,管道终点,套接字端点,打开设备节点,如此,有一个文件说明。内核使用这些来跟踪文件中的位置,标志(读,写,附加,关闭执行),记录锁等等。
文件描述是内核的内核,不属于任何特定的进程(在典型的实现中)。
-
文件描述符
从工艺角度看,文件描述符是标识打开的文件,管道,套接字,FIFO中,或设备的整数。
操作系统内核为每个进程保留一个描述符表。进程使用的文件描述符只是该表的索引。
文件描述符表中的条目是指内核文件描述。
当一个进程使用dup()
or dup2()
复制一个文件描述符,内核只复制在该进程的文件描述符表中的条目;它不会复制它自己保存的文件描述。
当进程分叉时,子进程获取自己的文件描述符表,但这些条目仍指向完全相同的内核文件描述。 (这基本上是一个shallow copy,所有文件描述符表项都将引用文件描述,引用被复制;引用的目标保持不变)
当进程通过Unix将文件描述符发送到另一个进程域套接字辅助消息,内核实际上在接收器上分配一个新的描述符,并复制所传输的描述符所引用的文件描述。
这一切都工作得非常好,虽然它是一个有点混乱是“文件描述符”和“文件说明”是如此的相似。
与OP看到的效果有什么关系?
每当创建新进程时,通常会打开目标设备,管道或套接字,并描述符标准输入,标准输出和标准错误。这导致所有三个标准描述符指向相同的文件描述,因此无论使用一个文件描述符的操作是否有效,使用其他文件描述符也是有效的。
当在控制台上运行程序时,这是最常见的,因为这三个描述符都明确指向相同的文件描述;并且该文件描述描述伪终端字符设备的从端。
考虑下面的程序,run.c:
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
static void wrerrp(const char *p, const char *q)
{
while (p < q) {
ssize_t n = write(STDERR_FILENO, p, (size_t)(q - p));
if (n > 0)
p += n;
else
return;
}
}
static inline void wrerr(const char *s)
{
if (s)
wrerrp(s, s + strlen(s));
}
int main(int argc, char *argv[])
{
int fd;
if (argc < 3) {
wrerr("\nUsage: ");
wrerr(argv[0]);
wrerr(" FILE-OR-DEVICE COMMAND [ ARGS ... ]\n\n");
return 127;
}
fd = open(argv[1], O_RDWR | O_CREAT, 0666);
if (fd == -1) {
const char *msg = strerror(errno);
wrerr(argv[1]);
wrerr(": Cannot open file: ");
wrerr(msg);
wrerr(".\n");
return 127;
}
if (dup2(fd, STDIN_FILENO) != STDIN_FILENO ||
dup2(fd, STDOUT_FILENO) != STDOUT_FILENO) {
const char *msg = strerror(errno);
wrerr("Cannot duplicate file descriptors: ");
wrerr(msg);
wrerr(".\n");
return 126;
}
if (dup2(fd, STDERR_FILENO) != STDERR_FILENO) {
/* We might not have standard error anymore.. */
return 126;
}
/* Close fd, since it is no longer needed. */
if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO)
close(fd);
/* Execute the command. */
if (strchr(argv[2], '/'))
execv(argv[2], argv + 2); /* Command has /, so it is a path */
else
execvp(argv[2], argv + 2); /* command has no /, so it is a filename */
/* Whoops; failed. But we have no stderr left.. */
return 125;
}
它有两个或多个参数。第一个参数是文件或设备,第二个参数是命令,其余参数提供给该命令。运行该命令,将所有三个标准描述符重定向到第一个参数中指定的文件或设备。你可以使用gcc编译上面的例子。
gcc -Wall -O2 run.c -o run
让我们写一个小的测试工具,report.c:
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
int main(int argc, char *argv[])
{
char buffer[16] = { "\n" };
ssize_t result;
FILE *out;
if (argc != 2) {
fprintf(stderr, "\nUsage: %s FILENAME\n\n", argv[0]);
return EXIT_FAILURE;
}
out = fopen(argv[1], "w");
if (!out)
return EXIT_FAILURE;
result = write(STDIN_FILENO, buffer, 1);
if (result == -1) {
const int err = errno;
fprintf(out, "write(STDIN_FILENO, buffer, 1) = -1, errno = %d (%s).\n", err, strerror(err));
} else {
fprintf(out, "write(STDIN_FILENO, buffer, 1) = %zd%s\n", result, (result == 1) ? ", success" : "");
}
result = read(STDOUT_FILENO, buffer, 1);
if (result == -1) {
const int err = errno;
fprintf(out, "read(STDOUT_FILENO, buffer, 1) = -1, errno = %d (%s).\n", err, strerror(err));
} else {
fprintf(out, "read(STDOUT_FILENO, buffer, 1) = %zd%s\n", result, (result == 1) ? ", success" : "");
}
result = read(STDERR_FILENO, buffer, 1);
if (result == -1) {
const int err = errno;
fprintf(out, "read(STDERR_FILENO, buffer, 1) = -1, errno = %d (%s).\n", err, strerror(err));
} else {
fprintf(out, "read(STDERR_FILENO, buffer, 1) = %zd%s\n", result, (result == 1) ? ", success" : "");
}
if (ferror(out))
return EXIT_FAILURE;
if (fclose(out))
return EXIT_FAILURE;
return EXIT_SUCCESS;
}
它带一个参数,文件或设备写入,报告是否写入标准输入,并从标准输出读取和错误工作。 (我们通常可以在Bash和POSIX shell中使用$(tty)
来引用实际的终端设备,以便报告在终端上可见)。现在
gcc -Wall -O2 report.c -o report
,我们可以检查一些设备:
./run /dev/null ./report $(tty)
./run /dev/zero ./report $(tty)
./run /dev/urandom ./report $(tty)
或任何人所愿。在我的机器,当我在文件上运行此,说
./run some-file ./report $(tty)
写入标准输入,并从标准输出和标准错误的所有作品阅读 - 这是象预期的那样文件描述符指的是同一个,可读写,文件描述。
结束后,玩了上面,是有这里没有什么奇怪的行为在这里根本没有。如果所使用的文件描述符仅仅是对操作系统内部文件的文件描述的简单引用,并且标准输入,输出和错误描述符是彼此的许可证,则它们的行为完全如预期。
我想这是因为我在Linux下,0
和1
默认情况下与开读/写到/dev/tty
是哪个进程的控制终端。所以确实可以从stdout
甚至读。
然而,这一旦打破,你管东西或缩小:
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
int main() {
errno = 0;
write(0, "Hello world!\n", 14);
perror("write");
}
与运行
% ./a.out
Hello world!
write: Success
% echo | ./a.out
write: Bad file descriptor
termios
函数总是实际的底层终端对象上工作,所以它不无论0
还是1
只要它打开到tty都可以使用。
如果我们深入细节,那甚至会比这更有趣。每个*文件描述符*数字指Linux和Unixy系统中称为*文件描述*的内核结构。 'dup()'创建一个新的文件描述符(通过复制旧文件描述符);新引用的是相同的*文件描述*。在一个终端应用程序中,所有三个标准流都来自伪终端,这三个标准流的行为完全相同(也就是说,您可以写入STDIN_FILENO,并从STDOUT_FILENO和STDERR_FILENO读取')。然而,这不限于伪终端:[...] –
[...]只要标准输入和输出/错误来自相同的,可写的*文件描述* - 它可以/将发生一个伪终端(tty ),文件甚至套接字。如果有兴趣,我可以提供一个可用于测试和探索的POSIX便携式示例程序。 –
@NominalAnimal你应该写一个答案。我从*开始*我猜*,因为我没有任何关于这种情况发生的权威来源,其中哪些是POSIX,哪些只是Linux,当然超出'dup2'。 –
为什么在这个世界上,你认为这是写任何标准输出?它正在写入您的终端。您的流程的标准输出可能与您的终端相关联,但它们不是一回事。不要混淆两者。在你的情况下,标准输入也与终端相关联,所以写入标准输入写入终端并不奇怪。 –