Unix'te işlem hatları nasıl uygulanır?

Unix'te işlem hatları nasıl uygulanır?
Bu makale, Unix çekirdeğinde boru hatlarının uygulanmasını açıklamaktadır. " başlıklı yeni bir makale beni biraz hayal kırıklığına uğrattı.Boru hatları Unix'te nasıl çalışır?» çıktı hayır iç yapı hakkında. Merak ettim ve cevabı bulmak için eski kaynakları araştırdım.

Ne bahsediyoruz?

Ardışık hatlar, "Unix'teki muhtemelen en önemli icattır" - Unix'in altında yatan küçük programları bir araya getirme felsefesinin ve tanıdık komut satırı sloganının belirleyici bir özelliği:

$ echo hello | wc -c
6

Bu işlevsellik, çekirdek tarafından sağlanan sistem çağrısına bağlıdır pipe, dokümantasyon sayfalarında açıklanan boru(7) и boru(2):

İşlem hatları, süreçler arası iletişim için tek yönlü bir kanal sağlar. İşlem hattının bir girişi (yazma ucu) ve bir çıkışı (okuma ucu) vardır. Boru hattının girişine yazılan veriler çıkışta okunabilir.

Ardışık düzen çağrılarak oluşturulur pipe(2), iki dosya tanıtıcı döndürür: biri ardışık düzenin girişini, ikincisi ise çıktıyı ifade eder.

Yukarıdaki komutun izleme çıktısı, bir ardışık düzenin oluşturulmasını ve bir işlemden diğerine veri akışını gösterir:

$ 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

Üst süreç çağrıları pipe()ekli dosya tanımlayıcılarını almak için. Bir alt süreç, bir tanımlayıcıya yazar ve başka bir süreç, başka bir tanımlayıcıdan aynı verileri okur. Kabuk, stdin ve stdout ile eşleşmesi için 2 ve 3 tanımlayıcılarını dup4 ile "yeniden adlandırır".

Ardışık hatlar olmadan, kabuğun bir işlemin çıktısını bir dosyaya yazması ve dosyadaki verileri okumak için onu başka bir işleme aktarması gerekir. Sonuç olarak, daha fazla kaynak ve disk alanı israf etmiş oluruz. Ancak, işlem hatları geçici dosyalardan kaçınmaktan daha fazlası için iyidir:

Bir işlem boş bir ardışık düzenden okumaya çalışırsa, o zaman read(2) veriler mevcut olana kadar engelleyecektir. Bir işlem tam bir boru hattına yazmaya çalışırsa, o zaman write(2) yazmayı tamamlamak için ardışık düzenden yeterli veri okunana kadar engelleyecektir.

POSIX gereksinimi gibi, bu da önemli bir özelliktir: boru hattına yazma PIPE_BUF baytların (en az 512) atomik olması gerekir, böylece süreçler, normal dosyaların (bu tür garantiler sağlamayan) yapamayacağı şekilde ardışık düzen aracılığıyla birbirleriyle iletişim kurabilir.

Normal bir dosyayla, bir süreç tüm çıktısını ona yazabilir ve onu başka bir sürece aktarabilir. Veya süreçler, bir yazma veya okumanın tamamlanması hakkında birbirlerini bilgilendirmek için harici bir sinyal mekanizması (bir semafor gibi) kullanarak sert bir paralel modda çalışabilir. Konveyörler bizi tüm bu zahmetten kurtarıyor.

Ne arıyoruz?

Bir konveyörün nasıl çalışabileceğini hayal etmenizi kolaylaştırmak için parmaklarımla açıklayacağım. Bellekte bir arabellek ve bazı durumlar ayırmanız gerekecek. Arabelleğe veri eklemek ve çıkarmak için işlevlere ihtiyacınız olacak. Dosya tanıtıcılarda okuma ve yazma işlemleri sırasında işlevleri çağırmak için bazı olanaklara ihtiyacınız olacaktır. Ve yukarıda açıklanan özel davranışı uygulamak için kilitlere ihtiyaç vardır.

Belirsiz zihinsel modelimizi doğrulamak veya çürütmek için parlak lamba ışığı altında çekirdeğin kaynak kodunu sorgulamaya artık hazırız. Ancak beklenmedik durumlara karşı her zaman hazırlıklı olun.

Nereye bakıyoruz?

Ünlü kitabın benim kopyamın nerede olduğunu bilmiyorum.aslanlar kitabı« Unix 6 kaynak kodu ile, ancak sayesinde Unix Miras Topluluğu çevrimiçi aranabilir kaynak kodu Unix'in eski sürümleri bile.

TUHS arşivlerinde dolaşmak bir müzeyi gezmek gibidir. Ortak tarihimize bakabiliriz ve tüm bu materyali eski kasetlerden ve çıktılardan parça parça kurtarmak için yıllarca verilen çabaya saygı duyuyorum. Ve hala kayıp olan bu parçaların kesinlikle farkındayım.

Boru hatlarının antik tarihi hakkındaki merakımızı giderdikten sonra, karşılaştırma için modern çekirdeklere bakabiliriz.

Bu arada, pipe tablodaki sistem çağrı numarası 42'dir sysent[]. Tesadüf?

Geleneksel Unix çekirdekleri (1970–1974)

hiçbir iz bulamadım pipe(2) ne de PDP-7 Unix'i (Ocak 1970), ne de ilk sürüm Unix (Kasım 1971), ne de eksik kaynak kodunda ikinci baskı (Haziran 1972).

TUHS iddia ediyor üçüncü sürüm Unix (Şubat 1973), boru hatlarına sahip ilk versiyondu:

Unix'in üçüncü baskısı, birleştiricide yazılmış bir çekirdeğe sahip son sürümdü, ama aynı zamanda işlem hatlarına sahip ilk sürümdü. 1973'te üçüncü baskıyı geliştirmek için çalışmalar yapılıyordu, çekirdek C'de yeniden yazıldı ve böylece Unix'in dördüncü baskısı doğdu.

Bir okuyucu, Doug McIlroy'un "programları bir bahçe hortumu gibi bağlama" fikrini önerdiği bir belgenin tarandığını buldu.

Unix'te işlem hatları nasıl uygulanır?
Brian Kernighan'ın kitabındaUnix: Bir Tarih ve Bir Anı”, konveyörlerin ortaya çıkış tarihi de şu belgeden bahsediyor: “... 30 yıl boyunca Bell Laboratuarlarındaki ofisimde duvarda asılı kaldı.” Burada McIlroy ile röportajve başka bir hikaye McIlroy'un 2014'te yazılmış çalışması:

Unix ortaya çıktığında, eşyordamlara olan tutkum, işletim sistemi yazarı Ken Thompson'dan bazı işlemlere yazılan verilerin yalnızca aygıta değil, aynı zamanda başka bir işlemin çıkışına da gitmesine izin vermesini istememe neden oldu. Ken bunun mümkün olduğunu düşündü. Ancak bir minimalist olarak, her sistem özelliğinin önemli bir rol oynamasını istedi. İşlemler arasında doğrudan yazma, bir ara dosyaya yazmaya göre gerçekten büyük bir avantaj mı? Ve ancak akılda kalıcı "boru hattı" adıyla ve süreçlerin etkileşiminin sözdiziminin bir açıklamasıyla belirli bir teklif yaptığımda, Ken sonunda "Yapacağım!"

Ve yaptım. Önemli bir akşam, Ken çekirdeği ve kabuğu değiştirdi, girdileri nasıl kabul ettiklerini (bir işlem hattından gelebilir) standart hale getirmek için birkaç standart programı düzeltti ve dosya adlarını değiştirdi. Ertesi gün, uygulamalarda boru hatları çok yaygın olarak kullanılmaya başlandı. Haftanın sonunda, sekreterler belgeleri kelime işlemcilerden yazıcıya göndermek için kullandılar. Bir süre sonra Ken, boru hatlarının kullanımını o zamandan beri kullanılan daha temiz kurallarla sarmak için orijinal API'yi ve sözdizimini değiştirdi.

Ne yazık ki, üçüncü sürüm Unix çekirdeğinin kaynak kodu kayboldu. Ve C ile yazılmış çekirdek kaynak koduna sahip olmamıza rağmen dördüncü baskı, Kasım 1973'te yayınlandı, ancak resmi yayından birkaç ay önce çıktı ve boru hatlarının uygulanmasını içermiyor. Bu efsanevi Unix özelliğinin kaynak kodunun belki de sonsuza kadar kaybolmuş olması üzücü.

Şunun için dokümantasyon metnimiz var: pipe(2) her iki sürümden, böylece belgeleri arayarak başlayabilirsiniz üçüncü baskı ("manuel olarak" altı çizili belirli kelimeler için, bir ^H sabit değeri dizisi ve ardından bir alt çizgi!). bu proto-pipe(2) birleştiricide yazılır ve yalnızca bir dosya tanıtıcı döndürür, ancak zaten beklenen temel işlevselliği sağlar:

Sistem çağrısı boru boru hattı adı verilen bir G/Ç mekanizması oluşturur. Döndürülen dosya tanımlayıcı, okuma ve yazma işlemleri için kullanılabilir. Ardışık düzene bir şey yazıldığında, 504 bayta kadar veriyi arabelleğe alır ve ardından yazma işlemi askıya alınır. Ardışık düzenden okunurken arabelleğe alınan veriler alınır.

Ertesi yıl, çekirdek C'de yeniden yazıldı ve boru(2) dördüncü baskı prototipi ile modern görünümüne kavuştu”pipe(fildes)"

Sistem çağrısı boru boru hattı adı verilen bir G/Ç mekanizması oluşturur. Döndürülen dosya tanıtıcıları, okuma ve yazma işlemlerinde kullanılabilir. Ardışık düzene bir şey yazıldığında, r1'de döndürülen tanımlayıcı (resp. fildes[1]) kullanılır, 4096 bayta kadar veri arabelleğe alınır ve ardından yazma işlemi askıya alınır. Ardışık düzenden okurken, r0'a döndürülen tanımlayıcı (resp. fildes[0]) verileri alır.

Bir ardışık düzen tanımlandıktan sonra, iki (veya daha fazla) etkileşimli sürecin (sonraki çağrılarla yaratıldığı) varsayılır. çatal) çağrıları kullanarak işlem hattından veri iletir okumak и yazmak.

Kabuk, bir ardışık düzen aracılığıyla bağlanan doğrusal bir süreç dizisini tanımlamak için bir sözdizimine sahiptir.

Yalnızca bir ucu olan (tüm yazma dosyası tanımlayıcıları kapalı) boş bir ardışık düzenden (arabelleğe alınmış veri içermeyen) okuma çağrıları "dosyanın sonu"nu döndürür. Benzer bir durumdaki yazma çağrıları yoksayılır.

en erken korunmuş boru hattı uygulaması geçerlidir Unix'in beşinci baskısına (Haziran 1974), ancak bir sonraki sürümde çıkanla neredeyse aynı. Yalnızca yorumlar eklendi, bu nedenle beşinci baskı atlanabilir.

Unix Altıncı Baskı (1975)

Unix kaynak kodunu okumaya başlama altıncı baskı (Mayıs 1975). Büyük ölçüde teşekkürler Aslanlar bulmak önceki sürümlerin kaynaklarından çok daha kolaydır:

Uzun yıllar kitap Aslanlar Bell Labs dışında Unix çekirdeğiyle ilgili tek belgeydi. Altıncı baskı lisansı öğretmenlerin kaynak kodunu kullanmasına izin verse de, yedinci baskı lisansı bu olasılığı dışladı, bu nedenle kitap daktiloyla yazılmış yasa dışı kopyalarla dağıtıldı.

Bugün, kapağı fotokopi makinesindeki öğrencileri tasvir eden kitabın yeniden basılmış bir kopyasını satın alabilirsiniz. Ve (TUHS projesini başlatan) Warren Toomey sayesinde, indirebilirsiniz Altıncı Baskı Kaynak PDF. Dosyayı oluşturmak için ne kadar çaba harcandığına dair size bir fikir vermek istiyorum:

15 yılı aşkın bir süre önce, içinde sağlanan kaynak kodunun bir kopyasını yazdım. Aslanlarçünkü bilinmeyen sayıda başka kopyadan kopyamın kalitesini beğenmedim. TUHS henüz yoktu ve eski kaynaklara erişimim yoktu. Ancak 1988'de, bir PDP9 bilgisayarından yedeği olan 11 parçalı eski bir kaset buldum. İşe yarayıp yaramadığını bilmek zordu, ancak dosyaların çoğunun 1979 olarak işaretlendiği bozulmamış bir /usr/src/ ağacı vardı ve o zaman bile eski görünüyordu. Yedinci baskıydı ya da bir PWB türeviydi, diye düşündüm.

Bulguyu temel aldım ve kaynakları altıncı baskının durumuna göre manuel olarak düzenledim. Kodun bir kısmı aynı kaldı, modern belirteci += eskimiş =+ olarak değiştirerek, bir kısmı biraz düzenlenmeliydi. Bir şey basitçe silindi ve bir şeyin tamamen yeniden yazılması gerekiyordu, ancak çok fazla değil.

Ve bugün TUHS'de çevrimiçi olarak altıncı baskısının kaynak kodunu okuyabiliyoruz. Dennis Ritchie'nin elinin olduğu arşiv.

Bu arada, ilk bakışta Kernighan ve Ritchie döneminden önceki C kodunun ana özelliği, kısa. Sitemdeki nispeten dar bir görüntüleme alanına sığdırmak için kapsamlı düzenleme yapmadan kod parçacıkları ekleyemem.

Erken /usr/sys/ken/pipe.c açıklayıcı bir yorum var (ve evet, dahası 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

Tampon boyutu dördüncü baskıdan bu yana değişmedi. Ancak burada, herhangi bir kamuya açık belge olmaksızın, ardışık düzenlerin bir zamanlar dosyaları yedek depolama olarak kullandığını görüyoruz!

LARG dosyalarına gelince, bunlar şuna karşılık gelir: inode bayrağı LARGişlemek için "büyük adresleme algoritması" tarafından kullanılan dolaylı bloklar daha büyük dosya sistemlerini desteklemek için. Ken onları kullanmamanın daha iyi olduğunu söylediğine göre, onun sözünü seve seve kabul edeceğim.

İşte gerçek sistem çağrısı 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;
}

Yorum, burada neler olduğunu açıkça açıklıyor. Ancak kodu anlamak o kadar kolay değil, kısmen "yapı kullanıcısı» ve kaydeder R0 и R1 sistem çağrısı parametreleri ve dönüş değerleri iletilir.

ile deneyelim ialloc() diske yerleştir düğüm (inode)ve yardımı ile falloc() - iki tane sakla dosya. Her şey yolunda giderse, bu dosyaları işlem hattının iki ucu olarak tanımlayan bayraklar ayarlayacağız, onları aynı inode'a yönlendireceğiz (referans sayısı 2 olur) ve inode'u değiştirilmiş ve kullanımda olarak işaretleyeceğiz. isteklerine dikkat edin döndürmek() yeni inode'daki referans sayısını azaltmak için hata yollarında.

pipe() nedeniyle R0 и R1 okuma ve yazma için dosya tanımlayıcı numaralarını döndürür. falloc() bir dosya yapısına bir işaretçi döndürür, ancak aynı zamanda aracılığıyla "döndürür" u.u_ar0[R0] ve bir dosya tanıtıcı. Yani, kod depolanır r okumak için dosya tanıtıcısı ve doğrudan dosyadan yazmak için bir tanımlayıcı atar. u.u_ar0[R0] ikinci aramadan sonra falloc().

Bayrak FPIPEişlem hattını oluştururken ayarladığımız , işlevin davranışını kontrol eder sys2.c'de rdwr(), belirli G / Ç rutinlerini çağıran:

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

Daha sonra fonksiyon readp() в pipe.c boru hattından veri okur. Ancak, uygulamanın başından itibaren izlenmesi daha iyidir. writep(). Yine, argüman geçiş kuralının doğası gereği kod daha karmaşık hale geldi, ancak bazı ayrıntılar atlanabilir.

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

Boru hattı girişine bayt yazmak istiyoruz u.u_count. İlk önce inode'u kilitlememiz gerekiyor (aşağıya bakın plock/prele).

Ardından inode referans sayısını kontrol ederiz. Boru hattının her iki ucu da açık kaldığı sürece sayaç 2 olmalıdır. rp->f_inode), dolayısıyla sayaç 2'den küçükse, bu, okuma işleminin ardışık düzenin sonunu kapattığı anlamına gelmelidir. Başka bir deyişle, kapalı bir ardışık düzene yazmaya çalışıyoruz ki bu bir hatadır. İlk hata kodu EPIPE ve sinyal SIGPIPE Unix'in altıncı baskısında yayınlandı.

Ancak konveyör açık olsa bile dolu olabilir. Bu durumda, başka bir işlemin boru hattından okuyacağı ve içinde yeterince yer açacağı umuduyla kilidi serbest bırakıyoruz ve uykuya geçiyoruz. Uyandığımızda başa dönüyoruz, kilidi tekrar kapatıyoruz ve yeni bir yazma döngüsü başlatıyoruz.

Ardışık düzende yeterli boş alan varsa, kullanarak ona veri yazarız. yaz()... Parametre i_size1 inode'a (boş bir ardışık düzen ile 0'a eşit olabilir) halihazırda içerdiği verilerin sonunu gösterir. Yazmak için yeterli alan varsa, boru hattını şuradan doldurabiliriz: i_size1 karşı PIPESIZ. Ardından kilidi serbest bırakır ve boru hattından okumayı bekleyen herhangi bir işlemi uyandırmaya çalışırız. İhtiyacımız olan kadar bayt yazıp yazamadığımızı görmek için başa dönüyoruz. Değilse, yeni bir kayıt döngüsü başlatırız.

Genellikle parametre i_mode inode izinleri depolamak için kullanılır r, w и x. Ancak işlem hatları söz konusu olduğunda, bitleri kullanarak bazı işlemlerin bir yazma veya okuma beklediğini bildiririz. IREAD и IWRITE sırasıyla. İşlem bayrağı ayarlar ve çağırır sleep()ve gelecekte başka bir işlemin çağrılması bekleniyor wakeup().

Gerçek sihir olur sleep() и wakeup(). Onlar uygulanır slp.c, ünlü "Bunu anlamanız beklenmiyor" yorumunun kaynağı. Neyse ki, kodu anlamamıza gerek yok, sadece bazı yorumlara bakı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) /* … */

Çağıran süreç sleep() belirli bir kanal için, daha sonra arayacak olan başka bir işlem tarafından uyandırılabilir. wakeup() aynı kanal için writep() и readp() bu tür eşleştirilmiş aramalar yoluyla eylemlerini koordine edin. dikkat pipe.c her zaman öncelik ver PPIPE çağrıldığında sleep(), Böylece hepsi sleep() bir sinyalle kesilebilir.

Şimdi işlevi anlamak için her şeye sahibiz 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 işlevi aşağıdan yukarıya okumayı daha kolay bulabilirsiniz. "Oku ve geri dön" dalı genellikle ardışık düzende bazı veriler olduğunda kullanılır. Bu durumda, kullanırız Okumak() geçerli olandan başlayarak mümkün olduğu kadar çok veriyi oku f_offset okuyun ve ardından karşılık gelen ofsetin değerini güncelleyin.

Sonraki okumalarda, okuma ofseti ulaştıysa boru hattı boş olacaktır. i_size1 düğümde. Konumu 0'a sıfırlıyoruz ve boru hattına yazmak isteyen herhangi bir işlemi uyandırmaya çalışıyoruz. Biliyoruz ki konveyör dolduğunda, writep() uyuyakalmak ip+1. Ve artık işlem hattı boş olduğuna göre, yazma döngüsüne devam etmek için onu uyandırabiliriz.

Okuyacak bir şey yoksa, o zaman readp() bir bayrak ayarlayabilir IREAD ve üzerinde uyuyakalmak ip+2. Onu neyin uyandıracağını biliyoruz. writep()boru hattına bazı veriler yazdığında.

İle ilgili yorumlar read() ve writei() " üzerinden parametreleri iletmek yerine bunu anlamanıza yardımcı olacaktır.u» bunları bellekte bir dosya, konum, arabellek alan ve okunacak veya yazılacak bayt sayısını sayan normal G/Ç işlevleri gibi ele alabiliriz.

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

"İhtiyatlı" engellemeye gelince, o zaman readp() и writep() düğümleri bitene veya bir sonuç alana kadar kilitleyin (ör. wakeup). plock() и prele() basitçe çalışın: farklı bir arama seti kullanma sleep и wakeup az önce serbest bıraktığımız kilide ihtiyaç duyan herhangi bir işlemi uyandırmamıza izin 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);
    }
}

İlk başta nedenini anlayamadım readp() neden olmaz prele(ip) aramadan önce wakeup(ip+1). İlk şey writep() döngüsündeki çağrılar, bu plock(ip), eğer bir kilitlenme ile sonuçlanır readp() henüz bloğunu kaldırmadı, bu yüzden kod bir şekilde doğru çalışıyor olmalı. eğer bakarsan wakeup(), yalnızca uyku sürecini yürütmeye hazır olarak işaretlediği anlaşılır, böylece gelecekte sched() gerçekten başlattı. Bu yüzden readp() nedenleri wakeup(), kilidini açar, ayarlar IREAD ve aramalar sleep(ip+2)- tüm bunlar daha önce writep() döngüyü yeniden başlatır.

Bu, altıncı baskıdaki boru hatlarının açıklamasını tamamlar. Basit kod, geniş kapsamlı çıkarımlar.

Yedinci Sürüm Unix (Ocak 1979), birçok yeni uygulamayı ve çekirdek özelliğini tanıtan yeni bir büyük sürümdü (dört yıl sonra). Tip döküm, rakorlar ve tipli işaretçilerin yapılara kullanımı ile bağlantılı olarak da önemli değişikliklere uğramıştır. Fakat boru hatları kodu pratik olarak değişmedi. Bu baskıyı atlayabiliriz.

Xv6, Unix benzeri basit bir çekirdek

Bir çekirdek oluşturmak için xv6 Unix'in altıncı baskısından etkilenmiş, ancak x86 işlemcilerde çalışacak şekilde modern C'de yazılmıştır. Kodun okunması ve anlaşılması kolaydır. Ayrıca, TUHS'li Unix kaynaklarının aksine, onu derleyebilir, değiştirebilir ve PDP 11/70 dışında bir şey üzerinde çalıştırabilirsiniz. Bu nedenle, bu çekirdek, üniversitelerde işletim sistemleri hakkında bir öğretim materyali olarak yaygın olarak kullanılmaktadır. kaynaklar Github'da.

Kod, açık ve düşünceli bir uygulama içeriyor boru.c, diskteki bir inode yerine bellekteki bir arabellek tarafından desteklenir. Burada sadece "yapısal boru hattı" tanımını ve işlevini veriyorum. 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() işlevleri içeren uygulamanın geri kalanının durumunu ayarlar piperead(), pipewrite() и pipeclose(). Gerçek sistem çağrısı sys_pipe uygulanan bir sarmalayıcıdır sistem dosyası.c. Tüm kodunu okumanızı tavsiye ederim. Karmaşıklık, altıncı baskının kaynak kodu düzeyindedir, ancak okuması çok daha kolay ve keyiflidir.

Linux 0.01

Linux 0.01 için kaynak kodunu bulabilirsiniz. Boru hatlarının uygulanmasını onun araştırmalarında incelemek öğretici olacaktır. fs/pipe.c. Burada, boru hattını temsil etmek için bir inode kullanılır, ancak boru hattının kendisi modern C'de yazılmıştır. Altıncı baskı kodunu hacklediyseniz, burada herhangi bir sorun yaşamayacaksınız. Fonksiyon böyle görünüyor 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;
}

Yapı tanımlarına bakmadan bile, bir yazma işleminin sonuçlanıp sonuçlanmadığını kontrol etmek için inode referans sayısının nasıl kullanıldığını anlayabilirsiniz. SIGPIPE. Bayt bayt çalışmaya ek olarak, bu işlevi yukarıdaki fikirlerle karşılaştırmak kolaydır. Hatta mantık sleep_on/wake_up çok yabancı görünmüyor.

Modern Linux Çekirdekleri, FreeBSD, NetBSD, OpenBSD

Hemen bazı modern çekirdeklerin üzerinden geçtim. Hiçbirinin zaten disk tabanlı bir uygulaması yok (şaşırtıcı değil). Linux'un kendi uygulaması vardır. Ve üç modern BSD çekirdeği, John Dyson tarafından yazılan koda dayalı uygulamalar içermesine rağmen, yıllar içinde birbirlerinden çok farklı hale geldiler.

Okumak fs/pipe.c (Linux'ta) veya sys/kern/sys_pipe.c (*BSD'de), gerçek özveri gerektirir. Vektör ve eşzamansız G/Ç gibi özellikler için performans ve destek, günümüzde kodda önemlidir. Ve bellek tahsisi, kilitler ve çekirdek yapılandırmasının ayrıntıları büyük ölçüde değişir. Üniversitelerin işletim sistemlerine giriş dersi için ihtiyacı olan şey bu değil.

Her halükarda, birkaç eski modeli ortaya çıkarmak benim için ilginçti (örneğin, SIGPIPE ve dönüş EPIPE kapalı bir boru hattına yazarken) tüm bu, çok farklı, modern çekirdeklerde. Muhtemelen bir PDP-11 bilgisayarını asla canlı görmeyeceğim, ancak ben doğmadan birkaç yıl önce yazılan koddan öğrenilecek çok şey var.

Divi Kapoor tarafından 2011 yılında yazılan "Boruların ve FIFO'ların Linux Çekirdeği UygulamasıLinux ardışık düzenlerinin (şimdiye kadar) nasıl çalıştığına genel bir bakış. A Linux'ta son taahhüt yetenekleri geçici dosyalarınkini aşan işlem hattı etkileşim modelini gösterir; ve ayrıca işlem hatlarının altıncı sürüm Unix çekirdeğindeki "çok muhafazakar kilitlemeden" ne kadar uzaklaştığını gösterir.

Kaynak: habr.com

Yorum ekle