Bir arka plan işleminin çıkış kodunu alın


131

Sonsuza kadar süren ana bourne kabuk betiğimden çağrılan bir CMD komutum var.

Komut dosyasını aşağıdaki gibi değiştirmek istiyorum:

  1. CMD komutunu arka plan işlemi olarak paralel olarak çalıştırın ( CMD &).
  2. Ana komut dosyasında, birkaç saniyede bir oluşturulan komutu izlemek için bir döngü oluşturun. Döngü ayrıca betiğin ilerleyişini gösteren bazı mesajları standart çıktıya yansıtır.
  3. Oluşturulan komut sona erdiğinde döngüden çıkın.
  4. Oluşturulan sürecin çıkış kodunu yakalayın ve bildirin.

Birisi bana bunu başarmam için ipuçları verebilir mi?


1
...ve kazanan?
TrueY

1
@TrueY .. bob soruyu sorduğu günden beri giriş yapmadı. Bilmemiz pek olası değil!
ghoti

Yanıtlar:


129

1: Bash'de, $!yürütülen son arka plan işleminin PID'sini tutar. Bu size hangi süreci izleyeceğinizi söyleyecektir.

4: wait <n>PID ile işlem <n>tamamlanana kadar bekler (işlem tamamlanana kadar bloke olur, bu nedenle işlemin tamamlandığından emin olana kadar bunu aramak istemeyebilirsiniz) ve ardından tamamlanan işlemin çıkış kodunu döndürür.

2, 3: psveya ps | grep " $! "işlemin hala devam edip etmediğini size söyleyebilir. Çıktıyı nasıl anlayacağınız ve bitirmeye ne kadar yakın olduğuna karar vereceğiniz size bağlıdır. ( ps | grepaptalca bir kanıt değildir. Vaktiniz varsa, sürecin hala çalışıp çalışmadığını anlamanın daha sağlam bir yolunu bulabilirsiniz).

İşte iskelet bir komut dosyası:

# simulate a long process that will have an identifiable exit code
(sleep 15 ; /bin/false) &
my_pid=$!

while   ps | grep " $my_pid "     # might also need  | grep -v grep  here
do
    echo $my_pid is still in the ps output. Must still be running.
    sleep 3
done

echo Oh, it looks like the process is done.
wait $my_pid
# The variable $? always holds the exit code of the last command to finish.
# Here it holds the exit code of $my_pid, since wait exits with that code. 
my_status=$?
echo The exit status of the process was $my_status

15
ps -p $my_pid -o pid=hiçbirine grepgerek yoktur.
sonraki duyuruya kadar duraklatıldı.

54
kill -0 $!bir sürecin hala çalışıp çalışmadığını anlamanın daha iyi bir yoludur. Aslında herhangi bir sinyal göndermez, yalnızca harici işlemler yerine yerleşik bir kabuk kullanarak sürecin canlı olup olmadığını kontrol eder. As man 2 kill"Eğer diyor sig , 0'dır o zaman hiçbir sinyal gönderilir, bunun yerine hata denetimi gerçekleştirilir; bu işlem kimliği ya da süreç grup kimliğinin varlığını denetlemek için kullanılabilir."
ephemient

14
@ephemient kill -0, çalışan bir işleme sinyal gönderme izniniz yoksa sıfırdan farklı bir değer döndürür. Maalesef 1hem bu durumda hem de sürecin olmadığı durumda geri dönüyor . Bu, sürece sahip olmadığınız sürece yararlı hale getirir - bu, oluşturduğunuz süreçler için sudo, buna benzer bir araç dahilse veya setuid ise (ve muhtemelen ayrıcalıkları bıraksa) bile geçerli olabilir.
Craig Ringer

13
waitdeğişkendeki çıkış kodunu döndürmez $?. Yalnızca çıkış kodunu döndürür ve $?en son ön plan programının çıkış kodudur.
MindlessRanger

7
'A oy veren birçok kişi için kill -0. Burada, CraigRinger'ın yorumunun yasal olduğunu gösteren, SO'dan hakemli bir referans var: kill -0çalışan işlemler için sıfırdan farklı bir değer döndürür ... ancak ps -pçalışan herhangi bir işlem için her zaman 0 döndürür .
Trevor Boyd Smith

58

Benzer bir ihtiyacım olduğunda bunu böyle çözdüm:

# Some function that takes a long time to process
longprocess() {
        # Sleep up to 14 seconds
        sleep $((RANDOM % 15))
        # Randomly exit with 0 or 1
        exit $((RANDOM % 2))
}

pids=""
# Run five concurrent processes
for i in {1..5}; do
        ( longprocess ) &
        # store PID of process
        pids+=" $!"
done

# Wait for all processes to finish, will take max 14s
# as it waits in order of launch, not order of finishing
for p in $pids; do
        if wait $p; then
                echo "Process $p success"
        else
                echo "Process $p fail"
        fi
done

Bu yaklaşımı beğendim.
Kemin Zhou

Teşekkürler! Bu bana en basit yaklaşım gibi görünüyor.
Luke Davis

4
Bu çözüm 2 numaralı gereksinimi karşılamıyor: arka plan işlemi başına bir izleme döngüsü. waits komut dosyasının (her) işlemin sonuna kadar beklemesine neden olur.
DKroot

Basit ve güzel bir yaklaşım .. oldukça ara ara bu çözümü arıyorlar ..
Santosh Kumar Arjunan

Bu çalışmıyor .. veya istediğiniz şeyi yapmıyor: arka planda çalışan işlemlerin çıkış durumlarını kontrol etmiyor mu?
conny

10

Arka planlı bir alt sürecin pid'i $! . Tüm alt süreçlerin pid'lerini bir dizide saklayabilirsiniz, örneğin PIDS [] .

wait [-n] [jobspec or pid …]

Her işlem kimliği pid veya iş belirtimi iş belirtimi tarafından belirtilen alt işlemin çıkmasını bekleyin ve beklenen son komutun çıkış durumuna dönün. Bir iş spesifikasyonu verilmişse, işteki tüm süreçler beklenir. Hiçbir bağımsız değişken belirtilmezse, şu anda etkin olan tüm alt süreçler beklenir ve dönüş durumu sıfırdır. -N seçeneği sağlanırsa, herhangi bir işin sona ermesini bekler ve çıkış durumuna döner. Ne jobspec ne de pid kabuğun aktif bir çocuk sürecini belirtmiyorsa, dönüş durumu 127'dir.

Wait komutunu kullanın , tüm alt işlemlerin bitmesini bekleyebilirsiniz, bu arada her çocuk işlemin çıkış durumunu $? ve durumu STATUS [] içine kaydedin . Ardından duruma göre bir şeyler yapabilirsiniz.

Aşağıdaki 2 çözümü denedim ve iyi çalışıyorlar. solution01 ederken, daha özlü solution02 karmaşık biraz.

solution01

#!/bin/bash

# start 3 child processes concurrently, and store each pid into array PIDS[].
process=(a.sh b.sh c.sh)
for app in ${process[@]}; do
  ./${app} &
  PIDS+=($!)
done

# wait for all processes to finish, and store each process's exit code into array STATUS[].
for pid in ${PIDS[@]}; do
  echo "pid=${pid}"
  wait ${pid}
  STATUS+=($?)
done

# after all processed finish, check their exit codes in STATUS[].
i=0
for st in ${STATUS[@]}; do
  if [[ ${st} -ne 0 ]]; then
    echo "$i failed"
  else
    echo "$i finish"
  fi
  ((i+=1))
done

solution02

#!/bin/bash

# start 3 child processes concurrently, and store each pid into array PIDS[].
i=0
process=(a.sh b.sh c.sh)
for app in ${process[@]}; do
  ./${app} &
  pid=$!
  PIDS[$i]=${pid}
  ((i+=1))
done

# wait for all processes to finish, and store each process's exit code into array STATUS[].
i=0
for pid in ${PIDS[@]}; do
  echo "pid=${pid}"
  wait ${pid}
  STATUS[$i]=$?
  ((i+=1))
done

# after all processed finish, check their exit codes in STATUS[].
i=0
for st in ${STATUS[@]}; do
  if [[ ${st} -ne 0 ]]; then
    echo "$i failed"
  else
    echo "$i finish"
  fi
  ((i+=1))
done

İyi çalıştığını denedim ve kanıtladım. Açıklamamı kodlu olarak okuyabilirsiniz.
Terry

Lütfen aşağıdaki bilgileri bulacağınız " İyi bir cevabı nasıl yazarım? " Bölümünü okuyun: ... cevabınızda herhangi bir sınırlama, varsayım veya basitleştirmeden bahsetmeye çalışın. Kısalık kabul edilebilir, ancak daha kapsamlı açıklamalar daha iyidir. Bu nedenle yanıtınız kabul edilebilir, ancak sorunu ve çözümünüzü ayrıntılarıyla açıklayabilirseniz, oy alma şansınız çok daha yüksektir. :-)
Noel Widmer

1
pid=$!; PIDS[$i]=${pid}; ((i+=1))PIDS+=($!)indeksleme için ayrı bir değişken veya pid'in kendisi kullanılmasına gerek kalmadan diziye eklenecek şekilde daha basit bir şekilde yazılabilir . Aynı şey STATUSdizi için de geçerlidir .
codeforester

1
@codeforester, öneriniz için teşekkür ederim, başlangıç ​​kodumu solution01 olarak değiştirdim, daha kısa görünüyor.
Terry

Aynı şey, bir diziye bir şeyler eklediğiniz diğer yerler için de geçerlidir.
codeforester

8

Gördüğüm gibi, hemen hemen tüm yanıtlar ps, arka plan işleminin durumunu sorgulamak için (çoğunlukla ) harici yardımcı programları kullanıyor . SIGCHLD sinyalini yakalayan daha tek bir çözüm var. Sinyal işleyicide hangi çocuk işlemin durdurulduğu kontrol edilmelidir. Yerleşik kill -0 <PID>(evrensel) veya /proc/<PID>dizinin varlığını kontrol ederek (Linux'a özgü) veya jobsyerleşik (özel. jobs -layrıca pid'i rapor eder. Bu durumda çıktının 3. alanı Durdurulabilir | Çalışıyor | Bitti | Çıkış olabilir. ).

İşte benim örneğim.

Başlatılan sürece denir loop.sh. -xArgüman olarak bir sayı kabul eder . İçin -xbu num * 5 saniye bekler bir numara için çıkış kodu 1. çıkılıyor olduğunu. Her 5 saniyede bir PID'sini yazdırır.

Başlatıcı işleminin adı launch.sh:

#!/bin/bash

handle_chld() {
    local tmp=()
    for((i=0;i<${#pids[@]};++i)); do
        if [ ! -d /proc/${pids[i]} ]; then
            wait ${pids[i]}
            echo "Stopped ${pids[i]}; exit code: $?"
        else tmp+=(${pids[i]})
        fi
    done
    pids=(${tmp[@]})
}

set -o monitor
trap "handle_chld" CHLD

# Start background processes
./loop.sh 3 &
pids+=($!)
./loop.sh 2 &
pids+=($!)
./loop.sh -x &
pids+=($!)

# Wait until all background processes are stopped
while [ ${#pids[@]} -gt 0 ]; do echo "WAITING FOR: ${pids[@]}"; sleep 2; done
echo STOPPED

Daha fazla açıklama için bakınız: Bash betiğinden bir işlem başlatılamadı


1
Bash'den bahsettiğimiz için, for döngüsü şu şekilde yazılabilirdi: for i in ${!pids[@]};parametre genişletme kullanarak.
PlasmaBinturong

8
#/bin/bash

#pgm to monitor
tail -f /var/log/messages >> /tmp/log&
# background cmd pid
pid=$!
# loop to monitor running background cmd
while :
do
    ps ax | grep $pid | grep -v grep
    ret=$?
    if test "$ret" != "0"
    then
        echo "Monitored pid ended"
        break
    fi
    sleep 5

done

wait $pid
echo $?

2
İşte önlemek için bir püf noktası grep -v. Aramayı satırın başıyla sınırlayabilirsiniz: grep '^'$pidArtı ps p $pid -o pid=, yine de yapabilirsiniz . Ayrıca, tail -fonu öldürene kadar bitmeyecek, bu yüzden bunun demo yapmanın çok iyi bir yolu olduğunu düşünmüyorum (en azından bunu belirtmeden). psKomutunuzun çıktısını yeniden yönlendirmek isteyebilirsiniz, /dev/nullaksi takdirde her yinelemede ekrana gider. Kişisel exitnedenler waitAtlanan olmak - muhtemelen olmalıdır break. Ama while/ psve waitgereksiz değil mi?
sonraki duyuruya kadar duraklatıldı.

5
Neden herkes unutuyor kill -0 $pid? Aslında herhangi bir sinyal göndermez, yalnızca harici işlemler yerine yerleşik bir kabuk kullanarak sürecin canlı olup olmadığını kontrol eder.
ephemient

3
Çünkü sadece sahip olduğunuz bir işlemi öldürebilirsiniz:bash: kill: (1) - Operation not permitted
errant.info

2
Döngü gereksizdir. Sadece bekle. Daha az kod => daha az uç durum.
Brais Gabin

@Brais Gabin İzleme döngüsü sorunun 2. gerekliliğidir
DKroot

5

Yaklaşımınızı biraz değiştirirdim. Komutun hala canlı olup olmadığını birkaç saniyede bir kontrol etmek ve bir mesaj bildirmek yerine, birkaç saniyede bir komutun hala çalıştığını bildiren ve ardından komut bittiğinde bu işlemi sonlandıran başka bir işleme sahip olun. Örneğin:

#! / Bin / sh

cmd () {uyku 5; çıkış 24; }

cmd & # Uzun süren süreci çalıştır
pid = $! # Pid'i kaydedin

# Sürekli olarak komutun çalışmakta olduğunu bildiren bir işlem oluşturun
echo "$ (tarih): $ pid hala çalışıyor"; 1 uyu; bitti &
echoer = $!

# İşlem bittiğinde muhabiri öldürmek için bir tuzak kurun
tuzak 'öldür $ echoer' 0

# İşlemin bitmesini bekleyin
$ pid beklerseniz; sonra
    echo "cmd başarılı"
Başka
    echo "cmd BAŞARISIZ !! ($ döndürüldü?)"
fi

harika şablon, paylaştığınız için teşekkürler! İnanıyorum ki tuzak yerine, biz de yapabiliriz while kill -0 $pid 2> /dev/null; do X; done, umarım gelecekte bu mesajı okuyan bir başkası için yararlıdır;)
punkbit

3

Ekibimiz, 25 dakikalık hareketsizlikten sonra zaman aşımına uğrayan, SSH tarafından yürütülen uzaktan bir komut dosyasıyla aynı ihtiyacı duyuyordu. Burada, izleme döngüsünün arka plan işlemini her saniye kontrol ettiği, ancak hareketsizlik zaman aşımını bastırmak için yalnızca 10 dakikada bir yazdırdığı bir çözüm var.

long_running.sh & 
pid=$!

# Wait on a background job completion. Query status every 10 minutes.
declare -i elapsed=0
# `ps -p ${pid}` works on macOS and CentOS. On both OSes `ps ${pid}` works as well.
while ps -p ${pid} >/dev/null; do
  sleep 1
  if ((++elapsed % 600 == 0)); then
    echo "Waiting for the completion of the main script. $((elapsed / 60))m and counting ..."
  fi
done

# Return the exit code of the terminated background process. This works in Bash 4.4 despite what Bash docs say:
# "If neither jobspec nor pid specifies an active child process of the shell, the return status is 127."
wait ${pid}

2

Yukarıdaki çözümlere benzer basit bir örnek. Bu, herhangi bir işlem çıktısının izlenmesini gerektirmez. Sonraki örnek, çıktıyı takip etmek için tail'i kullanır.

$ echo '#!/bin/bash' > tmp.sh
$ echo 'sleep 30; exit 5' >> tmp.sh
$ chmod +x tmp.sh
$ ./tmp.sh &
[1] 7454
$ pid=$!
$ wait $pid
[1]+  Exit 5                  ./tmp.sh
$ echo $?
5

İşlem çıktısını takip etmek ve işlem tamamlandığında çıkmak için tail'i kullanın.

$ echo '#!/bin/bash' > tmp.sh
$ echo 'i=0; while let "$i < 10"; do sleep 5; echo "$i"; let i=$i+1; done; exit 5;' >> tmp.sh
$ chmod +x tmp.sh
$ ./tmp.sh
0
1
2
^C
$ ./tmp.sh > /tmp/tmp.log 2>&1 &
[1] 7673
$ pid=$!
$ tail -f --pid $pid /tmp/tmp.log
0
1
2
3
4
5
6
7
8
9
[1]+  Exit 5                  ./tmp.sh > /tmp/tmp.log 2>&1
$ wait $pid
$ echo $?
5

1

Diğer bir çözüm ise işlemleri proc dosya sistemi aracılığıyla izlemektir (ps / grep combo'dan daha güvenli); Bir işlemi başlattığınızda, / proc / $ pid içinde karşılık gelen bir klasör vardır, bu nedenle çözüm

#!/bin/bash
....
doSomething &
local pid=$!
while [ -d /proc/$pid ]; do # While directory exists, the process is running
    doSomethingElse
    ....
else # when directory is removed from /proc, process has ended
    wait $pid
    local exit_status=$?
done
....

Artık $ exit_status değişkenini istediğiniz gibi kullanabilirsiniz.


Bash'de çalışmıyor mu? Syntax error: "else" unexpected (expecting "done")
benjaoming

1

Bu yöntemle, komut dosyanızın arka plan işlemini beklemesi gerekmez, yalnızca çıkış durumu için geçici bir dosyayı izlemeniz gerekir.

FUNCmyCmd() { sleep 3;return 6; };

export retFile=$(mktemp); 
FUNCexecAndWait() { FUNCmyCmd;echo $? >$retFile; }; 
FUNCexecAndWait&

artık, siz sadece retFile içeriğini izlemeye devam ederken betiğiniz başka her şeyi yapabilir (çıkış zamanı gibi istediğiniz diğer bilgileri de içerebilir).

Not: btw, bash ile düşünmeyi kodladım


0

Bu, sorunuzun ötesine geçebilir, ancak işlemlerin çalıştığı sürenin uzunluğu konusunda endişeleriniz varsa, belirli bir süre sonra arka planda çalışan işlemlerin durumunu kontrol etmek ilginizi çekebilir. Hala hangi çocuk PID'lerin çalıştığını kontrol etmek yeterince kolay pgrep -P $$, ancak zaten süresi dolmuş olan PID'lerin çıkış durumunu kontrol etmek için aşağıdaki çözümü buldum:

cmd1() { sleep 5; exit 24; }
cmd2() { sleep 10; exit 0; }

pids=()
cmd1 & pids+=("$!")
cmd2 & pids+=("$!")

lasttimeout=0
for timeout in 2 7 11; do
  echo -n "interval-$timeout: "
  sleep $((timeout-lasttimeout))

  # you can only wait on a pid once
  remainingpids=()
  for pid in ${pids[*]}; do
     if ! ps -p $pid >/dev/null ; then
        wait $pid
        echo -n "pid-$pid:exited($?); "
     else
        echo -n "pid-$pid:running; "
        remainingpids+=("$pid")
     fi
  done
  pids=( ${remainingpids[*]} )

  lasttimeout=$timeout
  echo
done

hangi çıktılar:

interval-2: pid-28083:running; pid-28084:running; 
interval-7: pid-28083:exited(24); pid-28084:running; 
interval-11: pid-28084:exited(0); 

Not: İsterseniz $pidsişleri basitleştirmek için dizi yerine bir dize değişkenine geçebilirsiniz .


0

Çözümüm, durumu bir izleme döngüsüne geçirmek için anonim bir kanal kullanmaktı. Durum değişimi için kullanılan geçici dosyalar olmadığından temizlenecek hiçbir şey yoktur. Arka plandaki işlerin sayısından emin değilseniz, mola koşulu olabilir [ -z "$(jobs -p)" ].

#!/bin/bash

exec 3<> <(:)

{ sleep 15 ; echo "sleep/exit $?" >&3 ; } &

while read -u 3 -t 1 -r STAT CODE || STAT="timeout" ; do
    echo "stat: ${STAT}; code: ${CODE}"
    if [ "${STAT}" = "sleep/exit" ] ; then
        break
    fi
done
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.