Un processus est un programme en train de s’exécuter. Un processus se compose d’un texte de programme (du code machine) et d’un état du programme (point de contrôle courant, valeur des variables, pile des retours de fonctions en attente, descripteurs de fichiers ouverts, etc.)
Cette partie présente les appels systèmes Unix permettant de créer de nouveaux processus et de leur faire exécuter d’autres programmes.
L’appel système fork permet de créer un processus.
val fork : unit -> int |
Le nouveau processus (appelé “le processus fils”) est un clone
presque parfait du processus qui a appelé fork
(dit “le processus
père” 1): les deux processus (père et fils) exécutent le même texte de
programme, sont initialement au même point de contrôle (le retour de
fork
), attribuent les mêmes valeurs aux variables, ont des piles de
retours de fonctions identiques, et détiennent les mêmes descripteurs
de fichiers ouverts sur les mêmes fichiers. Ce qui distingue les deux
processus, c’est la valeur renvoyée par fork
: zéro dans le processus
fils, un entier non nul dans le processus père. En testant la
valeur de retour de fork
, un programme peut donc déterminer s’il est
dans le processus père ou dans le processus fils, et se comporter
différemment dans les deux processus:
match fork() with 0 -> (* code execute uniquement par le fils *) | _ -> (* code execute uniquement par le pere *) |
L’entier non nul renvoyé par fork
dans le processus père est
l’identificateur du processus fils. Chaque processus est identifié
dans le noyau par un numéro, l’identificateur du processus (process id). Un processus peut obtenir son numéro d’identification
par l’appel getpid"()".
Le processus fils est initialement dans le même état que le
processus père (mêmes valeurs des variables, mêmes descripteurs de
fichiers ouverts). Cet état n’est pas partagé entre le père et
le fils, mais seulement dupliqué au moment du fork
. Par exemple,
si une variable est liée à une référence avant le fork
, une copie de
cette référence et de son contenu courant est faite au moment du
fork
; après le fork
, chaque processus modifie indépendamment
“sa” référence, sans que cela se répercute sur l’autre processus.
De même, les descripteurs de fichiers ouverts sont dupliqués au moment du
fork
: l’un peut être fermé et l’autre reste ouvert. Par contre, les deux
descripteurs désignent la même entrée dans la table des fichiers (qui
est allouée dans la mémoire système) et partagent donc leur position courante:
si l’un puis l’autre lit, chacun lira une partie différente du fichier; de
même les déplacement effectués par l’un avec lseek
sont immédiatement
visibles par l’autre. (Les descripteurs du fils et du père se
comportent donc comme les deux descripteurs argument et résultat après
exécution de la commande dup
, mais sont dans des processus différents au
lieu d’être dans le même processus.)
La commande leave
hhmm rend la main immédiatement, mais crée un
processus en tâche de fond qui, à l’heure hhmm, rappelle qu’il
est temps de partir.
open Sys;; open Unix;; let leave () = let hh = int_of_string (String.sub Sys.argv.(1) 0 2) and mm = int_of_string (String.sub Sys.argv.(1) 2 2) in let now = localtime(time()) in let delay = (hh - now.tm_hour) * 3600 + (mm - now.tm_min) * 60 in if delay <= 0 then begin print_endline "Hey! That time has already passed!"; exit 0 end; if fork() <> 0 then exit 0; sleep delay; print_endline "\007\007\007Time to leave!"; exit 0;; handle_unix_error leave ();; |
On commence par un parsing rudimentaire de la ligne de commande, pour
extraire l’heure voulue. On calcule ensuite la durée d’attente, en
secondes (ligne 8). (L’appel time
renvoie la date courante, en
secondes depuis le premier janvier 1970, minuit. La fonction
localtime
transforme ça en année/mois/jour/heures/minutes/secondes.)
On crée alors un nouveau processus par fork
. Le processus père
(celui pour lequel fork
renvoie un entier non nul) termine
immédiatement. Le shell qui a lancé leave
rend donc aussitôt la
main à l’utilisateur. Le processus fils (celui pour lequel fork
renvoie zéro) continue à tourner. Il ne fait rien pendant la durée
indiquée (appel sleep
), puis affiche son message et termine.
L’appel système wait attend qu’un des processus fils créés par
fork
ait terminé, et renvoie des informations sur la manière dont ce
processus a terminé. Il permet la synchronisation père-fils, ainsi
qu’une forme très rudimentaire de communication du fils vers le père.
L’appel système primitif est waitpid et
la fonction wait()
n’est qu’un racourci pour l’expression waitpid [] (-1)
.
L’appel système waitpid
[] p attend la terminaison du processus p,
si p>0 est strictement positif, ou d’un sous-ensemble de processus fils,
du même groupe si p=0, quelconque si p=−1, ou du groupe −p si p<−1.
Le premier résultat est le numéro du processus fils intercepté par
wait
. Le deuxième résultat peut être:
WEXITED (r) | le processus fils a terminé normalement (par exit
ou en arrivant au bout du programme); r est le code de retour
(l’argument passé à exit ) |
WSIGNALED (sig) | le processus fils a été tué par un signal
(ctrl-C, kill , etc.; voir plus bas pour les signaux); sig
identifie le type du signal |
WSTOPPED (sig) | le processus fils a été stoppé par le signal
sig; ne se produit que dans le cas très particulier où un processus
(typiquement un debugger) est en train de surveiller l’exécution d’un
autre (par l’appel ptrace ). |
Si un des processus fils a déjà terminé au moment où le père exécute
wait
, l’appel wait
retourne immédiatement. Sinon, wait
bloque le
père jusqu’à ce qu’un des fils termine (comportement dit “de
rendez-vous”). Pour attendre N fils, il faut répéter N
fois wait
.
La commande waitpid
accepte deux options facultatifs comme premier
argument: L’option WNOHANG
indique de ne pas attendre, s’il il a des fils
qui répondent à la demande mais qui n’ont pas encore terminé. Dans ce cas,
le premier résultat est 0 et le second non défini. L’option WUNTRACED
indique de retourner également les fils qui ont été arrêté par le signal
sigstop
. La commande lève l’erreur ECHILD
si aucun fils du processus
appelant ne répond à la spécification p (en particulier, si
p vaut −1 et que le processus courrant n’a pas ou plus de fils).
Exemple:
la fonction fork_search
ci-dessous fait une
recherche linéaire dans un vecteur, en deux processus. Elle s’appuie
sur la fonction simple_search
, qui fait la recherche linéaire simple.
open Unix exception Found;; let simple_search cond v = try for i = 0 to Array.length v - 1 do if cond v.(i) then raise Found done; false with Found -> true;; let fork_search cond v = let n = Array.length v in match fork() with 0 -> let found = simple_search cond (Array.sub v (n/2) (n-n/2)) in exit (if found then 0 else 1) | _ -> let found = simple_search cond (Array.sub v 0 (n/2)) in match wait() with (pid, WEXITED retcode) -> found or (retcode = 0) | (pid, _) -> failwith "fork_search";; |
Après le fork
, le processus fils parcourt la moitié
haute du tableau, et sort avec le code de retour 1 s’il a trouvé un élément
satisfaisant le prédicat cond
, ou 0 sinon (lignes 16 et 17). Le
processus père parcourt la moitié basse du tableau, puis
appelle wait
pour se synchroniser avec le processus fils (lignes 19 et
20). Si le fils a terminé normalement, on combine son code de retour
avec le booléen résultat de la recherche dans la moitié basse du
tableau. Sinon, quelque chose d’horrible s’est produit, et la fonction
fork_search
échoue.
En plus de la synchronisation entre processus, l’appel wait
assure
aussi la récupération de toutes les ressources utilisées par le
processus fils. Quand un processus termine, il passe dans un état dit
“zombie”, où la plupart des ressources qu’il utilise (espace
mémoire, etc) ont été désallouées, mais pas toutes: il continue à
occuper un emplacement dans la table des processus, afin de pouvoir
transmettre son code de retour au père via l’appel wait
. Ce n’est
que lorsque le père a exécuté wait
que le processus zombie disparaît de
la table des processus. Cette table étant de taille fixe, il importe,
pour éviter le débordement, de faire wait
sur les processus qu’on
lance.
Si le processus père termine avant le processus fils, le fils se voit
attribuer le processus numéro 1 (init
) comme père. Ce processus
contient une boucle infinie de wait
, et va donc faire disparaître
complètement le processus fils dès qu’il termine. Ceci débouche sur
une technique utile dans le cas où on ne peut pas facilement appeler
wait
sur chaque processus qu’on a créé (parce qu’on ne peut pas se
permettre de bloquer en attendant la terminaison des fils, par
exemple): la technique “du double fork
”.
match fork() with 0 -> if fork() <> 0 then exit 0; (* faire ce que le fils doit faire *) | _ -> wait(); (* faire ce que le pere doit faire *) |
Le fils termine par exit
juste après le deuxième fork
. Le petit-fils
se retrouve donc orphelin, et est adopté par le processus init
.
Il ne laissera donc pas de processus zombie. Le père exécute wait
aussitôt pour récupérer le fils. Ce wait
ne bloque pas longtemps
puisque le fils termine très vite.
Les appels système execve, execv et execvp lancent l’exécution d’un programme à l’intérieur du processus courant. Sauf en cas d’erreur, ces appels ne retournent jamais: ils arrêtent le déroulement du programme courant et se branchent au début du nouveau programme.
Le premier argument est le nom du fichier contenant le code du
programme à exécuter. Dans le cas de execvp
, ce nom est également
recherché dans les répertoires du path d’exécution (la valeur de la
variable d’environnement PATH
).
Le deuxième argument est la ligne de commande à transmettre au
programme exécuté; ce vecteur de chaînes va se retrouver dans
Sys.argv
du programme exécuté.
Dans le cas de execve
, le troisième argument est l’environnement à
transmettre au programme exécuté; execv
et execvp
transmettent
inchangé l’environnement courant.
Les appels execve
, execv
et execvp
ne retournent jamais de
résultat: ou bien tout se passe sans erreurs, et le processus se met à
exécuter un autre programme; ou bien une erreur se produit (fichier
non trouvé, etc.), et l’appel déclenche l’exception Unix_error
.
Exemple:
les trois formes ci-dessous sont équivalentes:
execve "/bin/ls" [|"ls"; "-l"; "/tmp"|] (environment()) execv "/bin/ls" [|"ls"; "-l"; "/tmp"|] execvp "ls" [|"ls"; "-l"; "/tmp"|] |
Exemple:
voici un “wrapper” autour de la commande grep
, qui
ajoute l’option -i
(confondre majuscules et minuscules) à la liste
d’arguments:
open Sys;; open Unix;; let grep () = execvp "grep" (Array.concat [ [|"grep"; "-i"|]; (Array.sub Sys.argv 1 (Array.length Sys.argv - 1)) ]) ;; handle_unix_error grep ();; |
Exemple:
voici un “wrapper” autour de la commande emacs
, qui
change le type du terminal:
open Sys;; open Unix;; let emacs () = execve "/usr/bin/emacs" Sys.argv (Array.concat [ [|"TERM=hacked-xterm"|]; (environment()) ]);; handle_unix_error emacs ();; |
C’est le même processus qui a fait exec
qui exécute le nouveau
programme. En conséquence, le nouveau programme hérite de certains
morceaux de l’environnement d’exécution du programme qui a fait
exec
:
wait
Le programme qui suit est un interprète de commandes simplifié: il lit des lignes sur l’entrée standard, les coupe en mots, lance la commande correspondante, et recommence jusqu’à une fin de fichier sur l’entrée standard. On commence par la fonction qui coupe une chaîne de caractères en une liste de mots. Pas de commentaires pour cette horreur.
open Unix;; let split_words s = let rec skip_blanks i = if i < String.length s & s.[i] = ' ' then skip_blanks (i+1) else i in let rec split start i = if i >= String.length s then [String.sub s start (i-start)] else if s.[i] = ' ' then let j = skip_blanks i in String.sub s start (i-start) :: split j j else split start (i+1) in Array.of_list (split 0 0);; |
On passe maintenant à la boucle principale de l’interpréteur.
let exec_command cmd = try execvp cmd.(0) cmd with Unix_error(err, _, _) -> Printf.printf "Cannot execute %s : %s\n%!" cmd.(0) (error_message err); exit 255 let print_status program status = match status with WEXITED 255 -> () | WEXITED status -> Printf.printf "%s exited with code %d\n%!" program status; | WSIGNALED signal -> Printf.printf "%s killed by signal %d\n%!" program signal; | WSTOPPED signal -> Printf.printf "%s stopped (???)\n%!" program;; |
La fonction exec_command
exécute une commande avec récupération des
erreurs. Le code de retour 255 indique que la commande n’a pas pu être
exécutée. (Ce n’est pas une convention standard; on espère que peu de
commandes renvoient le code de retour 255.) La fonction print_status
décode et imprime l’information d’état retournée par un processus, en
ignorant le code de retour 255.
let minishell () = try while true do let cmd = input_line Pervasives.stdin in let words = split_words cmd in match fork() with 0 -> exec_command words | pid_son -> let pid, status = wait() in print_status "Program" status done with End_of_file -> ();; handle_unix_error minishell ();; |
À chaque tour de boucle, on lit une ligne sur l’entrée standard, via
la fonction input_line
de la bibliothèque standard Pervasives
. (Cette
fonction déclenche l’exception End_of_file
quand la fin de fichier
est atteinte, faisant sortir de la boucle.) On coupe la ligne en mots,
puis on fait fork
. Le processus fils fait exec_command
pour lancer la
commande avec récupération des erreurs. Le processus père appelle wait
pour attendre que la commande termine et imprime
l’information d’état renvoyée par wait
.
&
.