2017-08-06 85 views
7

作爲一名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作品用於標準輸入?
+1

爲什麼在這個世界上,你認爲這是寫任何標準輸出?它正在寫入您的終端。您的流程的標準輸出可能與您的終端相關聯,但它們不是一回事。不要混淆兩者。在你的情況下,標準輸入也與終端相關聯,所以寫入標準輸入寫入終端並不奇怪。 –

回答

1

首先讓我們來回顧所涉及的一些關鍵概念:

  • 文件描述

    在操作系統內核,每一個文件,管道終點,套接字端點,打開設備節點,如此,有一個文件說明。內核使用這些來跟蹤文件中的位置,標誌(讀,寫,附加,關閉執行),記錄鎖等等。

    文件描述是內核的內核,不屬於任何特定的進程(在典型的實現中)。
     

  • 文件描述符

    從工藝角度看,文件描述符是標識打開的文件,管道,套接字,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) 

寫入標準輸入,並從標準輸出和標準錯誤的所有作品閱讀 - 這是象預期的那樣文件描述符指的是同一個,可讀寫,文件描述。

結束後,玩了上面,是有這裏沒有什麼奇怪的行爲在這裏根本沒有。如果所使用的文件描述符僅僅是對操作系統內部文件的文件描述的簡單引用,並且標準輸入,輸出和錯誤描述符是彼此的許可證,則它們的行爲完全如預期。

6

我想這是因爲我在Linux下,01默認情況下與開讀/寫/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都可以使用。

+2

如果我們深入細節,那甚至會比這更有趣。每個*文件描述符*數字指Linux和Unixy系統中稱爲*文件描述*的內核結構。 'dup()'創建一個新的文件描述符(通過複製舊文件描述符);新引用的是相同的*文件描述*。在一個終端應用程序中,所有三個標準流都來自僞終端,這三個標準流的行爲完全相同(也就是說,您可以寫入STDIN_FILENO,並從STDOUT_FILENO和STDERR_FILENO讀取')。然而,這不限於僞終端:[...] –

+1

[...]只要標準輸入和輸出/錯誤來自相同的,可寫的*文件描述* - 它可以/將發生一個僞終端(tty ),文件甚至套接字。如果有興趣,我可以提供一個可用於測試和探索的POSIX便攜式示例程序。 –

+0

@NominalAnimal你應該寫一個答案。我從*開始*我猜*,因爲我沒有任何關於這種情況發生的權威來源,其中哪些是POSIX,哪些只是Linux,當然超出'dup2'。 –