Unix-də boru kəmərləri necə həyata keçirilir

Unix-də boru kəmərləri necə həyata keçirilir
Bu məqalə Unix nüvəsində boru kəmərlərinin həyata keçirilməsini təsvir edir. “Bu yaxınlarda çıxan məqalə məni bir qədər məyus etdi.Unix-də boru kəmərləri necə işləyir?" bəlli oldu heç bir daxili quruluşu haqqında. Maraqlandım və cavab tapmaq üçün köhnə mənbələri araşdırdım.

Nədir?

Boru kəmərləri "ehtimal ki, Unix-də ən mühüm ixtiradır" - Unix-in kiçik proqramları bir araya toplamaq fəlsəfəsinin müəyyənedici xüsusiyyəti, eləcə də komanda xəttindəki tanış yazı:

$ echo hello | wc -c
6

Bu funksionallıq kernel tərəfindən təmin edilən sistem çağırışından asılıdır pipe, bu sənədlərin səhifələrində təsvir edilmişdir boru (7) и boru (2):

Boru kəmərləri proseslərarası əlaqə üçün birtərəfli kanal təmin edir. Boru kəmərinin girişi (yazma sonu) və çıxışı (oxumaq sonu) var. Boru kəmərinin girişinə yazılan məlumatlar çıxışda oxuna bilər.

Boru kəməri zəng etməklə yaradılır pipe(2), iki fayl deskriptorunu qaytarır: biri boru kəmərinin girişinə, ikincisi çıxışa aiddir.

Yuxarıdakı komandanın iz çıxışı boru kəmərinin yaradılmasını və onun vasitəsilə bir prosesdən digərinə məlumat axınını göstərir:

$ 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

Ana proses çağırır pipe()əlavə edilmiş fayl deskriptorlarını əldə etmək üçün. Bir uşaq proses bir deskriptora yazır və digər proses eyni məlumatları digər deskriptordan oxuyur. Qabıq stdin və stdout-a uyğunlaşdırmaq üçün 2 və 3-cü deskriptorları dup4 ilə "adlandırır".

Boru kəmərləri olmadan, qabıq bir prosesin çıxışını fayla yazmalı və fayldan məlumatları oxumaq üçün onu başqa bir prosesə ötürməli olardı. Nəticədə biz daha çox resurs və disk sahəsi sərf etmiş olarıq. Bununla belə, boru kəmərləri müvəqqəti fayllardan qaçmaqdan daha çox şey üçün yaxşıdır:

Bir proses boş bir boru kəmərindən oxumağa çalışırsa, o zaman read(2) məlumatlar mövcud olana qədər bloklanacaq. Bir proses tam boru xəttinə yazmağa çalışırsa, o zaman write(2) yazmağı tamamlamaq üçün boru kəmərindən kifayət qədər məlumat oxunana qədər bloklanacaq.

POSIX tələbi kimi, bu da mühüm xüsusiyyətdir: boru xəttinə qədər yazmaq PIPE_BUF bayt (ən azı 512) atomik olmalıdır ki, proseslər boru kəməri vasitəsilə bir-biri ilə normal faylların (belə zəmanətlər təqdim etməyən) edə bilmədiyi şəkildə əlaqə saxlasın.

Adi bir fayl ilə proses bütün çıxışını ona yaza və onu başqa prosesə ötürə bilər. Yaxud proseslər yazının və ya oxunmanın tamamlanması haqqında bir-birini məlumatlandırmaq üçün xarici siqnal mexanizmindən (semafor kimi) istifadə edərək sərt paralel rejimdə işləyə bilər. Konveyerlər bizi bütün bu əngəldən xilas edir.

Biz nə axtarırıq?

Konveyerin necə işlədiyini təsəvvür etməyinizi asanlaşdırmaq üçün barmaqlarımla izah edəcəyəm. Yaddaşda bufer və bəzi dövlətlər ayırmalı olacaqsınız. Buferdən məlumat əlavə etmək və silmək üçün sizə funksiyalar lazımdır. Fayl deskriptorlarında oxumaq və yazma əməliyyatları zamanı funksiyaları çağırmaq üçün sizə bəzi qurğular lazımdır. Və yuxarıda təsvir edilən xüsusi davranışı həyata keçirmək üçün kilidlər lazımdır.

Biz indi qeyri-müəyyən zehni modelimizi təsdiq və ya təkzib etmək üçün parlaq lampa işığı altında nüvənin mənbə kodunu sorğulamağa hazırıq. Ancaq həmişə gözlənilməzliyə hazır olun.

Biz hara baxırıq?

Məşhur kitabın nüsxəsinin harada olduğunu bilmirəm.Şirlər kitab« Unix 6 mənbə kodu ilə, lakin sayəsində Unix İrs Cəmiyyəti onlayn axtarış etmək olar mənbə kodu Unix-in hətta köhnə versiyaları.

TUHS arxivlərində dolaşmaq muzeyi ziyarət etmək kimidir. Biz ortaq tariximizə nəzər sala bilərik və mən köhnə kasetlərdən və çap materiallarından bütün bu materialı yavaş-yavaş bərpa etmək üçün uzun illər çəkdiyim səylərə hörmətlə yanaşıram. Mən hələ də itkin düşmüş fraqmentlərdən xəbərdaram.

Boru kəmərlərinin qədim tarixinə olan marağımızı təmin edərək, müqayisə üçün müasir nüvələrə baxa bilərik.

Yeri gəlmişkən, pipe Cədvəldəki 42 nömrəli sistem çağırışıdır sysent[]. Təsadüf?

Ənənəvi Unix nüvələri (1970-1974)

Heç bir iz tapmadım pipe(2) nə də PDP-7 Unix (Yanvar 1970), nə də Unix ilk nəşri (noyabr 1971), nə də natamam mənbə kodunda ikinci nəşr (1972-ci il iyun).

TUHS bunu iddia edir üçüncü nəşr Unix (Fevral 1973) boru kəmərləri ilə ilk versiya idi:

Unix-in üçüncü nəşri assemblerdə yazılmış nüvə ilə son versiya, həm də boru kəmərləri ilə ilk versiya idi. 1973-cü il ərzində üçüncü nəşrin təkmilləşdirilməsi üzərində iş gedirdi, nüvə C-də yenidən yazılır və beləliklə Unix-in dördüncü nəşri yaranır.

Bir oxucu, Doug McIlroy-un "proqramları bağ şlanqı kimi birləşdirmək" ideyasını təklif etdiyi bir sənədin skanını tapdı.

Unix-də boru kəmərləri necə həyata keçirilir
Brayan Kerniqanın kitabındaUnix: Tarix və Xatirə”, konveyerlərin yaranma tarixində bu sənəddən də bəhs edilir: “... o, 30 il ərzində Bell Labs-da mənim ofisimdə divardan asılıb”. Budur McIlroy ilə müsahibəvə başqa bir hekayə McIlroyun 2014-cü ildə yazılmış əsəri:

Unix peyda olanda koroutinlərə olan həvəsim məni OS müəllifi Ken Thompsondan xahiş etdim ki, hansısa prosesə yazılan məlumatların təkcə cihaza deyil, həm də başqa prosesə çıxışına icazə versin. Ken bunun mümkün olduğunu düşünürdü. Bununla belə, minimalist kimi o, hər bir sistem xüsusiyyətinin əhəmiyyətli rol oynamasını istəyirdi. Proseslər arasında birbaşa yazı, ara fayla yazmaqdan daha böyük üstünlükdürmü? Və yalnız mən cəlbedici "boru kəməri" adı və proseslərin qarşılıqlı sintaksisinin təsviri ilə xüsusi bir təklif edəndə, Ken nəhayət qışqırdı: "Mən bunu edəcəm!".

Və etdi. Taleyüklü axşamların birində Ken nüvəni və qabığı dəyişdirdi, girişi necə qəbul etdiyini standartlaşdırmaq üçün bir neçə standart proqramı düzəltdi (bu boru kəmərindən gələ bilər) və fayl adlarını dəyişdirdi. Ertəsi gün boru kəmərləri tətbiqlərdə çox geniş istifadə edildi. Həftənin sonuna qədər katiblər onlardan sənədləri mətn prosessorlarından printerə göndərmək üçün istifadə etdilər. Bir qədər sonra, Ken o vaxtdan bəri istifadə olunan daha təmiz konvensiyalarla boru kəmərlərinin istifadəsini əhatə etmək üçün orijinal API və sintaksisi əvəz etdi.

Təəssüf ki, üçüncü nəşr Unix nüvəsinin mənbə kodu itdi. Baxmayaraq ki, nüvənin mənbə kodu C dilində yazılmışdır dördüncü nəşr, 1973-cü ilin noyabrında buraxıldı, lakin rəsmi buraxılışdan bir neçə ay əvvəl çıxdı və boru kəmərlərinin həyata keçirilməsini ehtiva etmir. Təəssüf ki, bu əfsanəvi Unix funksiyasının mənbə kodu bəlkə də əbədi olaraq itdi.

üçün sənədləşmə mətnimiz var pipe(2) hər iki buraxılışdan, beləliklə, sənədləri axtararaq başlaya bilərsiniz üçüncü nəşr (müəyyən sözlər üçün, altından xətt çəkilmiş "əllə", ^H hərfi, ardınca alt xətt!). Bu proto-pipe(2) assemblerdə yazılır və yalnız bir fayl deskriptorunu qaytarır, lakin artıq gözlənilən əsas funksionallığı təmin edir:

Sistem zəngi boru boru kəməri adlanan I/O mexanizmi yaradır. Qaytarılan fayl deskriptoru oxumaq və yazma əməliyyatları üçün istifadə edilə bilər. Boru kəmərinə bir şey yazıldıqda, o, 504 bayta qədər məlumatı bufer edir, bundan sonra yazı prosesi dayandırılır. Boru kəmərindən oxuyarkən tamponlanmış məlumatlar alınır.

Gələn il nüvə C-də yenidən yazılmışdı və boru (2) dördüncü nəşr prototiplə müasir görkəmini aldı”pipe(fildes)'

Sistem zəngi boru boru kəməri adlanan I/O mexanizmi yaradır. Qaytarılan fayl deskriptorları oxuma və yazma əməliyyatlarında istifadə edilə bilər. Boru kəmərinə nəsə yazıldıqda, r1-də qaytarılmış deskriptordan (cavab. fildes[1]) istifadə olunur, 4096 bayta qədər məlumat buferlənir, bundan sonra yazma prosesi dayandırılır. Boru kəmərindən oxuyarkən r0-a qayıdan deskriptor (cavab. fildes[0]) məlumatları qəbul edir.

Güman edilir ki, boru kəməri müəyyən edildikdən sonra iki (və ya daha çox) qarşılıqlı əlaqədə olan proseslər (sonrakı çağırışlarla yaradılmışdır) yaba) zənglərdən istifadə edərək boru kəmərindən məlumatları ötürəcək oxumaq и yazmaq.

Qabıqda boru kəməri ilə birləşdirilən proseslərin xətti massivini təyin etmək üçün sintaksis var.

Yalnız bir ucu olan (bütün yazı faylı deskriptorları bağlıdır) boş boru kəmərindən oxumağa çağırışlar "faylın sonu"nu qaytarır. Bənzər bir vəziyyətdə yazılan zənglərə məhəl qoyulmur.

Ən erkən qorunan boru kəmərinin icrası aiddir Unix-in beşinci nəşrinə (iyun 1974), lakin o, növbəti buraxılışda görünənlə demək olar ki, eynidir. Yalnız şərhlər əlavə edildi, buna görə də beşinci nəşri buraxmaq olar.

Unix Altıncı Nəşr (1975)

Unix mənbə kodunu oxumağa başlayır altıncı nəşr (May 1975). Çox sağolun Lions onu tapmaq əvvəlki versiyaların mənbələrindən daha asandır:

Uzun illər kitab Lions Bell Laboratoriyasından kənarda mövcud olan Unix ləpəsində yeganə sənəd idi. Altıncı nəşr lisenziyası müəllimlərə öz mənbə kodundan istifadə etməyə icazə versə də, yeddinci nəşr lisenziyası bu imkanı istisna edirdi, buna görə də kitab qeyri-qanuni olaraq maşınla yazılmış nüsxələrdə paylandı.

Bu gün siz, üz qabığında surətçıxarma maşınında tələbələri təsvir edən kitabın təkrar nəşrini ala bilərsiniz. Və Warren Toomey (TUHS layihəsini başlatan) sayəsində yükləyə bilərsiniz Altıncı nəşr mənbə PDF. Mən sizə faylı yaratmaq üçün nə qədər səy sərf edildiyi barədə fikir vermək istəyirəm:

15 ildən çox əvvəl mən təqdim olunan mənbə kodunun surətini daxil etdim Lionsçünki naməlum sayda başqa nüsxələrdən olan surətimin keyfiyyətini bəyənmədim. TUHS hələ mövcud deyildi və mənim köhnə mənbələrə çıxışım yox idi. Lakin 1988-ci ildə mən PDP9 kompüterindən ehtiyat nüsxəsi olan 11 trekdən ibarət köhnə lent tapdım. İşləyib-işləmədiyini bilmək çətin idi, lakin bütöv bir /usr/src/ ağacı var idi ki, orada faylların çoxu 1979-cu il kimi qeyd olunurdu və o zaman da qədim görünürdü. Düşündüm ki, bu, yeddinci nəşr və ya PWB törəməsi idi.

Mən tapıntını əsas götürdüm və mənbələri altıncı nəşrin vəziyyətinə əl ilə redaktə etdim. Kodun bir hissəsi olduğu kimi qaldı, bir hissəsi müasir += işarəsini köhnəlmiş =+ ilə dəyişdirərək bir az redaktə edilməli idi. Bir şey sadəcə silindi və bir şey tamamilə yenidən yazılmalı idi, amma çox deyil.

Və bu gün biz TUHS-də altıncı nəşrin mənbə kodunu onlayn oxuya bilərik Dennis Ritchie-nin əli olduğu arxiv.

Yeri gəlmişkən, ilk baxışdan Kerniqan və Ritçi dövründən əvvəlki C kodunun əsas xüsusiyyəti onun qısalıq. Saytımdakı nisbətən dar ekran sahəsinə sığdırmaq üçün geniş redaktə etmədən kod fraqmentlərini daxil edə bilməyim çox vaxt olmur.

Başlanğıcda /usr/sys/ken/pipe.c izahlı bir şərh var (və bəli, daha çox var /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

Bufer ölçüsü dördüncü nəşrdən bəri dəyişməyib. Ancaq burada heç bir ictimai sənəd olmadan görürük ki, boru kəmərləri bir vaxtlar faylları ehtiyat yaddaş kimi istifadə edirdi!

LARG fayllarına gəldikdə, onlar uyğun gəlir inode-bayraq LARG, emal etmək üçün "böyük ünvanlama alqoritmi" tərəfindən istifadə olunur dolayı bloklar daha böyük fayl sistemlərini dəstəkləmək üçün. Ken onlardan istifadə etməməyin daha yaxşı olduğunu dediyi üçün onun sözünü qəbul etməkdən məmnunam.

Budur əsl sistem çağırışı 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;
}

Şərh burada baş verənləri açıq şəkildə təsvir edir. Amma kodu başa düşmək o qədər də asan deyil, qismən ona görə ki, "struktur istifadəçisi u» və qeydiyyatdan keçir R0 и R1 sistem çağırış parametrləri və qaytarma dəyərləri ötürülür.

ilə cəhd edək ialloc() diskə yerləşdirin inode (inode), və köməyi ilə falloc() - iki saxla fayl. Hər şey qaydasındadırsa, biz bu faylları boru kəmərinin iki ucu kimi müəyyən etmək üçün bayraqlar təyin edəcəyik, onları eyni inoda yönəldəcəyik (istinad sayı 2 olur) və inodu dəyişdirilmiş və istifadədə olan kimi qeyd edəcəyik. müraciətlərə diqqət yetirin spin() yeni inode-da istinad sayını azaltmaq üçün səhv yollarında.

pipe() vasitəsilə R0 и R1 oxumaq və yazmaq üçün fayl deskriptor nömrələrini qaytarın. falloc() fayl strukturuna göstərici qaytarır, həm də vasitəsilə "qaytarır" u.u_ar0[R0] və fayl deskriptoru. Yəni kod saxlanılır r oxumaq üçün fayl deskriptoru və birbaşa ondan yazmaq üçün deskriptor təyin edir u.u_ar0[R0] ikinci zəngdən sonra falloc().

Bayraq FPIPEboru kəmərini yaratarkən təyin etdiyimiz , funksiyanın davranışına nəzarət edir rdwr() sys2.c-də, xüsusi giriş/çıxış qaydalarını çağırır:

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

Sonra funksiya readp() в pipe.c boru kəmərindən məlumatları oxuyur. Amma daha yaxşı olar ki, həyata keçirilməsini ondan başlayaraq izləyəsiniz writep(). Yenə də kod konvensiyadan keçən arqumentin təbiətinə görə daha mürəkkəbləşdi, lakin bəzi detallar buraxıla bilər.

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

Biz boru xəttinin girişinə bayt yazmaq istəyirik u.u_count. Əvvəlcə inodu bağlamalıyıq (aşağıya baxın plock/prele).

Sonra inode istinad sayını yoxlayırıq. Boru kəmərinin hər iki ucu açıq qaldıqca, sayğac 2 olmalıdır. Biz bir linkdən (dan rp->f_inode), belə ki, sayğac 2-dən azdırsa, bu, oxu prosesinin boru kəmərinin sonunu bağladığını bildirməlidir. Başqa sözlə, biz qapalı boru xəttinə yazmağa çalışırıq, bu, səhvdir. İlk səhv kodu EPIPE və siqnal SIGPIPE Unix-in altıncı nəşrində çıxdı.

Amma konveyer açıq olsa belə, dolu ola bilər. Bu vəziyyətdə, kilidi buraxırıq və başqa bir prosesin boru kəmərindən oxuyacağı və içərisində kifayət qədər yer açacağı ümidi ilə yuxuya gedirik. Yuxudan oyandıqdan sonra başlanğıca qayıdırıq, kilidi yenidən asırıq və yeni yazma dövrünə başlayırıq.

Boru kəmərində kifayət qədər boş yer varsa, istifadə edərək ona məlumat yazırıq yaz(). Parametr i_size1 inode'a (boş boru kəməri ilə 0-a bərabər ola bilər) artıq ehtiva etdiyi məlumatın sonuna işarə edir. Yazmaq üçün kifayət qədər yer varsa, boru kəmərini doldura bilərik i_size1 üzrə PIPESIZ. Sonra kilidi buraxırıq və boru kəmərindən oxunmasını gözləyən hər hansı bir prosesi oyatmağa çalışırıq. Lazım olan qədər bayt yaza bildiyimizi görmək üçün başlanğıca qayıdırıq. Əgər yoxsa, o zaman yeni qeyd dövrünə başlayırıq.

Adətən parametr i_mode inode icazələri saxlamaq üçün istifadə olunur r, w и x. Ancaq boru kəmərləri vəziyyətində, bəzi prosesin bitlərdən istifadə edərək yazmağı və ya oxumağı gözlədiyini bildiririk IREAD и IWRITE müvafiq olaraq. Proses bayrağı və zəngləri təyin edir sleep(), və gələcəkdə başqa bir prosesin çağırılacağı gözlənilir wakeup().

Əsl sehr içində baş verir sleep() и wakeup(). Onlar həyata keçirilir slp.c, məşhur mənbə "Bunu başa düşməyiniz gözlənilmir" şərhi. Xoşbəxtlikdən kodu anlamaq məcburiyyətində deyilik, sadəcə bəzi şərhlərə baxın:

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

Zəng edən proses sleep() müəyyən bir kanal üçün, daha sonra zəng edəcək başqa bir proses tərəfindən oyandırıla bilər wakeup() eyni kanal üçün. writep() и readp() belə qoşalaşmış zənglər vasitəsilə öz hərəkətlərini əlaqələndirirlər. qeyd edin ki pipe.c həmişə prioritet PPIPE çağıranda sleep(), belə ki, hamısı sleep() siqnalla kəsilə bilər.

İndi funksiyanı başa düşmək üçün hər şeyimiz var 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);
}

Bu funksiyanı aşağıdan yuxarı oxumaq daha asan ola bilər. "Oxumaq və qaytarmaq" bölməsi adətən boru kəmərində bəzi məlumatlar olduqda istifadə olunur. Bu vəziyyətdə istifadə edirik oxu () mövcud olandan başlayaraq mümkün qədər çox məlumat oxuyun f_offset oxuyun və sonra müvafiq ofsetin dəyərini yeniləyin.

Sonrakı oxunuşlarda oxunuş ofsetinə çatdıqda boru kəməri boş olacaq i_size1 inode. Mövqeyi 0-a qaytarırıq və boru xəttinə yazmaq istəyən hər hansı bir prosesi oyatmağa çalışırıq. Biz bilirik ki, konveyer dolu olduqda, writep() yatmaq ip+1. İndi boru kəməri boşdur, biz onu oyandıraraq yazma dövrünü davam etdirə bilərik.

Oxumağa bir şey yoxdursa, deməli readp() bayraq qoya bilər IREAD və yuxuya get ip+2. Onu nəyin oyatacağını bilirik writep()boru kəmərinə bəzi məlumatları yazdıqda.

Şərhlər oxu () və yaz () Parametrləri keçmək əvəzinə, başa düşməyə kömək edəcək "u» biz onlara fayl, mövqe, yaddaşda bufer götürən və oxumaq və ya yazmaq üçün baytların sayını hesablayan adi I/O funksiyaları kimi davrana bilərik.

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

O ki qaldı “mühafizəkar” bloklamaya, o zaman readp() и writep() bitənə və ya nəticə əldə edənə qədər inodları kilidləyin (yəni zəng edin wakeup). plock() и prele() sadə işləyin: fərqli zənglər dəstindən istifadə edin sleep и wakeup İndi buraxdığımız kilidə ehtiyac duyan hər hansı bir prosesi oyatmağa icazə verin:

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

Əvvəlcə səbəbini başa düşə bilmədim readp() səbəb olmur prele(ip) zəngdən əvvəl wakeup(ip+1). İlk şey writep() öz döngəsində çağırır, bu plock(ip), əgər dalana səbəb olur readp() hələ blokunu silməyib, ona görə də kod birtəhər düzgün işləməlidir. Baxsanız wakeup(), aydın olur ki, o, yalnız yuxu prosesini icraya hazır olaraq qeyd edir, belə ki, gələcəkdə sched() həqiqətən işə saldı. Belə ki readp() səbəbləri wakeup(), kilidləri açır, təyin edir IREAD və zənglər sleep(ip+2)- bütün bunlar əvvəl writep() dövrü yenidən başlayır.

Bu, altıncı nəşrdə boru kəmərlərinin təsvirini tamamlayır. Sadə kod, geniş təsirlər.

Yeddinci Nəşr Unix (Yanvar 1979) bir çox yeni proqramlar və nüvə xüsusiyyətlərini təqdim edən yeni əsas buraxılış (dörd il sonra) idi. O, həmçinin tipli tökmə, birləşmələr və strukturlara tipli göstəricilərin istifadəsi ilə əlaqədar əhəmiyyətli dəyişikliklərə məruz qalmışdır. Lakin boru kəmərləri kodu praktiki olaraq dəyişməyib. Bu nəşri keçə bilərik.

Xv6, Unix-ə bənzər sadə nüvədir

Nüvə yaratmaq üçün Xv6 Unix-in altıncı nəşrindən təsirlənmiş, lakin x86 prosessorlarında işləmək üçün müasir C dilində yazılmışdır. Kodu oxumaq və başa düşmək asandır. Həmçinin, TUHS ilə Unix mənbələrindən fərqli olaraq, siz onu tərtib edə, dəyişdirə və PDP 11/70-dən başqa bir şeydə işlədə bilərsiniz. Buna görə də bu nüvədən əməliyyat sistemləri üzrə tədris materialı kimi universitetlərdə geniş istifadə olunur. Mənbələr Github-dadır.

Kod aydın və düşünülmüş icradan ibarətdir boru.c, diskdəki inode əvəzinə yaddaşdakı bufer tərəfindən dəstəklənir. Burada mən yalnız “struktur boru kəməri”nin tərifini və funksiyasını verirəm 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() funksiyaları ehtiva edən icranın qalan bütün hissələrinin vəziyyətini təyin edir piperead(), pipewrite() и pipeclose(). Həqiqi sistem çağırışı sys_pipe -də həyata keçirilən sarğıdır sysfile.c. Mən onun bütün kodunu oxumağı məsləhət görürəm. Mürəkkəblik altıncı nəşrin mənbə kodu səviyyəsindədir, lakin oxumaq daha asan və daha xoşdur.

Linux 0.01

Linux 0.01 üçün mənbə kodunu tapa bilərsiniz. Onun ərazisində boru kəmərlərinin həyata keçirilməsini öyrənmək ibrətamiz olacaqdır fs/pipe.c. Burada boru kəmərini təmsil etmək üçün inode istifadə olunur, lakin boru kəmərinin özü müasir C dilində yazılmışdır. Əgər altıncı nəşr kodunu sındırmısınızsa, burada heç bir probleminiz olmayacaq. Funksiya belə görünür 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;
}

Struktur təriflərinə baxmadan belə, yazma əməliyyatının nəticə verib-vermədiyini yoxlamaq üçün inode istinad sayının necə istifadə olunduğunu anlaya bilərsiniz. SIGPIPE. Bayt-bayt işinə əlavə olaraq, bu funksiyanı yuxarıdakı fikirlərlə müqayisə etmək asandır. Hətta məntiq sleep_on/wake_up o qədər də yad görünmür.

Müasir Linux nüvələri, FreeBSD, NetBSD, OpenBSD

Mən tez bəzi müasir ləpələri nəzərdən keçirdim. Onların heç birinin artıq disk əsaslı tətbiqi yoxdur (təəccüblü deyil). Linux-un öz tətbiqi var. Üç müasir BSD nüvəsi Con Dayson tərəfindən yazılmış koda əsaslanan tətbiqləri ehtiva etsə də, illər keçdikcə onlar bir-birindən çox fərqli oldular.

Oxumaq fs/pipe.c (Linux-da) və ya sys/kern/sys_pipe.c (*BSD-də), bu, əsl fədakarlıq tələb edir. Performans və vektor və asinxron I/O kimi funksiyalar üçün dəstək bu gün kodda vacibdir. Yaddaşın ayrılması, kilidlər və nüvə konfiqurasiyasının təfərrüatları çox fərqlidir. Bu, universitetlərə əməliyyat sistemləri ilə bağlı giriş kursu üçün lazım olan şey deyil.

Hər halda, mənim üçün bir neçə köhnə nümunələri (məsələn, generating SIGPIPE və qayıt EPIPE qapalı boru xəttinə yazarkən) bütün bu, çox fərqli, müasir nüvələrdə. Mən yəqin ki, heç vaxt PDP-11 kompüterini canlı görməyəcəyəm, amma mən doğulmamışdan bir neçə il əvvəl yazılmış koddan öyrənməli olduğum çox şey var.

Divi Kapur tərəfindən 2011-ci ildə yazılmış məqalə "Boruların və FİFO-ların Linux Kernel TətbiqiLinux boru kəmərlərinin (indiyə qədər) necə işlədiyinə dair ümumi baxışdır. A linux-da son öhdəlik imkanları müvəqqəti faylların imkanlarından artıq olan qarşılıqlı əlaqənin boru kəməri modelini təsvir edir; və həmçinin altıncı nəşr Unix nüvəsində boru kəmərlərinin "çox mühafizəkar kilidləmə"dən nə qədər uzaqlaşdığını göstərir.

Mənbə: www.habr.com

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