
In diesem Artikel wird die Implementierung von Pipes im Unix-Kern beschrieben. Ich war etwas enttäuscht, dass der kürzlich erschienene Artikel mit dem Titel „“ sich als nicht über die interne Struktur herausstellte. Ich wurde neugierig und durchsuchte alte Quellen, um eine Antwort zu finden.
Worum geht es?
Pipes — „wahrscheinlich die wichtigste Erfindung in Unix“ — sind ein definierendes Merkmal der zugrunde liegenden Unix-Philosophie, kleine Programme zusammenzuführen, und auch eine vertraute Eingabeaufforderung:
$ echo hello | wc -c
6
Diese Funktionalität basiert auf einem vom Kernel bereitgestellten Systemaufruf Pipe, der in der Dokumentation beschrieben ist und :
Pipes bieten einen unidirektionalen Kanal für die zwischenprozessliche Kommunikation. Eine Pipe hat ein Ende für die Eingabe (write end) und ein Ende für die Ausgabe (read end). Die in das Eingangsende der Pipe geschriebenen Daten können am Ausgang gelesen werden.
Eine Pipe wird durch den Aufruf erstellt
pipe(2), der zwei Dateideskriptoren zurückgibt: einer verweist auf das Eingangsende der Pipe, der andere auf das Ausgangsende.
Die Ergebnisse der oben genannten Kommandotrace zeigen die Erstellung einer Pipe 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, "hellon", 6) = 6
[pid 2604796] dup2(3, 0) = 0
[pid 2604796] execve("/usr/bin/wc", ["wc", "-c"], …)
[pid 2604796] read(0, "hellon", 16384) = 6
[pid 2604796] write(1, "6n", 2) = 2
Der übergeordnete Prozess ruft pipe(), um verbundene Dateideskriptoren zu erhalten. Ein Kindprozess schreibt in einen Deskriptor, während ein anderer Prozess die gleichen Daten aus einem anderen Deskriptor liest. Die Shell "benennt" die Deskriptoren 3 und 4 mithilfe von dup2 um, sodass sie stdin und stdout entsprechen.
Ohne Pipelines müsste die Shell das Ergebnis eines Prozesses in eine Datei schreiben und an einen anderen Prozess übergeben, damit dieser die Daten aus der Datei liest. Dadurch würden wir mehr Ressourcen und Speicherplatz verschwenden. Pipelines sind jedoch nicht nur deshalb von Vorteil, weil sie die Verwendung von temporären Dateien vermeiden:
Wenn ein Prozess versucht, aus einer leeren Pipeline zu lesen, dann
read(2)blockiert, bis die Daten verfügbar sind. Wenn ein Prozess versucht, in eine volle Pipeline zu schreiben, dannwrite(2)sperrt, bis ausreichend Daten aus der Pipe gelesen wurden, um eine Aufzeichnung durchzuführen.
Wie das POSIX-Anforderungsmerkmal ist dies eine wichtige Eigenschaft: Das Schreiben in die Pipe bis zu PIPE_BUF Bytes (mindestens 512) muss atomar sein, damit Prozesse über die Pipe so interagieren können, wie es normale Dateien (die solche Garantien nicht bieten) nicht können.
Wenn eine normale Datei verwendet wird, kann ein Prozess seine gesamten Ausgaben darin schreiben und an einen anderen Prozess übergeben. Alternativ können Prozesse im Modus des engen Parallelbetriebs agieren und sich über einen externen Signalisierungsmechanismus (wie ein Semaphore) über den Abschluss des Schreibens oder Lesens informieren. Pipes befreien uns von all diesen Schwierigkeiten.
Was suchen wir?
Ich erkläre es einfach, damit Sie sich besser vorstellen können, wie eine Pipeline funktionieren könnte. Sie benötigen einen Puffer und einen bestimmten Zustand im Speicher. Es werden Funktionen zum Hinzufügen und Entfernen von Daten aus dem Puffer erforderlich sein. Zudem benötigt man ein Mittel, um Funktionen während der Lese- und Schreibvorgänge in die Dateideskriptoren aufzurufen. Und es sind Sperren nötig, um das vorgenannte spezielle Verhalten zu implementieren.
Jetzt sind wir bereit, im hellen Licht der Lampen den Quellcode des Kernels zu untersuchen, um unser vages mentalen Modell zu bestätigen oder zu widerlegen. Seien Sie jedoch stets auf Überraschungen gefasst.
Wonach suchen wir?
Ich weiß nicht, wo mein Exemplar des bekannten Buches „“ mit dem Quellcode von Unix 6 liegt, aber dank kann man online nach noch älterer Unix-Versionen suchen.
Das Stöbern in den Archiven von TUHS ähnelt einem Besuch im Museum. Wir können auf unsere gemeinsame Geschichte zurückblicken, und ich empfinde Respekt für die jahrelangen Bemühungen, all diese Materialien Stück für Stück von alten Kassetten und Drucken wiederherzustellen. Und ich bin mir der Fragmente, die noch fehlen, schmerzlich bewusst.
Nachdem wir unsere Neugier für die Geschichte der Pipes gestillt haben, können wir die modernen Kerne zum Vergleich betrachten.
Übrigens, Pipe ist der Systemaufruf Nummer 42 in der Tabelle sysent[]. Zufall?
Traditionelle Unix-Kerne (1970–1974)
Ich habe keine Spuren gefunden pipe(2) weder in (Januar 1970), noch in (November 1971), noch im unvollständigen Quellcode (Juni 1972).
TUHS behauptet, dass (Februar 1973) die erste Version mit Pipes war:
Die dritte Version von Unix war die letzte Version mit einem in Assembler geschriebenen Kernel, aber die erste Version mit Pipes. Im Jahr 1973 wurden Verbesserungen für die dritte Version durchgeführt, der Kernel wurde in C neu geschrieben und damit entstand die vierte Version von Unix.
Einer der Leser fand einen Scan eines Dokuments, in dem Doug McIlroy die Idee vorschlug, »Programme wie einen Gartenschlauch zu verbinden«.

In Brian Kernighans Buch „“ wird auch dieses Dokument in der Geschichte der Entstehung von Pipes erwähnt: „… es hing 30 Jahre lang an der Wand meines Büros bei Bell Labs“. Hier ist , und noch eine Geschichte aus :
Als Unix entstand, führte mich meine Leidenschaft für Koroutinen dazu, den Autor des Betriebssystems, Ken Thompson, zu bitten, dass Daten, die in einem Prozess geschrieben wurden, nicht nur auf ein Gerät, sondern auch an einen anderen Prozess ausgegeben werden. Ken war der Meinung, dass dies möglich ist. Als Minimalist wollte er jedoch, dass jede Systemfunktion eine bedeutende Rolle spielt. Hat das direkte Schreiben zwischen Prozessen wirklich einen großen Vorteil gegenüber dem Schreiben in eine Zwischenablage? Erst als ich einen konkreten Vorschlag mit dem eingängigen Namen "Pipeline" und einer Beschreibung der Syntax für die Interaktion zwischen Prozessen einbrachte, rief Ken schließlich aus: "Das mache ich!".
Und so geschah es. An einem schicksalhaften Abend änderte Ken den Kernel und die Shell, korrigierte mehrere Standardprogramme und standardisierte deren Eingabeverfahren (die von der Pipeline stammen können), sowie änderte er die Dateinamen. Am nächsten Tag begannen die Pipelines, in Anwendungen weit verbreitet eingesetzt zu werden. Ende der Woche druckten Sekretärinnen mit ihrer Hilfe Dokumente aus Textverarbeitungsprogrammen aus. Kurz darauf ersetzte Ken die ursprüngliche API und die Syntax für die Shellnutzung von Pipelines durch sauberere Konventionen, die seither angewendet werden.
Leider ist der Quellcode des Kernels der dritten Edition von Unix verloren gegangen. Und obwohl wir den in C geschriebenen Quellcode des Kernels , der im November 1973 veröffentlicht wurde, haben, kam dieser mehrere Monate vor der offiziellen Veröffentlichung heraus und enthält keine Implementierung von Pipelines. Es ist schade, dass der Quellcode der legendären Unix-Funktion verloren gegangen ist, vielleicht für immer.
Wir haben den Dokumentationstext zu pipe(2) beiden Veröffentlichungen, daher können wir mit der Suche in der Dokumentation beginnen (für bestimmte, manuell hervorgehobene Wörter, eine Zeichenfolge von Literalen ^H, gefolgt von einem Unterstrich!). Dieser Proto-pipe(2) in Assembler geschrieben und gibt nur einen Dateideskriptor zurück, bietet jedoch bereits die erwartete Grundfunktionalität:
Systemaufruf Pipe erzeugt einen Ein- und Ausgabe-Mechanismus, der als Pipeline bezeichnet wird. Der zurückgegebene Dateideskriptor kann für Lese- und Schreiboperationen verwendet werden. Wenn etwas in die Pipeline geschrieben wird, werden bis zu 504 Byte Daten zwischengespeichert, nach denen der Schreibvorgang angehalten wird. Beim Lesen aus der Pipeline werden die zwischengespeicherten Daten entnommen.
Bis zum nächsten Jahr wurde der Kern in C neu geschrieben, und erhielt sein modernes Aussehen mit dem Prototyp 'pipe(fildes)»:
Systemaufruf Pipe erstellt einen Eingabe-Ausgabe-Mechanismus, der als Pipeline bezeichnet wird. Die zurückgegebenen Dateideskriptoren können für Lese- und Schreibvorgänge verwendet werden. Wenn etwas in die Pipeline geschrieben wird, wird der Deskriptor verwendet, der in r1 zurückgegeben wird (entsprechend fildes[1]), wobei bis zu 4096 Bytes Daten gepuffert werden, bevor der Schreibvorgang angehalten wird. Beim Lesen aus der Pipeline übernimmt der Deskriptor, der in r0 zurückgegeben wird (entsprechend fildes[0]), die Daten.
Es wird vorausgesetzt, dass nach der Definition der Pipeline zwei (oder mehr) interagierende Prozesse (die durch nachfolgende Aufrufe erstellt werden) Die Semantik des Aufrufs) Daten aus der Pipeline mittels Aufrufen übertragen werden. read und write.
In der Shell gibt es eine Syntax zur Definition eines linearen Arrays von Prozessen, die durch eine Pipeline verbunden sind.
Aufrufe zum Lesen aus einer leeren Pipeline (die keine gepufferten Daten enthält) mit nur einem Ende (alle schreibenden Dateideskriptoren sind geschlossen), geben „Ende der Datei“ zurück. Schreiboperationen in einer ähnlichen Situation werden ignoriert.
Die früheste bezieht sich (Juni 1974), aber sie ist fast identisch mit der, die in der nächsten Version erschienen ist. Es wurden nur Kommentare hinzugefügt, sodass die fünfte Ausgabe übersprungen werden kann.
Die sechste Ausgabe von Unix (1975)
Beginnen wir, den Quellcode von Unix zu lesen (Mai 1975). Viele verdanken das Lions viel leichter zu finden als den Quellcode früherer Versionen:
Viele Jahre lang war das Buch Lions das einzige Dokument über den Unix-Kernel, das außerhalb der Bell Labs verfügbar war. Obwohl die Lizenz der sechsten Ausgabe es Lehrenden erlaubte, ihren Quellcode zu nutzen, schloss die Lizenz der siebten Ausgabe diese Möglichkeit aus, weshalb das Buch in Form illegaler maschinenschriftlicher Kopien verbreitet wurde.
Heute kann man einen Reprint des Buches kaufen, auf dem Studierende an einem Kopierer abgebildet sind. Dank Warren Toomey (der das TUHS-Projekt ins Leben gerufen hat) können Sie . Ich möchte Ihnen eine Vorstellung davon geben, wie viel Aufwand in die Erstellung der Datei geflossen ist:
Vor über 15 Jahren habe ich eine Kopie des bereitgestellten Quellcodes eingegeben. Lions, weil mir die Qualität meiner Kopie von einer unbekannten Anzahl anderer Kopien nicht gefiel. TUHS existierte noch nicht, und ich hatte keinen Zugang zu den alten Quellcodes. Aber 1988 fand ich ein altes Band mit 9 Spuren, auf dem eine Sicherung vom PDP11-Rechner war. Es war schwierig zu erkennen, ob es funktionierte, aber es gab einen unbeschädigten Baum /usr/src/, in dem die meisten Dateien mit dem Jahr 1979 gekennzeichnet waren, was zu diesem Zeitpunkt schon als uralt erschien. Es war die siebte Auflage oder ihre abgeleitete PWB, wie ich dachte.
Ich nahm die Entdeckung als Grundlage und bearbeitete die Quellcodes manuell bis zum Zustand der sechsten Auflage. Ein Teil des Codes blieb unverändert, bei einem anderen musste ich leicht editieren, indem ich das moderne Token += durch das veraltete =+ ersetzte. Etwas hastig gelöscht, während ich anderes komplett umschreiben musste, aber nicht zu viel.
Und heute können wir online auf TUHS den Quellcode der sechsten Auflage lesen aus .
Übrigens ist auf den ersten Blick das Hauptmerkmal des C-Codes vor der Zeit von Kernighan und Ritchie seine Kürze.. Es ist nicht oft, dass ich Code-Schnipsel ohne umfangreiche Anpassungen einfügen kann, damit sie in das relativ schmale Anzeigeformat auf meiner Webseite passen.
Am Anfang gibt es einen erläuternden Kommentar (und ja, da ist noch mehr) ):
/*
* 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 Größe des Puffers hat sich seit der vierten Auflage nicht geändert. Doch hier sehen wir ohne jede öffentliche Dokumentation, dass früher Pipelines Dateien als Backup-Speicher verwendet haben!
Was die LARG-Dateien betrifft, so entsprechen sie , das vom „Algorithmus großer Adressierung“ zur Verarbeitung verwendet wird, um größere Dateisysteme zu unterstützen. Da Ken gesagt hat, dass man sie besser nicht verwenden sollte, glaube ich ihm ohne weiteres.
Hier ist der echte 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;
}
Im Kommentar wird klar beschrieben, was hier passiert. Aber den Code zu durchdringen ist nicht ganz einfach, partly wegen der Art, wie die Parameter der Systemaufrufe und Rückgabewerte durch „“ und die Register R0 und R1 übergeben werden.
Lassen Sie uns versuchen, mit auf der Festplatte , und mit — zwei im Speicher zu platzieren . Wenn alles gut läuft, setzen wir die Flags, um diese Dateien als die beiden Enden der Pipeline zu kennzeichnen, verweisen sie auf dasselbe Inode (dessen Zähler gleich 2 wird) und markieren das Inode als verändert und in Gebrauch. Achten Sie auf Zugriffe auf in Fehlerpfaden (error paths), um den Zähler des neuen Inodes zu verringern.
pipe() soll durch R0 und R1 die Dateideskriptoren für Lesen und Schreiben zurückgeben. falloc() gibt einen Zeiger auf die Datei-Struktur zurück, sondern "gibt" auch über u.u_ar0[R0] und den Dateideskriptor zurück. Das heißt, der Code speichert im r Dateideskriptor für das Lesen und weist den Deskriptor für das Schreiben direkt aus u.u_ar0[R0] nach dem zweiten Aufruf falloc().
Flagge FPIPE, der bei der Erstellung der Pipeline festgelegt wurde, steuert das Verhalten der Funktion , die spezifische Eingabe-/Ausgabeunterprogramme 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.c Daten aus der Pipeline. Es ist jedoch besser, die Implementierung beginnend mit writep(). Ich wiederhole, der Code wurde komplizierter aufgrund der Besonderheiten des Argumentübergabeverfahrens, aber einige Details können weggelassen werden.
writep(fp)
{
register *rp, *ip, c;
rp = fp;
ip = rp->f_inode;
c = u.u_count;
loop:
/* Wenn alles erledigt ist, zurückkehren. */
plock(ip);
if(c == 0) {
prele(ip);
u.u_count = 0;
return;
}
/*
* Wenn nicht sowohl die Lese- als auch die Schreibseite des
* Rohres aktiv sind, Fehler zurückgeben und ebenfalls signalisieren.
*/
if(ip->i_count i_size1 == PIPSIZ) {
ip->i_mode =| IWRITE;
prele(ip);
sleep(ip+1, PPIPE);
goto loop;
}
/* Schreibe, was möglich ist, und gehe zurück zur Schleife. */
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;
}
In das Rohr möchten wir Bytes schreiben. u.u_count. Zuerst müssen wir den Index-Descriptor sperren (siehe unten). plock/prele).
Dann überprüfen wir den Linkzähler des Inodes. Solange beide Enden des Rohrs geöffnet bleiben, sollte der Zähler 2 betragen. Wir halten einen Verweis (aus rp->f_inode), sodass wenn der Zähler kleiner als 2 wird, das bedeuten muss, dass der lesende Prozess sein Ende des Rohrs geschlossen hat. Mit anderen Worten, wir versuchen, in ein geschlossenen Rohr zu schreiben, was einen Fehler darstellt. Der Fehlercode lautet zuerst EPIPE und Signal SIGPIPE erscheinen in der sechsten Unix-Edition.
Selbst wenn die Pipeline geöffnet ist, kann sie gefüllt sein. In diesem Fall heben wir die Sperre auf und gehen schlafen, in der Hoffnung, dass ein anderer Prozess aus der Pipeline liest und genügend Platz freigibt. Wenn wir aufwachen, kehren wir zum Anfang zurück, setzen die Sperre wieder und starten einen neuen Schreibzyklus.
Wenn in der Pipeline genügend freier Platz vorhanden ist, schreiben wir die Daten mithilfe von . Parameter i_size1 der inode (kann bei leerer Pipeline 0 sein) zeigt auf das Ende der bereits enthaltenen Daten. Wenn genügend Platz zum Schreiben vorhanden ist, können wir die Pipeline bis zu i_size1 bis zu PIPESIZfüllen. Danach heben wir die Sperre auf und versuchen, jeden Prozess aufzuwecken, der darauf wartet, aus der Pipeline zu lesen. Wir gehen zurück zum Anfang, um zu sehen, ob wir die benötigte Anzahl von Bytes schreiben konnten. Wenn nicht, beginnen wir einen neuen Schreibzyklus.
Normalerweise wird das Attribut i_mode der inode verwendet, um Berechtigungen zu speichern. r, w und xBei Pipelines signalisieren wir jedoch das Warten eines Schreib- oder Leseprozesses mithilfe der Bits IREAD und IWRITE entsprechend. Der Prozess setzt ein Flag und ruft sleep(), und es wird erwartet, dass in Zukunft ein anderer Prozess aufgerufen wird wakeup().
Die eigentliche Magie passiert in sleep() und wakeup(). Sie sind implementiert in , der Quelle des berühmten Kommentars „Sie sind nicht verpflichtet, das zu verstehen“ (You are not expected to understand this). Glücklicherweise sind wir nicht verpflichtet, den Code zu verstehen, sondern schauen uns 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) /* … */
Der Prozess, der aufruft sleep() für einen bestimmten Kanal, kann später von einem anderen Prozess geweckt werden, der aufruft wakeup() für denselben Kanal. writep() und readp() koordinieren ihre Aktionen über solche Paaraufrufe. Beachten Sie, dass pipe.c immer Priorität hat PPIPE beim Aufruf sleep(), daher können alle sleep() durch ein Signal unterbrochen werden.
Jetzt haben wir alles, um die Funktion zu verstehen readp():
readp(fp)
int *fp;
{
register *rp, *ip;
rp = fp;
ip = rp->f_inode;
loop:
/* Sehr konservative Sperrung. */
plock(ip);
/*
* Wenn der Kopf (lesen) den Schwanz (schreiben) eingeholt hat,
* setzen Sie beide auf 0 zurück.
*/
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);
}
}
/*
* Wenn nicht sowohl Leser als auch
* Schreiber aktiv sind, kehren Sie zurück,
* ohne das Lesen zu erfüllen.
*/
prele(ip);
if(ip->i_count i_mode =| IREAD;
sleep(ip+2, PPIPE);
goto loop;
}
/* Lesen und zurückgeben */
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);
}
Es könnte einfacher sein, diese Funktion von unten nach oben zu lesen. Der Branch „lesen und zurückgeben“ wird normalerweise verwendet, wenn Daten im Puffer vorhanden sind. In diesem Fall verwenden wir wir lesen so viele Daten, wie verfügbar sind, beginnend mit dem aktuellen f_offset für das Lesen und aktualisieren dann den Wert des entsprechenden Versatzes.
Bei nachfolgendem Lesen wird der Puffer leer sein, wenn der Leseversatz den Wert erreicht hat i_size1 des inode. Wir setzen die Position auf 0 zurück und versuchen, jeden Prozess zu wecken, der in den Puffer schreiben möchte. Wir wissen, dass, wenn der Puffer voll ist, writep() er wird schlafen bei ip+1. Und jetzt, wo das FIFO leer ist, können wir es wecken, damit es seinen Aufnahmezyklus wieder aufnimmt.
Wenn es nichts zu lesen gibt, readp() kann es ein Flag setzen IREAD und schlafen auf ip+2.Wir wissen, dass es geweckt wird writep(), wenn es einige Daten in die FIFO schreibt.
Die Kommentare zu helfen zu verstehen, dass wir anstelle von Parametern über „u“ sie wie normale Ein-/Ausgabefunktionen behandeln können, die eine Datei, eine Position, einen Speicherpuffer annehmen und die Anzahl der Bytes zählen, die gelesen oder geschrieben werden sollen.
/*
* 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;
/* … */
Was die „konservative“ Sperrung betrifft, readp() und writep() sperren sie den inode, bis sie ihre Arbeit beendet haben oder ein Ergebnis erhalten (also den wakeup). plock() und prele() funktionieren einfach: mit einem anderen Satz von Aufrufen sleep und wakeup erlauben es uns, jeden Prozess zu wecken, der eine Sperre benötigt, die wir gerade freigegeben 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 readp() keine prele(ip) vor dem Aufruf von wakeup(ip+1). Das erste, was writep() in seiner Schleife aufruft, ist plock(ip),was zu einer wechselseitigen Blockierung führt, wenn readp() es seine Sperre noch nicht aufgehoben hat, daher muss der Code irgendwie richtig funktionieren. Wenn man sich das anschaut wakeup(), dann wird klar, dass es nur den schlafenden Prozess als bereit zur Ausführung markiert, um ihn in Zukunft sched() tatsächlich zu starten. Also readp() verursacht das Update wakeup(), hebt die Sperre auf, setzt IREAD und ruft sleep(ip+2)— all das, bevor writep() der Zyklus wieder aufgenommen wird.
Damit endet die Beschreibung von Pipelines in der sechsten Auflage. Einfacher Code, weitreichende Folgen.
(Januar 1979) war eine neue Hauptversion (nach vier Jahren), in der viele neue Anwendungen und Eigenschaften des Kernels eingeführt wurden. Es gab auch erhebliche Änderungen bezüglich der Verwendung von Typumwandlungen, Unionen und typisierten Zeigern auf Strukturen. Allerdings praktisch unverändert. Wir können diese Auflage überspringen.
Xv6, ein einfaches Unix-ähnliches Kernel
Die Erstellung des Kernels Die sechste Version von Unix hatte Einfluss, jedoch ist sie in modernem C geschrieben, sodass sie auf x86-Prozessoren ausgeführt werden kann. Der Code ist leicht lesbar und verständlich. Außerdem, im Gegensatz zu den Unix-Quellen von TUHS, können Sie ihn kompilieren, ändern und auf etwas anderem als dem PDP 11/70 ausführen. Daher wird dieser Kernel häufig an Universitäten als Lehrmaterial für Betriebssysteme verwendet. Der Quellcode .
Der Code enthält eine verständliche und gut durchdachte Implementierung , unterstützt von einem Speicherpuffer anstelle eines Inodes auf der Festplatte. Hier gebe ich nur die Definition des "strukturellen Pipes" und die Funktionen an 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() bestimmt den Zustand der gesamten restlichen Implementierung, die die Funktionen umfasst piperead(), pipewrite() und pipeclose(). Der tatsächliche Systemaufruf sys_pipe ist eine in . Ich empfehle, den gesamten Code zu lesen. Die Komplexität entspricht der der sechsten Version, aber es ist erheblich einfacher und angenehmer zu lesen.
Linux 0.01
Der Quellcode von Linux 0.01 ist verfügbar. Es wird lehrreich sein, die Implementierung der Pipes darin zu studieren. fs/pipe.c. Hier wird für das Pipe-System ein inode verwendet, aber das Pipe selbst ist in modernem C geschrieben. Wenn Sie durch den Code der sechsten Version gekommen sind, sollten Sie hier 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) { /* keine Leser */
current->signal |= (1< 0) {
while (PIPE_FULL(*inode)) {
wake_up(&inode->i_wait);
if (inode->i_count != 2) {
current->signal |= (1<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 Strukturdokumentationen kann man nachvollziehen, wie der Referenzzähler des inode verwendet wird, um zu überprüfen, ob die Schreiboperation zu SIGPIPE. Neben der byteweisen Verarbeitung lässt sich diese Funktion leicht mit den oben beschriebenen Ideen in Einklang bringen. Selbst die Logik sleep_on/wake_up wirkt nicht so fremd.
Moderne Linux-Kerne, FreeBSD, NetBSD, OpenBSD
Ich habe mich schnell durch einige moderne Kerne geblättert. Keiner von ihnen hat noch eine Implementierung mit Disketten (nicht überraschend). In Linux gibt es eine eigene Implementierung. Auch wenn die drei modernen BSD-Kerne Implementierungen auf Basis von Code enthalten, der von John Dyson geschrieben wurde, haben sie sich im Laufe der Jahre zu stark voneinander unterschieden.
Um zu lesen fs/pipe.c (unter Linux) oder sys/kern/sys_pipe.c (unter *BSD) erfordert es echtes Engagement. Heute zählen im Code Leistung und Unterstützung für Funktionen wie vektorbasierte und asynchrone Ein-/Ausgabeoperationen. Außerdem variieren die Details zur Speicherzuweisung, Sperren und Kernelkonfiguration erheblich. Das ist nicht das, was Hochschulen für einen Einführungskurs in Betriebssysteme brauchen.
Wie dem auch sei, ich fand es interessant, einige alte Muster (zum Beispiel die Generierung SIGPIPE und Rückgabe EPIPE beim Schreiben in eine geschlossene Pipe) in all diesen so unterschiedlichen modernen Kernen auszugraben. Wahrscheinlich werde ich einen PDP-11-Computer nie im echten Leben sehen, aber es gibt immer noch viel aus dem Code zu lernen, der einige Jahre vor meiner Geburt geschrieben wurde.
Der Artikel von Divi Kapoor aus dem Jahr 2011 „» bietet einen Überblick über die Funktionsweise von Pipelines in Linux. A illustriert das Modell der Pipeline-Interaktion, dessen Möglichkeiten die temporären Dateien übertreffen; es zeigt auch, wie weit sich Pipelines von der "sehr konservativen Sperrung" im Kern der sechsten Unix-Version entfernt haben.
Quelle: habr.com
