C Programmation

Votre premier programme C utilisant l'appel système Fork

Votre premier programme C utilisant l'appel système Fork
Par défaut, les programmes C n'ont pas de concurrence ou de parallélisme, une seule tâche se produit à la fois, chaque ligne de code est lue séquentiellement. Mais parfois, vous devez lire un fichier ou - pire encore - une prise connectée à un ordinateur distant et cela prend vraiment beaucoup de temps pour un ordinateur. Cela prend généralement moins d'une seconde, mais rappelez-vous qu'un seul cœur de processeur peut exécuter 1 ou 2 milliards d'instructions pendant ce temps.

Donc, en bon développeur, vous serez tenté de demander à votre programme C de faire quelque chose de plus utile en attendant. C'est là que la programmation simultanée est là pour votre secours - et rend votre ordinateur malheureux parce qu'il doit fonctionner plus.

Ici, je vais vous montrer l'appel système Linux fork, l'un des moyens les plus sûrs de faire de la programmation concurrente.

La programmation simultanée peut être dangereuse?

Oui il peut. Par exemple, il existe également une autre façon d'appeler multithreading. Il a l'avantage d'être plus léger mais il peut vraiment allez mal si vous l'utilisez de manière incorrecte. Si votre programme, par erreur, lit une variable et écrit dans le même variable en même temps, votre programme deviendra incohérent et il sera presque indétectable - l'un des pires cauchemars des développeurs.

Comme vous le verrez ci-dessous, fork copie la mémoire donc il n'est pas possible d'avoir de tels problèmes avec les variables. De plus, fork crée un processus indépendant pour chaque tâche simultanée. En raison de ces mesures de sécurité, il est environ 5 fois plus lent de lancer une nouvelle tâche simultanée à l'aide de fork qu'avec le multithreading. Comme vous pouvez le voir, ce n'est pas beaucoup pour les avantages qu'il apporte.

Maintenant, assez d'explications, il est temps de tester votre premier programme C en utilisant l'appel fork.

L'exemple du fork Linux

Voici le code :

#inclure
#inclure
#inclure
#inclure
#inclure
int main()
pid_t forkStatus;
forkStatus = fork();
/* Enfant… */
if        (forkStatus == 0)
printf("L'enfant est en cours d'exécution, traitement.\n");
dormir(5) ;
printf("L'enfant est terminé, sortie.\n");
/* Parent… */
else if (forkStatus != -1)
printf("Le parent attend… \n");
attendre(NULL);
printf("Le parent quitte... \n");
autre
perror("Erreur lors de l'appel de la fonction fork");

renvoie 0 ;

Je vous invite à tester, compiler et exécuter le code ci-dessus mais si vous voulez voir à quoi ressemblerait la sortie et que vous êtes trop « fainéant » pour le compiler - après tout, vous êtes peut-être un développeur fatigué qui a compilé des programmes C à longueur de journée - vous pouvez trouver la sortie du programme C ci-dessous avec la commande que j'ai utilisée pour le compiler :

$ gcc -std=c89 -Wpedantic -Wall forkSleep.c -o forkSleep -O2
$ ./forkSommeil
Le parent attend…
L'enfant court, traite.
L'enfant est fait, sortant.
Le parent s'en va…

N'ayez pas peur si la sortie n'est pas 100% identique à ma sortie ci-dessus. N'oubliez pas que l'exécution simultanée signifie que les tâches s'exécutent dans le désordre, il n'y a pas d'ordre prédéfini. Dans cet exemple, vous pouvez voir que l'enfant exécute avant que le parent attend, et il n'y a rien de mal à ça. En général, l'ordre dépend de la version du noyau, du nombre de cœurs CPU, des programmes en cours d'exécution sur votre ordinateur, etc.

OK, maintenant reviens au code. Avant la ligne avec fork(), ce programme C est parfaitement normal : 1 ligne s'exécute à la fois, il n'y a qu'un seul processus pour ce programme (s'il y avait un petit délai avant fork, vous pouvez le confirmer dans votre gestionnaire de tâches).

Après le fork(), il y a maintenant 2 processus qui peuvent s'exécuter en parallèle. Tout d'abord, il y a un processus enfant. Ce processus est celui qui a été créé sur fork(). Ce processus enfant est spécial : il n'a exécuté aucune des lignes de code au-dessus de la ligne avec fork(). Au lieu de rechercher la fonction principale, il exécutera plutôt la ligne fork().

Qu'en est-il des variables déclarées avant fork?

Eh bien, Linux fork() est intéressant car il répond intelligemment à cette question. Les variables et, en fait, toute la mémoire des programmes C est copiée dans le processus fils.

Permettez-moi de définir ce que fait fork en quelques mots : cela crée un cloner du processus l'appelant. Les 2 processus sont quasiment identiques : toutes les variables contiendront les mêmes valeurs et les deux processus exécuteront la ligne juste après fork(). Cependant, après le processus de clonage, ils sont séparés. Si vous mettez à jour une variable dans un processus, l'autre processus habitude avoir sa variable mise à jour. C'est vraiment un clone, une copie, les processus ne partagent presque rien. C'est vraiment utile : vous pouvez préparer beaucoup de données puis fork() et utiliser ces données dans tous les clones.

La séparation commence lorsque fork() renvoie une valeur. Le processus d'origine (il s'appelle le processus parent) obtiendra l'ID de processus du processus cloné. De l'autre côté, le processus cloné (celui-ci s'appelle le processus enfant) obtiendra le nombre 0. Maintenant, vous devriez commencer à comprendre pourquoi j'ai mis des instructions if/else if après la ligne fork(). En utilisant la valeur de retour, vous pouvez demander à l'enfant de faire quelque chose de différent de ce que fait le parent - et crois moi c'est utile.

D'un côté, dans l'exemple de code ci-dessus, l'enfant fait une tâche qui prend 5 secondes et imprime un message. Pour imiter un processus qui prend du temps, j'utilise la fonction sleep. Ensuite, l'enfant sort avec succès.

De l'autre côté, le parent imprime un message, attend que l'enfant sorte et imprime enfin un autre message. Le fait que le parent attende son enfant est important. A titre d'exemple, le parent est en attente la plupart du temps pour attendre son enfant. Mais, j'aurais pu demander au parent de faire n'importe quel type de tâches de longue durée avant de lui dire d'attendre. De cette façon, il aurait fait des tâches utiles au lieu d'attendre - après tout, c'est pourquoi nous utilisons fourche(), non?

Cependant, comme je l'ai dit plus haut, il est vraiment important que parent attend ses enfants. Et c'est important à cause de processus zombies.

À quel point l'attente est importante

Les parents veulent généralement savoir si les enfants ont terminé leur traitement. Par exemple, vous souhaitez exécuter des tâches en parallèle mais tu ne veux certainement pas le parent doit quitter avant que les enfants aient terminé, car si cela se produisait, le shell renverrait une invite alors que les enfants n'ont pas encore terminé - ce qui est bizarre.

La fonction wait permet d'attendre que l'un des processus fils soit terminé. Si un parent appelle 10 fois fork(), il devra également appeler 10 fois wait(), une fois pour chaque enfant créé.

Mais que se passe-t-il si le parent appelle la fonction wait alors que tous les enfants ont déjà sorti? C'est là que les processus zombies sont nécessaires.

Lorsqu'un enfant se termine avant que le parent n'appelle wait(), le noyau Linux laissera l'enfant sortir mais il gardera un ticket dire que l'enfant est sorti. Ensuite, lorsque le parent appelle wait(), il trouvera le ticket, supprimera ce ticket et la fonction wait() reviendra immédiatement parce qu'il sait que le parent a besoin de savoir quand l'enfant a fini. Ce billet s'appelle un processus zombie.

C'est pourquoi il est important que le parent appelle wait() : s'il ne le fait pas, les processus zombies restent en mémoire et dans le noyau Linux ne peut pas garder de nombreux processus zombies en mémoire. Une fois la limite atteinte, votre ordinateur is incapable de créer un nouveau processus et ainsi vous serez dans un très mauvais état: même pour tuer un processus, vous devrez peut-être créer un nouveau processus pour cela. Par exemple, si vous souhaitez ouvrir votre gestionnaire de tâches pour tuer un processus, vous ne pouvez pas, car votre gestionnaire de tâches aura besoin d'un nouveau processus. Pire encore, vous ne pouvez pas tuer un processus zombie.

C'est pourquoi appeler wait est important : cela permet au noyau nettoyer le processus enfant au lieu de continuer à s'accumuler avec une liste de processus terminés. Et si le parent sort sans jamais appeler attendre()?

Heureusement, comme le parent est terminé, personne d'autre ne peut appeler wait() pour ces enfants, il y a donc sans raison pour garder ces processus zombies. Par conséquent, lorsqu'un parent quitte, tout reste processus zombies lié à ce parent sont enlevés. Les processus zombies sont vraiment seulement utile pour permettre aux processus parents de trouver qu'un enfant s'est terminé avant que le parent n'ait appelé wait().

Maintenant, vous préférerez peut-être connaître quelques mesures de sécurité pour vous permettre la meilleure utilisation de la fourche sans aucun problème.

Règles simples pour que la fourche fonctionne comme prévu

Tout d'abord, si vous connaissez le multithreading, s'il vous plaît ne forkez pas un programme utilisant des threads. En fait, évitez en général de mélanger plusieurs technologies de simultanéité. fork suppose de fonctionner dans les programmes C normaux, il n'a l'intention de cloner qu'une tâche parallèle, pas plus.

Deuxièmement, évitez d'ouvrir ou de fopen des fichiers avant fork(). Les fichiers sont l'une des seules choses partagé et pas cloné entre parent et enfant. Si vous lisez 16 octets dans le parent, le curseur de lecture avancera de 16 octets tous les deux chez le parent et chez l'enfant. Pire, si l'enfant et le parent écrivent des octets dans le même fichier en même temps, les octets du parent peuvent être mixte avec les octets de l'enfant!

Pour être clair, en dehors de STDIN, STDOUT et STDERR, vous ne voulez vraiment pas partager de fichiers ouverts avec des clones.

Troisièmement, faites attention aux sockets. Les prises sont aussi partagé entre parents et enfants. C'est utile pour écouter un port, puis laisser plusieurs travailleurs enfants prêts à gérer une nouvelle connexion client. pourtant, si vous l'utilisez mal, vous aurez des ennuis.

Quatrièmement, si vous souhaitez appeler fork() dans une boucle, faites-le avec soin extrême. Prenons ce code :

/* NE PAS COMPILER CECI */
const int targetFork = 4;
pid_t forkRésultat
 
pour (int i = 0; i < targetFork; i++)
forkResult = fork();
/*… */
 

Si vous lisez le code, vous pouvez vous attendre à ce qu'il crée 4 enfants. Mais cela créera plutôt 16 enfants. C'est parce que les enfants vont également exécuter la boucle et donc les enfants appelleront à leur tour fork(). Lorsque la boucle est infinie, cela s'appelle un bombe à fourche et est l'un des moyens de ralentir un système Linux tellement que ça ne marche plus et aura besoin d'un redémarrage. En un mot, gardez à l'esprit que Clone Wars n'est pas seulement dangereux dans Star Wars!

Vous avez maintenant vu comment une simple boucle peut mal tourner, comment utiliser des boucles avec fork()? Si vous avez besoin d'une boucle, vérifiez toujours la valeur de retour de fork :

const int targetFork = 4;
pid_t forkResult;
entier je = 0;
fais
forkResult = fork();
/*… */
je++ ;
while ((forkResult != 0 && résultat de la fourchette != -1) && (je < targetFork));

Conclusion

Il est maintenant temps pour vous de faire vos propres expériences avec fork()! Essayez de nouvelles façons d'optimiser le temps en effectuant des tâches sur plusieurs cœurs de processeur ou effectuez un traitement en arrière-plan pendant que vous attendez la lecture d'un fichier!

N'hésitez pas à lire les pages de manuel via la commande man. Vous apprendrez comment fonctionne précisément fork(), quelles erreurs vous pouvez obtenir, etc. Et profitez de la simultanéité!

Comment télécharger et jouer à Civilization VI de Sid Meier sur Linux
Présentation du jeu Civilization 6 est une version moderne du concept classique introduit dans la série de jeux Age of Empires. L'idée était assez sim...
Comment installer et jouer à Doom sur Linux
Introduction à Doom La série Doom est née dans les années 90 après la sortie du Doom original. Ce fut un succès instantané et à partir de ce moment-là...
Vulkan pour les utilisateurs Linux
Avec chaque nouvelle génération de cartes graphiques, nous voyons les développeurs de jeux repousser les limites de la fidélité graphique et se rappro...