La gestion multi-processus dans PHP avec PCNTL et POSIX
La gestion des processus dans PHP implémente le style Unix concernant la création de processus, l’exécution du programme, la gestion des signaux et la terminaison du processus. La contrôle de processus ne doit pas être activé dans un environnement “serveur web” pour éviter des résultats inattendus.
Ceci est possible en utilisant PCNTL, une extension qui devient disponible en activant l’option –enable-pcntl lors de la compilation de PHP.
Dans ce qui suit, je vais supposer que vous êtes a l’aise avec les notions théoriques de gestion, d’ordonnancement, de supervision de processus et des signaux. Je vais essayer de couvrir les mécanismes de création, de gestion et de terminaison de processus.
Sommaire
La création de processus
La création d’un processus se fait via la fonction pcntl_fork(), qui copie l’exécution du programme a un processus fils, le contexte de travail peut alors être distribué entre le processus père et le processus fils via l’identifiant de processus (PID).
Il est alors possible en créant plusieurs fils de créer des exécutions parallèles. Il est aussi possible d’utiliser une ressource partagée (un fichier, un tableau, une base de données etc.) entre ces processus.
Création de processus
$pid = pcntl_fork();
if ($pid == -1)
{
die('Creation de processus impossible');
}
elseif ($pid)
{
echo "[pere] Je suis le pere, mon processus fils a un PID $pid".PHP_EOL;
echo "[pere] J'attends la mort de mon processus fils :D ".PHP_EOL;
pcntl_wait($status);
echo "[pere] Mon fils est mort! Bye!".PHP_EOL;
}
else
{
for($i=0; $i<10; $i++)
{
echo "[fils] Je suis le fils, c'est l'iteration $i, je v dormir pour une seconde! ...".PHP_EOL;
sleep(1);
}
echo "[fils] Woups! je meurs!".PHP_EOL;
exit;
}
Création de plusieurs processus
for($i=0;$i<10;$i++)
{
$pid = pcntl_fork();
if ($pid == -1)
{
die('Creation de processus impossible');
}
elseif ($pid == 0)
{
echo "[fils] Fils $i travaille".PHP_EOL;
sleep(1);
echo "[fils] Fils $i finit son execution".PHP_EOL;
exit;
}
else
{
echo "[pere] Debut du pere $i".PHP_EOL;
pcntl_wait($status);
echo "[pere] fin du pere $i".PHP_EOL;
}
}
On peut aussi créer des processus multiples sans avoir a faire un wait. Exemple:
echo "[pere] Debut du pere".PHP_EOL;
for ($i=0; $i<10; $i++)
{
$pid = pcntl_fork();
if ($pid == -1)
{
die('Impossible de creer un processus');
}
elseif ($pid == 0)
{
echo "[fils] Fils $i Travaille".PHP_EOL;
sleep($i);
echo "[fils] Fils $i Finit son execution".PHP_EOL;
exit;
}
}
echo "[pere] Fin du pere".PHP_EOL;
exit;
Les signaux
On peut définir un signal Unix comme une forme limitée de communication entre processus. Un signal représente une notification asynchrone qui est envoyée a un processus afin de le notifier d'un évènement. Quand un signal est envoyé au processus, le système d'exploitation interrompt le flux normal d’exécution. l’exécution peut être interrompue durant n'importe quelle instruction. Si le processus aurait déjà enregistré un gestionnaire de signaux, cette routine sera exécuté, sinon le gestionnaire par defaut sera exécuté a la place.
Quelques signaux
SIGTERM
Le signal SIGTERM est utilisé pour terminer l’exécution d'un processus. il est plus "propre" d'utiliser SIGTERM que SIGKILL puisque le premier permet de nettoyer le contexte du processus avant sa terminaison. SIGTERM est déclenché par un "kill" (et pas via un kill -9)
SIGINT
SIGINT est utilisé pour terminer l’exécution d'un processus via une interruption. il est déclenchée via CTRL+C.
Exemple d'utilisation de SIGTERM et SIGINT :
declare(ticks = 1);
function sig_handler($signo)
{
switch ($signo)
{
case SIGTERM:
echo "SIGTERM" . PHP_EOL;
exit();
break;
case SIGINT:
echo "SIGINT" . PHP_EOL;
exit();
break;
}
}
pcntl_signal(SIGTERM, "sig_handler");
pcntl_signal(SIGINT, "sig_handler");
sleep(50);
Pour plus d'infos sur la fonction declare referez vous a http://www.php.net/declare.
SIGCHLD
SIGCHLD est envoyé au père si le processus fils est terminé. C'est une technique commune lors d'utilisation de la technique fork. Par defaut, SIGCHLD est ignoré et un processus zombie est créé. Pour éviter ce comportement, il faut s'assurer d'utiliser SIGCHLD uniquement lorsque le père fait un wait.
Exemple
declare(ticks = 1);
$max=5;
$parallel=0;
$childId=0;
function sig_handler($signo)
{
global $parallel,$childId;
switch ($signo)
{
case SIGCHLD:
$parallel--;
echo "SIGCHLD recu pour le fils #$childId" . PHP_EOL;
echo "Decrementation a $parallel".PHP_EOL;
break;
}
}
pcntl_signal(SIGCHLD, "sig_handler");
for ($childId=0; $childId < 10; $childId++)
{
$pid=pcntl_fork();
if ($pid == -1)
{
die("Impossible de creer le processus");
}
elseif ($pid)
{
if ( $parallel >= $max )
{
pcntl_wait($status);
}
else
{
$parallel++;
echo "Incrementation a $parallel".PHP_EOL;
}
}
else
{
echo "Creation d'un nouveau fils : #$childId ".PHP_EOL;
echo "Nous avons maintenant $parallel processus fils en parallele".PHP_EOL;
sleep(rand(2,4));
exit;
}
}
Les fonctions POSIX
On a vu comment creer des processus. Voyons maintenant comment les terminer. ceci est possible avec les fonctions POSIX qui permettent d'interagir avec l'interface POSIX du système d'exploitation. Les fonctions POSIX sont utiles pour determiner les informations sur les pid de processus ainsi que pour les terminer.
Récupérer les infos sur un processus
- posix_getpid() : retourne l'id du processus Courant. Notez que la valeur de retour de pcntl_fork() pour le processus parent est la même que celle de posix_getpid() pour le processus fils.
- posix_getppid() : retourne le pid du parent du processus courant(). Notez que la valeur de retour de posix_getpid() pour le processus parent est la même que celle de posix_getppid() pour le processus père. La valeur de retour de posix_getppid() pour le processus parent est celle de la session shell.
Exemple :
echo "[avant fork] PID POSIX : ". posix_getpid() . ", PID du pere: " . posix_getppid() . PHP_EOL;
for ($i=0; $i < 3; $i++)
{
$pid = pcntl_fork();
if ($pid == -1)
{
die('could not fork');
}
elseif ($pid == 0)
{
echo "[fils] ID: $i, PID: " . posix_getpid() . ", PID du pere: ".posix_getppid().PHP_EOL;
exit;
}
else
{
echo "[fils] PID : " . posix_getpid() . ", PID du pere: " . posix_getppid() . PHP_EOL;
pcntl_wait($status);
}
}
Terminer l’exécution d'un processus
La fonction posix_kill() permet d'envoyer des signaux a un processus
- posix_kill($pid,0) retourne le status, 0 si le pid existe, false sinon
- posix_kill($pid,9) (ou posix_kill($pid,SIGKILL)), posix_kill($pid,15) (ou posix_kill($pid,SIGTERM)) termine l’exécution du processus
Exemple :
$pid = pcntl_fork();
if ($pid == -1)
{
die("Impossible de creer le processus");
}
elseif ($pid)
{
$exists = posix_kill($pid,0) ? 'existe encore' : 'n\'existe plus';
echo "[pere] Le processus fils $pid $exists" . PHP_EOL;
echo "[pere] Terminaison du processus fils $pid" . PHP_EOL;
posix_kill($pid, SIGKILL);
echo "[pere] Le processus fils $pid est mort :D" . PHP_EOL;
pcntl_wait($status);
$exists = posix_kill($pid,0) ? 'existe encore' : 'n\'existe plus';
echo "[pere] Le processus fils $pid $exists" . PHP_EOL;
}
else
{
while(true)
{
sleep(50);
}
exit;
}
Conclusion
Lorsque la situation le nécessite, il est tres intéressant d'utiliser le mécanisme des multi processus dans vos applications PHP. Il faut cependant etre conscient de la question de performance quand on crée plusieurs processus fils. Il faut aussi utiliser PCNTL uniquement avec PHP CLI.
Assurez vous aussi de bien maîtriser et controler les processus que vous créez afin d’éviter d'avoir des processus zombies. Il faut être toujours en mesure de pouvoir tuer vos processus fils et, d'en tuer que les votres.