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).
Lorsqu’un processus reçoit un signal, plusieurs comportements sont possibles.
core
,
qu’on peut examiner plus tard avec un debugger; c’est ce qu’on
appelle un core dump.
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é:
Nom | Signification | Comportement |
sighup | Hang-up (fin de connexion) | Terminaison |
sigint | Interruption (ctrl-C ) | Terminaison |
sigquit | Interruption forte (ctrl-\ ) | Terminaison + core dump |
sigfpe | Erreur arithmétique (division par zéro) | Terminaison + core dump |
sigkill | Interruption très forte (ne peut être ignorée) | Terminaison |
sigsegv | Violation des protections mémoire | Terminaison + core dump |
sigpipe | Écriture sur un tuyau sans lecteurs | Terminaison |
sigalrm | Interruption d’horloge | Ignoré |
sigtstp | Arrêt temporaire d’un processus (ctrl-Z ) | Suspension |
sigcont | Redémarrage d’un processus arrêté | Ignoré |
sigchld | Un des processus fils est mort ou a été arrêté | Ignoré |
Les signaux reçus par un programme proviennent de plusieurs sources possibles:
sigint
à tous les processus lancés depuis ce
terminal (qui n’ont pas été mis en arrière plan) quand l’utilisateur tape le
caractère d’interruption ctrl-C
. De même, il envoie sigquit
quand
l’utilisateur tape
ctrl-\
1. Et il envoie sighup
lorsque la connexion avec
le terminal est fermée, ou bien parce que l’utilisateur s’est déconnecté, ou
bien, dans le cas d’une connexion à travers un modem, parce que la liaison
téléphonique a été coupée.
kill
. Cette commande permet
d’envoyer un signal quelconque à un processus quelconque. Par exemple,
kill -KILL 194
envoie le signal sigkill
au processus 194,
ce qui a pour effet de terminer à coup sûr ce processus.
kill
(le cas précédent en étant un cas particulier).
sigfpe
.
sigchld
.
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.
L’appel système signal permet de changer le comportement du processus lorsqu’il reçoit un signal d’un certain type.
val signal : int -> 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 stdout; alarm 30; ();; ... signal sigalrm (Signal_handle beep); alarm 30;; |
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.
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_BLOCK | les signaux sigs sont ajoutés aux signaux bloqués. |
SIG_UNBLOCK | les signaux sigs sont retirés des signaux débloqués. |
SIG_SETMASK | les 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_mask) in Misc.try_finalize treat () reset () |
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 [] pid
où pid
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
flags
)
pid
.
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_mask) in let system_call () = match fork() with | 0 -> reset(); begin try exec args with _ -> exit 127 end | k -> snd (restart_on_EINTR (waitpid []) k) in 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).
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 <> sigalrm) old_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.)
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 |
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_REAL | temps réel | sigalrm |
ITIMER_VIRTUAL | temps utilisateur | sigvtalrm |
ITIMER_PROF | utilisateur 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).
it_interval
indique la période du sablier.
it_value
est la valeur courante du sablier; lorsqu’elle devient
nulle, le signal sigvtalarm
est émis et le sablier est remis à la valeur
it_interval
.
Un sablier est donc inactif lorsque ses deux champs sont nuls. Les sabliers peuvent être consultés ou modifiés avec les fonctions suivantes:
La valeur retournée par setitimer
est l’ancienne valeur du sablier au
moment de la modification.
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 |
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é).
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 ??.
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.