Bagaimana proses substitusi diimplementasikan dalam bash?

12

Saya sedang meneliti pertanyaan lain , ketika saya menyadari saya tidak mengerti apa yang terjadi di bawah tenda, /dev/fd/*file apa itu dan bagaimana proses anak bisa membukanya.

x-yuri
sumber
Bukankah pertanyaan itu dijawab?
phk

Jawaban:

21

Ya, ada banyak aspek untuk itu.

Penjelas file

Untuk setiap proses, kernel memelihara tabel file yang terbuka (well, itu mungkin diimplementasikan secara berbeda, tetapi karena Anda tidak dapat melihatnya, Anda bisa menganggap itu adalah tabel sederhana). Tabel itu berisi informasi tentang file mana itu / di mana ia dapat ditemukan, dalam mode apa Anda membukanya, di posisi mana Anda sedang membaca / menulis, dan apa pun yang diperlukan untuk benar-benar melakukan operasi I / O pada file itu. Sekarang prosesnya tidak pernah membaca (atau bahkan menulis) tabel itu. Ketika proses membuka file, itu akan kembali disebut file descriptor. Yang hanya merupakan indeks ke dalam tabel.

Direktori /dev/fddan isinya

Di Linux dev/fdsebenarnya tautan simbolis ke /proc/self/fd. /procadalah sistem file pseudo di mana kernel memetakan beberapa struktur data internal untuk diakses dengan file API (sehingga mereka hanya terlihat seperti file biasa / direktori / symlink ke program). Terutama ada informasi tentang semua proses (yang memberi nama itu). Tautan simbolik /proc/selfselalu merujuk ke direktori yang terkait dengan proses yang sedang berjalan (yaitu, proses memintanya; proses yang berbeda akan melihat nilai yang berbeda). Di direktori proses, ada subdirektorifd yang untuk setiap file yang terbuka berisi tautan simbolik yang namanya hanyalah representasi desimal dari deskriptor file (indeks ke dalam tabel file proses, lihat bagian sebelumnya), dan yang targetnya adalah file yang sesuai dengan itu.

Deskriptor file saat membuat proses anak

Proses anak dibuat oleh a fork. A forkmembuat salinan deskriptor file, yang berarti proses anak yang dibuat memiliki daftar file terbuka yang sama dengan proses induk. Jadi kecuali salah satu file yang terbuka ditutup oleh anak, mengakses deskriptor file yang diwarisi pada anak akan mengakses file yang sama seperti mengakses deskriptor file asli dalam proses induk.

Perhatikan bahwa setelah garpu, Anda awalnya memiliki dua salinan dari proses yang sama yang hanya berbeda dalam nilai balik dari panggilan garpu (orang tua mendapat PID anak, anak mendapat 0). Biasanya, sebuah garpu diikuti oleh execuntuk menggantikan salah satu salinan oleh executable lain. Deskriptor file terbuka bertahan dari eksekutif itu. Perhatikan juga bahwa sebelum eksekutif, proses tersebut dapat melakukan manipulasi lain (seperti menutup file yang seharusnya tidak didapat oleh proses baru, atau membuka file lain).

Pipa yang tidak disebutkan namanya

Pipa tanpa nama hanyalah sepasang deskriptor file yang dibuat berdasarkan permintaan oleh kernel, sehingga semua yang ditulis ke deskriptor file pertama diteruskan ke yang kedua. Penggunaan yang paling umum adalah untuk membangun pipa foo | bardari bash, di mana output standar foodiganti dengan menulis bagian dari pipa, dan input standar Menggantikan oleh bagian dibaca. Input standar dan output standar hanyalah dua entri pertama dalam tabel file (entri 0 dan 1; 2 adalah kesalahan standar), dan karenanya menggantinya berarti hanya menulis ulang entri tabel tersebut dengan data yang sesuai dengan deskriptor file lainnya (sekali lagi, implementasi aktual mungkin berbeda). Karena proses tidak dapat mengakses tabel secara langsung, ada fungsi kernel untuk melakukan itu.

Substitusi proses

Sekarang kita memiliki segalanya bersama untuk memahami bagaimana proses substitusi bekerja:

  1. Proses bash menciptakan pipa tanpa nama untuk komunikasi antara dua proses yang dibuat kemudian.
  2. Fork pesta untuk echoproses. Proses anak (yang merupakan salinan tepat dari bashproses asli ) menutup ujung pembacaan pipa dan mengganti output standar sendiri dengan ujung penulisan pipa. Mengingat bahwa itu echoadalah builtin shell, bashmungkin mengampuni execpanggilan itu sendiri , tetapi toh itu tidak masalah (shell builtin juga dapat dinonaktifkan, dalam hal ini ia menjalankan /bin/echo).
  3. Bash (yang asli, yang induk) menggantikan ekspresi <(echo 1)dengan tautan file pseudo yang /dev/fdmerujuk pada ujung bacaan dari pipa yang tidak disebutkan namanya.
  4. Eksekutif Bash untuk proses PHP (perhatikan bahwa setelah garpu, kita masih di dalam [salinan] bash). Proses baru menutup ujung tulis yang diwariskan dari pipa yang tidak disebutkan namanya (dan melakukan beberapa langkah persiapan lainnya), tetapi membiarkan ujung baca tetap terbuka. Kemudian dieksekusi PHP.
  5. Program PHP menerima nama di /dev/fd/. Karena deskriptor file yang sesuai masih terbuka, itu masih sesuai dengan ujung pipa pembacaan. Oleh karena itu jika program PHP membuka file yang diberikan untuk dibaca, apa yang sebenarnya dilakukannya adalah membuat seconddeskriptor file untuk ujung bacaan dari pipa yang tidak disebutkan namanya. Tapi itu tidak masalah, bisa dibaca dari keduanya.
  6. Sekarang program PHP dapat membaca ujung pembacaan pipa melalui deskriptor file baru, dan dengan demikian menerima output standar dari echoperintah yang menuju akhir penulisan dari pipa yang sama.
celtschk
sumber
Tentu, saya menghargai usaha Anda. Tetapi saya ingin menunjukkan beberapa masalah. Pertama, Anda berbicara tentang phpskenario, tetapi phptidak menangani pipa dengan baik . Juga, mengingat perintah cat <(echo test), yang aneh di sini adalah bahwa bashgarpu sekali untuk cat, tetapi dua kali untuk echo test.
x-yuri
13

Meminjam dari celtschkjawaban, /dev/fdadalah tautan simbolis ke /proc/self/fd. Dan /procadalah sistem file pseudo, yang menyajikan informasi tentang proses dan informasi sistem lainnya dalam struktur seperti file hirarkis. File dalam /dev/fdberhubungan dengan file, dibuka oleh suatu proses dan memiliki deskriptor file sebagai nama mereka dan file itu sendiri sebagai target mereka. Membuka file /dev/fd/Nsama dengan duplikasi deskriptor N(dengan asumsi deskriptor Nterbuka).

Dan berikut ini adalah hasil penyelidikan saya tentang cara kerjanya ( straceoutput menghilangkan detail yang tidak perlu dan dimodifikasi untuk mengekspresikan apa yang terjadi dengan lebih baik):

$ cat 1.c
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    char buf[100];
    int fd;
    fd = open(argv[1], O_RDONLY);
    read(fd, buf, 100);
    write(STDOUT_FILENO, buf, n_read);
    return 0;
}
$ gcc 1.c -o 1.out
$ cat 2.c
#include <unistd.h>
#include <string.h>

int main(void)
{
    char *p = "hello, world\n";
    write(STDOUT_FILENO, p, strlen(p));
    return 0;
}
$ gcc 2.c -o 2.out
$ strace -f -e pipe,fcntl,dup2,close,clone,close,execve,wait4,read,open,write bash -c './1.out <(./2.out)'
[bash] pipe([3, 4]) = 0
[bash] dup2(3, 63) = 63
[bash] close(3) = 0
[bash] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p2
Process p2 attached
[bash] close(4) = 0
[bash] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p1
Process p1 attached
[bash] close(63) = 0
[p2] dup2(4, 1) = 1
[p2] close(4) = 0
[p2] close(63) = 0
[bash] wait4(-1, <unfinished ...>
Process bash suspended
[p1] execve("/home/yuri/_/1.out", ["/home/yuri/_/1.out", "/dev/fd/63"], [/* 31 vars */]) = 0
[p2] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p22
Process p22 attached
[p22] execve("/home/yuri/_/2.out", ["/home/yuri/_/2.out"], [/* 31 vars */]) = 0
[p2] wait4(-1, <unfinished ...>
Process p2 suspended
[p1] open("/dev/fd/63", O_RDONLY) = 3
[p1] read(3,  <unfinished ...>
[p22] write(1, "hello, world\n", 13) = 13
[p1] <... read resumed> "hello, world\n", 100) = 13
Process p2 resumed
Process p22 detached
[p1] write(1, "hello, world\n", 13) = 13
hello, world
[p2] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = p22
[p2] --- SIGCHLD (Child exited) @ 0 (0) ---
[p2] wait4(-1, 0x7fff190f289c, WNOHANG, NULL) = -1 ECHILD (No child processes)
Process bash resumed
Process p1 detached
[bash] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = p1
[bash] --- SIGCHLD (Child exited) @ 0 (0) ---
Process p2 detached
[bash] wait4(-1, 0x7fff190f2bdc, WNOHANG, NULL) = 0
--- SIGCHLD (Child exited) @ 0 (0) ---
[bash] wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG, NULL) = p2
[bash] wait4(-1, 0x7fff190f299c, WNOHANG, NULL) = -1 ECHILD (No child processes)

Pada dasarnya, bashbuat sebuah pipa dan memberikan ujung-ujungnya kepada anak-anaknya sebagai deskriptor file (baca akhir 1.out, dan tuliskan akhir 2.out). Dan melewati baca sebagai parameter baris perintah ke 1.out( /dev/fd/63). Cara 1.outini bisa terbuka /dev/fd/63.

x-yuri
sumber