Wie Unix-Pipelines implementiert werden


Dieser Artikel beschreibt die Implementierung von Pipelines im Unix-Kernel. Ich war etwas enttäuscht von einem kürzlich erschienenen Artikel mit dem Titel " Wie funktionieren Pipelines unter Unix?" "Ging nicht um das interne Gerät. Ich wurde interessiert und vergrub mich in den alten Quellen, um die Antwort zu finden.

Worüber reden wir?


Pipelines - „wahrscheinlich die wichtigste Erfindung unter Unix“ - sind das bestimmende Merkmal der zugrunde liegenden Philosophie von Unix, kleine Programme miteinander zu kombinieren, sowie die bekannte Befehlszeile:

$ echo hello | wc -c
6

Diese Funktionalität hängt vom Systemaufruf des Kernels ab pipe, der auf den Dokumentationsseiten Pipe (7) und Pipe (2) beschrieben wird :

Förderer bieten einen unidirektionalen Interprozess-Kommunikationskanal. Die Pipeline hat einen Eingang (Schreibende) und einen Ausgang (Leseende). Auf den Eingang der Pipeline geschriebene Daten können ausgelesen werden.

Die Pipeline wird mithilfe eines Aufrufs erstellt pipe(2), der zwei Dateideskriptoren zurückgibt: einer bezieht sich auf die Eingabe der Pipeline, der zweite auf die Ausgabe.

Die Ablaufverfolgungsergebnisse des obigen Befehls veranschaulichen die Erstellung einer Pipeline und den Datenfluss durch sie von einem Prozess zum anderen:

$ 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

Der übergeordnete Prozess ruft pipe()auf, um die angehängten Dateideskriptoren abzurufen. Ein untergeordneter Prozess schreibt in einen Deskriptor, und ein anderer Prozess liest dieselben Daten aus einem anderen Deskriptor. Der Wrapper, der dup2 verwendet, „benennt“ die Deskriptoren 3 und 4 um, um stdin und stdout abzugleichen.

Ohne Pipelines müsste die Shell das Ergebnis eines Prozesses in eine Datei schreiben und in einen anderen Prozess übertragen, damit sie die Daten aus der Datei liest. Infolgedessen würden wir mehr Ressourcen und Speicherplatz ausgeben. Pipelines sind jedoch nicht nur deshalb gut, weil sie die Verwendung temporärer Dateien vermeiden:

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

Wie die POSIX-Anforderung ist dies eine wichtige Eigenschaft: Das Schreiben in die Pipeline bis zu PIPE_BUFBytes (mindestens 512) muss atomar sein, damit Prozesse über die Pipeline auf dieselbe Weise miteinander interagieren können wie reguläre Dateien (die keine solchen Garantien bieten).

Bei Verwendung einer regulären Datei kann ein Prozess alle seine Ausgabedaten in diese Datei schreiben und an einen anderen Prozess übertragen. Oder Prozesse können im harten Parallelisierungsmodus arbeiten und einen externen Signalisierungsmechanismus (z. B. ein Semaphor) verwenden, um sich gegenseitig über den Abschluss des Schreibens oder Lesens zu informieren. Förderer ersparen uns all diese Probleme.

Was suchen wir?


Ich erkläre es an meinen Fingern, damit Sie sich leichter vorstellen können, wie der Förderer funktionieren kann. Sie müssen einen Puffer und einen Status im Speicher zuweisen. Sie benötigen Funktionen zum Hinzufügen und Entfernen von Daten zum Puffer. Es erfordert einige Mittel, um Funktionen während Lese- und Schreibvorgängen in Dateideskriptoren aufzurufen. Und Sperren sind erforderlich, um das oben beschriebene spezielle Verhalten zu implementieren.

Jetzt sind wir bereit, im hellen Licht der Lampen den Quellcode des Kernels abzufragen, um unser vages mentales Modell zu bestätigen oder zu widerlegen. Aber seien Sie immer auf das Unerwartete vorbereitet.

Wo suchen wir?


Ich weiß nicht, wo sich meine Kopie des berühmten Buches " Lions-Buch " mit Unix 6-Quellcode befindet, aber dank der Unix Heritage Society können Sie online nach noch älteren Versionen von Unix im Quellcode suchen .

Ein Spaziergang durch die TUHS-Archive ist wie ein Museumsbesuch. Wir können einen Blick auf unsere gemeinsame Geschichte werfen, und ich respektiere die langjährigen Bemühungen, all diese Materialien Stück für Stück aus alten Kassetten und Ausdrucken wiederzugewinnen. Und ich bin mir der Fragmente, die noch fehlen, sehr bewusst.

Nachdem wir unsere Neugier auf die alte Geschichte der Förderer befriedigt haben, können wir uns moderne Kerne zum Vergleich ansehen.

Übrigens pipeist eine Systemrufnummer 42 in der Tabelle sysent[]. Zufall?

Traditionelle Unix-Kernel (1970–1974)


Ich habe pipe(2)weder in PDP-7 Unix (Januar 1970) noch in der ersten Ausgabe von Unix (November 1971) oder im unvollständigen Quellcode der zweiten Ausgabe (Juni 1972) eine Spur gefunden .

TUHS behauptet, dass die dritte Ausgabe von Unix (Februar 1973) die erste Version mit Pipelines war:

Die dritte Ausgabe von Unix war die neueste Version mit einem in Assemblersprache geschriebenen Kernel, aber die erste Version mit Pipelines. Im Jahr 1973 wurde an der Verbesserung der dritten Ausgabe gearbeitet, der Kern wurde in C umgeschrieben, und so erschien die vierte Ausgabe von Unix.

Einer der Leser fand einen Scan eines Dokuments, in dem Doug McIlroy die Idee vorschlug, „Programme nach dem Prinzip eines Gartenschlauchs zu verbinden“.


In dem Buch von Brian Kernighan „ Unix: Eine Geschichte und eine Erinnerung “, in der Geschichte des Auftretens von Förderbändern, wird dieses Dokument auch erwähnt: „... es hing 30 Jahre lang in meinem Büro bei Bell Labs an der Wand.“ Hier ist ein Interview mit McIlroy und eine weitere Geschichte aus McIlroys Arbeit aus dem Jahr 2014 :

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

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

Leider geht der Kernel-Quellcode für die dritte Unix-Edition verloren. Und obwohl wir den Quellcode für den Kernel der vierten Ausgabe in C haben , der im November 1973 veröffentlicht wurde, wurde er einige Monate vor der offiziellen Veröffentlichung veröffentlicht und enthält keine Pipeline-Implementierungen. Schade, dass der Quellcode der legendären Unix-Funktion möglicherweise für immer verloren geht.

Wir haben den Text der Dokumentation pipe(2)aus beiden Versionen, sodass Sie zunächst die dritte Ausgabe der Dokumentation durchsuchen können (nach bestimmten Wörtern, die „manuell“ unterstrichen sind, eine Zeichenfolge von Literalen ^ H, gefolgt von Unterstrichen!). Dieses Proto ist pipe(2)in Assembler geschrieben und gibt nur einen Dateideskriptor zurück, bietet jedoch bereits die erwartete Grundfunktionalität:

Der Pipe -Systemaufruf erstellt einen Ausgabeeingabemechanismus, der als Pipeline bezeichnet wird. Der zurückgegebene Dateideskriptor kann für Lese- und Schreibvorgänge verwendet werden. Wenn etwas in die Pipeline geschrieben wird, werden bis zu 504 Datenbytes gepuffert, wonach der Schreibvorgang angehalten wird. Beim Lesen aus einer Pipeline werden gepufferte Daten erfasst.

Bis zum nächsten Jahr wurde der Kernel in C umgeschrieben, und Pipe (2) in der vierten Ausgabe fand mit dem Prototyp " pipe(fildes)" sein modernes Aussehen :

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

, ( ) ( fork) read write.

Die Shell verfügt über eine Syntax zum Definieren eines linearen Arrays von Prozessen, die über eine Pipeline verbunden sind.

Leseaufrufe von einer leeren Pipeline (die keine gepufferten Daten enthält) mit nur einem Ende (alle Schreibdateideskriptoren sind geschlossen) geben das "Dateiende" zurück. Das Aufzeichnen von Anrufen in einer ähnlichen Situation wird ignoriert.

Die früheste überlebende Pipeline-Implementierung stammt aus der fünften Ausgabe von Unix (Juni 1974), ist jedoch fast identisch mit der in der nächsten Version veröffentlichten. Es wurden nur Kommentare hinzugefügt, sodass die fünfte Ausgabe übersprungen werden kann.

Sechste Ausgabe von Unix (1975)


Wir beginnen mit dem Lesen des sechsten Unix-Quellcodes (Mai 1975). Vor allem dank Lions ist es viel einfacher, es zu finden als den Quellcode früherer Versionen:

Lions ist seit vielen Jahren das einzige Unix-Kerndokument, das außerhalb der Wände von Bell Labs verfügbar ist. Obwohl die Lizenz der sechsten Ausgabe es Lehrern erlaubte, ihren Quellcode zu verwenden, schloss die Lizenz der siebten Ausgabe diese Möglichkeit aus, so dass das Buch in Form von illegalen maschinengeschriebenen Kopien verteilt wurde.

Heute können Sie eine Nachdruckkopie des Buches kaufen, auf dessen Cover die Schüler beim Fotokopierer gezeigt werden. Und dank Warren Tumi (der das TUHS-Projekt ins Leben gerufen hat) können Sie die PDF-Datei mit dem Quellcode für die sechste Ausgabe herunterladen . Ich möchte Ihnen eine Vorstellung davon geben, wie viel Aufwand beim Erstellen der Datei erforderlich war:

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

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

Und heute können wir online auf TUHS den Quellcode der sechsten Ausgabe aus dem Archiv lesen , an dem Dennis Ritchie beteiligt war .

Auf den ersten Blick ist das Hauptmerkmal des C-Codes vor der Kernigan- und Richie-Zeit übrigens seine Kürze . Nicht so oft schaffe ich es, Codefragmente ohne umfangreiche Bearbeitung einzubetten, um sie an einen relativ engen Anzeigebereich auf meiner Website anzupassen.

Am Anfang von /usr/sys/ken/pipe.c steht ein erklärender Kommentar (und ja, es gibt auch / 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

Die Puffergröße hat sich seit der vierten Ausgabe nicht geändert. Aber hier, ohne öffentliche Dokumentation, sehen wir, dass einmal die Pipelines Dateien als Backup-Speicher verwendet haben!

LARG-Dateien entsprechen dem LARG-Inode-Flag , das vom "High-Addressing-Algorithmus" zur Verarbeitung indirekter Blöcke verwendet wird, um größere Dateisysteme zu unterstützen. Da Ken sagte, es sei besser, sie nicht zu benutzen, nehme ich gerne sein Wort dafür.

Hier ist der eigentliche Systemaufruf 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;
}

Der Kommentar beschreibt klar, was hier passiert. Das Verstehen des Codes ist jedoch nicht einfach, teilweise aufgrund der Möglichkeit, mithilfe von « a struct user u » Parameter von Systemaufrufen und Rückgabewerten zu registrieren R0und zu R1übertragen.

Versuchen wir, mit ialloc () Inode (Inode ) auf der Festplatte zu platzieren und mit Falloc () zwei Dateien im Speicher abzulegen . Wenn alles gut geht, setzen wir Flags, um diese Dateien als die beiden Enden der Pipeline zu definieren, zeigen sie auf denselben Inode (dessen Referenzanzahl 2 ist) und markieren den Inode als geändert und verwendet. Achten Sie auf Aufrufe von iput ()in Fehlerpfaden, um die Referenzanzahl im neuen Inode zu verringern.

pipe()muss die Dateideskriptornummern zum Lesen und Schreiben durchlaufen R0und R1zurückgeben. falloc()Gibt einen Zeiger auf die Dateistruktur zurück, gibt aber auch über den u.u_ar0[R0]Dateideskriptor zurück. Das heißt, der Code wird rzum Lesen in einem Dateideskriptor gespeichert und weist direkt u.u_ar0[R0]nach dem zweiten Aufruf einen Deskriptor zum Schreiben zu falloc().

Das Flag FPIPE, das wir beim Erstellen der Pipeline setzen, steuert das Verhalten der Funktion rdwr () in sys2.c , die bestimmte E / A-E / A-Routinen aufruft:

/*
 * 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);
    }
        /* … */
}

Dann liest die Funktion readp()in pipe.cdie Daten aus der Pipeline. Es ist jedoch besser, die Implementierung zu verfolgen writep(). Erneut wurde der Code aufgrund der Besonderheiten der Argumentübertragungsvereinbarung komplizierter, aber einige Details können weggelassen werden.

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;
}

Wir wollen Bytes in die Eingabe der Pipeline schreiben u.u_count. Zuerst müssen wir den Inode sperren (siehe unten plock/ prele).

Überprüfen Sie dann den Inode-Referenzzähler. Während beide Enden der Pipeline offen bleiben, sollte der Zähler 2 sein. Wir behalten eine Verbindung (out rp->f_inode) bei. Wenn der Zähler also kleiner als 2 ist, sollte dies bedeuten, dass der Lesevorgang sein Ende der Pipeline geschlossen hat. Mit anderen Worten, wir versuchen, in einer geschlossenen Pipeline zu schreiben, und dies ist ein Fehler. Der Fehlercode EPIPEund das Signal wurden erstmals SIGPIPEin der sechsten Ausgabe von Unix veröffentlicht.

Aber selbst wenn der Förderer geöffnet ist, kann er voll sein. In diesem Fall entfernen wir die Sperre und schlafen ein, in der Hoffnung, dass ein anderer Prozess aus der Pipeline liest und genügend Speicherplatz darin freigibt. Nach dem Aufwachen kehren wir zum Anfang zurück, blockieren erneut die Sperre und starten einen neuen Aufnahmezyklus.

Wenn in der Pipeline genügend freier Speicherplatz vorhanden ist, schreiben wir Daten mit writei () darauf . Der Parameter i_size1in Inode (bei einer leeren Pipeline kann 0 sein) gibt das Ende der Daten an, die bereits enthalten sind. Wenn genügend Aufnahmeraum vorhanden ist, können wir den Förderer von i_size1bis füllenPIPESIZ. Dann entfernen wir die Sperre und versuchen, jeden Prozess zu aktivieren, der auf die Gelegenheit wartet, aus der Pipeline zu lesen. Wir gehen zurück zum Anfang, um zu sehen, ob wir es geschafft haben, so viele Bytes zu schreiben, wie wir brauchten. Wenn dies fehlgeschlagen ist, beginnen wir einen neuen Aufnahmezyklus.

Typischerweise i_modewird ein Inode wird Parameter speichern Berechtigungen verwendet r, wund x. Im Fall von Pipelines signalisieren wir jedoch, dass ein Prozess darauf wartet, mit Bits IREADbzw. zu schreiben oder zu lesen IWRITE. Ein Prozess setzt ein Flag und ruft auf sleep(), und es wird erwartet, dass in Zukunft ein anderer Prozess aufruft wakeup().

Wirkliche Magie passiert in sleep()und wakeup(). Sie sind in slp.c implementiert, die Quelle des berühmten Kommentars: "Es wird nicht erwartet, dass Sie dies verstehen." Glücklicherweise müssen wir den Code nicht verstehen, sehen Sie sich nur einige Kommentare an:

/*
 * 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) /* … */

Ein Prozess, der sleep()für einen bestimmten Kanal aufgerufen wird, kann später von einem anderen Prozess geweckt werden, der wakeup()für denselben Kanal aufgerufen wird. writep()und readp()koordinieren ihre Aktionen durch solche gepaarten Anrufe. Bitte beachten Sie, dass beim Anrufen pipe.cimmer Vorrang hat , damit jeder das Signal unterbrechen kann. Jetzt haben wir alles, um die Funktion zu verstehen :PPIPEsleep()sleep()

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);
}

Möglicherweise fällt es Ihnen leichter, diese Funktion von unten nach oben zu lesen. Der Zweig "Lesen und Zurückgeben" wird normalerweise verwendet, wenn sich Daten in der Pipeline befinden. In diesem Fall verwenden wir readi (), um ab dem aktuellen Lesevorgang so viele Daten zu lesen, wie verfügbar sind f_offset, und aktualisieren dann den Wert des entsprechenden Offsets.

Bei nachfolgenden Lesevorgängen ist die Pipeline leer, wenn der Leseversatz den Wert des i_size1Inodes erreicht hat. Wir setzen die Position auf 0 zurück und versuchen, jeden Prozess zu aktivieren, der in die Pipeline geschrieben werden soll. Wir wissen, dass der Förderer, wenn er voll ist, writep()einschlafen wird ip+1. Und jetzt, da die Pipeline leer ist, können wir sie aufwecken, damit sie ihren Aufnahmezyklus fortsetzt.

Wenn es nichts zu lesen gibt, readp()kann es eine Flagge setzen IREADund einschlafenip+2. Wir wissen, was ihn wecken wird, writep()wenn er einige Daten in die Pipeline schreibt.

Die Kommentare zu readi () und writei () helfen zu verstehen, dass uwir Parameter nicht durch " " übergeben können , sondern wie übliche E / A-Funktionen behandeln können, die eine Datei, Position, Puffer im Speicher und die Anzahl der zu lesenden oder zu schreibenden Bytes zählen .

/*
 * 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;
/* … */

Wie für die „konservativen“ lock, dann readp()und writep()Block Inode , solange sie einen Job zu beenden oder nicht bekommen , das Ergebnis (dh die Ursache wakeup). plock()und sie prele()funktionieren einfach: Verwenden Sie einen anderen Satz von Aufrufen sleepund wakeupermöglichen Sie uns, jeden Prozess zu aktivieren, der eine Sperre benötigt, die wir gerade entfernt haben:

/*
 * 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);
    }
}

Zuerst konnte ich nicht verstehen, warum es vor dem Anruf readp()nicht prele(ip)anrief wakeup(ip+1). Das erste, was writep()in seiner Schleife verursacht wird, ist, dass es plock(ip)zu einem Deadlock führt, wenn readp()es seinen Block noch nicht entfernt hat, so dass der Code irgendwie korrekt funktionieren muss. Wenn Sie es sich ansehen wakeup(), wird klar, dass er den Schlafprozess nur als zur Ausführung bereit markiert, damit er in Zukunft sched()wirklich gestartet wird. Es readp()verursacht wakeup(), entsperrt, setzt IREADund ruft sleep(ip+2)- all dies, bevor writep()der Zyklus fortgesetzt wird.

Damit ist die Beschreibung der Förderer in der sechsten Ausgabe abgeschlossen. Einfacher Code, weitreichende Konsequenzen.

Siebte Ausgabe von Unix(Januar 1979) war die neue Hauptversion (vier Jahre später), in der viele neue Anwendungen und Kernel-Eigenschaften erschienen. Es gab auch signifikante Änderungen im Zusammenhang mit der Verwendung von Typguss, Union'ov und typisierten Zeigern auf Strukturen. Der Pipeline-Code hat sich jedoch nicht wesentlich geändert. Wir können diese Ausgabe überspringen.

Xv6, ein einfacher Unix-förmiger Kernel


Die sechste Ausgabe von Unix hat die Erstellung des Xv6- Kerns beeinflusst , ist jedoch in modernem C für die Ausführung auf x86-Prozessoren geschrieben. Der Code ist leicht zu lesen, es ist klar. Im Gegensatz zu Unix-Quellen mit TUHS können Sie es außerdem kompilieren, ändern und auf etwas anderem als PDP 11/70 ausführen. Daher wird dieser Kern an Universitäten häufig als Lehrmaterial für Betriebssysteme verwendet. Quellen sind auf Github .

Der Code enthält eine übersichtliche und durchdachte Implementierung von pipe.c , die durch einen Puffer im Speicher anstelle von Inode auf der Festplatte gesichert wird. Hier gebe ich nur die Definition von "strukturelle Pipeline" und Funktion 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()Legt den Status der restlichen Implementierung fest, die Funktionen enthält piperead(), pipewrite()und pipeclose(). Der eigentliche Systemaufruf sys_pipeist ein in sysfile.c implementierter Wrapper . Ich empfehle, den gesamten Code zu lesen. Die Komplexität liegt auf der Quellenebene der sechsten Ausgabe, ist aber viel einfacher und angenehmer zu lesen.

Linux 0.01


Sie finden den Quellcode für Linux 0.01. Es wird lehrreich sein, die Implementierung von Pipelines in seinem fs/ zu studieren pipe.c. Hier wird Inode verwendet, um die Pipeline darzustellen, aber die Pipeline selbst ist in modernem C geschrieben. Wenn Sie den Code der sechsten Ausgabe durchlaufen haben, werden Sie keine Schwierigkeiten haben. So sieht die Funktion aus 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;
}

Auch ohne die Definitionen von Strukturen zu betrachten, können Sie herausfinden, wie der Inode-Referenzzähler verwendet wird, um zu überprüfen, ob die Schreiboperation dazu führt SIGPIPE. Zusätzlich zur Bytearbeit kann diese Funktion leicht mit den obigen Ideen korreliert werden. Auch die Logik sleep_on/ wake_upsieht nicht so fremd aus.

Moderne Linux-, FreeBSD-, NetBSD- und OpenBSD-Kernel


Ich ging schnell einige moderne Kernel durch. Keiner von ihnen hat bereits eine Festplattenimplementierung (nicht überraschend). Linux hat eine eigene Implementierung. Obwohl die drei modernen BSD-Kernel Implementierungen enthalten, die auf Code basieren, der von John Dyson geschrieben wurde, sind sie im Laufe der Jahre zu unterschiedlich geworden.

Zum Lesen von fs/ pipe.c(unter Linux) oder sys/ kern/ sys_pipe.c(unter * BSD) ist eine echte Widmung erforderlich. Leistung und Unterstützung für Funktionen wie Vektor und asynchrone E / A sind heute im Code wichtig. Und die Details der Speicherzuordnung, Sperren und Kernelkonfiguration - all dies ist sehr unterschiedlich. Dies ist nicht das, was Universitäten für einen Einführungskurs in Betriebssysteme benötigen.

Auf jeden Fall war es für mich interessant, in all diesen so unterschiedlichen, modernen Kernen mehrere alte Muster zu entdecken (z. B. Generieren SIGPIPEund Zurückkehren EPIPEbeim Schreiben in eine geschlossene Pipeline). Ich werde wahrscheinlich nie einen Live-PDP-11-Computer sehen, aber es gibt noch viel zu lernen aus dem Code, der einige Jahre vor meiner Geburt geschrieben wurde.

Der 2011 von Divi Kapoor verfasste Artikel „ Die Implementierung von Pipes und FIFOs im Linux-Kernel “ bietet einen Überblick über die (bisherige) Funktionsweise von Pipelines unter Linux. Das jüngste Linux-Commit zeigt ein Pipeline-Interaktionsmodell, dessen Funktionen die Funktionen temporärer Dateien übertreffen. und zeigt auch, wie weit die Pipelines vom „sehr konservativen Sperren“ im Unix-Kernel der sechsten Ausgabe entfernt sind.

All Articles