首先讓我們來回顧所涉及的一些關鍵概念:
文件描述
在操作系統內核,每一個文件,管道終點,套接字端點,打開設備節點,如此,有一個文件說明。內核使用這些來跟蹤文件中的位置,標誌(讀,寫,附加,關閉執行),記錄鎖等等。
文件描述是內核的內核,不屬於任何特定的進程(在典型的實現中)。
文件描述符
從工藝角度看,文件描述符是標識打開的文件,管道,套接字,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)
寫入標準輸入,並從標準輸出和標準錯誤的所有作品閱讀 - 這是象預期的那樣文件描述符指的是同一個,可讀寫,文件描述。
結束後,玩了上面,是有這裏沒有什麼奇怪的行爲在這裏根本沒有。如果所使用的文件描述符僅僅是對操作系統內部文件的文件描述的簡單引用,並且標準輸入,輸出和錯誤描述符是彼此的許可證,則它們的行爲完全如預期。
爲什麼在這個世界上,你認爲這是寫任何標準輸出?它正在寫入您的終端。您的流程的標準輸出可能與您的終端相關聯,但它們不是一回事。不要混淆兩者。在你的情況下,標準輸入也與終端相關聯,所以寫入標準輸入寫入終端並不奇怪。 –