GNU/Linux >> Tutoriels Linux >  >> Linux

Pourquoi la bifurcation de mon processus entraîne-t-elle la lecture infinie du fichier

Je suis surpris qu'il y ait un problème, mais cela semble être un problème sous Linux (j'ai testé sur Ubuntu 16.04 LTS exécuté sur une machine virtuelle VMWare Fusion sur mon Mac) - mais ce n'était pas un problème sur mon Mac exécutant macOS 10.13. 4 (High Sierra), et je ne m'attendrais pas non plus à ce que ce soit un problème sur d'autres variantes d'Unix.

Comme je l'ai noté dans un commentaire :

Il y a une description de fichier ouvert et un descripteur de fichier ouvert derrière chaque flux. Lorsque le processus bifurque, l'enfant a son propre ensemble de descripteurs de fichiers ouverts (et de flux de fichiers), mais chaque descripteur de fichier dans l'enfant partage la description de fichier ouvert avec le parent. SI (et c'est un gros 'si') le processus enfant fermant les descripteurs de fichier a d'abord fait l'équivalent de lseek(fd, 0, SEEK_SET) , cela positionnerait également le descripteur de fichier pour le processus parent, et cela pourrait conduire à une boucle infinie. Cependant, je n'ai jamais entendu parler d'une bibliothèque qui recherche cela; il n'y a aucune raison de le faire.

Voir POSIX open() et fork() pour plus d'informations sur les descripteurs de fichiers ouverts et les descriptions de fichiers ouverts.

Les descripteurs de fichiers ouverts sont privés pour un processus ; les descriptions de fichier ouvertes sont partagées par toutes les copies du descripteur de fichier créées par une opération initiale d'"ouverture de fichier". L'une des propriétés clés de la description du fichier ouvert est la position de recherche actuelle. Cela signifie qu'un processus enfant peut modifier la position de recherche actuelle d'un parent, car elle se trouve dans la description du fichier ouvert partagé.

neof97.c

J'ai utilisé le code suivant - une version légèrement adaptée de l'original qui se compile proprement avec des options de compilation rigoureuses :

#include "posixver.h"
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

enum { MAX = 100 };

int main(void)
{
    if (freopen("input.txt", "r", stdin) == 0)
        return 1;
    char s[MAX];
    for (int i = 0; i < 30 && fgets(s, MAX, stdin) != NULL; i++)
    {
        // Commenting out this region fixes the issue
        int status;
        pid_t pid = fork();
        if (pid == 0)
        {
            exit(0);
        }
        else
        {
            waitpid(pid, &status, 0);
        }
        // End region
        printf("%s", s);
    }
    return 0;
}

L'une des modifications limite le nombre de cycles (enfants) à seulement 30. J'ai utilisé un fichier de données avec 4 lignes de 20 lettres aléatoires plus une nouvelle ligne (84 octets au total) :

ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe

J'ai exécuté la commande sous strace sur Ubuntu :

$ strace -ff -o st-out -- neof97
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe
…
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
$

Il y avait 31 fichiers avec des noms de la forme st-out.808## où les hachages étaient des nombres à 2 chiffres. Le fichier de processus principal était assez volumineux ; les autres étaient petites, avec l'une des tailles 66, 110, 111 ou 137 :

$ cat st-out.80833
lseek(0, -63, SEEK_CUR)                 = 21
exit_group(0)                           = ?
+++ exited with 0 +++
$ cat st-out.80834
lseek(0, -42, SEEK_CUR)                 = -1 EINVAL (Invalid argument)
exit_group(0)                           = ?
+++ exited with 0 +++
$ cat st-out.80835
lseek(0, -21, SEEK_CUR)                 = 0
exit_group(0)                           = ?
+++ exited with 0 +++
$ cat st-out.80836
exit_group(0)                           = ?
+++ exited with 0 +++
$

Il se trouve que les 4 premiers enfants ont chacun présenté l'un des quatre comportements - et chaque autre groupe de 4 enfants a présenté le même schéma.

Cela montre que trois enfants sur quatre faisaient effectivement un lseek() sur l'entrée standard avant de quitter. Évidemment, j'ai maintenant vu une bibliothèque le faire. Je n'ai aucune idée de pourquoi on pense que c'est une bonne idée, mais empiriquement, c'est ce qui se passe.

neof67.c

Cette version du code, utilisant un flux de fichier séparé (et un descripteur de fichier) et fopen() au lieu de freopen() rencontre également le problème.

#include "posixver.h"
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

enum { MAX = 100 };

int main(void)
{
    FILE *fp = fopen("input.txt", "r");
    if (fp == 0)
        return 1;
    char s[MAX];
    for (int i = 0; i < 30 && fgets(s, MAX, fp) != NULL; i++)
    {
        // Commenting out this region fixes the issue
        int status;
        pid_t pid = fork();
        if (pid == 0)
        {
            exit(0);
        }
        else
        {
            waitpid(pid, &status, 0);
        }
        // End region
        printf("%s", s);
    }
    return 0;
}

Cela présente également le même comportement, sauf que le descripteur de fichier sur lequel la recherche se produit est 3 au lieu de 0 . Donc, deux de mes hypothèses sont réfutées - c'est lié à freopen() et stdin; les deux sont affichés incorrects par le deuxième code de test.

Diagnostic préliminaire

OMI, c'est un bug. Vous ne devriez pas être en mesure de rencontrer ce problème. Il s'agit probablement d'un bogue dans la bibliothèque Linux (GNU C) plutôt que dans le noyau. Il est causé par le lseek() dans les processus enfants. Ce n'est pas clair (parce que je ne suis pas allé voir le code source) ce que fait la bibliothèque ni pourquoi.

Bogue GLIBC 23151

GLIBC Bug 23151 - Un processus forké avec un fichier non fermé recherche avant de quitter et peut provoquer une boucle infinie dans les E/S parentes.

Le bogue a été créé le 2018-05-08 US/Pacific et a été fermé comme INVALIDE le 2018-05-09. La raison invoquée était :

Veuillez lire http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_05_01, en particulier ce paragraphe :

Notez qu'après un fork() , deux poignées existent là où il n'y en avait qu'une auparavant. […]

POSIX

La section complète de POSIX à laquelle il est fait référence (à part le verbiage notant que cela n'est pas couvert par la norme C) est la suivante :

2.5.1 Interaction des descripteurs de fichiers et des flux d'E/S standard

Une description de fichier ouverte est accessible via un descripteur de fichier, qui est créé à l'aide de fonctions telles que open() ou pipe() , ou via un flux créé à l'aide de fonctions telles que fopen() ou popen() . Un descripteur de fichier ou un flux est appelé un "descripteur" sur la description de fichier ouverte à laquelle il se réfère ; une description de fichier ouverte peut avoir plusieurs descripteurs.

Les descripteurs peuvent être créés ou détruits par une action explicite de l'utilisateur, sans affecter la description du fichier ouvert sous-jacent. Certaines des façons de les créer incluent fcntl() , dup() , fdopen() , fileno() , et fork() . Ils peuvent être détruits par au moins fclose() , close() , et le exec fonctions.

Un descripteur de fichier qui n'est jamais utilisé dans une opération susceptible d'affecter le décalage du fichier (par exemple, read() , write() , ou lseek() ) n'est pas considéré comme un handle pour cette discussion, mais pourrait en donner lieu (par exemple, à la suite de fdopen() , dup() , ou fork() ). Cette exception n'inclut pas le descripteur de fichier sous-jacent à un flux, qu'il soit créé avec fopen() ou fdopen() , tant qu'il n'est pas utilisé directement par l'application pour affecter le décalage du fichier. Le read() et write() les fonctions affectent implicitement le décalage du fichier ; lseek() l'affecte explicitement.

Le résultat des appels de fonction impliquant un handle (le "handle actif") est défini ailleurs dans ce volume de POSIX.1-2017, mais si deux handles ou plus sont utilisés, et que l'un d'entre eux est un flux, l'application doit s'assurer que leurs actions sont coordonnées comme décrit ci-dessous. Si cela n'est pas fait, le résultat est indéfini.

Un descripteur qui est un flux est considéré comme fermé lorsqu'un fclose() , ou freopen() avec un nom de fichier non complet, est exécuté dessus (pour freopen() avec un nom de fichier nul, il est défini par l'implémentation si un nouveau handle est créé ou si celui existant est réutilisé), ou lorsque le processus propriétaire de ce flux se termine par exit() , abort() , ou à cause d'un signal. Un descripteur de fichier est fermé par close() , _exit() , ou le exec() fonctionne lorsque FD_CLOEXEC est défini sur ce descripteur de fichier.

[sic] Utiliser 'non-full' est probablement une faute de frappe pour 'non-null'.

Pour qu'une poignée devienne la poignée active, l'application doit s'assurer que les actions ci-dessous sont effectuées entre la dernière utilisation de la poignée (la poignée active actuelle) et la première utilisation de la deuxième poignée (la future poignée active). La deuxième poignée devient alors la poignée active. Toute activité de l'application affectant le décalage de fichier sur le premier descripteur doit être suspendue jusqu'à ce qu'il redevienne le descripteur de fichier actif. (Si une fonction de flux a comme fonction sous-jacente une fonction qui affecte le décalage de fichier, la fonction de flux doit être considérée comme affectant le décalage de fichier.)

Les descripteurs n'ont pas besoin d'être dans le même processus pour que ces règles s'appliquent.

Notez qu'après un fork() , deux poignées existent là où il n'y en avait qu'une auparavant. L'application doit s'assurer que, si les deux descripteurs sont accessibles, ils sont tous les deux dans un état où l'autre pourrait devenir le premier descripteur actif. L'application doit préparer un fork() exactement comme s'il s'agissait d'un changement de poignée active. (Si la seule action effectuée par l'un des processus est l'une des exec() fonctions ou _exit() (pas exit() ), le handle n'est jamais accessible dans ce processus.)

Pour la première poignée, la première condition applicable ci-dessous s'applique. Une fois les actions requises ci-dessous effectuées, si le descripteur est toujours ouvert, l'application peut le fermer.

  • S'il s'agit d'un descripteur de fichier, aucune action n'est requise.

  • Si la seule action supplémentaire à effectuer sur un descripteur de ce descripteur de fichier ouvert est de le fermer, aucune action n'est nécessaire.

  • S'il s'agit d'un flux non tamponné, aucune action n'est nécessaire.

  • S'il s'agit d'un flux dont la ligne est mise en mémoire tampon et que le dernier octet écrit dans le flux était un <newline> (c'est-à-dire, comme si un putc('\n') était l'opération la plus récente sur ce flux), aucune action n'est nécessaire.

  • S'il s'agit d'un flux ouvert en écriture ou en ajout (mais pas également ouvert en lecture), l'application doit soit effectuer un fflush() , ou le flux sera fermé.

  • Si le flux est ouvert en lecture et qu'il se trouve en fin de fichier (feof() est vrai), aucune action n'est nécessaire.

  • Si le flux est ouvert avec un mode permettant la lecture et que la description sous-jacente du fichier ouvert fait référence à un appareil capable de rechercher, l'application doit soit effectuer un fflush() , ou le flux sera fermé.

Pour la seconde poignée :

  • Si un handle actif précédent a été utilisé par une fonction qui a explicitement changé le décalage du fichier, sauf comme requis ci-dessus pour le premier handle, l'application doit effectuer un lseek() ou fseek() (selon le type de poignée) à un emplacement approprié.

Si le descripteur actif cesse d'être accessible avant que les exigences sur le premier descripteur, ci-dessus, aient été satisfaites, l'état de la description de fichier ouvert devient indéfini. Cela peut se produire lors de fonctions telles qu'un fork() ou _exit() .

Le exec() rendent inaccessibles tous les flux qui sont ouverts au moment où ils sont appelés, indépendamment des flux ou des descripteurs de fichiers qui peuvent être disponibles pour la nouvelle image de processus.

Lorsque ces règles sont respectées, quelle que soit la séquence des descripteurs utilisés, les implémentations doivent garantir qu'une application, même composée de plusieurs processus, donne des résultats corrects :aucune donnée ne doit être perdue ou dupliquée lors de l'écriture, et toutes les données doivent être écrites en commande, sauf si demandé par cherche. Il est défini par la mise en œuvre si, et dans quelles conditions, toutes les entrées sont vues exactement une fois.

Chaque fonction qui opère sur un flux est dite avoir zéro ou plusieurs "fonctions sous-jacentes". Cela signifie que la fonction de flux partage certains traits avec les fonctions sous-jacentes, mais n'exige pas qu'il y ait une relation entre les implémentations de la fonction de flux et ses fonctions sous-jacentes.

Exégèse

C'est difficile à lire ! Si vous n'êtes pas clair sur la distinction entre le descripteur de fichier ouvert et la description de fichier ouvert, lisez la spécification de open() et fork() (et dup() ou dup2() ). Les définitions de descripteur de fichier et de description de fichier ouvert sont également pertinentes, si elles sont concises.

Dans le contexte du code de cette question (et également pour les processus enfants indésirables créés lors de la lecture de fichiers), nous avons un descripteur de flux de fichiers ouvert en lecture uniquement qui n'a pas encore rencontré EOF (donc feof() ne renverrait pas vrai, même si la position de lecture est à la fin du fichier).

L'une des parties cruciales de la spécification est :L'application doit se préparer à un fork() exactement comme s'il s'agissait d'un changement de pseudo actif.

Cela signifie que les étapes décrites pour le "premier descripteur de fichier" sont pertinentes et qu'en les parcourant, la première condition applicable est la dernière :

  • Si le flux est ouvert avec un mode qui permet la lecture et que la description du fichier ouvert sous-jacent fait référence à un appareil capable de rechercher, l'application doit soit effectuer un fflush() , ou le flux sera fermé.

Si vous regardez la définition de fflush() , vous trouvez :

Si diffuser pointe vers un flux de sortie ou un flux de mise à jour dans lequel l'opération la plus récente n'a pas été entrée, fflush() doit entraîner l'écriture dans le fichier de toutes les données non écrites pour ce flux, [CX] ⌦ et la dernière modification des données et les horodatages du dernier changement d'état du fichier du fichier sous-jacent doivent être marqués pour mise à jour.

Pour un flux ouvert en lecture avec une description de fichier sous-jacente, si le fichier n'est pas déjà à EOF et que le fichier est capable de rechercher, le décalage de fichier de la description de fichier ouverte sous-jacente doit être défini sur la position de fichier du flux, et tous les caractères repoussés dans le flux par ungetc() ou ungetwc() qui n'ont pas été lus par la suite à partir du flux doivent être rejetés (sans modifier davantage le décalage du fichier). ⌫

Ce qui se passe si vous appliquez fflush() n'est pas exactement clair. à un flux d'entrée associé à un fichier non consultable, mais ce n'est pas notre préoccupation immédiate. Cependant, si vous écrivez du code de bibliothèque générique, vous devrez peut-être savoir si le descripteur de fichier sous-jacent est recherchable avant de faire un fflush() sur le ruisseau. Sinon, utilisez fflush(NULL) pour que le système fasse tout ce qui est nécessaire pour tous les flux d'E/S, en notant que cela perdra tous les caractères repoussés (via ungetc() etc.).

Le lseek() opérations indiquées dans le strace la sortie semble implémenter le fflush() sémantique associant l'offset de fichier de la description de fichier ouvert à la position de fichier du flux.

Donc, pour le code de cette question, il semble que fflush(stdin) est nécessaire avant le fork() pour assurer la cohérence. Ne pas le faire conduit à un comportement indéfini ('si cela n'est pas fait, le résultat n'est pas défini') - comme une boucle indéfinie.


L'appel exit() ferme tous les descripteurs de fichiers ouverts. Après le fork, l'enfant et le parent ont des copies identiques de la pile d'exécution, y compris le pointeur FileHandle. Lorsque l'enfant quitte, il ferme le fichier et réinitialise le pointeur.

  int main(){
        freopen("input.txt", "r", stdin);
        char s[MAX];
        prompt(s);
        int i = 0;
        char* ret = fgets(s, MAX, stdin);
        while (ret != NULL) {
            //Commenting out this region fixes the issue
            int status;
            pid_t pid = fork();   // At this point both processes has a copy of the filehandle
            if (pid == 0) {
                exit(0);          // At this point the child closes the filehandle
            } else {
                waitpid(pid, &status, 0);
            }
            //End region
            printf("%s", s);
            ret = fgets(s, MAX, stdin);
        }
    }

Linux
  1. Pourquoi le script Bash ne reconnaît-il pas les alias ?

  2. Pourquoi l'utilisateur racine a-t-il besoin d'une autorisation Sudo ?

  3. Tail lit-il tout le fichier ?

  4. Que signifie la sortie de Ps ?

  5. Pourquoi le descripteur de fichier est-il ouvert et lu une seule fois ?

Qu'est-ce que la table des processus Linux ? En quoi cela consiste?

Pourquoi select est-il utilisé sous Linux

Pourquoi l'arrêt net rpc échoue-t-il avec les bonnes informations d'identification ?

À quoi sert l'autorisation d'exécution ?

Pourquoi wget'ing une image me donne-t-il un fichier, pas une image ?

Pourquoi le répertoire racine est-il désigné par un signe / ?