1. Introduction : Que fait le noyau Linux lorsqu’un système semble figé ?
Lorsqu’un serveur Linux cesse de répondre — les requêtes ne sont plus traitées, les logs stagnent, mais SSH reste accessible — il est tentant de redémarrer la machine. Pourtant, le noyau dispose de mécanismes intégrés pour détecter ces situations critiques. Deux d’entre eux sont particulièrement utiles : la détection des tâches bloquées (hung tasks) et celle des soft lockups.
Le mécanisme hung task surveille les processus coincés dans l’état D (TASK_UNINTERRUPTIBLE), c’est-à-dire en attente d’une ressource bloquante (E/S disque, verrou noyau, etc.) sans possibilité d’interruption, même par un SIGKILL. Le soft lockup, quant à lui, détecte lorsqu’un cœur CPU est monopolisé trop longtemps par du code noyau non préemptible, empêchant toute planification.
2. Mécanisme de détection des tâches bloquées
2.1 Comprendre l’état D et ses risques
Un processus en état D est en attente d’un événement noyau critique. Contrairement à l’état S (sleep interruptible), il ignore tous les signaux. Si cette attente dure anormalement longtemps — à cause d’un périphérique défaillant ou d’un bogue dans un pilote — le processus devient une hung task, consommant des ressources sans libération possible.
2.2 Fonctionnmeent interne du détecteur
Le noyau exécute un thread démon nommé khungtaskd, défini dans kernel/hung_task.c. Ce thread vérifie périodiquement tous les processus :
static int hung_detector_thread(void *unused)
{
for (;;) {
unsigned long timeout = hung_task_timeout_secs;
unsigned long interval = hung_task_check_interval_secs;
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(interval * HZ);
if (!freezing(current))
check_hung_uninterruptible_tasks(timeout);
}
return 0;
}
La fonction check_hung_uninterruptible_tasks() parcourt la liste des tâches. Pour chaque tâche en TASK_UNINTERRUPTIBLE, elle compare le temps écoulé depuis last_switch_count (ou équivalent selon la version) avec le seuil configuré via /proc/sys/kernel/hung_task_timeout_secs. Seules les tâches strictement non-interruptibles sont considérées ; les tâches TASK_KILLABLE sont ignorées.
2.3 Exemple pratique : génération contrôlée d’une tâche bloquée
Le module noyau suivant crée deux threads : l’un en TASK_UNINTERRUPTIBLE, l’autre en TASK_KILLABLE, afin d’observer le comportement du détecteur :
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/sched.h>
static struct task_struct *worker_unint = NULL;
static struct task_struct *worker_killable = NULL;
static int unint_worker_fn(void *data)
{
while (!kthread_should_stop()) {
set_current_state(TASK_UNINTERRUPTIBLE);
schedule(); // Ne se réveillera jamais seul
}
return 0;
}
static int killable_worker_fn(void *data)
{
while (!kthread_should_stop()) {
set_current_state(TASK_KILLABLE);
schedule();
}
return 0;
}
static int __init trigger_hung_init(void)
{
worker_unint = kthread_run(unint_worker_fn, NULL, "hung-unint");
worker_killable = kthread_run(killable_worker_fn, NULL, "hung-killable");
return 0;
}
static void __exit trigger_hung_exit(void)
{
if (worker_unint)
kthread_stop(worker_unint);
if (worker_killable)
kthread_stop(worker_killable);
}
module_init(trigger_hung_init);
module_exit(trigger_hung_exit);
MODULE_LICENSE("GPL");
Après chargement du module, le noyau affichera un rapport après le délai configuré (par défaut 120 secondes), incluant la pile d’appels (call trace) de la tâche bloquée, permettant d’identifier l’origine exacte du blocage.