Procesy w PHP

Procesy od podstaw

Od tego miejsca tego artykułu, zamierzam wytłumaczyć zasadę działania klasy, którą znaleźć można powyżej. Część tą poświęcam ludziom, którym efekt nie wystarcza i chcą poznać, jak to wszystko działa. Spróbuję wszystko opisać jak najprościej, aczkolwiek nie mogę zapewnić, że pewna wiedza na temat działania systemów operacyjnych nie będzie potrzebna, aby to wszystko zrozumieć. Aby łatwiej Ci było zagłębić się w temat, wszystkie ważne funkcje będę linkował do manuala PHP, abyś podczas swojej zabawy z procesami w tymże języku, miał wszystko „pod ręką”, dzięki czemu łatwiej będzie Ci cokolwiek napisać.

Trochę teorii: zasada działania

Wszystko „opiera się” praktycznie rzecz biorąc na jednej funkcji, pcntl_fork(). To ją właśnie musimy wykonać jako pierwszą.  Jest to miejsce, gdzie „rozgałęziamy” Nasz proces główny. Podstawowym problemem jest zrozumienie tego, co się dzieje później, a mianowicie: jaki kod zostanie wykonany później? Spieszę z pomocą i już tłumaczę: wyobraźmy sobie Nasz proces jako drzewo. Pień jest Naszym procesem głównym, a z niego wyrastają gałęzie. Pień, czyli Nasz „rodzic” będzie dalej się wykonywał, a jedynie powstanie równoległa do niego gałąź, która pozornie będzie taka sama. Taka sama, gdyż ten sam kod zostanie przekazany do obydwu procesów. Pozornie, ponieważ w zależności od tego, co Nam zwróci funkcja pcntl_fork(), możemy odpowiednio pokierować nim tak, aby wykonał to, co chcemy. Dzięki prostym instrukcjom warunkowym, możemy oddzielić część kodu dla jednego procesu od części dla drugiego. Niestety, do pamięci i tak zostanie załadowany cały kod, mimo widocznego dla Nas rozdzielenia. Użyć będzie trzeba również funkcji pcntl_waitpid(), która zapobiegnie tworzeniu procesów zombie, lecz o tym opowiem za chwilę.

Trochę praktyki

Po pewnej ilości słów teorii, wypadałoby przejść do praktyki, gdyż bez niej, najzwyczajniej w świecie, nic nie zrobimy. Myślę, że najlepszym sposobem na opisanie głównych zasad w kodzie będzie umieszczenie w nim komentarzy, gdyż będzie to najbardziej obrazowy sposób na zrozumienie tego, co napisałem wyżej:

echo "Włączam skrypt - rodzic\n";
$pid = pcntl_fork(); // "Rozgałęziamy" proces
// W tym miejscu mamy już dwa procesy: główny i potomny
if($pid == 0) { // pcntl_fork() zwraca 0, jeżeli kod aktualnie wykonuje się w dziecku
	echo "Zgłaszam się - dziecko\n";
} elseif($pid > 0) { // pcntl_fork() zwraca ID procesu potomnego, jeżeli udało się go utworzyć
	echo "Zgłaszam się - rodzic\n";
} else { // W innym przypadku nie udało się utworzyć nowego procesu - może to być np. limit systemu, jesteśmy wtedy w jedynym istniejącym procesie, czyli rzekomym rodzicu
	echo "Nie udało się utworzyć podprocesu\n";
}
echo "Zgłaszam się - dziecko lub rodzic\n";
if($pid == 0) { // Kod tylko dla dziecka(procesu potomnego)
	echo "Kończę działanie - dziecko\n";
	exit(); // Kończymy proces potomny
}
pcntl_wait($status); // Rodzic czeka na zakończenie działania dziecka i odbiera jego status
// Tutaj istnieje już tylko rodzic
echo "Kończę działanie - rodzic\n";

Niestety, jeżeli mimo komentarzy od razu nie zrozumiałeś, musisz poświęcić na to jeszcze chwilkę, aby zrobić to ponownie. Próbowałem to bardziej zobrazować, choćby na rysunku, ale po uwzględnieniu wszystkich rzeczy, staje się on nieczytelny i jeszcze bardziej niezrozumiały.

Pobieranie ID procesu

Funkcja pcntl_fork() w przypadku dziecka, zwraca Nam wartość równą zeru. Co gdybyśmy chcieli pobrać ID procesu potomnego w nim samym? Nic prostszego, gdyż PHP oferuje Nam funkcję getmypid(), która zwróci interesującą Nas liczbę.

„Procesy zombie”

Przed wyjaśnieniem, czym są procesy zombie, chciałbym przedstawić Ci po krótce zasadę współdziałania procesu rodzica z procesem dziecka. Najważniejszą rzeczą jest to, że proces potomny jest ciągle przypisany do procesu nadrzędnego. Jeżeli nadrzędny skończy swoją pracę, procesy potomne również będą zmuszone przerwać swoją pracę. Procesy potomne zostają w tablicy procesów systemowych, dopóki ich proces nadrzędny nie odczyta ich statusu. W praktyce już nie istnieją, ale niestety musimy sprawdzić ich stan, aby wyczyścić z nich tablicę systemową. Takimi właśnie procesami są procesy zombie(zakończyły swoją pracę, ale nadal są w tablicy procesów). Jest to wielki problem, ponieważ w przypadku, kiedy Nasza aplikacja działa „24/7″, może pozostawić w tablicy procesów tysiące takich wpisów. Tablica niestety nie jest nieograniczona, więc jej przepełnienie(podobno jest to kilkanaście tysięcy procesów) powoduje destabilizacją systemu, na którym jest uruchomiona. O skutkach lepiej nie wspominać, jeżeli mamy na niej jakieś ważne dane. Teoretycznie wystarczy wykonać funkcję pcntl_waitpid(), lecz tutaj natrafiamy na kolejny problem: proces nadrzędny zostaje przez nią wstrzymany aż do czasu zakończenia pracy procesu podrzędnego. Powoduje to, że korzystanie z procesów traci sens, bo i tak tylko jedna rzecz może działać w jednym momencie. Na szczęście i z tą niedogodnością można sobie poradzić. Moim sposobem na to, jest wywołanie w wierszu poleceń komendy wypisania wszystkich procesów, wycięcie z niej tylko procesów utworzonych przez aktualny proces, a potem jeszcze pozostaje wybrać z nich te, które zakończyły swoją pracę. Procesy zombie są oznaczone jako defunct, więc nie jest to trudne do wykonania. Wystarczy trochę pokombinować i dochodzimy do takiego polecenia:

ps -fe | grep "defunct" | awk '$3 == ID_Rodzica { print $2 }'

Po jego wykonaniu, do PHP zostanie zwrócona tablica z ID procesów zombie danego rodzica. Wystarczy na nich wykonać wcześniej wspomnianą funkcję pcntl_waitpid(), gdyż tym razem od razu zwróci Nam status procesu, dzięki czemu praktycznie nie stracimy czasu na to, aby obsłużyć status procesów podrzędnych. Po „opakowaniu” tego w kod PHP, wyglądać to będzie tak:

exec('ps -fe | grep "defunct" | awk \'$3 == '.getmypid().' { print $2 }\'', $pids);
foreach($pids as $pid) {
	pcntl_waitpid($pid, $status);
}

Powyższy kod oczywiście wykonuje podane wcześniej polecenie w tak zwanym shellu(linii poleceń), a potem na podstawie pobranej tablicy z ID procesów zombie, wykonuje funkcję pcntl_waitpid() z odpowiednim argumentem. Proces podrzędny zakończył swoją pracę, więc wynik w postaci statusu zostanie zwrócony momentalnie.

„Odłączanie się” od procesu nadrzędnego, który uruchomił Naszą aplikację PHP

Domyślnie, podczas uruchamiania Naszej aplikacji napisanej w PHP, staje się ona procesem podrzędnym do procesu terminala(konsoli), w którym wpisywaliśmy polecenie jej uruchomienia. Nie jest to zbyt dobre rozwiązanie, jeżeli chcemy, aby Nasz proces działał w trybie „daemona”, czyli po prostu, aby działał bez przerwy, w tak zwanym „tle”. Aby temu zapobiec, na początku Naszego skryptu musimy wykonać funkcję posix_setsid(). Sprawi to, że proces Naszego skryptu zostanie oznaczony, jako dziecko głównego procesu systemowego(np. initd).

Zakończenie

Na zakończenie tego artykułu, chciałbym prosić o wykorzystywanie procesów w PHP z rozwagą, gdyż często lepiej jest napisać daną aplikację w innym języku. Nie nakłaniam do tego bez powodu: po prostu PHP nie jest dopasowane do tego typu rozwiązań i mogą się natrafić problemy z ciągłym przyrostem pamięci, co może być poważnym problemem w przypadku skryptów działających „24/7″.

Za ten artykuł podziękowano 1 raz(y). Chcesz i Ty ?

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

*

Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">