Comment les pipelines Unix sont implémentés


Cet article décrit l'implémentation de pipelines dans le noyau Unix. J'ai été quelque peu déçu par un récent article intitulé " Comment fonctionnent les pipelines sous Unix?" "Il ne s'agissait pas du périphérique interne. Je me suis intéressé et je me suis enfoui dans les anciennes sources pour trouver la réponse.

De quoi parle-t-on?


Les pipelines - "probablement l'invention la plus importante sur Unix" - est la caractéristique déterminante de la philosophie sous-jacente d'Unix consistant à combiner de petits programmes, ainsi que la ligne de commande familière:

$ echo hello | wc -c
6

Cette fonctionnalité dépend de l'appel système fourni par le noyau pipe, qui est décrit dans les pages de documentation de pipe (7) et pipe (2) :

Les convoyeurs fournissent un canal de communication interprocessus unidirectionnel. Le pipeline a une entrée (fin d'écriture) et une sortie (fin de lecture). Les données écrites à l'entrée du pipeline peuvent être lues.

Le pipeline est créé à l'aide d'un appel pipe(2)qui renvoie deux descripteurs de fichier: l'un se réfère à l'entrée du pipeline, le second à la sortie.

Les résultats de trace de la commande ci-dessus illustrent la création d'un pipeline et le flux de données qui le traverse d'un processus à un autre:

$ strace -qf -e execve,pipe,dup2,read,write \
    sh -c 'echo hello | wc -c'

execve("/bin/sh", ["sh", "-c", "echo hello | wc -c"], …)
pipe([3, 4])                            = 0
[pid 2604795] dup2(4, 1)                = 1
[pid 2604795] write(1, "hello\n", 6)    = 6
[pid 2604796] dup2(3, 0)                = 0
[pid 2604796] execve("/usr/bin/wc", ["wc", "-c"], …)
[pid 2604796] read(0, "hello\n", 16384) = 6
[pid 2604796] write(1, "6\n", 2)        = 2

Le processus parent appelle pipe()pour obtenir les descripteurs de fichiers joints. Un processus enfant écrit dans un descripteur et un autre processus lit les mêmes données dans un autre descripteur. Le wrapper utilisant dup2 «renomme» les descripteurs 3 et 4 pour faire correspondre stdin et stdout.

Sans pipelines, le shell devrait écrire le résultat d'un processus dans un fichier et le transférer dans un autre processus afin qu'il lise les données du fichier. En conséquence, nous dépenserions plus de ressources et d'espace disque. Cependant, les pipelines sont bons non seulement parce qu'ils évitent l'utilisation de fichiers temporaires:

, read(2) , . , write(2) , .

Comme l'exigence POSIX, il s'agit d'une propriété importante: l'écriture dans le pipeline jusqu'à PIPE_BUFoctets (au moins 512) doit être atomique afin que les processus puissent communiquer entre eux via le pipeline de la même manière que les fichiers ordinaires (qui ne fournissent pas de telles garanties).

Lors de l'utilisation d'un fichier normal, un processus peut y écrire toutes ses données de sortie et les transférer vers un autre processus. Ou les processus peuvent fonctionner en mode de parallélisation dure, en utilisant un mécanisme de signalisation externe (tel qu'un sémaphore) pour s'informer mutuellement de la fin de l'écriture ou de la lecture. Les convoyeurs nous épargnent tous ces ennuis.

Que cherchons-nous?


Je vais l'expliquer sur mes doigts pour vous permettre d'imaginer plus facilement comment le convoyeur peut fonctionner. Vous devrez allouer un tampon et un état en mémoire. Vous aurez besoin de fonctions pour ajouter et supprimer des données du tampon. Il faudra des moyens pour appeler des fonctions pendant les opérations de lecture et d'écriture sur les descripteurs de fichiers. Et des verrous sont nécessaires pour implémenter le comportement spécial décrit ci-dessus.

Nous sommes maintenant prêts à interroger dans la lumière vive des lampes le code source du noyau pour confirmer ou réfuter notre vague modèle mental. Mais soyez toujours préparé à l'inattendu.

Où cherchons-nous?


Je ne sais pas où se trouve ma copie du célèbre livre « Lions book » avec le code source Unix 6, mais grâce à The Unix Heritage Society, vous pouvez rechercher en ligne des versions encore plus anciennes d'Unix dans le code source .

Se promener dans les archives du TUHS revient à visiter un musée. Nous pouvons jeter un œil à notre histoire commune, et je respecte les nombreuses années d'efforts pour récupérer tous ces matériaux petit à petit à partir de vieilles cassettes et impressions. Et je suis parfaitement conscient de ces fragments qui manquent encore.

Après avoir satisfait notre curiosité concernant l'histoire ancienne des convoyeurs, nous pouvons comparer les noyaux modernes.

Soit dit en passant, pipeest un numéro d'appel système 42 dans le tableau sysent[]. Coïncidence?

Cerneaux Unix traditionnels (1970–1974)


Je n'ai trouvé aucune trace pipe(2)ni dans PDP-7 Unix (janvier 1970), ni dans la première édition d'Unix (novembre 1971), ni dans le code source incomplet de la deuxième édition (juin 1972).

TUHS affirme que la troisième édition d'Unix (février 1973) était la première version avec pipelines:

La troisième édition d'Unix était la dernière version avec un noyau écrit en langage assembleur, mais la première version avec des pipelines. En 1973, des travaux ont été effectués pour améliorer la troisième édition, le noyau a été réécrit en C, et la quatrième édition d'Unix est apparue.

Un des lecteurs a trouvé une analyse d'un document dans lequel Doug McIlroy a proposé l'idée de «connecter des programmes par le principe d'un tuyau d'arrosage».


Dans le livre de Brian Kernighan « Unix: une histoire et un mémoire », dans l'histoire de l'apparition des convoyeurs, ce document est également mentionné: «... il a été accroché au mur dans mon bureau des Bell Labs pendant 30 ans.» Voici une interview avec McIlroy , et une autre histoire du travail de McIlroy écrite en 2014 :

Unix, , , , - , , . , . , , , . ? «» , , -, : « !».

. , , ( ), . . . API , .

Malheureusement, le code source du noyau pour la troisième édition d'Unix est perdu. Et bien que nous ayons le code source du noyau de la quatrième édition écrit en C , sorti en novembre 1973, il a été publié quelques mois avant la sortie officielle et ne contient pas d'implémentations de pipeline. Il est dommage que le code source de la légendaire fonction Unix soit perdu, peut-être pour toujours.

Nous avons le texte de la documentation pipe(2)des deux versions, vous pouvez donc commencer par rechercher la troisième édition de la documentation (pour certains mots, souligné «manuellement», une chaîne de littéraux ^ H, suivis de soulignés!). Ce proto est pipe(2)écrit en assembleur et ne renvoie qu'un seul descripteur de fichier, mais fournit déjà la fonctionnalité de base attendue:

L'appel du système de tuyaux crée un mécanisme d'entrée de sortie appelé pipeline. Le descripteur de fichier renvoyé peut être utilisé pour les opérations de lecture et d'écriture. Lorsque quelque chose est écrit dans le pipeline, jusqu'à 504 octets de données sont mis en mémoire tampon, après quoi le processus d'écriture est interrompu. Lors de la lecture à partir d'un pipeline, des données tamponnées sont prises.

L'année suivante, le noyau a été réécrit en C, et pipe (2) dans la quatrième édition a retrouvé son look moderne avec le prototype " pipe(fildes)":

pipe , . . - , , r1 (. fildes[1]), 4096 , . , r0 (. fildes[0]), .

, ( ) ( fork) read write.

Le shell a une syntaxe pour définir un tableau linéaire de processus connectés via un pipeline.

Les appels de lecture à partir d'un pipeline vide (ne contenant pas de données en mémoire tampon) qui n'a qu'une seule extrémité (tous les descripteurs de fichiers d'écriture sont fermés) renvoient la «fin de fichier». L'enregistrement des appels dans une situation similaire est ignoré.

La première implémentation de pipeline encore en vigueur remonte à la cinquième édition d'Unix (juin 1974), mais elle est presque identique à celle qui figurait dans la prochaine version. Seuls les commentaires ont été ajoutés, de sorte que la cinquième édition peut être ignorée.

Sixième édition d'Unix (1975)


Nous commençons à lire le code source Unix de la sixième édition (mai 1975). En grande partie grâce aux Lions, il est beaucoup plus facile de le trouver que le code source des versions antérieures:

Depuis de nombreuses années, les Lions sont le seul document de base Unix disponible en dehors des murs des Bell Labs. Bien que la licence de la sixième édition permette aux enseignants d'utiliser son code source, la licence de la septième édition excluait cette possibilité, de sorte que le livre a été distribué sous forme de copies illégales dactylographiées.

Aujourd'hui, vous pouvez acheter une réimpression du livre, sur la couverture de laquelle les élèves sont présentés au photocopieur. Et grâce à Warren Tumi (qui a lancé le projet TUHS), vous pouvez télécharger le fichier PDF avec le code source de la sixième édition . Je veux vous donner une idée de l'effort nécessaire pour créer le fichier:

15 , Lions, . TUHS , . 1988- 9 , PDP11. , , /usr/src/, 1979- , . PWB, .

. , , += =+. - , - , .

Et aujourd'hui, nous pouvons lire en ligne sur TUHS le code source de la sixième édition des archives, à laquelle Dennis Ritchie a eu un coup de main .

Soit dit en passant, à première vue, la principale caractéristique du code C avant la période de Kernigan et Richie est sa brièveté . Pas si souvent, je parviens à intégrer des extraits de code sans modification approfondie pour s'adapter à une zone d'affichage relativement étroite sur mon site.

Au début de /usr/sys/ken/pipe.c il y a un commentaire explicatif (et oui, il y a aussi / usr / sys / dmr ):

/*
 * Max allowable buffering per pipe.
 * This is also the max size of the
 * file created to implement the pipe.
 * If this size is bigger than 4096,
 * pipes will be implemented in LARG
 * files, which is probably not good.
 */
#define    PIPSIZ    4096

La taille du tampon n'a pas changé depuis la quatrième édition. Mais ici, sans aucune documentation publique, nous voyons qu'une fois que les pipelines ont utilisé des fichiers comme stockage de sauvegarde!

Quant aux fichiers LARG, ils correspondent au drapeau d'inode LARG , qui est utilisé par «l'algorithme d'adressage élevé» pour traiter les blocs indirects afin de prendre en charge des systèmes de fichiers plus importants. Comme Ken a dit qu'il valait mieux ne pas les utiliser, je me ferai un plaisir de le croire sur parole.

Voici le véritable appel système pipe:

/*
 * The sys-pipe entry.
 * Allocate an inode on the root device.
 * Allocate 2 file structures.
 * Put it all together with flags.
 */
pipe()
{
    register *ip, *rf, *wf;
    int r;

    ip = ialloc(rootdev);
    if(ip == NULL)
        return;
    rf = falloc();
    if(rf == NULL) {
        iput(ip);
        return;
    }
    r = u.u_ar0[R0];
    wf = falloc();
    if(wf == NULL) {
        rf->f_count = 0;
        u.u_ofile[r] = NULL;
        iput(ip);
        return;
    }
    u.u_ar0[R1] = u.u_ar0[R0]; /* wf's fd */
    u.u_ar0[R0] = r;           /* rf's fd */
    wf->f_flag = FWRITE|FPIPE;
    wf->f_inode = ip;
    rf->f_flag = FREAD|FPIPE;
    rf->f_inode = ip;
    ip->i_count = 2;
    ip->i_flag = IACC|IUPD;
    ip->i_mode = IALLOC;
}

Le commentaire décrit clairement ce qui se passe ici. Mais comprendre le code n'est pas facile, en partie à cause de la manière avec l'aide d' un « utilisateur struct u » et enregistre R0et R1transfère les paramètres des appels système et les valeurs de retour.

Essayons d'utiliser ialloc () pour placer l' inode (inode ) sur le disque et d'utiliser falloc () pour placer deux fichiers en mémoire . Si tout se passe bien, nous allons définir des indicateurs pour définir ces fichiers comme les deux extrémités du pipeline, les pointer vers le même inode (dont le nombre de références sera 2) et marquer l'inode comme modifié et utilisé. Faites attention aux appels à iput ()dans les chemins d'erreur pour diminuer le nombre de références dans le nouvel inode.

pipe()doit traverser R0et R1renvoyer les numéros de descripteur de fichier pour la lecture et l'écriture. falloc()renvoie un pointeur sur la structure du fichier, mais aussi «retourne» via le u.u_ar0[R0]descripteur de fichier. Autrement dit, le code enregistre dans un rdescripteur de fichier pour la lecture et attribue un descripteur pour l'écriture directement u.u_ar0[R0]après le deuxième appel falloc().

L'indicateur FPIPEque nous avons défini lors de la création du pipeline contrôle le comportement de la fonction rdwr () dans sys2.c , qui appelle des routines d'E / S d'E / S spécifiques:

/*
 * common code for read and write calls:
 * check permissions, set base, count, and offset,
 * and switch out to readi, writei, or pipe code.
 */
rdwr(mode)
{
    register *fp, m;

    m = mode;
    fp = getf(u.u_ar0[R0]);
        /* … */

    if(fp->f_flag&FPIPE) {
        if(m==FREAD)
            readp(fp); else
            writep(fp);
    }
        /* … */
}

Ensuite, la fonction readp()dans pipe.clit les données du pipeline. Mais il est préférable de commencer par suivre la mise en œuvre writep(). Encore une fois, le code est devenu plus compliqué en raison des spécificités de l'accord de transfert d'arguments, mais certains détails peuvent être omis.

writep(fp)
{
    register *rp, *ip, c;

    rp = fp;
    ip = rp->f_inode;
    c = u.u_count;

loop:
    /* If all done, return. */

    plock(ip);
    if(c == 0) {
        prele(ip);
        u.u_count = 0;
        return;
    }

    /*
     * If there are not both read and write sides of the
     * pipe active, return error and signal too.
     */

    if(ip->i_count < 2) {
        prele(ip);
        u.u_error = EPIPE;
        psignal(u.u_procp, SIGPIPE);
        return;
    }

    /*
     * If the pipe is full, wait for reads to deplete
     * and truncate it.
     */

    if(ip->i_size1 == PIPSIZ) {
        ip->i_mode =| IWRITE;
        prele(ip);
        sleep(ip+1, PPIPE);
        goto loop;
    }

    /* Write what is possible and loop back. */

    u.u_offset[0] = 0;
    u.u_offset[1] = ip->i_size1;
    u.u_count = min(c, PIPSIZ-u.u_offset[1]);
    c =- u.u_count;
    writei(ip);
    prele(ip);
    if(ip->i_mode&IREAD) {
        ip->i_mode =& ~IREAD;
        wakeup(ip+2);
    }
    goto loop;
}

Nous voulons écrire des octets à l'entrée du pipeline u.u_count. Nous devons d'abord verrouiller l'inode (voir ci-dessous plock/ prele).

Vérifiez ensuite le nombre de références d'inode. Alors que les deux extrémités du pipeline restent ouvertes, le compteur doit être 2. Nous gardons un lien (out rp->f_inode), donc si le compteur est inférieur à 2, cela devrait signifier que le processus de lecture a fermé son extrémité du pipeline. En d'autres termes, nous essayons d'écrire dans un pipeline fermé, et c'est une erreur. Le code d'erreur EPIPEet le signal SIGPIPEsont apparus pour la première fois dans la sixième édition d'Unix.

Mais même si le convoyeur est ouvert, il peut être plein. Dans ce cas, nous retirons le verrou et nous nous endormons dans l'espoir qu'un autre processus lira dans le pipeline et libérera suffisamment d'espace à l'intérieur. Une fois réveillé, nous revenons au début, encore une fois nous bloquons la serrure et commençons un nouveau cycle d'enregistrement.

S'il y a suffisamment d'espace libre dans le pipeline, nous y écrivons des données en utilisant writei () . Le paramètre i_size1inode (avec un pipeline vide peut être 0) indique la fin des données qu'il contient déjà. S'il y a suffisamment d'espace d'enregistrement, nous pouvons remplir le convoyeur de i_size1àPIPESIZ. Ensuite, nous supprimons le verrou et essayons de réveiller tout processus qui attend la possibilité de lire à partir du pipeline. Nous remontons au début pour voir si nous avons réussi à écrire autant d'octets que nécessaire. S'il échoue, nous commençons alors un nouveau cycle d'enregistrement.

En règle générale i_mode, un paramètre inode est utilisé pour stocker les autorisations r, wet x. Mais dans le cas des pipelines, nous signalons qu'un processus attend d'écrire ou de lire en utilisant respectivement les bits IREADet IWRITE. Un processus définit un indicateur et appelle sleep(), et il est prévu qu'un autre processus appelle à l'avenir wakeup().

La vraie magie se produit dans sleep()et wakeup(). Ils sont implémentés dans slp.c, la source du célèbre commentaire, "Vous n'êtes pas censé comprendre cela." Heureusement, nous ne sommes pas obligés de comprendre le code, il suffit de voir quelques commentaires:

/*
 * Give up the processor till a wakeup occurs
 * on chan, at which time the process
 * enters the scheduling queue at priority pri.
 * The most important effect of pri is that when
 * pri<0 a signal cannot disturb the sleep;
 * if pri>=0 signals will be processed.
 * Callers of this routine must be prepared for
 * premature return, and check that the reason for
 * sleeping has gone away.
 */
sleep(chan, pri) /* … */

/*
 * Wake up all processes sleeping on chan.
 */
wakeup(chan) /* … */

Un processus qui invoque sleep()pour un canal particulier peut être ultérieurement réveillé par un autre processus qui invoquera wakeup()pour le même canal. writep()et readp()coordonner leurs actions par le biais de ces appels jumelés. Veuillez noter que pipe.cdonne toujours la priorité PPIPElors de l'appel sleep(), donc tout le monde sleep()peut interrompre le signal.

Maintenant, nous avons tout pour comprendre la fonction readp():

readp(fp)
int *fp;
{
    register *rp, *ip;

    rp = fp;
    ip = rp->f_inode;

loop:
    /* Very conservative locking. */

    plock(ip);

    /*
     * If the head (read) has caught up with
     * the tail (write), reset both to 0.
     */

    if(rp->f_offset[1] == ip->i_size1) {
        if(rp->f_offset[1] != 0) {
            rp->f_offset[1] = 0;
            ip->i_size1 = 0;
            if(ip->i_mode&IWRITE) {
                ip->i_mode =& ~IWRITE;
                wakeup(ip+1);
            }
        }

        /*
         * If there are not both reader and
         * writer active, return without
         * satisfying read.
         */

        prele(ip);
        if(ip->i_count < 2)
            return;
        ip->i_mode =| IREAD;
        sleep(ip+2, PPIPE);
        goto loop;
    }

    /* Read and return */

    u.u_offset[0] = 0;
    u.u_offset[1] = rp->f_offset[1];
    readi(ip);
    rp->f_offset[1] = u.u_offset[1];
    prele(ip);
}

Vous pouvez trouver plus facile de lire cette fonction de bas en haut. La branche «lecture et retour» est généralement utilisée lorsqu'il y a des données dans le pipeline. Dans ce cas, nous utilisons readi () pour lire autant de données que possible à partir de la f_offsetlecture en cours, puis mettons à jour la valeur du décalage correspondant.

Lors des lectures suivantes, le pipeline sera vide si le décalage de lecture a atteint la valeur de i_size1l'inode. Nous remettons la position à 0 et essayons de réveiller tout processus qu'il souhaite écrire dans le pipeline. Nous savons que lorsque le convoyeur est plein, il writep()s'endort ip+1. Et maintenant que le pipeline est vide, on peut le réveiller pour qu'il reprenne son cycle d'enregistrement.

S'il n'y a rien à lire, il readp()peut mettre un drapeau IREADet s'endormirip+2. Nous savons ce qui va le réveiller writep()quand il écrit des données dans le pipeline.

Les commentaires sur readi () et writei () aideront à comprendre qu'au lieu de passer des paramètres par " u", nous pouvons les traiter comme des fonctions d'E / S habituelles qui prennent un fichier, une position, un tampon en mémoire et comptent le nombre d'octets à lire ou à écrire .

/*
 * Read the file corresponding to
 * the inode pointed at by the argument.
 * The actual read arguments are found
 * in the variables:
 *    u_base        core address for destination
 *    u_offset    byte offset in file
 *    u_count        number of bytes to read
 *    u_segflg    read to kernel/user
 */
readi(aip)
struct inode *aip;
/* … */

/*
 * Write the file corresponding to
 * the inode pointed at by the argument.
 * The actual write arguments are found
 * in the variables:
 *    u_base        core address for source
 *    u_offset    byte offset in file
 *    u_count        number of bytes to write
 *    u_segflg    write to kernel/user
 */
writei(aip)
struct inode *aip;
/* … */

Quant au verrou "conservateur", alors readp()et writep()bloquer l'inode aussi longtemps qu'ils terminent un travail ou n'obtiennent pas le résultat (c'est-à-dire la cause wakeup). plock()et ils prele()fonctionnent simplement: en utilisant un ensemble différent d'appels sleepet wakeupnous permettent de réveiller tout processus qui a besoin d'un verrou que nous venons de supprimer:

/*
 * Lock a pipe.
 * If its already locked, set the WANT bit and sleep.
 */
plock(ip)
int *ip;
{
    register *rp;

    rp = ip;
    while(rp->i_flag&ILOCK) {
        rp->i_flag =| IWANT;
        sleep(rp, PPIPE);
    }
    rp->i_flag =| ILOCK;
}

/*
 * Unlock a pipe.
 * If WANT bit is on, wakeup.
 * This routine is also used to unlock inodes in general.
 */
prele(ip)
int *ip;
{
    register *rp;

    rp = ip;
    rp->i_flag =& ~ILOCK;
    if(rp->i_flag&IWANT) {
        rp->i_flag =& ~IWANT;
        wakeup(rp);
    }
}

Au début, je ne pouvais pas comprendre pourquoi il readp()n'avait pas appelé prele(ip)avant l'appel wakeup(ip+1). La première chose qui writep()provoque dans sa boucle est qu'elle plock(ip)conduit à un blocage si readp()elle n'a pas encore supprimé son bloc, donc le code doit en quelque sorte fonctionner correctement. Si vous le regardez wakeup(), il devient clair qu'il ne marque que le processus de sommeil comme prêt à être exécuté, de sorte qu'à l'avenir, il le sched()lancera vraiment. Ainsi, il readp()provoque wakeup(), déverrouille, définit IREADet appelle sleep(ip+2)- tout cela avant de writep()reprendre le cycle.

Ceci complète la description des convoyeurs dans la sixième édition. Code simple, conséquences étendues.

Septième édition d'Unix(Janvier 1979) a été la nouvelle version majeure (quatre ans plus tard), dans laquelle de nombreuses nouvelles applications et propriétés du noyau sont apparues. En outre, il y a eu des changements importants en ce qui concerne l'utilisation de la fonte de type, union'ov et des pointeurs typés pour les structures. Cependant, le code du pipeline n'a pas beaucoup changé. Nous pouvons sauter cette édition.

Xv6, un simple noyau en forme Unix


La sixième édition d'Unix a influencé la création du noyau Xv6 , mais il est écrit en C moderne pour fonctionner sur des processeurs x86. Le code est facile à lire, il est clair. De plus, contrairement aux sources Unix avec TUHS, vous pouvez le compiler, le modifier et l'exécuter sur autre chose que PDP 11/70. Par conséquent, ce noyau est largement utilisé dans les universités comme matériel pédagogique sur les systèmes d'exploitation. Les sources sont sur Github .

Le code contient une implémentation claire et bien pensée de pipe.c , sauvegardée par un tampon en mémoire au lieu d'un inode sur le disque. Ici, je donne seulement la définition de «pipeline structurel» et la fonction pipealloc():

#define PIPESIZE 512

struct pipe {
  struct spinlock lock;
  char data[PIPESIZE];
  uint nread;     // number of bytes read
  uint nwrite;    // number of bytes written
  int readopen;   // read fd is still open
  int writeopen;  // write fd is still open
};

int
pipealloc(struct file **f0, struct file **f1)
{
  struct pipe *p;

  p = 0;
  *f0 = *f1 = 0;
  if((*f0 = filealloc()) == 0 || (*f1 = filealloc()) == 0)
    goto bad;
  if((p = (struct pipe*)kalloc()) == 0)
    goto bad;
  p->readopen = 1;
  p->writeopen = 1;
  p->nwrite = 0;
  p->nread = 0;
  initlock(&p->lock, "pipe");
  (*f0)->type = FD_PIPE;
  (*f0)->readable = 1;
  (*f0)->writable = 0;
  (*f0)->pipe = p;
  (*f1)->type = FD_PIPE;
  (*f1)->readable = 0;
  (*f1)->writable = 1;
  (*f1)->pipe = p;
  return 0;

 bad:
  if(p)
    kfree((char*)p);
  if(*f0)
    fileclose(*f0);
  if(*f1)
    fileclose(*f1);
  return -1;
}

pipealloc()définit l'état du reste de l'implémentation, qui comprend les fonctions piperead(), pipewrite()et pipeclose(). L'appel système réel sys_pipeest un wrapper implémenté dans sysfile.c . Je recommande de lire tout son code. La complexité est au niveau source de la sixième édition, mais elle est beaucoup plus simple et agréable à lire.

Linux 0,01


Vous pouvez trouver le code source pour Linux 0.01. Il sera instructif d'étudier la mise en œuvre des pipelines dans son fs/ pipe.c. Ici, l'inode est utilisé pour représenter le pipeline, mais le pipeline lui-même est écrit en C. moderne. Si vous avez traversé le code de la sixième édition, vous ne rencontrerez pas de difficultés. Voici à quoi ressemble la fonction write_pipe():

int write_pipe(struct m_inode * inode, char * buf, int count)
{
    char * b=buf;

    wake_up(&inode->i_wait);
    if (inode->i_count != 2) { /* no readers */
        current->signal |= (1<<(SIGPIPE-1));
        return -1;
    }
    while (count-->0) {
        while (PIPE_FULL(*inode)) {
            wake_up(&inode->i_wait);
            if (inode->i_count != 2) {
                current->signal |= (1<<(SIGPIPE-1));
                return b-buf;
            }
            sleep_on(&inode->i_wait);
        }
        ((char *)inode->i_size)[PIPE_HEAD(*inode)] =
            get_fs_byte(b++);
        INC_PIPE( PIPE_HEAD(*inode) );
        wake_up(&inode->i_wait);
    }
    wake_up(&inode->i_wait);
    return b-buf;
}

Même sans regarder les définitions des structures, vous pouvez comprendre comment le compteur de référence d'inode est utilisé pour vérifier si l'opération d'écriture aboutit SIGPIPE. En plus du travail d'octets, cette fonction est facilement corrélée avec les idées ci-dessus. Même la logique sleep_on/ wake_upne semble pas si étrangère.

Linux moderne, FreeBSD, NetBSD, noyaux OpenBSD


Je suis rapidement passé en revue certains grains modernes. Aucun d'entre eux n'a déjà une implémentation de disque (sans surprise). Linux a sa propre implémentation. Bien que les trois noyaux BSD modernes contiennent des implémentations basées sur du code écrit par John Dyson, au fil des ans, ils sont devenus trop différents les uns des autres.

Pour lire fs/ pipe.c(sous Linux) ou sys/ kern/ sys_pipe.c(sur * BSD), une véritable dédicace est requise. Les performances et la prise en charge de fonctionnalités telles que les E / S vectorielles et asynchrones sont aujourd'hui importantes dans le code. Et les détails de l'allocation de mémoire, des verrous et de la configuration du noyau - tout cela est très différent. Ce n'est pas ce dont les universités ont besoin pour un cours d'introduction aux systèmes d'exploitation.

Dans tous les cas, il était intéressant pour moi de découvrir quelques anciens modèles (par exemple, générer SIGPIPEet retourner EPIPElors de l'écriture dans un pipeline fermé) dans tous ces cœurs modernes, si différents. Je ne verrai probablement jamais un ordinateur PDP-11 en direct, mais il y a encore beaucoup à apprendre du code qui a été écrit quelques années avant ma naissance.

Écrit par Divi Kapoor en 2011, l'article « La mise en œuvre du noyau Linux des tuyaux et des FIFO » donne un aperçu du fonctionnement (jusqu'à présent) des pipelines sur Linux. Et la récente validation Linux illustre un modèle d'interaction en pipeline dont les capacités dépassent les capacités des fichiers temporaires; et montre également jusqu'où les pipelines sont passés du «verrouillage très conservateur» dans le noyau Unix de la sixième édition.

All Articles