Institut de Physique Théorique Philippe Meyer


Accueil > Pratique > Documentation

Points de contrôle

Afin d’éviter de perdre des jours de calcul en cas d’arrêt imprévu d’un job, il est préférable de faire régulièrement des points de contrôle, c’est-à-dire des sauvegardes sur support pérenne de l’état d’un processus, afin de pouvoir le redémarrer plus tard en reprennant là où il en était à la dernière sauvegarde.

Pour ce faire, on peut modifier un programme pour qu’il écrive régulièrement dans un fichier sur le disque dur les variables qui lui permettront de reprendre plus tard son calcul, et pour qu’il vérifie au démarrage s’il doit exploiter une telle sauvegarde. Les points de contrôles peuvent être déclenchés à des intervalles prédéterminés d’itérations de boucle du programme ou à certains moments clés, mais il peut être plus intéteressant de les déclencher à un intervalle de temps régulier ou juste avant un redémarrage de l’ordinateur sur lequel tourne le processus, en traitant la réception de signaux Posix.

Les signaux Posix

Sur un système de type Unix, les signaux Posix sont des messages envoyés aux processus, consistant en un nombre à la signification standardisée. Il existe des noms standards pour chacun des signaux. Ces signaux peuvent être envoyés par le noyau, par la commande kill, par des combinaisons de touches telles que Ctrl-C ou Ctrl-Z sur la ligne de commande du shell, ou encore par un autre processus. Parmi les signaux les plus communs on trouve [1] :

  • KILL : met fin de façon abrupte à un processus ;
  • USR1 et USR2 : destinés à une utilisation personnalisée ;
  • SEGV : envoyé par le noyau en cas de violation mémoire ;
  • TERM : demande à un processus de se terminer ;
  • INT : interrompt un processus (déclenché par Ctrl-C) ;
  • STOP : suspend un processus ;
  • CONT : faire reprendre l’exécution un processus suspendu.

Le système d’exploitation a un traitement par défaut pour chaque signal, qu’il applique aux processus quand ils les reçoivent. Cependant un programme peut intercepter des signaux (sauf KILL et STOP) pour leur appliquer un traitement personnalisé. C’est cette fonctionnalité que l’on peut exploiter pour déclencher des points de contrôle. En effet on peut faire envoyer des signaux par Grid Engine à intervalle de temps régulier. De plus, au moment où un poste Linux du LPT s’arrête proprement, il envoie le signal TERM à tous les processus environ 10 secondes avant de les tuer avec KILL.

En langage C l’appel système sigaction() permet d’associer un signal à une fonction. Aussitôt qu’un signal est reçu, le programme interrompt toute tâche en cours et exécute la fonction associée au signal. La procédure est de définir une fonction du prototype suivant :

void some_handler(int signal);

puis d’initialiser une structure sigaction, en définissant notament sa fonction associée :

#include <signal.h>

struct sigaction action;

action.sa_handler = some_handler;
action.sa_flags = 0;

et enfin d’appeler sigaction() pour associer un signal donné à cette fonction [2] :

sigaction(SIGUSR1, &action, NULL);

Étant donné que le signal peut-être délivré n’importe quand, par exemple au milieu d’une boucle ou d’un appel de sous-fonction, il est préférable de ne pas faire effectuer de tâche compliquée directement depuis la fonction appelée par le gestionaire de signal, mais de simplement changer une variable globale depuis cette fonction, puis de vérifier la valeur de cette variable à un endroit adequat du programme principal, et enfin de réinitialiser cette variable.

Un problème peut survenir si un programme a défini l’interception de plusieurs signaux et qu’un signal est envoyé pendant le traitement d’un autre, ce qui peut aboutir à des situations plus ou moins imprévisibles. Pour éviter ce genre de conflit, en même temps qu’on associe un signal à une fonction avec sigaction(), on peut définir un masque de signaux qui seront bloqués le temps que le signal d’origine soit traité. On complète pour cela la structure sigaction. Ainsi si on veut bloquer le signal TERM dans notre exemple précédent, on ajoute les lignes suivantes avant l’appel à sigaction() :

/* initialise le masque en le vidant */
sigemptyset(&action.sa_mask);
/* ajoute le signal TERM au masque */
sigaddset(&action.sa_mask, SIGTERM);

Utilisation avec Grid Engine

Une fois le programme adapté à la gestion des signaux, on peut l’interfacer avec Grid Engine pour en profiter pleinement. Dans la configuration de Gride Engine au LPT, est défini un environement de point de contrôle nommé transp_usr1 qui permet d’envoyer au job un signal USR1 toutes les heures. On active son utilisation de la façon suivante :

qsub -ckpt transp_usr1 $HOME/prog/simulation

On peut demander à Grid Engine d’envoyer les signaux à un intervalle de temps plus grand que celui par défaut en utilisant l’option -c avec un temps défini au format HH:MM:SS. Ainsi pour un intervalle de 6 heures :

qsub -ckpt transp_usr1 -c 06:00:00 $HOME/prog/simulation

Il est possible de faire relancer un job par Grid Engine quand il détecte que ce job a été interrompu brutalement ou si l’ordinateur sur lequel tourne ce job ne donne pas signe de vie pendant un temps trop long. Dans ce cas le job repart dans la queue des jobs et est lancé sur l’ordinateur que Grid Engine lui attribue (généralement différent de celui précédement utilisé). Le job garde le même job_id disponible via la variable d’environement $JOB_ID, et a la variable $RESTARTED à 1 (elle vaut 0 sinon). On peut exploiter ces deux variables d’environement pour savoir quand utiliser une sauvegarde et pour distinguer les jobs entre eux. On active la relance automatique des jobs avec l’option -r :

qsub -ckpt transp_usr1 -r yes $HOME/prog/simulation

Remarque : l’option -r est également utilisable sans l’environement de point de contrôle. Cela peut-être utile si on ne désire pas faire de sauvegarde régulière, mais qu’on veut qu’un job soit relancé en cas de problème.

Enfin il est possible de demander à Grid Engine d’envoyer un signal USR2 une minute avant le signal KILL envoyé par qdel. On utilise pour cela l’option -notify :

qsub -notify $HOME/prog/simulation

Exemple de programme

Voici un exemple de programme C pour illustrer l’utilisation des signaux Posix. Attention : ce qui suit n’est qu’un exemple et peut nécessiter beaucoup d’adaptations. Il faut notamment garder à l’esprit que le signal USR1 arrive généralement à un moment où on a le temps de sauvegarder beaucoup de données, tandis qu’on a seulement 10 secondes avec le signal TERM. Il est peut également être judicieux de prévoir le cas d’un crash de la machine pendant la sauvegarde (prévoir deux niveaux de sauvegarde ?).

Le programme suivant calcule la somme des entiers de 1 à LIMIT :

#include <stdio.h>
#include <stdlib.h>
#define LIMIT 1000

int main()
{
       unsigned int i, sum;

       i = 1;
       sum = 0;
       while (i <= LIMIT)
       {
               sum += i++;
       }
       printf("somme=%u\n", sum);
       exit(0);
}

On crée une structure de données ad hoc pour sauver les données permettant au programme de reprendre son calcul [3] :

typedef struct backup
{
    unsigned int counter;
    unsigned int sum;
} backup_data;

On écrit des fonctions permettant de sauver l’état du programme dans un fichier, de restaurer l’état et d’effacer le fichier d’état :

int save(backup_data backup, char *backup_file_name);
int restore(backup_data *backup, char *backup_file_name);
int clean_backup(char *backup_file_name);

On prépare le programme à gérer le signal USR1 :

int flag_usr1 = 0;
void sigusr1_handler(int signal)
{
    flag_usr1 = 1;
}
void prepare_signals(void)
{
    struct sigaction action_usr1;

    action_usr1.sa_handler = sigusr1_handler;
    action_usr1.sa_flags = 0;
    sigaction(SIGUSR1, &action_usr1, NULL);
}

On interface le programme avec la gestion des signaux et des sauvegardes :

int main()
{
       unsigned int i, sum;

       backup_data backup;
       char *backup_file_name = "backup";
       int flag_restarted = 0;

       prepare_signals();

       if (!strcmp(getenv("RESTARTED"), "1"))
       {
               restore(&backup, backup_file_name);
               flag_restarted = 1;
       }

       /* l'initialisation de i et sum dépend de la restauration */
       if (flag_restarted)
       {
               i = backup.counter;
               sum = backup.sum;
       }
       else
       {
               i = 1;
               sum = 0;
       }

       while (i <= LIMIT)
       {
               sum += i++;

               /* On traite le signal à un endroit du programme aproprié pour les
                * sauvegardes */
               if (flag_usr1 == 1)
               {
                       backup.counter = i;
                       backup.sum = sum;
                       save(backup, backup_file_name);
                       flag_usr1 = 0;
               }
       }
       printf("somme=%u\n", sum);
       /* ne pas oublier d'effacer un éventuel fichier de sauvegarde */
       clean_backup(backup_file_name);
       exit(0);
}

On ajoute la gestion du signal TERM et on protège mutuellement les gestions des signaux USR1 et TERM. À cette fin, on ajoute les lignes suivantes à la fonction prepare_signals() :

struct sigaction action_term;

/* Avant l'appel à sigaction(SIGUSR1, ...) */
sigemptyset(&action_usr1.sa_mask);
sigaddset(&action_usr1.sa_mask, SIGTERM);

/* On configure la gestion du signal TERM de façon symétrique à celle du signal
 * USR1.
 * La fonction sigterm_handler() est similaire à sigusr1_handler(). */
action_term.sa_handler = sigterm_handler;
action_term.sa_flags = 0;
sigemptyset(&action_term.sa_mask);
sigaddset(&action_term.sa_mask, SIGUSR1);
sigaction(SIGTERM, &action_term, NULL);

Voici l’exemple complet avec une gestion plus sérieuse des valeurs de retour des fonctions :

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#define LIMIT 1000

/* structure pour sauver l'état */
typedef struct backup
{
	unsigned int counter;
	unsigned int sum;
} backup_data;

/* flags pour déclencher des sauvegardes */
int flag_usr1 = 0;
int flag_term = 0;

/* Appelée en cas de réception du signal SIGUSR1 */
void sigusr1_handler(int signal)
{
	flag_usr1 = 1;
}

/* Appelée en cas de réception du signal SIGTERM */
void sigterm_handler(int signal)
{
	flag_term = 1;
}

/* prépare le programme à traiter les signaux */
void prepare_signals(void)
{
	struct sigaction action_usr1, action_term;

	action_usr1.sa_handler = sigusr1_handler;
	action_usr1.sa_flags = 0;
	sigemptyset(&action_usr1.sa_mask);
	sigaddset(&action_usr1.sa_mask, SIGTERM);
	if (sigaction(SIGUSR1, &action_usr1, NULL) < 0)
	{
		perror ("sigaction");
		exit(-1);
	}

	action_term.sa_handler = sigterm_handler;
	action_term.sa_flags = 0;
	sigemptyset(&action_term.sa_mask);
	sigaddset(&action_term.sa_mask, SIGUSR1);
	if (sigaction(SIGTERM, &action_term, NULL) < 0)
	{
		perror("sigaction");
		exit(-1);
	}
}

/*
 * Remplit le fichier backup_file_name avec le contenu de la variable backup
 * Retourne 1 en cas de succès, 0 sinon
 */
int save(backup_data backup, char *backup_file_name)
{
	FILE *backup_file;

	if ((backup_file = fopen(backup_file_name, "w")))
	{
		if (!fwrite(&backup, sizeof(backup), 1, backup_file))
		{
			perror("Problème fwrite dans save()");
			return(0);
		}
		if (fclose(backup_file))
		{
			perror("Problème fclose dans save()");
			return(0);
		}
		/* pour forcer l'écriture immédiate des données sur le support physique */
		fsync(fileno(backup_file));
		return(1);
	}
	else
	{
		perror("Problème fopen dans save()");
		return(0);
	}
}

/*
 * Remplit la variable backup avec le contenu du fichier backup_file_name
 * Retourne 1 en cas de succès, 0 sinon
 */
int restore(backup_data *backup, char *backup_file_name)
{
	FILE *backup_file;

	if ((backup_file = fopen(backup_file_name, "r")))
	{
		if (!fread(backup, sizeof(backup_data), 1, backup_file))
		{
			perror("Problème fread dans restore()");
			return(0);
		}
		if (fclose(backup_file))
		{
			perror("Problème fclose dans restore()");
			return(0);
		}
		return(1);
	}
	else
	{
		perror("Problème fopen dans restore()");
		return(0);
	}
}

/*
 * Efface le fichier de sauvegarde
 * Retourne 1 en cas de succès, 0 sinon
 */
int clean_backup(char *backup_file_name)
{
	struct stat stat_backup;

	if (stat(backup_file_name, &stat_backup) == -1)
	{
		perror("Problème stat dans clean_backup()");
		return(0);
	}
	if (unlink(backup_file_name) == -1)
	{
		perror("Problème unlink dans clean_backup()");
		return(0);
	}
	return(1);
}

int main()
{
	unsigned int i, sum;

	backup_data backup;
	struct stat stat_backup;
	char *JOB_ID = getenv("JOB_ID") != NULL ? getenv("JOB_ID") : "";
	char *RESTARTED = getenv("RESTARTED") != NULL ? getenv("RESTARTED") : "0";
	char *backup_file_prefix = "backup.";
	char backup_file_name[strlen(backup_file_prefix) + strlen(JOB_ID) + sizeof('\0')];
	int flag_restarted = 0;

	sprintf(backup_file_name, "%s%s", backup_file_prefix, JOB_ID);
	prepare_signals();

	if (!strcmp(RESTARTED, "1"))
	{
		/* tentative de restauration */
		if (stat(backup_file_name, &stat_backup) == -1)
		{
			/* le fichier n'existe pas */
			perror("stat");
		}
		else
		{
			if (restore(&backup, backup_file_name))
			{
				flag_restarted = 1;
			}
		}
	}

	/* l'initialisation de i et sum dépend de la restauration */
	if (flag_restarted)
	{
		i = backup.counter;
		sum = backup.sum;
	}
	else
	{
		i = 1;
		sum = 0;
	}

	while (i <= LIMIT)
	{
		sum += i;
		i++;

		/* On traite le signal à un endroit du programme aproprié pour les
		 * sauvegardes */
		if (flag_usr1 == 1 || flag_term == 1)
		{
			backup.counter = i;
			backup.sum = sum;
			save(backup, backup_file_name);
			flag_usr1 = 0;
			flag_term = 0;
		}
	}
	printf("somme=%u\n", sum);
	/* ne pas oublier d'effacer un éventuel fichier de sauvegarde */
	if (!stat(backup_file_name, &stat_backup))
	{
		clean_backup(backup_file_name);
	}
	exit(0);
}

[1pour une liste complète, voir le page de manuel de kill et la commande kill -l

[2certaines anciennes documentations font référence à l’appel système signal(), qui est obsolète et problématique

[3l’utilisation de variables de taille dynamique obligerait à utiliser un système plus complexe, avec, par exemple, des indexes et des appels à fseek()