Як рэалізаваны канвееры ў Unix

Як рэалізаваны канвееры ў Unix
У гэтым артыкуле апісана рэалізацыя канвеераў у ядры Unix. Я быў некалькі расчараваны, што нядаўні артыкул пад назвай «Як працуюць канвееры ў Unix?» апынулася ня пра ўнутраную прыладу. Мне стала цікава, і я закапаўся ў старыя крыніцы, каб знайсці адказ.

Аб чым гаворка?

Канвееры - "верагодна, найважнейшае вынаходства ў Unix" - гэта вызначальная характарыстыка ляжалай у аснове Unix філасофіі аб'яднання разам маленькіх праграм, а таксама знаёмы надпіс у камандным радку:

$ echo hello | wc -c
6

Гэтая функцыянальнасць залежыць ад які прадстаўляецца ядром сістэмнага выкліку pipe, які апісаны на старонках дакументацыі pipe(7) и pipe(2):

Канвееры забяспечваюць аднанакіраваны канал міжпрацэснага ўзаемадзеяння. У канвеера ёсць уваход (write end) і выйсце (read end). Дадзеныя, запісаныя ва ўваход канвеера, могуць быць прачытаныя на выхадзе.

Канвеер ствараецца з дапамогай выкліку pipe(2), які вяртае два файлавыя дэскрыптары: адзін спасылаецца на ўваход канвеера, другі на выхад.

Вынікі трасіроўкі прыведзенай вышэй каманды дэманструюць стварэнне канвеера і паток дадзеных праз яго з аднаго працэсу ў іншы:

$ 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

Бацькоўскі працэс выклікае pipe(), каб атрымаць падлучаныя файлавыя дэскрыптары. Адзін даччыны працэс запісвае ў адзін дэскрыптар, а іншы працэс счытвае тыя ж дадзеныя з іншага дэскрыптара. Абалонка з дапамогай dup2 "пераназывае" дэскрыптары 3 і 4, каб яны адпавядалі stdin і stdout.

Без канвеераў абалонцы прыйшлося б запісваць вынік аднаго працэсу ў файл і перадаваць яго іншаму працэсу, каб той прачытаў дадзеныя з файла. У выніку мы марнавалі б больш рэсурсаў і месца на дыску. Аднак канвееры добрыя не толькі тым, што дазваляюць пазбегнуць выкарыстанні часавых файлаў:

Калі працэс спрабуе прачытаць з пустога канвеера, тады read(2) заблакуе да таго часу, пакуль дадзеныя не стануць даступныя. Калі працэс паспрабуе запісаць у запоўнены канвеер, тады write(2) заблакуе да таго часу, пакуль з канвеера не будзе лічана дастаткова дадзеных для выканання запісу.

Як і POSIX-патрабаванне, гэта важная ўласцівасць: запіс у канвеер аж да PIPE_BUF байтаў (як мінімум 512) павінна быць атамарнай, каб працэсы маглі ўзаемадзейнічаць сябар з сябрам праз канвеер так, як звычайныя файлы (не якія прадстаўляюць такіх гарантый) не могуць.

Пры выкарыстанні звычайнага файла працэс можа запісаць у яго ўсе свае выходныя дадзеныя і перадаць іншаму працэсу. Або працэсы могуць дзейнічаць у рэжыме цвёрдага распаралельвання, з дапамогай вонкавага сігнальнага механізму (накшталт семафора) паведамляючы адзін аднаму аб завяршэнні запісу або чытанні. Канвееры пазбаўляюць нас ад усіх гэтых клопатаў.

Што мы шукаем?

Растлумачу на пальцах, каб вам было лягчэй уявіць, як можа працаваць канвеер. Вам спатрэбіцца вылучыць у памяці буфер і нейкі стан. Спатрэбяцца функцыі для дадання і выдаленні дадзеных з буфера. Спатрэбіцца нейкі сродак, каб выклікаць функцыі падчас аперацый чытання і запісы ў файлавыя дэскрыптары. І спатрэбяцца блакіроўкі, каб рэалізаваць апісаныя вышэй спецыяльныя паводзіны.

Цяпер мы гатовыя дапытаць пры яркім святле лямпаў зыходны код ядра, каб пацвердзіць або абвергнуць нашу цьмяную разумовую мадэль. Але заўсёды будзьце гатовы да нечаканасцяў.

Дзе мы шукаем?

Я не ведаю, дзе ляжыць мой экзэмпляр вядомай кнігі.Lions book« з зыходным кодам Unix 6, але дзякуючы The Unix Heritage Society можна ў анлайне пашукаць у зыходным кодзе яшчэ больш старых версіяў Unix.

Блуканне па архівах TUHS падобна наведванню музея. Мы можам зірнуць на нашу агульную гісторыю, і я адчуваю павагу да шматгадовых намаганняў па аднаўленні ўсіх гэтых матэрыялаў біт за бітам са старых касет і раздруковак. І востра ўсведамляю тыя фрагменты, якія яшчэ адсутнічаюць.

Задаволіўшы сваю цікаўнасць у частцы старажытнай гісторыі канвеераў, для параўнання можам паглядзець на сучасныя ядры.

Дарэчы, pipe з'яўляецца сістэмным выклікам нумар 42 у табліцы sysent[]. Супадзенне?

Традыцыйныя ядры Unix (1970–1974)

Я не знайшоў ніякіх слядоў pipe(2) ні ў PDP-7 Unix (студзень 1970-го), ні ў першай рэдакцыі Unix (лістапад 1971-го), ні ў няпоўным зыходным кодзе другой рэдакцыі (чэрвень 1972-га).

TUHS сцвярджае, што трэцяя рэдакцыя Unix (люты 1973-го) стала першай версіяй з канвеерамі:

Трэцяя рэдакцыя Unix была апошняй версіяй з ядром, напісаным на асэмблеры, але пры гэтым першай версіяй з канвеерамі. На працягу 1973-го вяліся працы па паляпшэнні трэцяй рэдакцыі, ядро ​​перапісалі на З, і так з'явілася чацвёртая рэдакцыя Unix.

Адзін з чытачоў знайшоў скан дакумента, у якім Даг МакІлрой прапанаваў ідэю "злучэння праграм па прынцыпе садовага шланга".

Як рэалізаваны канвееры ў Unix
У кнізе Браяна Кернігана «Unix: History and Memoir», у гісторыі з'яўлення канвеераў таксама згадваецца гэты дакумент: «… ён вісеў на сцяне ў маім офісе ў Bell Labs на працягу 30 гадоў». Вось інтэрв'ю з МакІлроем, і яшчэ адна гісторыя з працы МакІлроя, напісанай у 2014-м:

Калі з'явілася Unix, маё захапленне каруцінамі прымусіла мяне папытаць аўтара АС, Кена Томпсана, дазволіць дадзеным, запісаным у нейкі працэс, ісці не толькі на прыладу, але і на вынахад да іншага працэсу. Кен вырашыў, што гэта магчыма. Аднак, як мінімаліст, ён хацеў, каб кожная сістэмная функцыя адыгрывала значную ролю. Ці сапраўды прамы запіс паміж працэсамі мае вялікую перавагу ў параўнанні з запісам у прамежкавы файл? І толькі калі я занёс пэўную прапанову з кідкім назовам «канвеер» і апісаннем сінтаксісу ўзаемадзеяння працэсаў, Кен, нарэшце-то, усклікнуў: «Я зраблю гэта!».

І зрабіў. Адным лёсавызначальным увечар Кен змяніў ядро ​​і абалонку, выправіў некалькі стандартных праграм, стандартаваўшы іх працэдуру прыняцця ўваходных дадзеных (якія могуць паступаць з канвеера), а таксама памяняў імёны файлаў. На наступны дзень канвееры пачалі вельмі шырока прымяняць у дадатках. Да канца тыдня сакратаркі з іх дапамогай адпраўлялі на друкарку дакументы з тэкставых рэдактараў. Крыху пазней Кен замяніў арыгінальны API і сінтаксіс для абалонкі выкарыстання канвеераў на больш чыстыя пагадненні, якія з тых часоў і прымяняюцца.

Нажаль, зыходны код ядра трэцяй рэдакцыі Unix згублены. І хоць у нас ёсць напісаны на З зыходны код ядра чацвёртай рэдакцыі, якая выйшла ў лістападзе 1973-га, аднак яна выйшла за некалькі месяцаў да афіцыйнага рэлізу і не ўтрымлівае рэалізацыі канвеераў. Шкада, што зыходны код легендарнай функцыі Unix згублены, магчыма, назаўжды.

У нас ёсць тэкст дакументацыі па pipe(2) з абодвух рэлізаў, таму можна пачаць з пошуку ў дакументацыі трэцяй рэдакцыі (паводле пэўных слоў, падкрэсленых «уручную», радок з літаралаў ^H, пасля якога ідзе ніжняе падкрэсленне!). Гэты прота-pipe(2) напісаны на асэмблеры і вяртае толькі адзін файлавы дэскрыптар, але ўжо падае чаканую асноўную функцыянальнасць:

Сістэмны выклік труба стварае механізм уводу высновы, які называецца канвеерам. Які вяртаецца файлавы дэскрыптар можна выкарыстоўваць для аперацый чытання і запісы. Калі ў канвеер нешта запісваецца, тое буферызуецца да 504 байтаў дадзеных, пасля чаго працэс запісу прыпыняецца. Пры чытанні з канвеера буферызаваных дадзеныя залазяць.

Да наступнага года ядро ​​было перапісана на З, а pipe(2) у чацвёртай рэдакцыі здабыў сваё сучаснае аблічча з прататыпамpipe(fildes)»:

Сістэмны выклік труба стварае механізм уводу высновы, які называецца канвеерам. Якія вяртаюцца файлавыя дэскрыптары можна выкарыстоўваць у аперацыях чытання і запісы. Калі нешта запісваецца ў канвеер, то выкарыстоўваецца дэскрыптар, які вяртаецца ў r1 (соотв. fildes[1]), то буферызуецца да 4096 байтаў дадзеных, пасля чаго працэс запісу прыпыняецца. Пры чытанні з канвеера дэскрыптар, які вяртаецца ў r0 (соотв. fildes [0]), забірае дадзеныя.

Мяркуецца, што пасля вызначэння канвеера два (ці больш) якія ўзаемадзейнічаюць працэсу (створаных наступнымі выклікамі відэлец) будуць перадаваць дадзеныя з канвеера з дапамогай выклікаў счытванне и запіс.

У абалонцы ёсць сінтаксіс для вызначэння лінейнага масіва працэсаў, злучаных з дапамогай канвеера.

Выклікі на чытанне з пустога канвеера (які не змяшчае буферызаваных дадзеных), які мае толькі адзін канец (закрыты ўсе якія запісваюць файлавыя дэскрыптары), вяртаюць «канец файла». Выклікі на запіс у аналагічнай сітуацыі ігнаруюцца.

Самая ранняя захавалася рэалізацыя канвеера ставіцца да пятай рэдакцыі Unix (чэрвень 1974-го), але яна амаль ідэнтычная той, што з'явілася ў наступным рэлізе. Толькі дадаліся каментары, таму пятую рэдакцыю можна прапусціць.

Шостая рэдакцыя Unix (1975)

Пачынаем чытаць зыходны код Unix шосты рэдакцыі (травень 1975-га). Шмат у чым дзякуючы Львоў знайсці яго значна лягчэй, чым зыходнікі больш ранніх версій:

Многія гады кніга Львоў была адзіным дакументам па ядры Unix, даступным па-за сценамі Bell Labs. Хаця ліцэнзія шостай рэдакцыі дазваляла выкладчыкам выкарыстоўваць яе зыходны код, аднак ліцэнзія сёмай рэдакцыі выключыла гэтую магчымасць, таму кніга распаўсюджвалася ў выглядзе нелегальных машынапісных копій.

Сёння можна купіць рэпрынтны экзэмпляр кнігі, на вокладцы якой намаляваны студэнты ля капіравальнага апарата. А дзякуючы Уорэну Тумі (які запусціў праект TUHS) вы можаце спампаваць PDF-файл з зыходным кодам шостай рэдакцыі. Жадаю даць вам уяўленне, колькі сіл сышло на стварэнне файла:

Больш за 15 гадоў таму я набраў копію зыходнага кода, прыведзенага ў Львоў, таму што мне не падабалася якасць маёй копіі з невядомай колькасці іншых копій. TUHS яшчэ не існавала, і ў мяне не было доступу да старых зыходнікаў. Але ў 1988-м я знайшоў старую стужку з 9 дарожкамі, на якой была рэзервовая копія з кампутара PDP11. Цяжка было зразумець, ці працуе яна, але тамака было непашкоджанае дрэва /usr/src/, у якім большасць файлаў былі пазначаныя 1979-м годам, што ўжо тады выглядала старажытнасцю. Гэта была сёмая рэдакцыя ці яе вытворная PWB, як я лічыў.

Я ўзяў знаходку за аснову і ўручную адрэдагаваў зыходнікі да стану шостай рэдакцыі. Частка кода засталася такой жа, частка прыйшлося злёгку падрэдагаваць, памяняўшы сучасны токен += на састарэлы =+. Нешта проста выдаліў, а нешта прыйшлося цалкам перапісаць, але не зашмат.

І сёння мы можам у анлайне чытаць на TUHS зыходны код шостай рэдакцыі з архіва, да якога прыклаў руку Дэніс Рычы.

Дарэчы, на першы погляд, галоўнай асаблівасцю С-кода да перыяду Кернігана і Рычы з'яўляецца яго сцісласць. Не так часта мне атрымоўваецца ўстаўляць фрагменты кода без шырокага рэдагавання, каб ён адпавядаў адносна вузкай вобласці адлюстравання на маім сайце.

У пачатку /usr/sys/ken/pipe.c ёсць тлумачальны каментар (і так, там ёсць яшчэ /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

Памер буфера не мяняўся з часоў чацвёртай рэдакцыі. Але тут мы без усялякай публічнай дакументацыі бачым, што калісьці канвееры выкарыстоўвалі файлы ў якасці запаснога сховішча!

Што да LARG-файлаў, то яны адпавядаюць inode-сцягу LARG, які выкарыстоўваецца «алгарытмам вялікай адрасацыі» для апрацоўкі ўскосных (indirect) блокаў з мэтай падтрымкі буйнейшых файлавых сістэм. Раз Кен сказаў, што лепш іх не выкарыстоўваць, то я з радасцю паверу яму на слова.

Вось сапраўдны сістэмны выклік 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;
}

У каментарыі зразумела апісана, што тут адбываецца. Але разабрацца ў кодзе не так проста, збольшага з-за таго, як з дапамогайstruct user u» і рэгістраў R0 и R1 перадаюцца параметры сістэмных выклікаў і якія вяртаюцца значэння.

Паспрабуем з дапамогай ialloc() размясціць на дыску inode (індэксны дэскрыптар), а з дапамогай falloc() - размясціць у памяці два файл. Калі ўсё пройдзе добра, то мы зададзім сцягі для вызначэння гэтых файлаў як двух канцоў канвеера, пакажам іх у тым жа inode (чый лічыльнік спасылак стане роўны 2), і пазначым inode як зменены і які выкарыстоўваецца. Звярніце ўвагу на звароты да iput() у памылковых шляхах (error paths) для памяншэння лічыльніка спасылак у новым inode.

pipe() павінен праз R0 и R1 вяртаць нумары файлавых дэскрыптараў для чытання і запісы. falloc() вяртае паказальнік на файлавую структуру, але таксама "вяртае" праз u.u_ar0[R0] і файлавы дэскрыптар. Гэта значыць код захоўвае ў r файлавы дэскрыптар для чытання і прысвойвае дэскрыптар для запісу прама з u.u_ar0[R0] пасля другога выкліку falloc().

сцяг FPIPE, які мы задалі пры стварэнні канвеера, кіруе паводзінамі функцыі rdwr() у sys2.c, якая выклікае канкрэтныя падпраграмы ўводу-вываду I/O:

/*
 * common code for read and write calls:
 * check permissions, set base, count, and offset,
 * and switch out to readi, writei, or pipe code.
 */
rdwr(mode)
{
    register *fp, m;

    m = mode;
    fp = getf(u.u_ar0[R0]);
        /* … */

    if(fp->f_flag&FPIPE) {
        if(m==FREAD)
            readp(fp); else
            writep(fp);
    }
        /* … */
}

Затым функцыя readp() в pipe.c счытвае дадзеныя з канвеера. Але прасачыць рэалізацыю лепш пачынаючы з writep(). Паўтаруся, код ускладніўся з-за асаблівасцяў дамовы аб перадачы аргументаў, але некаторыя падрабязнасці можна апусціць.

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

На ўваход канвеера мы жадаем запісаць байты u.u_count. Спачатку запатрабуем заблакаваць індэксны дэскрыптар (гл. ніжэй plock/prele).

Затым правяраем лічыльнік спасылак inode. Пакуль абодва канцы канвеера застаюцца адкрытымі, лічыльнік павінен быць роўны 2. Мы прытрымліваем адну спасылку (з rp->f_inode), так што калі лічыльнік будзе менш 2, то гэта павінна азначаць, што які чытае працэс зачыніў свой канец канвеера. Іншымі словамі, мы спрабуем пісаць у зачынены канвеер, а гэта з'яўляецца памылкай. Упершыню код памылкі EPIPE і сігнал SIGPIPE з'явіліся ў шостай рэдакцыі Unix

Але нават калі канвеер адчынены, ён можа быць запоўнены. У гэтым выпадку мы здымаем блакаванне і ідзем спаць у надзеі, што іншы працэс прачытае з канвеера і вызваліць у ім дастаткова месца. Прачнуўшыся, мы вяртаемся да пачатку, зноў вешаем блакаванне і запускаем новы цыкл запісу.

Калі ў канвееры дастаткова вольнага месца, то мы запісваем у яго дадзеныя з дапамогай writei(). Параметр i_size1 у inode'а (пры пустым канвееры можа быць роўны 0) паказвае на канец дадзеных, якія ў ім ужо ўтрымоўваюцца. Калі месца для запісу дастаткова, мы можам запоўніць канвеер ад i_size1 да PIPESIZ. Затым здымаем блакіроўку і спрабуем абудзіць любы працэс, які чакае магчымасці прачытаць з канвеера. Вяртаемся да пачатку, каб паглядзець, ці ўдалося запісаць столькі байтаў, колькі нам было патрэбна. Калі не ўдалося, то пачынаем новы цыкл запісу.

Звычайна параметр i_mode у inode'а выкарыстоўваецца для захоўвання дазволаў r, w и x. Але ў выпадку з канвеерамі мы сігналізуем аб чаканні нейкім працэсам запісу ці чытанні з дапамогай бітаў. IREAD и IWRITE адпаведна. Працэс задае сцяг і выклікае sleep(), і чакаецца, што будучыні нейкі іншы працэс выкліча wakeup().

Сапраўднае чараўніцтва адбываецца ў sleep() и wakeup(). Яны рэалізаваны ў slp.c, крыніцы знакамітага каментара "Вы не абавязаны гэта разумець" (You are not expected to understand this). На шчасце, мы не абавязаны разумець код, проста паглядзім некаторыя каментары:

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

Працэс, які выклікае sleep() для пэўнага канала, можа быць пазней абуджаны іншым працэсам, які выкліча wakeup() для таго ж канала. writep() и readp() каардынуюць свае дзеянні з дапамогай такіх парных выклікаў. Звярніце ўвагу, што pipe.c заўсёды аддае прыярытэт PPIPE пры выкліку sleep(), таму ўсё sleep() могуць перарывацца па сігнале.

Цяпер у нас ёсць усё, каб разабрацца ў функцыі 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);
}

Магчыма, вам будзе прасцей чытаць гэтую функцыю знізу ўгару. Галінка "read and return" звычайна выкарыстоўваецца, калі ў канвееры ёсць нейкія дадзеныя. У гэтым выпадку мы з дапамогай readi() счытваем столькі дадзеных, колькі даступна пачынаючы з бягучага f_offset чытанні, а затым абнаўляем значэнне адпаведнага зрушэння.

Пры наступным чытанні канвеер будзе пустым, калі зрушэнне чытання дасягнула значэння i_size1 у inode'а. Мы скідаем пазіцыю на 0 і спрабуем абудзіць любы працэс, які хоча запісаць у канвеер. Мы ведаем, што калі канвеер будзе поўны, writep() засне на ip+1. А зараз, калі канвеер пусты, мы можам абудзіць яго, каб ён аднавіў свой цыкл запісу.

Калі чытаць няма чаго, то readp() можа задаць сцяг IREAD і заснуць на ip+2. Мы ведаем, што яго абудзіць writep(), калі запіша ў канвеер якія-небудзь дадзеныя.

Каментары да readi() і writei() дапамогуць зразумець, што замест перадачы параметраў празu» мы можам звяртацца з імі як са звычайнымі функцыямі ўводу-высновы, якія бяруць файл, пазіцыю, буфер у памяці, і падлічваюць колькасць байтаў для чытання ці запісы.

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

Што да «кансерватыўнай» блакіроўкі, то readp() и writep() блакуюць inode датуль, пакуль не скончаць працу ці не атрымаюць вынік (гэта значыць выклічуць wakeup). plock() и prele() працуюць проста: з дапамогай іншага набору выклікаў sleep и wakeup дазваляюць нам абуджаць любы працэс, якому патрэбна блакаванне, якое мы толькі што знялі:

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

Спачатку я не мог зразумець, чаму readp() не выклікае prele(ip) да выкліку wakeup(ip+1). Першае, што writep() выклікае ў сваім цыкле, гэта plock(ip), які прыводзіць да ўзаемаблакіроўкі, калі readp() яшчэ не зняў свой блок, таму код нейкім чынам павінен працаваць правільна. Калі паглядзець на wakeup(), то становіцца зразумела, што ён толькі пазначае спячы працэс як гатовы да выканання, каб у будучыні sched() сапраўды запусціла яго. Так што readp() выклікае wakeup(), здымае блакіроўку, задае IREAD і выклікае sleep(ip+2)- усё гэта да таго, як writep() аднаўляе цыкл.

На гэтым апісанне канвеераў у шостай рэдакцыі скончана. Просты код, далёка ідучыя наступствы.

Сёмая рэдакцыя Unix (студзень 1979-го) была новым асноўным рэлізам (праз чатыры гады), у якой з'явілася шмат новых прыкладанняў і ўласцівасцяў ядра. Таксама ў ім адбыліся значныя змены ў сувязі з выкарыстаннем прывядзення тыпаў, union'аў і тыпізаваных паказальнікаў на структуры. Аднак код канвеераў практычна не змяніўся. Можам прапусьціць гэтую рэдакцыю.

Xv6, простае Unix-вобразнае ядро

На стварэнне ядра Xv6 паўплывала шостая рэдакцыя Unix, аднак яно напісана на сучасным З, каб яго запускалі на x86-працэсарах. Код лёгка чытаць, ён зразумелы. Да таго ж, у адрозненне ад зыходнікаў Unix з TUHS, вы можаце скампіляваць яго, мадыфікаваць і запусціць на чымсьці яшчэ акрамя PDP 11/70. Таму гэта ядро ​​шырока выкарыстоўваецца ў ВНУ як навучальны матэрыял па аперацыйных сістэмах. Зыходнікі ляжаць на Github.

У кодзе змяшчаецца зразумелая і прадуманая рэалізацыя pipe.c, падмацаваная буферам у памяці замест inode на дыску. Тут я прыводжу толькі вызначэнне "структурнага канвеера" і функцыі 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() задае стан усё астатняй рэалізацыі, якая ўключае ў сябе функцыі piperead(), pipewrite() и pipeclose(). Фактычны сістэмны выклік sys_pipe з'яўляецца абгорткай, рэалізаванай у sysfile.c. Рэкамендую прачытаць увесь яго код. Складанасць на ўзроўні зыходніка шостай рэдакцыі, але чытаць значна лягчэй і прыемней.

Linux 0.01

Можна знайсці зыходны код Linux 0.01. Будзе павучальна вывучыць рэалізацыю канвеераў у яго fs/pipe.c. Тут для падання канвеера выкарыстоўваецца inode, але сам канвеер напісаны на сучасным C. Калі вы прадзерліся праз код шостай рэдакцыі, то тут вы не выпрабуеце цяжкасцяў. Так выглядае функцыя 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;
}

Нават не гледзячы на ​​азначэнні структур можна разабрацца, як лічыльнік спасылак inode выкарыстоўваецца для праверкі, ці прыводзіць аперацыя запісу да SIGPIPE. Апроч пабайтавай працы гэтую функцыю лёгка супаставіць з вышэйапісанымі ідэямі. Нават логіка sleep_on/wake_up не выглядае такой чужароднай.

Сучасныя ядры Linux, FreeBSD, NetBSD, OpenBSD

Я хутка прабегся па некаторых сучасных ядрах. Ні ў адным з іх ужо няма рэалізацыі з выкарыстаннем кружэлкі (не дзіўна). У Linux свая ўласная рэалізацыя. І хоць тры сучасных BSD-ядры ўтрымоўваюць рэалізацыі на аснове кода, які быў напісаны Джонам Дайсанам, за мінулыя гады яны сталі занадта моцна адрознівацца сябар ад сябра.

Каб чытаць fs/pipe.c (на Linux) або sys/kern/sys_pipe.c (на *BSD), патрабуецца сапраўдная самааддача. Сёння ў кодзе важныя прадукцыйнасць і падтрымка такіх функцый, як вектарныя і асінхронныя аперацыі ўводу-высновы. А падрабязнасці вылучэння памяці, блакіровак і канфігурацыі ядра – усё гэта моцна адрозніваецца. Гэта не тое, што трэба ВНУ для ўступнага курса па аперацыйных сістэмах.

У любым выпадку, мне было цікава раскапаць некалькі старадаўніх патэрнаў (напрыклад, генераванне SIGPIPE і вяртанне EPIPE пры запісе ў закрыты канвеер) ва ўсіх гэтых, такіх розных, сучасных ядрах. Верагодна, я ніколі не ўбачу ўжывую кампутар PDP-11, але яшчэ ёсць чаму павучыцца на кодзе, які быў напісаны за некалькі гадоў да майго нараджэння.

Напісаны Дзіві Капурам у 2011-м годзе артыкул «Linux Kernel Implementation of Pipes and FIFOs» уяўляе сабой агляд, як працуюць (да гэтага часу) канвееры ў Linux. А нядаўні коміт у Linux ілюструе канвеерную мадэль узаемадзеяння, чые магчымасці перавышаюць магчымасці часавых файлаў; а таксама паказвае, наколькі далёка сышлі канвееры ад "вельмі кансерватыўнай блакіроўкі" у ядры Unix шостай рэдакцыі.

Крыніца: habr.com

Дадаць каментар