Previous Up Next

Chapter 4  Les signaux

Les signaux, ou interruptions logicielles, sont des événements externes qui changent le déroulement d’un programme de manière asynchrone, c’est-à-dire à n’importe quel instant lors de l’exécution du programme. En ceci les signaux s’opposent aux autres formes de communications où les programmes doivent explicitement demander à recevoir les messages externes en attente, par exemple en faisant read sur un tuyau.

Les signaux transportent peu d’information (le type du signal et rien d’autre) et n’ont pas été conçus pour communiquer entre processus mais pour permettre à un processus de recevoir des informations atomiques sur l’évolution de l’environnement extérieur (l’état du système ou d’autres processus).

4.1  Le comportement par défaut

Lorsqu’un processus reçoit un signal, plusieurs comportements sont possibles.

Il y a plusieurs types de signaux, indiquant chacun une condition particulière. Le type énuméré signal en donne la liste. En voici quelques-uns, avec le comportement par défaut associé:

NomSignificationComportement
sighupHang-up (fin de connexion)Terminaison
sigintInterruption (ctrl-C)Terminaison
sigquitInterruption forte (ctrl-\)Terminaison + core dump
sigfpeErreur arithmétique (division par zéro)Terminaison + core dump
sigkillInterruption très forte (ne peut être ignorée)Terminaison
sigsegvViolation des protections mémoireTerminaison + core dump
sigpipeÉcriture sur un tuyau sans lecteursTerminaison
sigalrmInterruption d’horlogeIgnoré
sigtstpArrêt temporaire d’un processus (ctrl-Z)Suspension
sigcontRedémarrage d’un processus arrêtéIgnoré
sigchldUn des processus fils est mort ou a été arrêtéIgnoré

Les signaux reçus par un programme proviennent de plusieurs sources possibles:

4.2  Produire des signaux

L’appel système kill permet d’envoyer un signal à un processus.

     
   val kill : int -> int -> unit

Le paramètre entier est le numéro du processus auquel le signal est destiné. Une erreur se produit si on envoie un signal à un processus n’appartenant pas au même utilisateur que le processus émetteur. Un processus peut s’envoyer des signaux à lui-même. Lorsque l’appel système kill retourne, il est garanti que le signal a été délivré au processus destinataire. C’est-à-dire que si le processus destinataire n’ignore pas et ne masque pas le signal, sa première action sera de traiter un signal (celui-ci ou un autre). Si un processus reçoit plusieurs fois le même signal pendant un laps de temps très court, il peut n’exécuter qu’un seule fois (le code associé à) ce signal. Un programme ne peut donc pas compter le nombre de fois qu’il reçoit un signal, mais seulement le nombre de fois qu’il le traite.

L’appel système alarm permet de produire des interruptions d’horloge.

     
   val alarm : int -> int

L’appel alarm s retourne immédiatement, mais fait envoyer au processus le signal sigalrm (au moins) s secondes plus tard (le temps maximal d’attente n’est pas garanti). L’appel renvoie le nombre de seconde restante jusqu’à la programmation précédente. Si s est nulle, l’effet est simplement d’annuler la précédente programmation de l’alarme.

4.3  Changer l’effet d’un signal

L’appel système signal permet de changer le comportement du processus lorsqu’il reçoit un signal d’un certain type.

     
   val signalint -> signal_behavior -> signal_behavior

Le deuxième argument indique le comportement désiré. Si le deuxième argument est la constante Signal_ignore, le signal est ignoré. Si le deuxième argument est Signal_default, le comportement par défaut est restauré. Si le deuxième argument est Signal_handle f, où f est une fonction de type unit -> unit, la fonction f sera appelée à chaque fois qu’on reçoit le signal.

L’appel fork préserve les comportements des signaux: les comportements initiaux pour le fils sont ceux pour le père au moment du fork. Les appels exec remettent les comportements à Signal_default, à une exception près: les signaux ignorés avant le exec restent ignorés après.

Exemple:

On veut parfois se déconnecter en laissant tourner des tâches de fond (gros calculs, programmes “espions”, etc). Pour ce faire, il faut éviter que les processus qu’on veut laisser tourner ne terminent lorsqu’ils reçoivent le signal SIGHUP envoyé au moment où l’on se déconnecte. Il existe une commande nohup qui fait précisément cela:

nohup   cmd   arg1   …   argn 

exécute la commande cmd   arg1   …   argn en la rendant insensible au signal SIGHUP. (Certains shells font automatiquement nohup sur tous les processus lancés en tâche de fond.) Voici comment l’implémenter en trois lignes:

     
   open Sys;;
   signal sighup Signal_ignore;;
   Unix.execvp argv.(1) (Array.sub argv 1 (Array.length argv - 1));;

L’appel execvp préserve le fait que sighup est ignoré.

Voici maintenant quelques exemples d’interception de signaux.

Exemple:

Pour sortir en douceur quand le programme s’est mal comporté. Par exemple, un programme comme tar peut essayer de sauver une information importante dans le fichier ou détruire le fichier corrompu avant de s’arrêter. Il suffit d’exécuter, au début du programme:

     
   signal sigquit (Signal_handle quit);
   signal sigsegv (Signal_handle quit);
   signal sigfpe  (Signal_handle quit);

où la fonction quit est de la forme:

     
   let quit() =
     (* Essayer de sauver l'information importante dans le fichier *);
     exit 100;;

Exemple:

Pour récupérer les interruptions de l’utilisateur. Certains programmes interactifs peuvent par exemple vouloir revenir dans la boucle de commande lorsque l’utilisateur frappe ctrl-C. Il suffit de déclencher une exception lorsqu’on reçoit le signal SIGINT.

     
   exception Break;;
   let break () = raise Break;;
   ...
   let main_loop() =
     signal sigint (Signal_handle break);
     while true do
       try
         (* lire et exécuter une commande *)
       with Break ->
         (* afficher "Interrompu" *)
     done;;

Exemple:

Pour exécuter des tâches périodiques (animations, etc) entrelacées avec l’exécution du programme principal. Par exemple, voici comment faire “bip” toutes les 30 secondes, quel que soit l’activité du programme (calculs, entrées-sorties).

     
   let beep () = output_char stdout `\007`; flush stdoutalarm 30; ();;
   ...
   signal sigalrm (Signal_handle beep); alarm 30;;

Points de contrôle

Les signaux transmettent au programme une information de façon asynchrone. C’est leur raison d’être, et ce qui les rend souvent incontournables, mais c’est aussi ce qui en fait l’une des grandes difficultés de la programmation système.

En effet, le code de traitement s’exécute à la réception d’un signal, qui est asynchrone, donc de façon pseudo concurrente (c’est-à-dire entrelacée) avec le code principal du programme. Comme le traitement d’un signal ne retourne pas de valeur, leur intérêt est de faire des effets de bords, typiquement modifier l’état d’une variable globale. Il s’ensuit une compétition (race condition) entre le signal et le programme principal pour l’accès à cette variable globale. La solution consiste en général à bloquer les signaux pendant le traitement de ces zones critiques comme expliqué ci-dessous.

Toutefois, OCaml ne traite pas les signaux de façon tout à faire asynchrone. À la réception du signal, il se contente d’enregistrer sa réception et le traitement ne sera effectué, c’est-à-dire le code de traitement associé au signal effectivement exécuté, seulement à certains points de contrôles. Ceux-ci sont suffisamment fréquents pour donner l’illusion d’un traitement asynchrone. Les points de contrôles sont typiquement les points d’allocation, de contrôle de boucles ou d’interaction avec le système (en particulier autour des appels systèmes). OCaml garantit qu’un code qui ne boucle pas, n’alloue pas, et n’interagit pas avec le système ne sera pas entrelacer avec le traitement d’un signal. En particulier l’écriture d’une valeur non allouée (entier, booléen, etc. mais pas un flottant!) dans une référence ne pose pas de problème de compétition.

4.4  Masquer des signaux

Les signaux peuvent être bloqués. Les signaux bloqués ne sont pas ignorés, mais simplement mis en attente, en général pour être délivrés ultérieurement. L’appel système sigprocmask permet de changer le masque des signaux bloqués:

     
   val sigprocmask : sigprocmask_command -> int list -> int list

sigprocmask cmd sigs change l’ensemble des signaux bloqués et retourne la liste des signaux qui étaient bloqués juste avant l’exécution de la commande, ce qui permettra ultérieurement de remettre le masque des signaux bloqués dans son état initial. L’argument sigs est une liste de signaux dont le sens dépend de la commande cmd:

SIG_BLOCKles signaux sigs sont ajoutés aux signaux bloqués.
SIG_UNBLOCKles signaux sigs sont retirés des signaux débloqués.
SIG_SETMASKles signaux sigs sont exactement les signaux bloqués.

Un usage typique de sigprocmask est de masquer temporairement certains signaux.

     
   let old_mask = sigprocmask cmd sigs in
   (* do something *)
   let _ = sigprocmask SIG_SETMASK old_mark

Bien souvent, on devra se protéger contre les erreurs éventuelles en utilisant plutôt le schéma:

     
   let old_mask = sigprocmask cmd sigs in
   let treat() = ((* do something *)in
   let reset() = ignore (sigprocmask SIG_SETMASK old_maskin
   Misc.try_finalize treat () reset ()

4.5  Signaux et appels-système

Attention! un signal non ignoré peut interrompre certains appels système. En général, les appels systèmes interruptibles sont seulement des appels systèmes dits lents, qui peuvent a priori prendre un temps arbitrairement long: par exemple, lectures/écritures au terminal, select (voir plus loin), system, etc. En cas d’interruption, l’appel système n’est pas exécuté et déclenche l’exception EINTR. Noter que l’écriture/lecture dans un fichier ne sont pas interruptibles: bien que ces opérations puissent suspendre le processus courant pour donner la main à un autre le temps que les données soient lues sur le disques, lorsque c’est nécessaire, cette attente sera toujours brève—si le disque fonctionne correctement. En particulier, l’arrivée des données ne dépend que du système et pas d’un autre processus utilisateur.

Les signaux ignorés ne sont jamais délivrés. Un signal n’est pas délivré tant qu’il est masqué. Dans les autres cas, il faut se prémunir contre une interruption possible.

Un exemple typique est l’attente de la terminaison d’un fils. Dans ce cas, le père exécute waitpid [] pidpid est le numéro du fils à attendre. Il s’agit d’un appel système bloquant, donc «lent», qui sera interrompu par l’arrivée éventuelle d’un signal. En particulier, le signal sigchld est envoyé au processus père à la mort d’un fils.

Le module Misc contient la fonction suivante restart_on_EINTR de type (’a -> ’b) -> ’a -> ’b qui permet de lancer un appel système et de le répéter lorsqu’il est interrompu par un signal, i.e. lorsqu’il lève l’exception EINTR.

     
   let rec restart_on_EINTR f x =
     try f x with Unix_error (EINTR__) -> restart_on_EINTR f x

Pour attendre réellement un fils, on pourra alors simplement écrire restart_on_EINTR (waitpid flagspid.

Exemple:

Le père peut aussi récupérer ses fils de façon asynchrone, en particulier lorsque la valeur de retour n’importe pas pour la suite de l’exécution. Cela peut se faire en exécutant une fonction free_children à la réception du signal sigchld. Nous plaçons cette fonction d’usage général dans la bibliothèque Misc.

     
   let free_children signal =
     try while fst (waitpid [ WNOHANG ] (-1)) > 0 do () done
     with Unix_error (ECHILD__) -> ()

Cette fonction appelle la fonction waitpid en mode non bloquant (option WNOHANG) et sur n’importe quel fils, et répète l’appel quand qu’un fils a pu être retourné. Elle s’arrête lorsque il ne reste plus que des fils vivants (zéro est retourné à la place de l’identité du processus délivré) ou lorsqu’il n’y a plus de fils (exception ECHILD). Lorsque le processus reçoit le signal sigchld il est en effet impossible de savoir le nombre de processus ayant terminé, si le signal est émis plusieurs fois dans un intervalle de temps suffisamment court, le père ne verra qu’un seul signal. Noter qu’ici il n’est pas nécessaire de se prémunir contre le signal EINTR car waitpid n’est pas bloquant lorsqu’il est appelé avec l’option WNOHANG.

Dans d’autres cas, ce signal ne peut être ignoré (l’action associée sera alors de libérer tous les fils ayant terminé, de façon non bloquante—on ne sait jamais combien de fois le signal à été émis).

Exemple:

La commande system du module Unix est simplement définie par

     
   let system cmd =
     match fork() with
       0 ->
         begin try
           execv "/bin/sh" [| "/bin/sh""-c"cmd |]; assert false
         with _ -> exit 127
         end
     | id -> snd(waitpid [] id);;

L’assertion qui suit l’appel système execv est là pour corriger une restriction erronée du type de retour de la fonction execv (dans les version antérieures ou égale à 3.07). L’appel système ne retournant pas, aucune contrainte ne doit porter sur la valeur retournée, et bien sûr l’assertion ne sera jamais exécutée.

La commande system de la bibliothèque standard de la bibliothèque C précise que le père ignore les signaux sigint et sigquit et masque le signal sigchld pendant l’exécution de la commande. Cela permet d’interrompre ou de tuer le programme appelé (qui reçoit le signal) sans que le programme principal ne soit affecté pendant l’exécution de la commande.

Nous préférons définir la fonction system comme spécialisation d’une fonction plus générale exec_as_system qui n’oblige pas à faire exécuter la commande par le shell. Nous la plaçons dans le module Misc.

     
   let exec_as_system exec args =
     let old_mask = sigprocmask SIG_BLOCK [sigchld ] in
     let old_int = signal sigint Signal_ignore in
     let old_quit = signal sigquit Signal_ignore in
     let reset() =
       ignore (signal sigint old_int);
       ignore (signal sigquit old_quit);
       ignore (sigprocmask SIG_SETMASK old_maskin
     let system_call () =
       match fork() with
       | 0 ->
           reset();
           begin try
             exec args
           with _ -> exit 127
           end
       | k ->
           snd (restart_on_EINTR (waitpid []) kin
     try_finalize system_call() reset();;
   
   let system cmd =
     exec_as_system (execv "/bin/sh") [| "/bin/sh""-c"cmd |];;

Noter que le changement des signaux doit être effectué avant l’appel à fork. En effet, immédiatement après cet appel, seulement l’un des deux processus fils ou père a la main (en général le fils). Pendant le laps de temps où le fils prend la main, le père pourrait recevoir des signaux, notamment sigchld si le fils termine immédiatement. En conséquence, il faut remettre les signaux à leur valeur initiale dans le fils avant d’exécuter la commande (ligne 13). En effet, l’ensemble des signaux ignorés est préservé par fork et exec et le comportement des signaux est lui-même préservé par fork. La commande exec remet normalement les signaux à leur valeur par défaut, sauf justement si le comportement est d’ignorer le signal.

Enfin, le père doit remettre également les signaux à leur valeur initiale, immédiatement après l’appel, y compris en cas d’erreur, d’où l’utilisation de la commande try_finalize (ligne 20).

4.6  Le temps qui passe

Temps anciens

Dans les premières versions d’Unix, le temps était compté en secondes. Par soucis de compatibilité, on peut toujours compter le temps en secondes. La date elle-même est comptée en secondes écoulées depuis le 1er janvier 1970 à 00:00:00 GMT. Elle est retournée par la fonction:

     
   val time : unit -> float

L’appel système sleep peut arrêter l’exécution du programme pendant le nombre de secondes donné en argument:

     
   val sleep : int -> unit

Cependant, cette fonction n’est pas primitive. Elle est programmable avec des appels systèmes plus élémentaire à l’aide de la fonction alarm (vue plus haut) et sigsuspend:

     
   val sigsuspend : int list -> unit

L’appel sigsuspend l suspend temporairement les signaux de la liste l, puis arrête l’exécution du programme jusqu’à la réception d’un signal non ignoré non suspendu (au retour, le masque des signaux est remis à son ancienne valeur par le système). Exemple:

Nous pouvons maintenant programmer la fonction sleep.

     
   let sleep s =
     let old_alarm = signal sigalrm (Signal_handle (fun s -> ())) in
     let old_mask = sigprocmask SIG_UNBLOCK [ sigalrm ] in
     let _ = alarm s in
     let new_mask = List.filter (fun x -> x <> sigalrmold_mask in
     sigsuspend new_mask;
     let _ = alarm 0 in
     ignore (signal sigalrm old_alarm);
     ignore (sigprocmask SIG_SETMASK old_mask);;

Dans un premier temps, le comportement du signal sigalarm est de ne rien faire. Notez que «ne rien faire» n’est pas équivalent à ignorer le signal. Dans le second cas, le processus ne serait pas réveillé à la réception du signal. Le signal sigalarm est mis dans l’état non bloquant. Puis on se met en attente en suspendant tous les autres signaux qui ne l’étaient pas déjà (old_mask). Après le réveil, on défait les modifications précédentes. (Noter que la ligne 9 aurait pu est placée immédiatement après la ligne 2, car l’appel à sigsuspend préserve le masque des signaux.)

Temps modernes et sabliers

Dans les Unix récents, le temps peut aussi se mesurer en micro-secondes. En OCaml les temps en micro-secondes sont représentés comme des flottants. La fonction gettimeofday est l’équivalent de time pour les temps modernes.

     
   val gettimeofday : unit -> float

Les sabliers

Dans les Unix modernes chaque processus est équipé de trois sabliers, chacun décomptant le temps de façon différente. Les types de sabliers sont:

ITIMER_REALtemps réelsigalrm
ITIMER_VIRTUALtemps utilisateursigvtalrm
ITIMER_PROFutilisateur et système (debug)sigprof

L’état d’un sablier est décrit par le type interval_timer_status qui est une structure à deux champs (de type float représentant le temps).

Un sablier est donc inactif lorsque ses deux champs sont nuls. Les sabliers peuvent être consultés ou modifiés avec les fonctions suivantes:

     
   val getitimer : interval_timer -> interval_timer_status
   val setitimer :
       interval_timer -> interval_timer_status -> interval_timer_status

La valeur retournée par setitimer est l’ancienne valeur du sablier au moment de la modification.

Exercice 11   Pour gérer plusieurs sabliers, écrire une module ayant l’interface suivante:
     
   module type Timer = sig
   
  open Unix
   
  type t
   
  val new_timer : interval_timer -> (unit -> unit) -> t
   
  val get_timer : t -> interval_timer_status
   
  val set_timer : t -> interval_timer_status -> interval_timer_status
   
end
La fonction new_timer k f créé un nouveau sablier de type k déclenchant l’action f, inactif à sa création; la fonction set_timer t permettant de régler le sablier t (l’ancien réglage étant retourné).

Calcul des dates

Les versions modernes d’Unix fournissent également des fonctions de manipulation des dates en bibliothèque: voir la structure tm qui permet de représenter les dates selon le calendrier (année, mois, etc.) ainsi que les fonctions de conversion: gmtime, localtime, mktime, etc. dans l’annexe ??.

4.7  Problèmes avec les signaux

L’utilisation des signaux, en particulier comme mécanisme de communication asynchrone inter-processus, se heurte à un certain nombre de limitations et de difficultés:

Les signaux apportent donc toutes les difficultés de la communication asynchrone, tout en n’en fournissant qu’une forme très limitée. On aura donc intérêt à s’en passer lorsqu’il que c’est possible, par exemple en utilisant select plutôt qu’une alarme pour se mettre en attente. Toutefois, dans certaines situations (langage de commandes, par essence), leur prise en compte est vraiment indispensable.

Les signaux sont peut-être la partie la moins bien conçue du système Unix. Sur certaines anciennes versions d’Unix (en particulier System V), le comportement d’un signal est automatiquement remis à Signal_default lorsqu’il est intercepté. La fonction associée peut bien sûr rétablir le bon comportement elle-même; ainsi, dans l’exemple du “bip” toutes les 30 secondes, il faudrait écrire:

     
   let rec beep () =
     set_signal SIGALRM (Signal_handle beep);
     output_char stdout `\007`; flush stdout;
     alarm 30; ();;

Le problème est que les signaux qui arrivent entre le moment où le comportement est automatiquement remis à Signal_default et le moment où le set_signal est exécuté ne sont pas traités correctement: suivant le type du signal, ils peuvent être ignorés, ou causer la mort du processus, au lieu d’exécuter la fonction associée.

D’autres versions d’Unix (BSD ou Linux) traitent les signaux de manière plus satisfaisante: le comportement associé à un signal n’est pas changé lorsqu’on le reçoit; et lorsqu’un signal est en cours de traitement, les autres signaux du même type sont mis en attente.


1
Ces caractères sont choisis par défaut, mais il est possible de les changer en modifiant les paramètres du terminal, voir section 2.13.

Previous Up Next