• Aucun résultat trouvé

Signaler une erreur

Dans le document Programmation système en C sous (Page 63-68)

Il y a des cas où la gestion d'erreur doit être moins drastique qu'un arrêt anormal du programme. Pour cela, les appels-système remplissent la variable globale externe errno avec une valeur numérique entière représentant le type d'erreur qui s'est produite. Toutes les valeurs représentant une erreur sont non nulles.

ATTENTION Le fait que errno soit remplie avec une valeur non nulle n'est pas suffisant pour en déduire qu'une erreur s'est produite. Il faut pour cela que l'appel-système échoue explicitement (la plupart du temps en renvoyant -1 et non pas 0). Ceci est encore plus vrai avec des routines de bibliothèque qui peuvent invoquer plusieurs appels-système et remédier aux conditions d'erreur. errno sera alors modifiée à plusieurs reprises avant le retour de la fonction.

Il existe des constantes symboliques représentant chaque erreur possible. Elles sont définies dans le fichier <errno.h>. Toutefois, celui-ci inclut automatiquement

<bits/errno.h>, <linux/errno.h> et <asm/errno.h>, qui définissent l'essentiel des constantes d'erreur. Il est bon de connaître ces fichiers car un coup d'oeil rapide permet parfois d'identifier une erreur qu'on n'avait pas prévue, à partir de son numéro. Les principales erreurs qu'on rencontre fréquemment dans les appels-système sont décrites dans le tableau ci-dessous. Nous en avons limité la liste aux plus courantes. Il en existe de nombreuses autres, par exemple dans le domaine de la programmation réseau, que nous détaillerons le moment venu.

Les erreurs signalées par un astérisque ne sont pas définies par Posix. l .

Nom Signification

E2BIG La liste d'arguments fournie à l'une des fonctions de la famille exec( ) est trop longue.

EACCES L'accès demandé est interdit, par exemple dans une tentative d'ouverture de fichier avec open( ) .

EAGAIN L’opération est momentanément impossible. il faut réessayer. Par exemple, on demande une lecture non bloquante avec read( ) , mais aucune donnée n'est encore disponible.

EBADF Le descripteur de fichier transmis à l'appel-système, par exemple close( ), est invalide.

EBUSY Le répertoire ou le fichier considéré est en cours d'utilisation. Ainsi, umount ( )

Nom Signification

ECHILD Le processus attendu par waitpid( ) ou wait4( ) n'existe pas ou n'est pas un fils du processus appelant.

EDEADLK Le verrouillage en écriture par fcntl( ) du fichier demandé conduirait à un blocage.

EDOM La valeur transmise à la fonction mathématique est hors de son domaine de définition. Par exemple. on appelle acos( ) avec une valeur inférieure à -1 ou supérieure à 1.

EEXIST Le fichier ou le répertoire indiqué pour une création existe déjà. Par exemple, avec open( ), mkdir( ), mknod( )...

EFAULT Un pointeur transmis en argument pointe en dehors de l'espace d'adressage du processus. Cette erreur révèle un bogue grave dans le programme.

EFBIG On a tenté de créer un fichier de taille plus grande que la limite autorisée pour le processus.

EINTR L'appel-système a été interrompu par l'arrivée d'un signal qui a été intercepté par un gestionnaire installé par le processus.

ET NVAL Un argument de type entier, ou représenté par une constante symbolique, a une valeur invalide ou incohérente.

EIO Une erreur d'entrée-sortie de bas niveau s'est produite pendant un accès au fichier.

EISDIR Le descripteur de fichier transmis à l'appel-système. par exemple read( ), correspond à un répertoire.

ELOOP(*) On a rencontré trop de liens symboliques successifs, il y a probablement une boucle entre eux.

EMFILE Le processus a atteint le nombre maximal de fichiers ouverts simultanément.

EMLINK On a déjà créé le nombre maximal de liens sur un fichier.

ENAMETOOLONG Le chemin d'accès transmis en argument, par exemple pour chdir( ), est trop long.

ENFILE On a atteint le nombre maximal de fichiers ouverts simultanément sur l'ensemble du système.

ENODEV Le fichier spécial de périphérique n'est pas valide, par exemple dans l'appel-système mount().

ENOENT Un répertoire contenu dans le chemin d'accès fourni à l'appel-système n'existe pas, ou est un lien symbolique pointant dans le vide.

ENOEXEC Le fichier exécutable indiqué à l'un des appels de la famille exec( ) n'est pas dans un format reconnu par le noyau.

ENOLCK Il n'y a plus de place dans la table système pour ajouter un verrou avec l'appel-système fcntl( ).

ENOMEM II n'y a plus assez de place mémoire pour allouer une structure supplémentaire dans une table système.

ENOSPC Le périphérique sur lequel on veut créer un nouveau fichier ou écrire des données supplémentaires n'a plus de place disponible.

ENOSYS La fonctionnalité demandée par l'appel-système n'est pas disponible dans le noyau. II peut s'agir d'un problème de version ou d'options lors de la compilation du noyau.

ENOTBLK(*) Le fichier spécial qu'on tente de monter avec mount( ) ne représente pas un périphérique de type « blocst.

ENOTDIR Un élément du chemin d'accès fourni n'est pas un répertoire.

ENOTEMPTY Le répertoire qu'on veut détruire n'est pas vide, ou le nouveau nom d'un répertoire à renommer existe déjà et n'est pas vide.

ENOTTY Le fichier indiqué en argument à ioctl ( ) n'est pas un terminal.

Nom Signification

ENXIO Le fichier spécial indiqué n'est pas reconnu par le noyau (par exemple un numéro de noeud majeur invalide).

EPERM Le processus appelant n'a pas les autorisations nécessaires pour effectuer l'opération prévue. Souvent. il s'agit d'une fonctionnalité réservée à root.

EPIPE Tentative d'écriture avec write( ) dans un tube dont l'autre extrémité a été fermée parle processus lecteur. Cette erreur n'est envoyée que si le signal SIGPIPE est bloqué ou ignoré.

ERANGE Une valeur numérique attendue dans une fonction mathématique est invalide.

EROFS On tente une modification sur un fichier appartenant à un système de fichiers monté en lecture seule.

ESPIPE On essaye de déplacer le pointeur de lecture. avec lseek( ), sur un descripteur de fichier ne le permettant pas, comme un tube ou une socket.

ESRCH Le processus visé, par exemple avec kill( ) . n'existe pas.

ETXTBSY(*) On essaye d'exécuter un fichier déjà ouvert en écriture par un autre processus.

EWOULDBLOCK(*) Synonyme de EAGAIN qu'on rencontre dans la description de nombreuses fonctionnalités réseau.

EXDEV On essaye de renommer un fichier ou de créer un lien matériel entre deux systèmes de fichier différents.

On voit, à l'énoncé d'une telle liste (qui ne représente qu'un tiers environ de toutes les erreurs pouvant se produire sous Linux), qu'il est difficile de gérer tous les cas à chaque appel-système effectué par le programme.

Alors que faire si. par exemple, l'appel-système open( ) échoue ? À lui seul, il peut déjà renvoyer une bonne quinzaine d'erreurs différentes. Tout dépend du degré de convivialité du logiciel développé. Dans certains cas, on peut se contenter de mettre un message sur la sortie d'erreur «impossible d'ouvrir le fichier xxx », et arrêter le programme. A l'opposé, on peut aussi diagnostiquer qu'un des répertoires du chemin d'accès est invalide et afficher pour l'utilisateur la liste des répertoires du même niveau pour qu'il corrige son erreur.

Ainsi, certaines erreurs ne doivent jamais se produire dans un programme bien débogué.

C'est le cas de EFAULT, qui signale un pointeur mal initialisé, de EDOM ou ERANGE, qui indiquent qu'une fonction mathématique a été appelée sans vérifier si les variables appartiennent à son domaine de définition. Ces cas-là peuvent être contrôlés dans des appels à assert( ), car il s'agit de bogues à éliminer avant la distribution du logiciel.

Dans d'autres cas, le problème concerne le système, et le pauvre programme ne peut rien faire pour corriger l'erreur. C'est le cas par exemple de ENOMEM, qui indique que le noyau n'a plus assez de place mémoire, ou de ENFILE, qui se produit lorsque le nombre maximal de fichiers ouverts sur le système est atteint. Il n'y a guère d'autres possibilités alors que d'abandonner l'opération après avoir signalé le problème à l'utilisateur.

La règle absolue est de ne jamais passer sous silence les conditions d'erreur qui paraissent improbables. Si une application doit être distribuée largement et utilisée pendant de longues heures par ses utilisateurs, il est pratiquement certain qu'elle sera un jour confrontée au problème d'une partition disque saturée. Ignorer le code de retour de

Si on ne désire pas traiter au cas par cas toutes les erreurs possibles, on peut employer la fonction strerror( ) , déclarée dans <string.h> ainsi :

char * strerror (int numero erreur);

Cette fonction renvoie un pointeur sur une chaîne de caractères statique décrivant l'erreur produite. Cette chaîne de caractères peut être écrasée à chaque nouvel appel de strerror( ). Pour éviter ce problème dans le cas d'une programmation multi-thread. on peut utiliser l'extension Gnu strerror_r( ), déclarée ainsi :

char * strerror_r (int numero erreur, char * chaire, size_t longueur) Cette fonction n'écrit jamais dans la chaîne plus d'octets que la longueur indiquée. y compris le caractère nul final.

Dans tous les cas, il convient de consulter la page de manuel des appels-système et des fonctions de bibliothèque employés (en espérant que les informations soient à jour), et de prévoir une gestion adéquate pour les erreurs les plus fréquentes. Une gestion générique peut être mise en place pour les cas les plus rares. Prenons l'exemple de open( ). Une manière assez simple mais correcte d'opérer serait :

while (1) {

if ((fd = open (fichier, mode)) == -1) { assert (errno != EFAULT);

switch (errno) { case EMFILE case ENFILE case ENOMEM

fprintf (stderr, "Erreur critique : %s\n", strerror (errno));

return (-1);

default :

fprintf (stderr, "Erreur d'ouverture de %s %s\n"

fichier, strerror (errno));

break;

}

if (corriger_le_nom_pour_reessayer( ) != 0) /* l'utilisateur préfère abandonner */

return (-1);

else

continue; /* recommencer */

} else {

/* pas d'erreur */

break;

}

return (0);

}

Cela permet à la fois de différencier les erreurs irrécupérables de celles qu'on peut corriger, et donne à l'utilisateur la possibilité de modifier sa demande ou d'abandonner l'opération.

Nous allons voir un petit exemple d'utilisation de strerror( ), en l'invoquant pour une dizaine d'erreurs courantes :

exemple_strerror.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <errno.h>

int main (void) {

fprintf (stdout, "strerror (EACCES) = %s\n", strerror (EACCES));

fprintf (stdout, "strerror (EAGAIN) = %s\n", strerror (EAGAIN));

fprintf (stdout, "strerror (EBUSY) = %s\n", strerror (EBUSY));

fprintf (stdout, "strerror (ECHILD) = %s\n", strerror (ECHILD));

fprintf (stdout, "strerror (EEXIST) = %s\n", strerror (EEXIST));

fprintf (stdout, "strerror (EFAULT) = %s\n", strerror (EFAULT));

fprintf (stdout, "strerror (EINTR) = %s\n", strerror (EINTR));

fprintf (stdout, "strerror (EINVAL) = %s\n", strerror (EINVAL));

fprintf (stdout, "strerror (EISDIR) = %s\n", strerror (EISDIR));

fprintf (stdout, "strerror (EMFILE) = %s\n", strerror (EMFILE));

fprintf (stdout, "strerror (ENODEV) = %s\n", strerror (ENODEV));

fprintf (stdout, "strerror (ENOMEM) = %s\n", strerror (ENOMEM));

fprintf (stdout, "strerror (ENOSPC) = %s\n", strerror (ENOSPC));

fprintf (stdout, "strerror (EPERM) = %s\n", strerror (EPERM));

fprintf (stdout, "strerror (EPIPE) = %s\n", strerror (EPIPE));

fprintf (stdout, "strerror (ESRCH) = %s\n", strerror (ESRCH));

return (0);

}

L'exécution montre que la fonction strerror( ) de la GlibC est sensible à la localisation :

$ echo $LC_ALL fr_FR

$ ./exemple strerror

strerror (EACCES) = Permission non accordée

strerror (EAGAIN) = Ressource temporairement non disponible strerror (EBUSY) = Périphérique ou ressource occupé

strerror (ECHILD) = Aucun processus enfant strerror (EEXIST) = Le fichier existe strerror (EFAULT) = Mauvaise adresse strerror (EINTR) = Appel-système interrompu strerror (EINVAL) = Paramètre invalide strerror (EISDIR) = Est un répertoire strerror (EMFILE) = Trop de fichiers ouverts strerror (ENODEV) = Aucun périphérique de ce type strerror (ENOMEM) = Ne peut allouer de la mémoire

strerror (ENOSPC) = Aucun espace disponible sur le périphérique strerror (EPERM) = Opération non permise

strerror (EPIPE) = Relais brisé (pipe)

strerror (ESRCH) = Aucun processus de ce type

$

Il existe également une fonction permettant d'afficher directement sur la sortie standard, précédée éventuellement d'une chaîne de caractères permettant de situer l'erreur. Cette fonction. nommée perror( ), est déclarée dans <stdio.h> ainsi :

void perror (const char * s);

On l'utilise généralement de la manière suivante : if ((fd = open (nom_fichier, mode)) == -1) { perror ("open");

exit (1);

}

Le message s'affiche ainsi :

open: Aucun fichier ou répertoire de ce type Il s'agit dans ce cas de l'erreur ENOENT.

Notre exemple va utiliser perror( ) en cas d'échec de fork( ). Pour faire échouer celui-ci, nous allons diminuer la limite RLIMITNPROC1 , puis nous bouclerons sur un appel fork( ).

exemple_perror.c :

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/resource.h>

#include <sys/wait.h>

int main (void) {

struct rlimit limite; pid_t pid;

if (getrlimit (RLIMIT_NPROC, & limite) != 0) { perror ("getrlimit");

exit (1);

}

limite . rlim_cur = 16;

if (setrlimit (RLIMIT_NPROC, & limite) != 0) { perror ("setrlimit"):

exit (1);

}

while (1) { pid = fork( );

if (pid == (pid_t) -1) { perror ("fork");

exit (1);

}

if (pid != 0) {

fprintf (stdout, "%u\n", pid);

fflush (stdout);

if (waitpid (pid, NULL, 0) != pid)

perror ("waitpid");

return (0);

} }

return (0):

}

Comme nous avons déjà plusieurs processus qui tournent avant même de lancer le programme, nous n'atteindrons pas les seize fork( ). Pour vérifier le nombre de processus en cours, nous allons d'abord lancer une série de commandes shell, avant d'exécuter le programme.

$ ps aux | grep 'whoami' | wc -l 10

$ ./exemple_perror 6922

6923 6924 6925 6926 6927 6928

fork: Ressource temporairement non disponible

$

Dans les dix processus qui tournaient avant le lancement, il faut compter la commande ps elle-même. Il est donc normal que fork( ) n'échoue qu'une fois arrivé au septième processus fils. Le message correspond à celui de l'erreur EAGAIN. En effet, le système considère que l'un des processus va se finir tôt ou tard et que fork( ) pourra réussir alors.

Un autre moyen d'accéder directement aux messages d'erreur est d'employer la table sys_errlist[ ], définie dans <errno.h> ainsi :

const char * sys_errlist [];

Chaque élément de la table est un pointeur sur une chaîne de caractères décrivant l'erreur correspondant à l'index de la chaîne dans la table. Il existe une variable globale décrivant le nombre d'entrées dans la table :

int sys_nerr;

Il faut être très prudent avec les accès dans cette table car il y a des valeurs ne correspondant à aucune erreur. C'est le cas, par exemple, de 41 et 58 sous Linux 2.2 avec la GIibC 2. Pareille-ment, dans la même configuration. sys_nerr vaut 125, ce qui signifie que les erreurs s'étendent de 0 à 124. Pourtant, il existe une erreur ECANCELED valant 125, non utilisée dans les appels-système.

L'accès à la table doit donc être précautionneux, du genre : if ((erreur < 0) || (erreur >= sys_nerr)) {

fprintf (stderr, "Erreur invalide %d\n", erreur);

} else if (sys_errlist [erreur] == NULL) {

fprintf (stderr, "Erreur non documentée %d\n", erreur);

} else {

fprintf (stderr, "%s\n", sys_errlist [erreur]);

}

Nous allons utiliser la table sys_errlist[] pour afficher tous les libellés des erreurs connues.

exemple_sys_errlist.c #include <stdio.h>

#include <errno.h>

int main (void) {

int i;

for (i = 0; i < sys_nerr; i++) if (sys_errlist [i] != NULL)

fprintf (stdout, "%d : %s\n", i, sys_errlist [i]);

else

fprintf (stdout, "** Pas de message pour %d **\n";

return (0);

}

Dans l'exemple d'exécution suivant, nous avons éliminé quelques passages pour éviter d'afficher inutilement toute la liste :

$ ./exemple_sys_errlist 0 : Success

1 : Operation not permitted 2 : No such file or directory 3 : No such process

4 : Interrupted system call 5 : Input/output error 6 : Device not configured 7 : Argument list too long 8 : Exec format error 9 : Bad file descriptor 10 : No child processes [...]

39 : Directory not empty

40 : Too many levels of symbolic links

** Pas de message pour 41 **

42 : No message of desired type [...]

56 Invalid request code 57 Invalid slot

** Pas de message pour 58 **

59 : Bad font file format 60 Device not a stream 61 : No data available 62 : Timer expired [...]

121 : Remote I/O error 122 : Disk quota exceeded 123 : No medium found 124 : Wrong medium type

$

On remarque plusieurs choses : d'abord les messages de sys_errlist[ ] ne sont pas traduits automatiquement par la localisation, contrairement à strerror( ), ensuite l'erreur 0, bien que documentée pour simplifier l'accès à cette table, n'est par définition pas une erreur, enfin les erreurs 41 et 58 n'existent effectivement pas.

Conclusion

Nous avons étudié dans ce chapitre les principaux points importants concernant la fin d'un processus, due à des causes normales ou à un problème irrémédiable.

Nous avons également essayé de définir un comportement raisonnable en cas de détection d'erreur, et nous avons analysé les méthodes pour obtenir des informations sur les raisons ayant conduit un processus fils à se terminer. En ce qui concerne le code de débogage, et les macros assert( ), on trouvera des réflexions intéressantes dans [McCONNELL 1994] Programmation professionnelle.

Nous avons aussi indiqué qu'un processus pouvait être tué par un signal. Nous allons à présent développer ce sujet dans les quelques chapitres à venir.

6

Gestion classique

Dans le document Programmation système en C sous (Page 63-68)