2017-08-06 142 views
7

Trong suốt những năm làm lập trình viên C, tôi luôn bị nhầm lẫn về các mô tả tệp luồng chuẩn. Một số nơi, như Wikipedia [1], nói:Hành vi lạ khi thực hiện các chức năng thư viện trên STDOUT và các tệp mô tả của STDIN

Trong ngôn ngữ lập trình C, đầu vào, đầu ra, và dòng sai số chuẩn được gắn vào các tập tin mô tả Unix hiện 0, 1 và 2 tương ứng.

này được hỗ trợ bởi unistd.h:

/* Standard file descriptors. */ 
#define STDIN_FILENO 0  /* Standard input. */ 
#define STDOUT_FILENO 1  /* Standard output. */ 
#define STDERR_FILENO 2  /* Standard error output. */ 

Tuy nhiên, mã này (trên hệ thống có):

write(0, "Hello, World!\n", 14); 

Sẽ in Hello, World! (và một dòng mới) để STDOUT. Điều này là kỳ quặc bởi vì mô tả tập tin của STDOUT được cho là 1. write -người gửi đến bộ mô tả 1 cũng in tới STDOUT.

Thực hiện ioctl trên bộ mô tả tệp 0 thay đổi đầu vào chuẩn [2] và trên bộ mô tả tệp 1 thay đổi đầu ra tiêu chuẩn. Tuy nhiên, thực hiện termios functions trên 0 hoặc 1 thay đổi đầu vào tiêu chuẩn [3][4].

Tôi rất bối rối về hành vi của file descriptor 1 và 0. Có ai biết lý do tại sao:

  • write ing tới 1 hoặc 0 ghi vào đầu ra tiêu chuẩn?
  • Thực hiện ioctl trên 1 sửa đổi đầu ra tiêu chuẩn và trên 0 sửa đổi đầu vào tiêu chuẩn, nhưng thực hiện tcsetattr/tcgetattr trên 1 hoặc 0 hoạt động cho đầu vào tiêu chuẩn?
+1

Tại sao trên thế giới bạn nghĩ rằng nó đang viết bất cứ điều gì để stdout? Nó đang viết cho thiết bị đầu cuối của bạn. Các stdout của quá trình của bạn có thể được kết hợp với thiết bị đầu cuối của bạn, nhưng họ không phải là điều tương tự. Đừng xúi giục cả hai. Trong trường hợp của bạn, stdin cũng được kết hợp với thiết bị đầu cuối vì vậy nó không đáng ngạc nhiên khi ghi vào stdin hiển thị trên thiết bị đầu cuối. –

Trả lời

1

Hãy bắt đầu bằng cách xem xét một số khái niệm quan trọng liên quan đến:

  • tập tin mô tả

    Trong hạt nhân hệ điều hành, mỗi tập tin, thiết bị đầu cuối đường ống, thiết bị đầu cuối ổ cắm, nút thiết bị mở, và cứ như vậy, có một mô tả tệp . Hạt nhân sử dụng chúng để theo dõi vị trí trong tệp, các cờ (đọc, viết, nối thêm, đóng-trên-exec), khóa ghi, v.v.

    Mô tả tệp nằm bên trong hạt nhân và không thuộc về bất kỳ quá trình cụ thể nào (trong các triển khai điển hình).
     

  • file descriptor

    Từ quá trình quan điểm, mô tả tập tin là các số nguyên mà xác định tập tin mở, đường ống, ổ cắm, FIFOs, hoặc các thiết bị.

    Hạt nhân hệ điều hành giữ bảng mô tả cho từng quy trình. Bộ mô tả tập tin được sử dụng bởi quá trình này chỉ đơn giản là một chỉ mục cho bảng này.

    Các mục nhập trong bảng mô tả tệp tham chiếu đến mô tả tệp hạt nhân.

Bất kỳ khi nào quá trình sử dụng dup() or dup2() để nhân bản bộ mô tả tệp, hạt nhân chỉ sao chép mục nhập trong bảng mô tả tệp cho quá trình đó; nó không trùng lặp với các mô tả tập tin nó giữ cho chính nó.

Khi một quy trình dĩa, tiến trình con sẽ có bảng mô tả tệp riêng, nhưng các mục vẫn trỏ đến cùng một mô tả tệp hạt nhân. (Về bản chất là một shallow copy, tất cả sẽ ghi các mục bảng mô tả là tham chiếu đến các mô tả tệp. Các tham chiếu được sao chép; các mục tiêu được đề cập vẫn giữ nguyên.)

Khi một quá trình gửi một bộ mô tả tệp đến một quy trình khác qua Unix Thông báo phụ trợ của ổ cắm miền, hạt nhân thực sự phân bổ một bộ mô tả mới trên máy thu, và sao chép mô tả tệp mà bộ mô tả được chuyển đến đề cập đến.

Tất cả hoạt động rất tốt, mặc dù có một chút nhầm lẫn rằng "mô tả tệp""mô tả tệp" rất giống nhau.

Tất cả những gì phải làm với các hiệu ứng mà OP đang thấy?

Bất cứ khi nào các quy trình mới được tạo, thường mở thiết bị đích, đường ống hoặc ổ cắm và dup2() bộ mô tả cho đầu vào tiêu chuẩn, đầu ra tiêu chuẩn và lỗi chuẩn. Điều này dẫn đến tất cả ba mô tả chuẩn đề cập đến cùng một mô tả tệp và do đó bất kỳ hoạt động nào hợp lệ sử dụng một bộ mô tả tệp, đều hợp lệ bằng cách sử dụng các trình mô tả tệp khác.

Điều này là phổ biến nhất khi chạy các chương trình trên bảng điều khiển, khi đó ba trình mô tả tất cả đều tham chiếu đến cùng một mô tả tệp; và mô tả tập tin đó mô tả kết thúc nô lệ của một thiết bị nhân vật giả.

Hãy xem xét các chương trình sau đây, 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; 
} 

Phải mất hai hoặc nhiều tham số. Tham số đầu tiên là một tệp hoặc thiết bị, và thứ hai là lệnh, với phần còn lại của các tham số được cung cấp cho lệnh. Lệnh này được chạy, với tất cả ba mô tả tiêu chuẩn được chuyển hướng đến tệp hoặc thiết bị được đặt tên trong tham số đầu tiên. Bạn có thể biên dịch ở trên bằng gcc bằng cách sử dụng ví dụ:

gcc -Wall -O2 run.c -o run 

Hãy viết một tiện ích thử nghiệm nhỏ, 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; 
} 

Phải mất đúng một tham số, một tập tin hoặc điện thoại để viết, để báo cáo bằng văn bản cho dù đầu vào tiêu chuẩn, và đọc từ đầu ra tiêu chuẩn và công việc lỗi. (Chúng ta thường có thể sử dụng $(tty) trong các vỏ Bash và POSIX, để tham chiếu đến thiết bị đầu cuối thực tế, sao cho báo cáo có thể nhìn thấy trên thiết bị đầu cuối.) Biên dịch nó bằng cách sử dụng ví dụ:

gcc -Wall -O2 report.c -o report 

Bây giờ, chúng ta có thể kiểm tra một số thiết bị:

./run /dev/null ./report $(tty) 
./run /dev/zero ./report $(tty) 
./run /dev/urandom ./report $(tty) 

hoặc trên bất cứ điều gì chúng tôi muốn. Trên máy tính của tôi, khi tôi chạy trên một tập tin, nói

./run some-file ./report $(tty) 

bằng văn bản cho đầu vào tiêu chuẩn, và đọc từ đầu ra tiêu chuẩn và sai số chuẩn tất cả các công việc - được như mong đợi, như mô tả tập tin tham khảo cùng , có thể đọc và ghi được, mô tả tệp.

Kết luận, sau khi chơi với phần trên, là có không có hành vi lạ ở đây tại tất cả. Tất cả đều hoạt động chính xác như mong đợi, nếu mô tả tệp được sử dụng bởi các quy trình chỉ đơn giản là tham chiếu đến mô tả nội bộ của hệ điều hành mô tả tệp và mô tả đầu vào, đầu ra và lỗi chuẩn là dup ủy quyền cho nhau.

6

Tôi đoán đó là vì trong Linux của tôi, cả hai 01 theo mặc định mở ra với đọc/viết đến /dev/tty đó là thiết bị đầu cuối kiểm soát của quá trình. Vì vậy, thực sự có thể thậm chí là đọc từ stdout.

Tuy nhiên điều này phá vỡ ngay sau khi bạn ống một cái gì đó hoặc thu nhỏ:

#include <unistd.h> 
#include <errno.h> 
#include <stdio.h> 

int main() { 
    errno = 0; 
    write(0, "Hello world!\n", 14); 
    perror("write"); 
} 

và chạy với

% ./a.out 
Hello world! 
write: Success 
% echo | ./a.out 
write: Bad file descriptor 

termios chức năng luôn làm việc trên các đối tượng thiết bị đầu cuối cơ bản thực tế, vì vậy nó doesn cho dù 0 hoặc 1 được sử dụng miễn là nó được mở thành một tty.

+2

Nếu chúng ta nghiên cứu chi tiết, nó thậm chí còn thú vị hơn một chút. Mỗi * tập tin mô tả * số đề cập đến một cấu trúc hạt nhân được gọi là * tập tin mô tả * trong Linux và hệ thống Unixy. 'dup()' tạo một bộ mô tả tập tin mới (bằng cách nhân bản cái cũ); cái mới đề cập đến cùng một * mô tả tập tin *. Trong một ứng dụng đầu cuối, tất cả ba luồng tiêu chuẩn là 'dup2()' 'đã được sửa đổi từ pseudoterminal, và cả ba hành xử theo cùng một cách chính xác (nghĩa là, bạn có thể ghi vào 'STDIN_FILENO' và đọc từ' STDOUT_FILENO' và 'STDERR_FILENO '). Tuy nhiên, điều này không giới hạn đối với giả giả: [...] –

+1

[...] Nó có thể/sẽ xảy ra bất cứ khi nào đầu vào và đầu ra/lỗi chuẩn bắt nguồn từ cùng một mô tả tệp * có thể ghi * - là một giả (tty)), tệp hoặc thậm chí là một ổ cắm. Nếu có sự quan tâm, tôi có thể cung cấp một chương trình ví dụ cầm tay POSIX có thể được sử dụng để thử nghiệm và thăm dò. –

+0

@NominalAnimal bạn nên viết câu trả lời sau đó. Tôi bắt đầu với * Tôi đoán * bởi vì tôi không có bất kỳ nguồn có thẩm quyền về cách điều này xảy ra, và đó là nó là POSIX và đó chỉ là Linux, ngoài 'dup2' tất nhiên. –

Các vấn đề liên quan