Cum sunt implementate conductele în Unix

Cum sunt implementate conductele în Unix
Acest articol descrie implementarea conductelor în nucleul Unix. Am fost oarecum dezamăgit că un articol recent intitulat „Cum funcționează conductele în Unix?" s-a adeverit nu despre structura internă. Am devenit curios și am săpat în surse vechi pentru a găsi răspunsul.

Despre ce vorbim?

Conductele sunt „probabil cea mai importantă invenție din Unix” - o caracteristică definitorie a filozofiei de bază a Unix de a pune împreună programe mici și sloganul familiar al liniei de comandă:

$ echo hello | wc -c
6

Această funcționalitate depinde de apelul de sistem furnizat de kernel pipe, care este descris pe paginile de documentație teava(7) и teava(2):

Conductele oferă un canal unidirecțional pentru comunicarea între procese. Conducta are o intrare (sfârșit de scriere) și o ieșire (capăt de citire). Datele scrise la intrarea conductei pot fi citite la ieșire.

Conducta este creată prin apelare pipe(2), care returnează doi descriptori de fișier: unul se referă la intrarea conductei, al doilea la ieșire.

Ieșirea de urmărire din comanda de mai sus arată crearea unei conducte și fluxul de date prin aceasta de la un proces la altul:

$ 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

Procesul părinte sună pipe()pentru a obține descriptori de fișiere atașați. Un proces copil scrie într-un descriptor, iar un alt proces citește aceleași date dintr-un alt descriptor. Shell-ul „redenimează” descriptorii 2 și 3 cu dup4 pentru a se potrivi cu stdin și stdout.

Fără conducte, shell-ul ar trebui să scrie rezultatul unui proces într-un fișier și să îl conducă către un alt proces pentru a citi datele din fișier. Ca rezultat, am irosi mai multe resurse și spațiu pe disc. Cu toate acestea, conductele sunt bune pentru mai mult decât pentru evitarea fișierelor temporare:

Dacă un proces încearcă să citească dintr-o conductă goală, atunci read(2) se va bloca până când datele sunt disponibile. Dacă un proces încearcă să scrie într-o conductă completă, atunci write(2) se va bloca până când au fost citite suficiente date din conductă pentru a finaliza scrierea.

La fel ca cerința POSIX, aceasta este o proprietate importantă: scrierea în conductă până la PIPE_BUF octeții (cel puțin 512) trebuie să fie atomici, astfel încât procesele să poată comunica între ele prin conductă într-un mod în care fișierele normale (care nu oferă astfel de garanții) nu pot.

Cu un fișier obișnuit, un proces își poate scrie toată ieșirea și o poate transmite altui proces. Sau procesele pot funcționa într-un mod greu paralel, folosind un mecanism de semnalizare extern (cum ar fi un semafor) pentru a se informa reciproc despre finalizarea unei scrieri sau citiri. Transportoarele ne salvează de toată această bătaie de cap.

Ce căutăm?

Îți voi explica pe degete pentru a-ți fi mai ușor să-ți imaginezi cum poate funcționa un transportor. Va trebui să alocați un buffer și o anumită stare în memorie. Veți avea nevoie de funcții pentru a adăuga și elimina date din buffer. Veți avea nevoie de anumite facilități pentru a apela funcții în timpul operațiunilor de citire și scriere pe descriptori de fișiere. Și sunt necesare încuietori pentru a implementa comportamentul special descris mai sus.

Suntem acum gata să interogăm codul sursă al nucleului la lumina puternică a lămpii pentru a confirma sau infirma modelul nostru mental vag. Dar fii mereu pregătit pentru neașteptat.

Unde căutăm?

Nu știu unde se află copia mea a celebrei cărți.Cartea Leii« cu codul sursă Unix 6, dar datorită Societatea Patrimoniului Unix poate fi căutat online cod sursa chiar și versiuni mai vechi de Unix.

Rătăcirea prin arhivele TUHS este ca și cum ai vizita un muzeu. Ne putem uita la istoria noastră comună și am respect pentru anii de efort de a recupera tot acest material din casete vechi și imprimate. Și sunt foarte conștient de acele fragmente care încă lipsesc.

După ce ne-am satisfăcut curiozitatea cu privire la istoria antică a conductelor, ne putem uita la nucleele moderne pentru comparație.

Apropo, pipe este numărul de apel de sistem 42 din tabel sysent[]. Coincidență?

Nuezele Unix tradiționale (1970–1974)

Nu am găsit nicio urmă pipe(2) nici in PDP-7 Unix (ianuarie 1970), nici în prima ediție Unix (noiembrie 1971), nici în cod sursă incomplet a doua editie (iunie 1972).

TUHS susține că a treia ediție Unix (februarie 1973) a fost prima versiune cu conducte:

A treia ediție de Unix a fost ultima versiune cu un nucleu scris în asamblare, dar și prima versiune cu pipeline. În cursul anului 1973, se lucrează la îmbunătățirea celei de-a treia ediții, nucleul a fost rescris în C și astfel s-a născut cea de-a patra ediție a Unix.

Un cititor a găsit o scanare a unui document în care Doug McIlroy a propus ideea de „conectare a programelor ca un furtun de grădină”.

Cum sunt implementate conductele în Unix
În cartea lui Brian KernighanUnix: O istorie și o memorie”, istoria apariției benzilor transportoare menționează și acest document: „... a atârnat pe perete în biroul meu de la Bell Labs timp de 30 de ani.” Aici interviu cu McIlroysi o alta poveste din Lucrarea lui McIlroy, scrisă în 2014:

Când a apărut Unix, pasiunea mea pentru coroutine m-a făcut să-i cer autorului sistemului de operare, Ken Thompson, să permită ca datele scrise într-un proces să ajungă nu numai la dispozitiv, ci și la ieșirea într-un alt proces. Ken a decis că este posibil. Cu toate acestea, ca minimalist, a vrut ca fiecare caracteristică a sistemului să joace un rol semnificativ. Este scrierea directă între procese într-adevăr un mare avantaj față de scrierea într-un fișier intermediar? Și numai când am făcut o propunere specifică cu numele captivant „pipeline” și o descriere a sintaxei interacțiunii proceselor, Ken a exclamat în cele din urmă: „O voi face!”.

Și a făcut. Într-o seară fatidică, Ken a schimbat nucleul și shell-ul, a reparat mai multe programe standard pentru a standardiza modul în care acceptă intrarea (care ar putea veni de la o conductă) și a schimbat numele fișierelor. A doua zi, conductele au fost utilizate pe scară largă în aplicații. Până la sfârșitul săptămânii, secretarele le foloseau pentru a trimite documente de la procesoarele de text la imprimantă. Ceva mai târziu, Ken a înlocuit API-ul și sintaxa originale pentru a include utilizarea conductelor cu convenții mai curate care au fost folosite de atunci.

Din păcate, codul sursă pentru a treia ediție a nucleului Unix a fost pierdut. Și deși avem codul sursă al nucleului scris în C a patra editie, care a fost lansat în noiembrie 1973, dar a apărut cu câteva luni înainte de lansarea oficială și nu conține implementarea conductelor. Este păcat că codul sursă pentru această caracteristică legendară Unix s-a pierdut, poate pentru totdeauna.

Avem text de documentație pentru pipe(2) din ambele versiuni, așa că puteți începe prin a căuta în documentație A treia editie (pentru anumite cuvinte, subliniate „manual”, un șir de ^H literale urmate de un caracter de subliniere!). Acest proto-pipe(2) este scris în asamblator și returnează un singur descriptor de fișier, dar oferă deja funcționalitatea de bază așteptată:

Apel de sistem ţeavă creează un mecanism I/O numit conductă. Descriptorul de fișier returnat poate fi utilizat pentru operațiuni de citire și scriere. Când ceva este scris în conductă, acesta stochează până la 504 octeți de date, după care procesul de scriere este suspendat. La citirea din conductă, sunt preluate datele stocate în tampon.

Până în anul următor, nucleul fusese rescris în C și pipe(2) ediția a patra și-a dobândit aspectul modern cu prototipul "pipe(fildes)»:

Apel de sistem ţeavă creează un mecanism I/O numit conductă. Descriptorii de fișier returnați pot fi utilizați în operațiuni de citire și scriere. Când ceva este scris în pipeline, se folosește descriptorul returnat în r1 (respectiv fildes[1]), tamponat până la 4096 octeți de date, după care procesul de scriere este suspendat. La citirea din pipeline, descriptorul revenit la r0 (respectiv fildes[0]) preia datele.

Se presupune că odată ce o conductă a fost definită, două (sau mai multe) procese care interacționează (create prin invocări ulterioare furculiţă) va transmite date din conductă folosind apeluri citit и scrie.

Shell-ul are o sintaxă pentru definirea unei rețele liniare de procese conectate printr-o conductă.

Apelurile de citire dintr-o conductă goală (care nu conține date în buffer) care are un singur capăt (toți descriptorii de fișiere de scriere închise) returnează „sfârșitul fișierului”. Apelurile de scriere într-o situație similară sunt ignorate.

cel mai devreme implementarea conductei păstrate se aplică la cea de-a cincea ediție a Unix (iunie 1974), dar este aproape identic cu cel care a apărut în următoarea ediție. Au adăugat doar comentarii, astfel încât cea de-a cincea ediție poate fi omisă.

Ediția a șasea Unix (1975)

Începe să citească codul sursă Unix a șasea ediție (mai 1975). În mare parte datorită Lions este mult mai ușor de găsit decât sursele versiunilor anterioare:

De mulți ani cartea Lions a fost singurul document de pe nucleul Unix disponibil în afara Bell Labs. Deși licența ediției a șasea permitea profesorilor să folosească codul sursă, licența ediției a șaptea excludea această posibilitate, astfel că cartea a fost distribuită în copii ilegale dactilografiate.

Astăzi puteți cumpăra o copie retipărită a cărții, a cărei coperta îi înfățișează pe elevi la copiator. Și datorită lui Warren Toomey (care a început proiectul TUHS), puteți descărca Ediția a șasea Sursă PDF. Vreau să vă dau o idee despre cât de mult efort a depus în crearea fișierului:

Cu peste 15 ani în urmă, am tastat o copie a codului sursă furnizat în Lionspentru că nu mi-a plăcut calitatea copiei mele dintr-un număr necunoscut de alte copii. TUHS nu exista încă și nu aveam acces la sursele vechi. Dar în 1988 am găsit o bandă veche cu 9 piese care avea o copie de rezervă de la un computer PDP11. Era greu de știut dacă a funcționat, dar a existat un arbore /usr/src/ intact în care majoritatea fișierelor erau marcate 1979, care chiar și atunci părea vechi. Era a șaptea ediție sau un derivat PWB, m-am gândit.

Am luat descoperirea ca bază și am editat manual sursele la starea celei de-a șasea ediții. O parte a codului a rămas aceeași, o parte a trebuit să fie ușor editată, schimbând tokenul modern += cu =+ învechit. Ceva a fost pur și simplu șters și ceva a trebuit să fie rescris complet, dar nu prea mult.

Și astăzi putem citi online la TUHS codul sursă al celei de-a șasea ediții a arhivă, la care a avut o mână de ajutor Dennis Ritchie.

Apropo, la prima vedere, principala caracteristică a codului C înainte de perioada lui Kernighan și Ritchie este concizie. Nu se întâmplă adesea să pot introduce fragmente de cod fără o editare extinsă pentru a se potrivi într-o zonă de afișare relativ îngustă de pe site-ul meu.

Devreme /usr/sys/ken/pipe.c există un comentariu explicativ (și da, există mai multe /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

Dimensiunea tamponului nu s-a schimbat de la a patra ediție. Dar aici vedem, fără nicio documentare publică, că conductele au folosit cândva fișierele ca stocare alternativă!

În ceea ce privește fișierele LARG, acestea corespund inode-steag LARG, care este folosit de „algoritmul de adresare mare” pentru procesare blocuri indirecte pentru a suporta sisteme de fișiere mai mari. Din moment ce Ken a spus că este mai bine să nu le folosesc, sunt bucuros să-l cred pe cuvânt.

Aici este adevăratul apel de sistem 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;
}

Comentariul descrie clar ce se întâmplă aici. Dar nu este atât de ușor de înțeles codul, parțial din cauza modului în care "struct user u» și înregistrări R0 и R1 parametrii de apel de sistem și valorile returnate sunt transmise.

Să încercăm cu ialloc() plasați pe disc inod (inod), și cu ajutorul falloc() - depozitați două fişier. Dacă totul merge bine, vom seta steaguri pentru a identifica aceste fișiere ca cele două capete ale conductei, le vom îndrepta către același inod (al cărui număr de referință devine 2) și vom marca inodul ca modificat și în uz. Acordați atenție solicitărilor către eu pun() în căile de eroare pentru a reduce numărul de referințe în noul inod.

pipe() datorat prin R0 и R1 returnează numerele de descriptor ale fișierului pentru citire și scriere. falloc() returnează un pointer către o structură de fișier, dar și „returnează” prin u.u_ar0[R0] și un descriptor de fișier. Adică codul este stocat în r descriptor de fișier pentru citire și atribuie un descriptor pentru scriere direct din u.u_ar0[R0] după al doilea apel falloc().

pavilion FPIPE, pe care l-am setat la crearea conductei, controlează comportamentul funcției rdwr() în sys2.c, care apelează rutine I/O specifice:

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

Apoi funcția readp() в pipe.c citește datele din conductă. Dar este mai bine să urmăriți implementarea pornind de la writep(). Din nou, codul a devenit mai complicat din cauza naturii convenției de trecere a argumentelor, dar unele detalii pot fi omise.

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

Vrem să scriem octeți în intrarea conductei u.u_count. Mai întâi trebuie să blocăm inodul (vezi mai jos plock/prele).

Apoi verificăm numărul de referințe ale inodului. Atâta timp cât ambele capete ale conductei rămân deschise, contorul ar trebui să fie 2. Ne ținem de o legătură (de la rp->f_inode), deci dacă contorul este mai mic de 2, atunci acest lucru ar trebui să însemne că procesul de citire și-a închis capătul conductei. Cu alte cuvinte, încercăm să scriem la o conductă închisă, ceea ce este o greșeală. Primul cod de eroare EPIPE și semnal SIGPIPE a apărut în cea de-a șasea ediție a Unix.

Dar chiar dacă transportorul este deschis, acesta poate fi plin. În acest caz, eliberăm blocarea și ne culcăm în speranța că un alt proces va citi din conductă și va elibera suficient spațiu în ea. Când ne trezim, revenim la început, închidem din nou lacătul și începem un nou ciclu de scriere.

Dacă există suficient spațiu liber în conductă, atunci scriem date în el folosind scrie(). Parametru i_size1 inode'a (cu o conductă goală poate fi egală cu 0) indică la sfârșitul datelor pe care le conține deja. Dacă există suficient spațiu pentru a scrie, putem umple conducta de la i_size1 la PIPESIZ. Apoi eliberăm blocarea și încercăm să trezim orice proces care așteaptă să fie citit din conductă. Ne întoarcem la început pentru a vedea dacă am reușit să scriem câte octeți aveam nevoie. Dacă nu, atunci începem un nou ciclu de înregistrare.

De obicei parametru i_mode inode este folosit pentru a stoca permisiunile r, w и x. Dar în cazul conductelor, semnalăm că un proces așteaptă o scriere sau o citire folosind biți IREAD и IWRITE respectiv. Procesul setează steag și apelează sleep(), și este de așteptat ca în viitor să apeleze un alt proces wakeup().

Adevărata magie se întâmplă în sleep() и wakeup(). Sunt implementate în slp.c, sursa celebrului comentariu „Nu ești de așteptat să înțelegi asta”. Din fericire, nu trebuie să înțelegem codul, doar uită-te la câteva comentarii:

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

Procesul care cheamă sleep() pentru un anumit canal, poate fi trezit ulterior de un alt proces, care va apela wakeup() pentru acelasi canal. writep() и readp() coordonează acțiunile lor prin astfel de apeluri pereche. Rețineți că pipe.c prioritizează întotdeauna PPIPE când chemat sleep(), deci toate sleep() poate fi întreruptă de un semnal.

Acum avem totul pentru a înțelege funcția 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);
}

S-ar putea să vă fie mai ușor să citiți această funcție de jos în sus. Ramura „citire și returnare” este de obicei folosită atunci când există anumite date în conductă. În acest caz, folosim citit() citiți câte date sunt disponibile începând de la cea actuală f_offset citiți și apoi actualizați valoarea offset-ului corespunzător.

La citirile ulterioare, conducta va fi goală dacă a atins decalajul de citire i_size1 la inod. Resetăm poziția la 0 și încercăm să trezim orice proces care dorește să scrie în conductă. Știm că atunci când transportorul este plin, writep() adormi pe ip+1. Și acum că conducta este goală, o putem trezi pentru a-și relua ciclul de scriere.

Dacă nu este nimic de citit, atunci readp() poate pune un steag IREAD și adormi pe ip+2. Știm ce îl va trezi writep()când scrie unele date în conductă.

Comentarii la citește() și scrie() vă va ajuta să înțelegeți că, în loc să treceți parametrii prin "u» le putem trata ca pe niște funcții I/O obișnuite care preiau un fișier, o poziție, un buffer în memorie și numără numărul de octeți de citit sau de scris.

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

Cât despre blocarea „conservatoare”, atunci readp() и writep() blocați inodurile până când se termină sau obțin un rezultat (adică apelați wakeup). plock() и prele() lucrează simplu: folosind un set diferit de apeluri sleep и wakeup permiteți-ne să trezim orice proces care are nevoie de blocarea pe care tocmai am lansat-o:

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

La început nu am înțeles de ce readp() nu provoacă prele(ip) înainte de apel wakeup(ip+1). Primul lucru writep() apelează în bucla, aceasta plock(ip), ceea ce duce la un impas dacă readp() încă nu și-a eliminat blocul, așa că codul trebuie cumva să funcționeze corect. Daca te uiti la wakeup(), devine clar că marchează doar procesul de somn ca fiind gata de execuție, astfel încât în ​​viitor sched() chiar l-a lansat. Asa de readp() cauze wakeup(), deblochează, setează IREAD și apeluri sleep(ip+2)- toate acestea înainte writep() repornește ciclul.

Aceasta completează descrierea conductelor din a șasea ediție. Cod simplu, implicații de anvergură.

Ediția a șaptea Unix (ianuarie 1979) a fost o nouă lansare majoră (patru ani mai târziu) care a introdus multe aplicații noi și caracteristici ale nucleului. De asemenea, a suferit modificări semnificative în legătură cu utilizarea turnării tipului, a îmbinărilor și a indicatoarelor tipizate către structuri. in orice caz codul conductelor practic nu s-a schimbat. Putem sări peste această ediție.

Xv6, un nucleu simplu asemănător Unix

Pentru a crea un nucleu Xv6 influențat de cea de-a șasea ediție a Unix, dar scris în C modern pentru a rula pe procesoare x86. Codul este ușor de citit și de înțeles. De asemenea, spre deosebire de sursele Unix cu TUHS, îl puteți compila, modifica și rula pe altceva decât PDP 11/70. Prin urmare, acest nucleu este utilizat pe scară largă în universități ca material didactic despre sistemele de operare. Surse sunt pe Github.

Codul conține o implementare clară și atentă conductă.c, susținut de un buffer în memorie în loc de un inod de pe disc. Aici dau doar definiția „conductei structurale” și a funcției 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() stabilește starea restului implementării, care include funcții piperead(), pipewrite() и pipeclose(). Apelul de sistem propriu-zis sys_pipe este un wrapper implementat în sysfile.c. Recomand să citești tot codul său. Complexitatea este la nivelul codului sursă al celei de-a șasea ediții, dar este mult mai ușor și mai plăcut de citit.

Linux 0.01

Puteți găsi codul sursă pentru Linux 0.01. Va fi instructiv să studiezi implementarea conductelor în el fs/pipe.c. Aici, un inode este folosit pentru a reprezenta conducta, dar conducta în sine este scrisă în C modern. Dacă v-ați spart drumul prin codul ediției a șasea, nu veți avea probleme aici. Așa arată funcția 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;
}

Chiar și fără să te uiți la definițiile structurii, poți să-ți dai seama cum este folosit numărul de referințe inode pentru a verifica dacă o operație de scriere are ca rezultat SIGPIPE. Pe lângă lucrul octet cu octet, această funcție este ușor de comparat cu ideile de mai sus. Chiar și logica sleep_on/wake_up nu pare atât de străin.

Kernel-uri Linux moderne, FreeBSD, NetBSD, OpenBSD

Am trecut rapid peste niște nuclee moderne. Niciuna dintre ele nu are deja o implementare pe disc (nu este surprinzător). Linux are propria sa implementare. Și deși cele trei nuclee BSD moderne conțin implementări bazate pe cod care a fost scris de John Dyson, de-a lungul anilor au devenit prea diferiți unul de celălalt.

A citi fs/pipe.c (pe Linux) sau sys/kern/sys_pipe.c (pe *BSD), este nevoie de dedicare reală. Performanța și suportul pentru caracteristici precum vector și I/O asincron sunt importante în codul de astăzi. Iar detaliile alocării memoriei, blocărilor și configurației kernelului variază foarte mult. Nu de asta au nevoie universitățile pentru un curs introductiv privind sistemele de operare.

În orice caz, a fost interesant pentru mine să descopăr câteva modele vechi (de exemplu, generarea SIGPIPE și întoarce-te EPIPE când scrieți într-o conductă închisă) în toate aceste nuclee, atât de diferite, moderne. Probabil că nu voi vedea niciodată un computer PDP-11 live, dar mai sunt multe de învățat din codul care a fost scris cu câțiva ani înainte de a mă naște.

Scris de Divi Kapoor în 2011, articolul „Implementarea nucleului Linux a conductelor și FIFO-uriloreste o prezentare generală a modului în care funcționează conductele Linux (până acum). A comisie recentă pe Linux ilustrează modelul pipeline de interacțiune, ale cărui capacități le depășesc pe cele ale fișierelor temporare; și arată, de asemenea, cât de departe au mers conductele de la „blocarea foarte conservatoare” în a șasea ediție a nucleului Unix.

Sursa: www.habr.com

Adauga un comentariu