Bagaimana Unix pipeline diimplementasikan


Artikel ini menjelaskan implementasi saluran pipa di kernel Unix. Saya agak kecewa dengan artikel terbaru yang berjudul " Bagaimana cara kerja pipeline pada Unix?" " Bukan tentang perangkat internal. Saya menjadi tertarik, dan saya membenamkan diri dalam sumber-sumber lama untuk menemukan jawabannya.

Apa yang kita bicarakan?


Pipa - "mungkin penemuan paling penting di Unix" - adalah ciri khas dari filosofi dasar Unix dalam menggabungkan program kecil bersama-sama, serta baris perintah yang sudah dikenal:

$ echo hello | wc -c
6

Fungsionalitas ini tergantung pada panggilan sistem yang disediakan oleh kernel pipe, yang dijelaskan pada halaman dokumentasi pipe (7) dan pipe (2) :

Konveyor menyediakan saluran komunikasi antar proses searah. Pipeline memiliki input (akhir penulisan) dan output (akhir baca). Data yang ditulis ke input dari pipa dapat dibacakan.

Pipeline dibuat menggunakan panggilan pipe(2)yang mengembalikan dua deskriptor file: satu merujuk pada input dari pipeline, yang kedua ke output.

Jejak hasil dari perintah di atas menunjukkan penciptaan pipa dan aliran data melalui itu dari satu proses ke proses 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, "hello\n", 6)    = 6
[pid 2604796] dup2(3, 0)                = 0
[pid 2604796] execve("/usr/bin/wc", ["wc", "-c"], …)
[pid 2604796] read(0, "hello\n", 16384) = 6
[pid 2604796] write(1, "6\n", 2)        = 2

Proses induk memanggil pipe()untuk mendapatkan deskriptor file terlampir. Satu proses anak menulis ke satu deskriptor, dan proses lain membaca data yang sama dari deskriptor lain. Pembungkus menggunakan dup2 “renames” deskriptor 3 dan 4 untuk mencocokkan stdin dan stdout.

Tanpa pipa, shell harus menulis hasil dari satu proses ke file dan mentransfernya ke proses lain sehingga membaca data dari file. Akibatnya, kami akan menghabiskan lebih banyak sumber daya dan ruang disk. Namun, jalur pipa bagus bukan hanya karena mereka menghindari penggunaan file sementara:

, read(2) , . , write(2) , .

Seperti persyaratan POSIX, ini adalah properti penting: menulis ke saluran pipa hingga PIPE_BUFbyte (setidaknya 512) harus berupa atom sehingga proses dapat berinteraksi satu sama lain melalui pipa dengan cara yang sama seperti file biasa (yang tidak memberikan jaminan seperti itu) dapat.

Saat menggunakan file biasa, suatu proses dapat menulis semua data outputnya ke sana dan mentransfernya ke proses lain. Atau proses dapat beroperasi dalam mode paralelisasi keras, menggunakan mekanisme pensinyalan eksternal (seperti semaphore) untuk saling menginformasikan tentang penyelesaian penulisan atau membaca. Konveyor menyelamatkan kita dari semua masalah ini.

Apa yang kita cari


Saya akan menjelaskannya dengan jari saya untuk memudahkan Anda membayangkan bagaimana konveyor dapat bekerja. Anda perlu mengalokasikan buffer dan beberapa status dalam memori. Anda akan membutuhkan fungsi untuk menambah dan menghapus data dari buffer. Dibutuhkan beberapa cara untuk memanggil fungsi selama operasi baca dan tulis ke file deskriptor. Dan kunci diperlukan untuk menerapkan perilaku khusus yang dijelaskan di atas.

Sekarang kita siap untuk menginterogasi dalam cahaya terang dari kode sumber kernel untuk mengkonfirmasi atau menyangkal model mental samar kita. Tapi selalu bersiap untuk yang tak terduga.

Di mana kita mencari?


Saya tidak tahu di mana salinan buku terkenal " Lions book " dengan kode sumber Unix 6 berada, tetapi berkat The Unix Heritage Society, Anda dapat mencari versi Unix yang lebih lama di kode sumber .

Berkeliaran di sekitar arsip TUHS mirip dengan mengunjungi museum. Kita dapat melihat sejarah umum kita, dan saya menghargai upaya bertahun-tahun untuk memulihkan semua materi ini sedikit demi sedikit dari kaset dan cetakan lama. Dan saya sangat menyadari fragmen-fragmen yang masih hilang.

Setelah memuaskan keingintahuan kami tentang sejarah kuno konveyor, kita dapat melihat core modern untuk perbandingan.

Omong-omong, pipeadalah nomor sistem panggilan 42 dalam tabel sysent[]. Kebetulan?

Kernel Unix Tradisional (1970–1974)


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

TUHS mengklaim bahwa edisi ketiga Unix (Februari 1973) adalah versi pertama dengan saluran pipa:

Edisi ketiga Unix adalah versi terbaru dengan kernel yang ditulis dalam bahasa assembly, tetapi versi pertama dengan saluran pipa. Selama tahun 1973, pekerjaan dilakukan untuk memperbaiki edisi ketiga, intinya ditulis ulang dalam bahasa C, sehingga muncullah edisi keempat Unix.

Salah satu pembaca menemukan pemindaian dokumen di mana Doug McIlroy mengusulkan gagasan "menghubungkan program dengan prinsip selang kebun."


Dalam buku Brian Kernighan " Unix: A History and a Memoir ", dalam sejarah penampilan konveyor, dokumen ini juga disebutkan: "... itu tergantung di dinding di kantor saya di Bell Labs selama 30 tahun." Berikut wawancara dengan McIlroy , dan kisah lain dari karya McIlroy yang ditulis pada tahun 2014 :

Unix, , , , - , , . , . , , , . ? «» , , -, : « !».

. , , ( ), . . . API , .

Sayangnya, kode sumber kernel untuk Unix edisi ketiga hilang. Dan meskipun kami memiliki kode sumber untuk kernel edisi keempat yang ditulis dalam C , dirilis pada November 1973, itu dirilis beberapa bulan sebelum rilis resmi dan tidak mengandung implementasi pipa. Sangat disayangkan bahwa kode sumber fungsi Unix yang legendaris hilang, mungkin selamanya.

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

Panggilan sistem pipa menciptakan mekanisme input output yang disebut pipeline. Deskriptor file yang dikembalikan dapat digunakan untuk operasi baca dan tulis. Ketika sesuatu ditulis ke saluran pipa, hingga 504 byte data buffer, setelah itu proses penulisan dijeda. Saat membaca dari saluran pipa, data buffer diambil.

Pada tahun berikutnya, kernel ditulis ulang dalam C, dan pipe (2) pada edisi keempat menemukan tampilan modernnya dengan prototipe " pipe(fildes)":

pipe , . . - , , r1 (. fildes[1]), 4096 , . , r0 (. fildes[0]), .

, ( ) ( fork) read write.

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

Baca panggilan dari saluran pipa kosong (tidak mengandung data buffered) yang hanya memiliki satu ujung (semua deskriptor file penulisan ditutup) mengembalikan "akhir file". Merekam panggilan dalam situasi yang sama diabaikan.

Implementasi pipa yang masih ada paling awal berasal dari Unix edisi kelima (Juni 1974), tetapi hampir identik dengan yang muncul pada rilis berikutnya. Hanya komentar yang ditambahkan, sehingga edisi kelima dapat dilewati.

Edisi Keenam Unix (1975)


Kita mulai membaca kode sumber Unix edisi keenam (Mei 1975). Berkat Lions, menemukannya jauh lebih mudah daripada kode sumber versi sebelumnya:

Selama bertahun-tahun, Lions telah menjadi satu-satunya dokumen inti Unix yang tersedia di luar dinding Bell Labs. Meskipun lisensi edisi keenam memungkinkan guru untuk menggunakan kode sumbernya, lisensi edisi ketujuh mengecualikan kemungkinan ini, sehingga buku itu didistribusikan dalam bentuk salinan tulisan tangan yang tidak sah.

Hari ini Anda dapat membeli salinan cetak ulang buku tersebut, di sampulnya siswa diperlihatkan di mesin fotokopi. Dan terima kasih kepada Warren Tumi (yang meluncurkan proyek TUHS), Anda dapat mengunduh file PDF dengan kode sumber untuk edisi keenam . Saya ingin memberi Anda gambaran tentang berapa banyak upaya yang diperlukan untuk membuat file:

15 , Lions, . TUHS , . 1988- 9 , PDP11. , , /usr/src/, 1979- , . PWB, .

. , , += =+. - , - , .

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

Ngomong-ngomong, pada pandangan pertama, fitur utama dari C-code sebelum periode Kernigan dan Richie adalah singkatnya . Tidak sering, saya berhasil menyematkan cuplikan kode tanpa pengeditan ekstensif agar sesuai dengan area tampilan yang relatif sempit di situs saya.

Di awal /usr/sys/ken/pipe.c ada komentar penjelasan (dan ya, ada juga / 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. Tapi di sini, tanpa dokumentasi publik, kita melihat bahwa sekali pipa menggunakan file sebagai penyimpanan cadangan!

Adapun file LARG, mereka sesuai dengan flag inode LARG , yang digunakan oleh "algoritma pengalamatan tinggi" untuk memproses blok tidak langsung untuk mendukung sistem file yang lebih besar. Karena Ken mengatakan lebih baik tidak menggunakannya, saya dengan senang hati akan mengambil kata-katanya untuk itu.

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 jelas menggambarkan apa yang terjadi di sini. Tetapi untuk memahami kode itu tidak mudah, sebagian karena cara dengan bantuan « pengguna struct » dan mendaftar R0dan R1mentransfer parameter panggilan sistem dan mengembalikan nilai.

Mari kita coba menggunakan ialloc () untuk menempatkan inode (inode ) pada disk , dan menggunakan falloc () untuk menempatkan dua file dalam memori . Jika semuanya berjalan dengan baik, maka kita akan mengatur flag untuk mendefinisikan file-file ini sebagai dua ujung pipa, arahkan mereka ke inode yang sama (yang jumlah referensi akan 2), dan tandai inode sebagai diubah dan digunakan. Perhatikan panggilan ke iput ()di jalur kesalahan untuk mengurangi jumlah referensi di inode baru.

pipe()harus melalui R0dan R1mengembalikan nomor deskriptor file untuk membaca dan menulis. falloc()mengembalikan pointer ke struktur file, tetapi juga "mengembalikan" melalui u.u_ar0[R0]deskriptor file. Artinya, kode menyimpan ke rdeskriptor file untuk membaca dan menetapkan deskriptor untuk menulis langsung dari u.u_ar0[R0]setelah panggilan kedua falloc().

Bendera FPIPEyang kita atur saat membuat pipeline mengontrol perilaku fungsi rdwr () di sys2.c , yang memanggil rutin I / O 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()dalam pipe.cmembaca data dari pipeline. Tetapi melacak implementasi lebih baik dimulai dengan writep(). Sekali lagi, kode menjadi lebih rumit karena spesifikasi perjanjian transfer 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 periksa jumlah referensi inode. Sementara kedua ujung pipa tetap terbuka, penghitung harus 2. Kami menyimpan satu tautan (keluar rp->f_inode), jadi jika penghitung kurang dari 2, ini harus berarti bahwa proses pembacaan telah menutup ujung pipa. Dengan kata lain, kami mencoba menulis dalam saluran tertutup, dan ini adalah kesalahan. Kode EPIPEdan sinyal kesalahan pertama kali SIGPIPEmuncul di edisi keenam Unix.

Tetapi bahkan jika konveyor terbuka, itu bisa penuh. Dalam hal ini, kami melepas kunci dan tidur dengan harapan proses lain akan membaca dari saluran pipa dan membebaskan ruang yang cukup di dalamnya. Setelah bangun, kami kembali ke awal, lagi-lagi kami memblokir kunci dan memulai siklus perekaman baru.

Jika ada cukup ruang kosong di dalam pipa, maka kami menulis data ke sana menggunakan writei () . Parameter i_size1dalam inode (dengan pipeline kosong bisa 0) menunjukkan akhir data yang sudah dikandungnya. Jika ada cukup ruang perekaman, kita dapat mengisi conveyor dari i_size1kePIPESIZ. Kemudian kami melepas kunci dan mencoba membangunkan setiap proses yang menunggu kesempatan untuk membaca dari saluran pipa. Kami kembali ke awal untuk melihat apakah kami berhasil menulis byte sebanyak yang kami butuhkan. Jika gagal, maka kami memulai siklus perekaman baru.

Biasanya i_mode, parameter inode digunakan untuk menyimpan izin r, wdan x. Tetapi dalam kasus jalur pipa, kami memberi sinyal bahwa beberapa proses menunggu untuk menulis atau membaca menggunakan bit IREADdan, IWRITEmasing-masing. Suatu proses menetapkan bendera dan panggilan sleep(), dan diharapkan beberapa proses lain akan memanggil di masa depan wakeup().

Sihir nyata terjadi di sleep()dan wakeup(). Mereka diimplementasikan dalam slp.c, sumber komentar terkenal, "Kamu tidak diharapkan untuk memahami ini." Untungnya, kita tidak diharuskan untuk memahami kodenya, cukup lihat 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) /* … */

Suatu proses yang memanggil sleep()untuk saluran tertentu nanti dapat dibangunkan oleh proses lain yang akan meminta wakeup()untuk saluran yang sama. writep()dan readp()mengoordinasikan tindakan mereka melalui panggilan berpasangan tersebut. Harap dicatat bahwa pipe.cselalu memberikan prioritas PPIPEsaat menelepon sleep(), sehingga semua orang sleep()dapat mengganggu 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 lebih mudah membaca fungsi ini dari bawah ke atas. Cabang "baca dan kembali" biasanya digunakan ketika ada beberapa data dalam pipa. Dalam hal ini, kami menggunakan readi () untuk membaca sebanyak mungkin data yang tersedia mulai dari f_offsetpembacaan saat ini , dan kemudian memperbarui nilai offset yang sesuai.

Pada pembacaan berikutnya, pipa akan kosong jika pembacaan offset telah mencapai nilai i_size1inode. Kami mengatur ulang posisi ke 0 dan mencoba membangunkan setiap proses yang ingin ditulisnya ke saluran pipa. Kita tahu bahwa ketika konveyor sudah penuh, konveyor writep()akan tertidur ip+1. Dan sekarang setelah pipanya kosong, kita bisa membangunkannya sehingga melanjutkan siklus rekamannya.

Jika tidak ada yang dibaca, itu readp()bisa mengatur bendera IREADdan tertidurip+2. Kita tahu apa yang akan membangunkannya writep()ketika dia menulis beberapa data ke saluran pipa.

Komentar pada readi () dan writei () akan membantu untuk memahami bahwa alih-alih melewati parameter melalui " u", kita dapat memperlakukannya seperti fungsi I / O biasa, yang mengambil file, posisi, buffer dalam memori, dan menghitung jumlah byte untuk membaca atau menulis .

/*
 * 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 kunci "konservatif", lalu readp()dan writep()blok inode selama mereka menyelesaikan pekerjaan atau tidak mendapatkan hasilnya (yaitu penyebabnya wakeup). plock()dan mereka prele()bekerja secara sederhana: menggunakan serangkaian panggilan yang berbeda sleepdan wakeupmemungkinkan kami untuk membangunkan setiap proses yang membutuhkan kunci yang baru saja kami hapus:

/*
 * 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 awalnya saya tidak mengerti mengapa readp()tidak menelepon prele(ip)sebelum panggilan wakeup(ip+1). Hal pertama yang writep()menyebabkan dalam perulangannya adalah ia plock(ip)menyebabkan kebuntuan jika readp()belum menghapus bloknya, jadi kodenya harus entah bagaimana bekerja dengan benar. Jika Anda melihatnya wakeup(), menjadi jelas bahwa ia hanya menandai proses tidur sebagai siap untuk dieksekusi, sehingga di masa depan itu sched()benar - benar meluncurkannya. Jadi itu readp()menyebabkan wakeup(), membuka kunci, set IREADdan panggilan sleep(ip+2)- semua ini sebelum writep()melanjutkan siklus.

Ini melengkapi deskripsi konveyor di edisi keenam. Kode sederhana, konsekuensi luas.

Edisi Ketujuh Unix(Januari 1979) adalah rilis utama baru (empat tahun kemudian), di mana banyak aplikasi baru dan sifat kernel muncul. Juga, telah ada perubahan signifikan sehubungan dengan penggunaan tipe casting, union'ov dan pointer yang diketik untuk struktur. Namun, kode pipa tidak banyak berubah. Kita dapat melewati edisi ini.

Xv6, kernel berbentuk Unix yang sederhana


Edisi keenam Unix memengaruhi pembuatan inti Xv6 , tetapi ditulis dalam C modern untuk dijalankan pada prosesor x86. Kode mudah dibaca, jelas. Selain itu, tidak seperti sumber Unix dengan TUHS, Anda dapat mengompilasinya, memodifikasinya, dan menjalankannya pada hal lain selain PDP 11/70. Oleh karena itu, inti ini banyak digunakan di universitas sebagai bahan pendidikan pada sistem operasi. Sumber ada di Github .

Kode tersebut berisi implementasi pipe.c yang jelas dan dipikirkan matang - matang , didukung oleh buffer dalam memori alih-alih inode pada disk. Di sini saya hanya memberikan definisi "pipa struktural" dan fungsi 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 status sisa implementasi, yang mencakup fungsi piperead(), pipewrite()dan pipeclose(). Panggilan sistem yang sebenarnya sys_pipeadalah pembungkus yang diimplementasikan di sysfile.c . Saya sarankan membaca semua kodenya. Kompleksitasnya ada pada level sumber dari edisi keenam, tetapi jauh lebih mudah dan lebih menyenangkan untuk dibaca.

Linux 0,01


Anda dapat menemukan kode sumber untuk Linux 0,01. Ini akan menjadi pelajaran untuk mempelajari implementasi pipa dalam fs/ nya pipe.c. Di sini, inode digunakan untuk mewakili pipeline, tetapi pipeline itu sendiri ditulis dalam C. modern. Jika Anda melewati kode edisi keenam, maka Anda tidak akan mengalami kesulitan. Begini tampilannya 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 struktur, Anda dapat mengetahui bagaimana penghitung referensi inode digunakan untuk memeriksa apakah operasi penulisan mengarah ke SIGPIPE. Selain pekerjaan byte, fungsi ini mudah dikorelasikan dengan ide-ide di atas. Bahkan logika sleep_on/ wake_uptidak terlihat begitu asing.

Linux modern, FreeBSD, NetBSD, kernel OpenBSD


Saya segera memeriksa beberapa kernel modern. Tak satu pun dari mereka sudah memiliki implementasi disk (tidak mengherankan). Linux memiliki implementasinya sendiri. Meskipun ketiga 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(atas * BSD), dedikasi benar diperlukan. Kinerja dan dukungan untuk fitur seperti vektor dan I / O yang tidak sinkron penting dalam kode saat ini. Dan rincian alokasi memori, kunci dan konfigurasi kernel - semua ini sangat berbeda. Ini bukan yang dibutuhkan universitas untuk kursus pengantar tentang sistem operasi.

Bagaimanapun, menarik bagi saya untuk menggali beberapa pola lama (misalnya, menghasilkan SIGPIPEdan kembali EPIPEketika menulis ke pipa tertutup) dalam semua inti modern yang sangat berbeda ini. Saya mungkin tidak akan pernah melihat komputer PDP-11 yang hidup, tetapi masih banyak yang harus dipelajari dari kode yang ditulis beberapa tahun sebelum kelahiran saya.

Ditulis oleh Divi Kapoor pada tahun 2011, artikel “ Implementasi Kernel Linux untuk Pipes dan FIFOs ” memberikan gambaran tentang bagaimana (sejauh ini) pipeline bekerja di Linux. Dan Linux commit baru - baru ini menggambarkan model interaksi pipelined yang kemampuannya melebihi kemampuan file sementara; dan juga menunjukkan sejauh mana jalur pipa dari “penguncian sangat konservatif” di kernel Unix edisi keenam.

All Articles