[Megoldva] Párhuzamos folyamatok bash-ben

Fórumok

Tekintsük az alábbi bash kódrészletet:


cd "$tempdir"
ls -1 | (
    for ((i=0; i<cores; i++)); do
        pids[i]=0
    done
    count=0                             # DEBUG
    EXEC='pnmtopng'
    while read; do
        if [ -f "$REPLY" -a -r "$REPLY" ]; then
            while :; do
                found=0
                for ((i=0; i<cores; i++)); do
                    if [ ${pids[i]} -eq 0 ]; then
                        found=1
                        break
                    # elif [ x"`ps -p${pids[i]} -o comm=`" != x"$EXEC" ]; then
                    elif ! pgrep -x "$EXEC" | grep -q "^${pids[i]}\$"; then
                        echo -e "pids[$i]=${pids[i]}\t:\t`ps -p${pids[i]} -o comm=`,\tEXEC=$EXEC"
                        pids[i]=0
                        found=1
                        break
                    fi
                done
                if [ $found -eq 1 ]; then
                    break
                else
                    sleep 0.2
                fi
            done
            "$EXEC" "$REPLY" >"$imagesdir/${REPLY%.*}.png" & pids[i]=$!
            ((count2+=suly2))
            ((count++))         # DEBUG
            echo -ne "\r$[count1/max1+count2/max2] %\t\t"
        fi
    done
    wait
    echo -e "\r100 %\t\t"
    echo "count=$count"         # DEBUG
)

A kimenet itt látható.

Az elképzelés szerint egy filelistát pipe-olok egy subshell-be, ott inicializálok egy tömböt, amely annyi elemű, ahány magos a CPU, jelenleg ez 4. A tömbben a 0 azt jelenti, hogy ehhez nem tartozik futó process. Egy ciklusban végigmegyek a tömbön, ha találok benne 0-t, akkor kilépek. Ha nem 0 a pids[i], akkor megnézem, az adott pidhez tartozóan fut-e process, s annak neve az-e, amelyet én indítottam. Ha a név más, az csak úgy lehet, hogy időközben felszabadult ez a pid, s a kernel kiosztotta egy egészen más process-nek, ezért ellenőrzöm a nevet. Ha ez történt, bejegyzem, a tömbbe, hogy felszabadult a pid, s kilépek a ciklusból. Meg persze kiírok egy debug sort: a pids[i], a hozzá tartozó név - ez üres, ha nem került megint kiosztásra az adott pid a programomtól független folyamat által.

Ezután, ha mind a 4 pid valós, általam indított, élő programra hivatkozik, várok 200 ms-ot, majd pollingolom újra, befejezte-e valamelyik folyamat a működést. Amennyiben valamelyik pid már kihalt - found=1 -, úgy kilépek a látszólag végtelen while ciklusból egy break-kel, s indítom a megfelelő paraméterekkel a következő konverziót a háttérben, a pid-jét pedig tárolom a tömb felszabadult helyére: pids[i]=$! .

Mindösszesen ennyi lenne az egész, nem bonyolult. Ennek fényében a kimeneten látszik, hogy ahol a kettőspont utáni tabulátorokat követően csak vessző van, ott jól működik, ugyanakkor van, ahol pdf2png-t mond a ps -p${pids[i]} -o comm= kimenete. Azt tudni kell, hogy a pdf2png a saját scriptem neve, tehát lényegében a basename "$0".

További furcsaság, hogy amikor meghülyül, a pidek 4-esével, 8-asával vagy 10-esével követik egymást szabályosan, s mindig ugyanazon i indexhez tartozóan.

Vagy valamit nagyon rosszul csinálok, vagy van egy bug a bash-ben, vagy a $! néha hülyeséget ad vissza, de akkor sem lehet, hogy 10-esével növekvő pid-del úgy gondolja, az a saját shell-em.

Nem értem. Ha valakinek van jó ötlete, jelezze!

Hozzászólások

Lefuttattam a scriptedet, Ubuntu 10.10 (bash 4.1.5) alatt nem hibázott. Nem hiszem, hogy számít, de egy magom van, viszont a cores értéke 4 volt.
Én a $REPLY értékét is kiíratnám, hátha valami szemét kerül bele valahonnan.

A dolog ott eshet hasra, amikor a háttérfolyamatot indítod el. A $! által visszaadott PID legelőször a pdf2png-t futtató shell gyereke (egy shell), ami ezután exec-eli a pnmtopng binárist. Más szóval a $! PID által jelölt processznek időben változik az image-e, és a neve is. Ezzel a változással fut versenyt a pdf2png script.

Ha a $! PID-ű processz image-e még a bash bináris, amikor a pgrep-pel rákeresel, akkor azt fogja hinni a script-ed, hogy az a PID már felszabadult (semelyik processzre sem igaz, hogy a megfelelő a PID-je és pnmtopng a neve). Ezután kiíratod a PID szerint a processz aktuális nevét. A pastebin-en található kimenet 99. sorában pl. azt látjuk, hogy eddigre már az exec lezajlott (abban a rövid időben, ami a pgrep-es ellenőrzés és a kiíratás között telt el), de pl. a 126-os sornál a child processz még mindig a bash image-et futtatta.

Jobban jársz, ha megnézed a GNU parallel-t.

Én egyébként a kérdéses feladatra megírtam a megoldást shell-ben, de iszonyatosan ronda lett, és például a ^C-vel történő megszakítás esetén nem küldi helyesen tovább a szignált az összes gyereknek (és unokának). Mondom, túl vastag. Mindenesetre a fő funkciót úgy fedi le, hogy a SIGCHLD-re rátesz egy handlert, ami minden egyes signal delivery-nél végignézi az egész PID tömböt, egyesével megpingelve (kill -0) az adott PID-et, és ha már nem létezik, akkor indít helyette egyet, egy új sort benyalva az stdin-ről (ha még nem értük el az stdin végét).

Itt a script lényege (public domain):


#!/bin/bash
set -e -C


declare PROGRAM_NAME
declare NUMBER_OF_WORKERS
declare WORKER_COMMAND
declare -x WORKER_ID
declare -a WORKER_PIDS
declare NO_MORE_JOBS
declare -x JOB_ID
declare -a JOB_ARGS


log0()
{
  local MESSAGE=$1

  echo "$PROGRAM_NAME: $MESSAGE" >&2
}


log()
{
  local MESSAGE=$1

  echo "$PROGRAM_NAME: $(date '+%s %Y-%m-%d %H:%M:%S %Z'): $MESSAGE" >&2
}



child_handler()
{
  local CUR_WORKER_PID
  local JOB_COMMAND
  local JOB_ARG
  local MESSAGE

  for (( WORKER_ID=0; $WORKER_ID < $NUMBER_OF_WORKERS; ++WORKER_ID )); do
    CUR_WORKER_PID=${WORKER_PIDS[$WORKER_ID]}

    if [ -z "$CUR_WORKER_PID" ] || ! kill -n 0 $CUR_WORKER_PID 2>/dev/null
    then
      if [ $NO_MORE_JOBS -eq 0 ] && read -a JOB_ARGS; then
        JOB_COMMAND=$WORKER_COMMAND
        for JOB_ARG in "${JOB_ARGS[@]}"; do
          JOB_COMMAND=$JOB_COMMAND\ \'${JOB_ARG//\'/\'\\\'\'}\'
        done
        JOB_COMMAND=$JOB_COMMAND\ \</dev/null

        # Small window to leak a process.
        eval "$JOB_COMMAND &"
        WORKER_PIDS[$WORKER_ID]=$!

        MESSAGE="spawned worker $WORKER_ID pid $! ($JOB_COMMAND)"
        if [ ! -z "$CUR_WORKER_PID" ]; then
          MESSAGE="$MESSAGE in place of pid $CUR_WORKER_PID"
        fi
        log "$MESSAGE"

        JOB_ID=$(($JOB_ID + 1))
      else
        NO_MORE_JOBS=1
        if [ ! -z "$CUR_WORKER_PID" ]; then
          log "worker $WORKER_ID pid $CUR_WORKER_PID terminated"
          WORKER_PIDS[$WORKER_ID]=""
        fi
      fi
    fi
  done
}


PROGRAM_NAME=$(basename "$0")
if [ $# -lt 2 ]; then
  log0 "Usage: $PROGRAM_NAME NUM_WORKERS WORKER_COMMAND ARG_1 ARG_2 ..."
  log0 "Invoke WORKER_COMMAND with ARGs for each command line read from stdin."
  log0 "Run at most NUM_WORKERS instances in parallel."
  log0 "Exported environment variables for WORKER_COMMAND are:"
  log0 "  JOB_ID:    index of command line read from stdin (zero based),"
  log0 "  WORKER_ID: integer instance identifier in [0, NUM_WORKERS)."
  exit 1
fi

NUMBER_OF_WORKERS=$1
if ! [ "$NUMBER_OF_WORKERS" -gt 0 ]; then
  log0 "NUM-WORKERS must be positive"
  exit 1
fi
shift

while [ $# -gt 0 ]; do
  WORKER_COMMAND=$WORKER_COMMAND\ \'${1//\'/\'\\\'\'}\'
  shift
done

set +e -m

NO_MORE_JOBS=0
JOB_ID=0

trap child_handler SIGCHLD

true &
wait

Kiegészítés:

  • A handler futása alatt a szignál blokkolva van. Amíg fut a handler, további child processz-ek is befejeződhetnek. Ezt vagy észrevesszük már a handler-ben, vagy nem. Mindenesetre ilyenkor a signal ismét pending-gé válik, és amint véget ér a handler, azonnal kézbesítődik a szignál újra. Ilyenkor az összes lefutott gyereket kezelni kell, nem csak egyet közülük. Ezért van a handler-ben ciklus.
  • A sok escape-elés arra való, hogy a bemeneti file-ba soronként argumentumoknak egy listáját lehessen beírni. A whitespace az argumentum-elválasztó, amit backslash-sel lehet escape-elni. Egy argumentum tartalmazhat idézőjelet ill. aposztrofot is.

Köszönöm a segítséged! A magyarázat logikus, s valóban indokolja a működést, illetve annak hiányát. :) Nem tudtam, hogyan indít a shell folyamatot a háttérben. Valamiért azt gondoltam, hogy az illető, általam indított nevű lesz az új process, amelynek a PID-jét visszakapom a $!-ben. Erre a tranziens folyamatra nem gondoltam, nem tudtam róla, s közben kihullott az összes hajam. Látszik a comment-elt sorból, hogy egy gyorsabb metódussal is megpróbáltam megvizsgálni az adott process nevét, az volt az eredeti - nyilván a pgrep kimenetét grep-be pipe-olva szűrni lassabb -, de már az is hasonlóképpen többnyire működött, ám sokszor nem.

Azt ugyan nem írtam, de a hibás működés egyfelől azért zavart, mert az nagyon nincs rendjén, ha valami nem úgy működik, ahogy elképzelem, s nem értem, mi a baja, másfelől meg így 4-nél lényegesen több példányban indítja adott esetben a konverziót, amely forkbombaként tud működni.

tr [:lower:] [:upper:] <<<locsemege
LOCSEMEGE

Ezt általában úgy csináljuk, hogy megszámoljuk hogy hány (pdf2png) processz fut, és ha kevés, akkor indítunk még párat, ha elég, akkor várunk kicsit. Persze ez nem túl pontos, de általában tökéletesen megfelelő megoldás.

A ctrl-c-re általában ez is rosszul szokott reagálni, én úgy állítom le, hogy van egy file, aminek a meglétét ellenőrzi, és ha az létezik, akkor kilép minden alprocessz.

Illetve másik gépen, másik scritben a load értékét figyelem, annak függvényében indítok újabb processzt.

Nem lesz túlságosan konstruktív, de leírom: én, amikor ilyen bonyolultságú feladat kerül elém, tuti nem shell scriptben erőlködök. Ennél jóval egyszerűbb dolgokra is sokkal hamarébb előveszem a perlt.

Most értem haza, nem voltam gépközelben, olvasom a válaszokat rögvest... :)

tr [:lower:] [:upper:] <<<locsemege
LOCSEMEGE

Itt folytatom, nem akartam túl hosszú hozzászólást. :)

A scriptemnek van egy első fele, hasonlóképpen párhuzamosítani kell. Az egész annyit csinál, hogy egy pdf file-t oldalanként png képfile-okba renderel, továbbá a pdf file-ból kiszedi az abba eredetileg képként berakott objektumokat. Mivel 600 dpi esetén egy 56 oldalas, képekkel teletűzdelt doksival ~18 perc alatt végzett, úgy éreztem, jó volna kihasználni a 4 magos CPU-t, hiszen ezért van. Így a futásidőt sikerült ~5 percre leszorítani.

Amíg volt a scriptemben egy bug, elindult 56 példányban a konverzió, felfalta a soványka 2 GB RAM-omat, megevett mellé 7 GB swap-et, az egér kurzort már nem bírtam megmozdítani, az óra, amelynek másodpercenként kellett volna frissülnie, úgy 5-10 percenként frissült, hasonlóképpen a conky, illetve a top is. Mondjuk érthető. :)

tr [:lower:] [:upper:] <<<locsemege
LOCSEMEGE

Barátkozom ezzel a parallel nevű szerszámmal...

tr [:lower:] [:upper:] <<<locsemege
LOCSEMEGE

Az hogyan lehetséges, hogy pipe-olok valamit a tee-nek, s amit ír file-t, az menet közben hízik, majd a végén összemegy?

Konkrétabban a parallel stderr-re írott kimenetéből akartam yad-dal progress bar-t csinálni vázlatosan így:

seq $p | parallel --eta paraméterek 2>&1 | tee "$HOME/debug" |\
awk 'script' | yad --progress --auto-close --egyebek

A debug file működés közben 13kB+ is lesz, aztán a végén összemegy 4.4 kB-ra. Ez hogyan van? Ami egyszer átment a csövön, annak még utána lehet nyúlni? Már megint nem értek valamit...

tr [:lower:] [:upper:] <<<locsemege
LOCSEMEGE

Ez a jelenség akkor szokott előjönni, amikor felülírod a fájlt. Ha jól emlékszem a tee alapból újrakezdi a fáljt, a "-a" -val appendel, ha a parallel valahogyan többször indul el, akkor valószínűleg az utoljára elindult szál outputja (ami persze a "maradékot" kapja és így rövidebb) marad meg csak.

Vagy máshol írod felül a debug fájlt.

Vagy az lehet - ezt majd megerősíti vagy -cáfolja az, aki ért a unix programozáshoz -, hogy parallel minden szálnak saját stdout-ot (és gondolom stderr-t is) nyit. Ha egyáltalán lehet ilyet.

vagy ez: pexec (ill kis reklam :] de van debian/ubuntu alatt is, `apt-get install pexec`)

Régebben fordítgattam forrásból, most viszont törexem arra, hogy lehetőleg mindent a disztribúció eszköztárából oldjak meg. Volt idő, amikor vanilla kernelt fordítottam, meg egyéb finomságok. Biztos öregszem, de ez kevéssé vonz már.

A parallel működik, csak egyelőre nem sikeredett kinyernem a yad számára az infót, hogy progress bar-t csináljak. Ha már látom, pontosan mit is tartalmaz a file, akkor megoldom.

Van vész forgatókönyvem is. Nézem egy háttérben elindított script-ben, mennyi file jött létre, tudom, mennyire számítok, a kettő hányadosa, amit a yad-nak kell pipe-olni. Tehát mindenképp megoldom, csak a kisebb gányolás irányába mennék. :)

tr [:lower:] [:upper:] <<<locsemege
LOCSEMEGE

A legjobb az, amikor a debugban lévő hiba vezet félre. Igen, a tee -a jobb megfejtés volt. Ugye a tee csak debug célokat szolgált. Ettől eltekintve egyetlen hiba volt benne: kihagytam az awk scriptből az fflush() hívást. Itt nem elég, hogy a végén legyen eredmény, rekordonként kell, hiszen a progress bar valós idejű. Az nem mutat jól, hogy a folyamat ideje alatt 0%-on áll, majd a végén pár ms alatt felszalad 100%-ra. :)

tr [:lower:] [:upper:] <<<locsemege
LOCSEMEGE

Azért még kérdezek. :)

Hogyan lehet megtudni, hogy egy pipe-ban mi a pid-je az egyes process-eknek? Mondom, mi a probléma. Használom ezt a yad nevű jószágot, de az a sanda gyanúm, hogy bugos, szeretném megkerülni a problémát. Az alábbi szerkezetű az adott részlet:

(itt vannak dolgok subshell-ben) | yad --multi-progress

Fontos tudni, hogy a --auto-close kapcsoló csak a --progress esetén működik. A gond az, hogy hiába hal ki a subshell, szűnik meg a pipe bal oldala, a yad éli világát, s bambán néz. Én meg azt szeretném, hogy például a subshell utolsó utasításával kinyiffantanám a yad-ot valahogy ekképpen:

(itt vannak dolgok; kill $PID_YAD) | yad --multi-progress

Gondoltam használni a bash coproc dolgát. Ezzel is az a baj, hogy a yad nem hajlandó meghalni, elárvult process marad. Az alábbi kód - egy sorban írtam, mert nem volt kedvem file-ba írni - és eredménye:


coproc { yad --multi-progress --bar=alma --bar=korte; };
for ((i=0; i<=100; i+=5)); do echo -e "1:$i\n2:$[i/2]";
[ $i -eq 80 ] && { echo "COPROC_PID=$COPROC_PID"; pgrep -lx yad; pgrep -lx coproc; } >&2;
sleep 0.2; done >&${COPROC[1]};
pgrep -lx yad; pgrep -lx coproc
[1] 27652
COPROC_PID=27652
27654 yad
27654 yad

Ugye itt megint az a baj, hogy a yad pid-jét nem tudom. Hogyan lehet megtudi?

tr [:lower:] [:upper:] <<<locsemege
LOCSEMEGE

Hogyan lehet megtudni, hogy egy pipe-ban mi a pid-je az egyes process-eknek?

Egy példával szemléltetve, valahogy így:


$ yes | cat > /dev/null
$ lsof | grep -E "^(yes|cat)" | grep pipe
yes       6098     user    1w     FIFO                0,8      0t0   358030 pipe
cat       6099     user    0r     FIFO                0,8      0t0   358030 pipe
$ readlink /proc/6098/fd/1 
pipe:[358030]
$ readlink /proc/6099/fd/0
pipe:[358030]

Illetve... mi van, ha több ilyen pipe van? Mert a killal yad eddig is "megoldás" lehetett volna. Olyasmit keresek, ami pontosan azonosítja a process-t, nem vaktában akarok lövöldözni rájuk. Bár, ha a yes egy subshell-ben van, akkor az ő szülőjének pid-je megtudható, az meg utána kideríthető az lsof-fal, hogy ki van a cső másik oldalán. Ha jól gondolom. Holnap kifaragom.

tr [:lower:] [:upper:] <<<locsemege
LOCSEMEGE

Ha tudod a shell pid-jét (miért ne tudhatnád), akkor a


$ pstree -p SHELL_PID
$ pstree -p | less

közül, amelyik szimpatikusabb az segít neked nagy valószínűséggel.
Ezután a megfelelő gyerek PID-jére keresel lsof-ban.
lsof-ból megtudod a pipe azonosítóját.
Ezután arra az azonosítóra keresel lsof-ban és megvan minden amit akartál, de már a pstree-ből látszani fog minden amúgy, valsz lsof nem is kell.

Természetesen nem vizuálisan szeretném látni, hanem a scriptben végezni azzal a nyamvadt yad-dal, amely nem hajlandó kipusztulni, ha a szülője meghal. Embernél ez érthető, process-től viszont nem ezt várom. :)

Szóval lsof lesz ebből, de manualt olvasok, mert szűkíteni szeretném a kört. A greppel történő szűrés szerintem elég lassú lehet, mert opciók nélkül az lsof kimenete igen terjedelmes, hiszen mindent visszaad. Mindenesetre már látom az alagút végét, köszönöm.

tr [:lower:] [:upper:] <<<locsemege
LOCSEMEGE

+subscribe

* Én egy indián vagyok. Minden indián hazudik.

Gondoltam, ideírom a megfejtést, de épp bénázok. Parancssorban működik, a scriptben már nem. Vagy elírtam valamit, vagy nem tudom. Most debugolok...

tr [:lower:] [:upper:] <<<locsemege
LOCSEMEGE

Megvan a nyomorult! :) Szóval azon csúsztam el, hogy a $BASHPID nevű változó nagyon helyesen az aktuális shell pid-jét adja vissza. Ez viszont egy pipe esetében nem az őt futtató subshell pid-je. Mindjárt részletezem.

Tehát a feladat így nézett ki. A gui változó azt mondja meg, hogy legyen-e grafikus felület, vagy terminálon futkorásszon.


if [ $gui -eq 1 ]; then
    output='yad --title="$selfname" --width=$width --multi-progress --bar="${label[0]}" --bar="${label[1]}"'
else
    output='cat'
fi
...
(
    itt előállítjuk a progressbar vezérlését vagy a terminálra írandókat
) | eval "$output"

A gond ugye az volt, hogy a végén az a yad, amely az eval-lal értékelődik ki, nem lép ki, s valahogy a pipe másik oldaláról szeretném lelőni, amikor elérte a zárójelben lévő rész a subshell-ben a működésének végét, s így a 100%-ot jelezte a yad-nak, amit az megjelenít.

A megoldás alant:


pipepid=`cat<<'EOF'
    BEGIN {
        pipe = -1;
    }

    /pipe$/ {
        if (pipe == -1) {
            if ($2 == bashpid) {
                pipe = $8;
                FNR = 1;
            }
        } else {
            if ($8 == pipe && $1 == procname) print $2;
        }
    }
EOF`
if [ $gui -eq 1 ]; then
    output='yad --title="$selfname" --width=$width --multi-progress --bar="${label[0]}" --bar="${label[1]}"'
else
    output='cat'
fi
...
(
    itt fut a program, valamint előállítja a yad-nak küldendő %-os értékeket stdout-ra
    bashpid=$BASHPID
    kill `lsof -u "$USER" -U | awk -v bashpid=$bashpid -v procname='yad' "$pipepid"`
) | eval "$output"

Fontos momentum, hogy nem írható az awk -v bashpid=$BASHPID, mert ott éppen más az értéke. Nekünk annak a subshell-nek kell a pid-je, amelyik a zárójelen belül van. Így aztán kell a bashpid=$BASHPID, majd ezt a változót adjuk át az awk-nak.

A pipepid awk script először megkeresi az lsof kimenetéből, hogy a subshell-ünk melyik pipe-ba ír, majd kikeresi - szintén elölről -, hogy ehhez a pipe-hoz melyik procname nevű process - ez ugye a keresett yad - csatlakozik, majd ezzel a pid-del küldünk SIGTERM-et a yad-nak. Erre már, ha unott képpel ugyan, de kegyeskedik kilépni. :)

tr [:lower:] [:upper:] <<<locsemege
LOCSEMEGE

Igen. Különben van benne egy bug, szánom-bánom bűnömet. Nem vettem észre, mert működik, a hiba viszont elvi. Természetesen az awk-nak nem lehet azt mondani, hogy újra az elejéről olvassa a record-okat. Az az FNR=1 marhaság. Csak azért működik, mert a pid-ek olyan sorrendben jönnek az lsof kimenetében, hogy miután megtaláltam a pipe azonosítóját, utána jön az a process, amit keresek. Mindjárt kiötlök valami elvileg is jó megoldást. Így jár az ember, ha hamarabb kommentel, mint gondolkodik. :-/

tr [:lower:] [:upper:] <<<locsemege
LOCSEMEGE

Akkor írom a javítást. Ez már nem csak gyakorlatilag, de elvileg is jó:


pipeawk=`cat<<'EOF'
    /pipe$/ {
        if ($2 == bashpid) {
            print $8;
            exit 0;
        }
    }
EOF`
pipepidawk=`cat<<'EOF'
    /pipe$/ {
        if ($8 == pipe && $1 == procname) print $2;
    }
EOF`
if [ $gui -eq 1 ]; then
    output='yad --title="$selfname" --width=$width --multi-progress --bar="${label[0]}" --bar="${label[1]}"'
else
    output='cat'
fi
...
(
    itt végzi, amit kell, s előállítja a progressbar vezérlési infót
    if [ $gui -eq 1 ]; then
        bashpid=$BASHPID
        lsoftext="`lsof -u "$USER" -U`"
        pipe=`awk -v bashpid=$bashpid "$pipeawk" <<<"$lsoftext"`
        kill `awk -v pipe=$pipe -v procname='yad' "$pipepidawk" <<<"$lsoftext"`
    fi
) | eval "$output"

A javítást azért írtam, mert fentebb ostobaság volt, ne az maradjon meg.

tr [:lower:] [:upper:] <<<locsemege
LOCSEMEGE