Bagaimana saluran paip dilaksanakan dalam Unix

Bagaimana saluran paip dilaksanakan dalam Unix
Artikel ini menerangkan pelaksanaan saluran paip dalam kernel Unix. Saya agak kecewa kerana artikel terbaru bertajuk "Bagaimanakah saluran paip berfungsi di Unix?"ternyata tiada tentang struktur dalaman. Saya menjadi ingin tahu dan menggali sumber lama untuk mencari jawapannya.

Apa yang kita bincangkan?

Pipelines, "mungkin ciptaan paling penting dalam Unix," adalah ciri yang menentukan falsafah Unix yang mendasari menghubungkan program kecil bersama-sama, serta tanda biasa pada baris arahan:

$ echo hello | wc -c
6

Fungsi ini bergantung pada panggilan sistem yang disediakan kernel pipe, yang diterangkan pada halaman dokumentasi paip(7) ΠΈ paip(2):

Talian paip menyediakan saluran satu arah untuk komunikasi antara proses. Saluran paip mempunyai input (tulis akhir) dan output (baca akhir). Data yang ditulis pada input saluran paip boleh dibaca pada output.

Saluran paip dibuat menggunakan panggilan pipe(2), yang mengembalikan dua deskriptor fail: satu merujuk kepada input saluran paip, yang kedua kepada output.

Output jejak daripada arahan di atas menunjukkan penciptaan saluran paip dan aliran data melaluinya dari satu proses ke proses yang lain:

$ 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

Proses ibu bapa memanggil pipe()untuk mendapatkan deskriptor fail yang dipasang. Satu proses kanak-kanak menulis kepada satu pemegang, dan satu lagi proses membaca data yang sama daripada pemegang lain. Cangkang menggunakan dup2 untuk "menamakan semula" deskriptor 3 dan 4 untuk memadankan stdin dan stdout.

Tanpa paip, shell perlu menulis hasil satu proses ke fail dan menghantarnya ke proses lain untuk membaca data daripada fail. Akibatnya, kami akan membazir lebih banyak sumber dan ruang cakera. Walau bagaimanapun, saluran paip adalah baik bukan sahaja kerana ia membolehkan anda mengelakkan penggunaan fail sementara:

Jika proses cuba membaca dari saluran paip kosong maka read(2) akan menyekat sehingga data tersedia. Jika proses cuba menulis ke saluran paip penuh, maka write(2) akan menyekat sehingga data yang mencukupi telah dibaca daripada saluran paip untuk melaksanakan penulisan.

Seperti keperluan POSIX, ini adalah sifat penting: menulis ke saluran paip sehingga PIPE_BUF bait (sekurang-kurangnya 512) mestilah atom supaya proses boleh berkomunikasi antara satu sama lain melalui saluran paip dengan cara yang fail biasa (yang tidak memberikan jaminan sedemikian) tidak boleh.

Apabila menggunakan fail biasa, proses boleh menulis semua outputnya kepadanya dan menyampaikannya kepada proses lain. Atau proses boleh beroperasi dalam mod yang sangat selari, menggunakan mekanisme isyarat luaran (seperti semafor) untuk memberitahu satu sama lain apabila menulis atau membaca telah selesai. Penghantar menyelamatkan kita daripada semua kerumitan ini.

Apa yang kita cari?

Saya akan menerangkannya dalam istilah orang awam untuk memudahkan anda membayangkan bagaimana penghantar boleh berfungsi. Anda perlu memperuntukkan penimbal dan beberapa keadaan dalam ingatan. Anda memerlukan fungsi untuk menambah dan mengalih keluar data daripada penimbal. Anda memerlukan beberapa cara untuk memanggil fungsi semasa operasi baca dan tulis pada deskriptor fail. Dan anda memerlukan kunci untuk melaksanakan tingkah laku khas yang diterangkan di atas.

Kini kami bersedia untuk menyoal siasat kod sumber kernel di bawah lampu lampu terang untuk mengesahkan atau menafikan model mental kami yang samar-samar. Tetapi sentiasa bersedia untuk perkara yang tidak dijangka.

Mana kita nak cari?

Saya tidak tahu di mana salinan buku terkenal saya "Buku singa"dengan kod sumber Unix 6, tetapi terima kasih kepada Persatuan Warisan Unix anda boleh mencari dalam talian di kod sumber malah versi Unix yang lebih lama.

Mengembara melalui arkib TUHS adalah seperti melawat muzium. Kita boleh melihat sejarah kita bersama, dan saya menghormati usaha bertahun-tahun untuk memulihkan semua bahan ini sedikit demi sedikit daripada pita dan cetakan lama. Dan saya sangat menyedari serpihan yang masih hilang.

Setelah memuaskan rasa ingin tahu kami tentang sejarah kuno penghantar, kami boleh melihat kernel moden untuk perbandingan.

By the way, pipe ialah panggilan sistem nombor 42 dalam jadual sysent[]. Kebetulan?

Inti Unix tradisional (1970–1974)

Saya tidak menemui sebarang kesan pipe(2) mahupun dalam PDP-7 Unix (Januari 1970), mahupun dalam edisi pertama Unix (November 1971), mahupun dalam kod sumber yang tidak lengkap edisi kedua (Jun 1972).

TUHS menyatakan bahawa edisi ketiga Unix (Februari 1973) menjadi versi pertama dengan penghantar:

Unix 1973rd Edition ialah versi terakhir dengan kernel yang ditulis dalam bahasa himpunan, tetapi juga versi pertama dengan saluran paip. Pada tahun XNUMX, kerja telah dijalankan untuk menambah baik edisi ketiga, kernel telah ditulis semula dalam C, dan oleh itu edisi keempat Unix muncul.

Seorang pembaca menemui imbasan dokumen di mana Doug McIlroy mencadangkan idea "menyambungkan program seperti hos taman."

Bagaimana saluran paip dilaksanakan dalam Unix
Dalam buku Brian KernighanUnix: Sejarah dan Memoir", dalam sejarah kemunculan penghantar, dokumen ini juga disebut: "... ia digantung di dinding di pejabat saya di Bell Labs selama 30 tahun." Di sini temu bual dengan McIlroy, dan satu lagi cerita daripada Karya McIlroy, yang ditulis pada 2014:

Apabila Unix keluar, ketertarikan saya dengan coroutine menyebabkan saya bertanya kepada pengarang OS, Ken Thompson, untuk membenarkan data yang ditulis kepada proses untuk pergi bukan sahaja ke peranti, tetapi juga untuk output ke proses lain. Ken memutuskan ia mungkin. Bagaimanapun, sebagai seorang minimalis, dia mahu setiap fungsi sistem memainkan peranan penting. Adakah menulis terus antara proses benar-benar kelebihan besar berbanding menulis ke fail perantaraan? Hanya apabila saya membuat cadangan khusus dengan nama yang menarik "talian paip" dan penerangan tentang sintaks untuk interaksi antara proses yang Ken akhirnya berseru: "Saya akan melakukannya!"

Dan lakukan. Pada suatu petang yang menentukan, Ken menukar kernel dan shell, membetulkan beberapa program standard untuk menyeragamkan cara mereka menerima input (yang boleh datang dari saluran paip), dan juga menukar nama fail. Keesokan harinya, saluran paip mula digunakan secara meluas dalam aplikasi. Menjelang akhir minggu, setiausaha telah menggunakannya untuk menghantar dokumen daripada pemproses perkataan ke pencetak. Tidak lama kemudian, Ken menggantikan API dan sintaks asal untuk membungkus penggunaan saluran paip dengan konvensyen yang lebih bersih, yang telah digunakan sejak itu.

Malangnya, kod sumber untuk kernel Unix edisi ketiga telah hilang. Dan walaupun kita mempunyai kod sumber kernel yang ditulis dalam C edisi keempat, dikeluarkan pada November 1973, tetapi ia dikeluarkan beberapa bulan sebelum keluaran rasmi dan tidak mengandungi pelaksanaan saluran paip. Sungguh memalukan bahawa kod sumber untuk fungsi Unix legenda ini hilang, mungkin selama-lamanya.

Kami mempunyai dokumentasi teks untuk pipe(2) daripada kedua-dua keluaran, jadi anda boleh mulakan dengan mencari dokumentasi edisi ketiga (untuk perkataan tertentu, digariskan "secara manual", rentetan huruf ^H, diikuti dengan garis bawah!). proto-pipe(2) ditulis dalam bahasa himpunan dan hanya mengembalikan satu deskriptor fail, tetapi sudah menyediakan fungsi teras yang diharapkan:

Panggilan sistem paip mencipta mekanisme input/output yang dipanggil saluran paip. Deskriptor fail yang dikembalikan boleh digunakan untuk operasi baca dan tulis. Apabila sesuatu ditulis pada saluran paip, sehingga 504 bait data ditimbal, selepas itu proses penulisan digantung. Apabila membaca dari saluran paip, data penimbal diambil.

Menjelang tahun berikutnya, kernel telah ditulis semula dalam C, dan paip(2) dalam edisi keempat memperoleh penampilan modennya dengan prototaip "pipe(fildes)"

Panggilan sistem paip mencipta mekanisme input/output yang dipanggil saluran paip. Deskriptor fail yang dikembalikan boleh digunakan dalam operasi baca dan tulis. Apabila sesuatu ditulis pada saluran paip, pemegang yang dikembalikan dalam r1 (resp. fildes[1]) digunakan, penimbal kepada 4096 bait data, selepas itu proses penulisan digantung. Apabila membaca daripada saluran paip, pemegang kembali ke r0 (resp. fildes[0]) mengambil data.

Diandaikan bahawa sebaik sahaja saluran paip ditakrifkan, dua (atau lebih) proses berkomunikasi (dicipta oleh panggilan berikutnya untuk garpu) akan memindahkan data dari saluran paip menggunakan panggilan membaca ΠΈ menulis.

Cangkang mempunyai sintaks untuk menentukan tatasusunan linear proses yang disambungkan oleh saluran paip.

Panggilan untuk membaca daripada saluran paip kosong (tidak mengandungi data buffer) yang hanya mempunyai satu hujung (semua deskriptor fail penulisan ditutup) mengembalikan "hujung fail". Panggilan untuk menulis dalam situasi yang sama diabaikan.

Paling awal pelaksanaan saluran paip terpelihara merujuk kepada edisi kelima Unix (Jun 1974), tetapi ia hampir sama dengan yang muncul dalam keluaran seterusnya. Komen baru sahaja ditambah, jadi anda boleh melangkau edisi kelima.

Unix edisi keenam (1975)

Mari mulakan membaca kod sumber Unix edisi keenam (Mei 1975). Terima kasih kepada Lions ia lebih mudah dicari daripada sumber versi terdahulu:

Selama bertahun-tahun buku itu Lions adalah satu-satunya dokumen pada kernel Unix yang tersedia di luar Bell Labs. Walaupun lesen edisi keenam membenarkan guru menggunakan kod sumbernya, lesen edisi ketujuh mengecualikan kemungkinan ini, jadi buku itu diedarkan dalam bentuk salinan taip haram.

Hari ini anda boleh membeli cetakan semula buku itu, yang muka depannya menunjukkan pelajar di mesin salinan. Dan terima kasih kepada Warren Toomey (yang memulakan projek TUHS) anda boleh memuat turun Fail PDF dengan kod sumber untuk edisi keenam. Saya ingin memberi anda gambaran tentang berapa banyak usaha yang dilakukan untuk mencipta fail:

Lebih daripada 15 tahun yang lalu, saya menaip salinan kod sumber yang diberikan Lions, kerana saya tidak menyukai kualiti salinan saya daripada bilangan salinan lain yang tidak diketahui. TUHS belum wujud lagi dan saya tidak mempunyai akses kepada sumber lama. Tetapi pada tahun 1988, saya menemui pita 9 trek lama yang mengandungi sandaran daripada komputer PDP11. Sukar untuk mengetahui sama ada ia berfungsi, tetapi terdapat pokok /usr/src/ yang utuh di mana kebanyakan fail dilabelkan dengan tahun 1979, yang pada masa itu kelihatan kuno. Ia adalah edisi ketujuh atau PWB terbitannya, seperti yang saya percaya.

Saya mengambil penemuan itu sebagai asas dan menyunting sumber secara manual ke edisi keenam. Sesetengah kod kekal sama, tetapi ada yang perlu disunting sedikit, menukar token += moden kepada =+ yang sudah lapuk. Sesetengah perkara dipadamkan begitu sahaja, dan ada yang perlu ditulis semula sepenuhnya, tetapi tidak terlalu banyak.

Dan hari ini kita boleh membaca dalam talian di TUHS kod sumber edisi keenam daripada arkib, yang dipegang oleh Dennis Ritchie.

Dengan cara ini, pada pandangan pertama, ciri utama kod C sebelum tempoh Kernighan dan Ritchie adalah Keringkasan. Tidak selalunya saya boleh memasukkan kepingan kod tanpa pengeditan yang meluas agar sesuai dengan kawasan paparan yang agak sempit di tapak saya.

Awal /usr/sys/ken/pipe.c terdapat ulasan penjelasan (dan ya, ada lagi /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

Saiz penimbal tidak berubah sejak edisi keempat. Tetapi di sini kita lihat, tanpa sebarang dokumentasi awam, saluran paip pernah menggunakan fail sebagai storan sandaran!

Bagi fail LARG, ia sepadan dengan bendera inode BESAR, yang digunakan oleh "algoritma pengalamatan besar" untuk memproses blok tidak langsung untuk menyokong sistem fail yang lebih besar. Memandangkan Ken berkata lebih baik tidak menggunakannya, saya dengan senang hati akan menerima kata-katanya.

Inilah panggilan sistem sebenar 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;
}

Komen itu menerangkan dengan jelas apa yang berlaku di sini. Tetapi memahami kod itu tidak begitu mudah, sebahagiannya kerana cara "pengguna struktur uΒ» dan mendaftar R0 ΠΈ R1 parameter panggilan sistem dan nilai pulangan diluluskan.

Jom cuba dengan ialloc() letak pada cakera inode (pemegang indeks), dan dengan bantuan falloc() - letak dua dalam ingatan fail. Jika semuanya berjalan lancar, kami akan menetapkan bendera untuk mengenal pasti fail ini sebagai dua hujung saluran paip, menghalakannya ke inod yang sama (yang kiraan rujukannya akan ditetapkan kepada 2), dan menandakan inod sebagai diubah suai dan sedang digunakan. Beri perhatian kepada permintaan kepada saya letak() dalam laluan ralat untuk mengurangkan kiraan rujukan dalam inod baharu.

pipe() mesti melalui R0 ΠΈ R1 kembalikan nombor deskriptor fail untuk membaca dan menulis. falloc() mengembalikan penunjuk kepada struktur fail, tetapi juga "mengembalikan" melalui u.u_ar0[R0] dan deskriptor fail. Iaitu, kod disimpan dalam r deskriptor fail untuk membaca dan menetapkan deskriptor fail untuk menulis terus daripada u.u_ar0[R0] selepas panggilan kedua falloc().

Π€Π»Π°Π³ FPIPE, yang kami tetapkan semasa membuat saluran paip, mengawal tingkah laku fungsi rdwr() dalam sys2.cmemanggil rutin I/O tertentu:

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

Kemudian fungsi readp() Π² pipe.c membaca data daripada saluran paip. Tetapi lebih baik untuk mengesan pelaksanaan bermula dari writep(). Sekali lagi, kod telah menjadi lebih kompleks disebabkan oleh konvensyen untuk meluluskan hujah, tetapi beberapa butiran boleh ditinggalkan.

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

Kami mahu menulis bait ke input saluran paip u.u_count. Mula-mula kita perlu mengunci inod (lihat di bawah plock/prele).

Kemudian kami menyemak kaunter rujukan inode. Selagi kedua-dua hujung saluran paip kekal terbuka, kaunter hendaklah sama dengan 2. Kami memegang satu pautan (dari rp->f_inode), jadi jika kaunter kurang daripada 2, ia mesti bermakna proses membaca telah menutup hujung saluran paipnya. Dalam erti kata lain, kami cuba menulis ke saluran paip tertutup, dan ini adalah ralat. Kod ralat kali pertama EPIPE dan isyarat SIGPIPE muncul dalam edisi keenam Unix.

Tetapi walaupun penghantar terbuka, ia mungkin penuh. Dalam kes ini, kami melepaskan kunci dan pergi tidur dengan harapan bahawa proses lain akan membaca dari saluran paip dan membebaskan ruang yang cukup di dalamnya. Setelah bangun, kami kembali ke permulaan, tutup kunci sekali lagi dan mulakan kitaran rakaman baharu.

Sekiranya terdapat ruang kosong yang mencukupi dalam saluran paip, maka kami menulis data kepadanya menggunakan writei(). Parameter i_size1 inode (jika saluran paip kosong, ia boleh sama dengan 0) menunjukkan penghujung data yang sudah ada. Jika terdapat ruang rakaman yang mencukupi, kami boleh mengisi saluran paip dari i_size1 kepada PIPESIZ. Kemudian kami melepaskan kunci dan cuba membangunkan sebarang proses yang sedang menunggu untuk dibaca dari saluran paip. Kami kembali ke permulaan untuk melihat sama ada kami dapat menulis seberapa banyak bait yang kami perlukan. Jika gagal, maka kita memulakan kitaran rakaman baharu.

Biasanya parameter i_mode inode digunakan untuk menyimpan kebenaran r, w ΠΈ x. Tetapi dalam kes saluran paip, kami memberi isyarat bahawa beberapa proses sedang menunggu untuk menulis atau membaca menggunakan bit IREAD ΠΈ IWRITE masing-masing. Proses menetapkan bendera dan panggilan sleep(), dan dijangka beberapa proses lain pada masa hadapan akan menyebabkan wakeup().

Keajaiban sebenar berlaku dalam sleep() ΠΈ wakeup(). Mereka dilaksanakan dalam slp.c, sumber komen terkenal "Anda tidak dijangka memahami perkara ini". Nasib baik, kita tidak perlu memahami kod tersebut, cuma lihat beberapa ulasan:

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

Proses yang menyebabkan sleep() untuk saluran tertentu, mungkin kemudian akan dikejutkan oleh proses lain, yang akan menyebabkan wakeup() untuk saluran yang sama. writep() ΠΈ readp() menyelaraskan tindakan mereka melalui panggilan berpasangan tersebut. ambil perhatian bahawa pipe.c sentiasa memberi keutamaan PPIPE apabila dipanggil sleep(), jadi itu sahaja sleep() mungkin terganggu oleh isyarat.

Sekarang kita mempunyai segala-galanya untuk memahami fungsi 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);
}

Anda mungkin mendapati lebih mudah untuk membaca fungsi ini dari bawah ke atas. Cawangan "baca dan kembali" biasanya digunakan apabila terdapat beberapa data dalam saluran paip. Dalam kes ini, kami menggunakan readi() kami membaca seberapa banyak data yang tersedia bermula dari yang semasa f_offset membaca, dan kemudian kemas kini nilai offset yang sepadan.

Pada bacaan berikutnya, saluran paip akan kosong jika offset bacaan telah mencapai i_size1 pada inode. Kami menetapkan semula kedudukan kepada 0 dan cuba membangkitkan sebarang proses yang ingin menulis ke saluran paip. Kita tahu bahawa apabila penghantar penuh, writep() akan tertidur pada ip+1. Dan sekarang apabila saluran paip itu kosong, kita boleh membangunkannya untuk menyambung semula kitaran penulisannya.

Jika anda tidak mempunyai apa-apa untuk dibaca, maka readp() boleh pasang bendera IREAD dan tertidur ip+2. Kita tahu apa yang akan menyedarkannya writep(), apabila ia menulis beberapa data ke saluran paip.

Komen kepada readi() dan writei() akan membantu anda memahami bahawa bukannya menghantar parameter melalui "u"Kita boleh melayannya seperti fungsi I/O biasa yang mengambil fail, kedudukan, penimbal dalam ingatan dan mengira bilangan bait untuk dibaca atau ditulis.

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

Bagi penyekatan "konservatif", maka readp() ΠΈ writep() menyekat inod sehingga mereka menyelesaikan kerja mereka atau menerima hasil (iaitu, call wakeup). plock() ΠΈ prele() berfungsi dengan mudah: menggunakan set panggilan yang berbeza sleep ΠΈ wakeup benarkan kami membangkitkan sebarang proses yang memerlukan kunci yang baru kami keluarkan:

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

Pada mulanya saya tidak faham kenapa readp() tidak menyebabkan prele(ip) sebelum panggilan wakeup(ip+1). Perkara pertama ialah writep() menyebabkan dalam kitarannya, ini plock(ip), yang membawa kepada kebuntuan jika readp() belum mengalih keluar blok saya lagi, jadi entah bagaimana kod mesti berfungsi dengan betul. Jika anda melihat wakeup(), maka menjadi jelas bahawa ia hanya menandakan proses tidur sebagai sedia untuk dilaksanakan, supaya pada masa hadapan sched() benar-benar melancarkannya. Jadi readp() sebab wakeup(), menanggalkan kunci, set IREAD dan panggilan sleep(ip+2)- semua ini sebelum ini writep() menyambung semula kitaran.

Ini melengkapkan penerangan tentang penghantar dalam edisi keenam. Kod mudah, akibat yang meluas.

Unix edisi ketujuh (Januari 1979) adalah keluaran utama baru (empat tahun kemudian) yang memperkenalkan banyak aplikasi baru dan ciri kernel. Ia juga mengalami perubahan ketara berkaitan dengan penggunaan tuangan jenis, kesatuan dan penunjuk ditaip kepada struktur. Namun begitu kod penghantar boleh dikatakan tidak berubah. Kita boleh melangkau edisi ini.

Xv6, kernel ringkas seperti Unix

Untuk mencipta kernel Xv6 dipengaruhi oleh edisi keenam Unix, tetapi ia ditulis dalam C moden untuk dijalankan pada pemproses x86. Kod ini mudah dibaca dan difahami. Selain itu, tidak seperti sumber Unix dengan TUHS, anda boleh menyusunnya, mengubah suainya dan menjalankannya pada sesuatu selain daripada PDP 11/70. Oleh itu, kernel ini digunakan secara meluas di universiti sebagai bahan pendidikan mengenai sistem pengendalian. Sumber berada di Github.

Kod tersebut mengandungi pelaksanaan yang jelas dan bernas paip.c, disokong oleh penimbal dalam ingatan dan bukannya inod pada cakera. Di sini saya hanya memberikan definisi "talian paip struktur" dan fungsinya 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() menetapkan keadaan pelaksanaan yang lain, yang merangkumi fungsi piperead(), pipewrite() ΠΈ pipeclose(). Panggilan sistem sebenar sys_pipe adalah pembalut yang dilaksanakan dalam sysfile.c. Saya cadangkan membaca keseluruhan kodnya. Kerumitannya berada pada tahap kod sumber edisi keenam, tetapi ia lebih mudah dan menyeronokkan untuk dibaca.

Linux 0.01

Kod sumber Linux 0.01 boleh didapati. Ia akan menjadi pengajaran untuk mengkaji pelaksanaan saluran paip dalam beliau fs/pipe.c. Ini menggunakan inod untuk mewakili saluran paip, tetapi saluran paip itu sendiri ditulis dalam C moden. Jika anda telah menyelesaikan kod edisi ke-6, anda tidak akan menghadapi masalah di sini. Inilah rupa fungsinya 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;
}

Tanpa melihat definisi struktur, anda boleh memikirkan bagaimana kiraan rujukan inode digunakan untuk menyemak sama ada operasi tulis menghasilkan SIGPIPE. Selain berfungsi bait demi bait, fungsi ini mudah dibandingkan dengan idea yang diterangkan di atas. Malah logik sleep_on/wake_up tidak kelihatan begitu asing.

Inti Linux moden, FreeBSD, NetBSD, OpenBSD

Saya dengan cepat berlari melalui beberapa biji moden. Tiada satu pun daripada mereka mempunyai pelaksanaan cakera lagi (tidak menghairankan). Linux mempunyai pelaksanaannya sendiri. Walaupun tiga kernel BSD moden mengandungi pelaksanaan berdasarkan kod yang ditulis oleh John Dyson, selama bertahun-tahun ia telah menjadi terlalu berbeza antara satu sama lain.

Untuk membaca fs/pipe.c (di Linux) atau sys/kern/sys_pipe.c (pada *BSD), ia memerlukan dedikasi yang sebenar. Kod hari ini adalah mengenai prestasi dan sokongan untuk ciri seperti vektor dan I/O tak segerak. Dan butiran peruntukan memori, kunci dan konfigurasi kernel semuanya sangat berbeza. Ini bukan apa yang kolej perlukan untuk kursus pengenalan sistem pengendalian.

Bagaimanapun, saya berminat untuk mencungkil beberapa corak lama (seperti menjana SIGPIPE dan kembali EPIPE apabila menulis ke saluran paip tertutup) dalam semua kernel moden yang berbeza ini. Saya mungkin tidak akan pernah melihat komputer PDP-11 dalam kehidupan sebenar, tetapi masih banyak yang perlu dipelajari daripada kod yang ditulis bertahun-tahun sebelum saya dilahirkan.

Artikel yang ditulis oleh Divi Kapoor pada tahun 2011:Pelaksanaan Inti Linux Paip dan FIFO" memberikan gambaran keseluruhan tentang cara saluran paip (masih) berfungsi dalam Linux. A komitmen baru-baru ini dalam Linux menggambarkan model saluran paip interaksi, yang keupayaannya melebihi fail sementara; dan juga menunjukkan sejauh mana saluran paip telah datang daripada "penguncian sangat konservatif" kernel Unix edisi keenam.

Sumber: www.habr.com

Tambah komen