Linux waitpid pipe-al

Fórumok

Ez már kezd az agyamra menni... Hogy kell lekérni Linux alatt a child visszatérési értékét, ha pipe is van? Rohadtul nem úgy működik, ahogy a man page állítja!

Pipe (és WNOHANG) nélkül minden okés:

    ret = 0;
    if(!(pid = fork())) {
        ret = 1;
        printf("child ret %d\n", ret);
        exit(ret);
    } else
    if(pid > 0) {
        waitpid(pid, &ret, 0);
        printf("parent ret %d\n", ret);
    }

Kimenet:

child ret 1
parent ret 1

Na de nekem az kellene, hogy a gyerek futtasson le egy parancsot, ha nem sikerül, akkor írja ki a hibát, és mindezt a szülő pipe-al olvassa, ugyanakkor a visszatérési érték is kéne, hogy sikerült-e a futtatás.

Teljes példa:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char **argv)
{
    char output[4096];
    int stdoutpipe[2], l, ret = 0;
    pid_t pid;

    if(pipe(stdoutpipe) < 0) { fprintf(stderr, "unable to create pipe?\n"); return 1; }
    if(!(pid = fork())) {
        dup2(stdoutpipe[1], 1);
        dup2(stdoutpipe[1], 2);
#ifdef NOTOKAY
        ret = system("echosaaa");
#else
        ret = system("echo");
#endif
        if(errno) perror(NULL);
        printf("child ret %d\n", ret);
        exit(ret);
    }
    if(pid > 0) {
        close(stdoutpipe[1]);
        do {
            if((l = read(stdoutpipe[0], output, 4096)) > 0)
                write(1, output, l);
        } while(waitpid(pid, &ret, WNOHANG) != pid);
        close(stdoutpipe[0]);
    } else {
        fprintf(stderr, "unable to fork?\n");
        close(stdoutpipe[0]);
        close(stdoutpipe[1]);
    }
    printf("parent ret %d exit %d\n", ret, WEXITSTATUS(ret));

    (void)argc; (void)argv;
    return 0;
}

Ez konstans 0-át ad vissza, akármi is a gyerek exit() paramétere.

$ gcc aaa.c -o aaa; ./aaa

child ret 0
parent ret 0 exit 0

$ gcc -DNOTOKAY aaa.c -o aaa; ./aaa
sh: line 1: echoaaa: command not found
child ret 32512
parent ret 0 exit 0

Magyarán a waitpid NEM állítja a státuszt, akkor sem, amikor a gyerek pidjével tér vissza, holott a man page szerint kéne neki. Példától függően vagy konstans -1, vagy konstans 0 lesz minden exit() értékre a ret.

WTF? Miért nem adja vissza a gyerek státuszát a watpid, ha WNOHANG opcióval hívódott?

És ha ez normális, akkor hogy a francba kell lekérni a gyerek státuszát, ha olvasni is akarja az ember a kimenetét??? Alaposan átolvastam a man page-eket, nagyon nem így kéne működnie! Rengeteg példát is áttúrtam, de bakker, azok vagy csak a visszatérési értéket kérik le, vagy a kimenetet, de egyik sem mindkettőt!

Ami leírást meg github-ot találtam, ott mindenhol azt írják, így működnie kéne. De mégsem működik! Arról sehol nem szól fáma, hogy a waitpid hibás értéket adna vissza a status paraméterben.

Stackoverflow-al sem kerültem közelebb a megoldáshoz, ezt találtam, ami hasonló (neki is a waitpid()-el van baja, ha WNOHANG meg van adva, mondjuk ő 0-át kap vissza), habár ez epoll-al is meg van spékelve. A megoldási javaslat: "One possible implementation is to repeat waitpid(..., WNOHANG) until it returns the expected PID", na de hát én pont eleve ezt csinálom! Akkor WTF?

Hozzászólások

A man leírja, hogy a status értékét a WIFEXITED és WEXITSTATUS makrókkal kell nézni.

Osz mire mész vele, ha mindkét esetben csupa 1-es bit a status??? Gyerek "exit(0)", szülő status=0xFFFFFFFF; gyerek "exit(1)", szülő status=0xFFFFFFFF!!!
Tisztában vagyok vele, hogy alsó 8 bitnek a visszatérési értéket kéne tartalmaznia, ezt írja a man is, de rohadtul nem azt tartalmazza, pont ez a bajom! Azt nem tudom, hogy miért nem. Nincs semmi signal, az exit()-nek meg minden nyitott fájlleírót be kell zárnia, de manuálisan is bezárom őket.

A parent oldalon valaszd kulon a do/while() loopot es a waitpidet. Ha child lezarta az stdout-jat mert kilepett exit()-tel akkor a parent oldali read() nullaval ter vissza. Ekkor megszakitod a while() ciklust, majd ezutan egy blocking waitpid()-del megnezed az exit statust:

while ( 0<(l=read(stdoutpipe[0],buff,4096)) )
 {  csinaljvalamit(buff,l);
 }
waitpid(pid,&status,0);
printf("child exited with:",WEXITSTATUS(status));

ezt most csak ugy vakon beirtam, de ugy remlik hogy igy szoktam csinalni ezeket... 

Ugye a do-while megoldasoddal az a gond hogy a read()==0 esetben megszakitod a ciklust, igy az a waitpid() nem is fog lefutni valojaban... es ha blocking read()-dal is meg nonblocking waitpid()-del is tesztelsz akkor az valojaban egy race condition ha jobban belegondolok. 

A while-ban való vizsgálatod miatt nem derül ki, hogy mi miatt nem a pid-del tér vissza, azaz 0-val vagy -1-gyel:

       waitpid(): on success, returns the process ID of the child  whose  state
       has  changed; if WNOHANG was specified and one or more child(ren) speci‐
       fied by pid exist, but have not yet changed state, then 0  is  returned.
       On failure, -1 is returned.

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

Szerkesztve: 2024. 11. 30., szo – 13:49

szerk.:

Én úgy csinálnám, hogy - mivel úgyis van pipe -on keresztüli kommunikáció - a child írjon bele a pipe-ba minden infót, amit a parent-nek tudnia kell. Pl. ebben az esetben a saját pid-jét és a system() visszatérési értékét.

        if (!(pid = fork())) {
                printf("child...\n");
                system("ls -l");
                sleep(1);
                _exit(69);
        } else if(pid > 0) {
                int ret;

                printf("pid=%d\n", pid);
                while (1) {
                        int r = waitpid(pid, &ret, WNOHANG);
                        printf("r=%d ret=%d status=%d\n", r, ret, WEXITSTATUS(ret));
                        usleep(100000);
                }
        }

s az output, ott van az exit code:

pid=614357
r=0 ret=32144 status=125
child...
total 3
-rwxr-xr-x 1 micsa micsa 15832 Nov 30 14:44 a.out
-rw-r--r-- 1 micsa micsa   405 Nov 30 14:44 korte.c
-rw-r--r-- 1 micsa micsa  1005 May 26  2016 pipe.c
r=0 ret=32144 status=125
r=0 ret=32144 status=125
r=0 ret=32144 status=125
r=0 ret=32144 status=125
r=0 ret=32144 status=125
r=0 ret=32144 status=125
r=0 ret=32144 status=125
r=0 ret=32144 status=125
r=0 ret=32144 status=125
r=614357 ret=17664 status=69
r=-1 ret=17664 status=69
r=-1 ret=17664 status=69
r=-1 ret=17664 status=69
r=-1 ret=17664 status=69

megy az pipe-pal is:

        int pp[2];
        pipe(pp);

        if (!(pid = fork())) {
                close(1); close(2);
                dup2(pp[1], 1);
                dup2(pp[1], 2);
                close(pp[0]); close(pp[1]);

                printf("child...\n");
                system("ls -l");
                sleep(1);
                _exit(69);
        } else if(pid > 0) {
                int ret;

                fcntl(pp[0], F_SETFL, fcntl(pp[0], F_GETFL) | O_NONBLOCK);
                printf("pid=%d\n", pid);
                while (1) {
                        char buf[4097];
                        int l;

                        if ((l = read(pp[0], buf, 4096)) > 0) {
                                buf[l] = 0;
                                printf("from child: '%s'\n", buf);
                        }
                        int r = waitpid(pid, &ret, WNOHANG);
                        printf("r=%d ret=%d status=%d\n", r, ret, WEXITSTATUS(ret));
                        usleep(100000);
                }
        }

ott az output, sleep nélkül... mi a túró nem megy?

pid=619048
r=0 ret=8 status=0
from child: 'total 4
-rwxr-xr-x 1 micsa micsa 16048 Nov 30 15:14 a.out
-rw-r--r-- 1 micsa micsa   733 Nov 30 15:14 korte.c
-rw-r--r-- 1 micsa micsa  1005 May 26  2016 pipe.c
'
r=619048 ret=17664 status=69
r=-1 ret=17664 status=69
r=-1 ret=17664 status=69
r=-1 ret=17664 status=69
r=-1 ret=17664 status=69
Szerkesztve: 2024. 11. 30., szo – 13:58

Nulladik: tibyke nevében a -val, -vel ragot tessék átismételni!

Első: Minimal reproducible example-t mutass! Kiegészítettem a kódodat működőképesre, de egyrészt miért kéne ezzel időt töltenie annak aki segíteni akar neked?, másrészt nem ugyanazt kapom amit te. Olyan apróságok, mint például az "l" változó típusa, előjeles vagy előjel nélküli, alulcsordul-e a -1, hibás összehasonlítást eredményezve, is számíthat.

Továbbiak: A gyerekben printf() és perror() függvénnyel írsz a kimenetre / hibakimenetre, amik már a pipe-ra vannak átrakva. Hogy jelenik meg a "child ret 0" sor? Nem értem. Hacsaknem a csinalvalamitakimenettel() függvény átlapátolja, de akkor miért nem lapátolja át a "sh: line 1: ./hibasparancs: Permission denied" sort is? Biztos vagy benne, hogy a(z amúgy működésképtelenre csonkított) programod kimenetét pontosan mutattad meg nekünk?

A system(), meg a legtöbb függvény, tudtommal nem ígéri meg hogy siker esetén az errno-t békén hagyja, vagy nullázza. (Lehet hogy tévedek amúgy, a man page nem dokumentálja ezt.) (Kivételes esetben van csak értelme kézzel nullázni az errno-t. Nézz rá mások programjaira, (szinte) sose látsz ilyet, elég gáz lenne ha úgy kéne programozni hogy minden függvényhívás előtt az errno-t nullázni kellene kézzel.) Először a ret-et vizsgáljuk, és csak ha az hibát jelez akkor nézünk rá az errno-ra.

Na de a lényegre ránézve: Az, hogy a gyerek processz kilép, és az, hogy a pipe-on EOF-ot kapsz, az két teljesen független esemény, akármelyik irányba sok-sok idő eltelhet köztük.

Ha a gyerek szül magának egy gyereket (unokát a fő programod szemszögéből), akkor az eredeti gyerek kiléphet, a pipe még él és virul és bonyolíthat további forgalmat, miközben a waitpid() már jelenti hogy kilépett. Ugyanez megtörténhet úgy is, hogy a fájlleírót átpasszolja egy másik processznek (perverz, ritkán látott, de működőképes dolog).

Ha viszont a gyerek bezárja a pipe író végét, akkor még vígan élhet sokáig, a pipe olvasó vége EOF-ot kap.

Ha csak "simán" kilép a gyerek és nem csinált semmi szokatlant, akkor a fentiek miatt olyan race condition-ök vannak, hogy nem tudhatod, hogy a két esemény közül melyiket észleli elsőként a szülő processzed, esetleg az egyik forgatókönyv történik az esetek 99.99%-ában, a másik pedig 0.01%-ban, sok sikert az ilyen hibák utólagos levadászásához :)

A programod amúgy mindkét sorrendet hibásan kezeli. Koncepcionálisan nem gondoltad végig hogy mit akarsz csinálni.

Ha előbb veszed észre hogy kilépett a gyerek, akkor a pipe-ban ott ragadhat adat, amit sose olvasol ki. Például a gyerek kiír egyben 5kB-ot és rögtön kilép, a szülő beolvas 4kB-ot majd látja hogy kilépett a gyerek és nem olvas tovább.

Ha meg előbb veszed észre az adat végét, vagyis a read() visszatérési értéke 0, akkor kiugrasz a ciklusból, a waitpid() le se fut egyszer se, a pid-ben marad ami benne volt. Ez az amibe beleütközöl, szerintem.

> hogy a francba kell lekérni a gyerek státuszát, ha olvasni is akarja az ember a kimenetét???

Először legyen egy pontos specifikációd, hogy mit is szeretnél csinálni az imént említett esetekben. Például ha tudod hogy nem lesz unoka (vagy ha esetleg lesz, oké ha őt is meg kell várnod), akkor először olvass EOF-ig, utána jöhet a waidpid.

Sajnos egyébként nincs igazán jó általános válasz. Terminál emulátor programok szenvednek is emiatt. Egyrészt elvárás, hogy kilépjen, amint a gyerek kilépett, akkor is ha van unoka aki elvileg még írhat később a terminálra. Másrészt elvárás, hogy a gyerek összes kimenetét mindenképp feldolgozza - ez nem fontos akkor ha rögtön be is záródik az ablak, de fontos ha a terminál esetleg úgy van beállítva hogy nyitva tartsa az ablakot, vagy új shell-t indítson, vagy fontos hogy accessibility szoftver felolvassa a teljes szöveget stb. A luit program a -x kapcsolóval a felhasználóra bízza hogy kétféle működés közül válasszon: a gyerek kilépése számítson (adatvesztés lehetséges) vagy a csatorna bezárása számítson (unoka fogva tarthatja a vonalat). A VTE-ben (GNOME Terminal és társai) igazi fekete mágiát alkalmazunk: ha a gyerek kilépett, akkor még olvasunk legfeljebb 64kB-ot nem blokkoló módon, de abbahagyjuk az olvasást ha épp nincs adat: ezzel a módszerrel tuti (ööö... szinte tuti) kiolvasunk mindent amit a közvetlen gyerek írt, hiszen ha már kilépett akkor új cuccot már nem írhat, a régi meg legfeljebb 12kB körül lehet, kábé ekkora a kernelben a terminál vonal puffermérete; viszont ha van még unoka aki írhat a továbbiakban akkor arra nem várunk, ő már nem érdekel minket.

tibyke nevében a -val, -vel ragot tessék átismételni!

Mindenhol jól irtam! Idézd be, hogy mire irogatsz, ha kaffogni akarsz!

Minimum reproducible example-t mutass!

Azt tettem, csak a nyilvánvalót hagytam ki, hogy ne terelje el a lényegről a figyelmet.

Olyan apróságok, mint például az "l" változó típusa

Kurvára tök mindegy ebben a példában, mert a read csak egyetlenegyszer fut le és pozitív értéket ad vissza, de már írtam ezt... Egyébként meg a default C típus, tessék felcsapni a C szabványt és kikeresni, mi a default típus.

Na de a lényegre ránézve: Az, hogy a gyerek processz kilép, és az, hogy a pipe-on EOF-ot kapsz, az két teljesen független esemény

Na de a lényeg, ez tök nyilvánvaló, a kérdés nem is ez volt, hanem az, miért ad vissza a waitpid -1-et státusznak, amikor a gyerek kilép.

Ha csak "simán" kilép a gyerek és nem csinált semmi szokatlant, akkor a fentiek miatt olyan race condition-ök vannak, hogy nem tudhatod

Ekkora bullshitet rég olvastam. Konkrétan 0 segítőgészség van az egész posztodban, csak ömlengés és digitális szemét. (Tisztázzuk, nincs race condition, egyáltalán, tudod, mi az? A waitpid a gyerek pid-jét adja vissza, arra lép ki a ciklus, de már leírtam ezt százszor.).

Nem tőled fog érkezni a megoldás, ebben egész biztos vagyok! >P

Az, hogy a gyerek processz kilép, és az, hogy a pipe-on EOF-ot kapsz, az két teljesen független esemény, akármelyik irányba sok-sok idő eltelhet köztük.

Így van; erre a hordozható megoldás a ppoll() / pselect(). A SIGCHLD-ot elkapjuk, de a programban végig maszkolva (blokkolva) tartjuk, csak a pselect / ppoll oldja fel ideiglenesen. Így tudunk multiplexelni a pipe IO-ra és a SIGCHLD-ra egyszerre. A SIGCHLD handler-ben max. egy volatile sig_atomic_t globális változót állítunk. A ppoll / pselect után lehet read(), write(), waitpid(). A waitpid()-nek nem kell WNOHANG (csak akkor hívjuk meg, ha volt SIGCHLD delivery a ppoll-on / pselect-en belül). Minimum a write()-nak viszont így is kell az O_NONBLOCK, mivel a ppoll / pselect már akkor is írhatóságot jelez pipe-on és named FIFO-n, ha csak PIPE_BUF byte-nyi hely van a pipe buffer-ben, viszont a write O_NONBLOCK nélkül pipe-on és named FIFO-n nem végez részleges írást, csak teljes buffert ír ki.

Szerkesztve: 2024. 11. 30., szo – 14:37

Teljes példa (de aki ennélkül nem érti a kérdést, attól aligha várható megoldás, csak megjegyzem...):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char **argv)
{
    char output[4096];
    int stdoutpipe[2], l, ret = 0;
    pid_t pid;

    if(pipe(stdoutpipe) < 0) { fprintf(stderr, "unable to create pipe?\n"); return 1; }
    if(!(pid = fork())) {
        dup2(stdoutpipe[1], 1);
        dup2(stdoutpipe[1], 2);
#ifdef NOTOKAY
        ret = system("echosaaa");
#else
        ret = system("echo");
#endif
        if(errno) perror(NULL);
        printf("child ret %d\n", ret);
        exit(ret);
    }
    if(pid > 0) {
        close(stdoutpipe[1]);
        do {
            if((l = read(stdoutpipe[0], output, 4096)) > 0)
                write(1, output, l);
        } while(waitpid(pid, &ret, WNOHANG) != pid);
        close(stdoutpipe[0]);
    } else {
        fprintf(stderr, "unable to fork?\n");
        close(stdoutpipe[0]);
        close(stdoutpipe[1]);
    }
    printf("parent ret %d exit %d\n", ret, WEXITSTATUS(ret));

    (void)argc; (void)argv;
    return 0;
}

Ez konstans 0-át ad vissza, akármi is a gyerek exit() paramétere.

$ gcc aaa.c -o aaa; ./aaa

child ret 0
parent ret 0 exit 0

$ gcc -DNOTOKAY aaa.c -o aaa; ./aaa
sh: line 1: echoaaa: command not found
child ret 32512
parent ret 0 exit 0

Magyarán a waitpid NEM állítja a státuszt, akkor sem, amikor a gyerek pidjével tér vissza. Jól látszik, hogy az az érték marad benne, ami volt.

Szóval a kérdés, hogy teljesen tiszta legyen mindenkinek: hogy kell egy gyerek processz kimenetét ÉS visszatérési értékét is megszerezni a szülőben?

Nicsak-nicsak, egy MRE amely nem ekvivalens az első postolt verzióval. Ugyanis a do cikluson belüli break kikerült. [Szerk: látom, azóta az eredeti postból is, így az eddig rá érkezett kommentek egy része nem igazán értelmezhető.]

A legeslegelső komment amit kaptál rámutatott egy hibára: a W* makrók hiányára. Visszadobtad a labdát azzal, hogy az nem oszt, nem szoroz, és emiatt hibásan hagytad azt a kódrészletet. Többen - köztük én - megmutattuk, hogy más hibák is vannak a kódodban.

Ezen más hibák közül egyhez hozzányúltál, a break eltűnt. És lám-lám most már nem csupa 1-es bit a gyerek visszatérési értéke.

Lehet hogy csak össze kéne raknod a puzzle-t? Hogy a kódodban több hiba is van, melyeket - mily meglepő - mindet javítani kéne? És esetleg nem zsigerből, fantasztikus tapasztalatoddal és rálátásoddal, remek minőségű kódodat lebegtetve lehülyézni azokat akik a több hibát nem mindet egyszerre veszik észre??

Igen, valóban ez az alapvető hiba a programban:

        ret = system("...");
        exit(ret);

A két függvény specifikációja (kiemelés tőlem):

Tehát ha úgy hívjuk meg az exit()-et, hogy utána waitpid()-del akarjuk lekaszálni, akkor a 0..255 tartományba eső értéket "érdemes" neki adnunk. Ezt a feltételt a system() közvetlen visszatérési értéke pedig nem elégíti ki. (A debug kimenetből látszik, hogy a system() visszatérési értéke közvetlenül 32512; ahogy Micsa írta is, 0x7F00.)

Tehát a system() és az exit() között kell egy WIFEXITED() + WEXITSTATUS().

Ebből pedig kiderül a nagyobb probléma is: ebben a megközelítésben azt nem tudjuk visszaadni a parent-nek, ha a system() visszatérési értéke WIFSIGNALED()+WTERMSIG()-nek megfelelő (vagyis a gyerek shell-gyerekének gyereke szignállal ért véget a gyerek shell-gyereke szignállal ért véget).

Erre az intézményesített workaround / konvenció az, hogy az exit status-ban nem használunk 8 bitet, hanem csak 7-et; tehát normál (nem szignálos) befejeződésre minden utility 0-127 közötti értéket ad vissza. (Pontosabban 0-125 közöttit, ugyanis a 126-ot és a 127-et speciális értékeknek tekintjük.) És ha a system() azt mondja, hogy WIFSIGNALED(), akkor a gyerek azt az exit()-en keresztül úgy közli, hogy 128 + WTERMSIG(). (Érdekesség, hogy a legkülső szülő pl. a 128+11=139-es értékből nem tudja megkülönböztetni, hogy a gyerekben a system() által forkolt shell halt meg SIGSEGV-vel, vagy pedig a nevezett shell-nek a gyereke (a példában az echo). Utóbbi esetben a system() által índított shell végzi el a 128+WTERMSIG() összeadást, és a gyerek ezt az értéket csak továbbítja (mivel WIFEXITED()); előbbi esetben az összeadást a gyerek végzi el (mivel WIFSIGNALED())).

🍿 azok a fránya makrók újra támadnak

// Happy debugging, suckers
#define true (rand() > 10)

Szerkesztve: 2024. 11. 30., szo – 18:13

Erdemben nem tok hozzaszolni, mert hala istennek nem vagyok programozo de az altalanos, h ti igy szivjatok egymas veret? :D:D (monduk a legdurvabb, h a kerdezo, aki segitseget ker ugye, hogy olt mindenkit...)