Come sono implementati i pipeline in Unix

Come sono implementati i pipeline in Unix
In questo articolo viene descritta l'implementazione delle pipe nel nucleo Unix. Sono rimasto un po' deluso che un recente articolo intitolato "Come funzionano le pipe in Unix?" si è rivelato non sul funzionamento interno. Sono stato incuriosito e mi sono messo a cercare in vecchie fonti per trovare una risposta.

Di cosa si tratta?

Le pipe sono "probabilmente la più importante invenzione in Unix" — sono una caratteristica distintiva della filosofia Unix di unire piccole funzionalità in programmi, così come la nota familiare nella riga di comando:

$ echo hello | wc -c
6

Questa funzionalità dipende dalla chiamata di sistema fornita dal nucleo pipe, che è descritta nelle pagine della documentazione pipe(7) e pipe(2):

Le pipe forniscono un canale unidirezionale per la comunicazione tra processi. Una pipe ha un ingresso (write end) e un'uscita (read end). I dati scritti nell'ingresso della pipe possono essere letti in uscita.

La pipe è creata con la chiamata pipe(2), che restituisce due descrittori di file: uno si riferisce all'ingresso della pipe, l'altro all'uscita.

Le risultanze del tracciamento del comando sopra riportato mostrano la creazione della pipe 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 chiama pipe(), per ottenere i descrittori di file collegati. Un processo figlio scrive in un descrittore, mentre un altro processo legge gli stessi dati dall'altro descrittore. La shell tramite dup2 "rinomina" i descrittori 3 e 4 affinché corrispondano a stdin e stdout.

Senza le pipe, la shell dovrebbe scrivere il risultato di un processo in un file e passarne un altro processo affinché legga i dati dal file. Questo avrebbe comportato un maggior sfruttamento delle risorse e spazio su disco. Tuttavia, le pipe non sono utili solo per evitare l'uso di file temporanei:

Se un processo cerca di leggere da una pipe vuota, allora read(2) si bloccherà fino a quando i dati non saranno disponibili. Se un processo cerca di scrivere in una pipe piena, allora write(2) bloccherà fino a quando non saranno stati letti dal tubo un numero sufficiente di dati per eseguire la scrittura.

Come richiesto da POSIX, questa è una proprietà importante: la scrittura nel tubo fino a PIPE_BUF byte (almeno 512) deve essere atomica, in modo che i processi possano interagire l'uno con l'altro attraverso il tubo in modi che i file normali (che non forniscono tali garanzie) non possono.

Utilizzando un file normale, un processo può scrivere tutte le sue uscite e passarle a un altro processo. Oppure i processi possono funzionare in modalità di parallelismo stretto, utilizzando un meccanismo di segnalazione esterno (come un semaforo) per comunicare tra loro il completamento della scrittura o della lettura. I tubi ci liberano da tutti questi problemi.

Cosa stiamo cercando?

Spiegherò in modo semplice, per aiutarvi a comprendere come può funzionare un tubo. Avrete bisogno di allocare in memoria un buffer e uno stato. Serviranno funzioni per aggiungere e rimuovere dati dal buffer. Sarà necessaria qualche modalità per chiamare le funzioni durante le operazioni di lettura e scrittura sui descrittori di file. E saranno necessarie delle serrature per implementare il comportamento speciale descritto sopra.

Ora siamo pronti a interrogare sotto la luce intensa delle lampade il codice sorgente del kernel, per confermare o confutare il nostro vago modello mentale. Ma siate sempre pronti a sorprese.

Dove stiamo cercando?

Non so dove si trovi la mia copia del noto libro “Lions book“ con il codice sorgente di Unix 6, ma grazie a The Unix Heritage Society è possibile cercare online nel codice sorgente di versioni ancora più vecchie di Unix.

Navigare negli archivi del TUHS è come visitare un museo. Possiamo dare un'occhiata alla nostra storia comune e ho grande rispetto per gli sforzi pluriennali per recuperare tutto questo materiale bit per bit da vecchie cassette e stampe. E sono acutamente consapevole dei frammenti che mancano ancora.

Soddisfatta la mia curiosità sulla storia antica dei tubi, per confronto possiamo guardare ai kernel moderni.

A proposito, pipe è la chiamata di sistema numero 42 nella tabella sysent[]. Coincidenza?

I kernel tradizionali di Unix (1970–1974)

Non ho trovato tracce pipe(2) né in PDP-7 Unix (gennaio 1970), né nella prima edizione di Unix (novembre 1971), né nel codice sorgente incompleto della della seconda edizione (giugno 1972).

TUHS afferma che la terza edizione di Unix (febbraio 1973) è stata la prima versione con pipe:

La terza edizione di Unix è stata l'ultima versione con un kernel scritto in assembler, ma è stata anche la prima versione con pipe. Nel corso del 1973, ci sono stati lavori di miglioramento della terza edizione, il kernel è stato riscritto in C, dando origine alla quarta edizione di Unix.

Uno dei lettori ha trovato uno scanner di un documento in cui Doug McIlroy ha proposto l'idea di "collegare programmi secondo il principio del tubo da giardino".

Come sono implementati i pipeline in Unix
Nel libro di Brian Kernighan "Unix: A History and a Memoir", questa documentazione è anche menzionata nella storia dell'emergere delle pipe: "… è stata appesa al muro del mio ufficio nei Bell Labs per 30 anni". Ecco un'intervista con McIlroy, e un'altra storia dal lavoro di McIlroy, scritto nel 2014:

Quando Unix è stato introdotto, il mio interesse per le coroutine mi ha spinto a chiedere all'autore del sistema operativo, Ken Thompson, di permettere ai dati registrati in un processo di andare non solo a un dispositivo, ma anche all'uscita di un altro processo. Ken ha ritenuto che fosse possibile. Tuttavia, da minimalista, voleva che ogni funzione di sistema avesse un ruolo significativo. Ha davvero un grande vantaggio la scrittura diretta tra processi rispetto alla scrittura su un file intermedio? E solo quando ho presentato una proposta concreta con un nome accattivante "pipe" e una descrizione della sintassi per l'interazione tra processi, Ken ha esclamato finalmente: "Lo farò!".

E così ha fatto. Una fatidica sera, Ken ha modificato il kernel e la shell, ha corretto alcuni programmi standard, standardizzando le loro procedure di ingresso (che possono provenire da una pipe), e ha anche cambiato i nomi dei file. Il giorno dopo le pipe hanno iniziato a essere applicate in modo molto ampio nelle applicazioni. Alla fine della settimana, le segretarie le usavano per inviare documenti dai text editor alla stampante. Poco dopo, Ken ha sostituito l'API originale e la sintassi per l'uso delle pipe nella shell con convenzioni più pulite, che sono state adottate da allora.

Purtroppo, il codice sorgente del kernel della terza edizione di Unix è andato perso. E sebbene abbiamo il codice sorgente del kernel della quarta edizione, rilasciato a novembre 1973, è stato pubblicato alcuni mesi prima del rilascio ufficiale e non contiene l'implementazione delle pipe. È un peccato che il codice sorgente della leggendaria funzione di Unix sia andato perso, forse per sempre.

Abbiamo del testo di documentazione su pipe(2) entrambi i rilasci, quindi possiamo iniziare a cercare nella documentazione terza edizione (cercando parole specifiche, messe in evidenza «manualmente», la stringa di letterali ^H, seguita da un trattino basso!). Questo proto-pipe(2) è scritto in assembler e restituisce solo un file descriptor, ma offre già la funzionalità di base attesa:

Chiamata di sistema pipe crea un meccanismo di input/output chiamato pipeline. Il file descriptor restituito può essere utilizzato per operazioni di lettura e scrittura. Quando si scrive qualcosa nella pipeline, viene bufferizzato fino a 504 byte di dati, dopo di che il processo di scrittura viene sospeso. Durante la lettura dalla pipeline, i dati bufferizzati vengono prelevati.

Entro l'anno successivo, il kernel è stato riscritto in C, e pipe(2) nella quarta edizione ha acquisito il suo aspetto moderno con il prototipo «pipe(fildes)»:

Chiamata di sistema pipe crea un meccanismo di input/output chiamato pipeline. I file descriptor restituiti possono essere utilizzati in operazioni di lettura e scrittura. Quando qualcosa viene scritto nella pipeline, viene utilizzato 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]) preleva i dati.

Si presume che dopo la definizione della pipeline due (o più) processi interagenti (creati da chiamate successive a fork) trasferiranno dati dalla pipeline tramite chiamate a read e write.

Nella shell c'è una sintassi per definire un array lineare di processi collegati tramite pipeline.

Le chiamate per la lettura da una pipeline vuota (senza dati bufferizzati), che ha solo un'estremità (tutti i file descriptor di scrittura sono chiusi), restituiscono «fine file». Le chiamate per la scrittura in una situazione simile vengono ignorate.

La più antica implementazione di pipeline è fino alla quinta edizione di Unix (giugno 1974), ma è quasi identica a quella apparso nel rilascio successivo. Sono stati solo aggiunti commenti, quindi la quinta edizione può essere saltata.

La sesta edizione di Unix (1975)

Iniziamo a leggere il codice sorgente di Unix sesta edizione (maggio 1975). In gran parte grazie a Lions trovarlo è molto più facile rispetto ai sorgenti di versioni precedenti:

Molti anni libro Lions è stato l'unico documento sul nucleo Unix disponibile al di fuori delle mura della Bell Labs. Sebbene la licenza della sesta edizione consentisse agli istruttori di utilizzare il suo codice sorgente, la licenza della settima edizione ha escluso questa possibilità, quindi il libro è stato distribuito sotto forma di copie dattiloscritte illegali.

Oggi è possibile acquistare un esemplare in ristampa del libro, con in copertina degli studenti presso una fotocopiatrice. E grazie a Warren Toomey (che ha avviato il progetto TUHS) puoi scaricare il file PDF con il codice sorgente della sesta edizione. Voglio darti un'idea di quanti sforzi siano stati necessari per creare il file:

Più di 15 anni fa ho digitato una copia del codice sorgente riportato in Lions, perché non mi piaceva la qualità della mia copia proveniente da un numero imprecisato di altre copie. TUHS non esisteva ancora e non avevo accesso ai vecchi sorgenti. Ma nel 1988 ho trovato una vecchia cassetta con 9 tracce, contenente un backup di un computer PDP11. Era difficile capire se funzionasse, ma c'era una struttura intatta di /usr/src/, dove la maggior parte dei file era datata 1979, che già all'epoca sembrava antica. Era la settima edizione o la sua derivata PWB, come credevo.

Ho preso il ritrovamento come base e ho modificato manualmente i sorgenti fino allo stato della sesta edizione. Parte del codice è rimasta la stessa, parte ha necessitato di piccole modifiche, cambiando il token moderno += con il superato =+. Qualcosa è stato semplicemente eliminato, mentre altro ha dovuto essere completamente riscritto, ma non troppo.

E oggi possiamo leggere online su TUHS il codice sorgente della sesta edizione proveniente da un archivio, cui ha contribuito Dennis Ritchie.

A proposito, a prima vista, la principale caratteristica del codice C prima del periodo di Kernighan e Ritchie è la sua concisione. Non capita così spesso che riesca a inserire frammenti di codice senza un ampio editing per adattarli a un'area di visualizzazione relativamente ristretta nel mio sito.

All'inizio /usr/sys/ken/pipe.c c'è un commento esplicativo (e sì, c'è ancora /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 dimensione del buffer non è cambiata dai tempi della quarta edizione. Ma qui vediamo senza alcuna documentazione pubblica che una volta le pipeline usavano i file come deposito di riserva!

Per quanto riguarda i file LARG, corrispondono al flag inode LARG, che viene utilizzato dall'"algoritmo di grande indirizzamento" per la gestione blocchi indiretti per supportare sistemi di file più grandi. Poiché Ken ha detto che è meglio non usarli, sono felice di credergli.

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 capire il codice non è così semplice, in parte a causa di come vengono passati i parametri delle chiamate di sistema tramite «struct user u» e registri R0 e R1 .

Proviamo con ialloc() a posizionare su disco inode (descrittore di indice), mentre con falloc() posizioniamo in memoria due del file. Se tutto va bene, imposteremo i flag per identificare questi file come le due estremità di una pipeline, li indicheremo nello stesso inode (il cui contatore dei riferimenti sarà pari a 2) e segneremo l'inode come modificato e in uso. Nota le chiamate a iput() nei percorsi di errore per ridurre il contatore dei riferimenti nel nuovo inode.

pipe() dovrebbe restituire numeri di descrittori di file per lettura e scrittura. R0 e R1 restituisce un puntatore alla struttura del file, ma "restituisce" anche tramite falloc() u.u_ar0[R0] e il descrittore del file. Cioè il codice memorizza in il descrittore del file per la lettura e assegna il descrittore per la scrittura direttamente da r dopo la seconda chiamata e il descrittore del file. Cioè il codice memorizza in FPIPE falloc().

Bandiera , che abbiamo impostato durante la creazione della pipeline, gestisce il comportamento della funzionerdwr() in sys2.c , che chiama le specifiche sottomodule di I/O:Quindi la funzione

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

readp() pipe.c in legge i dati dalla pipeline. Ma è meglio seguire l'implementazione a partire da writep(). Ripeto, il codice è diventato complicato a causa delle peculiarità del convenzione di passaggio degli argomenti, ma alcuni dettagli possono essere omessi. writep(fp) { register *rp, *ip, c;rp = fp; ip = rp->f_inode; c = u.u_count;loop: /* Se tutto è fatto, ritorna. */plock(ip); if(c == 0) { prele(ip); u.u_count = 0; return; }/* * Se non ci sono entrambe le estremità di lettura e scrittura del * tubo attive, restituire errore e segnale 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; }All'ingresso della pipeline vogliamo scrivere byte

u.u_count

. Prima di tutto richiederemo di bloccare il descrittore di indice (vedi sotto plockprele }/prele).

Poi controlliamo il contatore degli inode del collegamento. Finché entrambe le estremità del tubo sono aperte, il contatore deve essere uguale a 2. Manteniamo un collegamento (da rp->f_inode), quindi se il contatore sarà inferiore a 2, ciò dovrà significare che il processo di lettura ha chiuso la sua estremità del tubo. In altre parole, stiamo cercando di scrivere in un tubo chiuso, il che è un errore. Per la prima volta appare il codice di errore EPIPE e il segnale SIGPIPE nella sesta edizione di Unix.

Ma anche se il tubo è aperto, potrebbe essere pieno. In questo caso, rimuoviamo il blocco e andiamo a dormire nella speranza che un altro processo legga dal tubo e liberi sufficiente spazio al suo interno. Una volta svegliati, torniamo all'inizio, mettiamo di nuovo il blocco e avviamo un nuovo ciclo di scrittura.

Se nel tubo c'è spazio sufficiente, scriviamo i dati utilizzando writei(). Il parametro i_size1 dell'inode (può essere 0 nel caso di un tubo vuoto) indica la fine dei dati già presenti al suo interno. Se c'è abbastanza spazio per scrivere, possiamo riempire il tubo da i_size1 fino a PIPESIZ. Poi rimuoviamo il blocco e cerchiamo di risvegliare qualsiasi processo che sta aspettando di leggere dal tubo. Torniamo all'inizio per vedere se siamo riusciti a scrivere quanti più byte possibile. Se non ci siamo riusciti, avviamo un nuovo ciclo di scrittura.

Di solito il parametro i_mode dell'inode viene utilizzato per memorizzare i permessi r, w e x. Ma nel caso dei tubi segnaliamo l'attesa di un processo di scrittura o lettura con l'uso dei bit IREAD e IWRITE rispettivamente. Il processo imposta un flag e chiama sleep(), e ci si aspetta che in futuro un altro processo chiami wakeup().

La vera magia si svolge in sleep() e wakeup(). Sono implementati in slp.c, la fonte del famoso commento «Non sei obbligato a capire questo» (You are not expected to understand this). Fortunatamente, non siamo obbligati a comprendere il codice, basta guardare 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 determinato canale può essere successivamente risvegliato da un altro processo che chiama wakeup() per lo stesso canale. writep(fp) { register *rp, *ip, c;rp = fp; ip = rp->f_inode; c = u.u_count;loop: /* Se tutto è fatto, ritorna. */plock(ip); if(c == 0) { prele(ip); u.u_count = 0; return; }/* * Se non ci sono entrambe le estremità di lettura e scrittura del * tubo attive, restituire errore e segnale 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; } e pipe.c coordinano le loro azioni tramite tali chiamate a coppie. Si noti che legge i dati dalla pipeline. Ma è meglio seguire l'implementazione a partire da dà sempre la precedenza a PPIPE quando chiami sleep(), quindi tutti sleep() possono essere interrotti da un segnale.

Ora abbiamo tutto il necessario per capire la funzione pipe.c:

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

    rp = fp;
    ip = rp->f_inode;

loop:
    /* Blocco molto conservativo. */

    plock(ip);

    /*
     * Se l'intestazione (lettura) ha raggiunto
     * la coda (scrittura), ripristina 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 c'è sia il lettore che
         * lo scrittore attivi, restituisci senza
         * soddisfare la lettura.
         * */

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

    /* Leggi e restituisci */

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

Potrebbe esserti più facile leggere questa funzione dal basso verso l'alto. Il ramo "leggi e restituisci" viene solitamente utilizzato quando ci sono dati nel pipeline. In questo caso, con readi() leggiamo quanti più dati disponibili a partire dalla posizione attuale f_offset di lettura, e poi aggiorniamo il valore dello spostamento corrispondente.

In una lettura successiva, il pipeline sarà vuoto se lo spostamento di lettura ha raggiunto il valore i_size1 del inode. Ripristiniamo la posizione a 0 e cerchiamo di risvegliare qualsiasi processo che desidera scrivere nel pipeline. Sappiamo che quando il pipeline sarà pieno, writep(fp) { register *rp, *ip, c;rp = fp; ip = rp->f_inode; c = u.u_count;loop: /* Se tutto è fatto, ritorna. */plock(ip); if(c == 0) { prele(ip); u.u_count = 0; return; }/* * Se non ci sono entrambe le estremità di lettura e scrittura del * tubo attive, restituire errore e segnale 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; } si addormenterà su ip+1. Ora che il pipeline è vuoto, possiamo risvegliarlo perché riprenda il suo ciclo di scrittura.

Se non c'è nulla da leggere, può impostare un flag pipe.c e addormentarsi su IREAD ip+2 . Sappiamo che sarà risvegliatoquando scriverà alcuni dati nel pipeline. writep(fp) { register *rp, *ip, c;rp = fp; ip = rp->f_inode; c = u.u_count;loop: /* Se tutto è fatto, ritorna. */plock(ip); if(c == 0) { prele(ip); u.u_count = 0; return; }/* * Se non ci sono entrambe le estremità di lettura e scrittura del * tubo attive, restituire errore e segnale 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; }Commenti a

readi() e writei() aiuteranno a capire che invece di passare parametri attraverso « uuPer quanto riguarda il blocco "conservativo",

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

bloccano l'inode fino al termine del lavoro o fino a quando non ottengono il risultato (cioè chiamano pipe.c e writep(fp) { register *rp, *ip, c;rp = fp; ip = rp->f_inode; c = u.u_count;loop: /* Se tutto è fatto, ritorna. */plock(ip); if(c == 0) { prele(ip); u.u_count = 0; return; }/* * Se non ci sono entrambe le estremità di lettura e scrittura del * tubo attive, restituire errore e segnale 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; } wakeup plock()). prele() e funzionano semplicemente: con un altro insieme di chiamate sleep sleep e plock() All'inizio non riuscivo a capire perché

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

non chiami pipe.c prele(ip) prima di chiamare wakeup(ip+1) . La prima cosa chechiama nel suo ciclo è writep(fp) { register *rp, *ip, c;rp = fp; ip = rp->f_inode; c = u.u_count;loop: /* Se tutto è fatto, ritorna. */plock(ip); if(c == 0) { prele(ip); u.u_count = 0; return; }/* * Se non ci sono entrambe le estremità di lettura e scrittura del * tubo attive, restituire errore e segnale 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; } plock(ip) , che porta a un deadlock senon ha ancora rimosso il suo blocco, quindi il codice in qualche modo deve funzionare correttamente. Se guardiamo a pipe.c non ha ancora rilasciato il suo blocco, quindi il codice deve funzionare in qualche modo correttamente. Se si guarda a wakeup(), si capisce che segna solo il processo in attesa come pronto all'esecuzione, per poterlo eseguire in futuro sched() lo ha effettivamente avviato. Quindi pipe.c chiama wakeup(), rimuove il blocco, imposta IREAD e chiama sleep(ip+2)— tutto ciò prima che writep(fp) { register *rp, *ip, c;rp = fp; ip = rp->f_inode; c = u.u_count;loop: /* Se tutto è fatto, ritorna. */plock(ip); if(c == 0) { prele(ip); u.u_count = 0; return; }/* * Se non ci sono entrambe le estremità di lettura e scrittura del * tubo attive, restituire errore e segnale 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; } riprenda il ciclo.

Con questo si conclude la descrizione delle pipe nella sesta edizione. Un codice semplice, conseguenze lontane.

La settima edizione di Unix (gennaio 1979) è stata un nuovo rilascio principale (dopo quattro anni), che ha introdotto molte nuove applicazioni e caratteristiche del kernel. Ci sono stati anche notevoli cambiamenti riguardo l'uso di casting di tipo, unioni e puntatori fortemente tipizzati a strutture. Tuttavia il codice delle pipe è praticamente rimasto invariato. Possiamo saltare questa edizione.

Xv6, un semplice kernel simile a Unix

La creazione del kernel Xv6 è stata influenzata dalla sesta edizione di Unix, ma è scritta in C moderno per poterla eseguire su processori x86. Il codice è facile da leggere, è chiaro. Inoltre, a differenza dei sorgenti di Unix presenti in TUHS, puoi compilarlo, modificarlo e eseguirlo su qualcos'altro oltre al PDP 11/70. Perciò questo kernel è ampiamente utilizzato nelle università come materiale didattico sui sistemi operativi. I sorgenti sono disponibili su Github.

Il codice contiene un'implementazione chiara e ben pensata legge i dati dalla pipeline. Ma è meglio seguire l'implementazione a partire da, supportata da un buffer in memoria invece di un inode su disco. Qui fornisco solo la definizione di 'pipe strutturale' e la funzione 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() stabilisce lo stato di tutta l'altra implementazione, che include le funzioni piperead(), pipewrite() e pipeclose(). La chiamata di sistema effettiva sys_pipe è una wrapper implementata in sysfile.c. Consiglio di leggere l'intero codice. La complessità è a livello sorgente della sesta edizione, ma è molto più facile e piacevole da leggere.

Linux 0.01

Puoi trovare il codice sorgente di Linux 0.01. Sarà istruttivo studiare l'implementazione delle pipe in esso. fs/legge i dati dalla pipeline. Ma è meglio seguire l'implementazione a partire daQui per rappresentare la pipe viene utilizzato un inode, ma la pipe stessa è scritta in C moderno. Se hai già esaminato il codice della sesta edizione, qui non troverai 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) { /* nessun lettore */
        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, si può capire come viene utilizzato il contatore dei riferimenti inode per verificare se l'operazione di scrittura porta a SIGPIPE. Oltre al lavoro byte per byte, questa funzione è facilmente comparabile con le idee sopra descritte. Anche la logica sleep_on/wake_up non appare così estranea.

I moderni kernel Linux, FreeBSD, NetBSD, OpenBSD

Ho esaminato rapidamente alcuni kernel moderni. Nessuno di essi ha più un'implementazione basata su disco (non sorprende). In Linux c'è una propria implementazione. E anche se tre kernel BSD moderni contengono implementazioni basate su codice scritto da John Dyson, nel corso degli anni sono diventati troppo diversi tra loro.

Per leggere fs/legge i dati dalla pipeline. Ma è meglio seguire l'implementazione a partire da (su Linux) oppure sys/kern/sys_pipe.c (su *BSD), è necessaria una vera dedizione. Oggi nel codice sono importanti le prestazioni e il supporto per funzioni come le operazioni di input/output vettoriali e asincrone. E i dettagli sull'allocazione della memoria, i blocchi e la configurazione del kernel variano molto. Non è ciò di cui le università hanno bisogno per un corso introduttivo sui sistemi operativi.

In ogni caso, mi interessava scavare diversi schemi antichi (ad esempio, la generazione SIGPIPE e il ritorno EPIPE allo scrivere in una pipeline chiusa) in tutti questi moderni kernel così diversi. Probabilmente, non vedrò mai un computer PDP-11 dal vivo, ma c'è comunque molto da imparare dal codice scritto diversi anni prima della mia nascita.

L'articolo scritto da Divi Kapoor nel 2011 "The Linux Kernel Implementation of Pipes and FIFOs" è una panoramica su come funzionano (ancora oggi) le pipeline in Linux. E un recente commit in Linux illustra il modello di interazione della pipeline, le cui capacità superano quelle dei file temporanei; e mostra quanto siano lontane le pipeline dalla "molto conservativa gestione dei blocchi" nel kernel Unix della sesta edizione.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster