Paano ipinapatupad ang mga pipeline sa Unix

Paano ipinapatupad ang mga pipeline sa Unix
Inilalarawan ng artikulong ito ang pagpapatupad ng mga pipeline sa Unix kernel. Medyo nabigo ako na ang isang kamakailang artikulo na pinamagatang "Paano gumagana ang mga pipeline sa Unix?Β» lumabas hindi tungkol sa panloob na istraktura. Na-curious ako at naghanap ng sagot sa mga lumang source.

Tungkol Saan yan?

Ang mga pipeline ay "marahil ang pinakamahalagang imbensyon sa Unix" - isang pagtukoy sa katangian ng pinagbabatayan ng pilosopiya ng Unix sa pagsasama-sama ng maliliit na programa, at ang pamilyar na command-line slogan:

$ echo hello | wc -c
6

Nakadepende ang functionality na ito sa system call na ibinigay ng kernel pipe, na inilalarawan sa mga pahina ng dokumentasyon tubo(7) ΠΈ tubo(2):

Ang mga pipeline ay nagbibigay ng one-way na channel para sa inter-process na komunikasyon. Ang pipeline ay may input (write end) at isang output (read end). Ang data na nakasulat sa input ng pipeline ay mababasa sa output.

Ang pipeline ay nilikha sa pamamagitan ng pagtawag pipe(2), na nagbabalik ng dalawang descriptor ng file: ang isa ay tumutukoy sa input ng pipeline, ang pangalawa sa output.

Ang bakas na output mula sa utos sa itaas ay nagpapakita ng paglikha ng isang pipeline at ang daloy ng data sa pamamagitan nito mula sa isang proseso patungo sa isa pa:

$ 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

Ang proseso ng magulang ay tumatawag pipe()upang makakuha ng mga kalakip na deskriptor ng file. Nagsusulat ang isang proseso ng bata sa isang descriptor at ang isa pang proseso ay nagbabasa ng parehong data mula sa isa pang descriptor. Ang shell ay "pinangalanan" ang mga deskriptor 2 at 3 na may dup4 upang tumugma sa stdin at stdout.

Kung walang mga pipeline, kailangang isulat ng shell ang output ng isang proseso sa isang file at i-pipe ito sa isa pang proseso upang mabasa ang data mula sa file. Bilang resulta, mag-aaksaya kami ng mas maraming mapagkukunan at espasyo sa disk. Gayunpaman, ang mga pipeline ay mabuti para sa higit pa sa pag-iwas sa mga pansamantalang file:

Kung ang isang proseso ay sumusubok na magbasa mula sa isang walang laman na pipeline, kung gayon read(2) haharangan hanggang sa maging available ang data. Kung ang isang proseso ay sumusubok na sumulat sa isang buong pipeline, kung gayon write(2) haharangan hanggang sa sapat na data ang nabasa mula sa pipeline upang makumpleto ang pagsulat.

Tulad ng kinakailangan ng POSIX, ito ay isang mahalagang katangian: pagsulat sa pipeline hanggang sa PIPE_BUF Ang mga byte (hindi bababa sa 512) ay dapat na atomic upang ang mga proseso ay maaaring makipag-ugnayan sa isa't isa sa pamamagitan ng pipeline sa paraang hindi magagawa ng mga normal na file (na hindi nagbibigay ng mga ganoong garantiya).

Sa isang regular na file, maaaring isulat ng isang proseso ang lahat ng output nito dito at ipasa ito sa isa pang proseso. O ang mga proseso ay maaaring gumana sa isang hard parallel mode, gamit ang isang panlabas na mekanismo ng pagbibigay ng senyas (tulad ng isang semaphore) upang ipaalam sa isa't isa ang tungkol sa pagkumpleto ng isang pagsulat o pagbasa. Iniligtas tayo ng mga conveyor mula sa lahat ng abala na ito.

Ano ang hinahanap natin?

Ipapaliwanag ko sa aking mga daliri para mas madali mong isipin kung paano gumagana ang isang conveyor. Kakailanganin mong maglaan ng buffer at ilang estado sa memorya. Kakailanganin mo ang mga function upang magdagdag at mag-alis ng data mula sa buffer. Kakailanganin mo ang ilang pasilidad upang tumawag sa mga function sa panahon ng pagbabasa at pagsulat ng mga operasyon sa mga deskriptor ng file. At kailangan ang mga kandado upang maipatupad ang espesyal na gawi na inilarawan sa itaas.

Handa na kaming tanungin ang source code ng kernel sa ilalim ng maliwanag na lamplight para kumpirmahin o pabulaanan ang aming malabong mental na modelo. Ngunit laging maging handa sa hindi inaasahan.

Saan tayo nakatingin?

Hindi ko alam kung saan nakalagay ang kopya ko ng sikat na libro.Libro ng mga leonΒ« gamit ang Unix 6 source code, ngunit salamat sa Ang Unix Heritage Society maaaring hanapin online source code kahit na mas lumang bersyon ng Unix.

Ang paggala sa TUHS archive ay parang pagbisita sa isang museo. Maaari nating tingnan ang ating ibinahaging kasaysayan at iginagalang ko ang mga taon ng pagsisikap na mabawi ang lahat ng materyal na ito nang paunti-unti mula sa mga lumang cassette at printout. At lubos kong nalalaman ang mga fragment na iyon na nawawala pa rin.

Dahil nasiyahan ang aming pagkamausisa tungkol sa sinaunang kasaysayan ng mga pipeline, maaari naming tingnan ang mga modernong core para sa paghahambing.

Sa pamamagitan ng paraan, pipe ay system call number 42 sa talahanayan sysent[]. Pagkakataon?

Tradisyonal na Unix kernels (1970–1974)

Wala akong nakitang bakas pipe(2) ni sa PDP-7 Unix (Enero 1970), o sa unang edisyon Unix (Nobyembre 1971), o sa hindi kumpletong source code ikalawang edisyon (Hunyo 1972).

Sinasabi iyon ng TUHS ikatlong edisyon Unix (Pebrero 1973) ay ang unang bersyon na may mga pipeline:

Ang ikatlong edisyon ng Unix ay ang huling bersyon na may kernel na nakasulat sa assembler, ngunit din ang unang bersyon na may mga pipeline. Noong 1973, ang trabaho ay isinasagawa upang pahusayin ang ikatlong edisyon, ang kernel ay muling isinulat sa C, at sa gayon ay ipinanganak ang ikaapat na edisyon ng Unix.

Natagpuan ng isang mambabasa ang isang pag-scan ng isang dokumento kung saan iminungkahi ni Doug McIlroy ang ideya ng "pagkonekta ng mga programa tulad ng isang hose sa hardin."

Paano ipinapatupad ang mga pipeline sa Unix
Sa aklat ni Brian KernighanUnix: Isang Kasaysayan at Isang Memoir”, binanggit din ng kasaysayan ng paglitaw ng mga conveyor ang dokumentong ito: β€œ... nakabitin ito sa dingding sa aking opisina sa Bell Labs sa loob ng 30 taon.” Dito panayam kay McIlroyat isa pang kwento mula sa Ang gawa ni McIlroy, na isinulat noong 2014:

Nang lumitaw ang Unix, ang hilig ko sa mga coroutine ay nagtanong sa may-akda ng OS, si Ken Thompson, na payagan ang data na nakasulat sa ilang proseso na pumunta hindi lamang sa device, kundi pati na rin sa exit sa isa pang proseso. Akala ni Ken pwede. Gayunpaman, bilang isang minimalist, gusto niyang ang bawat feature ng system ay may mahalagang papel. Ang direktang pagsulat ba sa pagitan ng mga proseso ay talagang isang malaking kalamangan sa pagsusulat sa isang intermediate na file? At kapag gumawa ako ng isang partikular na panukala na may kaakit-akit na pangalan na "pipeline" at isang paglalarawan ng syntax ng pakikipag-ugnayan ng mga proseso, sa wakas ay bumulalas si Ken: "Gagawin ko ito!".

At ginawa. Isang nakamamatay na gabi, binago ni Ken ang kernel at shell, inayos ang ilang karaniwang mga programa upang i-standardize kung paano sila tumatanggap ng input (na maaaring nagmula sa isang pipeline), at binago ang mga filename. Kinabukasan, ang mga pipeline ay napakalawak na ginamit sa mga aplikasyon. Sa pagtatapos ng linggo, ginamit sila ng mga kalihim para magpadala ng mga dokumento mula sa mga word processor patungo sa printer. Maya-maya, pinalitan ni Ken ang orihinal na API at syntax para sa pagbabalot ng paggamit ng mga pipeline na may mas malinis na mga convention na ginamit mula noon.

Sa kasamaang palad, nawala ang source code para sa ikatlong edisyon ng Unix kernel. At kahit na mayroon kaming kernel source code na nakasulat sa C ikaapat na edisyon, na inilabas noong Nobyembre 1973, ngunit lumabas ito ilang buwan bago ang opisyal na pagpapalabas at hindi naglalaman ng pagpapatupad ng mga pipeline. Nakakalungkot lang na ang source code para sa maalamat na tampok na Unix na ito ay nawala, marahil magpakailanman.

Mayroon kaming dokumentasyong teksto para sa pipe(2) mula sa parehong mga release, para makapagsimula ka sa paghahanap sa dokumentasyon ikatlong edisyon (para sa ilang partikular na salita, may salungguhit na "manu-mano", isang string ng ^H literal na sinusundan ng underscore!). Ang proto-pipe(2) ay nakasulat sa assembler at nagbabalik lamang ng isang file descriptor, ngunit nagbibigay na ng inaasahang pangunahing pag-andar:

System call tubo lumilikha ng mekanismo ng I/O na tinatawag na pipeline. Ang ibinalik na deskriptor ng file ay maaaring gamitin para sa mga operasyon sa pagbasa at pagsulat. Kapag may isinulat sa pipeline, nag-buffer ito ng hanggang 504 bytes ng data, pagkatapos nito ay sinuspinde ang proseso ng pagsulat. Kapag nagbabasa mula sa pipeline, kinukuha ang buffered data.

Sa sumunod na taon, ang kernel ay muling naisulat sa C, at pipe(2) ikaapat na edisyon nakuha ang modernong hitsura nito gamit ang prototype "pipe(fildes)Β»:

System call tubo lumilikha ng mekanismo ng I/O na tinatawag na pipeline. Ang ibinalik na mga deskriptor ng file ay maaaring gamitin sa mga operasyon sa pagbasa at pagsulat. Kapag may isinulat sa pipeline, ginagamit ang descriptor na ibinalik sa r1 (resp. fildes[1]), na na-buffer ng hanggang 4096 bytes ng data, pagkatapos nito ay sinuspinde ang proseso ng pagsulat. Kapag nagbabasa mula sa pipeline, bumalik ang descriptor sa r0 (resp. fildes[0]) na kumukuha ng data.

Ipinapalagay na kapag natukoy na ang isang pipeline, dalawa (o higit pa) na mga prosesong nakikipag-ugnayan (nagawa ng mga kasunod na invocations tinidor) ay magpapasa ng data mula sa pipeline gamit ang mga tawag basahin ΠΈ magsulat.

Ang shell ay may syntax para sa pagtukoy ng isang linear array ng mga proseso na konektado sa pamamagitan ng pipeline.

Ang mga tawag para magbasa mula sa isang walang laman na pipeline (na naglalaman ng walang buffered na data) na may isang dulo lamang (lahat ng write file descriptor sarado) ay nagbabalik ng "end of file". Ang pagsusulat ng mga tawag sa isang katulad na sitwasyon ay hindi pinapansin.

pinakauna napanatili ang pagpapatupad ng pipeline naaangkop hanggang sa ikalimang edisyon ng Unix (Hunyo 1974), ngunit halos kapareho ito ng lumabas sa susunod na paglabas. Idinagdag lamang ang mga komento, kaya maaaring laktawan ang ikalimang edisyon.

Unix Sixth Edition (1975)

Nagsisimulang basahin ang Unix source code ikaanim na edisyon (Mayo 1975). Malaking salamat sa Lions mas madaling mahanap kaysa sa mga pinagmumulan ng mga naunang bersyon:

Sa loob ng maraming taon ang libro Lions ay ang tanging dokumento sa Unix kernel na magagamit sa labas ng Bell Labs. Bagama't pinahintulutan ng lisensya ng ikaanim na edisyon ang mga guro na gamitin ang source code nito, hindi isinama ng lisensya ng ikapitong edisyon ang posibilidad na ito, kaya ipinamahagi ang aklat sa mga ilegal na makinilya na kopya.

Ngayon ay maaari kang bumili ng kopya ng muling pag-print ng aklat, na ang pabalat nito ay nagpapakita ng mga mag-aaral sa copier. At salamat kay Warren Toomey (na nagsimula ng proyekto ng TUHS), maaari mong i-download Pang-anim na Edisyon Source PDF. Nais kong bigyan ka ng ideya kung gaano karaming pagsisikap ang ginawa sa paglikha ng file:

Mahigit 15 taon na ang nakalipas, nag-type ako ng kopya ng source code na ibinigay sa Lionsdahil hindi ko nagustuhan ang kalidad ng aking kopya mula sa hindi kilalang bilang ng iba pang mga kopya. Wala pa ang TUHS, at wala akong access sa mga lumang source. Ngunit noong 1988 nakakita ako ng lumang tape na may 9 na track na may backup mula sa isang PDP11 na computer. Mahirap malaman kung gumagana ito, ngunit mayroong isang buo na /usr/src/ puno kung saan ang karamihan sa mga file ay minarkahan ng 1979, na kahit noon ay mukhang sinaunang. Iyon ay ang ikapitong edisyon, o isang PWB derivative, naisip ko.

Kinuha ko ang paghahanap bilang batayan at manu-manong na-edit ang mga mapagkukunan sa estado ng ikaanim na edisyon. Ang bahagi ng code ay nanatiling pareho, ang bahagi ay kailangang bahagyang i-edit, binabago ang modernong token += sa hindi na ginagamit na =+. May na-delete lang, at kailangang ganap na isulat muli, ngunit hindi masyado.

At ngayon mababasa natin online sa TUHS ang source code ng ikaanim na edisyon ng archive, kung saan may kamay si Dennis Ritchie.

Sa pamamagitan ng paraan, sa unang tingin, ang pangunahing tampok ng C-code bago ang panahon ng Kernighan at Ritchie ay ang kaiklian. Hindi madalas na nakakapagpasok ako ng mga snippet ng code nang walang malawak na pag-edit upang magkasya sa isang medyo makitid na lugar ng display sa aking site.

Maagang /usr/sys/ken/pipe.c mayroong isang paliwanag na komento (at oo, mayroon pa /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

Ang laki ng buffer ay hindi nagbago mula noong ika-apat na edisyon. Ngunit dito nakikita natin, nang walang anumang pampublikong dokumentasyon, na ang mga pipeline ay minsang gumamit ng mga file bilang fallback storage!

Tulad ng para sa mga file na LARG, tumutugma sila sa inode-flag LARG, na ginagamit ng "malaking addressing algorithm" upang iproseso hindi direktang mga bloke upang suportahan ang mas malalaking file system. Dahil sinabi ni Ken na mas mabuting huwag na lang gamitin ang mga ito, I'm happy to take his word for it.

Narito ang tunay na tawag sa 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;
}

Ang komento ay malinaw na naglalarawan kung ano ang nangyayari dito. Ngunit hindi ganoon kadaling maunawaan ang code, bahagyang dahil sa kung paano "struct user uΒ»at nagrerehistro R0 ΠΈ R1 naipasa ang mga parameter ng system call at return value.

Subukan natin ialloc() ilagay sa disk inode (inode), at sa tulong falloc() - tindahan ng dalawa file. Kung magiging maayos ang lahat, magtatakda kami ng mga flag para tukuyin ang mga file na ito bilang dalawang dulo ng pipeline, ituro ang mga ito sa parehong inode (na ang bilang ng reference ay nagiging 2), at markahan ang inode bilang binago at ginagamit. Bigyang-pansin ang mga kahilingan sa nilagay ko() sa mga error path upang bawasan ang bilang ng sanggunian sa bagong inode.

pipe() dahil sa pamamagitan ng R0 ΠΈ R1 ibalik ang mga numero ng deskriptor ng file para sa pagbabasa at pagsulat. falloc() nagbabalik ng pointer sa isang istraktura ng file, ngunit "nagbabalik" din sa pamamagitan ng u.u_ar0[R0] at isang file descriptor. Iyon ay, ang code ay naka-imbak sa r file descriptor para sa pagbabasa at nagtatalaga ng descriptor para sa pagsusulat nang direkta mula sa u.u_ar0[R0] pagkatapos ng pangalawang tawag falloc().

I-flag FPIPE, na itinakda namin kapag lumilikha ng pipeline, kumokontrol sa pag-uugali ng function rdwr() sa sys2.c, na tumatawag sa mga partikular na I/O na gawain:

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

Pagkatapos ang function readp() Π² pipe.c nagbabasa ng data mula sa pipeline. Ngunit mas mahusay na subaybayan ang pagpapatupad simula sa writep(). Muli, ang code ay naging mas kumplikado dahil sa likas na katangian ng argumentong pumasa sa convention, ngunit ang ilang mga detalye ay maaaring tanggalin.

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

Gusto naming magsulat ng mga byte sa input ng pipeline u.u_count. Una kailangan nating i-lock ang inode (tingnan sa ibaba plock/prele).

Pagkatapos ay suriin namin ang bilang ng sanggunian ng inode. Hangga't mananatiling bukas ang magkabilang dulo ng pipeline, dapat na 2 ang counter. Kumapit kami sa isang link (mula sa rp->f_inode), kaya kung ang counter ay mas mababa sa 2, nangangahulugan ito na ang proseso ng pagbabasa ay isinara ang dulo nito ng pipeline. Sa madaling salita, sinusubukan naming sumulat sa isang saradong pipeline, na isang pagkakamali. Unang error code EPIPE at signal SIGPIPE lumitaw sa ikaanim na edisyon ng Unix.

Ngunit kahit na bukas ang conveyor, maaari itong puno. Sa kasong ito, pinakawalan namin ang lock at natutulog sa pag-asa na ang isa pang proseso ay magbabasa mula sa pipeline at magbakante ng sapat na espasyo sa loob nito. Kapag nagising tayo, bumalik tayo sa umpisa, isasabit muli ang lock at magsimula ng bagong yugto ng pagsulat.

Kung mayroong sapat na libreng espasyo sa pipeline, pagkatapos ay isusulat namin ang data dito gamit writei(). Parameter i_size1 ang inode'a (na may walang laman na pipeline ay maaaring katumbas ng 0) ay tumuturo sa dulo ng data na naglalaman na nito. Kung may sapat na espasyo para magsulat, maaari naming punan ang pipeline mula sa i_size1 sa PIPESIZ. Pagkatapos ay ilalabas namin ang lock at subukang gisingin ang anumang proseso na naghihintay na basahin mula sa pipeline. Bumalik kami sa simula upang makita kung nagawa naming magsulat ng maraming byte hangga't kailangan namin. Kung hindi, magsisimula kami ng bagong ikot ng pag-record.

Karaniwang parameter i_mode inode ay ginagamit upang mag-imbak ng mga pahintulot r, w ΠΈ x. Ngunit sa kaso ng mga pipeline, senyales namin na ang ilang proseso ay naghihintay para sa pagsulat o pagbabasa gamit ang mga bit IREAD ΠΈ IWRITE ayon sa pagkakabanggit. Ang proseso ay nagtatakda ng bandila at mga tawag sleep(), at inaasahan na sa hinaharap ay tatawag ang ibang proseso wakeup().

Ang totoong magic ay nangyayari sa sleep() ΠΈ wakeup(). Ang mga ito ay ipinatupad sa slp.c, ang pinagmulan ng sikat na "You are not expected to understand this" comment. Sa kabutihang palad, hindi namin kailangang maunawaan ang code, tingnan lamang ang ilang mga komento:

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

Ang proseso na tumatawag sleep() para sa isang partikular na channel, maaaring magising sa ibang pagkakataon ng isa pang proseso, na tatawag wakeup() para sa parehong channel. writep() ΠΈ readp() i-coordinate ang kanilang mga aksyon sa pamamagitan ng mga ipinares na tawag. tandaan mo yan pipe.c laging priority PPIPE kapag tinawag sleep(), sa lahat sleep() maaaring maputol ng isang senyas.

Ngayon mayroon kaming lahat upang maunawaan ang pag-andar 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);
}

Maaaring mas madali mong basahin ang function na ito mula sa ibaba hanggang sa itaas. Ang sangay na "basahin at ibalik" ay karaniwang ginagamit kapag mayroong ilang data sa pipeline. Sa kasong ito, ginagamit namin basahin() basahin ang mas maraming data hangga't magagamit simula sa kasalukuyan f_offset basahin, at pagkatapos ay i-update ang halaga ng katumbas na offset.

Sa mga kasunod na pagbabasa, magiging walang laman ang pipeline kung umabot na ang read offset i_size1 sa inode. Ni-reset namin ang posisyon sa 0 at sinusubukang gisingin ang anumang proseso na gustong sumulat sa pipeline. Alam natin na kapag puno na ang conveyor, writep() matulog sa ip+1. At ngayong walang laman ang pipeline, maaari natin itong gisingin para ipagpatuloy ang ikot ng pagsulat nito.

Kung walang babasahin, eh readp() maaaring magtakda ng bandila IREAD at matutulog na ip+2. Alam natin kung ano ang magigising sa kanya writep()kapag nagsusulat ito ng ilang data sa pipeline.

Mga komento sa basahin() at isulati() ay tutulong sa iyo na maunawaan na sa halip na ipasa ang mga parameter sa pamamagitan ng "uΒ» maaari naming ituring ang mga ito tulad ng mga regular na function ng I/O na kumukuha ng file, posisyon, buffer sa memorya, at bilangin ang bilang ng mga byte na babasahin o isusulat.

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

Kung tungkol sa "konserbatibo" na pagharang, kung gayon readp() ΠΈ writep() i-lock ang mga inode hanggang sa matapos o makakuha ng resulta (i.e. call wakeup). plock() ΠΈ prele() gumana nang simple: gamit ang ibang hanay ng mga tawag sleep ΠΈ wakeup hayaan kaming gisingin ang anumang proseso na nangangailangan ng lock na kakalabas lang namin:

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

Noong una hindi ko maintindihan kung bakit readp() hindi nagiging sanhi ng prele(ip) bago ang tawag wakeup(ip+1). Ang unang bagay writep() tawag sa loop nito, ito plock(ip), na nagreresulta sa isang deadlock kung readp() ay hindi pa naalis ang block nito, kaya dapat kahit papaano ay gumana nang tama ang code. Kung titingnan mo wakeup(), nagiging malinaw na minarkahan lamang nito ang proseso ng pagtulog bilang handa na para sa pagpapatupad, upang sa hinaharap sched() talagang inilunsad ito. Kaya readp() sanhi wakeup(), ina-unlock, itinatakda IREAD at mga tawag sleep(ip+2)- lahat ng ito dati writep() restart ang cycle.

Kinukumpleto nito ang paglalarawan ng mga pipeline sa ikaanim na edisyon. Simpleng code, malalayong implikasyon.

Ikapitong Edisyon Unix (Enero 1979) ay isang bagong pangunahing release (pagkalipas ng apat na taon) na nagpakilala ng maraming bagong aplikasyon at mga tampok ng kernel. Sumailalim din ito sa mga makabuluhang pagbabago kaugnay ng paggamit ng type casting, unions at typed pointers sa mga istruktura. Gayunpaman code ng mga pipeline halos hindi nagbago. Maaari nating laktawan ang edisyong ito.

Xv6, isang simpleng kernel na katulad ng Unix

Upang lumikha ng isang nucleus Xv6 naiimpluwensyahan ng ikaanim na edisyon ng Unix, ngunit nakasulat sa modernong C upang tumakbo sa mga x86 processor. Ang code ay madaling basahin at naiintindihan. Gayundin, hindi tulad ng mga mapagkukunan ng Unix na may TUHS, maaari mo itong i-compile, baguhin, at patakbuhin ito sa isang bagay maliban sa PDP 11/70. Samakatuwid, ang core na ito ay malawakang ginagamit sa mga unibersidad bilang isang materyal sa pagtuturo sa mga operating system. Mga pinagmumulan ay nasa Github.

Ang code ay naglalaman ng isang malinaw at maalalahaning pagpapatupad tubo.c, na sinusuportahan ng isang buffer sa memorya sa halip na isang inode sa disk. Dito binibigyan ko lamang ang kahulugan ng "structural pipeline" at ang function 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() nagtatakda ng estado ng lahat ng natitirang pagpapatupad, na kinabibilangan ng mga function piperead(), pipewrite() ΠΈ pipeclose(). Ang aktwal na tawag sa sistema sys_pipe ay isang wrapper na ipinatupad sa sysfile.c. Inirerekomenda kong basahin ang lahat ng kanyang code. Ang pagiging kumplikado ay nasa antas ng source code ng ikaanim na edisyon, ngunit ito ay mas madali at mas kaaya-ayang basahin.

Linux 0.01

Mahahanap mo ang source code para sa Linux 0.01. Magiging nakapagtuturo na pag-aralan ang pagpapatupad ng mga pipeline sa kanyang fs/pipe.c. Dito, ginagamit ang inode upang kumatawan sa pipeline, ngunit ang pipeline mismo ay nakasulat sa modernong C. Kung na-hack mo ang iyong paraan sa pamamagitan ng code ng ikaanim na edisyon, hindi ka magkakaroon ng anumang problema dito. Ito ang hitsura ng function 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;
}

Kahit na hindi tinitingnan ang mga kahulugan ng istruktura, maaari mong malaman kung paano ginagamit ang bilang ng sanggunian ng inode upang suriin kung ang isang write operation ay nagreresulta sa SIGPIPE. Bilang karagdagan sa byte-by-byte na gawain, ang function na ito ay madaling ihambing sa mga ideya sa itaas. Kahit logic sleep_on/wake_up mukhang hindi alien.

Mga Makabagong Linux Kernel, FreeBSD, NetBSD, OpenBSD

Mabilis akong nagpunta sa ilang modernong kernels. Wala sa kanila ang mayroon nang disk-based na pagpapatupad (hindi nakakagulat). Ang Linux ay may sariling pagpapatupad. At bagama't ang tatlong modernong BSD kernels ay naglalaman ng mga pagpapatupad batay sa code na isinulat ni John Dyson, sa paglipas ng mga taon sila ay naging masyadong naiiba sa isa't isa.

Upang basahin fs/pipe.c (sa Linux) o sys/kern/sys_pipe.c (sa *BSD), kailangan ng tunay na dedikasyon. Ang pagganap at suporta para sa mga tampok tulad ng vector at asynchronous na I/O ay mahalaga sa code ngayon. At ang mga detalye ng paglalaan ng memorya, mga kandado, at pagsasaayos ng kernel ay lubos na nag-iiba. Hindi ito ang kailangan ng mga unibersidad para sa isang panimulang kurso sa mga operating system.

Sa anumang kaso, kawili-wili para sa akin na mahukay ang ilang mga lumang pattern (halimbawa, pagbuo SIGPIPE at bumalik EPIPE kapag sumusulat sa isang saradong pipeline) sa lahat ng ito, napaka-iba, modernong kernels. Malamang na hindi ako makakakita ng isang PDP-11 na computer nang live, ngunit marami pa ring matututunan mula sa code na isinulat ilang taon bago ako isinilang.

Isinulat ni Divi Kapoor noong 2011, ang artikulong "Ang Linux Kernel Implementation ng Pipe at FIFOsay isang pangkalahatang-ideya kung paano gumagana ang mga pipeline ng Linux (sa ngayon). A kamakailang commit sa linux inilalarawan ang pipeline model ng pakikipag-ugnayan, na ang mga kakayahan ay lumampas sa mga pansamantalang file; at ipinapakita rin kung gaano kalayo ang napunta sa mga pipeline mula sa "very conservative locking" sa ikaanim na edisyon ng Unix kernel.

Pinagmulan: www.habr.com

Magdagdag ng komento