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()тиркелген файл дескрипторлорун алуу үчүн. Бир бала процесси бир дескрипторго жазат жана башка процесс башка дескриптордон ошол эле маалыматтарды окуйт. Shell stdin жана stdout менен дал келүү үчүн 2 жана 3 дескрипторлорун dup4 менен "атын өзгөртөт".

Түтүк өткөргүчтөрү жок болсо, кабык бир процесстин чыгышын файлга жазып, файлдан маалыматтарды окуу үчүн аны башка процесске өткөрүшү керек болчу. Натыйжада, биз көбүрөөк ресурстарды жана диск мейкиндигин текке кетирмекпиз. Бирок, түтүктөр убактылуу файлдарды болтурбоо үчүн гана жакшы:

Эгерде процесс бош түтүктөн окууга аракет кылса, анда 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-жылы үчүнчү басылышын өркүндөтүү боюнча иштер жүрүп жаткан, ядро ​​C тилинде кайра жазылган жана ошентип Unixтин төртүнчү басылышы жарык көргөн.

Окурмандардын бири документтин сканерин тапты, анда Даг Макилрой "программаларды бакча шлангындай туташтыруу" идеясын сунуштады.

Unixте түтүктөр кантип ишке ашырылат
Брайан Кернигандын китебиндеUnix: Тарых жана мемуар”, конвейерлердин пайда болуу тарыхында бул документ да эскерилет: “...ал менин Bell Labs кеңсемде 30 жыл бою дубалда илинип турган”. Бул жерде Макилрой менен болгон маегижана башка окуядан Макилройдун 2014-жылы жазылган эмгеги:

Unix пайда болгондо, менин корутиндерге болгон кызыгуум мени OS автору Кен Томпсондон кандайдыр бир процесске жазылган маалыматтар аппаратка гана эмес, башка процесске чыгууга да уруксат берүүнү суранды. Кен мүмкүн деп ойлоду. Бирок, минималист катары ал ар бир системанын өзгөчөлүгүнүн маанилүү ролду ойношун каалаган. Процесстердин ортосунда түз жазуу чындап эле ортодогу файлга жазуудан чоң артыкчылыгыбы? Ошондо гана мен “куур” деген жагымдуу аталыш менен жана процесстердин өз ара аракеттенүүсүнүн синтаксисинин сүрөттөлүшү менен конкреттүү сунуш киргизгенимде, акыры Кен: “Мен муну жасайм!” деп кыйкырып жиберди.

Жана кылды. Тагдырлуу кечтердин биринде Кен өзөктү жана кабыкты өзгөртүп, киргизүүнү кантип кабыл алаарын стандартташтыруу үчүн бир нече стандарттуу программаларды бекитти (ал түтүктөн келиши мүмкүн) жана файл атын өзгөрттү. Эртеси күнү, түтүктөр колдонууда абдан кеңири колдонулган. Аптанын аягында катчылар аларды тексттик процессорлордон принтерге документтерди жөнөтүү үчүн колдонушту. Бир аз убакыт өткөндөн кийин, Кен түпнуска APIди жана түтүктөрдү колдонууну андан бери колдонулуп келе жаткан таза конвенциялар менен ороп коюу үчүн синтаксисин алмаштырды.

Тилекке каршы, Unix үчүнчү чыгарылыш ядросунун баштапкы коду жоголду. Бизде ядронун баштапкы коду C тилинде жазылган төртүнчү басылышы, 1973-жылы ноябрда чыгарылган, бирок ал расмий релизден бир нече ай мурун чыккан жана түтүктөрдү ишке ашыруу камтылган эмес. Бул легендарлуу Unix функциясынын баштапкы коду, балким, түбөлүккө жоголуп кеткени өкүнүчтүү.

Бизде документация тексти бар pipe(2) эки релизден, ошондуктан сиз документтерди издөө менен баштасаңыз болот үчүнчү басылышы (белгилүү сөздөр үчүн, асты сызылган "кол менен", ^H литералдарынан кийин астын сызык!). Бул прото-pipe(2) ассемблерде жазылган жана бир гана файл дескрипторун кайтарат, бирок буга чейин күтүлгөн негизги функцияларды камсыз кылат:

Системалык чалуу түтүк трубопровод деп аталган киргизүү/чыгаруу механизмин түзөт. Кайтарылган файлдын дескрипторун окуу жана жазуу операциялары үчүн колдонсо болот. Түтүккө бир нерсе жазылганда, ал 504 байтка чейин маалыматты буферлейт, андан кийин жазуу процесси токтотулат. Түтүк өткөргүчтөн окууда буфердик маалыматтар алынат.

Кийинки жылы, ядро ​​C менен кайра жазылган жана түтүк(2) төртүнчү басылышы прототиби менен заманбап көрүнүшүн алган "pipe(fildes)":

Системалык чалуу түтүк трубопровод деп аталган киргизүү/чыгаруу механизмин түзөт. Кайтарылган файлдын дескрипторлору окуу жана жазуу операцияларында колдонулушу мүмкүн. Түтүк өткөргүчкө бир нерсе жазылганда, r1де кайтарылган дескриптор (мисалы. fildes[1]) колдонулат, 4096 байт маалыматка чейин буферленген, андан кийин жазуу процесси токтотулат. Түтүк өткөргүчтөн окуганда, r0ге кайтып келген дескриптор (ж. fildes[0]) маалыматтарды алат.

Түтүк жолу аныкталгандан кийин, эки (же андан көп) өз ара аракеттенүүчү процесстер (кийинки чакыруулар менен түзүлгөн) деп болжолдонууда. айры) чалууларды колдонуу менен түтүктөн маалыматтарды өткөрүп берет окуу и жазуу.

кабык түтүк аркылуу байланышкан процесстердин сызыктуу массивдерин аныктоо үчүн синтаксиске ээ.

Бир гана учу (бардык жазуу файлынын дескрипторлору жабык) бош конвейерден (буферленген берилиштерди камтыган) окууга чалуулар "файлдын аягы" деп кайтарат. Окшош кырдаалда чалууларды жазуу көңүл бурулбайт.

Эң эрте сакталган түтүктөрдү ишке ашыруу колдонулат Unixтин бешинчи чыгарылышына (Июнь 1974), бирок кийинки релизинде пайда болгон менен дээрлик окшош. Комментарийлер гана кошулду, ошондуктан бешинчи басылышын өткөрүп жиберүүгө болот.

Unix Sixth Edition (1975)

Unix баштапкы кодун окуй баштады алтынчы басылышы (1975-жыл, май). Чоң рахмат арстандар мурунку версиялардын булактарына караганда табуу оңой:

Көп жылдар бою китеп арстандар Bell Labs тышкары жеткиликтүү Unix ядросу боюнча жалгыз документ болгон. Алтынчы басылышынын лицензиясы мугалимдерге анын баштапкы кодун колдонууга уруксат бергени менен, жетинчи басылышынын лицензиясы бул мүмкүнчүлүктү жокко чыгаргандыктан, китеп машинкада мыйзамсыз басылган нускаларда таратылган.

Бүгүн сиз китептин кайра басып чыгаруу нускасын сатып алсаңыз болот, анын мукабасында окуучулардын сүрөтү көчүрүүчү машинада чагылдырылган. Ал эми Warren Toomey (TUHS долбоорун баштаган) рахмат, сиз жүктөп ала аласыз Алтынчы чыгарылыш булагы PDF. Мен сизге файлды түзүүгө канча күч жумшалганы жөнүндө түшүнүк бергим келет:

15 жылдан ашык убакыт мурун, мен берилген баштапкы коддун көчүрмөсүн терген элем арстандаранткени мага белгисиз сандагы башка нускалардан менин көчүрмөмдүн сапаты жаккан жок. TUHS али жок болчу жана мен эски булактарга кире алган жокмун. Бирок 1988-жылы мен PDP9 компьютеринен камдык көчүрмөсү бар 11 треки бар эски кассетаны таптым. Анын иштегенин билүү кыйын болчу, бирок бузулбаган /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иштетүү үчүн "чоң даректөө алгоритми" тарабынан колдонулат кыйыр блоктор чоң файл системаларын колдоо үчүн. Кен аларды колдонбогону жакшы деп айткандыктан, мен анын сөзүн кубануу менен кабыл алам.

Бул жерде чыныгы система чалуу 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() дискке жайгаштыруу инод (inode), жана жардамы менен 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да бар.

Кодекс так жана ойлонулган ишке ашырууну камтыйт pipe.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 ядросунда түтүктөр "өтө консервативдүү кулпудан" канчалык алыс кеткенин көрсөтөт.

Source: www.habr.com

Комментарий кошуу