Unix-те құбырлар қалай жүзеге асырылады

Unix-те құбырлар қалай жүзеге асырылады
Бұл мақалада Unix ядросында конвейерлерді іске асыру сипатталған. «Жақында жарияланған мақала мені біраз ренжітті.Unix-те құбырлар қалай жұмыс істейді?» шықты емес ішкі құрылымы туралы. Мен қызығып, жауабын табу үшін ескі дереккөздерді зерттедім.

Сіз не туралы айтып отырсыз?

Құбырлар - «Unix-тегі ең маңызды өнертабыс» - Unix-тің шағын бағдарламаларды біріктіру философиясының айқындаушы ерекшелігі және таныс командалық ұран:

$ echo hello | wc -c
6

Бұл функция ядромен қамтамасыз етілген жүйелік шақыруға байланысты pipe, ол құжаттама беттерінде сипатталған құбыр(7) и құбыр(2):

Құбырлар процесс аралық байланыс үшін бір жақты арнаны қамтамасыз етеді. Құбырдың кірісі (жазудың соңы) және шығысы (оқылатыны) бар. Құбырдың кірісіне жазылған деректерді шығыста оқуға болады.

Құбыр шақыру арқылы жасалады 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()тіркелген файл дескрипторларын алу үшін. Бір еншілес процесс бір дескрипторға жазады, ал басқа процесс басқа дескриптордан бірдей деректерді оқиды. Қабық stdin және stdout сәйкестендіру үшін dup2 бар 3 және 4 дескрипторларының "атын өзгертеді".

Құбырларсыз қабық бір процестің нәтижесін файлға жазып, файлдағы деректерді оқу үшін оны басқа процеске жіберуі керек еді. Нәтижесінде біз көбірек ресурстар мен дискілік кеңістікті босқа кетіретін едік. Дегенмен, құбырлар уақытша файлдарды болдырмау үшін ғана жақсы:

Егер процесс бос құбырдан оқуға тырысса, онда read(2) деректер қолжетімді болғанша блоктайды. Егер процесс толық конвейерге жазуға тырысса, онда write(2) жазуды аяқтау үшін конвейерден жеткілікті деректер оқылғанша блоктайды.

POSIX талабы сияқты, бұл маңызды қасиет: құбырға дейін жазу PIPE_BUF байт (кемінде 512) атомдық болуы керек, осылайша процестер бір-бірімен конвейер арқылы қалыпты файлдар (мұндай кепілдіктерді қамтамасыз етпейтін) байланыса алмайды.

Кәдімгі файлдың көмегімен процесс оған өзінің барлық шығысын жазып, оны басқа процесске бере алады. Немесе процестер жазу немесе оқудың аяқталуы туралы бір-бірін хабарлау үшін сыртқы сигнал беру механизмін (семафор сияқты) пайдалана отырып, қатты параллель режимде жұмыс істей алады. Конвейерлер бізді осы қиындықтардан құтқарады.

Біз не іздейміз?

Конвейердің қалай жұмыс істейтінін елестетуді жеңілдету үшін мен саусақтарыммен түсіндіремін. Жадта буфер мен кейбір күйді бөлу қажет болады. Буферден деректерді қосу және жою үшін сізге функциялар қажет болады. Файл дескрипторларында оқу және жазу операциялары кезінде функцияларды шақыру үшін сізге кейбір мүмкіндіктер қажет болады. Ал құлыптар жоғарыда сипатталған ерекше мінез-құлықты жүзеге асыру үшін қажет.

Енді біз анық емес психикалық модельімізді растау немесе жоққа шығару үшін жарық шамның астында ядроның бастапқы кодын сұрауға дайынбыз. Бірақ әрқашан күтпеген жағдайға дайын болыңыз.

Біз қайда қарап тұрмыз?

Әйгілі кітабымның қайда жатқанын білмеймін.Арыстандар кітабы« Unix 6 бастапқы кодымен, бірақ арқасында 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: тарих және естелік», конвейерлердің пайда болу тарихында бұл құжатты да атап өтеді: «...ол 30 жыл бойы Bell Labs-тегі менің кеңсемде қабырғада ілулі тұрды». Мұнда Макилроймен сұхбатжәне тағы бір оқиға МакИлройдың 2014 жылы жазылған жұмысы:

Unix пайда болған кезде, менің корутиндерге деген құмарлығым мені ОЖ авторы Кен Томпсоннан қандай да бір процесске жазылған деректердің құрылғыға ғана емес, сонымен қатар басқа процеске шығуына да рұқсат беруін өтінді. Кен бұл мүмкін деп ойлады. Дегенмен, минималист ретінде ол әрбір жүйелік мүмкіндіктің маңызды рөл атқарғанын қалады. Процестер арасында тікелей жазу шынымен аралық файлға жазудан үлкен артықшылық па? Мен «құбыр» деген қызықты атаумен және процестердің өзара әрекеттесу синтаксисінің сипаттамасымен нақты ұсыныс жасағанда ғана, Кен ақыры: «Мен мұны істеймін!» деп айқайлады.

Және жасады. Бір тағдырлы кеште Кен ядро ​​мен қабықты өзгертті, енгізуді қалай қабылдайтынын стандарттау үшін бірнеше стандартты бағдарламаларды бекітті (ол құбырдан келуі мүмкін) және файл атауларын өзгертті. Келесі күні құбырлар қосымшаларда өте кең қолданылды. Аптаның соңына қарай хатшылар оларды мәтіндік процессорлардан принтерге құжаттарды жіберу үшін пайдаланды. Біраз уақыттан кейін Кен түпнұсқа API және құбырларды пайдалануды содан бері қолданылып келе жатқан таза конвенциялармен орау үшін синтаксисті ауыстырды.

Өкінішке орай, Unix ядросының үшінші шығарылымының бастапқы коды жоғалды. Бізде ядроның бастапқы коды C тілінде жазылғанымен төртінші басылым, ол 1973 жылдың қарашасында шығарылды, бірақ ол ресми шығарылымнан бірнеше ай бұрын шықты және құбырларды іске асыруды қамтымайды. Бұл аңызға айналған Unix мүмкіндігінің бастапқы коды, мүмкін, мәңгілікке жоғалып кеткені өкінішті.

Бізде құжаттама мәтіні бар pipe(2) екі шығарылымнан да, сондықтан құжаттаманы іздеуден бастауға болады үшінші басылым («қолмен» асты сызылған белгілі бір сөздер үшін ^H әріптерінен кейін астын сызу!). Бұл прото-pipe(2) ассемблерде жазылған және тек бір файл дескрипторын қайтарады, бірақ күтілетін негізгі функционалдылықты қамтамасыз етеді:

Жүйелік қоңырау Құбыр конвейер деп аталатын енгізу/шығару механизмін жасайды. Қайтарылған файл дескрипторын оқу және жазу әрекеттері үшін пайдалануға болады. Құбырға бірдеңе жазылғанда, ол 504 байтқа дейін деректерді буферлейді, содан кейін жазу процесі тоқтатылады. Құбырдан оқу кезінде буферленген деректер алынады.

Келесі жылы ядро ​​C тілінде қайта жазылды және құбыр(2) төртінші басылым прототипімен заманауи келбетін алды »pipe(fildes)«:

Жүйелік қоңырау Құбыр конвейер деп аталатын енгізу/шығару механизмін жасайды. Қайтарылған файл дескрипторларын оқу және жазу операцияларында пайдалануға болады. Құбырға бірдеңе жазылғанда, r1-де қайтарылған дескриптор (респ. fildes[1]) пайдаланылады, 4096 байт деректерге дейін буферленеді, содан кейін жазу процесі тоқтатылады. Конвейерден оқу кезінде r0-ге қайтарылған дескриптор (респ. файлдар[0]) деректерді қабылдайды.

Құбыр анықталғаннан кейін екі (немесе одан да көп) өзара әрекеттесетін процестер (кейінгі шақырулар арқылы жасалған) деп болжанады. Шанышқы) қоңырауларды пайдаланып құбырдан деректерді жібереді оқу и жазу.

Қабықта құбыр арқылы қосылған процестердің сызықтық массивін анықтауға арналған синтаксис бар.

Тек бір ұшы бар (барлық жазу файлы дескрипторлары жабық) бос конвейерден (буферленген деректері жоқ) оқуға шақырулар "файлдың соңын" қайтарады. Ұқсас жағдайдағы жазу қоңыраулары еленбейді.

Ең ерте құбырды жүзеге асыру сақталған проблемалар Unix бесінші басылымына дейін (1974 жылдың маусымы), бірақ ол келесі шығарылымда пайда болғанмен дерлік бірдей. Тек түсініктемелер қосылды, сондықтан бесінші басылымды өткізіп жіберуге болады.

Unix алтыншы басылымы (1975)

Unix бастапқы кодын оқуды бастау алтыншы басылым (1975 ж. мамыр). Үлкен рахмет Lions бұрынғы нұсқалардың көздеріне қарағанда табу оңайырақ:

Көп жылдар бойы кітап Lions Bell Labs-тан тыс қол жетімді Unix ядросындағы жалғыз құжат болды. Алтыншы басылым лицензиясы мұғалімдерге өзінің бастапқы кодын пайдалануға мүмкіндік бергенімен, жетінші басылым лицензиясы бұл мүмкіндікті жоққа шығарды, сондықтан кітап заңсыз машинкамен басылған көшірмелермен таратылды.

Бүгін сіз мұқабасында көшіргіштегі студенттер бейнеленген кітаптың қайта басылған көшірмесін сатып ала аласыз. Ал Уоррен Тумидің (TUHS жобасын бастаған) арқасында жүктеп алуға болады Алтыншы басылымның көзі PDF. Мен сізге файлды жасауға қанша күш жұмсалғаны туралы түсінік бергім келеді:

15 жылдан астам уақыт бұрын мен берілген бастапқы кодтың көшірмесін енгіздім Lionsсебебі белгісіз сандағы басқа көшірмелердегі көшірменің сапасы маған ұнамады. TUHS әлі болған жоқ және мен ескі дереккөздерге қол жеткізе алмадым. Бірақ 1988 жылы мен PDP9 компьютерінен сақтық көшірмесі бар 11 жолдан тұратын ескі таспаны таптым. Оның жұмыс істеп тұрғанын білу қиын болды, бірақ файлдардың көпшілігінде 1979 деп белгіленген, сол кезде де көне болып көрінетін бұзылмаған /usr/src/ ағашы болды. Бұл жетінші басылым немесе 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 файлдарына келетін болсақ, олар сәйкес келеді INOD жалауы LARG, оны өңдеу үшін «үлкен адрестеу алгоритмі» пайдаланады жанама блоктар үлкен файлдық жүйелерді қолдау үшін. Кен оларды пайдаланбаған дұрыс деп айтқандықтан, мен оның сөзін қуана қабылдаймын.

Міне, нақты жүйелік қоңырау 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;
}

Түсініктеме мұнда не болып жатқанын анық сипаттайды. Бірақ кодты түсіну оңай емес, ішінара қалай «құрылымдық пайдаланушы u» және тіркейді R0 и R1 жүйелік шақыру параметрлері және қайтару мәндері беріледі.

-мен тырысайық ialloc() дискіге қойыңыз инод (инод), және көмегімен falloc() - екі сақтау файл. Егер бәрі ойдағыдай болса, біз бұл файлдарды құбырдың екі ұшы ретінде анықтау үшін жалаушаларды орнатамыз, оларды бір инодқа бағыттаймыз (анықтама саны 2 болады) және инодты өзгертілген және қолданыста деп белгілейміз. Өтінімдерге назар аударыңыз енгізу() жаңа инодтағы сілтемелер санын азайту үшін қате жолдарында.

pipe() арқылы төленеді R0 и R1 оқу және жазу үшін файл дескрипторының нөмірлерін қайтару. falloc() файл құрылымына көрсеткішті қайтарады, сонымен қатар арқылы «қайтарады». u.u_ar0[R0] және файл дескрипторы. Яғни, код сақталады r оқуға арналған файл дескрипторы және тікелей жазу үшін дескриптор тағайындайды u.u_ar0[R0] екінші қоңыраудан кейін falloc().

Ту FPIPE, біз құбырды жасау кезінде орнатамыз, функцияның әрекетін басқарады rdwr() sys2.c, ол арнайы енгізу/шығару тәртібін шақырады:

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

Содан кейін біз инодтың анықтамалық санын тексереміз. Құбырдың екі шеті де ашық болғанша, есептегіш 2 болуы керек. Біз бір сілтемені (ден бастап) ұстаймыз. rp->f_inode), сондықтан есептегіш 2-ден аз болса, онда бұл оқу процесі құбырдың ұшын жауып тастағанын білдіруі керек. Басқаша айтқанда, біз жабық құбырға жазуға тырысамыз, бұл қате. Бірінші қате коды EPIPE және сигнал SIGPIPE Unix-тің алтыншы басылымында пайда болды.

Бірақ конвейер ашық болса да, ол толып кетуі мүмкін. Бұл жағдайда біз құлыпты босатып, басқа процесс құбырдан оқып, онда жеткілікті орын босатады деген үмітпен ұйықтаймыз. Біз оянғанда, біз басына ораламыз, құлыпты қайтадан іліп, жаңа жазу циклін бастаймыз.

Егер құбырда бос орын жеткілікті болса, біз оған деректерді пайдалана отырып жазамыз writei()... Параметр i_size1 inode'a (бос құбырмен 0-ге тең болуы мүмкін) құрамында бар деректердің соңына нұсқайды. Жазу үшін жеткілікті орын болса, біз құбырды толтыра аламыз i_size1 қарай PIPESIZ. Содан кейін біз құлыпты босатып, құбырдан оқуды күтіп тұрған кез келген процесті оятуға тырысамыз. Біз қажет болғанша көп байт жаза алдық па, жоқ па, соны білу үшін басына ораламыз. Олай болмаса, жаңа жазу циклін бастаймыз.

Әдетте параметр i_mode inode рұқсаттарды сақтау үшін пайдаланылады r, w и x. Бірақ құбырлар жағдайында біз қандай да бір процесс биттерді пайдаланып жазуды немесе оқуды күтіп тұрғанын білдіреміз IREAD и IWRITE тиісінше. Процесс жалаушаны және қоңырауларды орнатады sleep(), және болашақта басқа процесс шақырылады деп күтілуде wakeup().

Нағыз сиқыр сонда болады sleep() и wakeup(). Олар жылы жүзеге асырылады slp.c, әйгілі «Сізден мұны түсіну күтілмейді» деген пікірдің көзі. Бақытымызға орай, кодты түсінудің қажеті жоқ, жай ғана кейбір түсініктемелерді қараңыз:

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

Бұл функцияны төменнен жоғары қарай оқу оңайырақ болуы мүмкін. «Оқу және қайтару» тармағы әдетте құбырда кейбір деректер болған кезде қолданылады. Бұл жағдайда біз пайдаланамыз оқу() ағымдағы деректерден бастап қол жетімді болғанша оқыңыз f_offset оқыңыз, содан кейін сәйкес ығысу мәнін жаңартыңыз.

Келесі оқуларда, егер оқу ығысуына жеткен болса, құбыр бос болады i_size1 инодта. Біз позицияны 0-ге қалпына келтіреміз және құбырға жазуды қалайтын кез келген процесті оятуға тырысамыз. Біз білеміз, конвейер толған кезде, writep() ұйықтап қалу ip+1. Енді құбыр бос болғандықтан, жазу циклін жалғастыру үшін оны оятуға болады.

Егер оқитын ештеңе болмаса, онда readp() жалауша қоя алады IREAD және ұйықтап кетіңіз ip+2. Біз оны не оятатынын білеміз writep()ол құбырға кейбір деректерді жазғанда.

Пікірлер read() және 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() инодтарды аяқтағанша немесе нәтиже алғанша құлыптаңыз (яғни қоңырау 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 жылдың қаңтары) көптеген жаңа қолданбалар мен ядро ​​мүмкіндіктерін енгізген жаңа негізгі шығарылым (төрт жылдан кейін) болды. Ол сондай-ақ типті құюды, біріктірулерді және құрылымдарға терілген көрсеткіштерді қолдануға байланысты айтарлықтай өзгерістерге ұшырады. Дегенмен құбырлар коды іс жүзінде өзгерген жоқ. Бұл шығарылымды өткізіп жіберуге болады.

Xv6, Unix тәрізді қарапайым ядро

Ядро құру үшін Xv6 Unix-тің алтыншы шығарылымы әсер етті, бірақ x86 процессорларында жұмыс істеу үшін заманауи С тілінде жазылған. Код оқуға оңай және түсінікті. Сондай-ақ, TUHS бар Unix көздерінен айырмашылығы, оны құрастыруға, өзгертуге және PDP 11/70-ден басқа нәрседе іске қосуға болады. Сондықтан бұл ядро ​​операциялық жүйелер бойынша оқу материалы ретінде университеттерде кеңінен қолданылады. Дереккөздер Github сайтында.

Кодта нақты және ойластырылған енгізу бар құбыр.c, дискідегі инодтың орнына жадтағы буфермен қамтамасыз етілген. Мұнда мен тек «құрылымдық құбырдың» анықтамасын және функциясын беремін 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. Мұнда инод құбырды бейнелеу үшін пайдаланылады, бірақ конвейердің өзі қазіргі С тілінде жазылған. Егер сіз алтыншы шығарылым кодын бұзған болсаңыз, мұнда ешқандай қиындық болмайды. Функция осылай көрінеді 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;
}

Құрылымдық анықтамаларды қарамай-ақ, жазу операциясының нәтижесін тексеру үшін инодтың анықтамалық саны қалай пайдаланылатынын анықтауға болады. 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 жылы Диви Капур жазған мақала «Құбырлар мен FIFO-лардың Linux ядросының іске асырылуыLinux құбырларының (әзірге) қалай жұмыс істейтініне шолу болып табылады. А linux бойынша жақында жасалған міндеттеме мүмкіндіктері уақытша файлдардың мүмкіндіктерінен асатын өзара әрекеттесудің конвейер үлгісін суреттейді; сондай-ақ алтыншы шығарылым Unix ядросындағы құбырлардың «өте консервативті құлыптаудан» қаншалықты алысқа кеткенін көрсетеді.

Ақпарат көзі: www.habr.com

пікір қалдыру