Ինչպես են խողովակաշարերն իրականացվում 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()կից ֆայլերի նկարագրիչներ ստանալու համար: Մեկ երեխա գործընթաց գրում է մեկ նկարագրիչի վրա, իսկ մյուս պրոցեսը կարդում է նույն տվյալները մեկ այլ նկարագրիչից: Կեղևը «վերանվանում է» 2 և 3 նկարագրիչները dup4-ով, որպեսզի համապատասխանեն stdin-ին և stdout-ին:

Առանց խողովակաշարերի, կեղևը պետք է գրի մի գործընթացի արդյունքը ֆայլի մեջ և այն տեղափոխի մեկ այլ գործընթաց՝ ֆայլից տվյալները կարդալու համար: Արդյունքում մենք ավելի շատ ռեսուրսներ և սկավառակի տարածություն կվատնեինք: Այնուամենայնիվ, խողովակաշարերը լավ են ոչ միայն ժամանակավոր ֆայլերից խուսափելու համար.

Եթե ​​գործընթացը փորձում է կարդալ դատարկ խողովակաշարից, ապա read(2) կարգելափակվի, քանի դեռ տվյալները հասանելի չեն: Եթե ​​գործընթացը փորձում է գրել ամբողջական խողովակաշարի վրա, ապա write(2) կարգելափակվի այնքան ժամանակ, մինչև խողովակաշարից բավականաչափ տվյալներ չկարդանան՝ գրելն ավարտելու համար:

Ինչպես POSIX-ի պահանջը, սա կարևոր հատկություն է՝ գրել խողովակաշարին մինչև PIPE_BUF բայթերը (առնվազն 512) պետք է լինեն ատոմային, որպեսզի գործընթացները կարողանան միմյանց հետ հաղորդակցվել խողովակաշարի միջոցով այնպես, որ նորմալ ֆայլերը (որոնք նման երաշխիքներ չեն տալիս) չեն կարող:

Սովորական ֆայլով պրոցեսը կարող է գրել իր ամբողջ արդյունքը և փոխանցել այն մեկ այլ գործընթացի: Կամ պրոցեսները կարող են աշխատել կոշտ զուգահեռ ռեժիմում՝ օգտագործելով արտաքին ազդանշանային մեխանիզմ (ինչպես սեմալիստ), որպեսզի միմյանց տեղեկացնեն գրելու կամ կարդալու ավարտի մասին: Փոխակրիչները մեզ փրկում են այս բոլոր քաշքշուկներից:

Ի՞նչ ենք մենք փնտրում։

Ես կբացատրեմ իմ մատների վրա, որպեսզի ավելի հեշտ պատկերացնեմ, թե ինչպես կարող է փոխակրիչը աշխատել: Ձեզ անհրաժեշտ կլինի հիշողության մեջ հատկացնել բուֆեր և որոշակի վիճակ: Ձեզ անհրաժեշտ կլինեն բուֆերից տվյալներ ավելացնելու և հեռացնելու գործառույթներ: Ձեզ անհրաժեշտ կլինի որոշակի հնարավորություն՝ ֆայլերի նկարագրիչների վրա կարդալու և գրելու գործողությունների ժամանակ գործառույթներ կանչելու համար: Իսկ վերը նկարագրված հատուկ վարքագիծն իրականացնելու համար անհրաժեշտ են կողպեքներ:

Այժմ մենք պատրաստ ենք հարցաքննել միջուկի սկզբնական կոդը վառ լամպի լույսի ներքո՝ հաստատելու կամ հերքելու մեր անորոշ մտավոր մոդելը: Բայց միշտ պատրաստ եղեք անսպասելիին։

Որտե՞ղ ենք մենք փնտրում:

Ես չգիտեմ, թե որտեղ է գտնվում հայտնի գրքի իմ օրինակը։Առյուծների գիրք«Յունիքս 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-ի երրորդ հրատարակությունը վերջին տարբերակն էր միջուկով, որը գրված էր assembler-ով, բայց նաև առաջին տարբերակը խողովակաշարերով: 1973-ի ընթացքում աշխատանքներ էին տարվում երրորդ հրատարակությունը բարելավելու ուղղությամբ, միջուկը վերաշարադրվեց C-ով, և այդպիսով ծնվեց Յունիքսի չորրորդ հրատարակությունը։

Ընթերցողներից մեկը գտել է մի փաստաթղթի սկան, որտեղ Դագ Մաքիլրոյն առաջարկել է «այգու խողովակի նման ծրագրերը միացնելու» գաղափարը:

Ինչպես են խողովակաշարերն իրականացվում Unix-ում
Բրայան Քերնիգանի գրքումUnix: Պատմություն և հուշերՓոխակրիչների հայտնվելու պատմությունը նշում է նաև այս փաստաթուղթը. «...այն 30 տարի կախված էր Bell Labs-ի իմ գրասենյակի պատից»։ Այստեղ հարցազրույց Մաքիլրոյի հետև մեկ այլ պատմություն Մաքիլրոյի աշխատանքը՝ գրված 2014թ:

Երբ Unix-ը հայտնվեց, կորուտինների հանդեպ իմ կիրքը ստիպեց ինձ խնդրել ՕՀ-ի հեղինակ Քեն Թոմփսոնին, որպեսզի թույլ տա, որ ինչ-որ գործընթացում գրված տվյալները գնան ոչ միայն սարք, այլև դեպի մեկ այլ գործընթաց: Քենը որոշեց, որ դա հնարավոր է: Այնուամենայնիվ, որպես մինիմալիստ, նա ցանկանում էր, որ համակարգի յուրաքանչյուր հատկանիշ էական դեր խաղա: Արդյո՞ք գործընթացների միջև ուղղակի գրելը մեծ առավելություն է միջանկյալ ֆայլի վրա գրելու նկատմամբ: Եվ միայն այն ժամանակ, երբ ես կոնկրետ առաջարկ արեցի գրավիչ «խողովակաշար» անունով և գործընթացների փոխազդեցության շարահյուսության նկարագրությամբ, Քենը վերջապես բացականչեց. «Ես կանեմ դա»:

Եվ արեց: Մի ճակատագրական երեկո Քենը փոխեց միջուկը և կեղևը, ֆիքսեց մի քանի ստանդարտ ծրագրեր՝ ստանդարտացնելու համար, թե ինչպես են նրանք ընդունում մուտքերը (որը կարող է գալ խողովակաշարից) և փոխեց ֆայլերի անունները: Հաջորդ օրը խողովակաշարերը շատ լայնորեն կիրառվեցին կիրառություններում: Շաբաթվա վերջում քարտուղարներն օգտագործում էին դրանք՝ տեքստային պրոցեսորներից փաստաթղթեր ուղարկելու տպիչ։ Որոշ ժամանակ անց Քենը փոխարինեց սկզբնական API-ն և շարահյուսությունը՝ խողովակաշարերի օգտագործումը ավելի մաքուր կոնվենցիաներով փաթաթելու համար, որոնք այդ ժամանակվանից կիրառվել են:

Ցավոք, երրորդ հրատարակության Unix միջուկի սկզբնական կոդը կորել է: Եվ չնայած մենք ունենք միջուկի սկզբնական կոդը գրված C-ով չորրորդ հրատարակություն, որը թողարկվել է 1973 թվականի նոյեմբերին, սակայն այն դուրս է եկել պաշտոնական թողարկումից մի քանի ամիս առաջ և չի պարունակում խողովակաշարերի իրականացում։ Ափսոս, որ Unix-ի այս լեգենդար ֆունկցիայի սկզբնական կոդը կորել է, գուցե ընդմիշտ:

Մենք ունենք փաստաթղթերի տեքստ pipe(2) երկու թողարկումներից, այնպես որ կարող եք սկսել փաստաթղթերը որոնելով երրորդ հրատարակություն (որոշ բառերի համար՝ ընդգծված «ձեռքով», ^H տառերի տող, որին հաջորդում է ընդգծում:): Այս նախա-pipe(2) գրված է assembler-ում և վերադարձնում է միայն մեկ ֆայլի նկարագրիչ, բայց արդեն ապահովում է ակնկալվող հիմնական ֆունկցիոնալությունը.

Համակարգային զանգ խողովակ ստեղծում է I/O մեխանիզմ, որը կոչվում է խողովակաշար: Վերադարձված ֆայլի նկարագրիչը կարող է օգտագործվել կարդալու և գրելու գործողությունների համար: Երբ ինչ-որ բան գրվում է խողովակաշարում, այն պահում է մինչև 504 բայթ տվյալներ, որից հետո գրելու գործընթացը կասեցվում է: Խողովակաշարից կարդալիս վերցվում են բուֆերացված տվյալները:

Հաջորդ տարի միջուկը վերաշարադրվել էր C-ով և խողովակ (2) չորրորդ հրատարակություն իր ժամանակակից տեսքը ձեռք է բերել նախատիպի հետ»pipe(fildes)»:

Համակարգային զանգ խողովակ ստեղծում է I/O մեխանիզմ, որը կոչվում է խողովակաշար: Վերադարձված ֆայլի նկարագրիչները կարող են օգտագործվել կարդալու և գրելու գործողություններում: Երբ ինչ-որ բան գրվում է խողովակաշարում, օգտագործվում է r1-ով վերադարձված նկարագրիչը (resp. fildes[1]), որը բուֆերացված է մինչև 4096 բայթ տվյալների, որից հետո գրելու գործընթացը կասեցվում է: Խողովակաշարից կարդալիս, r0-ին վերադարձված նկարագրիչը (resp. fildes[0]) վերցնում է տվյալները:

Ենթադրվում է, որ երբ խողովակաշարը սահմանվել է, երկու (կամ ավելի) փոխազդող գործընթացներ (ստեղծվել են հետագա կանչերի միջոցով) պատառաքաղ) կփոխանցի տվյալներ խողովակաշարից՝ օգտագործելով զանգեր կարդալ и գրել.

Կեղևն ունի շարահյուսություն՝ խողովակաշարի միջոցով միացված գործընթացների գծային զանգված սահմանելու համար:

Դատարկ խողովակաշարից կարդալու կոչերը (որը չի պարունակում բուֆերացված տվյալներ), որն ունի միայն մեկ ծայր (գրելու ֆայլի բոլոր նկարագրիչները փակ են) վերադարձնում են «ֆայլի վերջ»: Նման իրավիճակում գրելու զանգերն անտեսվում են:

Ամենավաղը պահպանված խողովակաշարի իրականացում կիրառվում է Յունիքսի հինգերորդ հրատարակությանը (1974թ. հունիս), բայց այն գրեթե նույնական է հաջորդ թողարկումում հայտնվածի հետ: Միայն ավելացված մեկնաբանություններ, այնպես որ հինգերորդ հրատարակությունը կարելի է բաց թողնել:

Unix Sixth Edition (1975)

Սկսում է կարդալ Unix-ի սկզբնական կոդը վեցերորդ հրատարակություն (Մայիս 1975)։ Մեծ մասամբ շնորհիվ Lions դա շատ ավելի հեշտ է գտնել, քան ավելի վաղ տարբերակների աղբյուրները.

Երկար տարիներ գիրքը Lions Unix միջուկի միակ փաստաթուղթն էր, որը հասանելի էր Bell Labs-ից դուրս: Չնայած վեցերորդ հրատարակության լիցենզիան ուսուցիչներին թույլ էր տալիս օգտագործել իր սկզբնական կոդը, յոթերորդ հրատարակության լիցենզիան բացառում էր այդ հնարավորությունը, ուստի գիրքը տարածվեց անօրինական մեքենագրված օրինակներով:

Այսօր դուք կարող եք գնել գրքի վերահրատարակված օրինակը, որի շապիկին պատկերված են ուսանողները պատճենահանման սարքի մոտ: Եվ շնորհիվ Ուորեն Թումիի (ով սկսել է TUHS նախագիծը), կարող եք ներբեռնել Վեցերորդ հրատարակության աղբյուր PDF. Ես ուզում եմ ձեզ պատկերացում տալ, թե որքան ջանք է գործադրվել ֆայլը ստեղծելու համար.

Ավելի քան 15 տարի առաջ ես մուտքագրեցի ելակետային կոդի պատճենը, որը տրամադրված էր Lionsքանի որ ինձ դուր չեկավ իմ կրկնօրինակի որակը անհայտ թվով այլ օրինակներից: TUHS-ը դեռ գոյություն չուներ, և ես մուտք չունեի դեպի հին աղբյուրները: Բայց 1988-ին ես գտա մի հին ժապավեն՝ 9 հետքերով, որը պահուստային պահուստ ուներ PDP11 համակարգչից: Դժվար էր իմանալ, թե արդյոք այն աշխատում էր, բայց կար մի անձեռնմխելի /usr/src/ ծառ, որտեղ ֆայլերի մեծ մասը նշված էր 1979 թ., որը նույնիսկ այն ժամանակ հնաոճ տեսք ուներ: Ես մտածեցի, որ դա յոթերորդ հրատարակությունն էր, կամ PWB ածանցյալ:

Ես հիմք վերցրեցի գտածոն և ձեռքով խմբագրեցի աղբյուրները մինչև վեցերորդ հրատարակության վիճակը: Կոդի մի մասը մնաց նույնը, մի մասը պետք է մի փոքր խմբագրվեր՝ փոխելով ժամանակակից նշանը += հնացած =+ի։ Ինչ-որ բան ուղղակի ջնջվեց, և ինչ-որ բան պետք է ամբողջությամբ վերաշարադրվեր, բայց ոչ շատ:

Եվ այսօր մենք կարող ենք առցանց կարդալ TUHS-ի վեցերորդ հրատարակության սկզբնական կոդը արխիվը, որին ձեռք ուներ Դենիս Ռիչին.

Ի դեպ, առաջին հայացքից Քերնիգանի և Ռիչիի ժամանակաշրջանից առաջ C կոդի հիմնական առանձնահատկությունն այն է. հակիրճություն. Հաճախ չէ, որ ես կարողանում եմ կոդի հատվածներ տեղադրել առանց լայնածավալ խմբագրման՝ իմ կայքի համեմատաբար նեղ ցուցադրման տարածքը տեղավորելու համար:

Սկզբում /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-flag 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;
}

Մեկնաբանությունը հստակ նկարագրում է, թե ինչ է կատարվում այստեղ։ Բայց ծածկագիրը հասկանալն այնքան էլ հեշտ չէ, մասամբ այն պատճառով, թե ինչպես է «struct user u» և գրանցում R0 и R1 փոխանցվում են համակարգի զանգի պարամետրերը և վերադարձի արժեքները:

Եկեք փորձենք հետ ialloc () տեղադրել սկավառակի վրա ինոդ (ինոդ), և օգնությամբ falloc () - խանութ երկու ֆայլը. Եթե ​​ամեն ինչ լավ ընթանա, մենք դրոշակներ կսահմանենք այս ֆայլերը որպես խողովակաշարի երկու ծայրեր նույնականացնելու համար, դրանք կմատնանշենք նույն ինոդի վրա (որի հղումների թիվը դառնում է 2) և կնշենք ինոդը որպես փոփոխված և օգտագործվող: Ուշադրություն դարձրեք խնդրանքներին iput () նոր ինոդում հղումների քանակը նվազեցնելու սխալ ուղիներում:

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

Այնուհետև մենք ստուգում ենք ինոդի հղումների քանակը: Քանի դեռ խողովակաշարի երկու ծայրերը բաց են, հաշվիչը պետք է լինի 2: Մենք կառչում ենք մեկ օղակից (սկսած rp->f_inode), այնպես որ, եթե հաշվիչը 2-ից փոքր է, ապա դա պետք է նշանակի, որ ընթերցման գործընթացը փակել է խողովակաշարի իր ծայրը: Այսինքն՝ մենք փորձում ենք փակ խողովակաշարի վրա գրել, ինչը սխալ է։ Առաջին սխալի կոդը EPIPE և ազդանշան SIGPIPE հայտնվել է Unix-ի վեցերորդ հրատարակությունում։

Բայց նույնիսկ եթե փոխակրիչը բաց է, այն կարող է լցված լինել: Այս դեպքում մենք բաց ենք թողնում կողպեքը և գնում քնելու այն հույսով, որ մեկ այլ գործընթաց կկարդա խողովակաշարից և բավականաչափ տարածք կազատի դրանում: Երբ մենք արթնանում ենք, մենք վերադառնում ենք սկզբին, նորից կախում ենք կողպեքը և սկսում ենք գրելու նոր ցիկլ:

Եթե ​​խողովակաշարում բավականաչափ ազատ տարածություն կա, ապա մենք դրա վրա տվյալներ ենք գրում՝ օգտագործելով գրել (). Պարամետր 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()երբ այն գրում է որոշ տվյալներ խողովակաշարում:

Մեկնաբանություններ կարդալ () և գրել () կօգնի ձեզ հասկանալ, որ պարամետրերը փոխանցելու փոխարեն «u» մենք կարող ենք դրանք վերաբերվել որպես սովորական I/O ֆունկցիաների, որոնք վերցնում են ֆայլ, դիրք, հիշողության մեջ բուֆեր և հաշվում կարդալու կամ գրելու բայթերի քանակը:

/*
 * 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-ի վեցերորդ հրատարակությունից, սակայն գրված է ժամանակակից C-ով՝ x86 պրոցեսորներով աշխատելու համար: Կոդը հեշտ է կարդալ և հասկանալի: Բացի այդ, ի տարբերություն TUHS-ով Unix աղբյուրների, դուք կարող եք այն կազմել, փոփոխել և գործարկել PDP 11/70-ից բացի այլ բանի վրա: Ուստի այս միջուկը լայնորեն կիրառվում է համալսարաններում՝ որպես օպերացիոն համակարգերի ուսուցման նյութ։ Աղբյուրներ գտնվում են Github-ում.

Օրենսգիրքը պարունակում է հստակ և մտածված իրականացում խողովակ.գ, որը ապահովված է հիշողության մեջ բուֆերով՝ սկավառակի վրա ինոդի փոխարեն: Այստեղ ես տալիս եմ միայն «կառուցվածքային խողովակաշարի» սահմանումը և գործառույթը 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;
}

Նույնիսկ առանց կառուցվածքի սահմանումները դիտելու, դուք կարող եք պարզել, թե ինչպես է օգտագործվում ինոդի տեղեկանքների քանակը՝ ստուգելու համար, թե արդյոք գրելու գործողությունը հանգեցնում է SIGPIPE. Բացի բայթ առ բայթ աշխատանքից, այս ֆունկցիան հեշտ է համեմատել վերը նշված գաղափարների հետ: Նույնիսկ տրամաբանությունը sleep_on/wake_up այնքան էլ խորթ տեսք չունի:

Ժամանակակից Linux միջուկներ, FreeBSD, NetBSD, OpenBSD

Ես արագ անցա որոշ ժամանակակից միջուկներ: Նրանցից ոչ մեկն արդեն չունի սկավառակի վրա հիմնված իրականացում (զարմանալի չէ): Linux-ն ունի իր սեփական իրականացումը: Եվ չնայած երեք ժամանակակից BSD միջուկները պարունակում են իրականացումներ՝ հիմնված կոդի վրա, որը գրվել է Ջոն Դայսոնի կողմից, տարիների ընթացքում դրանք չափազանց տարբերվել են միմյանցից:

Կարդալ fs/pipe.c (Linux-ում) կամ sys/kern/sys_pipe.c (*BSD-ի վրա), այն իրական նվիրում է պահանջում: Կոդում այսօր կարևոր են այնպիսի հատկանիշների կատարումը և աջակցությունը, ինչպիսիք են վեկտորը և ասինխրոն I/O-ն: Իսկ հիշողության բաշխման, կողպեքների և միջուկի կազմաձևման մանրամասները շատ տարբեր են: Սա այն չէ, ինչ բուհերին անհրաժեշտ է օպերացիոն համակարգերի ներածական դասընթացի համար:

Ամեն դեպքում, ինձ համար հետաքրքիր էր բացահայտել մի քանի հին նախշեր (օրինակ՝ գեներացնող SIGPIPE և վերադառնալ EPIPE փակ խողովակաշարի վրա գրելիս) այս բոլոր, այնքան տարբեր, ժամանակակից միջուկներում։ Ես, հավանաբար, երբեք չեմ տեսնի PDP-11 համակարգիչ ուղիղ եթերում, բայց դեռ շատ բան կա սովորելու այն ծածկագրից, որը գրվել է իմ ծնվելուց մի քանի տարի առաջ:

Գրված է Դիվի Կապուրի կողմից 2011 թվականին, հոդվածը «Linux միջուկի խողովակների և FIFO-ների իրականացումակնարկ է, թե ինչպես են աշխատում Linux խողովակաշարերը (առայժմ): Ա վերջին commit-ը Linux-ում ցույց է տալիս փոխազդեցության խողովակաշարի մոդելը, որի հնարավորությունները գերազանցում են ժամանակավոր ֆայլերի հնարավորությունները. և նաև ցույց է տալիս, թե որքան հեռու են խողովակաշարերը «շատ պահպանողական արգելափակումից» Unix-ի վեցերորդ հրատարակության միջուկում:

Source: www.habr.com

Добавить комментарий