
In questo articolo viene descritta l'implementazione delle pipeline nel kernel Unix. Sono rimasto un po' deluso che un recente articolo intitolato «» si sia rivelato non sulla struttura interna. Sono diventato curioso e ho scavato in vecchie fonti per trovare una risposta.
Di cosa si tratta?
Le pipeline sono «probabilmente l'invenzione più importante in Unix» — una caratteristica fondamentale della filosofia Unix che unisce piccoli programmi, insieme a una nota familiare nella riga di comando:
$ echo hello | wc -c
6
Questa funzionalità si basa sulla system call fornita dal kernel pipe, che è descritta nelle pagine della documentazione e :
Le pipeline forniscono un canale unidirezionale per la comunicazione interprocesso. Una pipeline ha un ingresso (write end) e un'uscita (read end). I dati scritti nell'ingresso della pipeline possono essere letti in uscita.
La pipeline viene creata tramite la chiamata
pipe(2), che restituisce due descrittori di file: uno si riferisce all'ingresso della pipeline, l'altro all'uscita.
I risultati della traccia del comando sopra menzionato dimostrano la creazione della pipeline e il flusso di dati attraverso di essa da un processo all'altro:
$ 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
Il processo padre richiama pipe(), per ottenere i descrittori di file connessi. Un processo figlio scrive in un descrittore e un altro processo legge gli stessi dati da un altro descrittore. La shell rinomina, tramite dup2, i descrittori 3 e 4 in modo che corrispondano a stdin e stdout.
Senza pipe, la shell dovrebbe scrivere il risultato di un processo in un file e passarne i dati a un altro processo in modo che potesse leggerli. Di conseguenza, spenderemmo più risorse e spazio su disco. Tuttavia, le pipe non sono utili solo per evitare file temporanei:
Se un processo tenta di leggere da una pipe vuota, allora
read(2)si bloccherà fino a quando i dati non saranno disponibili. Se un processo tenta di scrivere in una pipe già piena, allorawrite(2)bloccherà fino a quando non verranno letti abbastanza dati dal tubo per effettuare la registrazione.
Come per il requisito POSIX, questa è una proprietà importante: la registrazione nel tubo fino a PIPE_BUF byte (almeno 512) deve essere atomica, affinché i processi possano interagire tra loro tramite il tubo come i normali file (che non forniscono tali garanzie) non possono fare.
Quando si utilizza un file normale, un processo può scrivere in esso tutti i suoi output e passarli a un altro processo. Oppure i processi possono agire in modalità di parallelismo rigido, attraverso un meccanismo di segnalazione esterno (come un semaforo) avvisandosi reciprocamente al termine della registrazione o lettura. I tubi ci liberano da tutte queste complicazioni.
Cosa stiamo cercando?
Spiegherò in parole semplici, in modo che possiate immaginare come potrebbe funzionare una pipeline. Avrete bisogno di riservare un buffer in memoria e di uno stato. Sono necessarie funzioni per aggiungere e rimuovere dati dal buffer. Serve un mezzo per invocare le funzioni durante le operazioni di lettura e scrittura nei file descriptor. E saranno necessarie delle lock per implementare il comportamento speciale descritto sopra.
Ora siamo pronti ad esaminare a fondo il codice sorgente del kernel per confermare o smentire il nostro vago modello mentale. Ma siate sempre pronti a sorprese.
Dove cerchiamo?
Non so dove si trovi il mio esemplare del famoso libro "" con il codice sorgente di Unix 6, ma grazie a si può cercare online in di versioni ancora più vecchie di Unix.
Navigare negli archivi del TUHS è come visitare un museo. Possiamo dare un'occhiata alla nostra storia condivisa e provo rispetto per gli sforzi di molti anni per recuperare tutto questo materiale bit per bit da vecchie cassette e stampe. Sono anche consapevole dei frammenti che ancora mancano.
Soddisfando la propria curiosità riguardo alla storia antica dei tubi, possiamo confrontarli con i kernel moderni.
A proposito, pipe è la chiamata di sistema numero 42 nella tabella sysent[]. Coincidenza?
I kernel Unix tradizionali (1970–1974)
Non ho trovato tracce pipe(2) né in (gennaio 1970), né nella (novembre 1971), né nel codice sorgente incompleto (giugno 1972).
TUHS sostiene che (febbraio 1973) è stata la prima versione con tubi:
La terza edizione di Unix è stata l'ultima versione con un kernel scritto in assembly, ma anche la prima versione con tubi. Durante il 1973 sono stati effettuati miglioramenti sulla terza edizione, il kernel è stato riscritto in C, dando origine alla quarta edizione di Unix.
Uno dei lettori ha trovato una scansione di un documento in cui Doug McIlroy propose l'idea di "collegare i programmi come un tubo da giardino".

Nel libro di Brian Kernighan "", si fa menzione anche di questo documento nell'evoluzione dei tubi: "... è rimasto appeso al muro del mio ufficio nei Bell Labs per 30 anni". Ecco , e un'altra storia da :
Quando Unix è nato, la mia passione per le coroutine mi ha spinto a chiedere all'autore del sistema operativo, Ken Thompson, di permettere che i dati scritti in un processo potessero andare non solo su un dispositivo, ma anche come output per un altro processo. Ken ha deciso che era possibile. Tuttavia, essendo un minimalista, voleva che ogni funzione di sistema avesse un ruolo significativo. Esiste davvero un grande vantaggio nel trasferimento diretto tra processi rispetto alla scrittura su un file temporaneo? Solo quando ho presentato una proposta concreta con un titolo accattivante "pipeline" e una descrizione della sintassi per l'interazione tra processi, Ken ha finalmente esclamato: "Lo farò!".
E così è stato. Una sera decisiva, Ken ha modificato il nucleo e la shell, ha corretto alcuni programmi standard, standardizzando la loro procedura di acquisizione dei dati in ingresso (che possono provenire da un pipeline), e ha anche cambiato i nomi dei file. Il giorno dopo, i pipeline hanno cominciato a essere ampiamente utilizzati nelle applicazioni. Alla fine della settimana, le segretarie li utilizzavano per inviare documenti dai loro editor di testo alla stampante. Poco dopo, Ken ha sostituito l'API originale e la sintassi per la shell dell'uso dei pipeline con convenzioni più pulite, che da allora sono in uso.
Sfortunatamente, il codice sorgente del nucleo della terza edizione di Unix è andato perso. E sebbene abbiamo il codice sorgente scritto in C del nucleo , rilasciato nel novembre del 1973, essa è stata pubblicata qualche mese prima del rilascio ufficiale e non contiene l'implementazione dei pipeline. È un peccato che il codice sorgente della leggendaria funzione di Unix sia andato perso, forse per sempre.
Abbiamo il testo della documentazione per pipe(2) entrambi i rilasci, quindi possiamo iniziare a cercare nella documentazione (per parole specifiche, sottolineate «manualmente», una stringa di letterali ^H, seguita da un trattino basso!). Questo proto-pipe(2) scritto in assembler e restituisce solo un descrittore di file, ma offre già la funzionalità di base attesa:
Chiamata di sistema pipe crea un meccanismo di input e output chiamato pipeline. Il descrittore di file restituito può essere utilizzato per operazioni di lettura e scrittura. Quando si scrive qualcosa nella pipeline, i dati vengono memorizzati in un buffer fino a 504 byte, dopodiché il processo di scrittura si sospende. Quando si legge dalla pipeline, i dati memorizzati vengono prelevati.
Entro l'anno successivo, il nucleo era stato riscritto in C, e ha assunto la sua forma moderna con il prototipo «pipe(fildes)»:
Chiamata di sistema pipe crea un meccanismo di input/output chiamato pipeline. I descrittori di file restituiti possono essere utilizzati nelle operazioni di lettura e scrittura. Quando qualcosa viene scritto nella pipeline, si utilizza il descrittore restituito in r1 (corrispondente a fildes[1]), che viene bufferizzato fino a 4096 byte di dati, dopo di che il processo di scrittura viene sospeso. Durante la lettura dalla pipeline, il descrittore restituito in r0 (corrispondente a fildes[0]) raccoglie i dati.
Si presume che, dopo la definizione della pipeline, due (o più) processi in interazione (creati da chiamate successive fork) trasmetteranno dati attraverso la pipeline usando le chiamate read e write.
Nella shell esiste una sintassi per definire un array lineare di processi collegati tramite pipeline.
Le chiamate di lettura da una pipeline vuota (che non contiene dati bufferizzati), che ha solo un'estremità (con tutti i descrittori di file di scrittura chiusi), restituiscono 'fine file'. Le chiamate di scrittura in una situazione analoga vengono ignorate.
La più antica risale (giugno 1974), ma è quasi identica a quella apparsa nella versione successiva. Sono stati aggiunti solo commenti, quindi si può saltare la quinta edizione.
Sesta edizione di Unix (1975)
Iniziamo a leggere il codice sorgente di Unix (maggio 1975). In gran parte grazie a Lions trovare il codice sorgente è molto più facile rispetto alle versioni precedenti:
Per molti anni, il libro Lions è stato l'unico documento sul kernel di Unix disponibile al di fuori di Bell Labs. Anche se la licenza della sesta edizione permetteva agli insegnanti di utilizzare il suo codice sorgente, la licenza della settima edizione escludeva questa possibilità, quindi il libro è stato distribuito in copie dattiloscritte illegali.
Oggi è possibile acquistare una copia riprodotta del libro, la cui copertina mostra studenti presso una fotocopiatrice. E grazie a Warren Toomey (che ha avviato il progetto TUHS), puoi scaricare . Vorrei darvi un'idea di quanto lavoro sia stato necessario per creare il file:
Oltre 15 anni fa, ho trascritto una copia del codice sorgente qui riportato. Lions, perché non mi piaceva la qualità della mia copia rispetto a un numero imprecisato di altre copie. TUHS non esisteva ancora, e non avevo accesso ai vecchi sorgenti. Ma nel 1988, trovai un vecchio nastro a 9 tracce che conteneva un backup del computer PDP11. Era difficile capire se funzionasse, ma c'era un albero intatto /usr/src/, in cui la maggior parte dei file era datata 1979, che già allora sembrava antica. Era la settima edizione o una sua derivata PWB, come ritenevo.
Ho preso quella scoperta come base e ho modificato manualmente i sorgenti fino a farli arrivare allo stato della sesta edizione. Parte del codice è rimasta la stessa, parte ho dovuto modificarla leggermente, cambiando il token moderno += con il superato =+. Qualcosa l'ho semplicemente eliminato, mentre altre cose le ho dovute riscrivere completamente, ma non troppe.
E oggi possiamo leggere online su TUHS il codice sorgente della sesta edizione da .
A proposito, a prima vista, la principale caratteristica del codice C fino al periodo di Kernighan e Ritchie è la sua brevità. Non capita spesso di riuscire a inserire frammenti di codice senza una lunga revisione per adattarli a un'area di visualizzazione relativamente ristretta sul mio sito.
Inizio c'è un commento esplicativo (e sì, ce n'è anche un altro ):
/*
* 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 dimensione del buffer non è cambiata dai tempi della quarta edizione. Ma qui, senza alcuna documentazione pubblica, vediamo che un tempo le pipeline usavano i file come deposito di riserva!
Per quanto riguarda i file LARG, corrispondono , che viene utilizzato dall'«algoritmo di grande indirizzamento» per gestire per supportare file system più grandi. Dal momento che Ken ha detto di non usarli, lo crederò volentieri.
Ecco una vera chiamata di sistema 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;
}
Il commento descrive chiaramente cosa sta succedendo. Ma districarsi nel codice non è così semplice, in parte a causa del modo in cui i parametri delle chiamate di sistema e i valori restituiti vengono trasmessi attraverso ‘’ e i registri R0 e R1 Il tentativo è di utilizzare
ialloc() inode (descriptor d'indice) falloc() — разместить в памяти два . Se tutto va bene, imposteremo i flag per identificare questi file come i due estremi della pipe, li indicheremo nello stesso inode (il cui contatore dei riferimenti diventerà pari a 2) e contrassegneremo l'inode come modificato e in uso. Si prega di notare le chiamate a nei percorsi di errore per ridurre il contatore dei riferimenti nel nuovo inode.
pipe() deve passare attraverso R0 e R1 restituire i numeri dei descrittori di file per la lettura e la scrittura. — allocare in memoria due restituisce un puntatore alla struttura del file, ma restituisce anche tramite u.u_ar0[R0] e il descrittore di file. Cioè, il codice salva in r il descrittore di file per la lettura e assegna il descrittore per la scrittura direttamente da u.u_ar0[R0] dopo la seconda chiamata — allocare in memoria due.
Flag FPIPE, che abbiamo impostato durante la creazione della pipe, gestisce il comportamento della funzione , che chiama specifici sotto-programmi di input-output I/O:
/*
* 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);
}
/* … */
}
La funzione readp() in pipe.c legge i dati dalla pipe. Ma è meglio seguire l'implementazione partendo da writep(). Ribadisco, il codice si è complicato a causa delle particolarità del contratto di passaggio degli argomenti, ma alcuni dettagli possono essere trascurati.
writep(fp)
{
register *rp, *ip, c;
rp = fp;
ip = rp->f_inode;
c = u.u_count;
loop:
/* Se tutto è fatto, restituisci. */
plock(ip);
if(c == 0) {
prele(ip);
u.u_count = 0;
return;
}
/*
* Se non ci sono entrambe le estremità di lettura e scrittura del
* pipe attive, restituisci errore e segnala anche.
*/
if(ip->i_count i_size1 == PIPSIZ) {
ip->i_mode =| IWRITE;
prele(ip);
sleep(ip+1, PPIPE);
goto loop;
}
/* Scrivi ciò che è possibile e torna indietro. */
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;
}
Vogliamo scrivere byte nel pipeline all'ingresso u.u_count. Prima richiediamo di bloccare il descrittore dell'indice (vedi sotto plock/prele).
Controlliamo poi il contatore dei riferimenti dell'inode. Finché entrambe le estremità del pipeline rimangono aperte, il contatore deve essere uguale a 2. Manteniamo un riferimento (da rp->f_inode), quindi se il contatore è inferiore a 2, ciò deve significare che il processo di lettura ha chiuso il suo estremo del pipeline. In altre parole, stiamo cercando di scrivere in un pipeline chiuso, e questo è un errore. Il codice di errore per la prima volta è EPIPE e il segnale SIGPIPE sono stati introdotti nella sesta edizione di Unix.
Ma anche se il tubo è aperto, può essere pieno. In questo caso, noi rimuoviamo il blocco e andiamo a dormire sperando che un altro processo legga dal tubo e liberi abbastanza spazio. Al risveglio, torniamo all'inizio, rimettiamo il blocco e avviamo un nuovo ciclo di scrittura.
Se c'è abbastanza spazio libero nel tubo, scriviamo i dati usando . Il parametro i_size1 del inode (in un tubo vuoto può essere uguale a 0) indica la fine dei dati già presenti. Se c'è abbastanza spazio per scrivere, possiamo riempire il tubo fino a i_size1 fino a PIPESIZ. Poi rimuoviamo il blocco e tentiamo di risvegliare qualsiasi processo che sta aspettando di leggere dal tubo. Torniamo all'inizio per vedere se siamo riusciti a scrivere il numero di byte necessario. Se non ci siamo riusciti, iniziamo un nuovo ciclo di scrittura.
Di solito il parametro i_mode del inode è usato per memorizzare i permessi. r, w e xMa nel caso dei tubi, segnaliamo l'attesa di un processo di scrittura o lettura usando i bit IREAD e IWRITE rispettivamente. Il processo imposta il flag e chiama sleep(), e ci si aspetta che in futuro un altro processo lo chiami wakeup().
La vera magia avviene in sleep() e wakeup(). Sono implementati in , sorgente del famoso commento «Non sei tenuto a capire questo» (You are not expected to understand this). Fortunatamente, non dobbiamo capire il codice, ci basterà dare un'occhiata ad alcuni commenti:
/*
* 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) /* … */
Il processo che chiama sleep() per un certo canale può essere successivamente risvegliato da un altro processo che chiama wakeup() per lo stesso canale. writep() e readp() coordinano le loro azioni tramite queste chiamate abbinate. Si noti che pipe.c dà sempre priorità a PPIPE quando viene chiamato sleep(), quindi tutti sleep() possono essere interrotti da un segnale.
Ora abbiamo tutto per comprendere la funzione readp():
readp(fp)
int *fp;
{
register *rp, *ip;
rp = fp;
ip = rp->f_inode;
loop:
/* Blocco molto conservativo. */
plock(ip);
/*
* Se la testa (lettura) ha raggiunto
* la coda (scrittura), azzera entrambi a 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);
}
}
/*
* Se non ci sono sia lettore che
* scrittore attivi, torna senza
* soddisfare la lettura.
*/
prele(ip);
if(ip->i_count i_mode |= IREAD;
sleep(ip+2, PPIPE);
goto loop;
}
/* Leggi e ritorna */
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);
}
Potresti trovare più facile leggere questa funzione dal basso verso l'alto. Il ramo «read and return» è solitamente usato quando ci sono dati nel pipeline. In questo caso, utilizziamo leggere i dati disponibili a partire dall'attuale f_offset di lettura, e poi aggiorniamo il valore dello spostamento corrispondente.
Con la successiva lettura, la pipeline sarà vuota se lo spostamento di lettura ha raggiunto il valore i_size1 del inode. Ripristiniamo la posizione a 0 e proviamo a risvegliare qualsiasi processo che desideri scrivere nella pipeline. Sappiamo che quando la pipeline sarà piena, writep() si addormenterà su ip+1. E ora che la coda è vuota, possiamo attivarla affinché riprenda il suo ciclo di registrazione.
Se non c'è nulla da leggere, readp() può impostare un flag IREAD e andare a dormire su ip+2.Sappiamo che verrà riattivato writep()quando registrerà dei dati nella coda.
I commenti a aiuteranno a comprendere che anziché passare parametri tramite «u», possiamo trattarli come normali funzioni di input/output che prendono un file, una posizione, un buffer in memoria e conteggiano il numero di byte da leggere o scrivere.
/*
* 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;
/* … */
Per quanto riguarda il blocco «conservativo», readp() e writep() bloccano l'inode fino al termine del lavoro o fino a quando non ottengono un risultato (ovvero invocano wakeup). plock() e prele() funzionano semplicemente: utilizzando un altro insieme di chiamate sleep e wakeup ci consentono di risvegliare qualsiasi processo che ha bisogno di un blocco che abbiamo appena rimosso:
/*
* 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);
}
}
All'inizio non riuscivo a capire perché readp() non invocasse prele(ip) prima di chiamare wakeup(ip+1).La prima cosa che writep() invoca nel suo ciclo è plock(ip),che porta a un deadlock, se readp() non ha già rimosso il suo blocco, quindi il codice in qualche modo deve funzionare correttamente. Se guardiamo a wakeup(), si rende chiaro che segna solo il processo dormiente come pronto per l'esecuzione, per il futuro sched() lo ha effettivamente avviato. Quindi readp() chiama wakeup(), rimuove il blocco, imposta IREAD e chiama sleep(ip+2)— tutto questo prima che writep() riprenda il ciclo.
A questo punto, la descrizione dei pipeline nella sesta edizione è conclusa. Codice semplice, conseguenze di vasta portata.
(gennaio 1979) è stata un nuovo rilascio principale (dopo quattro anni), in cui sono apparse molte nuove applicazioni e funzionalità nel kernel. Ci sono stati anche cambiamenti significativi riguardanti l'uso della conversione di tipi, delle union e dei puntatori tipizzati sulle strutture. Tuttavia è praticamente rimasto invariato. Possiamo saltare questa edizione.
Xv6, un semplice kernel simile a Unix
La creazione del kernel la sesta edizione di Unix ha avuto un impatto, ma è scritta in C moderno, rendendola compatibile con i processori x86. Il codice è facile da leggere e comprensibile. Inoltre, a differenza del codice sorgente Unix di TUHS, puoi compilarlo, modificarlo ed eseguirlo su qualcosa di diverso dal PDP 11/70. Per questo motivo, questo kernel è ampiamente utilizzato nelle università come materiale didattico sui sistemi operativi. I sorgenti .
Il codice contiene un'implementazione chiara e ben progettata , supportata da un buffer in memoria invece che da un inode su disco. Qui fornisco solo la definizione di "pipeline strutturale" e le funzioni 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() definisce lo stato per tutta la restante realizzazione, che include le funzioni piperead(), pipewrite() e pipeclose(). La chiamata di sistema effettiva sys_pipe è un wrapper implementato in . Ti consiglio di leggere tutto il suo codice. La complessità è a livello della sorgente della sesta edizione, ma è molto più facile e piacevole da leggere.
Linux 0.01
Puoi trovare il codice sorgente di Linux 0.01. Sarebbe istruttivo studiare l'implementazione delle pipeline nel suo fs/pipe.c. Qui per rappresentare la pipeline viene utilizzato l'inode, ma l'intera pipeline è scritta in C moderno. Se hai già esaminato il codice della sesta edizione, qui non avrai difficoltà. Ecco come appare la funzione 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<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;
}
Anche senza guardare le definizioni delle strutture, è possibile capire come il contatore dei riferimenti inode viene utilizzato per verificare se l'operazione di scrittura porta a SIGPIPE. Oltre al lavoro byte per byte, questa funzione può essere facilmente confrontata con le idee descritte sopra. Anche la logica sleep_on/wake_up non sembra così estranea.
I moderni kernel Linux, FreeBSD, NetBSD, OpenBSD
Ho esaminato rapidamente alcuni dei moderni kernel. Nessuno di essi ha più implementazioni che utilizzano il disco (non sorprende). In Linux c'è una propria implementazione. E sebbene i tre attuali kernel BSD contengano implementazioni basate su codice scritto da John Dyson, nel corso degli anni si sono differenziati notevolmente l'uno dall'altro.
Per leggere fs/pipe.c (su Linux) o sys/kern/sys_pipe.c (su *BSD), è necessaria una vera dedizione. Oggi nel codice sono importanti le prestazioni e il supporto per funzioni come operazioni di I/O vettoriale e asincrone. E i dettagli sulla gestione della memoria, sui lock e sulla configurazione del kernel variano notevolmente. Non è ciò di cui le università hanno bisogno per un corso introduttivo sui sistemi operativi.
Comunque, ero curioso di scavare alcuni vecchi pattern (come la generazione SIGPIPE e il ritorno EPIPE nella scrittura su una pipe chiusa) in tutti questi moderni kernel così diversi. Probabilmente non vedrò mai un computer PDP-11 dal vivo, ma c'è ancora molto da imparare dal codice scritto alcuni anni prima della mia nascita.
L'articolo scritto da Divi Kapoor nel 2011 "» è una panoramica su come funzionano (ancora) le pipe in Linux. Un illustra il modello di interazione delle pipe, le cui capacità superano quelle dei file temporanei; e mostra quanto siano evolute le pipe rispetto al "blocco molto conservativo" nel sesto rilascio del kernel Unix.
Fonte: habr.com
