Bagaimana pipeline diimplementasikan di Unix

Bagaimana pipeline diimplementasikan di Unix
Artikel ini menjelaskan implementasi pipeline di kernel Unix. Saya agak kecewa karena artikel terbaru berjudul "Bagaimana cara kerja pipeline di Unix?" ternyata tidak tentang struktur internal. Saya penasaran dan menggali sumber-sumber lama untuk menemukan jawabannya.

Apa yang kita bicarakan?

Pipeline adalah "mungkin penemuan paling penting di Unix" - fitur yang menentukan filosofi dasar Unix dalam menyusun program kecil, dan slogan baris perintah yang sudah dikenal:

$ echo hello | wc -c
6

Fungsionalitas ini bergantung pada panggilan sistem yang disediakan kernel pipe, yang dijelaskan di halaman dokumentasi pipa(7) ΠΈ pipa(2):

Pipeline menyediakan saluran satu arah untuk komunikasi antar proses. Pipa memiliki input (akhir tulis) dan output (akhir baca). Data yang ditulis ke input pipa dapat dibaca di output.

Pipa dibuat dengan menelepon pipe(2), yang mengembalikan dua deskriptor file: satu merujuk ke input pipa, yang kedua ke output.

Output jejak dari perintah di atas menunjukkan pembuatan pipa dan aliran data melaluinya dari satu proses ke proses lainnya:

$ 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

Panggilan proses induk pipe()untuk mendapatkan deskriptor file terlampir. Satu proses anak menulis ke satu deskriptor dan proses lain membaca data yang sama dari deskriptor lain. Shell "mengganti nama" deskriptor 2 dan 3 dengan dup4 agar sesuai dengan stdin dan stdout.

Tanpa pipa, shell harus menulis output dari satu proses ke file dan menyalurkannya ke proses lain untuk membaca data dari file. Akibatnya, kami akan membuang lebih banyak sumber daya dan ruang disk. Namun, pipeline lebih dari sekadar menghindari file sementara:

Jika suatu proses mencoba membaca dari pipa kosong, maka read(2) akan memblokir sampai data tersedia. Jika suatu proses mencoba untuk menulis ke pipa penuh, maka write(2) akan memblokir sampai data yang cukup telah dibaca dari pipa untuk menyelesaikan penulisan.

Seperti persyaratan POSIX, ini adalah properti penting: menulis ke pipa hingga PIPE_BUF byte (setidaknya 512) harus atom sehingga proses dapat berkomunikasi satu sama lain melalui pipa dengan cara yang tidak bisa dilakukan oleh file normal (yang tidak memberikan jaminan seperti itu).

Dengan file biasa, suatu proses dapat menulis semua keluarannya ke sana dan meneruskannya ke proses lain. Atau proses dapat beroperasi dalam mode paralel keras, menggunakan mekanisme pensinyalan eksternal (seperti semaphore) untuk saling menginformasikan tentang penyelesaian penulisan atau pembacaan. Konveyor menyelamatkan kita dari semua kerumitan ini.

Apa yang kita cari?

Saya akan menjelaskan dengan jari saya untuk memudahkan Anda membayangkan cara kerja konveyor. Anda perlu mengalokasikan buffer dan beberapa status di memori. Anda memerlukan fungsi untuk menambah dan menghapus data dari buffer. Anda memerlukan beberapa fasilitas untuk memanggil fungsi selama operasi baca dan tulis pada deskriptor file. Dan kunci diperlukan untuk mengimplementasikan perilaku khusus yang dijelaskan di atas.

Kami sekarang siap untuk menginterogasi kode sumber kernel di bawah cahaya lampu terang untuk mengkonfirmasi atau menyangkal model mental kami yang tidak jelas. Tapi selalu bersiaplah untuk hal yang tak terduga.

Di mana kita mencari?

Saya tidak tahu di mana salinan buku terkenal saya berada.Buku singaΒ« dengan kode sumber Unix 6, tetapi berkat Masyarakat Warisan Unix dapat dicari secara online Kode sumber bahkan versi Unix yang lebih lama.

Menelusuri arsip TUHS seperti mengunjungi museum. Kita dapat melihat sejarah bersama kita dan saya menghargai upaya bertahun-tahun untuk memulihkan semua materi ini sedikit demi sedikit dari kaset dan cetakan lama. Dan saya sangat sadar akan fragmen-fragmen yang masih hilang itu.

Setelah memuaskan keingintahuan kita tentang sejarah pipa kuno, kita dapat melihat inti modern sebagai perbandingan.

Kebetulan, pipe adalah system call nomor 42 dalam tabel sysent[]. Kebetulan?

Kernel Unix Tradisional (1970–1974)

Saya tidak menemukan jejak apapun pipe(2) tidak di PDP-7 Unix (Januari 1970), maupun di Unix edisi pertama (November 1971), maupun dalam kode sumber yang tidak lengkap edisi kedua (Juni 1972).

TUHS mengklaim itu Unix edisi ketiga (Februari 1973) adalah versi pertama dengan jalur pipa:

Edisi ketiga Unix adalah versi terakhir dengan kernel yang ditulis dalam assembler, tetapi juga versi pertama dengan saluran pipa. Selama tahun 1973, pekerjaan sedang dilakukan untuk meningkatkan edisi ketiga, kernel ditulis ulang dalam C, dan dengan demikian lahirlah Unix edisi keempat.

Seorang pembaca menemukan pindaian dokumen di mana Doug McIlroy mengusulkan gagasan "menghubungkan program seperti selang taman".

Bagaimana pipeline diimplementasikan di Unix
Dalam buku Brian KernighanUnix: Sejarah dan Memoar”, sejarah kemunculan konveyor juga menyebutkan dokumen ini: β€œ... itu tergantung di dinding kantor saya di Bell Labs selama 30 tahun.” Di Sini wawancara dengan McIlroydan cerita lain dari Karya McIlroy, ditulis pada tahun 2014:

Ketika Unix muncul, kecintaan saya pada coroutine membuat saya meminta penulis OS, Ken Thompson, untuk mengizinkan data yang ditulis ke beberapa proses agar tidak hanya masuk ke perangkat, tetapi juga keluar ke proses lain. Ken pikir itu mungkin. Namun, sebagai minimalis, dia ingin setiap fitur sistem memainkan peran penting. Apakah penulisan langsung antar proses benar-benar merupakan keuntungan besar dibandingkan menulis ke file perantara? Dan hanya ketika saya membuat proposal khusus dengan nama menarik "pipeline" dan deskripsi sintaks interaksi proses, Ken akhirnya berseru: "Saya akan melakukannya!".

Dan berhasil. Suatu malam yang menentukan, Ken mengubah kernel dan shell, memperbaiki beberapa program standar untuk membakukan cara mereka menerima input (yang mungkin berasal dari pipa), dan mengubah nama file. Keesokan harinya, pipa sangat banyak digunakan dalam aplikasi. Pada akhir minggu, sekretaris menggunakannya untuk mengirim dokumen dari pengolah kata ke printer. Beberapa saat kemudian, Ken mengganti API dan sintaks asli untuk membungkus penggunaan pipa dengan konvensi yang lebih bersih yang telah digunakan sejak saat itu.

Sayangnya, kode sumber untuk kernel Unix edisi ketiga telah hilang. Dan meskipun kami memiliki kode sumber kernel yang ditulis dalam C edisi keempat, yang dirilis pada November 1973, tetapi dirilis beberapa bulan sebelum rilis resminya dan tidak berisi penerapan jalur pipa. Sayang sekali kode sumber untuk fitur legendaris Unix ini hilang, mungkin untuk selamanya.

Kami memiliki teks dokumentasi untuk pipe(2) dari kedua rilis, sehingga Anda dapat memulai dengan menelusuri dokumentasi edisi ketiga (untuk kata-kata tertentu, digarisbawahi "manual", string ^H literal diikuti dengan garis bawah!). Proto-pipe(2) ditulis dalam assembler dan mengembalikan hanya satu deskriptor file, tetapi sudah menyediakan fungsionalitas inti yang diharapkan:

Panggilan sistem pipa menciptakan mekanisme I/O yang disebut pipeline. Deskriptor file yang dikembalikan dapat digunakan untuk operasi baca dan tulis. Ketika sesuatu ditulis ke dalam pipa, ia menyimpan hingga 504 byte data, setelah itu proses penulisan dihentikan. Saat membaca dari pipa, data buffer diambil.

Pada tahun berikutnya, kernel telah ditulis ulang dalam bahasa C, dan pipa(2) dalam edisi keempat memperoleh tampilan modernnya dengan prototipe "pipe(fildes)Β»:

Panggilan sistem pipa menciptakan mekanisme I/O yang disebut pipeline. Deskriptor file yang dikembalikan dapat digunakan dalam operasi baca dan tulis. Ketika sesuatu ditulis ke pipa, deskriptor yang dikembalikan dalam r1 (resp.fildes[1]) digunakan, di-buffer hingga 4096 byte data, setelah itu proses penulisan ditangguhkan. Saat membaca dari pipa, deskriptor kembali ke r0 (resp.fildes[0]) mengambil data.

Diasumsikan bahwa setelah sebuah pipa didefinisikan, dua (atau lebih) proses yang berinteraksi (diciptakan oleh pemanggilan berikutnya garpu) akan meneruskan data dari pipeline menggunakan panggilan Baca baca ΠΈ menulis.

Shell memiliki sintaks untuk mendefinisikan array linier dari proses yang terhubung melalui pipa.

Panggilan untuk membaca dari pipa kosong (tidak berisi data buffer) yang hanya memiliki satu ujung (semua deskriptor file tulis ditutup) mengembalikan "akhir file". Panggilan tulis dalam situasi serupa diabaikan.

Paling awal implementasi pipa yang diawetkan berhubungan ke edisi kelima Unix (Juni 1974), tetapi hampir identik dengan yang muncul di rilis berikutnya. Hanya ditambahkan komentar, jadi edisi kelima bisa dilewati.

Unix Edisi Keenam (1975)

Mulai membaca kode sumber Unix edisi keenam (Mei 1975). Sebagian besar berkat Lions jauh lebih mudah ditemukan daripada sumber versi sebelumnya:

Selama bertahun-tahun buku itu Lions adalah satu-satunya dokumen tentang kernel Unix yang tersedia di luar Bell Labs. Meskipun lisensi edisi keenam mengizinkan guru untuk menggunakan kode sumbernya, lisensi edisi ketujuh mengecualikan kemungkinan ini, sehingga buku tersebut didistribusikan dalam salinan yang diketik secara ilegal.

Hari ini Anda dapat membeli salinan cetak ulang buku tersebut, yang sampulnya menggambarkan siswa di mesin fotokopi. Dan terima kasih kepada Warren Toomey (yang memulai proyek TUHS), Anda dapat mengunduh PDF Sumber Edisi Keenam. Saya ingin memberi Anda gambaran tentang berapa banyak upaya yang dilakukan untuk membuat file:

Lebih dari 15 tahun yang lalu, saya mengetikkan salinan kode sumber yang disediakan Lionskarena saya tidak suka kualitas salinan saya dari salinan lain yang tidak diketahui jumlahnya. TUHS belum ada, dan saya tidak memiliki akses ke sumber lama. Tetapi pada tahun 1988 saya menemukan kaset lama dengan 9 trek yang memiliki cadangan dari komputer PDP11. Sulit untuk mengetahui apakah itu berhasil, tetapi ada pohon /usr/src/ utuh di mana sebagian besar file bertanda 1979, yang bahkan terlihat kuno. Itu edisi ketujuh, atau turunan PWB, pikirku.

Saya mengambil temuan itu sebagai dasar dan mengedit sumber secara manual ke edisi keenam. Sebagian kode tetap sama, sebagian harus sedikit diedit, mengubah token modern += menjadi =+ yang sudah usang. Sesuatu dihapus begitu saja, dan sesuatu harus ditulis ulang sepenuhnya, tetapi tidak terlalu banyak.

Dan hari ini kita bisa membaca secara online di TUHS kode sumber edisi keenam arsip, yang dibantu oleh Dennis Ritchie.

Omong-omong, sekilas fitur utama kode-C sebelum periode Kernighan dan Ritchie adalah keringkasan. Jarang saya dapat menyisipkan potongan kode tanpa pengeditan ekstensif agar sesuai dengan area tampilan yang relatif sempit di situs saya.

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

Ukuran buffer tidak berubah sejak edisi keempat. Namun di sini kita melihat, tanpa dokumentasi publik apa pun, bahwa pipeline pernah menggunakan file sebagai penyimpanan cadangan!

Adapun file LARG, mereka sesuai dengan inode-flag LARG, yang digunakan oleh "algoritme pengalamatan besar" untuk diproses blok tidak langsung untuk mendukung sistem file yang lebih besar. Karena Ken mengatakan lebih baik tidak menggunakannya, saya dengan senang hati mempercayai kata-katanya.

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

Komentar tersebut dengan jelas menggambarkan apa yang terjadi di sini. Tapi tidak semudah itu untuk memahami kodenya, sebagian karena caranya "struct pengguna uΒ» dan register R0 ΠΈ R1 parameter panggilan sistem dan nilai pengembalian diteruskan.

Mari kita coba dengan alloc() tempatkan di disk inode (inode), dan dengan bantuan kesalahan() - menyimpan dua mengajukan. Jika semuanya berjalan dengan baik, kami akan menyetel flag untuk mengidentifikasi file-file ini sebagai dua ujung pipa, mengarahkannya ke inode yang sama (yang jumlah referensinya menjadi 2), dan menandai inode sebagai dimodifikasi dan sedang digunakan. Perhatikan permintaan untuk masukan() di jalur kesalahan untuk mengurangi jumlah referensi di inode baru.

pipe() jatuh tempo R0 ΠΈ R1 mengembalikan nomor deskriptor file untuk membaca dan menulis. falloc() mengembalikan pointer ke struktur file, tetapi juga "mengembalikan" melalui u.u_ar0[R0] dan deskriptor file. Artinya, kode disimpan di r deskriptor file untuk membaca dan menetapkan deskriptor untuk menulis langsung dari u.u_ar0[R0] setelah panggilan kedua falloc().

Bendera FPIPE, yang kami setel saat membuat pipeline, mengontrol perilaku fungsi rdwr() di sys2.c, yang memanggil rutinitas 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 fungsinya readp() Π² pipe.c membaca data dari pipa. Tetapi lebih baik untuk melacak implementasinya mulai dari writep(). Sekali lagi, kode menjadi lebih rumit karena sifat dari konvensi penyampaian argumen, tetapi beberapa detail dapat dihilangkan.

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 ingin menulis byte ke input pipa u.u_count. Pertama kita perlu mengunci inode (lihat di bawah plock/prele).

Kemudian kami memeriksa jumlah referensi inode. Selama kedua ujung pipa tetap terbuka, penghitungnya harus 2. Kami berpegang pada satu tautan (dari rp->f_inode), jadi jika penghitungnya kurang dari 2, ini berarti bahwa proses pembacaan telah menutup ujung salurannya. Dengan kata lain, kami mencoba menulis ke pipa tertutup, yang merupakan kesalahan. Kode kesalahan pertama EPIPE dan sinyal SIGPIPE muncul di edisi keenam Unix.

Tetapi meskipun konveyor terbuka, konveyor bisa penuh. Dalam hal ini, kami melepaskan kunci dan tertidur dengan harapan proses lain akan membaca dari pipa dan mengosongkan cukup ruang di dalamnya. Saat kita bangun, kita kembali ke awal, menutup kuncinya lagi dan memulai siklus tulis baru.

Jika ada cukup ruang kosong di dalam pipa, maka kami menulis data menggunakan tulis(). Parameter i_size1 inode'a (dengan pipa kosong bisa sama dengan 0) menunjuk ke akhir data yang sudah dikandungnya. Jika ada cukup ruang untuk menulis, kita dapat mengisi saluran pipa i_size1 untuk PIPESIZ. Kemudian kami melepaskan kunci dan mencoba membangunkan proses apa pun yang menunggu untuk dibaca dari pipa. Kami kembali ke awal untuk melihat apakah kami berhasil menulis byte sebanyak yang kami butuhkan. Jika tidak, maka kami memulai siklus rekaman baru.

Biasanya parameter i_mode inode digunakan untuk menyimpan izin r, w ΠΈ x. Tetapi dalam kasus pipeline, kami memberi sinyal bahwa beberapa proses sedang menunggu penulisan atau pembacaan menggunakan bit IREAD ΠΈ IWRITE masing-masing. Proses menetapkan bendera dan panggilan sleep(), dan diharapkan di masa mendatang beberapa proses lain akan dipanggil wakeup().

Keajaiban nyata terjadi di sleep() ΠΈ wakeup(). Mereka diimplementasikan di slp.c, sumber dari komentar terkenal "Anda tidak diharapkan untuk memahami ini". Untungnya, kami tidak harus memahami kodenya, lihat saja beberapa komentar:

/*
 * 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 memanggil sleep() untuk saluran tertentu, nantinya dapat dibangunkan oleh proses lain, yang akan memanggil wakeup() untuk saluran yang sama. writep() ΠΈ readp() mengoordinasikan tindakan mereka melalui panggilan berpasangan tersebut. perhatikan itu pipe.c selalu mengutamakan PPIPE saat dipanggil sleep(), jadi semua sleep() dapat terganggu oleh sinyal.

Sekarang kita memiliki segalanya untuk memahami fungsinya 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 akan lebih mudah membaca fungsi ini dari bawah ke atas. Cabang "baca dan kembalikan" biasanya digunakan ketika ada beberapa data dalam saluran pipa. Dalam hal ini, kami menggunakan membaca() membaca data sebanyak yang tersedia mulai dari yang sekarang f_offset baca, lalu perbarui nilai offset yang sesuai.

Pada pembacaan selanjutnya, pipeline akan kosong jika pembacaan offset telah tercapai i_size1 di inode. Kami mengatur ulang posisi ke 0 dan mencoba membangunkan proses apa pun yang ingin menulis ke saluran pipa. Kita tahu bahwa ketika konveyor penuh, writep() tertidur di ip+1. Dan sekarang pipanya kosong, kita bisa membangunkannya untuk melanjutkan siklus tulisnya.

Jika tidak ada yang bisa dibaca, maka readp() dapat memasang bendera IREAD dan tertidur ip+2. Kita tahu apa yang akan membangunkannya writep()ketika menulis beberapa data ke pipa.

Komentar di baca() dan tulis() akan membantu Anda memahami bahwa alih-alih meneruskan parameter melalui "uΒ» kita dapat memperlakukannya seperti fungsi I/O biasa yang mengambil file, posisi, buffer di memori, dan menghitung jumlah byte 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;
/* … */

Adapun pemblokiran "konservatif", lalu readp() ΠΈ writep() mengunci inode sampai selesai atau mendapatkan hasil (mis wakeup). plock() ΠΈ prele() bekerja sederhana: menggunakan serangkaian panggilan yang berbeda sleep ΠΈ wakeup izinkan kami untuk membangunkan proses apa pun yang membutuhkan kunci yang baru saja kami lepaskan:

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

Awalnya saya tidak mengerti mengapa readp() tidak menyebabkan prele(ip) sebelum panggilan wakeup(ip+1). Hal pertama writep() panggilan dalam lingkarannya, ini plock(ip), yang menghasilkan kebuntuan jika readp() belum menghapus bloknya, jadi kodenya entah bagaimana harus bekerja dengan benar. Jika Anda melihat wakeup(), menjadi jelas bahwa itu hanya menandai proses tidur siap untuk dieksekusi, sehingga di masa depan sched() benar-benar meluncurkannya. Jadi readp() penyebab wakeup(), buka kunci, atur IREAD dan panggilan sleep(ip+2)- semua ini sebelumnya writep() memulai kembali siklus.

Ini melengkapi deskripsi saluran pipa di edisi keenam. Kode sederhana, implikasi luas.

Unix Edisi Ketujuh (Januari 1979) adalah rilis besar baru (empat tahun kemudian) yang memperkenalkan banyak aplikasi baru dan fitur kernel. Itu juga telah mengalami perubahan signifikan sehubungan dengan penggunaan pengecoran tipe, serikat pekerja dan penunjuk yang diketik ke struktur. Namun kode pipa praktis tidak berubah. Kita bisa melewati edisi ini.

Xv6, kernel sederhana mirip Unix

Untuk membuat nukleus Xv6 dipengaruhi oleh Unix edisi keenam, tetapi ditulis dalam bahasa C modern untuk dijalankan pada prosesor x86. Kodenya mudah dibaca dan dimengerti. Selain itu, tidak seperti sumber Unix dengan TUHS, Anda dapat mengompilasinya, memodifikasinya, dan menjalankannya di selain PDP 11/70. Oleh karena itu core ini banyak digunakan di perguruan tinggi sebagai bahan ajar sistem operasi. Sumber ada di Github.

Kode berisi implementasi yang jelas dan bijaksana pipa.c, didukung oleh buffer di memori, bukan inode di disk. Di sini saya hanya memberikan definisi "pipa struktural" 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() mengatur status semua implementasi lainnya, yang mencakup fungsi piperead(), pipewrite() ΠΈ pipeclose(). Panggilan sistem yang sebenarnya sys_pipe adalah pembungkus yang diimplementasikan di sysfile.c. Saya sarankan membaca semua kodenya. Kompleksitasnya berada pada level kode sumber edisi keenam, tetapi jauh lebih mudah dan menyenangkan untuk dibaca.

Linux 0.01

Anda dapat menemukan kode sumber untuk Linux 0.01. Akan sangat bermanfaat untuk mempelajari implementasi jaringan pipa di bukunya fs/pipe.c. Di sini, inode digunakan untuk merepresentasikan pipeline, tetapi pipeline itu sendiri ditulis dalam bahasa C modern. Jika Anda telah meretas kode edisi keenam, Anda tidak akan mengalami masalah di sini. Seperti inilah 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;
}

Bahkan tanpa melihat definisi struct, Anda dapat mengetahui bagaimana penghitungan referensi inode digunakan untuk memeriksa apakah operasi tulis menghasilkan SIGPIPE. Selain pekerjaan byte demi byte, fungsi ini mudah dibandingkan dengan ide di atas. Bahkan logika sleep_on/wake_up tidak terlihat begitu asing.

Kernel Linux Modern, FreeBSD, NetBSD, OpenBSD

Saya segera membahas beberapa kernel modern. Tak satu pun dari mereka sudah memiliki implementasi berbasis disk (tidak mengherankan). Linux memiliki implementasinya sendiri. Dan meskipun tiga kernel BSD modern berisi implementasi berdasarkan kode yang ditulis oleh John Dyson, selama bertahun-tahun mereka menjadi terlalu berbeda satu sama lain.

Untuk membaca fs/pipe.c (di Linux) atau sys/kern/sys_pipe.c (di *BSD), dibutuhkan dedikasi yang nyata. Performa dan dukungan untuk fitur seperti vektor dan I/O asinkron penting dalam kode saat ini. Dan detail alokasi memori, kunci, dan konfigurasi kernel semuanya sangat bervariasi. Ini bukan yang dibutuhkan universitas untuk kursus pengantar sistem operasi.

Bagaimanapun, menarik bagi saya untuk menggali beberapa pola lama (misalnya, generate SIGPIPE dan kembali EPIPE saat menulis ke pipa tertutup) di semua kernel modern yang sangat berbeda ini. Saya mungkin tidak akan pernah melihat komputer PDP-11 secara langsung, tetapi masih banyak yang harus dipelajari dari kode yang ditulis beberapa tahun sebelum saya lahir.

Ditulis oleh Divi Kapoor pada tahun 2011, artikel "Implementasi Pipa dan FIFO Kernel Linuxadalah ikhtisar tentang cara kerja pipeline Linux (sejauh ini). A komit terbaru di linux mengilustrasikan model interaksi pipa, yang kemampuannya melebihi kemampuan file sementara; dan juga menunjukkan seberapa jauh jalur pipa telah berubah dari "penguncian yang sangat konservatif" di kernel Unix edisi keenam.

Sumber: www.habr.com

Tambah komentar