Jednoho dlouhého zimního večera jsem se rozhodl, že si napíšu v C++ třídu, která bude multiplatformě řešit spouštění procesů třetích stran z aplikace. Slovem multiplatformní mám na myslí hlavně Linux a Windows verzi. Nakonec se to povedlo, ale bylo to náročné, skoro jsem se začínal obávat, že univerzální rozhraní není možné napsat… tak aby tam byly roury, přesměrování, čekání na proces, nebo jeho paralelní běh. Nechci teď řešit kolik knihoven a v kterých balících se tohle řeší. Já si prostě chtěl napsat vlastní v rámci již mé existující knihovny, tak aby byla konformní s existujícím API.
Nebudu teď řešit rozdíly mezi Linuxem a Windows, ale podělím se se zkušenostmi vývoje Linuxové verze a zejména popíšu jaké pasti, pastičky a bezpečnostní nástrahy běžného vývojáře čekají a na co všechno je třeba si dát pozor, co není z počátku vidět.
Pro začátečníky a na jednorázovky úplně postačí, ale musí se počítat s tím, že to funguje jak v DOSu. Process se spustí a ten kdo jej spouštěl musí čekat. Pokud by někdo chtěl řešit souběžný běh dvou procesů (mého a cizího) přes příkaz fork a system, tak to nedělejte. Vyrobíte totiž dva procesy na místo jednoho. (příkaz system() si sám zavola fork).
Tohle Windowsák prostě nepochopí. V Linuxu neexistuje příkaz na vytvoření nového procesu. Windowsovská varianta CreateProcess, tady prostě není. Procesy se zde spouští tak jaksi přes hlavu. Jako kdyby sekretářka vytvářela wordowské dokumenty tak, že z jednoho dokumentu vyrobí kopii, kterou následně otevře ve Wordu, obsah smaže a napíše dokument nový.
Přesně takhle to ale funguje v Linuxu. Zajímavý ne? No kdyby to byl jediný problém, tak se to dá celkem vydržet.
execl, execlp, execle, execv, execvp, execvpe – jen si vyberte, který vám vyhovuje, podle toho, jak se zadávají argumenty, nebo prostředí. No klid, nic nevybírejte, nebudete to potřebovat, jediný příkaz, který má smysl používat, je execve, protože ten jediný zvládne spustit i bashový skripty, tak jak jsme zvyklí ze shellu. A já jako programátor nechci řešit, zda na druhé straně je binárka, shell-skript, python, nebo nedejbože péhápko.
Protože budeme spouštět proces přes fork / exec, je třeba zajistit ošetření situace, kdy se exec nezdaří. Nemusí se zdařit, třeba proto, že v příkazové řádce je uvedena chybně cesta na binárku nebo, že binárka není přeložena pro aktuální platformu nebo, že nemáme práva ji spouštět. Zvlášť pokud pouštíme proces, který poběží v pozadí a naše aplikace s ním bude spojena rourou.
Proces spouštíme přes fork. V tom případě se rodič ještě nedozví o tom, zda se v dítěti nezdařil execve. Pokud rodič chce pokračovat až po tom, co si ověřil, že dítě úspěšně spustilo proces, bude muset použít dosti komplikovaný způsob synchronizace … a to mě zrovna teď nenapdá, jak by se to udělalo. Jakým způsobem předám výsledek volání funkce execve v tomto kódu?
int i = fork(); if (i < 0) chyba(); if (i == 0) { execve(....); _exit(errno); }
Pokud se execve zdaří, dítě už nikdy nedostane šanci informovat rodiče o úspěchu. Proto taky nemusím testovat návratovou hodnotu. Ta bude vždycky –1. Dítě ukončuji předáním errno, což zrovna není nic moc, protože při čtení statusu v rodiči nepoznám, zda návratová hodnota pochází z procesu nebo z nepovedeného execve. K řešení tohoto problému se dostanu v dalších bodech.
Poznámka: Samozřejmě že na to mohu kašlat, a rodič se může chovat jako by se to zdařilo. On mu totiž hned přiletí SIGCHLD, takže žádná křeč. Ale napište knihovní funkci, která po svém návratu může volajícímu s jistotou oznámit „je to okaj, požadovaný proces byl spuštěn a nyní běží“… či případně vyhodit výjimku která obsahuje kód chyby nastavené při neúspěšném execve.
Ve firmě, ve které pracuji a která je plná linuxáků, jsem narazil při vývoji linuxového backendu na problém, kdy mi fork() vyhazoval chybu, že je nedostatek paměti. Přičemž jediný, co jsem měl v plánu provést je spuštění wgetu v rouře s unzipem a to celé přivést rourou do aplikace (abych nemusel importovat HTTP klienta a zlib knihovnu). Šel jsem tedy za odborníky z vedlejšího oddělení, aby mi poradili, kde může být zádrhel.
No problém nebyl ani ve wgetu, ani v unzipu, ale v tom, že tou dobou moje aplikace zabírala v paměti slušných 1.5GB na virtuálu, který měl limit 2GB a se systémem tam zbývalo něco přes 300MB volného místa. Tam by se wget a unzip vešel, ale rozhodně se tam nevejde dalších 1.5GB mé aplikace. Ale co to plácám, vždyť fork přece neprovádí duplikaci, pouze nasdílí paměťové stránky přes Copy-On-Write! Taky vám tohle neustálě někdo vštěpuje do hlavy? Ona to pravda je, ale do hry tady vstupuje další linuxová specialita a to overcommit (o tom někdy jindy). Sice fork neprovede plnou kopii, ale nový proces by mohl časem těch 1.5GB požadovat a to ta mašina nemá k dispozici. Řeší se to tedy tak, že se na rezervaci prostoru pro nový proces pohlíží jako na obyčejný malloc, na který se vztahují pravidla overcommitu. Protože stroj měl volných 300MB a fork potřeboval vyhradit 1.5GB, musel by být overcommit nastaven aspoň na 500% a to nebyl.
Tohle všechno jsem se dozvěděl od našich linuxáků s tím, že pokud to chci jinak, tak mám prostě smůlu, Linux to jinak neumí. Ať pošteluju overcommit, nebo si zvětším virtual (pro unzip a wget?). No už vidím, jak naše konzervativní adminy přesvědčuju, že kvůli mé knihovně mají o hodně zvýšit overcommit na produkčních strojích.
Závěr: Pokud vaše aplikace potřebuje spouštět externí procesy, nesmí zabrat víc paměti, než je zhruba stejné množství volné paměti. Může to být trochu víc, ale pak záleží na tom, jak je nastaven overcommit. Psát univerzální knihovnu tak, že se předpokládá nějaké nestandardní nastavení systému, je ale prasárna.
No v manpages jsem objevil vfork(2). Rychlým pohledem na google a do diskuzí člověk získá dojem, že lidi si nejsou úplně jisti, jak tahle funkce pracuje. Já mám tuto zkušenost:
Jedná se o virtual fork. Tváří se to jako fork ale není to fork. Je to fakefork. Pokud někdo hledá funkci CreateProcess, tak ji právě našel. Funkce vfork vytvoří nové dítě ale v paměťovém prostoru rodiče. Rodič je zablokován … (a teď kontrolní otázka, co když má rodič vlákna, he?) … a místo něho běží dítě, a to do doby, než se dítě ukončí, nebo zavolá execve. Co je tady zajímavé, totiž, že dítě má plný přístup do paměťového prostoru rodiče, a to i pro zápis!
vfork řeší dvě bolístky
… a navíc, vfork je pravděpodobně rychlejší, protože nedochází ke zbytečnému duplikování stránkovacích tabulek, které jsou následně zahozeny.
Podívejme se, jak se bude řešit návratová hodnota z vforku:
static int execerr; execerr = 0; int i = vfork(); if (i < 0) chyba(); if (i == 0) { execve(....); execerr = errno; _exit(0); } if (execerr) throw UnableToExecuteProcessException(execerr);
Zjednodušeně řešeno, dítě vzniklé po vforku() má přístup k proměnné execerr, která je alokovaná staticky (pokud tam má člověk vlákna, je lepší, když je alokovaná na haldě). Proměnnou není možné dát do zásobníku, protože vfork provede duplikaci zásobníku a změna proměnné by se nepromítla do zásobníku rodiče. Pokud execve selže, nastaví se proměnné execerr a dítě skončí. Pokud neselže, zůstane tam hodnota 0.
Důležitým detailem je také to, že rodič je počas vykonávání dítěte zastaven. Proto není nutné řešit synchronizaci, v okamžiku, kdy je rodič spuštěn, se už může dozvědět výsledek. Pokud je v execerr nula, pak bylo spuštění procesu úspěšné. Pokud není nula, pak bylo neúspěšné.
Kdybych tohle přirovna k sekretářce, tak zatímco použití forku znamená, že sekretářka zkopíruje dokument a ten nový otevře ve Wordu a smaže ho aby napsala nový, u vforku to udělá tak, že ve Wordu otevře starý (dokument se interně zamkne) a následně sekretářka vyvolá příkaz Save As s novým názvem a až potom dokument smaže a začne psát nový. Mezi otevřením a přeuložením je krátký časový úsek, kdy s původním dokumentem nelze pracovat.
Aby to nebylo tak jednoduché, vfork není standardní. Navíc pořád mě dost děsí výstraha v manuálu, kde prý zápis do proměnné vytvořené v rodiči není definován. Tady je prostor pro budouci změny, aby třeba časem to dítě nevznikalo někde v hyperprostoru, bez možnosti komunikovat s rodičem.
Co je to proboha popisovač? No přece deskriptor. Miluju český jazyk :-)
Krásně se demonstruje na dvojici fork/exec vytváření rour. V rodičovi vytvořím rouru, forknu to, v dítěti jednu stranu zavřu a druhou zduplikuju na některý z popisovačů 0 až 2, podle potřeby. V rodičovi pak zavřu opačný konec a ten co mi zbude používám pro I/O s dítětem. Funkce execve má tu vlastnost, že zachová otevřené všechny souborové popisovače, čímž umožňuje realizovat právě onu nádherně vyvedenou pupeční šňůru mezi rodičem a dítětem.
(A to není všechno, fork popisovače duplikuje, takže si každý proces spravuje vlastní sadu popisovačů nezávisle na sobě. To vůbec nehledejte u Windows, kde se popisovače … HANDLE… sdílí a to takovým způsobem, že když rodič popisovač zavře, automaticky se zavře v dítěti a další operace s ním vedou na chybu "neplatný popisovač! Druhým rozdílem je, že když v Linuxu skončí dítě, zavřou se všechny jeho popisovače a roury spadnou do stavu SIGPIPE. Ve Windows zůstanou handly otevřené a čteni z rour bude viset v deadlocku!)
Edit: Tvrzení ve výše uvedeném odstavci se nepodařilo ověřit.
Až sem je to růžová zahrada, Linux rulez! Jenže ouha. V aplikaci máte otevřeno několik souborů, pár rour, a nějaká ta síťová spojení, třeba takový … ssh démon, který vytvoří dítě, spojí se s klientem a spustí bash. Jenže se nezduplikují jen ty popisovače, které chceme, ale všechny. Pokud nevíme nic o procesu, který naše aplikace spouští, koledujeme si o malér. Ten proces totiž může být nějaký malware, nebo jiná forma škodlivého zvířátka, které po svém spuštění udělá scan otevřených popisovačů. To není tak těžký, všechny mají IDčka někde kolem první stovky. Teoreticky se může dozvědět, jaké zdroje tyto popisovače představují. Mohou například manipulovat s ukazatelem pozice v souborech, které rodič čte a donutit rodiče udělat něco nepředvídatelného, nebo získat přístup k datům, ke kterým normálně nemusí mít práva.
A zatím co v růžovce může maximálně řádit doktor Mázl, tady může řádit někdo mnohem horší. Nejde o bezpečnostní díru, ale přímo o jámu. Co s tím?
Taky mě napadlo, že bych před spuštěním procesu provedl uzavření všech popisovačů kromě 0,1 a 2. Hmm, ale jak se dozvím, kolik jich je a jaké mají IDčka. Na jednom foru jsem se dozvěděl, že nějaká verze ssh uzavírá všechny popisovače od 3 do tuším 64 to spekulativně. Co kdyby tam něco bylo otevřeného. Taky řešení… Některé unixy oplývají funkci closefrom(3). Pokud ale chcete dát uživateli knihovny možnost vytvářet i další roury nad rámec prvních tří, pak neshledáte tuto funkci užitečnou.
Je to celkem nedávno, co v Linuxovém jádře zavedli příznak FD_CLOEXEC . Pokud tento příznak nastavíme přes fcntl, pak nám execve takový popisovač zavře. Takže je dobré si na to dát pozor a po vzniku jakéhokoliv popisovače (po otevření souboru, socketu, roury,…) ihned nastavit FD_CLOEXEC přes fcntl…
Dobrý nářez co? Zvedněte ruku, kdo tohle někdy řešil? Jo a to není všechno, pokud máte vlákna a zrovna tu smůlu, že mezi otevřením popisovače a nastavením příkazu jiné vlákno stihne udělat execve…? Postupně se objevují příkazy, které to umí v jednom kroku… třeba open(2) má příznak O_CLOEXEC. Nebo pipe2(2), nebo dup3(2). Mimochodem, dup nezduplikuje nastavení příznaku FD_CLOEXEC, takže výsledkem je opět popisovač, který lze ihned nasdílet do cizího procesu. A funkce dup3 nebyla k dispozici ještě na lennym, jak je to na sqeeze jsem nezkoušel.
bool Process::join(const Timeout &tm);
Tak vypadá prototyp funkce která počká na ukončení dítěte. Lze nastavit i timeout. Timeout? Nezbláznil jsem se? Přece funkce waitpid žádný timeout nemá… ?
Mimochodem, Windows rulez: WaitForSingleObject(hProcess,timeout);
No nic, vypadá to nějak takhle:
natural tmms = 1; int status; pid_t e = waitpid(ctx.processPid,&status, WNOHANG); while (e == 0) { if (tm.expired()) return false; Thread::sleep(tmms); //v milisekundách if (tmms < 500) tmms++; e = waitpid(ctx.processPid,&status, WNOHANG); }
Nebudu ani vysvětlovat co je ta omáčka kolem, princip je asi jasný. Jasně, že by asi šlo použít signál, ale tam je obtíž s tím, že vám SIGCHLD přiletí při každém ukončeném dítěti (nikoliv jen při jednom konkrétním) a v aplikaci složené z vláken bude ještě problém zjistit do kterého vlákna ten signál přiletí a kterou zrovna činnost to přeruší. Takový handlíř tohoto signálu bude asi superglobální objekt a bude muset znát všechny existující instance třídy Process, aby mohl příslušnou třídu informovat, že její proces skončil. Pěkně se to komplikuje.
… nechte si zajít chuť. To v Linuxu nejde. Jak vypadají init skripty?
Ve Windows? WaitForSingleObject(hProcess, timeout)
Spouštění a základní správa procesů v linuxu není zrovna jednoduchá záležitost. Je to prostě krásná ukázka toho, jak jednoduchou věc dělat složitě. Ano, jistě, stojí za tím nějaký historický vývoj, snaha udržet kompatibilitu s posixem a s původními unixy. Ale to se na tom podepisuje na každém kroku. Je otázkou, zda je nutné neustále držet historické API a ohýbat ho na všechny strany podle posledních trendů, nebo vytvářet nová API a ta stará pouze nějak emulovat.
Ja bych byl pro opustit archaismy POSIXu a konecne vytvorit nejake lepsi, pouzitelnejsi jaderne API. Zabyvam se debugovanim a muzu s klidem rict, ze napsat si vlastni debugger schopny pracovat s vlakny pod linuxem je dost desiva zalezitost... Dostat vlastni kod do existujici binarky je jednodussi nez debugovani, a nakonec jsem zanevrel na ptrace a misto toho vkladam 'debugger' do binarky pomoci LD_PRELOAD. Windows ma misto jednoho 'ptrace' mnoho specialnich funkci, nelimitovanych jednou formou volani. Naprikal ReadProcessMemory, ktere umoznuje cist pamet programu vicemene libovolne delky (pokud to nenarazi na necitelnou pamet / prazdne misto). Co ma linux? Pitomy ptrace, ktery umi precist *pouze* data velikost sizeof(int). A samozrejmne, sizeof(int) se meni v zavislosti na platforme, takze ten kod je nakonec plny #ifdefu...
Zabava na dlouhe zimni vecery a mnoho let :D
Divím se že nikdo nezmínil funkci popen ani v diskuzi. https://pubs.opengroup.org/onlinepubs/009696799/functions/popen.html. Ta navíc zaručí že lze z procesu číst jeho výstup
Jestli chces psat multiplatformne, tak vfork nepouzivej (respektive nespolehej na to ze se chova jinak nez fork). Jestli chces psat primo na linux, tak vfork taky nepouzivej a pouzij clone (zajimat te budou vlajky CLONE_FILES, CLONE_VFORK, CLONE_VM). Ta specialita fork+exec je PRAVE proto aby bylo mozne snadno vytvaret roury. A dokumentace closefrom ti jasne rika jak to muzes delat i bez ni. Pravda, taky si rikam, ze to trochu nedomysleli s tim forkovanim aplikaci co zabiraji mnozstvi pameti srovnatelne s celkovym mnozstvim pameti ...
A mimochodem, ta historicka API se drzi prave proto, ze pak jde psat multiplatformne. Tj. kdyz napises neco podle POSIXu, tak to chodi vsude (krome windows). Ano, napsat neco jenom na jeden system je vzdycky snazsi.
A co se tyka cekani na proces pusteny nekym jinym ... co je ti do toho co ten proces dela? Mas na to vubec prava? (Ano, je to neprijemne u procesu ktere jsou spustene tvym ditetem napriklad ...)
@2 Problém je, že si nejsem jist, jestli opravdu dneska je možné psát multiplatformě pro všechny linuxy, které jsou k mání. O psaní na další Unixy už ani neuvažuju. Proto jsem problém uchopil tak, že stavím své knihovny na základe předem daných user stories. V tomto případě je to požadavek na spouštění procesů, synchronizaci s nimi a základní komunikaci (roury). Je mi v celku jedno, co je na implementaci, jestli mám pro každou verzi linuxu vlastní komplet implentaci, nebo to mám v posixu, nebo prasácký zdroják plný ifdef endif
Na clone se podívám, ale pokud je vfork jen alias pro příkaz clone s určitými příznaky, tak by to mělo stačit.
Čekání na cizí proces, tyhle problémy jsou právě pěkně řešeny ve Windows a stačí se inspirovat. Pokud jeden proces chce čekat na jiný, pak musí jeho PID převést na interní HANDLE (rukojeť) pomocí funkce OpenProcess. Ve funkci uvádí PID procesu a požadovaná oprávnění. V případě čekání na ukončení stačí oprávnění SYNCHRONIZE. Pokud to oprávnění získá, obdrží process HANDLE, se kterým může jít do funkce WaitForSingleObject. Pokud získá process oprávnění číst exit code, tak si jej lze posléze vyzvednout přes GetExitCodeProcess. Co vím, tak lze takhle čekat i na process běžící pod UAC. Pokud to právo nezíská, nezíská ani HANDLE a pak zbývá jen analýza chyby GetLastError. Pokud je tam ERROR_ACCESS_DENIED, tak process běží, pokud je tam ERROR_NOT_FOUND tak už neběží. Ale tohle není moc spolehlivé, protože na uvolněné PID může být v následující milisekundu umístěn jiný process.
Schopnost synchronizovat se s cizím processem vidím jako důležitou vlastnost třeba u různých služeb či démonů, kdy chci ukončit process, který vlastní třeba nějaký zdroj, který ukončením uvolní.Pokud mě takový process poslechne, pak mám mít právo se dozvědět, že právě skončil.
Ještě se vrátím k tomu co jsem psal v @6, uvědomuju si, že Linuxák nemusí mít představu, jak to chodí ve Windows, tak to přeložím do linuxových reálií. Čekání na process by šlo realizovat nějakým syscallem, který by se jmenoval třeba int openProcess(int pid, int accessFlags). Na fd by se čekalo přes select, epoll nebo přes blokující read. Výsledkem čtení by byl exit code (bajt) a pak by se musel popisovač zavřít, protože další čtení by udělalo SIGPIPE.
Ahoj,
nemuzu si pomoct ale pokud se pohybujes kolem mozilly, toto prece pomerne elegantne resi knihovna NSPR (a multiplatforme):
http://www.mozilla.org/projects/nspr/reference/html/prprocess.html
https://developer.mozilla.org/en/NSPR_API_Reference
"To vůbec nehledejte u Windows, kde se popisovače ... HANDLE... sdílí a to takovým způsobem, že když rodič popisovač zavře, automaticky se zavře v dítěti a další operace s ním vedou na chybu "neplatný popisovač! Druhým rozdílem je, že když v Linuxu skončí dítě, zavřou se všechny jeho popisovače a roury spadnou do stavu SIGPIPE. Ve Windows zůstanou handly otevřené a čteni z rour bude viset v deadlocku!)"
Tomu, že když rodič zruší nějaké handle předané do dítěte, že se tato operace provede i v dítěti, moc nevěřím, ale nezkoušel jsem to. Ale ne že by rodič to handle v dítěti zrušit nemohl (Windows má na práci s handle "krásnou" funkci DuplicateHandle).
Jinak si u každého handle můžete nastavit, zda bude při vytváření dítěte kopírováno i do nového procesu či nikoliv (SetHandleInformation). CreateProcess má dokonce parametr, kde uvádíte, zda chcete dědit všechna handle či nikoliv.
Ve Windows je filozofie taková, že handle je něco na způsob nepřímého odkazu na daný objekt. Jádro si pamatuje počty existujících handle pro jednotlivé objekty. Když někdo zavře poslední handle nějaké entity, tento objekt je odstraněn z paměti (není to úplně přesně tak, ale dostatečně přesně).
To mimo jiné znamená, že pokud vytvoříte rouru (CreatePipe) a jeden její konec předáte dítěti a ono po nějakých výpočtech skončí, pořád máte v ruce svůj konec té roury a můžete jej používat. Pokud se z ní pokusíte číst, může to vést k zablokování, protože není pravděpodobné, že by si někdo jiný k té rouře otevřel handle a nějaká data do ní zapsal.
Mimochodem i Windows mají možnost provádět fork (na Windows XP jsem to zkoušel), ale musíte k tomu využít nedokumentovanou funkci NtCreateProcess (NtCreateProcessEx). Úspěsné spuštění procesu přes tuto funkci však vyžaduje hodně volání dalších méně známých rutin. Navíc konkrétní postup je závislý na verzi operačního systému a také na tom, zda daný proces používá GUI, takže si myslím, že krom pokusů jej asi nikdo nevyužije (vynecháme-li implementaci POSIX věcí pod Windows).
Co se týče čekání na ukončení procesu přes WaitForSingleObject: tahle vlastnost se mi velmi líbí. Dá se tak čekat i na ukončení vláken či dokončení operace nad souborem. Můžete také čekat na více objektů najednou (objektů různých druhů) a nechat se probudit, když jeden (či všechny) přejde do signálního stavu.
@11 No to jsem právě zkoušel, protože jsem psal i implementaci té třídy pro Windows. Je to tak, že pokud na HANDLE povolím "inherit", pak spuštěný proces neobdrží duplikáty těch handlů, ale přímo ty handly. Budou mít stejná čísla a opravdu, pakliže některá stran ten HANDLE uzavře, zavře se na obou stranách. Nejde tedy o duplikát, kde se vede čítač referenci, jde skutečně o sdílení, nebo dědění handlů
Tohle mi nadělalo docela komplikace. Pokud píšu komunikaci tak, že pustím process, nasypu do něj data a následně si přečtu výsledek až do EOFu, tedy na linuxu do stavu broken pipe... protože process skončil a svou stranu zavřel, ... tak na Windows se nikdy EOFu nedočkám. Protože skončený process nevlastnil duplikát, ale nasdílený HANDLE z parent procesu. Není možné, že bych tam měl chybu, protože to bylo první co mi to udělalo, zůstalo to na čtení v deadlocku. Napadlo mě tedy po spuštění process HANDLE pro druhou stranu u sebe zavřít a na druhé straně na mě vyskočila hláška "Neplatný popisovač". Tím jsem objevil to, že jde skutečně o sdílení HANDLŮ, nikoliv vytváření duplikátů.
Tyhle potíže vedou k tomu, že v mé knihovně mám několik specialit. Jednou z nich je, že otevření standardního IO se provádí vždycky duplikováním toho, co je vraceno přes GetStdHandle(), tak, aby to pak ta třída mohla bezpečně zavřít přes CloseHandle (a nezavřela si přitom konzoli celou). Na straně parenta je doporučení, že roury sdílené do druhého procesu je nutné zavírat až v okamžiku, kdy parent má jistotu, že si child všechny roury zduplikoval. U standardních I/O to řeším tak, že čtení z roury dělám přes třídu, která EOF kontroluje nejen přes uzavření roury druhou stranou, ale má k dispozici i handle procesu, který souběžně kontroluje. A pokud při čekání na data dojde k ukončení processu, vrátí do aplikace EOF. Je tam použit asynchroní roura přes OVERLAPPED a při čekání na data se čeká pomocí WaitForMultipleObjects.
On v tom Linux není sám, na Windows najdeme zase jiný extrabuřty
Vím, že to je jenom blog, ale určitě by neškodilo podívat se, co vlastně vfork dělá (zdrojové kódy máme) a už vůbec by nebylo od věci zmínit posix_spawn (http://pubs.opengroup.org/onlinepubs/009604499/functions/posix_spawn.html) a podívat se na jeho implementaci. Problém s memory overcommit, forkem atd. se obvykle řeší malou několikařádkovou utilitkou, která je spouštěna hned na začátku běhu našeho molocha. Ten v té době nemá mnoho nastránkován a tak se (obvykle) zadaří. Utilitka se potom kontroluje jakýmsi IPC (pipe, shmget, atd)
@11:
Chybou ERROR_INVALID_HANDLE bych se úplně neřídil. Spíš bych zkusil ověřit tu teorii o tom, že dědění != kopírování pomocí nástrojů jako je Process Explorer, který dovoluje pro proces zobrazit, která handle má otevřená. Windows v některých případech vrací podivné chybové kódy. A jak říkáte, mají také své různé bolístky.
Ono pokud se podíváme na implementaci těch handle... handle je ve skutečnosti index do tzv. tabulky handle, která zajišťuje překlad konkrétních hodnot handle na adresy příslušných objektů. Každý proces má vlastní tabulku handle. Z tohoto důvodu nevěřím, že by zrušení handle v dítěti znamenalo i zrušení handle v rodiči popř. v prarodiči a dále, pokud to dědění probíhalo delší dobu. Ve Windows může dojít k situaci, kdy proces A spustí proces B, který spustí proces C, přičemž proces B následně skončí; procesy A a C tedy dále existují, ale C nemá šanci se dozvědět, že jeho prarodičem je A. Jediné, co může zjistit, je PID jeho rodiče, které je ale už neplatné.
Ale to se zavíráním těch handle si vyzkouším. Můžete se mapsat, jak jste přesně postupoval, abychom každý nedělali něco jiného?
@14
1. Vytvořte rouru (CreatePipe)
2. jeden konec zduplikujte, tak aby se nastavilo sdílení (InheritHandle). Já to dělám s DUPLICATE_CLOSE_SOURCE, prostě jen kvůli tomu nastavení sdílení.
3. Nastavte tento konec do STARTUPINFO a zapněte příznak "UseStdHandles"
4. Vytvořte process CreateProcess
5. Zavřete ten konec předaný do nově vytvořeného procesu
6. Proveďte IO operaci na opačném konci podle směru.
7. Obdržíte chybu něco ve stylu "broken pipe" a to i v případě, že spuštěný proces bude ještě bežet.
8. Ve spuštěném procesu pokud handle vyzvednete přes GetStdHandle a provedete na něm IO, obdržíte ERROR_INVALID_HANDLE
Správnější postup by měl být, že na své straně ten druhý konec zavřete až v okamžiku, kdy si child process vyzvedne stdHandle a to zduplikuje. Ale to je šílenost. Druhé řešení je nezavírat ten konec, a při I/O s rourou si vždcky pošéfovat test, zda process ještě běží, aby to I/O bylo možno přerušit.
'Zvedněte ruku, kdo tohle někdy řešil?' --- zvedam ruku. A rozcarovan jsem z toho byl podobne, vcetne toho problemu jak se dozvedet ze selhal exec (to jsem nakonec nevyresil, jen pred forkem testuju ze dany soubor existuje, ale to neni idealni protoze ho nekdo mezitim muze smazat a taky kdyz na zacatku chce interpretr jako treba #/bin/bash a ten by nahodou neexistoval tak to velice zahadne selze --- v takovem pripade po forku uz exit()nu z celeho programu).
Mam nasledujici `API':
int run(char* direcory, int* filedes, char* command, ...)
kde directory je adersar ve kterem bude program spusten, filedes je pole deskriptoru, ktere budou zachovany (a remapovany na nejnizsi cisla), command je format string prikazu, kde se argumenty oddeluji specialnim znakem takze je mozne napsat treba
run(".", (int[]){0,1,2, pd[1],-1}, "prog01%s01%s", arg1, arg2);
a ono to spusti prog arg1 arg2 propojeny s mym programem pres pd[1] (on ho ma na deskriptoru
3) a zbytek ma obvykly stdin/stdout.
Delal jsem to pomoci /proc/PID/fd, coz je adresar ve kterem jsou vsechny otevrene deskriptory videt jako soubory, takze je mozne je postupne probrat a pozavirat, ty ktere nechci zavrit pred tim zduplikovat a nakonec je zduplikovat na zacatek (protoze ty puvodni jsou zavrene) a docasnych duplikatu se zbavit. Ale ma to taky nevyhodu, pokud ma spusteny proces mode 711 a je vlastnen rootem a ten kdo ho spusti je non-root tak tento adresar nejde precist.
@16:
Pokusil jsem se převést ten postup do kódu, viz: http://pastebin.com/aVGpjUNY.
Bohužel se s žádnými chybami nesetkávám. I když proces-dítě chvíli počká, aby bylo jisté, že bude z roury číst až po zrušení čtecího handle v rodiči, čtení se mu v pohodě podaří. Tady byste asi mohl namítnout, že to handle na tu rouru v dítěti si zduplikuje standardní knihovna ještě před tím Sleep() v rutině Child(). Kdyby tomu ale tak bylo, toto handle by nemělo stejnou hodnotu jako v rodiči.
Samozřejmě je také možné, že jsem ten postup nechtěně "upravil" a testuji něco jiného.
Pro otestování stačí provést: pipetest --parent
@21 Taky to teď nějak neumím reprodukovat. Možná jsem tam měl někde chybu, je fakt, že první pokusy s rourama jsem začínal na WIndows 2000, kde mi to nefungovalo dobře a dokonce jsem několikrát řešil problém, kdy mi CloseHandle na HANDLE roury zůstalo v deadlocku. Sám netuším, jak je to možné. Od té doby jsem kód několikrát překopával, vždy v tom duchu, aby handle druhé strany zůstávalo otevřene po čas běhu procesu a hlídal jsem si ten process v jiném vlákně, nebo během čekání na overlapped operaci. No a pak jsem ten kód překopal do třídy Process, kde tedy funguje v původním návrhu. Vedle jsem si teď udělal podobný example, tak jak píšete vy, a funguje mi to taky, jak tvrdíte Vy, a zaživýho boha se mi nedaří zreprodukovat původní chování. V článku to tedy škrtnu, jako neověřitelnou informaci.
@22:
Jediná teorie, kterou mám, je, že se vám podařilo to handle v rodičovi zavřít dříve, než bylo zděděno do potomka. Ale moc nevěřím tomu, že by k dědění handle docházelo až po návratu z CreateProcess, to mi přijde dost nebezpečné.
Jinak CloseHandle myslím při zadání špatného handle na zrušení vyhazuje výjimku STATUS_INVALID_HANDLE, což mohla teoreticky být příčina toho vašeho "deadlocku." Ale na Windows 2000 jsem už dlouho nic nepouštěl, takže nevím.
Každopádně díky moc za diskuzi.
Jak na emulaci WaitForSingleObject, tak na zjišťování návratové hodnoty pro CreateProcess používám ptrace (umí sledovat jakákoliv systémová volání, změny v paměti a spoustu dalšího). Má ale jednu nevýhodu — je problém mít víc procesů čekajících na stejný objekt. Tam už je pak lepší použít nějakého démona obalujícícho ptrace nebo jaderná volání (třeba systrace).
Co se týče vláken u vfork, tak se zablokuje jenom to vlákno, které dělá vfork. Vlákna jsou totiž v POSIXu brána jako samostatné procesy se sdíleným paměťovým prostorem.
Inu, takhle to vypadá, když fork/exec a spol. používá někdo, kdo je zvyklý na windowsové myšlení :-)
POSIX obsahuje samozřejmě i spoustu historického balastu, ale není těžké si vybrat podmnožinu, kterou všechny moderní UNIXy ovládají a která se používá příjemně. A pokud stačí, aby program běžel na Linuxu, je vyhráno. Například čekání na doběhnutí procesu v kombinaci s ostatními událostmi lze velice příjemně zvládnout kombinací signalfd() + epoll().
@26 většinou to není třeba. Nejčastěji spustím process, s rourou a nasypu do roury data a z druhé strany si přečtu výsledek a pak ten process "joinu", což se v linuxu dělá přes wait. Málokdy potřebuju čekat na různé procesy, když už, tak většinou na všechny naráz a pak mi stačí je joinovat postupně. Pokud už to mám hodně asynchroní, kde si procesy chodí jak chtějí, mám tam vlákna.
Ale to je zase linuxový myšlení, že musím čekat na dokončení procesů, aby nevznikaly zombie (ve Win to nehrozí). Ale já přece nemusím, ať si tam ty zombíci někde visej, až se k těm procesům dostanu, tak je sesbírám
Návratový kód z exec může prozradit, co selhalo. Třeba práva, nebo chybná cesta, nebo jiná blbost. Nestává se mi často, že by se process nepodařilo zinicializovat. Opravdu spíš, že tam nějaký jouda napíše blbě cestu.
@28 Čekat na dokončení procesů není potřeba. Pokud nastavíte, že signál SIGCHLD je ignorován, zombie nevznikají.
Bývá zvykem selhání execu zalogovat do stderr, takže jde poznat, co se stalo. Pokud opravdu potřebujete, aby se o tom dozvěděl rodič, stačí, když předá rouru s close-on-exec, a po neúspěšném execu do ní zapíšete, co se stalo.
@30 Pokud vás zajímá návratový kód, tak prostě nějakou formu asynchronního doručení zprávy použít musíte. Ale mimo wait() existují i jiné možnosti, například si pomocí sigaction() objednat doručení signálu SIGCHLD včetně siginfa, ve kterém je napsáno, jaký proces skončil a jak dopadl.
@31 já mám na téma signály rozmyšlen další lehce kriticky laděný článek, takže moc z toho nadšený nejsem. Obecně všechny mé linuxové aplikace (resp. aplikace portované do Linux/Windows) automaticky po startu maskují většinu signálů kromě SIGTERM a takových těch od procesoru, jako SIGSEG, SIGILL, a tak. Pokud některá komponenta potřebuje signály, tak si je musí odmaskovat. Takové ty běžné signály u démonů nepoužívám, pro komunikaci mám většinou nějakou formu roury, nebo sdílené paměti se semafory a vyzvedávám si to synchroně (jako je to ve Windows)
@32 I se signály jde velmi příjemně pracovat synchronně, viz signalfd(). Existují i jiné možnosti, například otevřít si rouru a nechat do ní signal handlery zapisovat a hlavní smyčku číst.
IMHO kriticky laděné články by člověk měl psát poté, co si důkladně nastuduje problém i s kontextem okolo, nikoliv v situaci, kdy se teprve rozkoukává po příchodu z jiné kultury a všechno, co je jinak, mu přijde divné :-) Tehdy spíš riskuje, že se ztrapní.
@33 S linuxem pracuju přes 5 let v C++. Ale protože všechny vyvíjím na obě platformy současně, musím si z obou platforem vybírat to nejjednodušší, co jde u obou udělat zhruba stejně, nebo aspoň tak, aby se to z venku tvářilo jako jednotné API. Signály mají smysl jen asynchroní, jinak ve srovnání s message queue ve Windows je to slabota... ale nechte se překvapit.
Signalfd() samozřejmě znám. Přestal jsem ho používat po tom, mi to na některých distribucích (debian lenny) vykopl překladač, že to nezná.
FYI: pohodlnější spouštění procesů umí knihovna Folly:
https://github.com/facebook/folly/blob/master/folly/Subprocess.h
Intenzivně se zabývám programováním zejména v jazyce C++. Vyvíjím vlastní knihovny, vzory, techniky, používám šablony, to vše proto, aby se mi usnadnil život při návrhu aplikací. Pracoval jsem jako programátor ve společnosti Seznam.cz. Nyní jsem se usadil v jednom startupu, kde vyvíjím serverové komponenty a informační systémy v C++
Přečteno 51 063×
Přečteno 23 939×
Přečteno 22 871×
Přečteno 20 952×
Přečteno 17 760×