2017-08-06 13 views
7

Cプログラマとしての私の長年にわたり、私はいつも標準のストリームファイル記述子について混乱しました。ウィキペディア[1]、のようないくつかの場所は、言う:Cプログラミング言語で STDOUTとSTDINのファイル記述子でライブラリ関数を実行する際の奇妙な動作

は、標準入力、出力、およびエラーストリームは、既存の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); 

STDOUTHello, World!(改行)を出力します。 STDOUTのファイル記述子が1であると想定されているため、これは奇妙です。write - ファイル記述子1へのアクセス もSTDOUTに出力されます。

入力[2]標準ioctl 0ファイル記述子に変更を行い、ファイル記述子1に標準出力を変化させます。ただし、0または1のいずれかでtermios functionsを実行すると、標準入力が変更されます。[3][4] 1または0にする

  • writeが標準出力に書き込みます。なぜ

    私はファイルディスクリプタ1と0の振る舞いについて非常に困惑している誰でも知っていますか?

  • ioctlを1で実行すると標準出力が変更され、0では標準入力が変更されますが、標準入力の場合はtcsetattr/tcgetattrが1または0で動作しますか?
+1

なぜ世界では、何が標準出力に書いていると思いますか?あなたの端末に書き込んでいます。あなたのプロセスの標準出力はあなたの端末に関連しているかもしれませんが、同じものではありません。両者を融合させないでください。あなたのケースでは、stdinも端末に関連付けられているので、stdinへの書き込みが端末に表示されることは驚くことではありません。 –

答えて

1

のは、関連する重要な概念の一部見直してみましょう:オペレーティング・システム・カーネル、各ファイル、パイプエンドポイント、ソケットエンドポイント、オープンデバイスノードで

  • ファイルの説明

    をし、このように、ファイルの説明があります。カーネルは、ファイル内の位置、フラグ(読み取り、書き込み、追加、exec on close)、レコードロックなどを追跡するためにこれらを使用します。

    ファイルの説明はカーネルの内部であり、特にどのプロセスにも属していません(一般的な実装では)。プロセスの観点から
     

  • ファイルディスクリプタ

    、ファイル記述子が開いているファイル、パイプ、ソケット、FIFOの、またはデバイスを識別する整数です。

    オペレーティングシステムのカーネルは、プロセスごとにディスクリプタのテーブルを保持します。プロセスによって使用されるファイル記述子は、単にこのテーブルのインデックスです。

    ファイルディスクリプタテーブルのエントリは、カーネルファイルの説明を参照します。プロセスがファイルディスクリプタを複製するdup() or dup2()を使用するたびに、カーネルは唯一のそのプロセスのファイルディスクリプタテーブルのエントリを複製

。自分が保持しているファイル記述を複製しません。

プロセスがforkするとき、子プロセスは独自のファイルディスクリプタテーブルを取得しますが、エントリはまったく同じカーネルファイル記述を指しています。

プロセスがUnixを介して別のプロセスにファイル記述子を送信すると、プロセスがファイル記述子を他のプロセスに送信するときに、ファイル記述子がファイル記述子への参照となります(基本的にshallow copyです。ドメインソケット補助メッセージでは、カーネルは実際には受信者に新しい記述子を割り当て、転送された記述子が参照するファイル記述をコピーします。

「ファイルディスクリプタ」「ファイルの説明」はとても似ていること。少し混乱していますが、それはすべて、非常にうまく機能

OPが見ている効果とは何が関係していますか?

新しいプロセスが作成されるたびに、ターゲットデバイス、パイプ、またはソケット、およびdup2()記述子を標準入力、標準出力、および標準エラーにオープンするのが一般的です。これは、同じのファイル記述を参照する3つの標準記述子すべてにつながります。したがって、1つのファイル記述子を使用して有効な操作は、他のファイル記述子を使用しても有効です。

これはコンソールでプログラムを実行する場合に最も一般的です.3つの記述子はすべて同じファイル記述を参照しているためです。そのファイル記述は擬似端末文字デバイスのスレーブ側を記述する。

次のプログラムを考えてみましょう、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; 
} 

それは、二つ以上のパラメータを取ります。最初のパラメータはファイルまたはデバイスで、2番目のパラメータはコマンドであり、残りのパラメータはコマンドに供給されます。このコマンドは、3つの標準ディスクリプタすべてが、最初のパラメータで指定されたファイルまたはデバイスにリダイレクトされて実行されます。例えば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シェルでは通常$(tty)を使用することができるので、端末上でレポートを見ることができます)。

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

のか、何で私たちは希望:

gcc -Wall -O2 report.c -o report 

今、我々はいくつかのデバイスを確認することができます。ファイルディスクリプタは同じ参照として、予想通りである - 私のマシンでは、私はファイルでこれを実行すると、

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

は、標準入力に書き込み、標準出力と標準エラーからのすべての作業を読んで言います、読み書き可能なファイル記述。

結論としては、上記で遊んだ後で、はここでは全く奇妙な動作ではないということです。です。 ファイル記述子がプロセスによって使用されている場合は、オペレーティングシステム内部のファイルの説明への参照であり、標準入力、出力、およびエラー記述子は、それぞれdupであると見なされます。

6

私は私のLinuxで、01の両方がデフォルトでプロセスの制御端末である/dev/ttyにの書き込み/読み取りで開かれているので、それがあると思います。実際にはからstdoutまで読むことも可能です。

しかし、これは、すぐに中またはアウトあなたパイプ何かのように破る:それはdoesnのよう常に、実際の根本的なターミナルのオブジェクト上で動作

#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システムの* file description *と呼ばれるカーネル構造を指します。 'dup()'は(古いものを複製することによって)新しいファイル記述子を作成します。新しいものは非常に同じ*ファイル記述*を指します。ターミナルアプリケーションでは、3つの標準ストリームはすべて擬似端末から 'dup2()'され、3つともまったく同じように振る舞います(つまり 'STDIN_FILENO'に書き込み、' STDOUT_FILENO'と 'STDERR_FILENO ')。しかし、これは擬似端末に限定されていません:[...] –

+1

[...]標準の入力と出力/エラーが同じ書き込み可能なファイル記述から発生したときにはいつでも起こります。 - 擬似端末(tty )、ファイル、さらにはソケットです。興味があれば、テストと探索に使用できるPOSIX移植可能なサンプルプログラムを提供することができます。 –

+0

@NominalAnimalあなたは答えを書く必要があります。私は、これがどうやって起こるかについての正式な情報源がなく、もちろんPOSIXであり、Linuxだけであるから、もちろん 'dup2 'を超えているから*で始まったと思います。 –

関連する問題