Procesy w PHP

Data dodania:
05 kwietnia 10
Autor:
Łukasz Rutkowski
Kategoria:
Nasza twórczość, PHP

Pomimo tego, że PHP zazwyczaj wykorzystuje się do tworzenia aplikacji WWW, można go też używać do programowania skryptów, wykonujących się w konsoli(“CLI” – ang. Command-Line Interface). Niestety, jest pod tym względem dosyć słabo rozwinięte, aczkolwiek i tak można z jego pomocą napisać wiele użytecznych rzeczy. Jednym z usprawnień, którego brakuje, jest prosta obsługa podprocesów lub wątków. Bez tego nie stworzymy choćby prostego serwera opartego na gniazdach(ang. socket), który byłby w stanie obsłużyć kilku klientów jednocześnie. Nie jest to najoptymalniejsze rozwiązanie, wręcz lepiej robić takie rzeczy w językach do tego przeznaczonych(np. w C++), ale jednak czasem może przeważyć oszczędność czasu i mała znajomość języków programowania innych niż PHP. Właśnie z myślą o takich przypadkach, lub o takich, w których optymalność nie gra zbyt dużej roli, napisałem ten artykuł.

UWAGA! Jeżeli chcemy, aby skrypt działał bez przerwy, powinniśmy najpierw przetestować, czy dane działania jesteśmy w stanie zapętlić w nieskończoność. PHP ma to do siebie, że w pewnym momencie może wystąpić błąd dojścia do limitu pamięci, który oczywiście można zwiększyć, ale jest to tylko czasowe ominięcie problemu, a nie jego całkowite rozwiązanie. Największe kłopoty mogą wystąpić przy edycji oraz generowaniu plików graficznych, ponieważ mimo teoretycznego usuwania ich z pamięci, potrafią się “rozpychać” bez umiaru.

Wybór metody

Do wyboru w tym artykule mamy dwie opcje: albo samemu wszystko zrobić “ręcznie”, albo skorzystać z mojej klasy, która nie dość, że powiększy czytelność kodu, to przyspieszy i ułatwi jego tworzenie. Zacznę od wariantu drugiego, gdyż podejrzewam, że możliwe jest, abyś właśnie szukał rozwiązania dla projektu, nad którym pracujesz i potrzebujesz czegoś łatwego i przyjemnego, niewymagającego potrzeby zrozumienia zasady działania całej operacji “od wewnątrz”. Jeżeli masz trochę więcej czasu, polecam zapoznać się też z podpunktem “Procesy od podstaw”, w którym postaram się wytłumaczyć wszystko od podszewki.

Moja klasa do obsługi procesów


Kod klasy:

/*
Author: Łukasz "Rutek" Rutkowski
License: Creative Commons - Attribution 3.0 Poland
License URI: http://creativecommons.org/licenses/by/3.0/
Full license URI: http://creativecommons.org/licenses/by/3.0/legalcode
*/
class ProcessException extends Exception {}
class Process {
	// Init process with if statement
	public static function start() {
		$pid = pcntl_fork();
		if($pid == -1) {
			throw new ProcessException('Could not create new process');
		}
		if($pid != 0) {
			return FALSE;
		} else {
			return TRUE;
		}
	}
 
	// Kill present process
	public static function end() {
		exit();
	}
 
	// Unattach from parent process
	public static function detach() {
		$done = posix_setsid();
		if($done == -1) {
			throw new ProcessException('Could not detach from parent');
		}
	}
 
	// Get present process PID
	public static function getPID() {
		return getmypid();
	}
 
	// Kill all zombies from parent process
	public static function killZombies() {
		$parentPID = Process::getPID();
		exec('ps -fe | grep "defunct" | awk \'$3 == '.$parentPID.' { print $2 }\'', $pids);
		foreach($pids as $pid) {
			pcntl_waitpid($pid, $status);
		}
	}
 
	// Kill all children processes with(or without) parent
	// $no == TRUE : do not kill present process
	public static function killParentWithChildren($parentPID, $no=FALSE) {
		exec('ps -ef | awk \'$3 == \''.$parentPID.'\' { print  $2 }\'', $buff);
		foreach($buff as $proc) {
			if($proc != $parentPID) {
				self::killParentWithChildren($proc);
			}
		}
		if($no == FALSE) {
			posix_kill($parentPID, 9);
		}
	}	
 
	// Kill present's process children
	public static function killChildren() {
		$parentPID = self::getPID();
		self::KillParentWithChildren($parentPID, TRUE);
	}
}

Uwaga! Powyższy kod działa jedynie na platformie GNU/Linux.

Przykład najprostszy

Użycie powyższej klasy na przykładzie wyświetlenia tekstu z innego procesu(nieużyteczne, ale chodzi o przedstawienie sposobu użycia):

echo "Rozpoczynamy skrypt\n";
if(Process::start()) {
	echo "Przemówiło proces potomny\n";
	Process::end();
}
echo "Przemówił proces nadrzędny\n";
echo "Kończę skrypt";

Jak widać, wszystko w najlepszym przypadku sprowadza się do użycia dwóch metod statycznych: start() i end(). Pierwsza oczywiście tworzy proces i w przypadku, kiedy kod zostanie wykonany w procesie potomnym, zwraca wartość true(ang. prawda), dzięki czemu wszystko zawarte w warunku, wykona się tylko w dziecku(skrypcie potomnym). Dla procesu nadrzędnego nie ustanawiamy żadnego warunku, ponieważ nowoutworzony kończymy na samym końcu kodu, w instrukcji warunkowej.

“Odcinanie się” od konsoli/procesu wykonującego kod PHP

Standardowo proces nadrzędny jest procesem potomnym konsoli, w której wykonaliśmy polecenie włączenia skryptu lub innego procesu tego typu. Jeżeli chcemy, aby skrypt działał “w tle”(jako tak zwany daemon), powinniśmy sprawić, aby procesem nadrzędnym dla Naszego procesu PHP był główny proces systemowy. Korzystając z mojego kodu, wystarczy na samym początku kodu wykonać poniższą instrukcję:

Process::unattach();

Spowoduje ona, że wszystkie podprocesy będą przypisane do głównego procesu systemowego. Jeżeli chcemy, aby cały skrypt działał na tej zasadzie, wystarczy umieścić go w nowym procesie:

Process::unattach();
if(Process::start()) {
	// Kod całego skryptu
}

Pobieranie ID procesu

Czasem do różnych czynności potrzebujemy ID procesu, choćby po to, aby móc zidentyfikować, który aktualnie proces obsługuje Naszego klienta. Aby pobrać ID aktualnego procesu, wystarczy wykonać poniższą metodę i jej wartość przypisać do dowolnej zmiennej:

$mojeID = Process::getPID();

Procesy zombie

W momencie, kiedy Nasz skrypt działa bez przerwy i kolejne podprocesy tworzymy np. w pętli, a ich proces nadrzędny nie jest nigdy zamykany, powstają tak zwane procesy zombie. Są to procesy, które zakończyły swoje działanie, ale system czeka z ich usunięciem z tablicy procesów do momentu, aż rodzic(proces nadrzędny) zauważy ten fakt, lub sam się zakończy. Przestrzegam przed tym problemem, ponieważ można poważnie zaszkodzić stabilności systemu, na którym włączymy taki skrypt i po godzinie pracy powstanie kilkanaście tysięcy procesów zombie. W pewnym momencie miejsce w tablicy procesów skończy się, co może zakończyć się błędem każdej z aplikacji. Z tego powodu do mojej klasy dopisałem metodę, która pobiera z tablicy procesów wszelkie procesy zombie dla danego rodzica i je usuwa. Najlepiej zobrazować sobie sposób jej używania na poniższym przykładzie:

if(Process::start()) {
	while(TRUE) {
		// Rodzic
		if(Process::start()) {
			// Dziecko
			Process::end();
		}
		// Rodzic
		Process::killZombies();
	}
	Process::end();
}

Zamykanie wszelkich procesów potomnych aktualnego rodzica

Aby “zabić” wszystkie procesy potomne w rodzicu, wystarczy wywołać metodę Process::killChildren(), bez żadnych parametrów, gdyż wszystkie dane potrzebne do wykonania operacji, zostaną automatycznie pobrane. Przykład:

while($i < 5) {
	// Rodzic
	if(Process::start()) {
		while(TRUE) {
			sleep(1);
		} // Proces podrzędny ma działać ciągle
		// Dziecko
		Process::end();
	}
}
// Rodzic
Process::killChildren(); // Zabij wszystkie procesy potomne

Zamykanie wszelkich procesów potomnych określonego rodzica

Bywa i tak, że chcemy zamknąć procesy potomne jakiegoś innego procesu, lecz z jakiegoś powodu nie możemy ich zamknąć wewnątrz niego. W takim przypadku, musimy najpierw z jakiegoś miejsca pobrać ID procesu rodzica i wykonać metodę Process::killParentWithChildren() z pierwszym argumentem równym PID(ID procesu) rodzica, a drugim równoznacznym z wartością logiczną TRUE.

Process::killParentWithChildren($pid, TRUE);

Zamykanie procesu razem z jego procesami potomnymi

Robimy to tak, jak wyżej, tyle że drugi argument zamieniamy na wartość TRUE, co spowoduje również zamknięcie procesu, którego ID podaliśmy.

Process::killParentWithChildren($pid, FALSE);

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″.

~Łukasz Rutkowski