Bash: buat fifo anonim


38

Kita semua tahu mkfifodan saluran pipa. Yang pertama membuat pipa bernama , jadi kita harus memilih nama, kemungkinan besar dengan mktempdan kemudian ingat untuk membatalkan tautan. Yang lain menciptakan pipa anonim, tidak ada kerumitan dengan nama dan penghapusan, tetapi ujung-ujung pipa terikat dengan perintah dalam pipa, itu tidak benar-benar nyaman entah bagaimana mendapatkan pegangan deskriptor file dan menggunakannya dalam sisanya naskah. Dalam program yang dikompilasi, saya hanya akan melakukannya ret=pipe(filedes); di Bash ada exec 5<>filesehingga orang akan mengharapkan sesuatu seperti "exec 5<> -"atau "pipe <5 >6"-Apakah ada sesuatu seperti itu di Bash?

Jawaban:


42

Anda dapat membatalkan tautan pipa bernama segera setelah melampirkannya ke proses saat ini, yang secara praktis menghasilkan pipa anonim:

# create a temporary named pipe
PIPE=$(mktemp -u)
mkfifo $PIPE
# attach it to file descriptor 3
exec 3<>$PIPE
# unlink the named pipe
rm $PIPE
...
# anything we write to fd 3 can be read back from it
echo 'Hello world!' >&3
head -n1 <&3
...
# close the file descriptor when we are finished (optional)
exec 3>&-

Jika Anda benar-benar ingin menghindari pipa bernama (mis. Sistem file hanya baca), ide "dapatkan pegangan dari deskriptor file" juga berfungsi. Perhatikan bahwa ini khusus untuk Linux karena penggunaan procfs.

# start a background pipeline with two processes running forever
tail -f /dev/null | tail -f /dev/null &
# save the process ids
PID2=$!
PID1=$(jobs -p %+)
# hijack the pipe's file descriptors using procfs
exec 3>/proc/$PID1/fd/1 4</proc/$PID2/fd/0
# kill the background processes we no longer need
# (using disown suppresses the 'Terminated' message)
disown $PID2
kill $PID1 $PID2
...
# anything we write to fd 3 can be read back from fd 4
echo 'Hello world!' >&3
head -n1 <&4
...
# close the file descriptors when we are finished (optional)
exec 3>&- 4<&-

Anda dapat menggabungkan ini dengan penemuan otomatis deskriptor file yang tidak digunakan: stackoverflow.com/questions/8297415/…
CMCDragonkai

23

Meskipun tidak ada cangkang yang saya tahu dapat membuat pipa tanpa forking, beberapa memang memiliki yang lebih baik daripada pipa dasar cangkang.

Dalam bash, ksh dan zsh, dengan asumsi sistem Anda mendukung /dev/fd(saat ini paling banyak dilakukan), Anda dapat mengikat input atau output dari perintah ke nama file: <(command)memperluas ke nama file yang menunjuk sebuah pipa yang terhubung ke output dari command, dan >(command)memperluas ke nama file yang menunjuk pipa yang terhubung ke input command. Fitur ini disebut proses substitusi . Tujuan utamanya adalah menyalurkan lebih dari satu perintah ke atau dari yang lain, misalnya,

diff <(transform <file1) <(transform <file2)
tee >(transform1 >out1) >(transform2 >out2)

Ini juga berguna untuk memerangi beberapa kekurangan pipa shell dasar. Misalnya, command2 < <(command1)setara dengan command1 | command2, kecuali statusnya adalah command2. Kasus penggunaan lain adalah exec > >(postprocessing), yang setara dengan, tetapi lebih mudah dibaca daripada, menempatkan seluruh sisa skrip di dalamnya { ... } | postprocessing.


Saya mencoba ini dengan diff dan itu berhasil tetapi dengan kdiff3 atau dengan emacs, itu tidak berhasil. Dugaan saya adalah bahwa file temporer / dev / fd sedang dihapus sebelum kdiff3 dapat membacanya. Atau mungkin kdiff3 mencoba membaca file dua kali dan pipa hanya mengirimnya sekali?
Eyal

@Eyal Dengan proses berkelanjutan, nama file adalah referensi "ajaib" ke pipa (atau file sementara pada varian Unix yang tidak mendukung varian sihir ini). Bagaimana keajaiban diterapkan tergantung pada OS. Linux mengimplementasikannya sebagai tautan simbolis "ajaib" yang targetnya bukan nama file yang valid (itu seperti pipe:[123456]). Emacs melihat bahwa target symlink bukan nama file yang ada dan cukup membingungkan sehingga tidak membaca file (mungkin ada opsi untuk membacanya, meskipun Emacs tidak suka membuka pipa sebagai file tetap).
Gilles 'SANGAT berhenti menjadi jahat'

10

Bash 4 memiliki coprocesses .

Sebuah coprocess dieksekusi secara tidak sinkron dalam sebuah subkulit, seolah-olah perintah telah diakhiri dengan operator kontrol '&', dengan pipa dua arah dibuat antara shell yang mengeksekusi dan coprocess.

Format untuk coprocess adalah:

coproc [NAME] command [redirections] 

3

Pada Oktober 2012 fungsi ini tampaknya masih belum ada di Bash, tetapi coproc dapat digunakan jika semua yang Anda butuhkan tanpa nama / pipa anonim adalah berbicara dengan proses anak. Masalah dengan coproc pada saat ini adalah bahwa ternyata hanya satu yang didukung pada suatu waktu. Saya tidak tahu mengapa coproc mendapatkan batasan ini. Mereka seharusnya merupakan peningkatan dari kode latar belakang tugas yang ada (the & op), tapi itu pertanyaan bagi penulis bash.


Tidak hanya satu proses yang didukung. Anda dapat memberi nama mereka, selama Anda tidak memberikan perintah sederhana. Berikan daftar perintah sebagai gantinya: coproc THING { dothing; }Sekarang FD Anda sudah masuk ${THING[*]}dan Anda dapat menjalankan coproc OTHERTHING { dothing; }dan mengirim dan menerima barang-barang ke dan dari keduanya.
clacke

2
@clacke in man bash, di bawah judul BUGS, mereka mengatakan ini: Mungkin hanya ada satu proses aktif pada satu waktu . Dan Anda mendapat peringatan jika Anda memulai coproc kedua. Tampaknya berfungsi, tetapi saya tidak tahu apa yang meledak di latar belakang.
Radu C

Ok, jadi saat ini hanya berfungsi karena keberuntungan, bukan karena disengaja. Peringatan yang adil, terima kasih. :-)
clacke

2

Sementara jawaban @ DavidAnderson mencakup semua pangkalan dan menawarkan beberapa perlindungan yang bagus, hal terpenting yang terungkap adalah bahwa mendapatkan tangan Anda pada pipa anonim semudah <(:), selama Anda tetap di Linux.

Jadi jawaban tersingkat dan paling sederhana untuk pertanyaan Anda adalah:

exec 5<> <(:)

Pada macOS itu tidak akan berfungsi, maka Anda harus membuat direktori sementara untuk menampung fifo yang dinamai hingga Anda dialihkan ke sana. Saya tidak tahu tentang BSD lainnya.


Anda menyadari jawaban Anda hanya berfungsi karena ada bug di linux. Bug ini tidak ada di macOS, sehingga membutuhkan solusi yang lebih kompleks. Versi terakhir yang saya posting akan bekerja di linux walaupun bug di linux sudah diperbaiki.
David Anderson

@ Davidvidders Kedengarannya seperti Anda memiliki pengetahuan yang lebih dalam tentang ini daripada saya. Mengapa perilaku Linux bug?
clacke

1
Jika execdilewatkan dan fifo anonim yang dibuka hanya untuk membaca, maka execjangan biarkan fifo anonim ini dibuka untuk dibaca dan ditulis menggunakan deskriptor file khusus. Anda harus mengharapkan untuk mendapatkan -bash: /dev/fd/5: Permission deniedpesan, yang merupakan masalah macOS. Saya percaya bug ini adalah bahwa Ubuntu tidak menghasilkan pesan yang sama. Saya akan bersedia mengubah pikiran saya jika seseorang dapat menghasilkan dokumentasi yang mengatakan exec 5<> <(:)diizinkan.
David Anderson

@ Davidvidders Wow, itu menarik. Saya berasumsi bash melakukan sesuatu secara internal, tetapi ternyata Linux yang memungkinkan hanya melakukan open(..., O_RDWR)pada satu ujung pipa searah yang disediakan oleh substitusi dan mengubahnya menjadi pipa dua arah dalam satu FD. Anda mungkin benar bahwa seseorang seharusnya tidak mengandalkan ini. :-D Keluaran dari menggunakan piper execline untuk membuat pipa, kemudian repurposing dengan bash <>: libranet.de/display/0b6b25a8-195c-84af-6ac7-ee6696661765
clacke

Bukan berarti itu penting, tetapi jika Anda ingin melihat di Ubuntu apa yang diteruskan exec 5<>, lalu masukkan fun() { ls -l $1; ls -lH $1; }; fun <(:).
David Anderson

1

Fungsi berikut diuji menggunakan GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu). Sistem operasinya adalah Ubuntu 18. Fungsi ini mengambil parameter tunggal yang merupakan deskriptor file yang diinginkan untuk FIFO anonim.

MakeFIFO() {
    local "MakeFIFO_upper=$(ulimit -n)" 
    if [[ $# -ne 1 || ${#1} -gt ${#MakeFIFO_upper} || -n ${1%%[0-9]*} || 10#$1 -le 2
        || 10#$1 -ge MakeFIFO_upper ]] || eval ! exec "$1<> " <(:) 2>"/dev/null"; then
        echo "$FUNCNAME: $1: Could not create FIFO" >&2
        return "1"
    fi
}

Fungsi berikut diuji menggunakan GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17). Sistem operasinya adalah macOS High Sierra. Fungsi ini dimulai dengan membuat FIFO bernama dalam direktori sementara hanya diketahui proses yang membuatnya . Selanjutnya, deskriptor file dialihkan ke FIFO. Akhirnya, FIFO dibatalkan tautannya dari nama file dengan menghapus direktori sementara. Ini membuat FIFO anonim.

MakeFIFO() {
    MakeFIFO.SetStatus() {
        return "${1:-$?}"
    }
    MakeFIFO.CleanUp() {
        local "MakeFIFO_status=$?"
        rm -rf "${MakeFIFO_directory:-}"    
        unset "MakeFIFO_directory"
        MakeFIFO.SetStatus "$MakeFIFO_status" && true
        eval eval "${MakeFIFO_handler:-:}'; true'" 
    }
    local "MakeFIFO_success=false" "MakeFIFO_upper=$(ulimit -n)" "MakeFIFO_file=" 
    MakeFIFO_handler="$(trap -p EXIT)"
    MakeFIFO_handler="${MakeFIFO_handler#trap -- }"
    MakeFIFO_handler="${MakeFIFO_handler% *}"
    trap -- 'MakeFIFO.CleanUp' EXIT
    until "$MakeFIFO_success"; do
        [[ $# -eq 1 && ${#1} -le ${#MakeFIFO_upper} && -z ${1%%[0-9]*}
        && 10#$1 -gt 2 && 10#$1 -lt MakeFIFO_upper ]] || break
        MakeFIFO_directory=$(mktemp -d) 2>"/dev/null" || break
        MakeFIFO_file="$MakeFIFO_directory/pipe"
        mkfifo -m 600 $MakeFIFO_file 2>"/dev/null" || break
        ! eval ! exec "$1<> $MakeFIFO_file" 2>"/dev/null" || break
        MakeFIFO_success="true"
    done
    rm -rf "${MakeFIFO_directory:-}"
    unset  "MakeFIFO_directory"
    eval trap -- "$MakeFIFO_handler" EXIT
    unset  "MakeFIFO_handler"
    "$MakeFIFO_success" || { echo "$FUNCNAME: $1: Could not create FIFO" >&2; return "1"; }
}

Fungsi-fungsi di atas dapat digabungkan menjadi satu fungsi yang akan bekerja pada kedua sistem operasi. Di bawah ini adalah contoh dari fungsi tersebut. Di sini, upaya dilakukan untuk membuat FIFO yang benar-benar anonim. Jika tidak berhasil, maka FIFO bernama dibuat dan dikonversi ke FIFO anonim.

MakeFIFO() {
    MakeFIFO.SetStatus() {
        return "${1:-$?}"
    }
    MakeFIFO.CleanUp() {
        local "MakeFIFO_status=$?"
        rm -rf "${MakeFIFO_directory:-}"    
        unset "MakeFIFO_directory"
        MakeFIFO.SetStatus "$MakeFIFO_status" && true
        eval eval "${MakeFIFO_handler:-:}'; true'" 
    }
    local "MakeFIFO_success=false" "MakeFIFO_upper=$(ulimit -n)" "MakeFIFO_file=" 
    MakeFIFO_handler="$(trap -p EXIT)"
    MakeFIFO_handler="${MakeFIFO_handler#trap -- }"
    MakeFIFO_handler="${MakeFIFO_handler% *}"
    trap -- 'MakeFIFO.CleanUp' EXIT
    until "$MakeFIFO_success"; do
        [[ $# -eq 1 && ${#1} -le ${#MakeFIFO_upper} && -z ${1%%[0-9]*}
        && 10#$1 -gt 2 && 10#$1 -lt MakeFIFO_upper ]] || break
        if eval ! exec "$1<> " <(:) 2>"/dev/null"; then
            MakeFIFO_directory=$(mktemp -d) 2>"/dev/null" || break
            MakeFIFO_file="$MakeFIFO_directory/pipe"
            mkfifo -m 600 $MakeFIFO_file 2>"/dev/null" || break
            ! eval ! exec "$1<> $MakeFIFO_file" 2>"/dev/null" || break
        fi
        MakeFIFO_success="true"
    done
    rm -rf "${MakeFIFO_directory:-}"
    unset  "MakeFIFO_directory"
    eval trap -- "$MakeFIFO_handler" EXIT
    unset  "MakeFIFO_handler"
    "$MakeFIFO_success" || { echo "$FUNCNAME: $1: Could not create FIFO" >&2; return "1"; }
}

Berikut adalah contoh membuat FIFO anonim, kemudian menulis beberapa teks ke FIFO yang sama.

fd="6"
MakeFIFO "$fd"
echo "Now is the" >&"$fd"
echo "time for all" >&"$fd"
echo "good men" >&"$fd"

Di bawah ini adalah contoh membaca seluruh konten FIFO anonim.

echo "EOF" >&"$fd"
while read -u "$fd" message; do
    [[ $message != *EOF ]] || break
    echo "$message"
done

Ini menghasilkan output berikut.

Now is the
time for all
good men

Perintah di bawah ini menutup FIFO anonim.

eval exec "$fd>&-"

Referensi:
Membuat pipa anonim untuk digunakan
di lain waktu File di Direktori yang Dapat Ditulis secara Publik Berbahaya
Keamanan Script Shell


0

Menggunakan jawaban yang bagus dan cerah dari htamas, saya memodifikasinya sedikit untuk menggunakannya dalam satu liner, ini dia:

# create a temporary named pipe
PIPE=(`(exec 0</dev/null 1</dev/null; (( read -d \  e < /proc/self/stat ; echo $e >&2 ; exec tail -f /dev/null 2> /dev/null ) | ( read -d \  e < /proc/self/stat ; echo $e  >&2 ; exec tail -f /dev/null 2> /dev/null )) &) 2>&1 | for ((i=0; i<2; i++)); do read e; printf "$e "; done`)
# attach it to file descriptors 3 and 4
exec 3>/proc/${PIPE[0]}/fd/1 4</proc/${PIPE[1]}/fd/0
...
# kill the temporary pids
kill ${PIPE[@]}
...
# anything we write to fd 3 can be read back from fd 4
echo 'Hello world!' >&3
head -n1 <&4
...
# close the file descriptor when we are finished (optional)
exec 3>&- 4<&-

7
Saya tidak bisa tidak memperhatikan bahwa liner Anda memiliki lebih dari satu baris.
Dmitry Grigoryev
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.