Kumpulkan kode keluar dari proses latar belakang paralel (subkulit)

18

Katakanlah kita memiliki skrip bash seperti:

echo "x" &
echo "y" &
echo "z" &
.....
echo "Z" &
wait

apakah ada cara untuk mengumpulkan kode keluar dari subkulit / proses sub? Mencari cara untuk melakukan ini dan tidak dapat menemukan apa pun. Saya perlu menjalankan subshell ini secara paralel, jika tidak ya ini akan lebih mudah.

Saya mencari solusi generik (Saya memiliki sejumlah proses sub yang tidak diketahui / dinamis untuk dijalankan secara paralel).

Alexander Mills
sumber
1
Saya akan menyarankan Anda mencari tahu apa yang Anda inginkan dan kemudian mengajukan pertanyaan baru, mencoba menjelaskan perilaku yang Anda cari (mungkin dengan kodesemu atau contoh yang lebih besar).
Michael Homer
3
Saya benar-benar berpikir pertanyaannya bagus sekarang - saya memiliki sejumlah proses yang dinamis Saya perlu mengumpulkan semua kode keluar. Itu saja.
Alexander Mills

Jawaban:

6

Jawaban oleh Alexander Mills yang menggunakan handleJobs memberi saya titik awal yang bagus, tetapi juga memberi saya kesalahan ini

peringatan: run_pending_traps: nilai buruk di trap_list [17]: 0x461010

Yang mungkin merupakan masalah kondisi ras bash

Sebaliknya saya hanya menyimpan pid dari setiap anak dan menunggu dan mendapatkan kode keluar untuk setiap anak secara khusus. Saya menemukan pembersih ini dalam hal subproses pemijahan subproses dalam fungsi dan menghindari risiko menunggu proses induk di mana saya dimaksudkan untuk menunggu anak. Lebih jelas apa yang terjadi karena tidak menggunakan jebakan.

#!/usr/bin/env bash

# it seems it does not work well if using echo for function return value, and calling inside $() (is a subprocess spawned?) 
function wait_and_get_exit_codes() {
    children=("$@")
    EXIT_CODE=0
    for job in "${children[@]}"; do
       echo "PID => ${job}"
       CODE=0;
       wait ${job} || CODE=$?
       if [[ "${CODE}" != "0" ]]; then
           echo "At least one test failed with exit code => ${CODE}" ;
           EXIT_CODE=1;
       fi
   done
}

DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
    )

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

children_pids=()
for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    children_pids+=("$!")
    echo "$i ith command has been issued as a background job"
done
# wait; # wait for all subshells to finish - its still valid to wait for all jobs to finish, before processing any exit-codes if we wanted to
#EXIT_CODE=0;  # exit code of overall script
wait_and_get_exit_codes "${children_pids[@]}"

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end
Arberg
sumber
keren, saya pikir for job in "${childen[@]}"; doseharusnya begitu for job in "${1}"; do, untuk kejelasan
Alexander Mills
satu-satunya masalah yang saya miliki dengan skrip ini, adalah jika children_pids+=("$!")benar-benar menangkap pid yang diinginkan untuk sub shell.
Alexander Mills
1
Saya menguji dengan "$ {1}" dan tidak berhasil. Saya melewatkan array ke fungsi, dan tampaknya itu membutuhkan perhatian khusus di bash. $! adalah pid dari pekerjaan yang terakhir muncul, lihat tldp.org/LDP/abs/html/internalvariables.html Tampaknya berfungsi dengan benar dalam pengujian saya, dan saya sekarang menggunakan dalam skrip cache_dirs dalam unRAID, dan tampaknya melakukan pekerjaannya. Saya menggunakan bash 4.4.12.
arberg
ya bagus sepertinya Anda benar
Alexander Mills
20

Gunakan waitdengan PID, yang akan:

Tunggu hingga proses anak yang ditentukan oleh setiap proses ID pid atau spesifikasi pekerjaan jobspec keluar dan kembalikan status keluar dari perintah terakhir yang menunggu.

Anda harus menyimpan PID dari setiap proses saat berjalan:

echo "x" & X=$!
echo "y" & Y=$!
echo "z" & Z=$!

Anda juga dapat mengaktifkan kontrol pekerjaan dalam skrip dengan set -mdan menggunakan %njobspec, tetapi Anda hampir pasti tidak mau - kontrol pekerjaan memiliki banyak efek samping lainnya .

waitakan mengembalikan kode yang sama saat proses selesai. Anda dapat menggunakan wait $Xkapan saja (masuk akal) nanti untuk mengakses kode akhir $?atau hanya menggunakannya sebagai benar / salah:

echo "x" & X=$!
echo "y" & Y=$!
...
wait $X
echo "job X returned $?"

wait akan berhenti sampai perintah selesai jika belum.

Jika Anda ingin menghindari kemacetan seperti itu, Anda dapat mengatur atrap padaSIGCHLD , menghitung jumlah terminasi, dan menangani semua waits sekaligus ketika mereka sudah semua selesai. Anda mungkin bisa lolos menggunakan waitsendiri hampir sepanjang waktu.

Michael Homer
sumber
1
ughh, maaf, saya perlu menjalankan subshell ini secara paralel, saya akan menentukan bahwa dalam pertanyaan ...
Alexander Mills
tidak masalah, mungkin ini berfungsi dengan pengaturan saya ... di mana perintah tunggu mulai berperan dalam kode Anda? Saya tidak mengikuti
Alexander Mills
1
@AlexanderMills Mereka yang berjalan secara paralel. Jika Anda memiliki nomor variabel, gunakan array. (seperti misalnya di sini yang mungkin merupakan duplikat).
Michael Homer
ya terima kasih saya akan memeriksanya, jika perintah tunggu berkaitan dengan jawaban Anda, silakan tambahkan
Alexander Mills
Anda menjalankan wait $Xpada titik mana pun (masuk akal) nanti.
Michael Homer
5

Jika Anda memiliki cara yang baik untuk mengidentifikasi perintah, Anda dapat mencetak kode keluarnya ke file tmp dan kemudian mengakses file tertentu yang Anda minati:

#!/bin/bash

for i in `seq 1 5`; do
    ( sleep $i ; echo $? > /tmp/cmd__${i} ) &
done

wait

for i in `seq 1 5`; do # or even /tmp/cmd__*
    echo "process $i:"
    cat /tmp/cmd__${i}
done

Jangan lupa untuk menghapus file tmp.

Rolf
sumber
4

Gunakan a compound command- masukkan pernyataan dalam tanda kurung:

( echo "x" ; echo X: $? ) &
( true ; echo TRUE: $? ) &
( false ; echo FALSE: $? ) &

akan memberikan output

x
X: 0
TRUE: 0
FALSE: 1

Cara yang sangat berbeda untuk menjalankan beberapa perintah secara paralel adalah dengan menggunakan GNU Parallel . Buat daftar perintah untuk dijalankan dan letakkan di file list:

cat > list
sleep 2 ; exit 7
sleep 3 ; exit 55
^D

Jalankan semua perintah secara paralel dan kumpulkan kode keluar di file job.log:

cat list | parallel -j0 --joblog job.log
cat job.log

dan hasilnya adalah:

Seq     Host    Starttime       JobRuntime      Send    Receive Exitval Signal  Command
1       :       1486892487.325       1.976      0       0       7       0       sleep 2 ; exit 7
2       :       1486892487.326       3.003      0       0       55      0       sleep 3 ; exit 55
hschou
sumber
ok terima kasih, adakah cara untuk menghasilkan ini? Saya tidak hanya memiliki 3 sub proses, saya memiliki Z sub proses.
Alexander Mills
Saya memperbarui pertanyaan awal untuk mencerminkan bahwa saya mencari solusi generik, terima kasih
Alexander Mills
Salah satu cara untuk menghasilkan itu mungkin dengan menggunakan konstruksi perulangan?
Alexander Mills
Looping? Apakah Anda memiliki daftar perintah yang tetap atau apakah itu dikendalikan oleh pengguna? Saya tidak yakin saya mengerti apa yang Anda coba lakukan tetapi mungkin PIPESTATUSadalah sesuatu yang harus Anda periksa. Ini seq 10 | gzip -c > seq.gz ; echo ${PIPESTATUS[@]}kembali 0 0(kode keluar dari perintah pertama dan terakhir).
hschou
Ya pada dasarnya dikendalikan oleh pengguna
Alexander Mills
2

ini skrip generik yang Anda cari. Satu-satunya downside adalah perintah Anda dalam tanda kutip yang berarti penyorotan sintaks melalui IDE Anda tidak akan benar-benar berfungsi. Kalau tidak, saya sudah mencoba beberapa jawaban lain dan ini adalah yang terbaik. Jawaban ini menggabungkan ide menggunakan yang wait <pid>diberikan oleh @Michael tetapi melangkah lebih jauh dengan menggunakan trapperintah yang tampaknya bekerja paling baik.

#!/usr/bin/env bash

set -m # allow for job control
EXIT_CODE=0;  # exit code of overall script

function handleJobs() {
     for job in `jobs -p`; do
         echo "PID => ${job}"
         CODE=0;
         wait ${job} || CODE=$?
         if [[ "${CODE}" != "0" ]]; then
         echo "At least one test failed with exit code => ${CODE}" ;
         EXIT_CODE=1;
         fi
     done
}

trap 'handleJobs' CHLD  # trap command is the key part
DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
)

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    echo "$i ith command has been issued as a background job"
done

wait; # wait for all subshells to finish

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end

terima kasih kepada @michael homer karena membawa saya ke jalur yang benar, tetapi menggunakan trapperintah adalah pendekatan terbaik AFAICT.

Alexander Mills
sumber
1
Anda juga dapat menggunakan perangkap SIGCHLD untuk memproses anak-anak ketika mereka keluar, seperti mencetak status pada waktu itu. Atau memperbarui penghitung kemajuan: mendeklarasikan fungsi lalu menggunakan "trap function_name CHLD" meskipun itu mungkin juga memerlukan opsi untuk dihidupkan dalam shell non-interaktif, seperti kemungkinan "set -m"
Chunko
1
Juga "tunggu -n" akan menunggu anak mana saja dan kemudian mengembalikan status keluar anak itu dalam $? variabel. Jadi, Anda dapat mencetak kemajuan saat masing-masing keluar. Namun perhatikan bahwa kecuali Anda menggunakan perangkap CHLD, Anda mungkin kehilangan beberapa anak keluar dengan cara itu.
Chunko
@ Chunko terima kasih! itu info yang bagus, bisakah Anda memperbarui jawaban dengan sesuatu yang menurut Anda terbaik?
Alexander Mills
terima kasih @ Chunko, jebakan bekerja lebih baik, Anda benar. Dengan menunggu <pid>, saya mendapat kesalahan.
Alexander Mills
Bisakah Anda menjelaskan bagaimana dan mengapa Anda percaya versi dengan jebakan lebih baik daripada yang tanpa jebakan? (Saya percaya itu tidak lebih baik, dan karena itu lebih buruk, karena lebih kompleks tanpa manfaat.)
Scott
1

Variasi lain dari jawaban @rolf:

Cara lain untuk menyimpan status keluar akan menjadi sesuatu seperti

mkdir /tmp/status_dir

dan kemudian masing-masing memiliki skrip

script_name="${0##*/}"  ## strip path from script name
tmpfile="/tmp/status_dir/${script_name}.$$"
do something
rc=$?
echo "$rc" > "$tmpfile"

Ini memberi Anda nama unik untuk setiap file status termasuk nama skrip yang membuatnya dan id prosesnya (jika lebih dari satu contoh skrip yang sama sedang berjalan) yang dapat Anda simpan untuk referensi nanti dan menempatkan semuanya di dalam tempat yang sama sehingga Anda bisa menghapus seluruh subdirektori setelah selesai.

Anda bahkan dapat menyimpan lebih dari satu status dari setiap skrip dengan melakukan sesuatu seperti

tmpfile="$(/bin/mktemp -q "/tmp/status_dir/${script_name}.$$.XXXXXX")"

yang membuat file seperti sebelumnya, tetapi menambahkan string acak yang unik untuk itu.

Atau, Anda bisa menambahkan lebih banyak informasi status ke file yang sama.

Joe
sumber
1

script3akan dieksekusi hanya jika script1dan script2berhasil dan script1dan script2akan dieksekusi secara paralel:

./script1 &
process1=$!

./script2 &
process2=$!

wait $process1
rc1=$?

wait $process2
rc2=$?

if [[ $rc1 -eq 0 ]] && [[ $rc2 -eq 0  ]];then
./script3
fi
venkata
sumber
AFAICT, ini tidak lebih dari pengulangan jawaban Michael Homer .
Scott