Programmation avancée sous Linux


précédentsommairesuivant

8. Appels Système Linux

Jusqu'ici, nous avons présenté diverses fonctions que votre programme peut utiliser pour accomplir des actions relatives au système, comme analyser des options de ligne de commande, manipuler des processus et mapper de la mémoire. Si vous y regardez de plus près, vous remarquerez que ces fonctions se répartissent en deux catégories, selon la façon dont elles sont implantées.

  • Une fonction de bibliothèque est une fonction ordinaire qui se trouve dans une bibliothèque externe à votre programme. La plupart des fonctions que nous avons présenté jusqu'ici se trouvent dans la bibliothèque standard du C, libc. Par exemple, getopt_long et mkstemp en font partie. Un appel à une fonction de bibliothèque est identique à l'appel de n'importe quelle autre fonction. Les arguments sont placés dans des registres du processeur ou sur la pile et l'exécution est transférée au début de la fonction qui se trouve généralement dans une bibliothèque partagée.
  • Un appel système est implanté au sein du noyau Linux. Lorsqu'un programme effectue un appel système, les arguments sont mis en forme et transférés au noyau qui prend la main jusqu'à la fin de l'appel. Un appel système n'est pas identique à un appel de fonction classique et une procédure spécifique est nécessaire pour transférer le contrôle au noyau. Cependant, la bibliothèque C GNU (l'implémentation de la bibliothèque standard du C fournie avec les systèmes GNU/Linux) masque les appels systèmes par des fonctions classiques afin qu'ils soient plus simples à utiliser. Les fonctions d'E/S de bas niveau comme open ou read font partie des appels systèmes Linux. Les appels système Linux constituent l'interface de base entre les programmes et le noyau Linux. À chaque appel correspond une opération ou une fonctionnalité de base. Certains appels sont très puissants et influent au niveau du système. Par exemple, il est possible d'éteindre le système ou d'utiliser des ressources système tout en interdisant leur accès aux autres utilisateurs. De tels appels ne sont utilisables que par des programmes s'exécutant avec les privilèges superutilisateur (lancé par root). Ils échouent si le programme ne dispose pas de ces droits.

Notez qu'une fonction de bibliothèque peut à son tour appeler une ou plusieurs fonctions de bibliothèques ou appels système.

Linux propose près de 300~appels système. La liste des appels disponibles sur votre système se trouve dans le fichier /usr/include/asm/unistd.h. Certains ne sont destinés qu'à être utilisés en interne par le noyau et d'autres ne servent que pour l'implémentation de certaines bibliothèques. Dans ce chapitre, nous vous présenterons ceux qui nous semblent les plus susceptibles de vous servir.

La plupart sont déclarés dans le fichier d'en-tête /usr/include/asm/unistd.h.

8-1. Utilisation de strace

Avant de commencer à parler des appels système, il est nécessaire de présenter une commande qui peut vous en apprendre beaucoup sur les appels système. La commande strace trace l'exécution d'un autre programme en dressant la liste des appels système qu'il effectue et des signaux qu'il reçoit.

Pour observer l'enchaînement des appels système et des signaux d'un programme invoquez simplement strace, suivi du nom du programme et de ses arguments. Par exemple, pour observer les appels systèmes effectués par la commande hostname(Invoquée sans aucune option, la commande hostname affiche simplement le nom d'hôte de la machine sur la sortie standard.), utilisez cette commande:

 
Sélectionnez

% strace hostname

Elle vous affichera un certain nombre d'informations. Chaque ligne correspond à un appel système. Pour chaque appel, vous trouverez son nom suivi de ses arguments (ou de leur abréviation s'ils sont trop longs) et de sa valeur de retour. Lorsque c'est possible, strace utilise des noms symboliques pour les arguments et la valeur de retour plutôt que leur valeur numérique et affiche les différents champs des structures passées via un pointeur à l'appel système. Notez que strace ne montre pas les appels de fonctions classiques.

La première ligne affichée par strace hostname montre l'appel système execve qui invoque le programme hostname(Sous Linux, la famille de fonction exec est implémentée via l'appel système execve.):

 
Sélectionnez

execve("/bin/hostname", ["hostname"], [/* 49 vars */]) = 0

Le premier argument est le nom du programme à exécuter; le second sa liste d'arguments, constituée d'un seul élément; et le troisième l'environnement du programme que strace n'affiche pas par souci de concision. Les 30 lignes suivantes, approximativement, font partie du mécanisme qui charge la bibliothèque standard du~C à partir d'un fichier de bibliothèque partagé.

Les appels systèmes effectivement utilisés par le programme pour fonctionner se trouvent vers la fin de l'affichage. L'appel système uname permet d'obtenir le nom d'hôte du système à partir du noyau:

 
Sélectionnez

uname({sys="Linux", node="myhostname", ...}) = 0

Remarquez que strace donne le nom des champs (sys et node) de la structure passée en argument. Cette structure est renseignée par l'appel système ? Linux place le nom du système d'exploitation dans le champ sys et le nom d'hôte du système dans le champ node. L'appel système uname est détaillé dans la Section 8.15, « uname ».

Enfin, l'appel système write affiche les informations. Souvenez-vous que le descripteur de fichier~1 correspond à la sortie standard. Le troisième argument est le nombre de caractères à écrire et la valeur de retour est le nombre de caractères effectivement écrits.

 
Sélectionnez

write(1, "myhostname\n", 11) = 11

L'affichage peut paraître un peu confus lorsque vous exécutez strace car la sortie du programme hostname est mélangée avec celle de strace.

Si le programme que vous analysez affiche beaucoup d'informations, il est parfois plus pratique de rediriger la sortie de strace vers un fichier. Pour cela, utilisez l'option -o nom_de_fichier.

La compréhension de tout ce qu'affiche strace nécessite une bonne connaissance du fonctionnement du noyau Linux et de l'environnement d'exécution ce qui présente un intérêt limité pour les programmeurs d'application. Cependant, une compréhension de base est utile pour déboguer des problèmes sournois et comprendre le fonctionnement d'autres programmes.

8-2. access : Tester les Permissions d'un Fichier

L'appel système access détermine si le processus appelant à le droit d'accéder à un fichier. Il peut vérifier toute combinaison des permissions de lecture, écriture ou exécution ainsi que tester l'existence d'un fichier.

L'appel access prend deux argument. Le premier est le chemin d'accès du fichier à tester. Le second un OU binaire entre R_OK, W_OK et X_OK, qui correspondent aux permissions en lecture, écriture et exécution. La valeur de retour est zéro si le processus dispose de toutes les permissions passées en paramètre. Si le fichier existe mais que le processus n'a pas les droits dessus, access renvoie~-1 et positionne errno à EACCES (ou EROFS si l'on a testé les droits en écriture d'un fichier situé sur un système de fichiers en lecture seule).

Si le second argument est F_OK, access vérifie simplement l'existence du fichier. Si le fichier existe, la valeur de retour est~0; sinon, elle vaut~-1 et errno est positionné à ENOENT. errno peut également être positionné à EACCES si l'un des répertoires du chemin est inaccessible.

Le programme du Listing checkaccess utilise access pour vérifier l'existence d'un fichier et déterminer ses permissions en lecture et en écriture. Spécifiez le nom du fichier à vérifier sur la ligne de commande.

Vérifier les Droits d'Accès à un Fichier check-access.c
Sélectionnez

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

int main (int argc, char* argv[])
{
  char* path = argv[1];
  int rval;

  /* Vérifie l'existence du fichier. */
  rval = access (path, F_OK);
  if (rval == 0)
    printf ("%s existe\n", path);
  else {
    if (errno == ENOENT)
       printf ("%s n'existe pas\n", path);
    else if (errno == EACCES)
       printf ("%s n'est pas accessible\n", path);
    return 0;
  }

  /* Vérifie l'accès en lecture. */
  rval = access (path, R_OK);
  if (rval == 0)
    printf ("%s est accessible en lecture\n", path);
  else
    printf ("%s n'est pas accessible en lecture (accès refusé)\n", path);

  /* Vérifie l'accès en écriture. */
  rval = access (path, W_OK);
  if (rval == 0)
    printf ("%s est accessible en écriture\n", path);
  else if (errno == EACCES)
    printf ("%s n'est pas accessible en écriture (accès refusé)\n", path);
  else if (errno == EROFS)
    printf ("%s n'est pas accessible en écriture (SF en lecture seule)\n", path);
  return 0;
}

Par exemple, pour tester les permissions d'accès à un fichier appelé README situé sur un CD-ROM, invoquez le programme comme ceci:

 
Sélectionnez

% ./check-access /mnt/cdrom/README
/mnt/cdrom/README existe
/mnt/cdrom/README est accessible en lecture
/mnt/cdrom/README n'est pas accessible en écriture (SF en lecture seule)

8-3. fcntl : Verrous et Opérations sur les Fichiers

L'appel système fcntl est le point d'accès de plusieurs opérations avancées sur les descripteurs de fichiers. Le premier argument de fcntl est un descripteur de fichiers ouvert et le second est une valeur indiquant quelle opération doit être effectuée. Pour certaines d'entre-elles, fcntl prend un argument supplémentaire. Nous décrirons ici l'une des opérations les plus utiles de fcntl: le verrouillage de fichier. Consultez la page de manuel de fcntl pour plus d'informations sur les autres opérations.

L'appel système fcntl permet à un programme de placer un verrou en lecture ou en écriture sur un fichier, d'une façon similaire à celle utilisée pour les verrous mutex traités dans le Chapitre threads, « Threads ». Un verrou en lecture se place sur un descripteur de fichier accessible en lecture et un verrou en écriture sur un descripteur de fichier accessible en écriture. Plusieurs processus peuvent détenir un verrou en lecture sur le même fichier au même moment, mais un seul peut détenir un verrou en écriture et le même fichier ne peut pas être verrouillé à la fois en lecture et en écriture. Notez que le fait de placer un verrou n'empêche pas réellement les autres processus d'ouvrir le fichier, d'y lire des données ou d'y écrire, à moins qu'il ne demandent eux aussi un verrou avec fcntl.

Pour placer un verrou sur un fichier, il faut tout d'abord créer une variable struct flock et la remplir de zéros. Positionnez le champ l_type de la structure à F_RDLCK pour un verrou en lecture ou F_WRLCK pour un verrou en écriture. Appelez ensuite fcntl en lui passant le descripteur du fichier à verrouiller, le code d'opération F_SETLKW et un pointeur vers la variable struct flock. Si un autre processus détient un verrou qui empêche l'acquisition du nouveau, l'appel à fcntl bloque jusqu'à ce que ce verrou soit relâché.

Le programme du Listing lockfile ouvre en écriture le fichier dont le nom est passé en paramètre puis place un verrou en écriture dessus. Le programme attend ensuite que l'utilisateur appuie sur la touche Entrée puis déverrouille et ferme le fichier.

Crée un Verrou en Écriture avec fcntl lock-file.c
Sélectionnez

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main (int argc, char* argv[])
{
  char* file = argv[1];
  int fd;
  struct flock lock;

  printf ("ouverture de %s\n", file);
  /* Ouvre un descripteur de fichier. */
  fd = open (file, O_WRONLY);
  printf ("verrouillage\n");
  /* Initialise la structure flock. */
  memset (&amp;lock, 0, sizeof(lock));
  lock.l_type = F_WRLCK;
  /* Place un verrou en écriture sur le fichier. */
  fcntl (fd, F_SETLKW, &amp;lock);

  printf ("verrouillé ; appuyez sur Entrée pour déverrouiller... ");
  /* Attend l'appui sur Entrée. */
  getchar ();

  printf ("déverrouillage\n");
  /* Libère le verrou. */
  lock.l_type = F_UNLCK;
  fcntl (fd, F_SETLKW, &amp;lock);

  close (fd);
  return 0;
}

Compilez et lancez le programme sur un fichier test ? par exemple, /tmp/fichier-test ? comme suit:

 
Sélectionnez

% cc -o lock-file lock-file.c
% touch /tmp/fichier-test
% ./lock-file /tmp/fichier-test
ouverture de /tmp/fichier-test
verrouillage
verrouillé ; appuyez sur Entrée pour déverouiller...

Maintenant, dans une autre fenêtre, essayez de le lancer sur le même fichier:

 
Sélectionnez

% ./lock-file /tmp/fichier-test
ouverture de /tmp/fichier-test
verrouillage

Notez que la seconde instance est bloqué lors de la tentative de verrouillage du fichier. Revenez dans la première fenêtre et appuyez sur Entrée:

 
Sélectionnez

Déverrouillage

Le programme s'exécutant dans la seconde fenêtre acquiert immédiatement le verrou.

Si vous préférez que fcntl ne soit pas bloquant si le verrou ne peut pas être obtenu, utilisez F_SETLK au lieu de F_SETLKW. Si le verrou ne peut pas être acquis, fcntl renvoie~-1 immédiatement.

Linux propose une autre implémentation du verrouillage de fichiers avec l'appel flock. La fonction fcntl dispose d'un avantage majeur: elle fonctionne sur les fichiers se trouvant sur un système de fichiers NFS(Network File System (NFS) est une technologie de partage de fichiers courante, comparable aux partages et aux lecteurs réseau Windows.) (si tant est que le serveur NFS soit relativement récent et correctement configuré). Ainsi, si vous disposez de deux machines qui ont toutes deux le même système de fichiers monté via NFS, vous pouvez reproduire l'exemple ci-dessus en utilisant deux machines différentes. Lancez lock-file sur une machine en lui passant un fichier situé sur le système de fichiers NFS puis lancez le sur la seconde en lui passant le même fichier. NFS relance le second programme lorsque le verrou est relâché par le premier.

8-4. fsync et fdatasync : Purge des Tampons Disque

Sur la plupart des système d'exploitation, lorsque vous écrivez dans un fichier, les données ne sont pas immédiatement écrites sur le disque. Au lieu de cela, le système d'exploitation met en cache les données écrites dans un tampon en mémoire, pour réduire le nombre d'écritures disque requises et améliorer la réactivité du programme. Lorsque le tampon est plein ou qu'un événement particulier survient (par exemple, au bout d'un temps donné), le système écrit les données sur le disque.

Linux fournit un système de mise en cache de ce type. Normalement, il s'agit d'une bonne chose en termes de performances. Cependant, ce comportement peut rendre instables les programmes qui dépendent de l'intégrité de données stockées sur le disque. Si le système s'arrête soudainement ? par exemple, en raison d'un crash du noyau ou d'une coupure de courant ? toute donnée écrite par le programme qui réside dans le cache en mémoire sans avoir été écrite sur le disque est perdue.

Par exemple, supposons que vous écriviez un programme de gestion de transactions qui tient un fichier journal. Ce dernier contient les enregistrements concernant toutes les transactions qui ont été traitées afin que si une panne système survient, le statut des données impliquées par les transactions puisse être restauré. Il est bien sûr important de préserver l'intégrité du fichier journal ? à chaque fois qu'une transaction a lieu, son entrée dans le journal doit être envoyée immédiatement sur le disque dur.

Pour faciliter l'implémentation de tels mécanismes, Linux propose l'appel système fsync. Celui-ci prend un seul argument, un descripteur de fichier ouvert en écriture, et envoie sur le disque toutes les données écrites dans le fichier. L'appel fsync ne se termine pas tant que les données n'ont pas été physiquement écrites.

La fonction du Listing writejournalentry illustre l'utilisation de fsync. Il écrit un enregistrement d'une ligne dans un fichier journal.

Écrit et Synchronise un enregistrement write_journal_entry.c
Sélectionnez

#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

const char* journal_filename = "journal.log";

void write_journal_entry (char* entry)
{
  int fd = open (journal_filename, O_WRONLY | O_CREAT | O_APPEND, 0660);
  write (fd, entry, strlen (entry));
  write (fd, "\n", 1);
  fsync (fd);
  close (fd);
}

Un autre appel système, fdatasync, a la même fonction. Cependant, alors que fsync garantit que la date de modification du fichier sera mise à jour, ce n'est pas le cas de fdatasync; ce dernier ne garantit que le fait que les données seront écrites. Cela signifie qu'en général, fdatasync peut s'exécuter plus vite que fsync car il n'a qu'une seule écriture à effectuer au lieu de deux.

Cependant, sur les version actuelles de Linux, ces deux appels système font en fait la même chose, mettant tous deux à jour la date de modification du fichier.

L'appel système fsync vous permet de forcer explicitement l'écriture d'un tampon. Vous pouvez également ouvrir un fichier en mode entrées/sorties synchrones, ce qui signifie que toutes les écritures sont envoyées sur le disque immédiatement. Pour cela, passez l'option O_SYNC lors de l'ouverture du fichier avec open.

8-5. getrlimit et setrlimit : Limites de Ressources

Les appels système getrlimit et setrlimit permettent à un processus de connaître et de définir des limites sur les ressources système qu'il peut consommer. Vous connaissez peut être la commande shell ulimit, qui vous permet de restreindre la consommation de ressources des programmes que vous exécutez(Consultez la page de manuel de votre shell pour plus d'informations sur ulimit.); ces appels système permettent à un programme de faire la même chose par programmation.

Pour chaque ressource il existe deux limites, la limite stricte et la limite souple. La limite souple ne doit jamais dépasser la limite dure. Typiquement, une application réduira la limite souple pour éviter une montée en puissance de sa consommation de ressources.

getrlimit et setrlimit prennent tous deux en argument un code spécifiant le type de limite de ressource et un pointeur vers une variable struct rlimit. L'appel getrlimit renseigne les champs de cette structure, tandis que setrlimit modifie la limite selon son contenu. La structure rlimit a deux champs: rlim_cur qui est la limite souple et rlim_max qui est la limite stricte.

Voici une liste des limites de ressources pouvant être modifiées les plus utiles, avec le code correspondant:

  • RLIMIT_CPU ? Temps processeur maximum, en secondes, utilisé par un programme. Il s'agit du temps pendant lequel le programme utilise effectivement le processeur, qui n'est pas forcément le temps d'exécution du programme. Si le programme dépasse cette limite, il est interrompu par un

signal SIGXCPU.

  • RLIMIT_DATA ? Quantité maximale de mémoire qu'un programme peut allouer pour ses données. Toute allocation au-delà de cette limite échouera.
  • RLIMIT_NPROC ? Nombre maximum de processus fils pouvant être exécutés par l'utilisateur. Si le processus appel fork et que trop de processus appartenant à l'utilisateur sont en cours d'exécution, fork échouera.
  • RLIMIT_NOFILE ? Nombre maximum de descripteurs de fichiers que le processus peut ouvrir en même temps.

Consultez la page de manuel de setrlimit pour une liste complète des ressources système.

Le programme du Listing limitcpu illustre l'utilisation de la limite de temps processeur consommé par un programme. Il définit un temps processeur de une seconde puis entre dans une boucle infinie. Linux tue le processus peu après, lorsqu'il dépasse la seconde de temps processeur.

Démonstration de la Limite de Temps Processeur limit-cpu.c
Sélectionnez

#include <sys/resource.h>
#include <sys/time.h>
#include <unistd.h>

int main ()
{
  struct rlimit rl;

  /* Récupère la limite courante. */
  getrlimit (RLIMIT_CPU, &amp;rl);
  /* Définit une limite de temps processeur d'une seconde. */
  rl.rlim_cur = 1;
  setrlimit (RLIMIT_CPU, &amp;rl);
  /* Occupe le programme. */
  while (1);

  return 0;
}

Lorsque le programme est terminé par SIGXCPU, le shell affiche un message interprétant le signal:

 
Sélectionnez

% ./limit_cpu
Temps UCT limite expiré

8-6. getrusage : Statistiques sur les Processus

L'appel système getrusage obtient des statistiques sur un processus à partir du noyau. Il peut être utilisé pour obtenir des statistiques pour le processus courant en passant RUSAGE_SELF comme premier argument ou pour les processus fils terminés qui ont été créés par ce processus et ses fils en passant RUSAGE_CHILDREN. Le second argument de getrusage est un pointeur vers une variable de type struct rusage, qui est renseignée avec les statistiques.

Voici quelques-uns des champs les plus intéressant d'une struct rusage:

  • ru_utime ? Champ de type struct timeval contenant la quantité de temps utilisateur, en secondes, que le processus a utilisé. Le temps utilisateur est le temps processeur passé à exécuté le programme par opposition à celui passé dans le noyau pour des appels système.
  • ru_stime ? Champ de type struct timeval contenant la quantité de temps système, en seconde, que le processus a utilisé. Le temps système est le temps processeur passé à exécuté des appels système à la demande du processus.
  • ru_maxrss ? Quantité maximale de mémoire physique occupée par le processus au cours son exécution.

La page de manuel de getrusage liste tous les champs disponibles. Consultez la Section gettimeofday, « gettimeofday : Heure Système » pour plus d'informations sur le type struct timeval.

La fonction du Listing printcputimes affiche les temps système et utilisateur du processus en cours.

Affiche les Temps Utilisateur et Processeur print-cpu-times.c
Sélectionnez

#include <stdio.h>
#include <sys/resource.h>
#include <sys/time.h>
#include <unistd.h>

void print_cpu_time()
{
  struct rusage usage;
  getrusage (RUSAGE_SELF, &amp;usage);
  printf ("Temps processeur : %ld. lds utilisateur, %ld. lds système\n",
          usage.ru_utime.tv_sec, usage.ru_utime.tv_usec,
          usage.ru_stime.tv_sec, usage.ru_stime.tv_usec);
}

8-7. gettimeofday : Heure Système

L'appel système gettimeofday renvoie l'heure système. Il prend un pointeur vers une variable de type struct timeval. Cette structure représente un temps, en secondes, séparé en deux champs. Le champ tv_sec contient la partie entière du nombre de secondes et le champ tv_usec la fraction de microsecondes. La valeur de la struct timeval représente le nombre de secondes écoulé depuis le début de l'epoch UNIX, c'est-à-dire le premier janvier 1970 à minuit UTC. L'appel gettimeofday prend également un second argument qui doit être NULL. Incluez <sys/time.h> si vous utilisez cet appel système.

Le nombre de secondes depuis l'epoch UNIX n'est généralement pas une façon très pratique de représenter les dates. Les fonctions de la bibliothèque standard localtime et strftime aident à manipuler les valeurs renvoyées par gettimeofday. La fonction localtime prend un pointeur vers un nombre de secondes (le champ tv_sec de struct timeval) et renvoie un pointeur vers un objet struct tm. Cette structure contient des champs plus utiles renseignés selon le fuseau horaire courant:

  • tm_hour, tm_min, tm_sec ? Heure du jour en heures, minutes et secondes.
  • tm_year, tm_mon, tm_day ? Année, mois, jour.
  • tm_wday ? Jour de la semaine. Zéro représente le Dimanche.
  • tm_yday ? Jour de l'année.
  • tm_isdst ? Drapeau indiquant si l'heure d'été est en vigueur ou non.

La fonction strftime permet de produire à partir d'un pointeur vers une struc tm une chaîne personnalisée et formatée représentant la date et l'heure. Le format est spécifié d'une façon similaire à printf, sous forme d'une chaîne contenant des codes qui indiquent les champs à inclure. Par exemple, la chaîne de format

 
Sélectionnez

"%Y-%m-%d %H:%M:%S"

Correspond à une date de la forme:

 
Sélectionnez

2006-07-15 21:00:42

Passez à strftime un tampon pour recevoir la chaîne, la longueur du tampon, la chaîne de format et un pointeur vers une variable struct tm. Consultez la page de manuel de strftime pour une liste complète des codes qui peuvent être utilisés dans la chaîne de format. Notez que ni localtime ni strftime ne prennent en compte la partie fractionnaire de l'heure courante avec une précision supérieure à la seconde. Si vous voulez exploiter le champ tv_usec de la struct timeval, vous devrez le faire manuellement.

Incluez <time.h> si vous appelez localtime ou strftime.

La fonction du Listing printtime affiche la date et l'heure courante, avec une précision de l'ordre de la milliseconde.

Affiche la Date et l'Heure print-time.c
Sélectionnez

#include <stdio.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>

void print_time ()
{
  struct timeval tv;
  struct tm* ptm;
  char time_string[40];
  long milliseconds;

  /* Récupère l'heure courante et la convertit en struct tm. */
  gettimeofday (&amp;tv, NULL);
  ptm = localtime (&amp;tv.tv_sec);	
  /* Formate la date et l'heure à la seconde près. */
  strftime (time_string, sizeof (time_string), "%Y-%m-%d %H:%M:%S", ptm);
  /* Calcule les millisecondes à partir des microsecondes. */
  milliseconds = tv.tv_usec / 1000;
  /* Affiche l'heure de façon formatée, en secondes, suivie d'un point
     décimal et des millisecondes. */
  printf ("%s. ld\n", time_string, milliseconds);
}

8-8. La famille mlock : Verrouillage de la Mémoire Physique

La famille d'appels système mlock permet à un programme de verrouiller tout ou partie de son espace d'adressage en mémoire physique. Cela évite que Linux ne l'envoie vers l'espace d'échange, même si le programme n'y accède pas pendant quelques temps.

Un programme pour lequel le temps est une ressource critique peut verrouiller la mémoire physique car le temps nécessaire au processus d'échange peut être trop long ou trop imprévisible. Un programme sensible au niveau de la sécurité pourrait vouloir empêcher l'envoi de données critiques vers un espace d'échange à partir duquel elles pourraient être récupérées après la fin du programme.

Le verrouillage d'une région de la mémoire consiste simplement à appeler mlock en lui passant un pointer vers le début de la région ainsi que la longueur de la région. Linux divise la mémoire en pages et ne peut verrouiller que des pages dans leur intégralité; chaque page qui contient une partie de la région de la mémoire passée à mlock est verrouillée. La fonction getpagesize renvoie la taille de page du système qui est de~4Ko sous Linux x86.

Par exemple pour allouer 32Mo d'espace d'adressage et le verrouiller en RAM, vous utiliserez ce code:

 
Sélectionnez

const int alloc_size = 32 * 1024 * 1024;
char* memory = malloc (alloc_size);
mlock (memory, alloc_size);

Notez que le simple fait d'allouer une page mémoire et de la verrouiller avec mlock ne réserve pas de mémoire physique pour le processus appelant car les pages peuvent être en copie à l'écriture(La copie à l'écriture (copy on write) signifie que Linux ne fait une copie privée de la page que lorsque le processus écrit une valeur à l'intérieur.). Vous devriez donc écrire une valeur quelconque sur chaque page:

 
Sélectionnez

size_t i;
size_t page_size = getpagesize ();
for (i = 0; i < alloc_size; i += page_size)
  memory[i] = 0;

Le fait d'écrire sur toutes les pages force Linux à allouer une page mémoire unique, non partagée, au processus pour chacune.

Pour déverrouiller une région, appelez munlock, qui prend les mêmes arguments que mlock.

Si vous voulez que l'intégralité de l'espace d'adressage de votre programme soit verrouillé en mémoire physique, appelez mlockall. Cet appel système ne prend qu'un seul argument: MCL_CURRENT verrouille toute la mémoire allouée mais les allocations suivantes ne sont pas verrouillées; MCL_FUTURE verrouille toutes les pages qui sont allouées après l'appel. Utilisez MCL_CURRENT|MCL_FUTURE pour verrouiller en mémoire à la fois les allocation en cours et les futures allocations.

Le verrouillage de grandes quantités de mémoire, en particulier en via mlockall, peut être dangereux pour tout le système Linux. Le verrouillage de mémoire sans discernement est un bon moyen de ralentir considérablement le système au point de le faire s'arrêter car les autres processus en cours d'exécution doivent s'arranger avec des ressources en mémoire physique moindres et avec le fait d'être envoyé et repris depuis l'espace d'échange. Si vous verrouillez trop de mémoire, le système sera totalement à court de mémoire et Linux commencera à tuer des processus.

Pour cette raison, seuls les processus avec les privilèges de superutilisateur peuvent verrouiller de la mémoire avec mlock ou mlockall. Si un processus ne disposant pas de ces privilèges appel l'une de ces fonctions, elle échouera en renvoyant~-1 et en positionnant errno à EPERM.

L'appel munlockall déverrouille toute la mémoire verrouillée par le processus courant, qu'elle ait été verrouillée par mlock ou mlockall.

Un moyen pratique de surveiller l'utilisation mémoire de votre programme est d'utiliser la commande top. La colonne VIRT indique la taille de l'espace d'adressage virtuel de chaque programme (cette taille inclut le code, les données et la pile dont certains peuvent être envoyés vers l'espace d'échange). La colonne RES (pour resident size) indique la quantité de mémoire physique effectivement occupée par le programme. La somme de toutes les valeurs présentes dans la colonne RES pour tous les programmes en cours d'exécution ne peut pas excéder la taille de la mémoire physique de votre ordinateur et la somme de toutes les tailles d'espace d'adressage est limitée à 2Go(NdT Cette limite n'est plus d'actualité avec les séries 2.4 et 2.6.) (pour les versions 32~bits de Linux).

Incluez <sys/mman.h> si vous utilisez l'un des appels système mlock.

8-9. mprotect : Définir des Permissions Mémoire

Dans la Section mmap, « Mémoire Mappée », nous avons montré comment utiliser l'appel système mmap pour mettre en correspondance un fichier avec la mémoire. Souvenez vous que le troisième argument de mmap est un ou binaire entre les indicateurs de protection mémoire PROT_READ, PROT_WRITE et PROT_EXEC pour des permissions en lecture, écriture ou exécution, respectivement, ou PROT_NONE pour empêcher l'accès à la mémoire. Si un programme tente d'effectuer une opération sur un emplacement mémoire sur lequel il n'a pas les bonnes permissions, il se termine sur la réception d'un signal SIGSEGV (erreur de segmentation).

Une fois que la mémoire a été mappée, ces permissions peuvent être modifiée par l'appel système mprotect. Les arguments de mprotect sont l'adresse d'une région mémoire, la taille de cette région et un jeu d'indicateurs de protection. La région mémoire consiste en un ensemble de pages complètes: l'adresse de la région doit être alignée sur la taille de page système et la longueur de la région doit être un multiple de la taille de page. Les indicateurs de protection de ces pages sont remplacés par la valeur passée en paramètre.

Notez que les régions mémoire renvoyées par malloc ne sont généralement pas alignées sur des pages, même si la taille de la mémoire est un multiple de la taille de page. Si vous voulez protéger de la mémoire obtenue via malloc, vous devez allouer une région plus importante que celle désirée et trouver une sous-région qui soit alignée sur une page. Vous pouvez également utiliser l'appel système mmap pour court-circuiter malloc et allouer de la mémoire alignée sur des pages directement à partir du noyau Linux. Consultez la Section mmap, « Mémoire Mappée », pour plus de détails.

Par exemple, supposons que votre programme alloue une page mémoire en mappant /dev/zero, comme décrit dans la Section mmapautres, « Autres Utilisations de mmap ». La mémoire est initialement

en lecture/écriture.

 
Sélectionnez

int fd = open ("/dev/zero", O_RDONLY);
char* memory = mmap (NULL, page_size, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE, fd, 0);
close (fd);

Plus loin, votre programme peut protéger la mémoire en écriture en appelant mprotect:

 
Sélectionnez

mprotect (memory, page_size, PROT_READ);

une technique avancée pour surveiller les accès mémoire est de protéger une région mémoire avec mmap ou mprotect puis de gérer le signal SIGSEGV que Linux envoie au programme lorsqu'il tente d'accéder à cette mémoire. L'exemple du Listing mprotect illustre cette technique.

Détecter un Accès Mémoire en Utilisant mprotect mprotect.c
Sélectionnez

#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

static int alloc_size;
static char* memory;

void segv_handler (int signal_number)
{
  printf ("accès mémoire !\n");
  mprotect (memory, alloc_size, PROT_READ | PROT_WRITE);
}

int main ()
{
  int fd;
  struct sigaction sa;

  /* Installe segv_handler comme gestionnaire de signal SIGSEGV.  */
  memset (&amp;sa, 0, sizeof (sa));
  sa.sa_handler = &amp;segv_handler;
  sigaction (SIGSEGV, &amp;sa, NULL);

  /* Alloue une page de mémoire en mappant /dev/zero. Mappe la mémoire
     en écriture seule initialement. */
  alloc_size = getpagesize ();
  fd = open ("/dev/zero", O_RDONLY);
  memory = mmap (NULL, alloc_size, PROT_WRITE, MAP_PRIVATE, fd, 0);
  close (fd);
  /* Écrit sur la page pour en obtenir une copie privée. */
  memory[0] = 0;
  /* Protège la mémoire en écriture. */
  mprotect (memory, alloc_size, PROT_NONE);

  /* Écrit dans la région qui vient d'être allouée. */
  memory[0] = 1;

  /* Terminé ; libère la mémoire. */
  printf ("Fini\n");
  munmap (memory, alloc_size);
  return 0;
}

Le programme effectue les opérations suivantes:

- Il déclare un gestionnaire de signal pour SIGSEGV;

- Il alloue une page mémoire en mappant /dev/zero et en écrivant une valeur dans la page obtenue pour en obtenir une copie privée.

- Il protège la mémoire en appelant mprotect avec l'option PROT_NONE;

- Lorsque le programme tente d'écrire en mémoire, Linux envoie un SIGSEGV qui est pris en charge par segv_handler. Le gestionnaire de signal supprime la protection de la mémoire ce qui autorise l'accès;

- Lorsque le gestionnaire de signal se termine, le contrôle repasse à main, où le programme libère la mémoire via munmap.

8-10. nanosleep : Pause en Haute Précision

L'appel système nanosleep est une version en haute précision de l'appel UNIX sleep. Au lieu de suspendre l'exécution pendant un nombre entier de secondes, nanosleep prend comme argument un pointeur vers un objet de type struct timespec, qui peut indiquer un temps à la nanoseconde près. Cependant, en raison de détails d'implémentation du noyau Linux, la précision fournie par nanosleep n'est que de 10~millisecondes ? c'est toujours mieux que celle offerte par sleep. Cette précision supplémentaire peut être utile, par exemple, pour ordonnancer des opérations fréquentes avec de faibles intervalles de temps entre elles.

La structure struct timespec a deux champs: tv_sec, le nombre entier de secondes et tv_nsec, un nombre supplémentaire de nanosecondes. La valeur de tv_nsec doit être inférieure à 10<sup>9</sup>.

L'appel nanosleep offre un autre avantage par rapport à sleep. Comme pour sleep, l'arrivée d'un signal interrompt l'exécution de nanosleep, qui positionne alors errno à EINTR et renvoie~-1. Cependant, nanosleep prend un second argument, un autre pointeur vers un objet struct timespec, qui, s'il n'est pas NULL, est renseigné avec le temps de pause qu'il restait à faire (c'est-à-dire la différence entre le temps de suspension demandé le temps de suspension effectif). Cela facilite la reprise de l'opération de suspension.

La fonction du Listing bettersleep fournit une implémentation alternative de sleep. Contrairement à l'appel système classique, cette fonction prend en paramètre une valeur en virgule flottante correspondant à la durée en secondes pour laquelle il faut suspendre l'exécution et reprend l'opération de suspension si elle est interrompue.

Fonction de Suspension Haute précision better-sleep.c
Sélectionnez

#include <errno.h>
#include <time.h>

int better_sleep (double sleep_time)
{
  struct timespec tv;
  /* Construit l'objet timespec à partir du nombre entier de seconde...  */
  tv.tv_sec = (time_t) sleep_time;
  /* ... et le reste en nanosecondes. */
  tv.tv_nsec = (long) ((sleep_time - tv.tv_sec) * 1e+9);

  while (1)
  {
    /* Suspend l'exécution pour un temps spécifié par tv. Si l'on est
       interrompu par un signal, le temps restant est replacé dans tv. */
    int rval = nanosleep (&amp;tv, &amp;tv);
    if (rval == 0)
      /* On a suspendu l'exécution pour le temps demandé ; terminé. */
      return 0;
    else if (errno == EINTR)
      /* Interrompu par un signal. Réessaie. */
      continue;
    else
      /* Autre erreur, abandon. */
      return rval;
  }
  return 0;
}

8-11. readlink : Lecture de Liens Symboliques

L'appel système readlink récupère la cible d'un lien symbolique. Il prend trois arguments: le chemin vers le lien symbolique, un tampon pour recevoir la cible du lien et sa longueur. Une particularité de readlink est qu'il n'insère pas de caractère NUL à la fin de la chaîne qu'il place dans le tampon. Cependant, il renvoie le nombre de caractères composant le chemin cible, placer soi-même le caractère NUL est donc simple.

Si le premier argument de readlink pointe vers un fichier qui n'est pas un lien symbolique, readlink positionne errno à EINVAL et renvoie~-1.

Le petit programme du Listing printsymlink affiche la cible du lien symbolique passé sur la ligne de commande.

Affiche la Cible d'un Lien Symbolique print-symlink.c
Sélectionnez

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

int main (int argc, char* argv[])
{
  char target_path[256];
  char* link_path = argv[1];

  /* Tente de lire la cible du lien symbolique. */
  int len = readlink (link_path, target_path, sizeof (target_path));
  if (len == -1) {
    /* L'appel a échoué. */
    if (errno == EINVAL)
      /* Il ne s'agit pas d'un lien symbolique ; en informe l'utlisateur. */
      fprintf (stderr, "%s n'est pas un lien symbolique\n", link_path);
    else
      /* Autre problème ; affiche un message générique. */
      perror ("readlink");
    return 1;
  }
  else {
    /* Place un caractère NUL à la fin de la cible. */
    target_path[len] = '\0';
    /* L'affiche. */
    printf ("%s\n", target_path);
    return 0;
  }
}

Par exemple, voici comment créer un lien symbolique et utiliser print-symlink pour le lire:

 
Sélectionnez

% ln -s /usr/bin/wc my_link
% ./print-symlink my_link
/usr/bin/wc

8-12. sendfile : Transferts de Données Rapides

L'appel système sendfile constitue un mécanisme efficace pour copier des données entre deux descripteurs de fichier. Les descripteurs de fichiers peuvent pointer vers un fichier sur le disque, un socket ou tout autre dispositif.

Typiquement, pour copier des données d'un descripteur de fichier vers un autre, un programme alloue un tampon de taille fixe, y copie des données provenant d'un des descripteurs, l'écrit sur l'autre et recommence jusqu'à ce que toutes les données aient été écrites. Ce procédé n'est pas efficace que ce soit en termes de temps ou d'espace car il nécessite l'utilisation de mémoire supplémentaire pour le tampon et ajoute une copie intermédiaire pour le remplir.

En utilisant sendfile, le tampon intermédiaire peut être supprimé. Appelez sendfile en lui passant le descripteur de fichier de destination, le descripteur source, un pointeur vers une variable de déplacement et le nombre d'octets à transférer. La variable de déplacement contient le déplacement à partir duquel lire le fichier source (0 correspond au début du fichier) et est mis à jour avec la position au sein du fichier à l'issue du transfert. La valeur de retour contient le nombre d'octets transférés. Incluez <sys/sendfile.h> dans votre programme s'il utilise sendfile.

Le programme du Listing copy est une implémentation simple mais extrêmement efficace de copie de fichier. Lorsqu'il est invoqué avec deux noms de fichiers sur la ligne de commande, il copie le contenu du premier dans le second. Il utilise fstat pour déterminer la taille, en octets, du fichier source.

Copie de Fichier avec sendfile copy.c
Sélectionnez

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

int main (int argc, char* argv[])
{
  int read_fd;
  int write_fd;
  struct stat stat_buf;
  off_t offset = 0;

  /* Ouvre le fichier source. */
  read_fd = open (argv[1], O_RDONLY);
  /* Evalue le fichier afin d'obtenir sa taille. */
  fstat (read_fd, &amp;stat_buf);
  /* Ouvre le fichier de destination en écriture avec les mêmes permissions
     que le fichier source. */
  write_fd = open (argv[2], O_WRONLY | O_CREAT, stat_buf.st_mode);
  /* Transfère les octets d'un fichier à l'autre. */
  sendfile (write_fd, read_fd, &amp;offset, stat_buf.st_size);
  /* Ferme tout. */
  close (read_fd);
  close (write_fd);

  return 0;
}

L'appel sendfile peut être utilisé dans de multiples occasions pour améliorer l'efficacité des copies. Un bon exemple est un serveur Web ou tout autre service réseau, qui envoie le contenu d'un fichier à un client via le réseau. Typiquement, ce genre de programme reçoit une requête depuis un socket connecté à la machine cliente. Le programme serveur ouvre le fichier local à transférer et écrit son contenu sur un socket réseau. Utiliser sendfile peut accélérer cette opération de façon significative. D'autres facteurs ont une influence sur l'efficacité du transfert, comme le paramétrage correct du socket. Cependant, ils sortent du cadre de ce livre.

8-13. setitimer : Créer des Temporisateurs

L'appel système setitimer est une généralisation de la fonction alarm. Il programme l'envoi d'un signal au programme après écoulement une période de temps donnée.

Un programme peut créer trois types différents de temporisateurs:

  • Si le code temporisateur est ITIMER_REAL, le processus reçoit un signal SIGALRM après que le temps spécifié s'est écoulé;
  • Si le code temporisateur est ITIMER_VIRTUAL, le processus reçoit un signal SIGVTALRM après s'être exécuté pendant un temps donné. Le temps pendant lequel le programme ne s'exécute pas (c'est-à-dire lorsque le noyau ou un autre processus est en cours d'exécution) n'est pas pris en compte;
  • Si le code temporisateur est ITIMER_PROF, le processus reçoit un signal SIGPROF lorsque le temps d'exécution du processus ou des appels système qu'il a invoqué atteint le temps spécifié.

Le premier argument de setitimer est un code temporisateur, indiquant le type de temporisateur à mettre en place. Le second argument est un pointeur vers un objet struct itimerval spécifiant les nouveaux paramètres du temporisateur. Le troisième argument, s'il n'est pas NULL est un pointeur vers un autre objet struct itimerval qui reçoit l'ancien paramétrage du temporisateur.

Une variable struct itimerval est constituée de deux champs:

  • it_value est un champ de type struct timeval qui contient le temps avant l'expiration suivante du temporisateur et l'envoi du signal. S'il vaut 0, le temporisateur est désactivé;
  • it_interval est un autre champ de type struct timeval contenant la valeur avec laquelle sera réinitialisé après son expiration. S'il vaut~0, le temporisateur sera désactivé après expiration. S'il est différent de zéro, le temporisateur expirera de façon répétitive à chaque écoulement de l'intervalle.

Le type struct timeval est décrit dans la Section gettimeofday, « gettimeofday : Heure Système ». Le programme du Listing timer illustre l'utilisation de setitimer pour suivre le temps d'exécution d'un programme. Un temporisateur est configuré pour expirer toutes les 250 millisecondes et envoyer un signal SIGVTALRM.

Exemple d'Utilisation d'un Temporisateur timer.c
Sélectionnez

#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>

void timer_handler (int signum)
{
  static int count = 0;
  printf ("le temporisateur a expiré %d fois.\n", ++count);
}

int main ()
{
  struct sigaction sa;
  struct itimerval timer;

  /* Installe timer_handler en tant que gestionnaire pour SIGVTALRM. */
  memset (&amp;sa, 0, sizeof (sa));
  sa.sa_handler = &amp;timer_handler;
  sigaction (SIGVTALRM, &amp;sa, NULL);

  /* Configure le temporisateur pour expirer après 250ms... */
  timer.it_value.tv_sec = 0;
  timer.it_value.tv_usec = 250000;
  /* ... puis régulièrement toutes les 250ms. */
  timer.it_interval.tv_sec = 0;
  timer.it_interval.tv_usec = 250000;
  /* Démarre un temporisateur virtuel. Il décompte dès que le processus
     s'exécute. */
  setitimer (ITIMER_VIRTUAL, &amp;timer, NULL);

  /* Perd du temps.  */
  while (1);
}

8-14. sysinfo : Récupération de Statistiques Système

L'appel système sysinfo renseigne une structure avec des statistiques sur le système. Son seul argument est un pointeur vers une variable struct sysinfo. Voici quelques uns des champs les plus intéressants de cette structure:

  • uptime ? Temps écoulé depuis le démarrage du système, en secondes;
  • totalram ? Mémoire physique disponible au total;
  • freeram ? Mémoire physique libre;
  • procs ? Nombre de processus s'exécutant sur le système.

Consultez la page de manuel de sysinfo pour une description complète du type struct sysinfo. Incluez <linux/kernel.h>, <linux/sys.h> et <sys/sysinfo.h> si vous utilisez sysinfo.

Le programme du Listing sysinfo affiche des statistiques sur le système courant.

Affiche des Statistiques Système sysinfo.c
Sélectionnez

#include <linux/kernel.h>
#include <linux/sys.h>
#include <stdio.h>
#include <sys/sysinfo.h>

int main ()
{
  /* Facteurs de conversion. */
  const long minute = 60;
  const long hour = minute * 60;
  const long day = hour * 24;
  const double megabyte = 1024 * 1024;
  /* Récupère les statistiques système. */
  struct sysinfo si;
  sysinfo (&amp;si);
  /* Affiche un résumé des informations intéressantes. */
  printf ("uptime système : %ld jours, %ld: ld: ld\n",
          si.uptime / day, (si.uptime % day) / hour,
          (si.uptime % hour) / minute, si.uptime % minute);
  printf ("RAM totale    : %5.1f Mo\n", si.totalram / megabyte);
  printf ("RAM libre     : %5.1f Mo\n", si.freeram / megabyte);
  printf ("nb processus  : %d\n", si.procs);
  return 0;
}

8-15. uname

L'appel système uname renseigne une structure avec diverses informations système, comme le nom de l'ordinateur, le nom de domaine et la version du système d'exploitation en cours d'exécution. Ne passez qu'un seul argument à uname: un pointeur vers un objet struct utsname. Incluez <sys/utsname.h> si vous utilisez uname.

L'appel à uname renseigne les champs suivants:

  • sysname ? Nom du système d'exploitation (par exemple, Linux);
  • release, version ? Numéros de version majeure et mineure du noyau;
  • machine ? Informations sur la plateforme système. Pour un Linux x86, ce sera

i386 ou i686 selon le processeur;

  • node ? Nom non qualifié de l'ordinateur;
  • %%__%%domainname ? Nom de domaine de l'ordinateur.

Chacun de ces champs est une chaîne de caractères.

Le petit programme du Listing printuname affiche les numéros de version du noyau et des informations sur le matériel.

Affiche le Numéro de Version et des Infos Matérielles print-uname.c
Sélectionnez

#include <stdio.h>
#include <sys/utsname.h>

int main ()
{
  struct utsname u;
  uname (&amp;u);
  printf ("%s version %s.%s sur système %s\n", u.sysname, u.release,
          u.version, u.machine);
  return 0;
}

précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Copyright © 2007 Mark Mitchell, Jeffrey Oldham et Alex Samuel. Ce document ne peut être distribué que dans le respect des termes et conditions définies par l?Open Poublication License, v1.0 ou ultérieure (la dernière version est disponible sur http://www.opencontent.org/openpub/).