web-dev-qa-db-fra.com

Pourquoi stdout a-t-il besoin d'un vidage explicite lorsqu'il est redirigé vers un fichier?

Le comportement de printf() semble dépendre de l'emplacement de stdout.

  1. Si stdout est envoyé à la console, alors printf() est tamponné en ligne et vidé après la nouvelle ligne.
  2. Si stdout est redirigé vers un fichier, le tampon n'est pas vidé sauf si fflush() est appelé.
  3. De plus, si printf() est utilisé avant que stdout soit redirigé vers un fichier, les écritures ultérieures (dans le fichier) sont mises en mémoire tampon et sont vidées après une nouvelle ligne.

Quand stdout est-il mis en mémoire tampon en ligne et quand doit-on appeler fflush()?

Exemple minimal de chacun:

void RedirectStdout2File(const char* log_path) {
    int fd = open(log_path, O_RDWR|O_APPEND|O_CREAT,S_IRWXU|S_IRWXG|S_IRWXO);
    dup2(fd,STDOUT_FILENO);
    if (fd != STDOUT_FILENO) close(fd);
}

int main_1(int argc, char* argv[]) {
    /* Case 1: stdout is line-buffered when run from console */
    printf("No redirect; printed immediately\n");
    sleep(10);
}

int main_2a(int argc, char* argv[]) {
    /* Case 2a: stdout is not line-buffered when redirected to file */
    RedirectStdout2File(argv[0]);
    printf("Will not go to file!\n");
    RedirectStdout2File("/dev/null");
}
int main_2b(int argc, char* argv[]) {
    /* Case 2b: flushing stdout does send output to file */
    RedirectStdout2File(argv[0]);
    printf("Will go to file if flushed\n");
    fflush(stdout);
    RedirectStdout2File("/dev/null");
}

int main_3(int argc, char* argv[]) {
    /* Case 3: printf before redirect; printf is line-buffered after */
    printf("Before redirect\n");
    RedirectStdout2File(argv[0]);
    printf("Does go to file!\n");
    RedirectStdout2File("/dev/null");
}
26
Patrick

Le vidage de stdout est déterminé par son comportement de mise en mémoire tampon. La mise en mémoire tampon peut être définie sur trois modes: _IOFBF (mise en mémoire tampon totale: attend jusqu'à ce que fflush() soit possible), _IOLBF (mise en mémoire tampon en ligne: newline déclenche le vidage automatique) et _IONBF (écriture directe toujours utilisée). "La prise en charge de ces caractéristiques est définie par la mise en oeuvre et peut être affectée via les fonctions setbuf() et setvbuf()." [C99: 7.19.3.3]

"Au démarrage du programme, trois flux de texte sont prédéfinis et n'ont pas besoin d'être ouverts explicitement - entrée standard (pour lire une entrée conventionnelle), sortie standard (pour écrire Sortie conventionnelle) et erreur standard (pour Lors de l'ouverture initiale du fichier , le flux d'erreur standard n'est pas entièrement mis en mémoire tampon; les flux d'entrée standard et de sortie standard sont intégralement mis en mémoire tampon si et seulement si le flux peut ne pas être référencé à un appareil interactif. " [C99: 7.19.3.7]

Explication du comportement observé

Donc, ce qui se passe, c’est que l’implémentation fait quelque chose de spécifique à la plate-forme pour décider si stdout sera tamponné ou non. Dans la plupart des implémentations de libc, ce test est effectué lors de la première utilisation du flux.

  1. Le comportement n ° 1 s’explique facilement: lorsque le flux est destiné à un périphérique interactif, il est mis en tampon de ligne et la printf() est vidée automatiquement.
  2. Le cas n ° 2 est également maintenant attendu: lorsque nous redirigeons vers un fichier, le flux est entièrement mis en mémoire tampon et ne sera pas vidé sauf avec fflush(), à moins que vous n'y écriviez des gabarits de données.
  3. Enfin, nous comprenons également le cas n ° 3 pour les implémentations qui effectuent la vérification du fd sous-jacent une seule fois. Comme nous avons forcé le tampon de stdout à être initialisé dans le premier printf(), stdout a acquis le mode de mise en tampon de ligne. Lorsque nous remplaçons le fichier fd par un fichier, celui-ci est toujours mis en mémoire tampon, de sorte que les données sont vidées automatiquement.

Quelques implémentations réelles

Chaque libc a la latitude nécessaire pour interpréter ces exigences, puisque C99 ne spécifie pas ce qu'est un "périphérique interactif", pas plus que l'entrée stdio de POSIX étend cette option (au-delà de la nécessité d'ouvrir stderr en lecture).

  1. Glibc. Voir filedoalloc.c: L111 . Ici, nous utilisons stat() pour tester si le fd est un tty et définissons le mode de mise en mémoire tampon en conséquence. (Ceci est appelé depuis fileops.c.) stdout a initialement un buffer nul, et il est alloué lors de la première utilisation du flux en fonction des caractéristiques de fd 1.

  2. BSD libc. Code très similaire, mais beaucoup plus propre à suivre! Voir cette ligne dans makebuf.c

33
Nicholas Wilson

Vous combinez à tort des fonctions tamponnées et non tamponnées IO. Une telle combinaison doit être faite avec beaucoup de soin surtout lorsque le code doit être portable. (et il est mauvais d'écrire du code non transférable ...)
Il est certainement préférable d'éviter de combiner IO tamponné et non tamponné sur le même descripteur de fichier.

Entrées/sorties tamponnées: fprintf(), fopen(), fclose(), freopen()...

E/S sans tampon: write(), open(), close(), dup()...

Lorsque vous utilisez dup2() pour rediriger stdout. La fonction n'a pas connaissance du tampon qui a été rempli par fprintf(). Ainsi, lorsque dup2() ferme l'ancien descripteur 1, il ne vide pas le tampon et le contenu peut être vidé sur une autre sortie. Dans votre cas 2a, il a été envoyé à /dev/null.

La solution

Dans votre cas, il vaut mieux utiliser freopen() au lieu de dup2(). Cela résout tous vos problèmes:

  1. Il vide les tampons du flux FILE d'origine. (cas 2a)
  2. Il définit le mode de mise en mémoire tampon en fonction du fichier récemment ouvert. (cas 3)

Voici la mise en œuvre correcte de votre fonction:

void RedirectStdout2File(const char* log_path) {
    if(freopen(log_path, "a+", stdout) == NULL) err(EXIT_FAILURE, NULL);
}

Malheureusement, avec la mémoire tampon IO, vous ne pouvez pas définir directement les autorisations d'un fichier nouvellement créé. Vous devez utiliser d'autres appels pour modifier les autorisations ou vous pouvez utiliser des extensions glibc non transférables. Voir la fopen() man page .

3
pabouk

Vous ne devez pas fermer le descripteur de fichier. Supprimez donc close(fd) et fermez stdout_bak_fd si vous souhaitez que le message soit imprimé uniquement dans le fichier.

0
usain