Այս հոդվածը նկարագրում է խողովակաշարերի իրականացումը Unix միջուկում: Ես ինչ-որ չափով հիասթափված էի, որ վերջին հոդվածը վերնագրված էր «
Ինչի՞ մասին ենք խոսում։
Խողովակաշարերը «Յունիքսում թերևս ամենակարևոր գյուտն են»՝ Unix-ի հիմքում ընկած փիլիսոփայության որոշիչ հատկանիշը՝ փոքր ծրագրերը հավաքելու և հրամանի տողի հայտնի կարգախոսը.
$ echo hello | wc -c
6
Այս ֆունկցիոնալությունը կախված է միջուկի կողմից տրամադրված համակարգի զանգից pipe
, որը նկարագրված է փաստաթղթերի էջերում
Խողովակաշարերը ապահովում են միակողմանի ալիք միջգործընթացային հաղորդակցության համար: Խողովակաշարն ունի մուտքային (գրելու վերջ) և ելք (կարդալու վերջ): Խողովակաշարի մուտքագրման վրա գրված տվյալները կարելի է կարդալ ելքի վրա:
Խողովակաշարը ստեղծվում է զանգով
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) պետք է լինեն ատոմային, որպեսզի գործընթացները կարողանան միմյանց հետ հաղորդակցվել խողովակաշարի միջոցով այնպես, որ նորմալ ֆայլերը (որոնք նման երաշխիքներ չեն տալիս) չեն կարող:
Սովորական ֆայլով պրոցեսը կարող է գրել իր ամբողջ արդյունքը և փոխանցել այն մեկ այլ գործընթացի: Կամ պրոցեսները կարող են աշխատել կոշտ զուգահեռ ռեժիմում՝ օգտագործելով արտաքին ազդանշանային մեխանիզմ (ինչպես սեմալիստ), որպեսզի միմյանց տեղեկացնեն գրելու կամ կարդալու ավարտի մասին: Փոխակրիչները մեզ փրկում են այս բոլոր քաշքշուկներից:
Ի՞նչ ենք մենք փնտրում։
Ես կբացատրեմ իմ մատների վրա, որպեսզի ավելի հեշտ պատկերացնեմ, թե ինչպես կարող է փոխակրիչը աշխատել: Ձեզ անհրաժեշտ կլինի հիշողության մեջ հատկացնել բուֆեր և որոշակի վիճակ: Ձեզ անհրաժեշտ կլինեն բուֆերից տվյալներ ավելացնելու և հեռացնելու գործառույթներ: Ձեզ անհրաժեշտ կլինի որոշակի հնարավորություն՝ ֆայլերի նկարագրիչների վրա կարդալու և գրելու գործողությունների ժամանակ գործառույթներ կանչելու համար: Իսկ վերը նկարագրված հատուկ վարքագիծն իրականացնելու համար անհրաժեշտ են կողպեքներ:
Այժմ մենք պատրաստ ենք հարցաքննել միջուկի սկզբնական կոդը վառ լամպի լույսի ներքո՝ հաստատելու կամ հերքելու մեր անորոշ մտավոր մոդելը: Բայց միշտ պատրաստ եղեք անսպասելիին։
Որտե՞ղ ենք մենք փնտրում:
Ես չգիտեմ, թե որտեղ է գտնվում հայտնի գրքի իմ օրինակը։
TUHS-ի արխիվներում թափառելը նման է թանգարան այցելելուն: Մենք կարող ենք նայել մեր ընդհանուր պատմությանը, և ես հարգանքով եմ վերաբերվում այս ամբողջ նյութը հին ձայներիզներից և տպագրություններից քիչ առ մաս վերականգնելու տարիների ջանքերին: Եվ ես խորապես տեղյակ եմ այդ բեկորների մասին, որոնք դեռևս բացակայում են։
Բավարարելով մեր հետաքրքրասիրությունը խողովակաշարերի հնագույն պատմության վերաբերյալ՝ համեմատության համար կարող ենք նայել ժամանակակից միջուկներին:
Ի դեպ, pipe
աղյուսակում նշված է համակարգային զանգի համարը 42 sysent[]
. Պատահականությո՞ւն։
Ավանդական Unix միջուկներ (1970–1974)
Ոչ մի հետք չգտա pipe(2)
ոչ ներս
TUHS-ը պնդում է, որ
Unix-ի երրորդ հրատարակությունը վերջին տարբերակն էր միջուկով, որը գրված էր assembler-ով, բայց նաև առաջին տարբերակը խողովակաշարերով: 1973-ի ընթացքում աշխատանքներ էին տարվում երրորդ հրատարակությունը բարելավելու ուղղությամբ, միջուկը վերաշարադրվեց C-ով, և այդպիսով ծնվեց Յունիքսի չորրորդ հրատարակությունը։
Ընթերցողներից մեկը գտել է մի փաստաթղթի սկան, որտեղ Դագ Մաքիլրոյն առաջարկել է «այգու խողովակի նման ծրագրերը միացնելու» գաղափարը:
Բրայան Քերնիգանի գրքում
Երբ Unix-ը հայտնվեց, կորուտինների հանդեպ իմ կիրքը ստիպեց ինձ խնդրել ՕՀ-ի հեղինակ Քեն Թոմփսոնին, որպեսզի թույլ տա, որ ինչ-որ գործընթացում գրված տվյալները գնան ոչ միայն սարք, այլև դեպի մեկ այլ գործընթաց: Քենը որոշեց, որ դա հնարավոր է: Այնուամենայնիվ, որպես մինիմալիստ, նա ցանկանում էր, որ համակարգի յուրաքանչյուր հատկանիշ էական դեր խաղա: Արդյո՞ք գործընթացների միջև ուղղակի գրելը մեծ առավելություն է միջանկյալ ֆայլի վրա գրելու նկատմամբ: Եվ միայն այն ժամանակ, երբ ես կոնկրետ առաջարկ արեցի գրավիչ «խողովակաշար» անունով և գործընթացների փոխազդեցության շարահյուսության նկարագրությամբ, Քենը վերջապես բացականչեց. «Ես կանեմ դա»:
Եվ արեց: Մի ճակատագրական երեկո Քենը փոխեց միջուկը և կեղևը, ֆիքսեց մի քանի ստանդարտ ծրագրեր՝ ստանդարտացնելու համար, թե ինչպես են նրանք ընդունում մուտքերը (որը կարող է գալ խողովակաշարից) և փոխեց ֆայլերի անունները: Հաջորդ օրը խողովակաշարերը շատ լայնորեն կիրառվեցին կիրառություններում: Շաբաթվա վերջում քարտուղարներն օգտագործում էին դրանք՝ տեքստային պրոցեսորներից փաստաթղթեր ուղարկելու տպիչ։ Որոշ ժամանակ անց Քենը փոխարինեց սկզբնական API-ն և շարահյուսությունը՝ խողովակաշարերի օգտագործումը ավելի մաքուր կոնվենցիաներով փաթաթելու համար, որոնք այդ ժամանակվանից կիրառվել են:
Ցավոք, երրորդ հրատարակության Unix միջուկի սկզբնական կոդը կորել է: Եվ չնայած մենք ունենք միջուկի սկզբնական կոդը գրված C-ով
Մենք ունենք փաստաթղթերի տեքստ pipe(2)
երկու թողարկումներից, այնպես որ կարող եք սկսել փաստաթղթերը որոնելով pipe(2)
գրված է assembler-ում և վերադարձնում է միայն մեկ ֆայլի նկարագրիչ, բայց արդեն ապահովում է ակնկալվող հիմնական ֆունկցիոնալությունը.
Համակարգային զանգ խողովակ ստեղծում է I/O մեխանիզմ, որը կոչվում է խողովակաշար: Վերադարձված ֆայլի նկարագրիչը կարող է օգտագործվել կարդալու և գրելու գործողությունների համար: Երբ ինչ-որ բան գրվում է խողովակաշարում, այն պահում է մինչև 504 բայթ տվյալներ, որից հետո գրելու գործընթացը կասեցվում է: Խողովակաշարից կարդալիս վերցվում են բուֆերացված տվյալները:
Հաջորդ տարի միջուկը վերաշարադրվել էր C-ով և pipe(fildes)
»:
Համակարգային զանգ խողովակ ստեղծում է I/O մեխանիզմ, որը կոչվում է խողովակաշար: Վերադարձված ֆայլի նկարագրիչները կարող են օգտագործվել կարդալու և գրելու գործողություններում: Երբ ինչ-որ բան գրվում է խողովակաշարում, օգտագործվում է r1-ով վերադարձված նկարագրիչը (resp. fildes[1]), որը բուֆերացված է մինչև 4096 բայթ տվյալների, որից հետո գրելու գործընթացը կասեցվում է: Խողովակաշարից կարդալիս, r0-ին վերադարձված նկարագրիչը (resp. fildes[0]) վերցնում է տվյալները:
Ենթադրվում է, որ երբ խողովակաշարը սահմանվել է, երկու (կամ ավելի) փոխազդող գործընթացներ (ստեղծվել են հետագա կանչերի միջոցով) պատառաքաղ) կփոխանցի տվյալներ խողովակաշարից՝ օգտագործելով զանգեր կարդալ и գրել.
Կեղևն ունի շարահյուսություն՝ խողովակաշարի միջոցով միացված գործընթացների գծային զանգված սահմանելու համար:
Դատարկ խողովակաշարից կարդալու կոչերը (որը չի պարունակում բուֆերացված տվյալներ), որն ունի միայն մեկ ծայր (գրելու ֆայլի բոլոր նկարագրիչները փակ են) վերադարձնում են «ֆայլի վերջ»: Նման իրավիճակում գրելու զանգերն անտեսվում են:
Ամենավաղը
Unix Sixth Edition (1975)
Սկսում է կարդալ Unix-ի սկզբնական կոդը
Երկար տարիներ գիրքը Lions Unix միջուկի միակ փաստաթուղթն էր, որը հասանելի էր Bell Labs-ից դուրս: Չնայած վեցերորդ հրատարակության լիցենզիան ուսուցիչներին թույլ էր տալիս օգտագործել իր սկզբնական կոդը, յոթերորդ հրատարակության լիցենզիան բացառում էր այդ հնարավորությունը, ուստի գիրքը տարածվեց անօրինական մեքենագրված օրինակներով:
Այսօր դուք կարող եք գնել գրքի վերահրատարակված օրինակը, որի շապիկին պատկերված են ուսանողները պատճենահանման սարքի մոտ: Եվ շնորհիվ Ուորեն Թումիի (ով սկսել է TUHS նախագիծը), կարող եք ներբեռնել
Ավելի քան 15 տարի առաջ ես մուտքագրեցի ելակետային կոդի պատճենը, որը տրամադրված էր Lionsքանի որ ինձ դուր չեկավ իմ կրկնօրինակի որակը անհայտ թվով այլ օրինակներից: TUHS-ը դեռ գոյություն չուներ, և ես մուտք չունեի դեպի հին աղբյուրները: Բայց 1988-ին ես գտա մի հին ժապավեն՝ 9 հետքերով, որը պահուստային պահուստ ուներ PDP11 համակարգչից: Դժվար էր իմանալ, թե արդյոք այն աշխատում էր, բայց կար մի անձեռնմխելի /usr/src/ ծառ, որտեղ ֆայլերի մեծ մասը նշված էր 1979 թ., որը նույնիսկ այն ժամանակ հնաոճ տեսք ուներ: Ես մտածեցի, որ դա յոթերորդ հրատարակությունն էր, կամ PWB ածանցյալ:
Ես հիմք վերցրեցի գտածոն և ձեռքով խմբագրեցի աղբյուրները մինչև վեցերորդ հրատարակության վիճակը: Կոդի մի մասը մնաց նույնը, մի մասը պետք է մի փոքր խմբագրվեր՝ փոխելով ժամանակակից նշանը += հնացած =+ի։ Ինչ-որ բան ուղղակի ջնջվեց, և ինչ-որ բան պետք է ամբողջությամբ վերաշարադրվեր, բայց ոչ շատ:
Եվ այսօր մենք կարող ենք առցանց կարդալ TUHS-ի վեցերորդ հրատարակության սկզբնական կոդը
Ի դեպ, առաջին հայացքից Քերնիգանի և Ռիչիի ժամանակաշրջանից առաջ C կոդի հիմնական առանձնահատկությունն այն է. հակիրճություն. Հաճախ չէ, որ ես կարողանում եմ կոդի հատվածներ տեղադրել առանց լայնածավալ խմբագրման՝ իմ կայքի համեմատաբար նեղ ցուցադրման տարածքը տեղավորելու համար:
Սկզբում
/*
* 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 ֆայլերին, դրանք համապատասխանում են
Ահա իրական համակարգի զանգը 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;
}
Մեկնաբանությունը հստակ նկարագրում է, թե ինչ է կատարվում այստեղ։ Բայց ծածկագիրը հասկանալն այնքան էլ հեշտ չէ, մասամբ այն պատճառով, թե ինչպես է «R0
и R1
փոխանցվում են համակարգի զանգի պարամետրերը և վերադարձի արժեքները:
Եկեք փորձենք հետ
pipe()
պայմանավորված միջոցով R0
и R1
վերադարձնել ֆայլերի նկարագրության համարները կարդալու և գրելու համար: falloc()
վերադարձնում է ցուցիչը ֆայլի կառուցվածքին, բայց նաև «վերադարձնում է» միջոցով u.u_ar0[R0]
և ֆայլի նկարագրիչ: Այսինքն, կոդը պահվում է r
ֆայլի նկարագրիչ՝ կարդալու համար և նշանակում է նկարագրիչ՝ անմիջապես գրելու համար u.u_ar0[R0]
երկրորդ զանգից հետո falloc()
.
Դրոշ FPIPE
, որը մենք սահմանել ենք խողովակաշարը ստեղծելիս, վերահսկում է ֆունկցիայի վարքը
/*
* 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()
. Դրանք իրականացվում են
/*
* 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()
վերսկսում է ցիկլը:
Սա լրացնում է խողովակաշարերի նկարագրությունը վեցերորդ հրատարակության մեջ: Պարզ ծածկագիր, հեռուն գնացող հետևանքներ:
Xv6, պարզ Unix-ի նման միջուկ
Միջուկ ստեղծելու համար
Օրենսգիրքը պարունակում է հստակ և մտածված իրականացում 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
փաթաթան է, որն իրականացվում է
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 թվականին, հոդվածը «
Source: www.habr.com