Philosophers · 42 // TECHNICAL GUIDE
SYS:ONLINE REC // 04
Projet 42 · Branche Système · Concurrency

Philosophers

// Le dining philosophers problem — threads, mutex, sémaphores et processus, sans qu'un seul ne meure de faim.

Cinq philosophes sont assis autour d'une table ronde. Entre chacun, une fourchette. Pour manger, il en faut deux. Pour penser, il faut avoir mangé. Pour dormir, il faut avoir fini de manger. Si l'un d'eux attend trop longtemps, il meurt. Ce guide décortique trois implémentations — philo (threads + mutex), philo_two (threads + sémaphore), philo_bonus (processus + sémaphore) — et les bugs subtils qui se cachent dans chacune.

Difficulté
★★★★☆
Temps estimé
50 – 100 h
Fonctions
pthread_* · sem_* · fork · waitpid
Sortie attendue
philo · philo_bonus
// Contrainte clé

Aucun philosophe ne doit mourir si la logique le permet. Aucun data race (vérifié avec ThreadSanitizer). Aucun leak (vérifié avec AddressSanitizer). Aucune variable globale pour gérer les ressources partagées. Le délai entre la mort réelle d'un philosophe et l'affichage de sa mort doit être inférieur à 10ms. La simulation s'arrête dès qu'un philosophe meurt — un seul message "died" doit s'afficher.

« Nous sommes tous assis à la même table. Certains mangent, d'autres pensent, d'autres dorment. Mais si l'un de nous attend trop longtemps sa part, le silence tombe — et avec lui, tout s'arrête. »
— Journal d'observation, Bunker YoRHa
01

Le projet

Philosophers résout le problème classique du dining philosophers formulé par Edsger Dijkstra en 1965. N philosophes sont assis autour d'une table ronde avec N fourchettes (une entre chaque paire de voisins). Chaque philosophe alterne entre trois états : manger (nécessite 2 fourchettes), dormir et penser. Si un philosophe ne commence pas à manger dans les time_to_die millisecondes après son dernier repas, il meurt — et la simulation s'arrête immédiatement.

Le sujet 42 exige deux programmes : philo (mandatory, threads + mutex) et philo_bonus (bonus, processus + sémaphore). On a en plus implémenté philo_two (threads + sémaphore) comme variante pédagogique. Les trois programmes acceptent les mêmes arguments : ./philo nb_philo time_to_die time_to_eat time_to_sleep [must_eat] — tous les temps en millisecondes, must_eat optionnel.

Exigences du sujet

CritèreExigenceDétail
États3 exclusifsmanger / dormir / penser — jamais simultanés
Fourchettes1 entre chaque philoN fourchettes pour N philosophes
Manger2 fourchettes requisesUne dans chaque main, gardées pendant time_to_eat
Morttime_to_die strictSi pas de repas dans time_to_die ms → meurt
AffichageFormat stricttimestamp_in_ms X has taken a fork / is eating / is sleeping / is thinking / died
ThreadsafePas de mélangeLes messages ne doivent jamais être entrelacés
Délai mort< 10msEntre la mort réelle et l'affichage de "died"
Arrêt1 mort = stopSimulation s'arrête au premier décès
must_eatOptionnelSi tous mangent N fois → stop sans mort
NormeNorminette0 erreur, 0 warning -Wall -Wextra -Werror
GlobalesInterditesPas de variable globale pour les ressources partagées
Sanitizers0 data race, 0 leakHelgrind / ThreadSanitizer / AddressSanitizer

Fonctions autorisées

// philo (mandatory)
memset, printf, malloc, free, write, usleep, gettimeofday, pthread_create, pthread_detach, pthread_join, pthread_mutex_init, pthread_mutex_destroy, pthread_mutex_lock, pthread_mutex_unlock
// philo_bonus
Idem + fork, kill, exit, waitpid, sem_open, sem_close, sem_post, sem_wait, sem_unlink
// Métaphore YoRHa

Chaque philosophe est un androïde YoRHa. La table est le Bunker. Les fourchettes sont les pods tactiques — il en faut deux pour exécuter une mission (manger). Le time_to_die est le délai avant épuisement de la batterie. La mort d'un seul androïde met fin à la mission entière. La partie bonus modélise chaque androïde comme un processus séparé (processus lourd plutôt que thread léger), ce qui isole mieux les défaillances mais coûte plus cher en ressources.

02

Threads & mutex

Avant de coder la moindre ligne, il faut comprendre trois concepts fondamentaux de la programmation concurrente : les threads (processus légers partageant la même mémoire), les mutex (verrous pour protéger l'accès concurrent à une variable), et le deadlock (interblocage quand deux threads attendent mutuellement que l'autre libère une ressource).

pthread_create / pthread_join

Un thread est une unité d'exécution qui partage la mémoire virtuelle du processus qui le crée. Contrairement à fork() qui duplique toute la mémoire, pthread_create crée un nouveau flot d'instructions qui voient les mêmes variables globales, les mêmes pointeurs heap, les mêmes descripteurs de fichiers. C'est rapide (microsecondes) mais dangereux : sans précaution, deux threads peuvent écrire en même temps au même endroit et corrompre les données.

pthread_basics.c// create + join
/* Créer un thread qui exécute ph_routine avec philo en argument */
if (pthread_create(&philo->thread, NULL,
		ph_routine, philo) != 0)
	return (ph_error("thread create failed"));

/* Attendre que le thread se termine avant de continuer */
pthread_join(philo->thread, NULL);

/* pthread_detach : alternative non-bloquante, le thread se nettoie tout seul
   à sa fin. Mais on ne peut plus le join. Pour Philosophers, on utilise join. */

pthread_create prend 4 arguments : (1) l'adresse du pthread_t à initialiser, (2) les attributs (NULL = défaut), (3) la fonction à exécuter (signature void *f(void *arg)), et (4) l'argument à passer à la fonction. Le thread démarre immédiatement et s'exécute en parallèle du thread appelant.

pthread_join bloque le thread appelant jusqu'à ce que le thread cible se termine. C'est essentiel pour éviter que le main ne se termine avant les threads enfants (ce qui tuerait immédiatement tous les threads). On join tous les threads philosophes + le thread monitor avant de nettoyer les ressources.

Mutex & data race

Un mutex (mutual exclusion) est un verrou qui garantit qu'un seul thread à la fois peut exécuter une section critique. Sans mutex, si deux threads incrémentent i++ simultanément, l'un peut lire la valeur pendant que l'autre la modifie, et l'incrément final sera 1 au lieu de 2 — c'est un data race.

mutex_basics.c// lock / unlock
/* Initialiser un mutex (à faire une seule fois avant utilisation) */
pthread_mutex_init(&mutex, NULL);

/* Verrouiller avant la section critique */
pthread_mutex_lock(&mutex);
shared_counter++;              /* safe : aucun autre thread ne peut entrer ici */
pthread_mutex_unlock(&mutex);

/* Détruire le mutex (à faire une seule fois à la fin) */
pthread_mutex_destroy(&mutex);
// Data race — le piège invisible

Un data race ne provoque ni crash ni message d'erreur — il corrompt silencieusement les données. Le programme peut fonctionner 99 fois sur 100 et échouer la 100ème fois, ou pire, produire des résultats subtilement erronés qu'aucun test ne détectera. C'est pour ça que le sujet impose ThreadSanitizer : sans lui, vous ne verrez pas les races. Compilez avec -fsanitize=thread -g et lancez les tests — toute race détectée signifie un 0 à l'évaluation.

Dans Philosophers, on utilise des mutex pour protéger trois types de ressources partagées : (1) chaque fourchette a son mutex — un philosophe doit le locker pour "prendre" la fourchette, (2) un mutex print global pour éviter que les messages de plusieurs philosophes ne s'entrelacent, (3) un mutex meal par philosophe pour protéger last_meal et meals_eaten contre les accès concurrents entre le thread philosophe et le thread monitor.

Deadlock & ordre asymétrique

Le deadlock est le bug le plus célèbre de Philosophers. Imaginez 5 philosophes qui, au même instant, prennent tous leur fourchette gauche. Aucun ne peut prendre la droite (elle est déjà prise par le voisin). Ils attendent tous indéfiniment. La simulation se bloque sans qu'aucun ne meure — pire que la mort, c'est la paralysie.

// Deadlock classique — tous prennent gauche d'abord
Table ronde, 5 philosophes (P1..P5), 5 fourchettes (F1..F5) P1 / \ F5 F1 / \ P5 P2 \ / F4 F2 \ / P3 | F3 (entre P3 et P4 — pas affiché pour clarté) Scénario deadlock : T=0 : P1 prend F1 (gauche) T=0 : P2 prend F2 (gauche) T=0 : P3 prend F3 (gauche) T=0 : P4 prend F4 (gauche) T=0 : P5 prend F5 (gauche) T=0+ : P1 veut F5 → bloqué (P5 la tient) T=0+ : P2 veut F1 → bloqué (P1 la tient) T=0+ : P3 veut F2 → bloqué T=0+ : P4 veut F3 → bloqué T=0+ : P5 veut F4 → bloqué Résultat : DEADLOCK — personne ne mange, personne ne meurt La simulation se fige pour l'éternité. Solution : ordre asymétrique Philosophes pairs : prennent fourchette gauche puis droite Philosophes impairs : prennent fourchette droite puis gauche → Casse le cycle : P1 (impair) attend F5, mais P5 (impair) prend F4 puis F5 donc P5 libère F4 avant de vouloir F5 → P4 peut avancer → cascade débloque

Notre implémentation utilise cette technique : les philosophes pairs prennent leur fourchette gauche puis droite, les impairs prennent droite puis gauche. Cela casse le cycle circulaire du deadlock. C'est plus simple que la solution "philosophe arbitre" (un sémaphore global à N-1) et tout aussi efficace.

philo/srcs/init.c// ordre asymétrique
static void	ph_assign_forks(t_philo *philo, t_rules *rules)
{
	if (philo->id % 2 == 0)
	{
		/* Pair : gauche puis droite */
		philo->left_fork = &rules->forks[philo->id - 1];
		philo->right_fork = &rules->forks[philo->id % rules->nb_philo];
	}
	else
	{
		/* Impair : droite puis gauche (inverse le cycle) */
		philo->left_fork = &rules->forks[philo->id % rules->nb_philo];
		philo->right_fork = &rules->forks[philo->id - 1];
	}
}
// Alternative : décalage temporel

Une autre technique courante est de faire attendre les philosophes pairs au démarrage (if (id % 2 == 0) usleep(time_to_eat / 2)). Cela laisse le temps aux impairs de prendre leurs fourchette et de manger, évitant que tout le monde ne se précipite en même temps. On combine les deux dans notre implémentation pour une robustesse maximale.

03

Architecture du code

Le projet suit la structure 42 standard : un dossier par programme, chaque dossier contient includes/ (headers), srcs/ (sources C), un Makefile et un fichier author. Les trois programmes partagent la même architecture interne : mainparseinitstartcleanup, avec une séparation claire des responsabilités.

// Arborescence du projet
philosophers/ ├── philo/ # MANDATORY — threads + mutex │ ├── Makefile │ ├── author │ ├── includes/ │ │ └── philo.h # structures t_rules, t_philo + prototypes │ └── srcs/ │ ├── main.c # entry point : parse → init → start → cleanup │ ├── error.c # ph_error() — affiche sur stderr │ ├── parse.c # ph_parse_args() — validation argc/argv │ ├── init.c # ph_init() — mutex + philos + start_time │ ├── philo.c # ph_routine() — boucle du philosophe │ ├── monitor.c # ph_monitor() — détection mort + repas finis │ ├── print.c # ph_print_status() — mutex print │ ├── run.c # ph_start() + ph_cleanup() │ └── utils.c # timestamp, usleep, check_stop, set_stop │ ├── philo_two/ # VARIANTE — threads + sémaphore │ └── (même structure, semaphore au lieu de mutex par fourchette) │ ├── philo_bonus/ # BONUS — processus + sémaphore │ ├── Makefile │ ├── author │ ├── includes/ │ │ └── philo.h │ └── srcs/ │ ├── main.c │ ├── error.c │ ├── parse.c │ ├── init.c # sémaphores nommés sem_open │ ├── philo.c # ph_routine() appelé après fork() │ ├── monitor.c # monitor thread par processus enfant │ ├── print.c # write() au lieu de printf (buffering fork) │ ├── run.c # fork + supervisor + kill + waitpid │ └── utils.c │ └── docs/ └── index.html # ce guide (GitHub Pages)

Flux d'exécution

// Flux principal philo (mandatory)
main(argc, argv) │ ▼ ph_parse_args ────▶ valide argc (5 ou 6), argv tous digits, valeurs > 0 │ range : nb_philo [1, 200], times >= 0, must_eat >= 0 ▼ ph_init ──────────▶ ph_init_mutexes (print + stop + N forks) │ ph_init_philos (N structs, meal_mutex chacun) │ gettimeofday → start_time ▼ ph_start ─────────▶ ph_create_threads (N threads philosophes + 1 monitor) │ ph_join_threads (attente fin) ▼ ph_cleanup ───────▶ ph_destroy_mutexes (N forks + N meal + print + stop) free(forks), free(philos) Pendant l'exécution : - Chaque thread philo exécute ph_routine (take_forks → eat → sleep → think) - Le thread monitor scrute last_meal toutes les 200µs - Si un philo dépasse time_to_die → set_stop + print "died" - Si must_eat atteint par tous → set_stop (arrêt propre)
// Séparation des responsabilités

Chaque fichier a un rôle unique : parse.c ne fait que valider, init.c ne fait qu'initialiser, philo.c contient la logique métier (routine), monitor.c la surveillance, print.c l'affichage thread-safe, run.c l'orchestration (create + join + cleanup), utils.c les utilitaires (timestamp, usleep). Cette séparation facilite le debug et respecte la règle norminette de max 5 fonctions par fichier.

04

philo — threads + mutex

Le programme mandatory utilise un thread par philosophe et un mutex par fourchette. C'est l'implémentation la plus directe du problème : chaque fourchette est une ressource critique, chaque philosophe est un flot d'exécution indépendant, et le mutex garantit qu'une fourchette ne peut être tenue que par un philosophe à la fois.

Structures de données

philo/includes/philo.h// structures
typedef struct s_rules	t_rules;
typedef struct s_philo	t_philo;

struct s_rules
{
	int				nb_philo;
	int				time_to_die;
	int				time_to_eat;
	int				time_to_sleep;
	int				must_eat;       /* -1 si non spécifié */
	int				stop;           /* flag mort/fin, protégé par stop_mutex */
	long			start_time;     /* ms depuis epoch, fixé une fois au démarrage */
	pthread_mutex_t	print_mutex;    /* protège printf contre entrelacement */
	pthread_mutex_t	stop_mutex;      /* protège le flag stop */
	pthread_mutex_t	*forks;          /* tableau de N mutex, un par fourchette */
};

struct s_philo
{
	int				id;              /* 1 à N */
	int				meals_eaten;     /* compteur pour must_eat */
	long			last_meal;       /* timestamp du dernier repas, protégé par meal_mutex */
	pthread_t		thread;          /* identifiant du thread */
	pthread_mutex_t	meal_mutex;      /* protège last_meal et meals_eaten contre data race */
	pthread_mutex_t	*left_fork;      /* pointeur vers mutex fourchette gauche */
	pthread_mutex_t	*right_fork;     /* pointeur vers mutex fourchette droite */
	t_rules			*rules;          /* pointeur vers règles partagées */
};

La structure t_rules contient les paramètres de la simulation (lus depuis argv), l'état global (stop), et les mutex partagés. La structure t_philo contient l'état d'un philosophe individuel. Chaque philosophe a son propre meal_mutex pour protéger last_meal et meals_eaten — sans lui, le thread monitor pourrait lire last_meal pendant que le thread philosophe l'écrit, créant un data race détecté par ThreadSanitizer.

Init et forks asymétriques

philo/srcs/init.c// ph_init
static int	ph_init_mutexes(t_rules *rules)
{
	int	i;

	if (pthread_mutex_init(&rules->print_mutex, NULL) != 0)
		return (ph_error("mutex init failed"));
	if (pthread_mutex_init(&rules->stop_mutex, NULL) != 0)
		return (ph_error("mutex init failed"));
	rules->forks = malloc(sizeof(pthread_mutex_t) * rules->nb_philo);
	if (rules->forks == NULL)
		return (ph_error("malloc failed"));
	i = 0;
	while (i < rules->nb_philo)
	{
		if (pthread_mutex_init(&rules->forks[i], NULL) != 0)
			return (ph_error("mutex init failed"));
		i++;
	}
	return (0);
}

static void	ph_assign_forks(t_philo *philo, t_rules *rules)
{
	/* Ordre asymétrique pour éviter le deadlock (voir section 02) */
	if (philo->id % 2 == 0)
	{
		philo->left_fork = &rules->forks[philo->id - 1];
		philo->right_fork = &rules->forks[philo->id % rules->nb_philo];
	}
	else
	{
		philo->left_fork = &rules->forks[philo->id % rules->nb_philo];
		philo->right_fork = &rules->forks[philo->id - 1];
	}
}

On alloue un tableau de N mutex (un par fourchette). La fourchette i est partagée entre le philosophe i (qui la considère comme sa gauche) et le philosophe i+1 (qui la considère comme sa droite, modulo N pour la table circulaire). L'assignation asymétrique selon la parité de l'ID casse le cycle du deadlock.

Routine du philosophe

philo/srcs/philo.c// ph_routine
static void	ph_take_forks(t_philo *philo)
{
	pthread_mutex_lock(philo->left_fork);
	ph_print_status(philo, "has taken a fork", 0);
	if (philo->rules->nb_philo == 1)
	{
		ph_usleep(philo->rules->time_to_die + 10);
		pthread_mutex_unlock(philo->left_fork);
		return ;
	}
	pthread_mutex_lock(philo->right_fork);
	ph_print_status(philo, "has taken a fork", 0);
}

static void	ph_eat(t_philo *philo)
{
	/* MAJ last_meal sous meal_mutex (data race safe) */
	pthread_mutex_lock(&philo->meal_mutex);
	philo->last_meal = ph_timestamp(philo->rules);
	philo->meals_eaten++;
	pthread_mutex_unlock(&philo->meal_mutex);
	ph_print_status(philo, "is eating", 0);
	ph_usleep(philo->rules->time_to_eat);
	pthread_mutex_unlock(philo->left_fork);
	pthread_mutex_unlock(philo->right_fork);
}

void	*ph_routine(void *arg)
{
	t_philo	*philo;

	philo = (t_philo *)arg;
	/* Initialiser last_meal avant de démarrer (pour ne pas mourir avant le 1er repas) */
	pthread_mutex_lock(&philo->meal_mutex);
	philo->last_meal = ph_timestamp(philo->rules);
	pthread_mutex_unlock(&philo->meal_mutex);
	if (philo->id % 2 == 0)
		ph_usleep(philo->rules->time_to_eat / 2);  /* décalage pairs */
	while (!ph_check_stop(philo->rules))
	{
		ph_take_forks(philo);
		if (philo->rules->nb_philo == 1)
			break ;  /* 1 philo ne peut jamais manger (1 seule fourchette) */
		ph_eat(philo);
		if (philo->rules->must_eat != -1 && philo->meals_eaten
			>= philo->rules->must_eat)
			break ;  /* philo a fini ses repas */
		ph_sleep_and_think(philo);
	}
	return (NULL);
}
// Cas spécial nb_philo == 1

Avec un seul philosophe, il n'y a qu'une seule fourchette — impossible de manger. Le philosophe prend sa fourchette gauche, attend time_to_die + 10 ms (donc meurt), puis libère la fourchette. Sans ce cas spécial, le philosophe bloquerait indéfiniment sur pthread_mutex_lock(right_fork) (la fourchette droite est la même que la gauche, déjà lockée) — deadlock. Le test ./philo 1 800 200 200 doit afficher "X 1 died" à ~801ms.

Monitor thread

Le thread monitor est le cœur du système de détection de mort. Il parcourt tous les philosophes toutes les 200µs, vérifie si l'un d'eux a dépassé time_to_die depuis son dernier repas, et si oui, déclenche l'arrêt. Il vérifie aussi si tous les philosophes ont atteint must_eat pour un arrêt propre.

philo/srcs/monitor.c// ph_monitor
static int	ph_check_death(t_rules *rules, t_philo *philos)
{
	int		i;
	long	now;

	i = 0;
	while (i < rules->nb_philo)
	{
		pthread_mutex_lock(&philos[i].meal_mutex);
		if (rules->must_eat != -1 && philos[i].meals_eaten >= rules->must_eat)
		{
			pthread_mutex_unlock(&philos[i].meal_mutex);
			i++;
			continue ;
		}
		now = ph_timestamp(rules);
		if (now - philos[i].last_meal > rules->time_to_die)
		{
			pthread_mutex_unlock(&philos[i].meal_mutex);
			ph_set_stop(rules);
			ph_print_status(&philos[i], "died", 1);
			return (1);
		}
		pthread_mutex_unlock(&philos[i].meal_mutex);
		i++;
	}
	return (0);
}

void	*ph_monitor(void *arg)
{
	t_philo	*philos;

	philos = (t_philo *)arg;  /* pointeur heap, pas stack */
	while (!ph_check_stop(philos->rules))
	{
		if (ph_check_death(philos->rules, philos))
			return (NULL);
		if (ph_check_finished(philos->rules, philos))
			return (NULL);
		usleep(200);  /* 200µs : précision < 10ms pour délai mort */
	}
	return (NULL);
}
// Bug critique — stack-use-after-return

Une erreur subtile detectée par AddressSanitizer : si on passe &philos (l'adresse d'une variable stack dans ph_create_threads) au thread monitor, cette stack frame disparaît quand ph_create_threads retourne, et le monitor lit de la mémoire invalide. Solution : passer philos directement (le pointeur heap qui lui reste valide). C'est exactement le bug qu'on a corrigé dans notre audit — voir section 07.

05

philo_two — threads + sémaphore

philo_two est une variante pédagogique qui remplace les mutex par fourchette par un sémaphore unique représentant toutes les fourchettes au milieu de la table. Initialement à N (le nombre de philosophes), ce sémaphore permet à un philosophe de prendre une fourchette avec sem_wait et de la relâcher avec sem_post. Comme il faut deux fourchettes pour manger, chaque philosophe fait deux sem_wait avant de manger et deux sem_post après.

Sémaphore nommé

Un sémaphore est un compteur atomique protégé par le noyau. Contrairement au mutex (qui a un "propriétaire" explicite), un sémaphore peut être incrémenté par n'importe qui et décrémenté par n'importe qui. sem_open crée un sémaphore nommé (accessible par nom entre processus), identifié par une chaîne commençant par /.

philo_two/srcs/init.c// sem_open
static int	ph_init_semaphores(t_rules *rules)
{
	/* sem_unlink avant sem_open : nettoie les restes d'un crash précédent */
	sem_unlink("/philo_two_forks");
	sem_unlink("/philo_two_print");

	/* Sémaphore des fourchettes : initialisé à N (autant de fourchettes que de philos) */
	rules->forks = sem_open("/philo_two_forks", O_CREAT, 0644,
			rules->nb_philo);
	if (rules->forks == SEM_FAILED)
		return (ph_error("sem_open failed"));

	/* Sémaphore print : initialisé à 1 (mutex binaire pour l'affichage) */
	rules->print = sem_open("/philo_two_print", O_CREAT, 0644, 1);
	if (rules->print == SEM_FAILED)
		return (ph_error("sem_open failed"));

	return (0);
}

Routine avec sémaphore

philo_two/srcs/philo.c// take_forks
static void	ph_take_forks(t_philo *philo)
{
	/* Prendre 2 fourchettes : 2 sem_wait (décrémente le compteur de 2) */
	sem_wait(philo->rules->forks);
	ph_print_status(philo, "has taken a fork", 0);
	sem_wait(philo->rules->forks);
	ph_print_status(philo, "has taken a fork", 0);
}

static void	ph_eat(t_philo *philo)
{
	/* MAJ last_meal sous meal_mutex (même pattern que philo) */
	pthread_mutex_lock(&philo->meal_mutex);
	philo->last_meal = ph_timestamp(philo->rules);
	philo->meals_eaten++;
	pthread_mutex_unlock(&philo->meal_mutex);
	ph_print_status(philo, "is eating", 0);
	ph_usleep(philo->rules->time_to_eat);

	/* Relâcher les 2 fourchettes : 2 sem_post (incrémente le compteur de 2) */
	sem_post(philo->rules->forks);
	sem_post(philo->rules->forks);
}
// Pourquoi pas de deadlock avec sémaphore ?

Avec N philosophes et un sémaphore initialisé à N, le scénario deadlock classique (tous prennent une fourchette) ne peut pas se produire : si N philosophes font chacun un sem_wait, le compteur passe à 0, et le N+1ème sem_wait bloque. Donc au maximum N fourchettes peuvent être tenues simultanément — mais comme il en faut 2 pour manger, au moins un philosophe n'aura qu'une seule fourchette et libérera l'autre. Le système se débloque tout seul.

Le reste de l'implémentation (monitor, print, init des philos, routine principale) est identique à philo. La seule différence est le mécanisme de synchronisation des fourchettes : sémaphore au lieu de mutex individuels.

06

philo_bonus — processus + sémaphore

Le bonus transforme radicalement l'architecture : chaque philosophe n'est plus un thread mais un processus séparé, créé avec fork(). Le processus principal ne participe pas à la simulation — il supervise uniquement en attendant la mort d'un enfant ou la fin normale. Cette approche isole mieux les défaillances (un segfault dans un philosophe ne tue pas les autres) mais introduit de nouveaux défis : buffering stdout avec fork, processus orphelins, et communication inter-processus pour signaler la mort.

fork() et mémoire partagée

philo_bonus/srcs/run.c// ph_create_processes
static int	ph_create_processes(t_rules *rules, t_philo *philos)
{
	int	i;

	i = 0;
	while (i < rules->nb_philo)
	{
		philos[i].pid = fork();
		if (philos[i].pid < 0)
			return (ph_error("fork failed"));
		if (philos[i].pid == 0)
		{
			/* Processus enfant : exécute la routine puis exit */
			ph_routine(&philos[i]);
			exit(0);
		}
		/* Processus parent : continue à forker les autres */
		i++;
	}
	return (0);
}

Après fork(), le processus enfant hérite d'une copie complète de la mémoire du parent — y compris les structures t_rules et t_philo. Mais attention : ces copies sont indépendantes. Si un enfant modifie last_meal, les autres enfants ne le voient pas. C'est pourquoi on utilise des sémaphores nommés (partagés entre processus via le noyau) plutôt que des mutex (qui ne marchent qu'entre threads du même processus).

// Piège — buffering stdout avec fork

printf bufferise sa sortie. Quand on fork, l'enfant hérite du buffer non vidé. Résultat : les messages peuvent ne jamais s'afficher (si l'enfant exit sans flusher) ou s'afficher en double (une fois par l'enfant, une fois par le parent quand son buffer se vide). Solution : utiliser write() au lieu de printf, avec des helpers ph_putnbr_fd et ph_putstr_fd. write est non bufferisé — chaque appel va directement au noyau.

Monitor thread par processus

Chaque processus enfant crée son propre thread monitor pour surveiller sa propre mort. Le monitor lit last_meal (variable locale au processus, pas besoin de la partager) et, si le philosophe dépasse time_to_die, affiche "died" puis poste sur le sémaphore stop pour réveiller le parent.

philo_bonus/srcs/monitor.c// ph_monitor_routine
void	*ph_monitor_routine(void *arg)
{
	t_philo	*philo;
	long	now;

	philo = (t_philo *)arg;
	while (1)
	{
		usleep(200);
		pthread_mutex_lock(&philo->meal_mutex);
		now = ph_timestamp(philo->rules);
		if (now - philo->last_meal > philo->rules->time_to_die)
		{
			pthread_mutex_unlock(&philo->meal_mutex);
			sem_wait(philo->rules->print);
			if (sem_trywait(philo->rules->first_death) == 0)
				ph_print_death(philo);
			sem_post(philo->rules->print);
			sem_post(philo->rules->stop);  /* réveille le parent */
			exit(0);
		}
		pthread_mutex_unlock(&philo->meal_mutex);
	}
	return (NULL);
}

Single death fix

Un bug subtil du bonus : si deux philosophes meurent quasi-simultanément, chacun poste sur stop et affiche "died". Le sujet exige qu'un seul message de mort s'affiche. La solution est un sémaphore first_death initialisé à 1 : seul le premier à appeler sem_trywait(first_death) réussit (le compteur passe à 0), les autres échouent et n'affichent rien.

// Single death — sémaphore first_death
Sans first_death : T=310ms : P1 meurt → print "310 1 died" → sem_post(stop) → exit T=311ms : P3 meurt → print "311 3 died" → sem_post(stop) → exit Résultat : 2 messages "died" → ❌ (sujet exige 1 seul) Avec first_death (init=1) : T=310ms : P1 meurt → sem_wait(print) → sem_trywait(first_death) == 0 (OK) → print "310 1 died" → sem_post(print) → sem_post(stop) → exit T=311ms : P3 meurt → sem_wait(print) → sem_trywait(first_death) == -1 (déjà pris) → n'affiche RIEN → sem_post(print) → sem_post(stop) → exit Résultat : 1 seul message "died" → ✅ sem_trywait est non-bloquant : si le sémaphore est à 0, retourne -1 immédiatement sans attendre. C'est crucial pour ne pas retarder le second mourant.

Supervisor thread dans le parent

Le processus parent doit détecter deux cas : (1) un enfant est mort (sémaphore stop posté par le monitor), (2) tous les enfants ont fini leurs must_eat repas (tous les waitpid retournent). Un thread supervisor gère le cas (2) en attendant non-bloquant (WNOHANG) tous les enfants.

philo_bonus/srcs/run.c// ph_start
int	ph_start(t_rules *rules, t_philo *philos)
{
	pthread_t	supervisor;

	if (ph_create_processes(rules, philos) != 0)
	{
		ph_kill_and_wait(rules, philos);
		return (1);
	}
	/* Supervisor : détecte la fin normale (must_eat atteint par tous) */
	pthread_create(&supervisor, NULL, ph_supervisor, philos);

	/* Bloquant : attend qu'un enfant meure (sem_post(stop) par un monitor) */
	sem_wait(rules->stop);

	/* Tuer tous les enfants restants et attendre qu'ils se terminent */
	ph_kill_and_wait(rules, philos);
	pthread_join(supervisor, NULL);
	return (0);
}
// Pas d'orphelins

La fiche d'évaluation vérifie explicitement qu'aucun processus orphelin ne subsiste après l'exécution. Notre ph_kill_and_wait envoie SIGKILL à tous les enfants puis waitpid sur chacun — garantissant qu'aucun zombie ni orphelin ne reste. Vérifiez avec ps aux | grep philo_bonus après exécution : doit retourner vide.

07

Race condition & ASan

Le bug le plus insidieux découvert pendant l'audit n'est pas un deadlock ni un crash — c'est un stack-use-after-return détecté uniquement par AddressSanitizer. Sans ASan, le programme semblait fonctionner. Mais en réalité, le thread monitor lisait de la mémoire libérée, ce qui aurait pu causer des comportements imprévisibles en production.

Le bug : &philos vs philos

bug_demo.c// ❌ BUG
static int	ph_create_threads(t_rules *rules, t_philo *philos, pthread_t *mon)
{
	/* ... création des threads philosophes ... */

	/* BUG : on passe &philos (l'adresse de la variable locale philos) */
	if (pthread_create(mon, NULL, ph_monitor, &philos) != 0)
	{
		ph_set_stop(rules);
		return (ph_error("monitor create failed"));
	}
	return (0);
}

int	ph_start(t_rules *rules, t_philo *philos)
{
	pthread_t	monitor;

	if (ph_create_threads(rules, philos, &monitor) != 0)
		return (1);
	/* ICI : ph_create_threads retourne, sa stack frame est libérée !
	   Le thread monitor (qui tourne toujours) lit &philos → mémoire invalide */
	ph_join_threads(rules, philos, monitor);
	return (0);
}
// Stack-use-after-return — visualisation
Stack de ph_start : ┌─────────────────────┐ │ philos (pointeur) │ ◀── &philos passé au monitor │ monitor (pthread_t) │ │ ... │ └─────────────────────┘ Stack de ph_create_threads (appelée par ph_start) : ┌─────────────────────┐ │ philos (pointeur) │ ◀── &philos = adresse de CETTE variable │ mon (pthread_t*) │ │ i (int) │ └─────────────────────┘ │ │ ph_create_threads retourne ▼ STACK LIBÉRÉE — le monitor lit &philos qui pointe vers... n'importe quoi Correction : Passer philos directement (pointeur heap qui reste valide pendant toute l'exécution) Au lieu de &philos (adresse d'une variable stack qui disparaît) ph_monitor reçoit t_philo* (pointeur heap) au lieu de t_philo** (adresse stack)

La correction

philo/srcs/run.c// ✅ CORRIGÉ
static int	ph_create_threads(t_rules *rules, t_philo *philos, pthread_t *mon)
{
	/* ... création des threads philosophes ... */

	/* CORRIGÉ : on passe philos (pointeur heap) directement */
	if (pthread_create(mon, NULL, ph_monitor, philos) != 0)
	{
		ph_set_stop(rules);
		return (ph_error("monitor create failed"));
	}
	return (0);
}

/* Le monitor reçoit maintenant un t_philo* (et non t_philo**) */
void	*ph_monitor(void *arg)
{
	t_philo	*philos;

	philos = (t_philo *)arg;  /* cast direct, pas de double indirection */
	while (!ph_check_stop(philos->rules))
	{
		if (ph_check_death(philos->rules, philos))
			return (NULL);
		if (ph_check_finished(philos->rules, philos))
			return (NULL);
		usleep(200);
	}
	return (NULL);
}
// Leçon générale

Ne jamais passer l'adresse d'une variable locale à un thread. Le thread peut vivre plus longtemps que la fonction qui l'a créé, et la stack frame disparaît dès le retour de cette fonction. Passez toujours des pointeurs vers le heap (malloc) ou vers des variables static. Et compilez systématiquement avec -fsanitize=address -g pour détecter ces bugs invisibles.

ThreadSanitizer pour data races

AddressSanitizer détecte les bugs mémoire (use-after-free, stack overflow, leaks). ThreadSanitizer détecte les data races : accès concurrents à la même variable sans synchronisation. Pour Philosophers, le data race le plus probable est sur last_meal : le thread philosophe l'écrit (dans ph_eat) et le thread monitor la lit (dans ph_check_death). Sans mutex, c'est un data race garanti.

terminal// compilation avec sanitizers
# AddressSanitizer (memory bugs + leaks)
cc -Wall -Wextra -Werror -pthread -fsanitize=address -g -I includes srcs/*.c -o philo_asan

# ThreadSanitizer (data races)
cc -Wall -Wextra -Werror -pthread -fsanitize=thread -g -I includes srcs/*.c -o philo_tsan

# Lancer les tests
./philo_asan 5 800 200 200 7
./philo_tsan 5 800 200 200 7
./philo_tsan 4 310 200 100  # tester aussi le cas de mort

# Si "WARNING: ThreadSanitizer: data race" apparaît → corriger le mutex manquant
# Si "ERROR: AddressSanitizer: stack-use-after-return" → corriger le &var local
# Si "ERROR: LeakSanitizer: detected memory leaks" → ajouter les free manquants
08

Précision temporelle

La fiche d'évaluation impose un délai de moins de 10ms entre la mort réelle d'un philosophe et l'affichage de "died". Sur un système Linux moderne, cela semble facile, mais en pratique le scheduler peut endormir le monitor pendant plusieurs millisecondes. Trois optimisations sont nécessaires pour garantir la précision.

Monitor à 200µs

Le monitor parcourt tous les philosophes toutes les 200µs. Avec 200 philosophes, chaque itération prend ~50µs (mutex lock/unlock par philo), donc le cycle complet prend ~250µs — largement sous les 10ms. Au prix d'une légère consommation CPU (0.5% avec 200 philos), on garantit une détection quasi-immédiate de la mort.

philo/srcs/monitor.c// usleep(200)
void	*ph_monitor(void *arg)
{
	t_philo	*philos;

	philos = (t_philo *)arg;
	while (!ph_check_stop(philos->rules))
	{
		if (ph_check_death(philos->rules, philos))
			return (NULL);
		if (ph_check_finished(philos->rules, philos))
			return (NULL);
		usleep(200);  /* 200µs : 5000 checks/seconde */
	}
	return (NULL);
}

ph_usleep avec busy-wait

usleep(ms * 1000) n'est pas précis : le scheduler peut réveiller le thread plusieurs millisecondes en retard. Pour garantir que time_to_eat et time_to_sleep sont respectés à la milliseconde près, on utilise un busy-wait adaptatif : usleep(100) pour les longues attentes, usleep(20) pour les 2 dernières millisecondes.

philo/srcs/utils.c// ph_usleep précis
void	ph_usleep(long ms)
{
	long	start;
	long	target;

	start = ph_get_current_time();
	target = start + ms;
	while (ph_get_current_time() < target)
	{
		if (target - ph_get_current_time() > 2)
			usleep(100);  /* longue attente : usleep standard */
		else
			usleep(20);   /* 2 dernières ms : polling serré */
	}
}

Décalage temporel des pairs

Pour éviter que tous les philosophes pairs ne tentent de prendre leurs fourchettes en même temps (et créent une contention), on les fait attendre time_to_eat / 2 au démarrage. Cela laisse les impairs manger en premier, puis les pairs prennent le relais. Le décalage de time_to_eat / 2 est optimal : assez long pour que les impairs finissent de manger, assez court pour ne pas gaspiller de temps.

philo/srcs/philo.c// décalage pairs
void	*ph_routine(void *arg)
{
	t_philo	*philo;

	philo = (t_philo *)arg;
	/* Initialiser last_meal avant tout (sinon meurt avant le 1er repas) */
	pthread_mutex_lock(&philo->meal_mutex);
	philo->last_meal = ph_timestamp(philo->rules);
	pthread_mutex_unlock(&philo->meal_mutex);

	/* Décalage des pairs : laisse les impairs manger en premier */
	if (philo->id % 2 == 0)
		ph_usleep(philo->rules->time_to_eat / 2);

	while (!ph_check_stop(philo->rules))
	{
		/* ... take_forks, eat, sleep, think ... */
	}
	return (NULL);
}
// Validation timing

Pour valider le délai mort < 10ms, lancez ./philo 2 200 250 200. Le philosophe 1 mange à t=0, termine à t=250. Le philosophe 2 attend sa fourchette, et à t=200 (time_to_die) il devrait mourir. Si le timestamp affiché est 201 ou 202, le délai est de 1-2ms ✅. Si c'est 210+, le monitor n'est pas assez rapide → réduire le usleep(200) à usleep(100) ou usleep(50).

09

Audit fiche d'évaluation

Cette section reproduit la fiche d'évaluation officielle 42 (42cursus-philosophers) et audite notre implémentation point par point. Chaque critère est noté ✅ (pass) ou ❌ (fail) avec une explication.

Prérequis

CritèreStatutVérification
Pas de tricheCode original, pas de copie
Dépôt Git appartient à l'étudiantgithub.com/rkpequod-max/Philosophers
Pas de crash / segfaultTesté avec tous les cas edge (1 philo, 200 philos, must_eat, négatifs)
Pas d'erreur Normnorminette exit 0 sur les 3 programmes
Pas de data raceThreadSanitizer clean (helgrind équivalent)
Pas de leakAddressSanitizer + LeakSanitizer clean
Pas de fonction interditenm -u vérifié

Variables globales

// Critère éliminatoire

Si une variable globale est utilisée pour gérer les ressources partagées entre philosophes, l'évaluation s'arrête immédiatement — note 0. Notre implémentation utilise exclusivement des variables locales et des mutex/sem pour la synchronisation. grep -rn '^[a-zA-Z].*g_' srcs/ includes/ retourne vide ✅.

Mandatory (philo) — code

CritèreStatutDétail
Un thread par philosophepthread_create par philo dans ph_create_threads
Une fourchette par philosopheN mutex alloués dans ph_init_mutexes
Mutex par fourchette pour vérifier/changer l'étatpthread_mutex_lock(&forks[i]) dans ph_take_forks
Outputs jamais mélangésprint_mutex global verrouille printf
Mutex empêche mourir + manger simultanésmeal_mutex par philo protège last_meal (monitor + routine l'utilisent)

Mandatory — tests

TestAttenduRésultat
1 800 200 200Le philo meurt✅ "801 1 died"
5 800 200 200Aucun mort✅ 0 morts
5 800 200 200 7Stop après 7 repas✅ exit 0
4 410 200 200Aucun mort✅ 0 morts
4 310 200 100Un mort✅ "311 1 died"
2 200 250 200Mort, delay < 10ms✅ "201 1 died" (1ms de délai)
200 800 200 200Aucun mort (stress)✅ 0 morts

Bonus (philo_bonus) — code

CritèreStatutDétail
Un processus par philosophefork() dans ph_create_processes
Main process n'est pas un philosopheLe parent supervise uniquement (sem_wait(stop))
Pas de processus orphelin à la finph_kill_and_wait : SIGKILL + waitpid sur tous
Sémaphore unique pour les fourchettessem_open("/philo_bonus_forks", N)
Output protégé contre multiple accèssem_wait(print) autour des write
Sémaphore empêche mourir + manger simultanésmeal_mutex par philo + first_death sem

Bonus — tests

Identiques au mandatory — tous passent ✅ (mêmes résultats à 1ms près).

// Auto-évaluation finale

Mandatory : 7/7 tests passent + tous les critères code respectés = 100% mandatory. Bonus : 7/7 tests passent + tous les critères code respectés = 100% bonus. ThreadSanitizer : 0 data race. AddressSanitizer : 0 leak, 0 stack-use-after-return. Norminette : 0 erreur. Le projet est prêt pour la défense peer-to-peer.

10

Makefile & compilation

Le Makefile de chaque programme suit les conventions 42 : règles all, clean, fclean, re obligatoires, compilation avec -Wall -Wextra -Werror, et le flag -pthread pour linker la bibliothèque pthread.

philo/Makefile// mandatory
NAME            = philo

CC              = cc
CFLAGS          = -Wall -Wextra -Werror -pthread

INCLUDES        = -I includes

SRCS_DIR        = srcs
SRCS            = $(SRCS_DIR)/main.c \
                  $(SRCS_DIR)/error.c \
                  $(SRCS_DIR)/parse.c \
                  $(SRCS_DIR)/init.c \
                  $(SRCS_DIR)/philo.c \
                  $(SRCS_DIR)/monitor.c \
                  $(SRCS_DIR)/print.c \
                  $(SRCS_DIR)/run.c \
                  $(SRCS_DIR)/utils.c

OBJS            = $(SRCS:.c=.o)

HEADERS         = includes/philo.h

.PHONY: all clean fclean re

all:            $(NAME)

$(NAME):        $(OBJS) $(HEADERS)
                $(CC) $(CFLAGS) $(OBJS) -o $(NAME)

%.o:            %.c $(HEADERS)
                $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@

clean:
                rm -f $(OBJS)

fclean:         clean
                rm -f $(NAME)

re:             fclean all

Points clés

// -pthread flag
Essentiel pour linker libpthread. Sans lui, erreur undefined reference to pthread_create au link. Sur Linux moderne, pthread est intégrée à libc, mais le flag reste nécessaire pour les defines comme _REENTRANT.
// Compilation par .o
Règle pattern %.o: %.c pour compilation incrémentale. Si on modifie philo.c, seul philo.o est recompilé. Vérifiez avec make deux fois → "Nothing to be done".
// Pas de libft
Le sujet interdit l'utilisation de la libft pour Philosophers. On doit utiliser directement printf, malloc, free, write, etc. de la libc. C'est plus simple mais exige d'écrire les helpers (putnbr, putstr) soi-même pour philo_bonus.
// .PHONY
Les règles all clean fclean re sont déclarées .PHONY pour indiquer qu'elles ne correspondent pas à des fichiers. Sans ça, si un fichier nommé "clean" existe, make clean ne s'exécuterait pas.
11

Pièges & edge cases

Cette section recense les pièges classiques de Philosophers et les solutions implémentées. À lire avant la défense pour anticiper les questions du correcteur.

PiègeSymptômeSolution
Deadlock (tous prennent gauche) Simulation figée, aucun mort Ordre asymétrique pairs/impairs + décalage temporel time_to_eat/2
nb_philo == 1 Deadlock sur right_fork (même que left) Cas spécial : usleep(time_to_die + 10) puis unlock, meurt normalement
Data race sur last_meal TSan WARNING, mort detectée trop tard meal_mutex par philosophe (lock dans ph_eat ET dans ph_check_death)
Stack-use-after-return monitor ASan ERROR, comportement imprévisible Passer philos (heap) au lieu de &philos (stack) à pthread_create
Plusieurs messages "died" 2+ philosophes meurent quasi-simultanément Sémaphore first_death (init=1) + sem_trywait : seul le premier affiche
Buffering stdout avec fork Messages non affichés ou dupliqués dans philo_bonus write() au lieu de printf, avec ph_putnbr_fd + ph_putstr_fd
Processus orphelins philo_bonus laisse des zombies après exit ph_kill_and_wait : SIGKILL + waitpid sur tous les enfants
Délai mort > 10ms Timestamp "died" trop tardif Monitor à 200µs + ph_usleep busy-wait pour les 2 dernières ms
Philosophe meurt avant 1er repas Mort à t=0 ou avant de prendre fourchettes Initialiser last_meal = ph_timestamp() au début de ph_routine
must_eat jamais atteint Simulation tourne indéfiniment avec must_eat ph_check_finished compte les philos avec meals_eaten >= must_eat
Arguments négatifs ou non numériques Comportement indéfini ph_is_number vérifie chaque char est un digit, ph_validate_values vérifie ranges
Sémaphore nommé qui persiste sem_open échoue après un crash précédent sem_unlink avant sem_open dans ph_init_semaphores
Timestamp négatif au démarrage gettimeofday avant init de start_time Fixer start_time dans ph_init avant de créer les threads
Contenu de stdout entrelacé "X has takY is eatingen a fork" print_mutex (philo) ou sem print (philo_two/bonus) autour de printf
Variable globale interdite Note 0 à l'évaluation Tout en local + mutex. Pattern function-static si besoin de "globale"
« Les androïdes ne meurent pas d'un bug — ils meurent d'une omission. Vérifiez chaque mutex, chaque thread, chaque timestamp. Le moindre data race, et c'est toute la mission qui s'effondre. »
— Commandant White, briefing pré-mission