Ինչպես իրականացնել խողովակները Unix-ում

Ինչպես իրականացնել խողովակները Unix-ում
Այս հոդվածում նկարագրվում է Unix միջուկի պողպատների իրականացումը: Ես մի փոքր հիասթափված էի, որ վերջերս հրապարակված "Unix- ում պողպատները ինչպես են աշխատում?" ի վեր չանցած հայտնվեց не ներքին կառուցվածքի վերաբերյալ: Դա հետաքրքրեց ինձ, և ես փորեցի հին աղբյուրները՝ պատասխան գտնելու համար:

Որևէ բանի մասին է խոսքը:

Պողպատները՝ "հավանաբար, Unix-ի ամենակարևոր նորարարությունը"՝ այն գլխավոր առանձնահատկությունն է, որը հիմնված է Unix-ի փիլիսոփայության վրա՝ միացնելով փոքր ծրագրերը, ինչպես նաև հայտնի գրառումը հրամանային տողում:

$ echo hello | wc -c
6

Այս գործառույթը կախված է միջուկի կողմից մատուցվող համակարգի կանչից pipe, որն նկարագրվում է փաստաթղթավորման էջերում pipe(7) և pipe(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(), որպեսզի ստանա միացված ֆայլերի նկարագրիչներ: Մեկ որդին գրանցում է մեկ նկարագրիչում, իսկ մյուս պրոցեսը կարդում է նույն տվյալները մյուս նկարագրիչից: Ռոն կարող է օգտագործել dup2-ը՝ "վերանվանել" 3 և 4 նկարագրիչները, որպեսզի նրանք համապատասխանեն stdin և stdout-ին:

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

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

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

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

Ինչ ենք փնտրում?

Կպատմեմ պարզով, որպեսզի ձեզ համար ավելի պարզ լինի, թե ինչպես կարող է աշխատել ցանցը: Vám անհրաժեշտ է հիշողությունում բուֆեր تخصնել եւ մի կերպ վիճակ: Ծրագրված ֆայլերի նկարագրման եւ ջնջելու համար անհրաժեշտ կլինեն ფუნქցիաներ: Որպեսզի հասնեք ֆայլային վարորդներին ընթերցման եւ գրելու գործողությունների, կպահանջենք միջոց: Եվ անհրաժեշտ են արգելափակումներ, որպեսզի իրականացնել վերոնշյալ հատուկ վարքագիծը:

ՆNOW մենք պատրաստ ենք քննութ տեսնել ցանցի հիմքային կոդան, որպեսզի հաստատենք կամ հերքենք մեր հինգտլուր մտավոր մոդելը: Բայց միշտ պատրաստ եղեք անսպասելիություններին:

Որտեղ ենք փնտրում?

Ես չգիտեմ, որտեղ է գտնվում իմ հայտնի գրքի օրինակ «Lions book« Unix 6-ի հետճանաչ, բայց շնորհիվ The Unix Heritage Society կարող եք առցանց որոնել անձեռնյալ կոդում նմանապես ավելի հինUnix տարբերակների:

TUHSարխիվներից դասավորելը նման է թանգարան այցելելուն: Մենք կարող ենք տեսնել մեր ընդհանուր պատմությունը, եւ ես հարգում եմ բազմամյա ջանքերը բոլոր այդ նյութերի վերականգնման համար: Դիպուկ զգում եմ մի կտոր, որը դեռ բացակայում է:

Գնալով հին ցանցերի պատմության իմ հետաքրքրությունն հագեցնելով, համեմատություն կարող ենք տեսնել ժամանակակից բջիջները:

Տես դառնալով, pipe է համակարգային կոչում 42- ի աղյուսակում sysent[]. Հակառակությունը:

Tradicional Unix kernels (1970–1974)

Ես չհայտնաբերեցի որեւէ հետք pipe(2) ոչ ոք PDP-7 Unix (հունվար 1970), ոչ Առաջին տարբերակում Unix (նոյեմբեր 1971), ոչ ոչնչացված מקורային կոդը Երկրորդ տարբերակում (հունիս 1972):

TUHS-ն պնդում է, որ երրորդ տարբերակը Unix (փետրվար 1973) առաջին անգամ դարձավ այն տարբերակը, որն ունի ցանցեր:

Եթե ասում եք, որ երրորդ տարբերակը Unix-ն էր վերջին տարիքով, որը գրել էր ասեմբլեր եւ առաջին էր ցանցերով: 1973 թվականին երրորդ տարբերակի բարելավումների վրա սկսվեցին աշխատանքները, եւ կորոնը վերաշարել էր Ս, եւ այդ կերպիվը հանդիսացավ չորրորդ տարբերակը Unix:

Կարդացողներից մեկը գտել է փաստաթղթի սկան, որտեղ Դագ ՄաքԻլրոյն առաջարկել է «ծրագիրը կապել ջրաადგարանի սկզբունքի»:

Ինչպես իրականացնել խողովակները Unix-ում
Բրայան Քերիգանի գրքում «Unix: A History and a Memoir» ցանցի պատմության մեջ այս փաստաթուղթը նույնպես հիշատակվում է: «... դա կախված է եղել իմ Բել լաբորատորիայի գրասենյակում 30 տարի»: Այժմ ՄաքԻլրոյի հարցազրույցը, եւ մեկ այլ պատմություն մաքԻլրոյի աշխատանքից, հեղինակած 2014 թվականին: работы МакИлроя, написанной в 2014-м:

Յունիքսը ստեղծվելու ժամանակ, իմ հետաքրքրությունը կոռուտինների նկատմամբ ստիպեց ինձ խնդրել օпераտիվ համակարգի հեղինակ Կեն Թոմփսոնին, որ տվյալները, որոնք գրանցվում են որեւէ գործընթացում, հանձնվեն ոչ միայն սարքին, այլ նաեւ մեկ այլ գործընթացի ելքին: Կենը որոշեց, որ դա հնարավոր է: Սակայն, որպես մինիմալիստ, նա ցանկանում էր, որ յուրաքանչյուր համակարգային ֆ λειτουργություն կարևոր դեր ունենա: Ճիշտ է, որ գործընթացների միջև անմիջական գրառումը մեծ առավելություն ունի համեմատաբար միջանկյալ ֆայլին գրառելուցո՞ւ: Եվ միայն այն ժամանակ, երբ ես ներկայացրեցի կոնկրետ առաջարկ մի ասույթով «հոսք» և գործընթացների փոխազդեցության սինտաքսի նկարագրությամբ, Կենը վերջապես exclaimed: «Ես դա կանեմ!»

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

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

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

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

pipe(2) չորրորդ տարբերակում ստացավ իր ժամանակակից տեսքը « pipe(fildes)pipe(fildes)»:

ստեղծում է ներմուծման/արտահանման մեխանիզմ, որը կոչում է հոսք: Վերադարձված ֆայլի նկարագիրը կարող է օգտագործվել ընթերցման և գրելու համար: Երբ հոսքում ինչ-որ բան գրանցվում է, այն բուֆերային է մինչև 504 բայթ տվյալների, որից հետո գրումը դադարեցվում է: Հոսքից ընթերցելիս բուֆերացված տվյալները վերցվում են: pipe ընկնում է կառավարման մեխանիզմի վրա, որը կոչվում է կոնվեյեր։ Վերադարձված ֆայլային նկարիչները կարող են օգտագործվել ընթերցանության և գրառման գործողություններում։ Երբ ինչ-որ բան գրանցվում է կոնվեյերում, օգտագործվում է նկարիչը, որը վերադարձվել է r1-ում (համապատասխան fildes[1]), և դա բուֆերիզվում է 4096 բայթ տվյալների, հետևանքում գրառման գործընթացը դադարում է։ Կոնվեյերից ընթերցելով, r0-ում (համապատասխան fildes[0]) վերադարձված նկարիչը վերցնում է տվյալները։

Ընդունվում է, որ կոնվեյերը որոշելուց հետո երկու (կամ ավելի) փոխկապակցված գործընթացներ (ստեղծված հաջորդ հրամաններով fork) փոխանցելու տղերքը կոնվեյերի միջոցով՝ օգտագործելով հրամաններ read և write.

Շելլ ունի սինտակս՝ գծային գործընթացների զանգվածը սահմանելու համար, որոնք միացած են կոնվեյերով։

Զրո հոսանքից դատարկ կոնվեյերից (որի մեջ չեն գտնվել բուֆերիզված տվյալներ), որը ունի միայն մեկ վերջ, (բոլոր գրող ֆայլային նկարիչները փակված են) վերադարձնում է «ֆայլի վերջ»։ Անալոգիական դեպքում գրելու հրամաններըIgnoredededմնացյալում։

Անհրաժեշտ ամենաառաջին պահպանված կոնվեյերի իրականացում կապվում է Unix-ի հինգերորդ խմբագրության հետ (1974 թվականի հունիս), սակայն այն գրեթե նույնն է, ինչ հաջորդ թողարկման մեջ հայտնվելը։ Միայն մեկնաբանություններ ավելացվել են, այնպես որ հինգերորդ խմբագրությունը կարելի է բաց թողնել։

Unix-ի վեցերորդ խմբագրություն (1975)

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

Պինտից տարիներ շարունակ գրքի Lions լինում էր միակ փաստաթուղթը Unix-ի միջուկի մասին, որը հասանելի էր Բելլ լաբորատորիաներից դուրս։ Թեև վեցերորդ խմբագրության լիցենզիան թույլատրում էր դասախոսներին օգտագործել դրա աղբյուրային կոդը, հետագայում յոթերորդ խմբագրության լիցենզիան թույլ չտվեց այդ հնարավորությունը, հետևաբար, գիրքը տարածվում էր անօրինական մեքենագրության օրինակներով։

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

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

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

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

Ինչպես նաև, առաջին հայացքից, C-kոդի գլխավոր առանձնահատկությունն ապրանքի հիմնադրման ժամանակ է ճնշվածություն։ Ավելի հազվադեպ եմ կարողանում հատվածներ կոդից զետեղել առանց լայն խմբագրման, որպեսզի դրանք համապատասխանեն իմ էջի համեմատաբար նեղ ցուցադրմանը։

Ավելի վաղ /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()

pipe() մի սխալված ճանապարհներում (error paths) հղումների հաշվարկը նվազեցնելու համար նոր inode-ի։ » և ռեգիստրները և R0 պետք է վերադարձի միջոցով և օգտագործելով վարելու սեղանի համար անհրաժեշտ ֆայլերի համար ֆայլի դեստրուկտորների համար։ վերադարձնում է ֆայլի կառուցվածքի վրա ցույց, բայց նաև «վերադարձնում է» u.u_ar0[R0] r և ֆայլի դեստրուկտոր։ Այսինքն, կոդը պահպանում է վերադարձնում է ֆայլի կառուցվածքի վրա ցույց, բայց նաև «վերադարձնում է» ֆայլի դեստրուկտորի համար ընթերցման եւ հատկացնում է գրառման համար ուղղակիորեն և օգտագործելով.

Դրոշ FPIPEորը մենք վերաբերում էինք, երբ ստեղծել ենք խմբաքանակ, վերահսկում է irdwr() ֆունկցիայի վարքագիծը 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 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. Այնուհետև снимаем блокировку и пытаемся пробудить любой процесс, который ждёт возможности прочитать из конвейера. Возвращаемся к началу, чтобы посмотреть, удалось ли записать столько байтов, сколько нам было нужно: если не удалось, то начинаем новый цикл записи.

ԶGenerally, the parameter 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() . для того же канала. ։ Նորից կրկնում եմ, կոդը բարդացել է արգումենտների փոխանցման համաձայնության առանձնահատկություններից, բայց որոշ մանրամասներ կան, որոնք կարող են բաց թողնվել։ և pipe.c ձևավորում են իրենց գործողությունները զույգ կոչերի միջոցով։ Զգուշացեք, որ օգտագործում է տվյալները տրանսպորտից։ Բայց ավելի լավ է իրականացումը հետևել սկսելով հ_ALWAYS գերադասում է PPIPE կոչի ընթացքում sleep(), ուստի բոլորը sleep() կարող են ընդհատվել ազդանշանի միջոցով։

Այժմ մենք ունենք ամենը, որպեսզի հասկանալով գործառույթը pipe.c:

readp(fp)
int *fp;
{
    register *rp, *ip;

    rp = fp;
    ip = rp->f_inode;

loop:
    /* Շատ պահպանողական կոդավորում */

    plock(ip);

    /*
     * Եթե գլխիկը (կարդալը) հասավ
     * պոչին (գրել), վերականգնել 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);
            }
        }

        /*
         * Եթե չկա ոչ ընթերցող և
         * գրող ակտիվ, վերադարձնել առանց
         * բավարարելու կարդալը։
         * /

        prele(ip);
        if(ip->i_count i_mode |= IREAD;
        sleep(ip+2, PPIPE);
        goto loop;
    }

    /* Կարդալ և վերադարձնել */

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

Հնարավոր է, ձեզ համար ավելի հեշտ լինի ընթերցել այս գործառույթը ներքևից դեպի վեր: «կարդալ և վերադարձնել» ճյուղը սովորաբար օգտագործվում է, երբ խողովակներում որոշ տվյալներ ունենք: Այս դեպքում մենք օգտագործում ենք readi() կարդալու թվով տվյալները, որոնք հասանելի են ներկայիս f_offset կարդալուց, հետո թարմացնում ենք համապատասխան տեղադրման արժեքը։

Հաջորդ կարդալու ժամանակ խողովակը կլինի դատարկ, եթե ընթերցման տեղաշարժն հասել է i_size1 inode-ի արժեքին։ Մենք վերաբերվում ենք դիրքը 0 -ին և փորձում ենք արթնացնել որևէ գործընթաց, որը ցանկանում է գրել խողովակ։ Մենք գիտենք, որ երբ խողովակը լի կլինի, ։ Նորից կրկնում եմ, կոդը բարդացել է արգումենտների փոխանցման համաձայնության առանձնահատկություններից, բայց որոշ մանրամասներ կան, որոնք կարող են բաց թողնվել։ զարկ կհասնի ip+1։ Իսկ հիմա, երբ խողովակը դատարկ է, մենք կարող ենք արթնացնել նրան, որպեսզի նա վերսկսի իր գրառման ցիկլը։

Եթե կարդալու ոչինչ չունենք, ապա pipe.c կարող է տեղադրել ախտանշան IREAD և արթնանալ ip+2։ Մենք գիտենք, որ նրան արթնացնելու է, երբ անցորդի կանոնադրությամբ որոշ տվյալներ կգրվեն խողովակ։ ։ Նորից կրկնում եմ, կոդը բարդացել է արգումենտների փոխանցման համաձայնության առանձնահատկություններից, բայց որոշ մանրամասներ կան, որոնք կարող են բաց թողնվել։Մեկնաբանությունները

readi() և writei() օգնում են հասկանալ, որ «u» միջոցով պարամետրեր փոխանցելու փոխարեն, մենք կարող ենք դրանցով աշխատել, ինչպես սովորական մուտքային-հետադարձ ֆունկցիաներ, որոնք վերցնում են ֆայլը, դիրքը, հիշողության բուֆերը և հաշվում են կարդալու կամ գրելու համար բայԹը։ Ինչ վերաբերում է «պահպանողական» կոդավորմանը, ապաinode-ը փակվում է, մինչև նրանց գործառույթը կամ արդյունքը (այսինքն՝ կանչում են) wakeup,

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

plock() pipe.c և ։ Նորից կրկնում եմ, կոդը բարդացել է արգումենտների փոխանցման համաձայնության առանձնահատկություններից, բայց որոշ մանրամասներ կան, որոնք կարող են բաց թողնվել։ prele() գործում են պարզապես. մեկ այլ կոչերի հավաքով). sleep և մեզ թույլ են տալիս արթնացնել որևէ գործընթաց, որը պահանջում է նշանակում, որ մենք հենց刚刚հանել ենք։ Ակնհայտ չէ, թե ես ինչու, չկա մեջ վարագույրների և գործում են պարզապես. մեկ այլ կոչերի հավաքով prele(ip) չի կարող լինել,

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

մինչև կանչել pipe.c wakeup(ip+1)։ Առաջին, որ կանչում է իր շրջադարձում, դա plock(ip), կանխագիծը, եթե դեռ չի առաջադրվի նրա շրջադարձը, ուստի կոդը պետք է ինչ-որ կերպ ճիշտ աշխատի։ Եթե նայենք, պարզ է, որ դա միայն նշում է քնած գործընթացը որպես սպասող գործողության, որպեսզի ապագայում ։ Նորից կրկնում եմ, կոդը բարդացել է արգումենտների փոխանցման համաձայնության առանձնահատկություններից, բայց որոշ մանրամասներ կան, որոնք կարող են բաց թողնվել։ sched() իրոք կարողանա սկսել այն։ Այսպեսկանչում է, ստանում է, տալուց հետո pipe.c և կանչում է wakeup()sleep(ip+2) — ամեն ինչի առաջ, երբ վերսկսում է ցիկլը։ pipe.c պահպանվում է wakeup()— կապույտ է,
որ թթվային նշողը 0 -ին IREAD մենք կարող ենք արթնացնել հանրային օրը, երբ նա արթնացնելու է, կոչ անելովsleep(ip+2), ։ Նորից կրկնում եմ, կոդը բարդացել է արգումենտների փոխանցման համաձայնության առանձնահատկություններից, բայց որոշ մանրամասներ կան, որոնք կարող են բաց թողնվել։ մեզ կընդգրկի, երբ նա ունի ևս մի խողովակ, որը կապվում է։

Այսպիսով, խողովակների նկարագրությունը վեցերորդ խմբագրությունում ավարտվեց։ Արժեքավոր կոդ, հեռու հետևանքներով։

Յոթերորդ խմբագրություն Unix (1979 թվականի հունվարի) նոր հիմնական թողարկում էր (քառորդ տարի անց), որտեղ հայտնվեցին բազմաթիվ նոր ծրագրեր և միջուկի հատկություններ։ Նաեւ զգալի փոփոխություններ տեղի ունեցան ձևափոխման, միությունների և կարգավորված ցուցիչների կառավարման պատճառով։ Սակայն խողովակների կոդը prácticamente no ha cambiado. Podemos saltar esta edición.

Xv6, պարզ Unix-աձև միջուկ

Նրա ստեղծման վրա ազդեցին Xv6 Երրորդ խմբագրության Unix-ից, սակայն այն գրված է ժամանակակից C լեզվով, որպեսզի կարողանանք այն աշխատացնել x86 պրոցեսորների վրա։ Կոդը հեշտ է կարդալ, պարզ է։ Բացի այդ, տարբերությամբ Unix-ի աղբյուրներից TUHS-ի, դուք կարող եք այն կազմավորել, փոփոխել և աշխատեցնել այլ բաների վրա, բացի 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 է wrapped, որն իրականացված է sysfile.c։ Խորհուրդ եմ տալիս կարդալ ամբողջ նրա կոդը։ Սложность на уровне исходника шестой редакции, но читать гораздо легче и приятнее։

Linux 0.01

Այստեղ կարող եք գտնել Linux 0.01-ի աղբյուրի կոդը։ Սովորել խողովակների իրականացմանը նրա fs/օգտագործում է տվյալները տրանսպորտից։ Բայց ավելի լավ է իրականացումը հետևել սկսելով։ Այստեղ խողովակի ներկայացման համար օգտագործվում է 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

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

Կարդալու համար fs/օգտագործում է տվյալները տրանսպորտից։ Բայց ավելի լավ է իրականացումը հետևել սկսելով (Լինուքսի վրա) կամ sys/kern/sys_pipe.c (BSD-ների համար) պահանջում է իրական նվիրում: Այսօր կոդում կարևոր են կատարողականը և ֆունկցիաների աջակցության առկայությունը, ինչպես օրինակ՝ վեկտորային և ասինխրոն գործողություններ մուտք-արտուք համար: Միքրօհավելվածների բաշխման, արգելափակման և քերականային կոնֆիգուրացիայի մանրամասները՝ ամենը շատ փոխվում է: Սա այն չէ, ինչ անհրաժեշտ է համալսարաններին օպերացիոն համակարգերի մուտքագրության դասընթացների համար:

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

Դիվի Կապուրի կողմից 2011 թվականի հրապարակելը «The Linux Kernel Implementation of Pipes and FIFOs» ներկայացնում է, թե ինչպես են (այսօր էլ) կոնվեյերը աշխատում Լինուքսում: Եվ Լինուքսում տեղի ունեցած վերջնական կոմիտեն ցույց է տալիս կոնվեյերի մոդելի փոխազդեցությունը, որի հնարավորությունները գերազանցում են ժամանակավոր ֆայլերի հնարավորությունները; և ցույց է տալիս, թե որքան հեռու են գնացել կոնվեյերները «շատ պահպանողական արգելափակման» օրինակով Unix-ի վեցերորդ խմբագրությունում:

Ընտանիք: habr.com

Գնել հուսալի հյուրընկալում DDoS պաշտպանությամբ, VPS VDS սերվերներով 🔥 Գնել հուսալի հյուրընկալում DDoS պաշտպանությամբ, VPS VDS սերվերներով | ProHoster