2017-08-06 17 views
7

Tout au long de mes années en tant que programmeur C, j'ai toujours été dérouté par les descripteurs de fichiers de flux standard. Certains endroits, comme Wikipedia [1], disent:Comportement étrange exécutant des fonctions de bibliothèque sur les descripteurs de fichier STDOUT et STDIN

Dans le langage de programmation C, l'entrée standard, la sortie et les flux d'erreur sont attachés aux descripteurs de fichiers Unix existants 0, 1 et 2 respectivement.

Ceci est soutenu par unistd.h:

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

Cependant, ce code (sur tout système):

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

va imprimer Hello, World! (et un saut de ligne) à STDOUT. Ceci est étrange car le descripteur de fichier STDOUT est supposé être 1. write -ing au fichier descripteur 1 imprime également à STDOUT.

Exécution d'une ioctl sur le descripteur de fichier 0 changements d'entrée standard [2], et le descripteur de fichier 1 change de sortie standard. Cependant, l'exécution de termios functions sur 0 ou 1 change l'entrée standard [3][4].

Je suis très confus au sujet du comportement des descripteurs de fichiers 1 et 0. Est-ce que quelqu'un sait pourquoi:

  • write ing à 1 ou 0 sur sa sortie standard?
  • Exécuter ioctl sur 1 modifie la sortie standard et sur 0 modifie l'entrée standard, mais exécuter tcsetattr/tcgetattr sur 1 ou 0 fonctionne pour l'entrée standard?
+1

Pourquoi dans le monde pensez-vous qu'il écrit quoi que ce soit sur stdout? Il écrit à votre terminal. La sortie standard de votre processus peut être associée à votre terminal, mais ce n'est pas la même chose. Ne confondez pas les deux. Dans votre cas, stdin est également associé au terminal, il n'est donc pas surprenant que les écritures sur stdin apparaissent sur le terminal. –

Répondre

1

LET début de en revue quelques-uns des concepts clés:

  • Description du fichier

    Dans le noyau du système d'exploitation, chaque fichier, point final de la conduite, point final de la prise, le nœud de l'appareil ouvert, et ainsi de suite, a une description de fichier . Le noyau les utilise pour garder trace de la position dans le fichier, des drapeaux (lire, écrire, ajouter, fermer-sur-exec), des verrous d'enregistrement, et ainsi de suite.

    Les descriptions de fichiers sont internes au noyau et n'appartiennent à aucun processus en particulier (dans les implémentations typiques).
     

  • descripteur de fichier

    Du point de vue des processus, les descripteurs de fichiers sont des nombres entiers qui identifient les fichiers ouverts, des tuyaux, des prises de courant, ou dispositifs FIFOs.

    Le noyau du système d'exploitation conserve une table de descripteurs pour chaque processus. Le descripteur de fichier utilisé par le processus est simplement un index de cette table.

    Les entrées de la table des descripteurs de fichiers font référence à une description de fichier de noyau.

Chaque fois qu'un processus utilise dup() or dup2() pour dupliquer un descripteur de fichier, le noyau ne fait que dupliquer l'entrée dans la table de descripteurs de fichier pour ce processus; il ne duplique pas la description du fichier qu'il garde pour lui-même. Quand un processus forks, le processus enfant obtient sa propre table de descripteur de fichier, mais les entrées pointent toujours vers les mêmes descriptions de fichier noyau. (Ce sont essentiellement des shallow copy, toutes les entrées de la table des descripteurs de fichiers étant des références aux descriptions de fichiers.Les références sont copiées, les cibles visées restent les mêmes.)

Lorsqu'un processus envoie un descripteur de fichier à un autre processus via un Unix Message auxiliaire de socket de domaine, le noyau alloue réellement un nouveau descripteur sur le récepteur, et copie la description du fichier auquel le descripteur transféré fait référence.

Tout fonctionne très bien, même si elle est un peu déroutant que « descripteur de fichier » et « Description du fichier » sont tellement semblables.

Qu'est-ce que tout ce qui a à voir avec les effets de l'OP?

Chaque fois que de nouveaux processus sont créés, il est courant d'ouvrir le périphérique, le canal ou le socket cible et dup2() le descripteur à l'entrée standard, à la sortie standard et à l'erreur standard. Cela conduit aux trois descripteurs standard faisant référence à la même description de fichier , et donc toute opération qui est valide en utilisant un descripteur de fichier, est également valable en utilisant les autres descripteurs de fichier.

Ceci est le plus courant lors de l'exécution de programmes sur la console, car alors les trois descripteurs se réfèrent tous à la même description de fichier; et cette description de fichier décrit l'extrémité esclave d'un dispositif de caractères pseudoterminal.

Tenir compte du programme suivant, 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; 
} 

Il prend deux ou plusieurs paramètres. Le premier paramètre est un fichier ou un périphérique, et le second est la commande, avec le reste des paramètres fournis à la commande. La commande est exécutée, avec les trois descripteurs standard redirigés vers le fichier ou le périphérique nommé dans le premier paramètre. Vous pouvez compiler ce qui précède avec gcc en utilisant par exemple.

gcc -Wall -O2 run.c -o run 

Écrivons un petit utilitaire testeur, 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; 
} 

Il faut exactement un paramètre, un fichier ou un périphérique à écrire, de faire rapport si l'écriture à l'entrée standard, et la lecture de la sortie standard et le travail d'erreur. (Nous pouvons normalement utiliser $(tty) dans les shells Bash et POSIX, pour faire référence au terminal actuel, afin que le rapport soit visible sur le terminal.) Compilez celui-ci en utilisant par ex.

gcc -Wall -O2 report.c -o report 

Maintenant, nous pouvons vérifier certains appareils:

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

ou sur ce que nous voulons. Sur ma machine, quand j'exécuter sur un fichier, par exemple

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

écrit à l'entrée standard, et la lecture de la sortie standard et l'erreur standard tous les travaux - qui est comme prévu, les descripteurs de fichiers se réfèrent au même , lisible et accessible en écriture, description du fichier.

La conclusion, après avoir joué avec ce qui précède, est qu'il n'y a pas comportement étrange ici du tout. Tout se comporte exactement comme on s'y attendrait, si les descripteurs de fichier utilisés par les processus sont simplement des références aux descriptions de fichiers du système d'exploitation interne, et les descripteurs d'entrée, de sortie et d'erreur standard sont dup l'un de l'autre.

6

Je suppose que c'est parce que dans mon Linux, les deux 0 et 1 sont par défaut ouvert avec lecture/écriture à la /dev/tty qui est le terminal de contrôle du processus. Donc, en effet, il est possible de même lire de stdout.

Cependant, cette brise dès que vous tuyau quelque chose dans ou à l'extérieur:

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

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

et courir avec

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

termios fonctions fonctionnent toujours sur l'objet terminal réel sous-jacent, il doesn Peu importe si 0 ou 1 est utilisé tant qu'il est ouvert à un tty.

+2

Si on se penche sur les détails, c'est en fait un peu plus intéressant que ça, même. Chaque * descripteur de fichier * fait référence à une structure de noyau appelée * description de fichier * dans les systèmes Linux et Unixy. 'dup()' crée un nouveau descripteur de fichier (en dupliquant l'ancien); le nouveau se réfère à la même * description du fichier *. Dans une application terminale, les trois flux standards sont dup2() à partir du pseudoterminal, et tous les trois se comportent exactement de la même manière (c'est-à-dire que vous pouvez écrire dans STDIN_FILENO et lire STDOUT_FILENO et STDERR_FILENO '). Pourtant, ceci n'est pas limité aux pseudoterminaux: [...] –

+1

[...] Cela peut/va se produire quand l'entrée standard et la sortie/erreur proviennent de la même, * description de fichier inscriptible * - être un pseudoterminal (tty), fichier, ou même une socket. S'il y a un intérêt, je peux fournir un exemple de programme portable POSIX qui peut être utilisé pour tester et explorer. –

+0

@NominalAnimal vous devriez écrire une réponse alors. J'ai commencé par * je suppose * parce que je n'ai aucune source d'autorité sur la façon dont cela se passe, et laquelle est POSIX et qui est juste Linux, au-delà de dup2 bien sûr. –