[ Megoldva ] Flock és fork pontos működése?

Készítettem egy scriptet, ami az aktuális mappában létrehoz egy lock fájlt, és egy -TERM szignálig nyitva is tartja, majd lezárja és letörli. Az elindított script megpróbálja létrehozni a lock fájl. Ha sikerül neki, kiírja a PID értékét, amit a kill parancsnak meg kell adni a zárolás feloldásához, és visszatér 0 értékkel. Ha TIMEOUT másodpercig nem sikerül, akkor visszatér 1 értékkel.

Parancssorból futtatva jól működik, de egy másik bash scriptből hívva a scipt futását megállítja. Pontosabban, ha úgy hívom meg, hogy a visszaadott PID értékét elteszem egy változóba, akkor fagy csak be. Azaz V=$(script) vagy V=`script` soroknál. A lock fájl ugyan  létrejön, de a script futása megáll legelső futtatásra is.

#!/bin/bash
TIMEOUT=3
LOCKFILE="./file.lock"

function wait_for_signal() {
    trap "rm $LOCKFILE" SIGTERM
    until false ; do
        sleep 1
    done
}

touch $LOCKFILE
exec {FD}<>$LOCKFILE
if ! flock -x -w $TIMEOUT $FD; then
    exit 1
else # Lock acquired
    wait_for_signal &
    echo $!
    exit 0
fi

Megírtam ez a scriptet perl-ben is, a perl saját lock-jával és forkjával, és pont ugyanígy működik.

Ezek szerint nem értem pontosan a lock és/vagy a fork működését, vagy valamit nem tudok.

Tudtok segíteni benne, mi a hiba, mitől fagy be egy scriptbe ágyazva a programom, és hogyan tudnám elérni, hogy scriptből hívva ugyanúgy jól működjön, mint parancssorból?

( Kérem az AI-copy válaszok mellőzését, azon már túl vagyok. )

Hozzászólások

Annyit még megpróbáltam, hogy C-ben is elkészítettem ugyanezt a kódot ... és abban is ugyanígy viselkedik.

Szerkesztve: 2024. 09. 27., p – 14:41

Ez az FD ez micsoda benne, hol van definiálva? Valahogy nem értem. Ilyen V=$(script) sort se látok benne.

Amire gondolni tudok, hogy az if feltétel mindig igaz, és nem hajtódik végre az else ág, vagy az a gond, hogy a lockfile-nál relatív utat adtál meg, nem abszolútat. Elsőre megnézném, hogy milyen hibakóddal lép ki a script, mikor azonnal megáll.

The world runs on Excel spreadsheets. (Dylan Beattie)

Az FD a fájlazonosító létrehozása - gondolom. Bash-ban nem vagyok olyan jó, egy példascriptben így volt. Más nyelveken ugye ott a fájl azonosító változója szerepel.

A V= pedig már ennek a scriptnek (mondjuk lock.sh) a felhasználásakor jelenik meg. Ha egy scriptben a "V=`lock.sh`" vagy "V=$(lock.sh)" sor szerepel, akkor a script lefagy - gondolom, vár valamire.

Érdekes módon, ha a felhasználó scriptem egy fájlba irányítja a PID-et (azaz a lock.sh script kimenetét), akkor rendben lefut. Vagyis ezzel a kóddal nincs baj:

lock.sh > tmp.pid
V=`cat tmp.pid`

Ilyen formában rendben lefut.

Bash script debugoláshoz hasznos lehet, ha a sor elején nem szimplán /bin/bash áll, hanem /bin/bash -x

En úgy emlékszem hogy bash-ban amikor sub-shellt indítasz átadja/másolja a nyitott FD-ket és ez viszi magával a lock-ot is. De már régen kellett ilyet nyomoznom és nem is biztos hogy most releváns.

A lock file-t ne te hozd létre kézzel ha FD-re lockolsz és nem állomány névre.

Én valami ilyesmivel próbálkoznék, csak fejből írom mert sietnem kell:

{
   if flock -x -n 42 -w 30
   then
      echo "Lock acquired"
   else
      echo "Better luck next time"
   fi
} 42> "$LOCKFILE"

Szerintem érdemes lenne megnézni a másik bash scriptet is ahonnan próbálod ezt hívni. Problémás lehet még a path is. És persze triviális a dolog, de a jogosultságok rendben vannak?

Egyébként várom zahy professzor urat, hogy elmagyarázza mi is történik. Imádom a fazont! Hihetetlen okos ember!

“The basic tool for the manipulation of reality is the manipulation of words. If you can control the meaning of words, you can control the people who must use them.”

― Philip K. Dick

Pontosabban, ha úgy hívom meg, hogy a visszaadott PID értékét elteszem egy változóba, akkor fagy csak be. Azaz V=$(script) vagy V=`script` soroknál.

Itt pontosan ezt most igy hogy? Elinditasz valamit a hatterben, akkor annak a PID-je a $!-ban lesz. Azt egy x=$! hivassal le tudod menteni izibe. Nem kell $(...) vagy `...`. Vagy azzal mit mentesz le? Ezutobbiak kulon processzt inditanak, ami ugye be tud kavarni mint azt feljebb is irtak a tobbiek.

Egy egyszerű minta script, ami a hibátlanul fut le:

#!/bin/sh
./lock.sh > tmp.pid
PID=`cat tmp.pid`
echo $PID

Ugyanez úgy, ahogy szeretném használni, de ahogyan már befagy:

#!/bin/sh
PID=$(./lock.sh)
echo $PID

Természetesen ennél összetettebb kódban szeretném használni, de ez a legegyszerűbb környezet, ahol jelentkezik a hiba.

Á! Ertem. Az első esetben a ./lock.sh a mintaszkripted (parent) subprocesse (child), a masodik esetben meg egy suprocessz subprocesse. Vsz itt tortenik valami furmanysag. Ezelobbi meg okes, ezutobbi mar tul sok reteg egymason. 

Szerk: illetve az is van itten hogy a wait_for_signal fuggveny az ugye szinten subprocessz lesz, ami ugye fut a hatterben miutan a ./lock.sh lefutotott. A ./lock.sh az ugye leall, de mivel a wait_for_signal az orokolte az FD-ket a ./lock.sh-tol ezert ezutobbi leallasa utan az meg ugy ott marad valahogy. Ha ekoze bekerul meg egy parent-child szint a `...` vagy $(...) miatt akkor ott mar nincs ki aki megtartsa azt a processzt igy az feltehetoen zombiva valik. Ezt is probald meg kinyomozni (sima `ps -A xuf` vagy hasonlo modon nezd meg a process tree-t).

A megoldas valami ilyesmi lehet:

( wait_for_signal </dev/null 2>&/dev/null & ) &

csak ugye ekkor a neked nem a gyerekprocessz pid-je kell hanem a gyerekprocesz gyerekenek a pid-je.

Barhogyis, ez a "demonizalas" (teljes levalasztas a futo terminaltol) eleve onmagaban egy erdekes/bonyolult dolog. Ugye ahhoz hogy tenyleg a hatterbe keruljon ez a processz, ahhoz le kell hogy szakadjon mindentol - de pont azaltal nem tud(sz) leszakadni hogy az ilyen stdin/stdout jellegu kapcsolatokat megsem szakitod meg mert informaciot akarsz azon keresztul kinyerni (esetedben az `echo $!`felhasznalasaval).

Kipróbáltam, de "kétértelmű átirányítás" hibaüzenettel visszautasította. Mivel az átirányításokkal volt baja, azokat kihagytam. Úgy lefut hiba nélkül, de ugye rossz PID értéket ír ki.

Módosítottam a sort:

( wait_for_signal & echo $! ) &

Így már jó PID értéket ír ki, és magában jól is működik, de a scriptből történő meghívás esetén továbbra is befagy. :(

 

Amúgy meg nem értem: a szülő processz elindítja a wait_for_signal gyermek ágat, kiírja a PID-jét, és kiszáll.

Innentől ez a processz már nincs a memóriában. Miért baj, ha ezt a szülő processzt egy másik processz hívja meg?

Innentől ez a processz már nincs a memóriában. Miért baj, ha ezt a szülő processzt egy másik processz hívja meg?

A gond az informaciocsere: ha valamilyen processzt ugy igazan a hatterbe akarsz tenni akkor teljesen le kell valasztanod a szulotol. Nyilvan minden processznek van egy szuloje, amitol orokli az fd-ket. A hatterben az tortenik hogy a kernel forkolja a szulot, lesz egy szulo + gyerek, megosztott/kozos fd-kkel. Ezutan, de meg az execve() hivasa elott (ezutobbi csereli le a gyerek processz image-jet egy masik image-re) mind a szulo, mind a gyerek "gatyaba razza" az fd-ket, azaz lezarja azokat amik biztos nem kellenek neki. Itt a szulo-gyerek kapcsolatban lehetnek pipe-ok, lehetnek atiranyitasok meg orokolheti a terminalt is meg ugy atlaban mindent. Szoval a fork() elott sok open(), dup2(), pipe() hivas is tortenik, a fork() utan pedig mind a szulo, mind a gyerek lezarja (close()) azokat az fd-ket amikre nincs szuksege. Idealisan, mikor valamit a "nagyon a hatterbe teszel", minden kapcsolatnak meg kell szakadnia, azaz a gyerek csak olyan fd-t tarthat nyitva amit, illetve aminek a masik veget(!) nem fogja a szulo[*]. Raadasul a processz tree-t is meg kell szakitanod, ezert mind a fenti hatterbe vevesnel amit irtam, mind pl a daemon() hivasnal ket fork() is van egymas utan, es valojaban nem a gyerek hanem az "unoka" kerul hatterbe (ugyhogy a gyerek az teljesen meghal, leall, igy az init processz gyereke lesz a processz tree-ben). Lasd `man 3 daemon`, ott leirja a folyamatot eleg jol. 

Viszont ha minden kapcsolat megszakad akkor hogyan is adod at azt az infot hogy "mi is a PID-je" a gyereknek? A pidfile egyebkent pont erre (lenne) jo, vsz nem veltetlen hogy ez az egyik sztenderd megoldas. Es akkor a filerendszer tolti be az interprocessz kommunikacios felulet szerepet (merthogy a sztenderd UNIX fd-alapu dolgok nem mukodnek). Egyebkent csinalhatod akar ezt is, mmint pidfile-t. Akar a lockfile is tartalmazhatja a PID-et, es akkor ugy(?). Nem tudom, nyilvan ez a sajat problemadtol, feladatodtol fugg hogy mit is szeretnel valojaban megoldani. En egy kicsit konzervativabban allnek egyebkent hozza, azaz a lock acquire illetve lock release ugyanabban scope-ban (ezesetben bash szkriptben) tortenjen. Ez azert egyszerusiti is a dolgokat.

[*] szerk: ez lehet hogy nem teljesen igaz, mert vegulis ahogy egy socket()-et utolag is meg tudsz nyitni ket fuggetlen processz kozott (lasd: internet, localhost, unix domain, stb), ugy akar egy socketpair() ket veget orokolheti a szulo meg az unoka. Csak az eleg ritka use case hogy daemon()izalnal egy processzt, nagyon a hatterbe teve, process tree-ben levalasztva, stb, de meg elotte azert fenntartasz egy kapcsolatot a szulovel. Ami egyebkent barmikor leallhat. Ilyet meg nem lattam elesben sehol sem.

Köszönöm, úgy tűnik, meglett a megoldás.

Próbálgattam a nohup és disown parancsokat, és egyszerre csak az elvárásoknak megfelelően működött. Ekkor elkezdtem kitörölgetni részeket, hogy kiderüljön, mitől javult meg. Így végül sem a nohup, sem a disown parancs nem kellett.

A lényeg, hogy a gyermekfolyamat indításánál mind az input, mind az output át legyen irányítva a /dev/null-ra. (Gondolom, bármilyen fájl jó lenne itt.)

Az elvárásoknak megfelelőnek tűnő működés még nem volt az igazi, mivel a SIGTERM-re a fájl ugyan törlődött, de a processz nem zárult le, így az "rm $LOCKFILE" után még bekerült egy exit is.

Így most jónak tűnik. A lock.sh script jelenleg így néz ki:

#!/bin/bash
TIMEOUT=3
LOCKFILE="./file.lock"

function cleanup() {
    rm $LOCKFILE
    exit 0
}

function wait_for_signal() {
    trap "cleanup" SIGTERM
    until false ; do
        sleep 1
    done
    exit 0
}

touch $LOCKFILE
exec {FD}<>$LOCKFILE
if ! flock -x -w $TIMEOUT $FD; then
    exit 1
else # Lock acquired
    wait_for_signal < /dev/null > /dev/null 2>&1 &
    echo $!
    exit 0
fi

A tesztelő script pedig nem változott:

#!/bin/sh
PID=$(./lock.sh)
echo $PID

Köszönöm a segítségeket!

Feltennék egy ünneprontó pótkérdést: Mi volt a feladat, amit meg kellett oldani? (Csak a saját szavaiddal!)

Talán, bár nekem olyan bonyolultnak tűnt, hogy nem is mertem beleszólni. ;) A flock-ot használom szorgalmasan. Ideírom, hátha ötletet adok:

#valahol a program elején
LOCKFILE=Lock_filename
LOCKFILE_FD=1020
eval "exec $LOCKFILE_FD>&-"
eval "exec $LOCKFILE_FD>>$LOCKFILE"

#egy forkolt függvényben
flock -x LOCKFILE_FD
commands
...
flock -u LOCKFILE_FD

A példa sok fork exkluzív lockolását oldja meg. A lockon belül a commands fér hozzá a közös objektum(ok) hoz, és ez a része a programnak mindig forkolva fut.

Ehhez van egy $LOCKFILE, amit megnyitunk egy FD-vel - hogy a továbbiakban tudjuk mi az FD értéke. Ebben a megoldásban minden művelet/objektum (csoporthoz) kell egy lockfile. A vázlatosan ábrázolt függvény nálam tobb száz példányban futhat.