wiki:InsiaProgCEvenement

Sommaire

Signaux et boucles d'événement

1. Signaux

Jusqu'ici nous avons écrit des programmes séquentiels: même lorsqu'ils interagissent avec des évenements extérieurs (ex: saisie clavier), ils le font de manière explicite en "attendant" l'information. Les signaux sont des événements qui peuvent être générés par différentes sources (noyau et autres processus) et vont interrompte le flux normal du déroulement du programme pour l'exécuter à un point précis et prédéfini.

Quelques exemples classiques de signaux avec leur source:

  • SIGSEGV: quand une erreur d'accès à la mémoire est détectée par le noyau, le noyau envoie un signal SIGSEGV au processus fautif. Par défaut, le message "Segmentation fault" est affiché et le programme arrêté.
  • SIGINT: ce signal est typiquement généré par votre shell lorsque vous appuyez sur "Ctrl+C" et est envoyé au processus en cours d'exécution. Par défaut, le programme est immédiatement interrompu.
  • SIGFPE: ce signal est émis par le noyau vers un processus qui a provoqué une erreur de calcul sur les nombres à virgule flottante (dépassement, division par zéro, etc.)
  • SIGKILL: ce signal provoque l'arrête inconditionnel et immédiat du programme qui le reçoit, et ce comportement peut être modifié. Aussi connu par la fameuse commande kill -9.

L'ensemble des signaux est documenté dans la page de manuel signal(7).

Note: les signaux font parties des mécanismes appelés IPC (inter-process communication). L'exemple le plus connu est celui de la commande d'administration /etc/init.d/apache reload qui demande à une instance d'Apache en cours d'exécution de recharger sa configuration en lui envoyant un signal précis.

Interception d'un signal

Un signal peut être intercepté par un programme à l'aide des fonctions signal(3) ou sigaction, la dernière forme étant largement préférée. Pour sa simplicité, nous illustrons le principe avec la première forme:

void signal_recu(int sigval) {
  printf("Signal %d reçu!\n", sigval);
}

int main(int argc, char** argv) {
  signal(SIGINT, signal_recu);
  signal(SIGHUP, signal_recu);
  ...
}

L'un des paramètres de signal(3) est une fonction dont le prototype est très simple. Cette fonction étant appelé avec le numéro du signal reçu, elle peut être utilisée pour intercepter différents signaux.

Interruption/reprise d'un appel système

Si votre programme est en train de s'exécuter normalement, la réception d'un signal provoque un simple branchement immédiat vers la fonction enregistrée, puis reprend son cours exactement à l'endroit de l'interruption. Mais que se passe-t-il si le programme a été interrompu alors qu'il ne s'exécutait pas, mais par exemple "attendait" sur un appel système comme fgets ?

Il est possible (avec sigaction) de spécifier si l'appel système interrompu doit être repris ou terminé. Ainsi, si on veut afficher un crhonomètre en attendant la saisie d'un utilisateur, on pourrait écrire ceci:

int decompte = 10;

void chronometre(int sigval) {
  printf("Temps restant: %d seconde(s)\n", decompte--);
  alarm(1);
}

int main(int argc, char** argv) {
  struct sigaction action;
  char ligne[100];

  action.sa_handler = chronometre;
  action.sa_flags   = SA_RESTART;
  sigaction(SIGALRM, &action, NULL);

  alarm(1);  
  fgets(ligne, 100, stdin);
  ...
}

Notez une nouvelle méthode pour émettre un signal, la fonction alarm(2). Lors de son appel, un signal SIGALRM est planifié n secondes plus tard, où n est le paramètre de la fonction. Il faut appeler alarm autant de fois que l'on veut recevoir de signaux. C'est pour cela que dans notre exemple la fonction est rappelée dans chronometre, on dit qu'on "réarme" le signal.

Réentrance

Le mécanisme d'interruption pour la gestion des signaux est problématique, car il change le flux d'exécution du programme d'une façon non prévisible par le programmeur. En particulier, on peut se retrouver dans le cas où alors que nous sommes interrompus dans une fonction donnée, le gestionnaire de signal nous ramène dans cette même fonction. Si cette fonction repose sur une ressource unique, on peut alors avoir un conflit d'intérêt. C'est en général toujours le cas quand cette fonction utilise une variable globale. Le problème de réentrance sera étudié plus en détail dans le threading.

2. Boucles d'événements

Très souvent un programme doit pouvoir réagir sur plusieurs sources d'événement de manière "simultanée". L'exemple classique est un programme de "chat", où à chaque instant un événément peut provenir du clavier (saisie locale) ou d'un autre processus connecté (saisie distante). Il s'agit en quelque sorte de la généralisation des fonctions qui "bloquent" sur des descripteurs de fichier (stdin, etc) et rendent la main au programme dès qu'elles ont obtenu leur information.

select()

Le système Unix possède une interface qui permet de pouvoir surveiller plusieurs sources d'événements à la fois, et qui repose sur les descripteurs de fichier. En effet, sous Unix, quasiment toutes les ressources de communication (fichier, réseau, périphérique, etc) peuvent être représentées par un descripteur de fichier. Exemple:

int boucle_chat(int clavier, int distant) {
  fd_set r_set;

  FD_ZERO(&r_set);
  FD_SET(clavier, &r_set);
  FD_SET(distant, &r_set);
  return select(distant + 1, &r_set, NULL, NULL, NULL);
}

int main(int argc, char** argv) {
  int clavier = 1;
  int distant;
  
  distant = socket(AF_INET, SOCK_STREAM, 0);
  ...
  while (boucle_chat(clavier, distant));
  return 0;
}

L'utilisation de select() est générique mais en général très fastidieuse à programmer. La plupart du temps on recourt à une bibliothèque qui fournit une interface beaucoup plus simple, comme un toolkit graphique (cf. GTK+, Swing ou Qt): on revient alors à une approche similaire à la gestion de signaux où l'on fournit des fonctions qui seront appelées lors d'un événement précis (appelées callback).

Charge

Le noyau du système d'exploitation possède lui-même une large boucle d'événement avec laquelle il distribue à la fois les événements matériels aux processus et également le temps processeur disponible. Il est intéressant de comprendre comment fonctionne cette distribution pour bien programmer ses boucles select().

En simplifiant, on peut considérer qu'un processus a deux états d'activité:

  • il est en cours d'exécution, appelé l'état R (running)
  • il est en attente d'un événement (par ex. dans un fgets()), appelé état S (sleeping)

Régulièrement, typiquement 100 à 200 fois par seconde, le noyau doit décider quel processus doit s'exécuter pendant un court laps de temps, simulant ainsi le partage effectif du processeur. Pour ce faire il va élire un des programmes avec l'état R pour chaque laps de temps. Il va également passer un processus de l'état S à R quand l'événement ad hoc intervient (ex: données du disque ou du clavier disponibles pour le fgets()).

On appelle "charge du processeur" le nombre moyen de processus à l'état R à un instant donné. Quelques propriétés de ce nombre:

  • si la charge vaut 0.5, cela signifie que le noyau a trouvé un programme à exécuter une fois sur deux, et donc que le processeur est utilisé à la moitié de ses capacités
  • si la charge vaut 2, cela signifie qu'il y a en moyenne 2 processus en cours d'exécution - mais bien sûr le noyau ne peut en exécuter réellement qu'un à chaque tranche de temps. On est alors en surcharge, car il y a plus de demande de temps processeur que de disponible
  • si la charge vaut 1, il y a en moyenne autant de processus en demande d'exécution que de temps disponible

On peut consulter la charge à l'aide des commandes uptime ou top. Elle est indiquée sous forme de valeur moyenne sur des intervalles de temps différents (typiquement 1, 5 et 15 minutes).

Note: sur les systèmes multi-processeur, la charge globale est la somme des charges des processeurs. Ainsi un système bi-processeur a une charge optimale de 2.

Last modified 14 years ago Last modified on Nov 13, 2006, 12:24:20 PM