C fork

De The Linux Craftsman
Révision datée du 21 décembre 2021 à 21:31 par Jc.forton (discussion | contributions) (→‎Création d'un fork)
(diff) ← Version précédente | Voir la version actuelle (diff) | Version suivante → (diff)
Aller à la navigation Aller à la recherche

Introduction

Un fork est un processus lourd, comprenez par là que l'intégralité du contexte d'exécution du processus père est recopié dans le nouveau processus fils. Ne cherchez pas, comme avec les threads, a échanger des variables entre les processus car cela ne marchera pas. Il faut utiliser des techniques de communication inter-processus comme les tubes, sockets ou encore sémaphores...

Fonctionnement

Pour créer un fork, il suffit d'appeler la fonction du même nom. Cette fonction aura différentes valeurs de retour en fonction du processus dans lequel on se trouve:

  • -1 → il y a une erreur ;
  • 0 → on est dans le processus fils ;
  • Le PID du fils → on est dans le processus père.

La valeur retournée par la fonction fork est de type pid_t, c'est pourquoi il faut obligatoirement inclure le fichier <sys/types.h>.

Dans le cas d'une erreur, celle-ci sera accessible grâce à la variable globale errno, il faudra donc inclure le fichier en-tête <errno.h>.

Fonctions annexes

Voici quelques fonctions annexes bien pratiques:

  • getpid → retourne le PID du processus appelant, de type pid_t :
#include <unistd.h>
#include <sys/types.h>

printf("Mon PID est %i\n", getpid()); // affichera par exemple "Mon PID est 14804"
  • getppid → retourne le PPID du processus appelant, de type pid_t :
#include <unistd.h>
#include <sys/types.h>

printf("Mon PPID est %i\n", getppid()); // affichera par exemple "Mon PPID est 14403"
  • getuid → retourne l'UID du processus appelant, de type uid_t :
#include <unistd.h>
#include <sys/types.h>

printf("Mon UID est %i\n", getuid()); // affichera par exemple "Mon UID est 0"
  • getgid → retourne le GID du processus appelant, de type gid_t :
#include <unistd.h>
#include <sys/types.h>

printf("Mon GID est %i\n", getgid()); // affichera par exemple "Mon GID est 0"
  • exit → permet de quitter le programme peu importe où dans le code :
#include <stdlib.h>

exit(EXIT_SUCCESS); // quitte le programme en retournant la valeur de succès
  • wait → permet au processus fils d'être libéré pour éviter les zombies:
#include <stdlib.h>
#include <sys/wait.h>

int status;

wait(&status); // la valeur de retour du processus fils sera dans la variable status

Création d'un fork

Ci-dessous un exemple de création d'un processus lourd :

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
	pid_t pid = fork();
	if (pid == -1) {
		// Il y a une erreur
		perror("fork");
		return EXIT_FAILURE;
	} else if (pid == 0) {
		// On est dans le fils
		printf("Mon PID est %i et celui de mon père est %i\n", getpid(),	getppid());
	} else {
		// On est dans le père
		printf("Mon PID est %i et celui de mon fils est %i\n", getpid(), pid);
	}
	return EXIT_SUCCESS;
}

Cet exemple peut retourner la sortie suivante :

Mon PID est 15150 et celui de mon fils est 15151
Mon PID est 15151 et celui de mon père est 15150

Cependant, on peut très bien avoir cela:

Mon PID est 15197 et celui de mon fils est 15198
Mon PID est 15198 et celui de mon père est 1

On voit que le numéro de processus du père est devenu 1. Cela vient du fait que, lorsque le père se termine, le fils est automatiquement rattaché au processus de PID 1 pour ne pas devenir un processus orphelin.

Création de plusieurs forks

Lorsque l'on a plusieurs forks à gérer, il faut garder la trace de chacun des numéros de processus et également attendre la fin de chacun des fils.

Ci-dessous un code qui démarre plusieurs forks qui vont incrémenter un compteur.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

// Nombre total de thread
#define NB_FORK 2
// Limite de l'incrément
#define INCREMENT_LIMIT 2
// Initialisation de la donnée
int data = 0;

// Fonction exécutée dans le fork
void job() {
	int tid = getpid();
	while (data < INCREMENT_LIMIT) {
		data++;
		printf("fork [ %i ] data [ %i ]\n", tid, data);
		// Pause l'exécution du fork pendant 1 seconde
		sleep(1);
	}
	printf("Fin du fork %i\n", tid);
	exit(EXIT_SUCCESS);
}
// Fonction qui attend chacun des processus fils
void waitForAll() {
	int status;
	pid_t pid;
	int n = 0;
	while (n < NB_FORK) {
		pid = wait(&status);
		printf("Fork [%i] terminé avec le code %i\n", pid, status);
		n++;
	}
}

int main() {
	for (int i = 0; i < NB_FORK; i++) {
		pid_t pid = fork();
		if (pid == -1) {
			// Il y a une erreur
			perror("fork");
			return EXIT_FAILURE;
		} else if (pid == 0) {
			// On est dans le fils
			job();
		} else {
			// On est dans le père
		}
	}
	waitForAll();
	return EXIT_SUCCESS;
}

Ce code donne, par exemple, le résultat suivant:

fork [ 15964 ] data [ 1 ]
fork [ 15963 ] data [ 1 ]
fork [ 15964 ] data [ 2 ]
fork [ 15963 ] data [ 2 ]
Fin du fork 15964
Fork [15964] terminé avec le code 0
Fin du fork 15963
Fork [15963] terminé avec le code 0

On voit bien que le contexte est recopié car les compteurs sont modifiés indépendamment.

Démonisation

Il peut être intéressant pour un programme, lorsque l'on cherche à écrire un serveur, que celui-ci se détache du processus qui l'a démarré : ce procédé s'appelle la démonisation.

Pour accomplir cela, rien de plus simple, il suffit d'appeler la fonction suivante:

#include <unistd.h>

int daemon(int nochdir, int noclose);
  • nochdir → si à 0, le démon utilise comme répertoire de travail la racine (/), sinon il conserve le répertoire de travail courant.
  • noclose → si à 0, le démon redirige la sortie standard dans /dev/null, sinon aucun changement n'est fait.

Pour comprendre le phénomène, utilisons le programme suivant :

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
	printf("PID du processus principal [ %i ]\n", getpid());
	sleep(10);
	return EXIT_SUCCESS;
}

Il ne reste plus qu'à compiler ce programme:

gcc -o daemon.bin daemon.c

Lorsque l'on exécute le programme, il ne rend pas la main et on doit attendre 10 secondes .

Si on ajoute, avant la ligne :

sleep(10);

la ligne suivante:

daemon(0,0);

le programme se détache du processus courant et on peut le voir grâce à la commande ps:

# ./daemon.bin
PID du processus principal [ 16559 ]
# ps -ef | grep ./daemon.bin
root      16560      1  0 07:52 ?        00:00:00 ./daemon.bin

On peut le voir plus simplement sur cet exemple :

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
	printf("PID du processus principal [ %i ]\n", getpid());
	// Passage du programme en arrière plan sans modifier la sortie standard
	daemon(0, 1);
	printf("PID du démon [ %i ]\n", getpid());
	return EXIT_SUCCESS;
}

L'exécution donne, par exemple :

PID du processus principal [ 16613 ]
PID du démon [ 16614 ]