TP Se1-2
TP Se1-2
TP Se1-2
Nom : Prénom :
Groupe :
T.P. de Système
d’Exploitation Unix
Maı̂trise E.E.A.
Y. Morère
2 T.P. de Système d’Exploitation
Les buts fixés pour cette série de tps sont les suivants :
V Se familiariser avec un système d’exploitation professionnel et maı̂triser les commandes de bases.
V Écrire des scripts (programmes) afin d’automatiser des tâches.
V Utiliser des programmes évolués du système afin d’automatiser des actions.
V Enfin se familiariser avec cet OS multitâches par l’intermédiaire de la gestion de processus grâce aux fonctions
systèmes.
V Comprendre et maı̂triser la programmation de scripts (bash, Perl pour automatiser des tâches
V Comprendre et maı̂triser la programmation multithreading.
Évaluation et notation :
V Le ou les compte-rendus comporteront les commandes saisies, les résultats obtenus ainsi que les réponses aux
questions.
1 Travaillons un peu 17
1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.1.1 Reconnaı̂tre votre shell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.1.2 Reconnaı̂tre votre système d’exploitation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.2 Commandes Générales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.2.1 Changer votre mot de passe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.2.2 Afficher la ligne (terminal) sur laquelle vous êtes connectés . . . . . . . . . . . . . . . . . . . . 17
1.2.3 Afficher les paramètres de votre terminal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.2.4 Reconfigurer la touche erase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.2.5 Qui êtes-vous ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.2.6 Qui est connecté sur la station ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.2.7 Afficher du texte et les valeurs des variables système . . . . . . . . . . . . . . . . . . . . . . . . 19
1.2.8 Afficher les variables de votre système . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.2.9 Création d’un fichier simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.2.10 Visualisation de votre fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.2.11 Compter le nombre de lignes, de mots, de caractères du fichier . . . . . . . . . . . . . . . . . . 21
1.2.12 Recherche d’une chaı̂ne dans un fichier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.2.13 Trier le contenu du ou des fichiers passés en argument . . . . . . . . . . . . . . . . . . . . . . . 21
3
4 T.P. de Système d’Exploitation
9
Cette page est laissée blanche intentionnellement
Liste des tableaux
11
Cette page est laissée blanche intentionnellement
Avant de commencer un peu de lecture
Procédure de Login
Suivant la salle où vous vous trouvez (ici c’est la salle Linus, en hommage au créateur du noyau Linux), le type de
distribution Linux installée sur la machine, de petites différences dans le mode de connexion peuvent survenir.
Pour se connecter à l’une des machines, il faut bien entendu, avoir un accès à celle-ci c-à-d avoir un compte.
La plupart du temps, pour se connecter, il suffit de donner son nom de login et son mot de passe au logiciel de
connexion (la plupart du temps xdm, gdm ou encore kdm). Vous obtenez une nouvelle fenêtre ressemblant à la figure 1.
Fig. 1 – Connexion à une machine en mode graphique (ici Ulysse avec gdm)
Toute la puissance d’un OS multitâches multiutilisateurs est a vous. Mais saurez-vous en tirer partie ?
13
14 T.P. de Système d’Exploitation
Exemple : réponse du système à la commande man ls sur la machine OSF1 flore V4.0 1091 alpha
ls(1)
NAME
SYNOPSIS
ls [-aAbcCdfFgilLmnopqrRstux1] [file...|directory...]
STANDARDS
Refer to the standards(5) reference page for more information about indus-
try standards and associated tags.
Pour toutes questions au sujet de la commande man, taper :
man man
Travaillons un peu
1.1 Introduction
1.1.1 Reconnaı̂tre votre shell
echo $SHELL
Réponse :
flore:/users/laih2/ymorere >uname -a
OSF1 flore V4.0 1091 alpha
flore:/users/laih2/ymorere >
Observations :
17
18 T.P. de Système d’Exploitation
flore:/users/laih2/ymorere >tty
/dev/ttyp5
flore:/users/laih2/ymorere >
Observations :
flore:/users/laih2/ymorere >stty
#2 disc;speed 9600 baud; -parity hupcl
brkint -inpck -istrip icrnl -ixany onlcr
echo echoe echok
flore:/users/laih2/ymorere >
flore:/users/laih2/ymorere >who am I
ymorere ttyp5 Oct 19 16:18
flore:/users/laih2/ymorere >
who am i
Réponse :
Observations :
flore:/users/laih2/ymorere >who
ericw ttyp0 Oct 15 18:49
ecreuse ttyp2 Oct 19 07:28
ymorere ttyp5 Oct 19 16:18
informa ttyp6 Oct 14 13:50
gomes ttyp7 Oct 19 17:41
ecreuse ttyp8 Oct 19 17:39
flore:/users/laih2/ymorere >
w
Réponse :
yann@yoda:~$ w
3:31pm up 21:48, 4 users, load average: 0.02, 0.12, 0.15
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
yann pts/0 :0 8:30am 8:16 6.55s 5.51s bash
yann pts/2 :0 7:20am 25:06 2:31 2.79s bash
yann pts/1 actarus.lasc.sci 3:29pm 0.00s 1.39s 0.32s w
yann@yoda:~$
users
Réponse :
yann@yoda:~$ users
yann yann yann yann
yann@yoda:~$
Observations :
echo $DISPLAY
Réponse :
Observations :
yann@yoda:~$ env
PWD=/home/yann
REMOTEHOST=actarus.lasc.sciences.univ-metz.fr
HZ=100
HOSTNAME=yoda
PS1=\u@\h:\w\$
USER=yann
MACHTYPE=sparc-unknown-linux-gnu
MAIL=/var/spool/mail/yann
LANG=C
LOGNAME=yann
SHLVL=1
HUSHLOGIN=FALSE
SHELL=/bin/bash
HOSTTYPE=sparc
OSTYPE=linux-gnu
TERM=ansi
HOME=/home/yann
PATH=/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:/usr/games
_=/usr/bin/env
yann@yoda:~$
more fic.txt
Réponse :
less fic.txt
Réponse :
yann@yoda:~$less fic.txt
mon premier fichier
bbbbbbbbbbbbbbbbbbb
ccccccccccccccccccc
yann@yoda:~$
Observations :
yann@yoda:~$ wc fic.txt
4 6 80 fic.txt
yann@yoda:~$
Observations :
Exemple :
X La commande ls *.txt permet de lister tous les fichiers possédant l’extension txt.
X La commande ls t ?t ?.txt permet de lister tous les fichiers titi.txt, toto.txt, tctc.txt ...
X La commande ls t[ao]t[ao].txt permet de lister tous les fichiers possédant l’extension tata.txt, tota.txt,
toto.txt, tato.txt.
23
24 T.P. de Système d’Exploitation
ls -l
Réponse :
ls -al
Réponse :
Observations :
Vérification :
Vérification :
Vérification :
Recréer un fichier nouveau_nom.txt et effacer le par rm -i nouveau_nom.txt
Réponse :
Vérification :
Observations :
Vérification :
Vérification :
Vérification :
Vérification :
Vérification :
Vérification :
Vérification :
Observations :
Les droits d’accès sont les ensembles d’autorisations, qui déterminent qui peut avoir accès aux fichiers en vue de quelle
utilisation.
Vérification :
Observations :
Créer un répertoire et visualiser ses droits d’accès. Modifier ses droits d’accès afin que tout le monde puisse
accéder à celui-ci mais ne puisse pas y faire de modification.
Réponse :
Vérification :
Observations :
Observations :
Observations :
3.1.1 vi
vi est un éditeur vidéo, c’est à dire pleine page et interactif. Il est possible d’insérer du texte à tout endroit dans le
fichier en édition. (Cf. figure 3.1)
L’appel de vi s’effectue de la manière suivante :
X vi
X vi fichier
X vi +n fichier pour se placer directement à la n-ième ligne du fichier
X vi +/motif fichier pour se placer directement à la première occurrence du motif dans le fichier
29
30 T.P. de Système d’Exploitation
3.1.2 emacs
Les principaux avantages d’emacs sont l’extensibilité, la personnalisation et l’auto-documentation. Il possède de nom-
breuses fonctionnalités autres que celles de l’édition.
On peut compiler un programme, lire du courrier électronique, lire les forums, récupérer un fichier par ftp... emacs
signifie editor macros.
L’appel d’emacs s’effectue de la manière suivante :
X emacs
X emacs fichier
X emacs +n fichier pour se placer directement à la n-ième ligne du fichier
X le mode Fill : les lignes sont automatiquement coupées quand elles dépassent la marge droite.
X le mode Abbrev : l’expansion des abréviations est impossible.
X le mode Ovwrt : mode recouvrement.
X le mode Narrow : rend accessible qu’une partie du buffer.
3.1.3 nedit
nedit est un éditeur graphique qui ressemble beaucoup aux éditeurs rencontrés sous les environnements PC, Mac,
Amiga (Linux, Windows 3.xx et 9x, DOS, Mac OS, Amiga OS). nedit peut être complètement géré à la souris et
possède des menus conviviaux pour l’édition, la recherche de texte, le copier coller, la sélection de texte.
Il possède aussi des prédispositions pour la programmation. Il gère l’auto-indentation des lignes de programmes, la
gestion du nombre des parenthèses, l’affichage des mots-clés du langage.
yann@yoda:~$ id
uid=1000(yann) gid=1000(yann) groups=1000(yann),24(cdrom),29(audio)
yann@yoda:~$
Observations :
yann@yoda:~$ ps
PID TTY TIME CMD
3350 pts/1 00:00:02 bash
3382 pts/1 00:00:00 ps
yann@yoda:~$
ps -uvotre login
Réponse :
35
36 T.P. de Système d’Exploitation
yann@yoda:~$ ps -u yann
PID TTY TIME CMD
276 ? 00:02:40 wmaker
304 ? 00:00:00 ssh-agent
306 ? 00:04:34 wmmon
307 ? 00:07:03 wmnd
308 ? 02:08:33 wmsysmon
25024 ? 00:00:04 rxvt
25027 pts/2 00:00:02 bash
25569 pts/2 00:02:30 lyx
25936 ? 00:00:26 gnomeicu
26068 ? 00:00:53 sylpheed
26177 pts/2 00:00:10 tgif
2270 ? 00:00:03 rxvt
2271 pts/0 00:00:05 bash
3274 pts/0 00:00:01 gensig
3281 pts/0 00:00:00 gensig
3350 pts/1 00:00:02 bash
3383 pts/1 00:00:00 ps
yann@yoda:~$
ps -aj
Réponse :
yann@yoda:~$ ps -aj
PID PGID SID TTY TIME CMD
25569 25569 25027 pts/2 00:02:31 lyx
26177 26177 25027 pts/2 00:00:10 tgif
3274 3274 2271 pts/0 00:00:01 gensig
3281 3274 2271 pts/0 00:00:00 gensig
3384 3384 3350 pts/1 00:00:00 ps
yann@yoda:~$
Observations :
nom du processus&
Lancer kedit en tâche de fond
Réponse :
Lancer kedit
Réponse :
Observations :
Observations :
nedit&
Observations :
real 2m49.598s
user 0m25.580s
sys 0m31.800s
yoda:/home/yann#
Observations :
Vérification :
39
40 T.P. de Système d’Exploitation
Vérification :
Vérification :
6.1 Le Shell
6.1.1 Introduction
Sous Unix, il existe plusieurs langages de commande, les plus connus sont les suivants : le Bourne-Shell (sh), le C-Shell
(csh), le Bash (Bourne Again Shell de Linux) (bash) et le Korn-Shell (ksh).
Dans la suite, on passera sous le shell bash par la commande /bin/bash.
Une variable possède un nom et une valeur. Son nom est une chaı̂ne de caractères commençant par une lettre et
composée de lettres, de chiffres et du caractère _. Sa valeur est une chaı̂ne de caractère quelconque.
Exemple :
$ x=gh
$ echo $x
gh
$echo $xijk
$echo ${x}ijk
ghijk
$
Réponse :
yann@yoda:~$ var=etudiant_telecom
yann@yoda:~$ echo $var
etudiant_telecom
yann@yoda:~$
41
42 T.P. de Système d’Exploitation
Vérification :
Exemple :
$ a=‘pwd‘
$ echo $a
/usr/lib
$
6.1.4.1 Créer une variable var2 qui renvoie la date du système lorsque l’on veut afficher son contenu
Réponse :
yann@yoda:~$ var2=‘date‘
yann@yoda:~$ echo $var2
Thu Oct 4 17:19:58 CEST 2001
yann@yoda:~$
Vérification :
Exemple :
6.1.5.1 Utiliser la commande read afin de créer différentes variables contenant votre adresse
Réponse :
Vérification :
PATH est une variable très importante. Sa valeur est une chaı̂ne de caractères indiquant une liste de références de tous
les répertoires susceptibles de contenir des commandes utilisées par l’utilisateur.
IFS a pour valeur l’ensemble des caractères interprétés comme séparateurs de chaı̂ne par le shell.
TERM est également une variable importante. Elle indique le type de terminal utilisé.
La commande set permet d’obtenir la liste des variables de l’environnement et de leurs valeurs. La commande unset
permet de supprimer une variable.
Réponse :
yann@yoda:~$ set
BASH=/bin/bash
BASH_VERSINFO=([0]="2" [1]="03" [2]="0" [3]="1" [4]="release" [5]="sparc-unknown
-linux-gnu")
BASH_VERSION=’2.03.0(1)-release’
COLUMNS=80
DIRSTACK=()
EUID=1000
GROUPS=()
HISTFILE=/home/yann/.bash_history
HISTFILESIZE=500
HISTSIZE=500
HOME=/home/yann
HOSTNAME=yoda
HOSTTYPE=sparc
HUSHLOGIN=FALSE
HZ=100
IFS=’
’
LANG=C
LINES=24
LOGNAME=yann
MACHTYPE=sparc-unknown-linux-gnu
LINES=24
LOGNAME=yann
MACHTYPE=sparc-unknown-linux-gnu
MAIL=/var/spool/mail/yann
MAILCHECK=60
OPTERR=1
OPTIND=1
OSTYPE=linux-gnu
PATH=/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:/usr/games
PIPESTATUS=([0]="0")
PPID=3349
PS1=’\u@\h:\w\$ ’
PS2=’> ’
PS4=’+ ’
PWD=/home/yann
REMOTEHOST=actarus.lasc.sciences.univ-metz.fr
SHELL=/bin/bash
SHELLOPTS=braceexpand:hashall:histexpand:monitor:history:interactive-comments:em
acs
SHLVL=1
TERM=ansi
UID=1000
USER=yann
_=titi
yann@yoda:~$
Observations :
Tout processus shell maintient une liste de chaı̂nes de caractères que l’on peut connaı̂tre par la valeur des variables *,
#,1, 2, 3, ..., 9.
# a pour valeur le nombre de chaı̂nes présentes dans la liste
* a pour valeur la liste des chaı̂nes de caractères
i pour i=1,...,9 a pour valeur la i-ème chaı̂ne de caractères
Remarque : Si le processus shell est crée pour exécuter une commande avec des arguments, alors la variable 0 prend
pour valeur le nom de la commande et * prend pour valeur la liste des arguments.
echo procedure
echo il y a $# paramètres
echo qui sont [$*]
echo le troisième est $3
echo tous les paramètres sont contenus dans la liste [$@]
echo ce script a comme PID [$$]
Observations :
Attention :
1. les différents termes intervenant doivent être séparés par des espaces.
2. si les opérateurs utilisés sont des caractères spéciaux, il doivent être déspécialisés (exemple : > doit être écrit \>).
3. on peut utiliser les parenthèses ( et ) pour regrouper des termes (il faut également les déspécialiser).
Les opérateurs utilisables sont les suivants :
X Le OU logique | : expression1 | expression2 a pour valeur celle de expression1 si expression1 n’est pas nulle (chaı̂ne
vide ou 0) et sinon pour valeur celle de expression2 (ou 0 si expression2 est vide).
X Le ET logique & : expression1 & expression2 a pour valeur celle de expression1 si expression1 et expression2 sont
toutes 2 non nulles et non vides ; vaut 0 dans le cas contraire.
X Les opérateurs de comparaisons : <,>,=,>=,<=, ! (différent de). expression1 opérateur expression2 vaut 1 si le résultat
de la comparaison est vrai, 0 sinon.
X Les opérateurs additifs : + et -
X Les opérateurs multiplicatifs : * multiplication, /division, % reste.
Excercice : Écrire le script qui permet de renvoyer la valeur en ¤ d’une valeur entrée en francs.
Réponse :
Excercice : Écrire le script somme qui permet de faire l’addition des nombres saisis au clavier (0 pour finir).
Réponse :
somme=0
while true
do
echo "nombre ? \c"
read nombre
if [-z nombre]
then
echo "somme= $somme"
exit 0
fi
somme=‘expr $somme + $nombre‘
done
Exemple : la commande ls délivre un code de retour nul si et seulement si son argument est un fichier du répertoire
de travail.
47
48 T.P. de Système d’Exploitation
Excercice : Écrire le script qui permet de vérifier qu’une procédure possède 3 arguments.
Réponse :
commande1 est exécutée ; si son code de retour est vrai (0), commande2 est exécutée et on sort de la structure ; si son
code de retour est faux (différent de 0), commande3 est exécutée et on sort de la structure.
Exemple :
flore:/users/laih2/ymorere >nbarg
il manque des arguments
flore:/users/laih2/ymorere >nbarg toto
le traitement
flore:/users/laih2/ymorere >cat nbarg
if test "$#" -eq 0
then
echo "il manque des arguments"
exit 1
else
Exercice : écrire un script-shell qui affiche convenablement la contenu d’un fichier selon qu’il s’agisse d’un fichier
ordinaire (emploi de cat) ou d’un catalogue (emploi de ls).
Réponse :
if [$# -ne 1]
then echo "syntaxe: $0 nom-de-fichier";
exit 1;
fi
if test -f $1
then cat $1
else if test -d $1
then ls $1
else echo $1 est un fichier special ou n\’existe pas
fi
fi
On examine si chaı̂ne appartient à l’ensemble d’expressions décrit par le modèle motif1 ; Dans ce cas commande1 est
exécutée et on sort de la structure ; sinon on examine si chaı̂ne satisfait motif2 et ainsi de suite. Ainsi la commande
associée au premier modèle satisfait est exécutée.
#!/bin/sh
set ‘date ‘
case $1 in
Mon) j=Lundi;;
Tue) j=Mardi;;
Wed) j=Mercredi;;
Thu) j=Jeudi;;
Fri) j=Vendredi;;
Sat) j=Samedi;;
Sun) j=Dimanche;;
esac
case $2 in
Jan) m=Janvier;;
Feb) m=Fevrier;;
Mar) m=Mars;;
Apr) m=Avril;;
May) m=Mai;;
Jun) m=Juin;;
Jul) m=Juillet;;
Aug) m=Aout;;
Sep) m=Septembre;;
Oct) m=Octobre;;
Nov) m=Novembre;;
Dec) m=Decembre;;
esac
echo $j $3 $m $6 $4
– le caractère * qui représente la liste des noms de fichier du répertoire courant (qui ne commencent pas par .) : for
i in *
– absente ou $*, il s’agit de la liste de paramètres de position $1, $2, etc... : for i in $*
flore:/users/laih2/ymorecat ex2
for fic in ‘ls‘
do
echo $fic present
done
flore:/users/laih2/ymorere >chmod a+rx ex2
flore:/users/laih2/ymorere >ex2
INBOX present
INBOX~ present
Mail present
Mwm present
Mwm.old present
REMY.MPG present
adt present
adt.c present
alpha present
autosave present
cd present
engtofr present
ess present
essai present
ex1 present
ex2 present
existfic present
existuser present
fic.txt present
fic_copie2.txt present
fic_lien2.txt present
ficlien.txt present
images present
latex present
listarg present
lmitool present
lyx present
lyx.tar.gz present
mbox present
nbarg present
netscape.ps present
news.ps present
nsmail present
ps present
pwd present
Exercice : écrire un script-shell liste tous les fichiers contenus dans les répertoires du PATH.
Réponse :
echo ",\c"
done
echo "\n$1 n’est plus connecte
flore:/users/laih2/ymorere >
flore:/users/laih2/ymorere >deconnecte ecreuse
,,,,,,,,
flore:/users/laih2/ymorere >
Exercice : écrire un script-shell invitant l’utilisateur à répondre au clavier par oui ou par non et réitérant l’invitation
jusqu’à ce qu’elle soit satisfaite.
Réponse :
Exercice : écrire un script-shell invitant l’utilisateur à répondre au clavier par oui ou par non et réitérant l’invitation
jusqu’à ce qu’elle soit satisfaite en même temps que l’émission d’un signal sonore.
Réponse :
2. La commande continue permet de passer au pas suivant de l’itération. Sous la forme continue n elle permet
de sortir de n-1 niveaux d’imbrication et de passer au pas suivant du n-ième niveau.
Exercice (break) : écrire un script-shell qui affiche les 5 premiers fichiers spéciaux d’un catalogue donné en argu-
ment.
Réponse :
nombre=0
for arg in $1/*
do
if [-c $arg]
then echo $arg; nombre=‘expr $nombre + 1‘;
if [$nombre -eq 5]
then break;
fi
fi
Exercice (continue) : écrire un fichier de commande options qui construit une liste des options apparaissant sur
la ligne de commande et l’affiche (une option sera définie par une chaı̂ne de caractères commançant par -).
Réponse :
for arg
do
case $arg in
-*) opt="$opt $arg";;
*) continue;;
esac
done
echo $opt
Les mots sont décomposés en une liste de mots. Cet ensemble de mots est imprimé sur la sortie standard (écran en
général), chacun précédé d’un numéro.
Si in mots n’est pas spécifié, $ est pris par défaut. Ensuite le prompt PS3 est affiché et le système attend une entrée
de la part de l’utilisateur sur l’entrée standard (le clavier en général).
Si ce qui est saisi est l’un des numéros correspondant au mots affichés, alors le mot est affecté à la variable nom. Si
la réponse saisie est vide, le menu est affiché de nouveau. Si le caractère EOF (touches ctrl-d) est saisi la commande
select se termine. Toute autres saisies que celles contenues dans le menu provoque l’affectation de nom à NULL.
Les commandes sont exécutées après chaque sélection jusqu’à ce qu’une instruction break soit rencontrée. Il est donc
judicieux de coupler l’instruction select avec l’instruction case.
Le programme suivant :
#!/bin/bash
PS3="Votre choix ? "
select choix in "Choix A" "Choix B";
do
case $REPLY in
1) echo "$choix --> $REPLY";;
2) echo "$choix --> $REPLY";;
*) echo "Vous avez tapé n’importe quoi !";
exit ;;
esac
done
#!/bin/bash
#-------------------------------------------------------
#TP 1 première partie
#Ecrire un script qui dit si le fichier passé
#en paramètre et un fichier, un répertoire ou autre chose
#-------------------------------------------------------
# Si le paramètre est nul on envoie un message
if [ -z $1 ] ; then # voir aussi -n
echo "Vous n’avez pas entré de paramètres"
echo "Le mode d’utilisation du script est $0 NomDuFichier"
exit 0
fi
echo "Il semble que $1 ne soit ni un fichier ni un répetoire ou alors n’existe pas."
#!/bin/bash
#-------------------------------------------------------
#TP 1 première partie
#Ecrire un script qui dit si le fichier passé
#en paramètre et un fichier, un répertoire ou autre chose
#-------------------------------------------------------
# Si le paramètre est nul on envoie un message
if [ -z $1 ] ; then # voir aussi -n
echo "Vous n’avez pas entré de paramètres"
echo "Le mode d’utilisation du script est $0 NomDuFichier"
#exit 0
fi
echo "Il semble que $1 ne soit ni un fichier ni un répetoire ou alors n’existe pas."
#-------------------------------------------------------
# TP 1 deuxième partie
# On utilisera la commande ‘ls‘ qui retourne les fichiers
#-------------------------------------------------------
#Donner la liste des fichiers fichiers
for i in ‘ls‘;
do
echo $i
done
j=0
for i in ‘ls‘ ;
do
if [ -f $i ]; then
j=‘expr $j + 1 ‘
fi
done
echo "Il y a $j fichiers standards dans le répertoire"
j=0
for i in ‘ls‘ ;
do
if [ -d $i ]; then
j=‘expr $j + 1 ‘
fi
done
echo "Il y a $j répertoires dans le répertoire"
#!/bin/bash
clear
echo "------------------------------------------------------------------------"
echo "<1> Comptabilité"
echo "<2> Gestion commerciale"
echo "<3> Paie"
echo ""
echo ""
echo ""
echo "Taper une option du menu 1, 2 ou 3, <q> pour quitter"
read saisie
case $saisie
in
1)
echo "Vous avez choisi l’option 1 Comptabilité" ;;
2)
echo "Vous avez choisi l’option 2 Gestion Commerciale" ;;
3)
echo "Vous avez choisi l’option 3 Paie" ;;
q|Q)
echo "Vous avez choisi de quitter l’application" ;;
En utilisant la structure for, écrire un programme qui donne les valeurs de y d’une fonction pour les valeurs de x allant
de -10 à 10 avec un incrément de 1.
– Réalisez le traitement pour les fonctions y=x, y = x puiss2
– Réécrivez les programmes avec la strucure répéter ... jusqu’à
– Adapter le script afin que les bornes -x, +x soient passées en paramètres au script.
– Modifiez le script de façon à ce que l’on puisse passer en paramètres l’incrément.
#!/bin/bash
#Première partie (avec for)
# Traitement de la fonction x=y
# pour x allant de -10 à 10 avec un incrément de 1
# On stockera dans un fichier inc toutes les valeurs
# comprises entre -10 et 10
for i in ‘cat inc‘; do
x=$i
y=$x
echo "Pour x valant $x, y vaut $y"
done
# fonction y = x puiss 2
echo -------- utilisation de until...
x=-10
abs=10
y=‘expr $x \* $x‘ # On calcule la première occurence
until [ $x -gt 10 ]; do # on regarde si x est égal = 10
echo "Pour x valant $x, y vaut $y"
x=‘expr $x + 1‘ #on incrémente
y=‘expr $x \* $x‘ #on recalcule y
done
x=$1
y=‘expr $x \* $x‘ # On calcule la première occurence
until [ $x -gt $2 ]; do # on regarde si x est égal = 10
echo "Pour x valant $x, y vaut $y"
x=‘expr $x + $3‘ #on incrémente selon le pas souhaité
y=‘expr $x \* $x‘ #on recalcule y
done
Vous allez utiliser un fichier dans lequel vous stockerez les informations suivantes :
– premier 3
– deuxième 10
– troisième 25
– quatrième 2
– cinquième 12
Construisez un script qui permet de n’afficher que les enregistrements dont la valeur est supérieure à 10.
#!/bin/bash
#----------------------------------------------------------
# Alix MASCRET éléments de corrigé
# Programmation shell
# Utilisation de la structure si
# utiliser -a pour "et", -o pour "ou", ! pour non
# Donner les éléments sur la commande set -- pour éclater
# les mots d’une ligne
# Donner les éléments pour réaliser la lecture d’un fichier
#---------------------------------------------------------
clear
echo "------------------------------------------------------------------------"
echo "<1> Comptabilité"
echo "<2> Gestion commerciale"
echo "<3> Paie"
echo ""
echo ""
echo ""
echo "Taper un chiffre une option du menu, <q> pour quitter"
read saisie
if [ $saisie == 1 ]; then
echo "Vous avez choisi l’option 1 Comptabilité"
elif [ $saisie == 2 ] ; then
echo "Vous avez choisi l’option 2 Gestion Commerciale"
elif [ $saisie == 3 ] ; then
echo "Vous avez choisi l’option 3 Paie"
elif [ $saisie == q -o $saisie == Q ] ; then
echo "Vous avez choisi de quitter l’application"
else
echo "Vous avez tapé n’importe quoi !!!"
echo $’\a’ # on beep
fi
*******************************************************
Saisisez une commande, commande <Q> pour quitter.
*******************************************************
#!/bin/bash
clear
saisie=xxx
set -- $saisie
until [ $1 == q -o $1 == Q ];
do
echo "Entrez une commande"
read -r saisie #récupérer toute la ligne avec les paramètres
# $saisie #Première solution : Exécution de la commande
eval $saisie #Deuxième solution (préférable)
# exec $saisie #Troisième solution mais quitte le shell
# echo $saisie #Pour debogguer
set -- $saisie #on split la ligne de commande
#pour éviter les msg d’erreurs sur le test
done
reponse=xxx
Vous allez à l’aide de la fonction select réaliser un menu à 4 options pour un utilisateur. Le script doit boucler tant
que l’utilisateur n’a pas choisi de quitter.
#!/bin/bash
select choix in \
"Affiche la listes des utilisateurs connectés" \
"Affiche la liste des processus" \
"Affiche les informations vous concernant" \
"QUITTER"
do
case $REPLY in
1) who ;;
2) ps ax ;;
3) echo -e "Bonjour $USER , voici les informations \n ‘id‘";;
4) echo "Vous allez quitter l’application"
exit 0 ;;
*) echo "Vous avez tapé n’importe quoi !";;
esac
done
-A- En utilisant les structures que vous connaissez, écrire un script qui affiche la table de multiplication d’un nombre
donné en paramètre. Exemple mul 4, donne la table de multiplication de 4. Vous afficherez les résultats pour un
multiplicateur allant de 1 à 10. L’affichage de la table de multiplication sera réalisé par une fonction affTABLE().
#!/bin/bash
#Première partie : traitement d’une table de multiplication
affTABLE0 () {
i=1
while [ $i -le 10 ] ;
do
echo -e "$1 * $i \t = ‘expr $1 \* $i‘"
i=‘expr $i + 1‘
done
}
#Ici le premier programme principal
affTABLE0 $1 # On passe le paramètre à la fonction
-B- Modifiez le script afin que les bornes du multiplicateur soient passés en paramètres : exemple mul 3 25 35. On
veut la table de multiplication de 3*25 jusqu’à 3*35
#!/bin/bash
#Deuxième partie on modifie la fonction
affTABLE1 () {
i=$2
while [ $i -le $3 ] ;
do
echo -e "$1 * $i \t = ‘expr $1 \* \$i‘"
i=‘expr $i + 1‘
done
}
-C- Modifier le programme de façon à écrire une calculatrice. L’utilisateur saisi un nombre (par exemple 3). Ensuite il
saisira des couples opérateur nombre (exemple + 3). Le programme réalisera les opérations jusqu’à ce que l’utilisateur
tape l’opérateur ”=” et affichera le résultat final.
#!/bin/bash
#Troisième partie la calculatrice
#Troisième partie on modifie la fontion
calcule () {
echo "---------------> $1 --- $2 --- $3"
case $2 in
"\+") nombre1=‘expr $nombre1 + $nombre2‘;;
"\-") nombre1=‘expr $nombre1 - $nombre2‘;;
"\/") nombre1=‘expr $nombre1 / $nombre2‘;; #Attention à la division par 0
"\*") nombre1=‘expr $nombre1 \* $nombre2‘;;
*) echo "Erreur de saisie"
exit 0;;
esac
}
Vous utiliserez la fonction getopts pour vérifier la saisie de l’utilisateur. Réaliser un script d’archivage qui utilisera
les options :
– -a (archive)
– -d (désarchive)
– -c (compresse)
– -x (décompresse)
Le fichier ou le répertoire à archiver sera passé en paramètre : exemple archive -a -c travail. Attention archive
-a -d est invalide.
#!/bin/bash
#Utilisation de getopts et optind
#On ne traite pas les incompatibilités ou erreurs
# de saisie, ce n’est pas l’opjet du TP
if [ -z $1 ]; then
echo "tar/compress $0 -t -c fichier "
echo "untar/décompresse $0 -u -d fichier "
exit 0
fi
for i in $@ ;
do
getopts tc option # On récupère la première option t ou c
if [ $option == ’t’ ] ; # Ici c’est pour archiverr
then
for i in $@ ;
do
getopts c option # On regarde s’il faut décompresser
done
if [ $option == ’c’ ] ; then
echo "ici compléter compression" #il faut égaleme
else
echo "ici compléter"
fi
elif [ $option == ’u’ ] ; then
for i in $@ ; do
getopts d option # On regarde s’il faut décompresser
done
if [ $option == ’d’ ] ; then
echo "ici compléter"
else
echo "ici compléter"
fi
fi
done
Remarque Pour archiver vous exploiterez la commande tar (uniquement sur les répertoires car il est inutile d’archiver
un fichier). Pour compresser gzip.
Le code ci-dessous permet d’intercepter le signal 2 (arrêt depuis le clavier). Tapez le et analysez son fonctionnement.
Ecrire un script ”pere” qui aura en charge le déclenchement de deux scripts enfants.
#!/bin/bash
# Ici commence le programme principal
# On crée une fonction qui récupère le PID
# du script enfant
_pid() {
echo -n "On kill le processus Nř ‘cat $1‘ "
echo "avec la commande \"kill -9 ‘cat $1‘\"" # On tue le processus
kill -9 ‘cat $1‘
}
while true; do
echo "On lance ping"
./ping &
_wait 100
_pid enf1 #On passe en paramètre le nom du fichier
#dans lequel est stocké le PID
#On aurait pu utiliser la table des processus
#On lance pong et on recommance
echo "On lance pong"
./pong &
_wait 100
_pid enf2
done
#!/bin/bash
#Appel et gestion inter processus
#On va utiliser le nř de processus retourné par $$
#Ce nř de processus sera stocké dans un fichier
#enf1 pour le script enfant 1
#enf2 pour le script enfant 2
### Code du processus ping
# pour le lancer en t^
ache de fond ./ping &
#!/bin/bash
#Appel et gestion inter processus
#On va utiliser le nř de processus retourné par $$
#Ce nř de processus sera stocké dans un fichier
#enf1 pour le script enfant 1
#enf2 pour le script enfant 2
### Code du processus pong
Maı̂trise E.E.A. Université de Metz 20 aôut 2004
#!/bin/bash
#script enfant 2 pour le TP sur les inter processus
Chapitre 8
Exemple : ainsi pour rechercher dans un fichier, toutes les lignes commençant par la lettre B, on aura :
Exemple : ainsi pour rechercher dans un fichier, toutes les lignes se terminant par le mot ”Paris”, on aura :
Exercice : Écrire la commande qui permet de rechercher les lignes blanches d’un fichier pour les compter.
Réponse :
69
70 T.P. de Système d’Exploitation
Exercice : Écrire la commande qui permet rechercher dans un fichier, les lignes qui commencent par ”Henri” et qui
se termine par ”Paris”.
Réponse :
8.1.1.2 Le point .
Le point ”.” permet de représenter un caractère quelconque dans une expression.
Exemple : ainsi pour rechercher dans un fichier ”F1”, toutes les lignes contenant une chaı̂ne de 8 caractères se
terminant par ”al” , on aura :
Exercice : Écrire la commande qui permet rechercher les programmes de /usr/bin dont le nom a une longueur de
4 caractères et se terminant par un ”r”. On commencera l’expression par un espace afin de limiter à 4 caractères.
Réponse :
Exemple : ainsi pour rechercher dans un fichier ”fichier”, toutes les lignes contenant au moins un des caractères
de l’ensemble A, E, F ou K, on aura :
Il est possible de combiner les diverses possibilités des expressions régulières entre elles. Ainsi pour définir toutes les
lignes commençant par un des caractères A, E, F ou K, on utilisera l’expression régulière suivante ”^[AEFK]”, respecti-
vement ”[AEFK]$” pour trouver toutes les lignes qui finissent par ces mêmes lettres.
Les ensembles de caractères peuvent être spécifiés sous la forme d’intervalle. Par exemple [a-z] signifie toutes les
lettres de a à z.
De même les classes de caractères peuvent être combinées entre elles. Ainsi il est possible de rechercher toutes les suites
de caractères dont le premier est pris dans l’ensemble A à E, le second égal à o, i ou u, suivi de trois caractères quel-
conques, suivi d’un b ou d’un c ou d’une lettre comprise entre i et m par l’expression suivante : [A-E][oiu]...[bci-m].
Exercice : Écrire la commande qui permet de recherche les lignes ne débutant pas par S, H ou J dans un fichier
fichier.
Réponse :
Exercice : Écrire la commande qui permet de rechercher les lignes commençant par P, suivi de a ou i, puis deux
lettres quelconques, et ayant pour cinquième lettre un r ou une a dans un fichier fichier.
Réponse :
Les accolades {} offrent la possibilité de spécifier des répétitions. Un nombre entre accolades suivant une expression
régulière indique le nombre de répétitions que l’on souhaite appliquer à cette expression. Les accolades doivent être
introduite par des antislash \. Par exemple pour représenter toutes les suites de caractères commençant par une lettre
majuscule, suivie de 5 lettres minuscules et suivie de 2 chiifres entre 0 et 9 nous avons : [A-Z][a-z]\{5\}[0-9]\{2\}.
Par exemple, pour rechercher dans un fichier F1 des suites de 8 lettres commençant par T ou t, on écrira : grep
’[Tt]\{7\}’ F1.
En règle générale, on précise le nombre minimum et maximum de répétitions \{p,q\}. Il existe de plus 3 caractères
particuliers pour exprimer des répétitions :
– * est équivalent à \{0,\} : le minimum est zéro et le maximum est infini.
– + est équivalent à \{1,\} : le minimum est un et le maximum est infini.
– ? est équivalent à \{0,1\} : l’expression est répétée une fois au plus.
Exercice : Écrire la commande qui permet de rechercher dans un fichier fichier des lignes commençant par B ou
C et se terminant par les chiffres 2, 4 ou 6.
Réponse :
Exercice : Écrire la commande qui permet de rechercher dans un fichier fichier des lignes dont le second caractère
est commençant par r ou e et l’avant dernier caractère est le chiffre 4.
Réponse :
Exercice : Écrire la commande qui permet de rechercher dans un fichier fichier des mots d’au moins 6 lettres
commençant par un P.
Réponse :
Exercice : Écrire la commande qui permet de rechercher dans un fichier fichier toutes les lignes pour lesquelles le
mot comporte 5 lettres..
Réponse :
Exemple :
$cat chrs
a b e
az^
^ e ^
u ^
ı
$cat -v chrs
a ^A b ^B e ^E
M-bzM-j M-{ M-n
Exemple :
$ head -2 prenoms
David
Francis
$ head -2 prenoms seneque
==> prenoms <==
David
Francis
==> seneque <==
Paucis natus est, qui populum aetatis suae cogitat.
Seneque.
$
La syntaxe de tail est tail [-+nombre] [-liste-de-fichiers]
tail écrit sur la sortie standard les lignes de chaque fichiers donnés en argument et situées à partir de nombre lignes
comptées à partir du début (respectivement de la fin) si l’on choisi l’option + (respectivement l’option -). Par défaut
nombre vaut 10.
Exemple :
$cat villes
Paris
Londres
Rome
Jerusalem
$ tail -2 villes
Rome
Jerusalem
$ tail +3 villes
Rome
Jerusalem
$
Exemple :
Notons que l’option -d utilisée dans tr -d chaı̂ne a pour effet d’éliminer tous les caractères de chaı̂ne.
Exemple :
$ cat fitest
Orange 18 124
Banane 23 98
Citron 12 112
Pamplemouse 17 65
Mangue 24 33
Goyave 28 12
Pomme 12 148
Poire 15 122
....
$ sort fitest
Abricot 23 45
Ananas 18 24
Banane 23 98
Brugnon 22 32
Carotte 33 55
Cerise 23 46
Citron 12 112
Fraise 21 43
Goyave 28 12
Mandarine 15 156
Mangue 24 33
....
Si l’on désire trier le fichier sur tous les caractères de la ligne à partir du second champ on aura :
$ sort +1 fitest
Citron 12 112
Pomme 12 148
Mandarine 15 156
Poire 15 122
Raisin 17 87
Orange 18 124
Ananas 18 24
Peches 18 33
Fraise 21 43
Brugnon 22 32
Banane 23 98
Carotte 33 55
Abricot 23 45
Cerise 23 46
....
Il est possible de faire le tri sur le troisième champ. Et on remarque que les données ne sont pas triées suivant un
critère numérique, mais suivant un critère de type de caractère.
$ sort +2 fitest
Citron 12 112
Goyave 28 12
Poire 15 122
Orange 18 124
Pomme 12 148
Mandarine 15 156
Mandarine 15 156
Raisin 17 87
Ananas 18 24
Brugnon 22 32
....
Afin de retrouver un ordre de tri numérique, il faut préciser l’option -n dans la commande de tri.
$ sort -n +2 fitest
Goyave 28 12
Ananas 18 24
Brugnon 22 32
Mangue 24 33
Fraise 21 43
Abricot 23 45
Carotte 33 55
....
Exercice : Écrire la commande qui permet de trier le fichier fichier où les champs sont séparés pas des ” :”, sur
les champs 3 et 4 en ordre inverse.
Réponse :
$cat toufo
pierre qui roule n’a masse pas mousse
dabo dabon dabonnet
2 peste soit des avatars et des avares au cieux
On remarque que seule la première occurence de dabo a été corrigée. Il faut demander explicitement une substitution
”globale” en ajoutant le drapeau g.
Supprimons maintenant le 2 suivi de l’espace. La commande suivante supprime tout chiffre en début de ligne suivi de
zéro ou plusieurs espaces :
$sed ’s/^[0-9]*[ ]*//’ toufo.2 > toutfo.3
$cat toufo.3
pierre qui roule n’amasse pas mousse
dubo dubon dubonnet
peste soit des avatars et des avares au cieux
Exercice : Écrire la commande qui permet de remplacer le point-virgule de la dernière ligne par un point et d’écrire
le résultat dans le fichier toufo.5.
Réponse :
Exercice : Écrire la commande qui permet de supprimer toutes les lignes vides du fichier toufo.5.
Réponse :
Exercice : Écrire la commande qui permet d’insérer 2 espaces en début de chaque ligne. On utilisera le fait que,
dans un motif, le point ”.” est un métacaractère désignant n’importe quel caractère autre que newline.
Réponse :
Lorsque de nombreuses commandes sed sont nécessaires, il devient intéressant de les réunir dans un fichier et de les
faire exécuter grâce à la commande :
sed -f fichier de commande fichier
Exercice : Réunir dans un fichier script.sed les commandes déjà utilisée et ajouter celle qui est nécessaire pour
compléter la correction du fichier toutfo.
Réponse :
cat script.sed
s/n’a masse/n’amasse/
s/dabo/dubo/g
s/^[0-9]*[ ]*//
s/[a-zA-Z]$/&;/
$s/[^a-zA-Z0-9]$/./
s/^$/d
s/^./ &/
s/avares aux cieux/avaricieux/
$
$sed -f script.sed toutfo > toubon
$cat toubon
pierre qui roule n’amasse pas mousse;
dubo dubon dubonnet;
peste soit des avatars et des avaricieux;
Sacha chassa son chat, Sancho secha ses choux.
Ajouter : [adresse]a\ suivi d’un texte sur la ligne suivante. Ajoute le texte après l’adresse indiquée.
Exemple :
$cat seneque
Paucis natus est, qui populum aetatis suae cogitat.
Seneque.
$cat script1
a\
Il est ne pour peu d’hommes, celui qui n’a en tete\
que les gens de son siècle.
$sed -f script1 seneque
$cat seneque
Paucis natus est, qui populum aetatis suae cogitat.
Seneque.
Il est ne pour peu d’hommes, celui qui n’a en tete\
que les gens de son siècle.
$
Insérer : [adresse]i\ suivi d’un texte sur la ligne suivante. Insére le texte après l’adresse indiquée.
Exemple :
$cat prenoms
David
Francis
Philippe
$cat script2
/Phi/i\
Marcel\
Paul
$sed -f script2 prenoms
$cat prenoms
David
Francis
Marcel
Paul
Philippe
$
Changer : [adresse]c\ suivi d’un texte sur la ligne suivante. Remplace la ligne qui se trouve à l’adresse par le texte.
Exemple :
$cat script3
/Francis/c\
Francois\
Maurice
$sed -f script3 prenoms
$cat prenoms
David
Francois
Marcel
Paul
Philippe
$
$cat Saintex
Car j’ai vu trop souvent le pitie s’egarer.
saint-exupery.
$sed 2y/aeinprstuxy/AEINPRSTUXY Saintex
Car j’ai vu trop souvent le pitie s’egarer.
SAINT-EXUPERY.
$
Quitter : [adresse]q
Stoppe le traitement d’un fichier dès que l’adresse a été atteinte.
Exercice : Le petit script que vous allez devoir écrire sera fort utile à ceux qui utilisent conjointement Linux et
Windows, et qui doivent échanger des fichiers texte entre ces systèmes. En effet Linux indique les fins de ligne avec un
seul caractère (\n), alors que windows en utilise 2 (\r\n). Les caractères (\r) sont souvent affichés sous Linux comme
des (^M). Il vous faut donc écrire le petit script qui va permettre d’enlever les ^M à chaque fin de ligne dans une série
de fichiers.
Réponse :
cat win2lintxt
for i in *.txt
do sed -e ’s/^M$//’ < $i > $i.tmp
mv $i.tmp $i
done
-perm nombre octal sélectionne les fichiers dont les droits d’accès sont ceux indiqués par le nombre octal.
-type caractere sélectionne les fichiers dont le type est celui indiqué par le caractère. C’est à dire :
– c pour un fichier spécial en mode caractère,
– b pour un fichier spécial en mode bloc,
– d pour un répertoire,
– f pour un fichier normal,
– l pour un lien symbolique.
-links nombre décimal sélectionne les fichiers dont le nombre de liens est donné par le nombre décimal. Si le
nombre est précédé d’un + (d’un -) cela signifie supérieur (inférieur) à ce nombre.
81
82 T.P. de Système d’Exploitation
-user n[ou]m utilisateur sélectionne les fichiers dont l’utilisateur propriétaire est le nom utilisateur ou dont la
numéro d’utilisateur (UID) est num utilisateur.
-size nombre décimal[c] sélectionne les fichiers dont la taille est de nombre decimal blocs. Si on post-fixe le
nombre decimal par le caractère c, alors la taille sera donnée en nombre de caractère.
-inum nombre décimal sélectionne les fichiers dont le numéro d’I-node est nombre decimal.
-atime nombre décimal sélectionne les fichiers qui ont été accèdés dans les nombre decimal derniers jours.
-mtime nombre décimal sélectionne les fichiers qui ont été modifiés dans les nombre decimal derniers jours.
-newer fichier sélectionne les fichiers qui sont plus récents que celui passé en argument.
-exec commande\ ; exécute commande sur tous les fichiers sélectionnés. Dans la commande Shell, {} sera remplacé
par les noms des fichiers sélectionnés.
-ok commande\ ; même chose que -exec, mais demande confirmation avant chaque exécution.
lp /usr/\textsl{utilisateur}/jour.sav ? y
lp /usr/\textsl{utilisateur}/nuit.sav ? y
lp /usr/\textsl{utilisateur}/essai.sav ? n
lp /usr/\textsl{utilisateur}/toto.sav ? n
imprime (ou non) chaque fichier .sav dans l’arborescence /usr/utilisateur après avoir demandé confirmation.
Exercice : Dans les deux commandes suivantes, la première fonctionne, mais pas la suivante. Pourquoi ?
Réponse :
Exercice : Dans l’arborescence complète, afficher toutes les informations de tous les fichiers dont vous êtes proprié-
taire.
Réponse :
at 12:30
$at 12:30
at>ping -c 5 192.168.0.1
at>^D
$
Vous entrez donc la commande que vous désirez effectuer et vous obtenez la réponse suivante :
La commande va envoyer un ping sur la machine 192.168.0.1 à 12h30. Il est bien sur possible de faire exécuter un
script. Ensuite at envoie par mail le résultat de cette commande à l’auteur.
Si vous désirez savoir ce qu’il va se passer vous pouvez tester la commande at -c 1. Cette option permet de montrer
la commande numéro 1.
La commande atq permet de lister toutes les commandes at, et la commande atrm permet de supprimer un des job
de at.
Exemples :
– 0 1 1 * * commande signifie que la commande sera exécutée le premier jour du mois à 1 heure.
– 0 1 * * mon commande signifie que la commande sera exécutée un fois par semaine le lundi à 1 heure.
– 0 1 1,15 * * commande signifie que la commande sera exécutée tous les 1 et 15 du mois à 1 heure.
– 0 1 1-15 * * commande signifie que la commande sera exécutée tous les 15 premiers jours du mois à 1 heure.
– 0 1 */5 * * commande signifie que la commande sera exécutée tous les 5 jours à 1 heure.
– */3 * * * * commande signifie que la commande sera exécutée toutes les 3 minutes.
La commande suivante efface tous les jours, les fichiers présents dans le répertoire /var/log vieux de plus de 7 jours.
0 1 * * * find /var/log -atime 7 -exec rm -f {} \ ;
9.2.3 Contrôle
Dans le cas de at ou crontab, il est possible de définir qui a le droit d’utiliser ces commandes. Pour cela il existe
les fichiers /etc/cron.allow et /etc/cron.deny et /etc/at.allow et /etc/at.deny. Par exemple, pour interdire
l’utilisation de la commande cron à certains utilisateurs, il suffit d’entrer leurs noms dans le fichier cron.deny.
Exercice : Créer un crontab qui sauvegarde toutes les nuits dans le répertoire /var/sauv sous la forme d’un fichier
tar compressé, votre répertoire home.
Réponse :
#include <stdio.h>
#include "helloworld.h"
int main(int argc, char *argv[])
{
hello();
exit(0);
}
Fichier helloworld.h
void hello();
Fichier helloworld.c
#include <stdio.h>
void hello()
{
printf("bonjour le monde\n");
}
Un Makefile contient ainsi un ensemble de règles, dont chacune est constituée d’une ”cible”, de ”dépendances” et de
commandes. Il est important de de réaliser l’indentation des lignes ci-dessus avec des tabulations et non des espaces.
Ceci occasionnerait des erreurs lors du make
La première règle définit la cible helloworld ce qui signifie que son rôle réside dans la production d’un fichier
helloworld. Cette règle possède 2 dépendances main.o et helloworld.o. Cela indique que pour élaborer le pro-
gramme helloworld, il faut préalablement disposer de ces 2 fichiers. Il vient ensuite la commande shell qui permet
de générer helloworld à partir des dépendances. Cette commande consiste à appeler la compilation pour obtenir
l’exécutable helloworld à partir des deux fichiers objets.
La règle suivante est encore plus simple, elle donne le moyen de créer le fichier objet main.o. La syntaxe d’un Makefile
se révèle donc assez simple. On peut alors l’utiliser en vue de la recompilation de notre programme simplement e,
lançant la commande make helloworld ou encore plus simplement make, car l’outil prend par défaut la première cible
trouvée.
Que va t’il se passer ? Make cherchera à générer helloworld : pour cela il vérifiera d’abord si les fichiers requis sont
disponibles. S’il manque par exemple main.o, il appliquera alors la règle pour produire ce fichier et ainsi de suite
si main.o nécessitait d’autres dépendances. Une fois toutes les dépendances satisfaites, la commande pour produire
helloworld sera exécutée afin d’obtenir notre fichier exécutable.
Exercice : Écrire les différents fichiers nécessaires à la mise en œvre du petit projet décrit ci-dessus (main.c,
helloworld.c, helloworld.h) sans oublier le Makefile. Lancer la compilation (debugger si nécessaire). Ensuite mo-
difier le fichier helloworld.c en remplaçant la phrase bonjour le monde\n par ce que vous voulez. Que remarquez-
vous ?
Réponse :
Ce permet de plus d’avoir un terme générique pour lancer la compilation complète de n’importe quel projet.
La seconde chose importante est l’installation du programme. Cette nouvelle règle est dépendante de la compilation
complète (all). Grâce à la commande make install, il sera possible de copier l’exécutable dans le sous-répertoire
bin de DESTDIR ainsi que ses diverses ressources dans DESTDIR/share, la documentation dans DESTDIR/doc et la page
de manuel dans DESTDIR/man.
install: all
cp helloworld $(DESTDIR)/bin
mkdir -p $(DESTDIR)/doc/helloworld
cp manual.ps $(DESTDIR)/doc/helloworld
cp README $(DESTDIR)/doc/helloworld
cp helloworld.1 $(DESTDIR)/man/man1
Une fois l’installation faite, il ne reste plus qu’a faire le ménage des fichiers temporaires créés lors de la compilation et
qui ne servent plus. La règle clean détruira ces fichiers. Il suffira de taper make clean pour obtenir une arborescence
propre.
clean:
rm -f *.o *~
Bien sur cette règle de dépend d’aucune autre.
Exercice : Compléter le fichier makefile précédent afin de pouvoir compiler, installer (dans le répertoire bin que
vous aurez créé dans votre répertoire) et nettoyer le projet helloworld.
Réponse :
CC= gcc
CFLAGS = -O2
DESTDIR = $(HOME)
OBJS = main.o helloworld.o
helloworld : $(OBJS)
$(CC) -o $(@) $(OBJ)
all: helloworld
install: all
cp helloworld $(DESTDIR)/bin
mkdir -p $(DESTDIR)/doc/helloworld
cp README $(DESTDIR)/doc/helloworld
clean:
rm -f *.o *~
10.1 Introduction
Larry Wall a inventé Perl pour introduire automatiquement des rapports et des états statistiques dans un système
interne de forums de discussions. L’intitulé complet de ce langage reflète d’ailleurs cette première orientation : Practical
Extraction and Report Langage.Perl est l’abréviation de Practical Extraction and Report Langage (langage pratique
d’extraction et de génération de rapport), même s’il a été également baptisé Pathologically Eclectic Rubbish Lister
(énumérateur de bétises pathologiquement éclectique).Très vite la structure de Perl, qui se voulait au départ une
simple extention de awk, évolua sous l’influence de nombreux utilisateurs, pour incorporer des héritages d’autres
langages comme C, Sed ou le Shell.
10.2 Généralités
Perl est un langage polyvalent, adaptable à de nombreuses situations, et d’une puissance sans cesse accrue. C’est un
langage interprété et il ne convient donc pas pour les applications bas niveau. Par contre pour l’essentiel des tâches
logiciels, il convient parfaitement.Le slogan de la communeauté Perl est ”TIMTOWTDI – There Is More Than One
Way To Do It”. C’est la richesse du langage qui permet ceci.
10.3 Utilisation
L’interpréteur Perl se trouve en général dans le répertoire /user/bin est est invoqué sous le nom perl. L’invocation
de l’interpréteur se fait avec la syntaxe suivante :
ou :
#!/usr/bin/perl
En pratique, on utilisera toujours l’option -w (warning) qui affiche des avertissements justifiés lorsque l’interpréteur
rencontre des expressions douteuses dans le code.
#!/usr/bin/perl -w
$perl -w -e ’commandes’
89
90 T.P. de Système d’Exploitation
valeur peut être un nombre (entier ou réel) ou une chaı̂ne de caractères.Dans un contexte de liste, l’évaluation four-
nit une succession de variables scalaires. Une liste est représentée comme une suite de valeurs scalaires séparées par
des virgules, qui est en général encadrée par des parenthèses.Une expression est évaluée dans le contexte correspon-
dant à ce que l’on attend d’elle. S’il s’agit d’en affecter le résultat à une variable scalaire, elle sera evaluée dans
un contexte scalaire. S’il faut l’ajouter à la fin d’une liste, le résultat est demandé dans un contexte de liste.Par
exemple, les constantes 1, 2, 3e-4, "zéro" sont des scalaires et (1,2,3,5,7,11,13,17) est une liste au même titre
que ("alpha","bravo","charlie","delta") ou ("un",2,"deux",4,"trois").Il y a un autre type de données sca-
laires, les références. Il s’agit de pointeurs vers des zones mémoire qui permettent d’accéder indirectement au contenu
d’un objet, variable ou constante.Le langage Perl permet de manipuler des variables, structurées sous trois formes
distinctes : les variables scalaires, les tableaux classiques et les tables de hachage.
yann@yoda:~/script_linux$ perl -e
’$i=42; $j="i"; print $$j; print "\n";’42yann@yoda:~/script_linux$
Ce concept est appelé référence symbolique, par analogie avec les liens symboliques que l’on trouve sur les systèmes
Unix. Ici une référence symbolique est une variable qui renferme le nom d’une autre variable.Lorsque le référence
symbolique est complexe (concaténation de deux noms de variables), il est possible d’employer des accolades pour
la délimiter explicitement. L’opérateur ’.’ permet de concaténer deux chaı̂nes de caractères ; nous l’utilisons pour
regrouper les deux moitiés du nom de la variable $deux et accéder à son contenu.
yann@yoda:~/script_linux$ perl -e ’$deux=2; $i="de";
$j="ux"; print ${$i.$j}; print "\n";’2yann@yoda:~/script_linux$
On a donc reconstruit dynamiquement le nom de la variable pour en obtenir son contenu.
10.4.1.1 Nombres
En Perl, tous les nombres en interne utilisent le même format. Il est possible de spécifier des entiers ou des nombres
en virgule flottante (réels, décimaux). Mais en interne Perl ne calcule que des valeurs en virgules flottantes. Il n’y a
donc pas de valeurs entières interne à Perl.
Entiers
0
2004
-40
61298040283768
61_298_040_283_768
Entiers non décimaux Perl vous permet d’indiquer des nombres dans une base différente de la base 10. Les octaux
(base 8) commencent par 0, les héxadécimaux (base 16) commencent par 0x, les binaires (base 2) commencent par 0b.
perl -e ’$toto=0b10 + 0b01;print
$toto\n";’
0377
0xff
0b11111111
0x50_65_72_7C
Opérateurs sur les nombres Perl propose les opérateurs ordinaires d’addition, de soustraction, de multiplication
et de division, etc. Par exemple :
2 + 3
5.1 -2.4
3 * 12
14 / 2
10.2 /0.3
10 / 3
Perl supporte aussi l’opérateur modulo (%). La valeur de l’expression 10 % 3 représente le reste de la division entière 10
par 3 soit 1. Il existe aussi un opérateur de puissance (élévation à la puissance) de type FORTRAN. Il est représenté
par une double astérisque, par exemple 2 ** 3 représente 23 soit 8.
10.4.1.2 Chaı̂nes
Les chaı̂nes sont des suites de caractères (comme hello). Elles peuvent contenir n’importe quelle combinaison de
n’importe quels caractères. La chaı̂ne la plus courte ne contient qu’un caractère. la chaı̂ne la plus longue replit toute
votre mémoire disponible. Les chaı̂nes typiques sont des séquences de caractères imprimables de lettres de chiffres et
de signes de ponctuation.
Chaı̂nes entre apostrophes simples Une chaı̂ne entre apostrophes simples est une suite de caractères encadrée
par des apostrophes simples ’toto’. Les apostrophes ne font pas partie de la chaı̂ne, elles ne sont là que pour indiquer
à Perl le début et la fin de la chaı̂ne.Tout autre caractère autre que l’apostrophe ou une barre oblique est valide. Pour
mettre une barre oblique inverse, il faut la faire précéder d’une autre barre oblique. Pour mettre une apostrophe, il
faut aussi la faire précéder d’une barre oblique.
Chaı̂nes entre guillemets Une chaı̂nes entre guillemets se comporte comme une chaı̂ne d’autres langages. Il s’agit
d’une série de caractères encadrée par des guillemets. Dans ce cas la barre oblique remplt son rôle en indiquant certains
caractères de contrôle.
"barney"
"hello world\n"
"caractère guillemet \""
"caractère \t tabulation"
Contenu Signification
\n Nouvelle ligne
\r Retour chariot
\t Tabulation
\f Saut de page
\b Espace arrière
\a Bell (sonnerie)
\e Escape (caractère escape ASCII)
\007 Toute valeur ASCII Octal (ici 007 bell)
\x7f Toute valeur ASCII Héxadécimal (ici 7f delete)
\cC Toute caractère de controle (ici CTRL-C)
\\ Barre oblique inverse
\" Guillemet
\l Lettre suivante en minuscule
\L Lettres suivantes en minuscule jusqu \E
\u Lettre suivante en majuscule
\U Lettres suivantes en majuscule jusqu \E
\Q Tous les signes non alphanumériques sont transformés en carac-
tères d’échappement jusque \E
\E Met fin à \L \U ou \Q
Les chaı̂nes en guillements sont à variables interpolées, ce qui signifie que les noms de certaines variables situées dans
les chaı̂nes sont remplacées par leurs valeurs courantes au moment où les chaı̂nes sont utilisées.
Opérateurs de chaı̂nes Les valeurs d’une chaı̂ne peuvent être concaténées par l’opérateur ..
l’opérateur répétition de chaı̂ne, constitué par la lettre minuscule x. Il prend son opérande de gauche (une chaı̂ne) et
en effectue autant de copies concaténées que le spécifie son opérande de droite (un nombre).
Conversion automatique entre nombres et chaı̂nes Perl effectue la conversion automatique des nombres en
chaı̂nes si nécessaire. Ceci dépend de l’opérateur utilisé sur la valeur scalaire. Quand un opérateur attend un nombre +,
Perl voit la valeur comme un nombre. Lorsqu’un opérateur attend une chaı̂ne ., Perl voit la valeur comme une chaı̂ne.
$fred = 17;
$barney = ’hello’;
$barney = $fred +3;
Il existe aussi des opérateurs d’affectation binaires qui sont en sorte des raccourcis d’opérations plus affectation. Ces
opérateurs permettent de réduire la quatité de code.
$fred = $fred + 5;
$fred += 5;
Les deux affectations précédentes sont équivalentes. On peut ainsi écrire un opérateur d’élévation à la puissance **=
On peut aussi fournir une série de valeurs séparées par des virgules
On désigne l’interpolation de variable par interpolation par guillements. Pour placer un vrai $ il faut le faire précéder
d’un \.
Si vous voulez accoler des lettres directements à la suite du contenu d’une variable, il faut utiliser les délimiteurs de
noms de variables que sont les accolades.
$fruit = "pomme";
$phrase = "j\’ai 5 ${fruit}s\n;
Associativité
Opérateurs
gauche les parenthèses et les opérateurs de listes
gauche ->
++ -- (auto-incrémentation, auto-décrémentation)
droite **
droite \ ! ~ + - opérateurs uniaires
gauche = ~ !~
gauche * / % x
gauche + - . opérateurs binaires
gauche << >>
opérateurs unaires nommés (tests de fichiers -X, rand)
< <= > >= lt le gt ge (les ”non egal”)
== != <=> eq ne cmp (les ”égal”)
gauche &
gauche | ^
gauche &&
gauche ||
droite ? : (ternaire)
droite = += -= .= (et les opérateurs d’affectations similaires)
gauche , => (opérateurs de listes vers la droite
droite not (non logique)
gauche and (et logique)
gauche or (ou logique) xor (ou exclusif)
Tab. 10.2 – Associativité et précédence des opérateurs du plus élevé au plus faible
Comment obtenir une valeur saisie au clavier dans un programme Perl. Le moyen le plus simple est l’opérateur d’entrée
standard <STDIN>. Lorsque vous utilisez <STDIN> à un endroit où une valeur scalaire est attendue, Perl lit la totalité
de la ligne de texte suivante de l’entrée standard jusqu’au prochain caractère de nouvelle ligne, et utilise cette chaı̂ne
comme valeur de <STDIN>.
$line = <STDIN>;
if ($line eq "\n")
{
print "Ceci n’était qu’une ligne vide!\n";
}
else
{
print "Cette ligne de saisie était : $line";
}
Cet opérateur enlève le dernier caractère d’une variable si celui-ci est un caractère de nouvelle ligne.
Il est alors possible de faire directement une affectation dans une variable à partir de STDIN en enlevant en même
temps le caractère de fin de ligne.
chomp($text=<STDIN>);
$text = <STDIN>;
chomp($text);
chomp est une fonction et renvoie une valeur : le nombre de caractères supprimés (ce qui est rarement utile). Lorqu’une
ligne se termine par plusieurs caractère de fin de ligne, la fonction chomp n’en supprime qu’un.
Que se passe-t-il si vous utilisez une variable scalaire avant de l’avoir affecté. Rien de fatal, les variables possèdent une
valeur spéciale undef avant d’avoir reçu leur première affectation. Si on essaie d’utiliser undef comme un nombre, la
valeur vaut 0, comme un chaı̂ne, la valeur vaut la chaı̂ne vide.
Il est possible aussi de fixer une valeur à undef.
$brel = undef;
L’opérateur d’entrée standard <SDTIN> peut renvoyer undef s’il n’y a plus d’entrée (fin de fichier par exemple). Pour
savoir si une valeur est undef, on utilise la fonction defined, qui renvoie faux pour undef et vrai pour tout le reste.
$brel = <STDIN>;
if (defined($brel))
{
print "La saisie est $brel";
}
else
{
print "Aucune saisie disponible!\n";
}
La dernière utilise l’opérateur étendue .., il crée une liste de valeurs en dénombrant à partir du scalaire de gauche
jusqu’au sclaire de droite, une par une.
Les élément d’un tableau ne sont pas forcément des constantes. Ils peuvent être des expressions réevaluées à chaque
utilisqation du littéral.
10.4.2.4 Le raccourcis qw
On a très souvent recours aux listes de mots simples. Le raccourci qw facilite leur génération en épargnant la saisie de
nombreuses guillemets :
qw/ toto tutu titi tata/ #idem ci dessus mais moins de lettres frappées
qw signifie quoted words (mots entre guillemets) ou quoted with whitespace (cités par des blancs). Perl les traı̂te
commeune chaı̂ne entre apostrophes (on ne peut donc pas utiliser \n ou $toto dans une liste qw). Le blanc (espace ou
nouvelle ligne) sont écartés.
qw/ toto
tutu titi
tata/ #idem ci dessus mais moins de lettres frappées
De même il n’est pas possible d’insérer des commantaires dans une liste qw. Perl permet de choisir n’importe quel
délimiteur de qw. Tous les caractères de ponctuation peuvent être utilisé.
Si vous désirez toujours utiliser le même délimiteur et insérer ce caractère dans la liste, il est toujours possible de la
faire en le faisant précéder d’un \.
De manière analogue à pop, shift renvoie undef en cas de variable de tableau vide.
Aucun espace supplémentaire n’est ajouté avant ou après un tableau interpolé. Le cas échéans vous devez les placer
vous même :
Attention à cette manière d’interpoler lorsque vous voulez écrire des adresses email.
L’index est calculé comme une expression ordinaire, comme si elle était extérieure à la chaı̂ne. Elle n’est pas d’abord
interpolée comme dans une variable. Si $y contient la chaı̂ne ”2*4”, nous parlons encore de l’élément 1, car la "2*4"
en tant que nombre vaut 2.
Si vous voulez faire suivre une variable scalaire d’un crochet gauche, vous devez délimiter celui-ci afin qu’il ne soit pas
considéré comme faisant partie d’une référence de tableau :
perl -e ’foreach $roches (qw/ sediment ardoise lave /){print "Une roche est $roches.\n";}’
Une roche est sediment.
Une roche est ardoise.
Une roche est lave.
La variable de contrôle $roches prend une nouvelle valeur de la liste à chaque itération. La variable de contrôle n’est
pas une copie de l’élément de la liste, c’est l’élément de la liste. Si la variable de contrôle est modifiée, l’élément de la
liste est modifié.
@roches = qw/ sediment ardoise lave /;
foreach $roche (@roches)
{
$roche = "\t$roche"; #place une tabulation devant chaque élément
$roche .= "\n"; # place une nouvelle ligne à la fin de chaque élément
}
print "les roches sont :\n", @roches;
perl -e ’@roches = qw/ sediment ardoise lave /;foreach $roche (@roches){$roche = "\t$roche";$roche .= "\n"
les roches sont :
sediment
ardoise
lave
Après la boucle, la variable de contrôle est identique à la valeur avant la boucle. Cette valeur est sauvegardée par Perl,
mais nous n’y avons pas accès.
Comme pour reverse, il faut un opérande de gauche pour pouvoir utiliser la fonction.
Même si qquechose est exactement la même séquence de caractères, dans un cas il peut donner une valeur scalaire
unique, et dans un autre cas, une liste.
Les expressions Perl renvoient toujours la valeur appropriée à leur contexte. Par exemple, un nom de tableau, dans
un contexte de liste renvoie la liste de des ses éléments. Dans un contexte scalaire il renvoie le nombre d’éléments du
tableau.
Voici quelques contextes courant pour vous familiariser avec les contextes :
$fred = quelquechose;
$fred[3] = quelquechose;
123 + quelquechose;
quelquechose + 654;
if (quelquechose) { ... }
while (quelquechose) { ... }
$fred[quelquechose] = quelquechose;
@toto = quelquechose;
($toto,$barney) = quelquechose;
($toto) = quelquechose;
push @fred, quelquechose;
foreach $fred (quelquechose) { ... }
sort quelquechose;
reverse quelquechose;
print quelquechose;
Attention, il y a un piège :
ou encore :
chomp(@lignes = <STDIN>); # lit toutes les lignes et supprime tous les caractères de nouvelle ligne
sub marine
{
$n+=1; #variable globale $n
print "Hello marin numéro $n!\n";
}
La définition des fonctions peuvent être situées à n’importe quel endroit de votre programme. Les définitions des
fonctions sont globales ; et en l’absence d’astuces, il n’existe pas de fonctions privées. Si deux fonctions portent le
même noms, la dernière supplante la première.
Dans tous les exemples précédents, toutes les variables étaient globales. Il est possibles de créer des variables privées.
On crée alors une table nommée @semaine, dont les éléments sont des chaı̂nes de caractères. Le premier élément est
d’indice 0 est "dim" et le dernier d’indice 6 est "sam". On peut très bien mélanger au sein de la même table des
chaı̂nes et des valeurs numériques. Les éléments sont indépendants.Si la table est toujours préfixée par @, ses éléments
individuels sont des scalaires et sont donc préfixés par un $.Pour modifier ou consulter les éléments de la table on
utilisera :
Il est important de comprendre que les espaces des noms de variables scalaires et des tables sont disjointes. La variable
$i et la table @i peuvent coexister sans conflit. De plus il faut comprendre que $i[0] est un élément de @i et n’a rien
a voir avec $i.On peut recopier directement une table dans une autre avec une simple affectation.
yann@yoda:~/script_linux$ perl -e
’@t=(1,2,3,4); @s=@t;print $s[3]; print "\n";’4
yann@yoda:~/script_linux$
On remarquera que tous les éléments de la table sont dupliqués. Si l’on modifie le contenu de la table initiale, la copie
n’est pas modifiée.
yann@yoda:~/script_linux$ perl -e ’@t=("1","2"); @s=@t;$t[0]="un";
print $s[0]; print " "; print $t[0]; print "\n";’
1 un
yann@yoda:~/script_linux$
Un dernière remarque : Lorsque l’on évalue une table dans un contexte scalaire, elle fournit le nombre de ces éléments.
Une table est évaluée dans un contexte scalaire quand on essaie de l’affecter dans une variable scalaire.
yann@yoda:~/script_linux$ perl -e ’@semaine=("dim",
"lun", "mar", "mer", "jeu", "ven", "sam"); $nb_jours=@semaine; print
$nb_jours;print "\n";’7yann@yoda:~/script_linux$
De même
yann@yoda:~/script_linux$ perl -e ’@semaine=("dim", "lun", "mar", "mer", "jeu",
"ven", "sam"); $nb_jours=@semaine; print @semaine + 0;print "\n";’
7
yann@yoda:~/script_linux$ perl -e ’@semaine=("dim", "lun", "mar", "mer", "jeu",
"ven", "sam"); $nb_jours=@semaine; print @semaine + 2;print "\n";’
9
yann@yoda:~/script_linux$
Ici on force l’évaluation de @semaine comme une valeur numérique pour l’additionner. Par contre si l’on a :
yann@yoda:~/script_linux$ perl -e ’@semaine=("dim", "lun", "mar", "mer", "jeu",
"ven", "sam"); print @semaine;print "\n";’
dimlunmarmerjeuvensam
yann@yoda:~/script_linux$
La table est évaluée dans un contexte de liste et est remplacée par la liste de tous ses éléments. Ici print accole tous
les éléments de la liste qu’on lui transmet en argument.Il faut donc bien distinguer les listes, qui sont une organisation
des données, et les tables, qui sont des variables. En particulier l’évaluation d’une liste dans un contexte scalaire en
fournit le dernier élément, alors que l’évaluation d’une table dans le même contexte donne le nombre de ses membres.
yann@yoda:~/script_linux$ perl -e
’@t=("un", "deux", "trois", "quatre"); $i=@t;print $i;print
"\n";’4yann@yoda:~/script_linux$ perl -e ’$i=("un", "deux", "trois", "quatre");
;print $i;print "\n";’quatre
yann@yoda:~/script_linux$
Comme les espaces des noms de variables scalaires et des tables sont disjointes, il est possible de créer une variable
scalaire avec le même nom qu’une table. En règle générale on y stockera le nombre d’éléments.
yann@yoda:~/script_linux$ perl -e ’@semaine=("dim",
"lun", "mar", "mer", "jeu", "ven", "sam"); $semaine=@semaine; print
$semaine;print "\n";’7yann@yoda:~/script_linux$
L’indice du dernier élément de la table peut être obtenu en préfixant le nom par #. Comme cette valeur est scalaire,
elle doit être elle-même préfixée par $.
[yann@ulysse script_linux]$ perl -e
’@semaine=("dim", "lun", "mar", "mer", "jeu", "ven", "sam"); print
$#semaine;print "\n";’6
[yann@ulysse script_linux]$
Si l’on utilise un indice négatif pour accéder à un élément, le décompte se fait à rebours à partir de la fin de la table.
Le fait d’accéder à un élément d’indice supérieur au dernier de la table ajoute automatiquement cet élément.
De même si l’on essaie d’insérer un élément dans le dixième case du tableau les éléments intermédiaires seront auto-
matiquement créés mais vides.
@nouvelle=@tab[4];
@tab[4] ne représente pas l’élément numéro 4 $tab[4] mais une table ne contenant qu’un seul élément. Il faudra alors
consulter cette valeur par :
print $nouvelle[0];
Une table ne contient que des scalaires, mais un scalaire peut être une référence pointant vers une table, ce qui permet
la création de tableaux multidimensionnels. On peut par exemple écrire :
Il faut bien comprendre que le contenu de la table ouvrable est intégralement insérée au même niveau que les autres
éléments de la liste. Les listes sont puissantes en Perl. Il est possible de les utiliser en tant que partie gauche d’une
affectation. Par exemple :
On remarquera toutefois que le remplissage de la table se fait de manière gloutonne. C’est à dire qu’elle conssomera
toutes les valeurs disponibles dans la liste de droite.
[yann@ulysse script_linux]$ perl -e ’($debut,@valeurs,$fin)=(4,6,12,15,14); print "debut:".$debut." fin:"
valeurs:".@valeurs."\n";’debut:4 fin: valeurs:4
[yann@ulysse script_linux]$
On voit ici que le valeur fin n’a pas été affectée puisque la table à récupérer les 4 dernières valeurs. Lorsque l’on
connait bien le nombre d’éléments de la liste, il est possible de restreindre l’affectation à des sous-ensembles des listes
contenues dans la partie gauche.
[yann@ulysse script_linux]$ perl -e
’(@vecteur_1[0,1], @vecteur_2[0,1], @vecteur_3[0,1])=(1,2,3,4,5,6); print
$vecteur_1[0]." ".$vecteur_1[1]." ".$vecteur_2[0]." ".$vecteur_2[1]."
".$vecteur_3[0]." ".$vecteur_3[1]."\n";’1 2 3 4 5 6
[yann@ulysse script_linux]$
Ici les différentes tables ne consomment que le nombre d’éléments correspondant à l’intervalle indiqué. Dans la pratique
l’insertion d’une variable table dans une liste qui se trouve en partie gauche d’une affectation se fera presque toujours en
dernière position de la liste. Cela est très utile quand on ne connait pas exactement la liste des éléments présents.On
est souvent amené à insérer ou à extraire des éléments en tête ou en fin de liste. Même s’il existe des opérateurs
spécialisés, il est possible de les remplacer par de simples affectations :
@table=($nouveau,@table);@table=(@table, $nouveau);
permet d’ajouter un élément en début de liste (resp. en fin de liste).
($inutile, @table)=@table;
permet de supprimer le premier élément de la liste. Pour supprimer le dernier élément de la liste, le méthode la plus
simple est de décrémenter l’indice du dernier élément.
$#table--;
Il est possible d’utiliser la même stratégie pour éliminer le ième élément d’une liste.
@table=(@table[0..$i-1], @table[$i+1..$#table]);
Voici un petit excercice qui va vous permettre de tester tout ce que nous avons vu.
#! /usr/bin/perl -w
# fichier tables.pl
#! /usr/bin/perl -w
#fichier protection.pl
$nombre = 12;
$chaine = "abc def";
@table = ("un", "deux", "trois");
print "\n";
Ecrivez ce petit script et testez le.
Pas de protection
12abc defundeuxtrois
[yann@ulysse 14]$
Les apostrophes offrent une protection forte, au sein desquelles les expressions sont considérées d’une manière littérale
sans aucune interprétation. Dans les chaı̂nes protégées par de guillemets les noms des variables sont remplacés par leur
contenu. Dans le cas d’une expression table, si on utilise une protection guillemets, elle est remplacée par la liste des Lorsque
éléments contenus dans la table séparés par des espaces. Si aucune protection n’est utilisée, alors nous avons la suite
des éléments sans séparation par des espaces.
l’on désire délimiter explicitement le nom d’une variable, on peut l’encadrer par des accolades. Si l’on souhaite affiche
le contenu de la variable $t, suivi de la chaı̂ne de caractère [0], sans que cela soit considérer comme le premier élément
de la table @t on peut ecrire ${t}[0]
[yann@ulysse script_linux]$ perl -e
’@t=(1,2); $t=3; print "${t}[0]\n";print "$t[0]\n"’3[0]1[yann@ulysse
script_linux]$
données important disponible en Perl correspond aux tables de hachage. On peut également les trouver sous le nom
de tableaux associatifs. Ces tableaux se représentent sensiblement comme des tableaux classiques, mais l’indexation
ne se fait plus par nombre entier, mais par une chaı̂ne de caractères que l’on nomme clé. Ce type de structure offre
un accès efficace aux données.Le nom de variable d’une table de hachage est préfixée par un %. Ses éléments, comme
ceux d’une table classique, sont des scalaires et donc préfixés par $. Pour accéder à un élément, on place la clé entre
accolades.Par exemple si %semaine est une table de hachage :
$semaine{"lundi"}="monday";$semaine{"mardi"}="tuesday";
...
Et l’on pourra consulter un élément par :
print $semaine{"lundi"};
qui affichera monday.
On remarquera que la chaı̂ne de caractères qui sert de clé peut contenir des caractère accentués, des caractères spéciaux
et même des espaces. Lorsque la clé ne contient que des caractères alphanumériques ”classiques” (7bits), les guillemets
à l’intérieur des accolades sont facultatives.L’initialisation d’une table de hachage se fait par l’intermédiaire d’une liste
qui contient des paires clés/valeurs :
%semaine=("lundi", "monday", "mardi", "tuesday",
"mercredi", "wednesday","jeudi", "thursday", "vendredi", "friday", "samedi",
"saturday"),"dimanche", "sunday");
Il est donc possible très rapidement de convertir une table de hachage en table standard par l’expression :
@semaine=%semaine;
L’implémentation d’une table de hachage ne préserve pas l’ordre de saisie des éléments. Aussi dans la table classique,
les paires clés/valeurs ne figureront elles pas dans le même ordre que la liste originale. Le script suivant illustre ceci.
[yann@ulysse 14]$ more hachages.pl#! /usr/bin/perl -w
# fichier hachages.pl
@semaine=%semaine;
Afin de rendre plus lisible l’association clé/valeur dans les initialisations statiques, le langage Perl propose l’opérateur
=> :
%semaine = (
"lundi" => "monday",
"mardi" => "tuesday",
"mercredi" => "wednesday",
"jeudi" => "thursday",
"vendredi" => "friday",
"samedi" => "saturday",
"dimanche" => "sunday"
);
Un certain nombre de variables sont prédéfinies en Perl, ce qui permet de paramétrer le comportement de l’interpréteur.
Par exemple à son lancement, l’interpréteur définit automatiquement une variable classique @ARG qui contient les
arguments passés en paramètre du script. Il définit également une table de hachage %ENV qui offre un accès aux
variables d’environement du processus. Les clés sont les chaı̂nes de caractères des noms des variables.Le script suivant
illustre ce que l’on vient d’exposer :
L’opérateur de comparaison <=> renvoie -1 si son argument de gauche est inférieur à celui de droite, 0 s’ils sont égaux
et +1 si l’opérande de gauche est supérieur à celui de droite.Comme en C, certain opérateur peuvent être combiné
avec une opération. On retrouve +=, -=, /=, *=.
On dispose aussi d’un opérateur de répétition noté "x", pour rappeler le signe multiplier. Il construit la chaı̂ne en
multipliant la chaı̂ne de gauche autant de fois qu’on lui indique à droite.
Cet opérateur est utile pour faire de la mise en page en mode texte. En guise d’exercice, écrire un petit script qui
affiche le texte qu’on lui passe en paramètre en l’encadrant. Vous devriez avoir un affichage de ce type :
#! /usr/bin/perl -w
# fichier encadre_2.pl
print "+" . "-" x 30 . "+\n";
#print "@ARGV";
La plupart du temps unless n’est pas utilisé avec un bloc else car le schéma devient difficile à lire. Il est souvent
employé comme modificateur d’instructions simples.
exemple :
[yann@ulysse yann]$ perl -e ’do {print "action $i\n"} while (1 == 0);’
action
[yann@ulysse yann]$
Une autre structure sert à itérer automatiquement les éléments d’une liste. Le mot clé foreach est un synonyme de
for. foreach s’utilise ainsi :
foreach $variable(liste)
{
action;
}
La séquence d’action sera répétée en plaçant dans la variable successivement tous les éléments de la liste. Cette dernière
peut être fournie sous la forme de constante entre parenthèses et virgules :
[yann@ulysse yann]$
perl -e ’foreach $a ("un","deux","trois") {print "$a\n"};’un
deux
trois
[yann@ulysse yann]$
mais cela ne présente que peu d’intérêt. Le plus souvent une liste d’argument sera une variable table :
%semaine = (
"lundi" => "monday",
"mardi" => "tuesday",
"mercredi" => "wednesday",
"jeudi" => "thursday",
"vendredi" => "friday",
"samedi" => "saturday",
"dimanche" => "sunday"
);
Lorsqu’une table de hachage est évaluée dans un contexte de liste, elle renvoie une succession de paires clés/valeurs.
Ecrivez
Aussi l’utilisation directe de foreach n’est pas très utile, car elle ne différencie pas les clés des valeurs.
maintenant ce script et testez le. Quelles conclusions tirez-vous.
%semaine = (
"lundi" => "monday",
"mardi" => "tuesday",
"mercredi" => "wednesday",
"jeudi" => "thursday",
"vendredi" => "friday",
"samedi" => "saturday",
"dimanche" => "sunday"
);
La fonction keys prend en argument une table de hachage, et renvoie une liste de toutes ses clés.
sub
fonction(arguments);
Pour invoquer une fonction, il suffit de citer son nom suivi des arguments :
ou
sub somme
{
$somme = 0;
foreach $val (@_) {
$somme += $val;
}
return ($somme);
}
[yann@ulysse 14]$
il s’agit d’une fonction qui additionne ses arguments et renvoie la somme. Cette fonction est capable de traiter un
nompre quelconque d’arguments en utilisant une boucle foreach sur le tableau @_. On remarquera que la fonction est Ecrivez
évaluée dans un contexte scalaire.
et testez ce petit programme.
sub produit_vectoriel
{
(@u[0..2], @v[0..2]) = @_;
$w [0] = $u[1] * $v[2] - $u[2] * $v[1];
$w [1] = $u[2] * $v[0] - $u[0] * $v[2];
$w [2] = $u[0] * $v[1] - $u[1] * $v[0];
return (@w);
}
sub affiche_vecteur
{
($x, $y, $z) = @_;
return ("($x, $y, $z)");
}
@i = (1, 0, 0);
@j = (0, 1, 0);
@k = produit_vectoriel (@i, @j);
Dans un premier temps on sépare les arguments de la fonction de manière à travailler plus facilement.
sub efface
{
for ($i = 0; $i < @_; $i++) {
$_[$i] = 0;
}
}
$a = 4;
$b = 5;
print "a=$a b=$b\n";
efface ($a, $b);
print "a=$a b=$b\n";
efface (12);
[yann@ulysse 14]$
La modification des variables se produit bien, et la tentative de modification d’une constantes n’a pas de sens. La copie
du tableau @_ dans les variables locales permet donc de travailler sur des copies.
Ecrivez et testez ce petit programme. Que remarquez vous ?
sub efface
{
@args=@_;
for ($i = 0; $i < @args; $i++) {
$args[$i] = 0;
}
}
$a = 4;
$b = 5;
print "a=$a b=$b\n";
efface ($a, $b);
print "a=$a b=$b\n";
efface (12);
[yann@ulysse 14]$
Les varibles ne sont plus modifiées car on travaille sur des copies. La seconde invocation ne pose pas de problème non
plus, car 12 est inscrite dans une variable locale
$a = "précédent";
sub fonction
{
$a="suivant";
$b="nouveau";
}
[yann@ulysse 14]$
Avant l’invocation de la fonction ligne 5, nous essayons d’afficher le contenu d’une variable non définie. L’interpréteur
donne un message d’erreur. Par contre au retour de la fonction, non seulement l’ancienne variable globale $a a été
modifiée, mais la nouvelle variable $b définie dans la fonction est toujours disponible.
sub fonction
{
my $a = "initiale";
local $b = "initiale";
sub fonction_2
{
$a="modifiée";
$b = "modifiée";
print "fonction_2() : a=$a, b=$b\n";
}
[yann@ulysse 14]$
La variable $b, déclarée en local, est visible et peut être modifiée par la sous fonction, alors que $a ne peut pas l’être.
&$nom_fonction(argument);
my $nom_fonction="factorielle";
my $resultat=&$nom_fonction(5);
print "$resultat\n";
sub factorielle
{
my ($val) = @_;
return 1 if ($val <= 1);
return $val * factorielle($val -1);
}
[yann@ulysse 14]$
Testez ce programme.
10.8.6 Prototypes
Afin de s’assurer qu’une fonction est bien invoquée avec les arguments corrects, il est possible de fournir un prototype
dans sa déclaration. Chaque argument est représenté par un caractère qui indique son type.
Cette partie du TP est issue de TPs existant (B. Dupouy et S. Gadret) disponible à l’adresse suivante : http:
//www.infres.enst.fr/~domas/BCI/Proc/TPproc.html
– fork() Cette fonction va créer un processus. La valeur de retour n de cette fonction indique :
– n > 0 On est dans le processus père
– n = 0 On est dans le processus fils
– n = −1 fork a échoué, on n’a pas pu créer de processus
– getpid() : Cette fonction retourne le numéro du processus courant,
– getppid() : Cette fonction retourne le numéro du processus père.
11.2.2 Exercice 1
Taper ou récupérer le programme exo1.c.
Fichier exo1.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
Après compilation de exo1.c, on exécutera le programme exo1 plusieurs fois. Que se passe-t-il ? Pourquoi ?
Réponse :
123
124 T.P. de Système d’Exploitation
11.2.3 Exercice 2
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
Remarque si on relève un numéro de processus égal à 1, il s’agit du processus init, père de tous les processus.
Init adopte les processus orphelins, c’est à dire qu’un processus dont le père s’est terminé devient fils de 1, et
getppid() renvoie 1.
Réponse :
Les fonctions se finissant par un p utilisent une variable d’environnement PATH pour rechercher le répertoire dans
lequel se situe l’application à lancer, alors que les autres nécessitent un chemin d’accès complet. La variable PATH est
déclarée dans l’environnement comme étant une liste de répertoire séparée par des deux-points.
Le prototype execve() est le suivant :
int execve (const char * appli, const char * argv [], const char * envp []);
La chaine appli doit contenir le chemin d’accès au programme à lancer à partir du répertoire de travail en cours ou
à partir de la racine du système de fichiers s’il commence par un /.
Le tableau argv [] contient des chaı̂nes de caractères correspondant aux arguments que l’on trouve habituellement
sur la ligne de commande.
La première chaine arg[0] doit contenir le nom de l’application à lancer (sans chemin d’accès).
Le troisième argument est un tableau de chaı̂nes déclarant les variables d’environnement. On peut éventuellement
utiliser la variable externe globale environ si on désire transmettre le même environnement au programme à lancer.
Les tableaux argv[] et envp[] doivent se terminer par des pointeurs NULL.
Récapitulons les caractéristiques des six fonctions de la famille exec :
– execv()
– tableau argv[] pour les arguments ;
– variable externe globale pour l’environnement ;
– nom d’application avec chemin d’accès complet.
– execve()
– tableau argv[] pour les arguments ;
– tableau envp[] pour l’environnement ;
– nom d’application avec chemin d’accès complet.
– execvp()
– tableau argv[] pour les arguments ;
– variable externe globale pour l’environnement ;
– application recherchée suivant le contenu de la variable PATH.
– execl()
– liste d’arguments arg0, arg1 ... NULL ;
– variable externe globale pour l’environnement ;
– nom d’application avec chemin d’accès complet.
– execle()
– liste d’arguments arg0, arg1 ... NULL ;
– tableau envp[] pour l’environnement ;
– nom d’application avec chemin d’accès complet.
– execlp()
– liste d’arguments arg0, arg1 ... NULL ;
– variable externe globale pour l’environnement ;
– application recherchée suivant le contenu de la variable PATH.
fic est le nom du fichier exécutable qui sera chargé dans la zone de code du processus.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
Une liste non exhaustive des options de gcc est résumée dans la tableau suivant :
yann@ulysse:~/projet_signal/prog_linux/04$ ./exemple_execvp
total 232
-rwxr-xr-x 1 1000 1000 22513 ao 6 15:50 exemple_execlp
-rw-r--r-- 1 1000 1000 394 mar 22 2000 exemple_execlp.c
-rwxr-xr-x 1 1000 1000 23750 ao 6 15:50 exemple_execv
-rw-r--r-- 1 1000 1000 942 mar 22 2000 exemple_execv.c
-rwxr-xr-x 1 1000 1000 22800 ao 6 15:50 exemple_execve
-rw-r--r-- 1 1000 1000 351 mar 22 2000 exemple_execve.c
-rwxr-xr-x 1 1000 1000 22456 ao 6 15:55 exemple_execvp
-rw-r--r-- 1 1000 1000 231 ao 6 15:54 exemple_execvp.c
-rwxr-xr-x 1 1000 1000 23598 ao 6 15:50 exemple_popen_1
-rw-r--r-- 1 1000 1000 602 mar 22 2000 exemple_popen_1.c
-rwxr-xr-x 1 1000 1000 23469 ao 6 15:50 exemple_popen_2
-rw-r--r-- 1 1000 1000 607 mar 22 2000 exemple_popen_2.c
-rwxr-xr-x 1 1000 1000 23626 ao 6 15:50 exemple_popen_3
-rw-r--r-- 1 1000 1000 1497 mar 22 2000 exemple_popen_3.c
-rwxr-xr-x 1 1000 1000 1123 mar 22 2000 exemple_popen_3.tk
-rwxr-xr-x 1 1000 1000 17243 ao 6 15:50 exemple_system
-rw-r--r-- 1 1000 1000 95 mar 22 2000 exemple_system.c
-rwxr-xr-x 1 1000 1000 52 mar 22 2000 ls
-rw-r--r-- 1 1000 1000 295 mar 22 2000 Makefile
yann@ulysse:~/projet_signal/prog_linux/04$
yann@ulysse:~/projet_signal/prog_linux/04$ whereis ls
ls: /bin/ls /usr/share/man/man1/ls.1.gz
Un programme peur se terminer de plusieurs manières. La plus simple est de revenir de la fonction main en renvoyant
un compte rendu d’exécution sous forme de valeur entière. Cette valeur est lue par le processus père qui peut en tirer
les conséquences adéquates. Par convention, un programme qui réussit à effectuer son travail renvoie une valeur nulle,
tandis que les cas d’échecs sont indiqués par des codes de retour non nuls.
Si seuls, la réussite ou l’échec du programme importent, il est possible d’employer les constantes symboliques EXIT_SUCCESS
ou EXIT_FAILURE définies dans <stdlib.h>.
Une autre manière de terminer un programme normalement est d’utiliser la fonction exit().
On lui transmet en argument le code de retour pour le processus père. L’effet est strictement égal à celui d’un retour
depuis la fonction main, à la différence que exit() peut être invoquée depuis n’importe quelle partie de programme.
Soit le programme exemple_exit_1.c suivant :
#include <stdlib.h>
void sortie (void);
Réponse :
int main(void)
{
sortie ();
exit(EXIT_SUCCESS);
}
return (EXIT_SUCCESS);
}
Lors de l’exécution, la première assertion passe, et le message est écrit sur stdout, mais la seconde échoue et assert()
affiche alors le détail du problème sur stderr.
if (pid == 0)
{
/* processus fils */
sleep (2);
fprintf (stdout, "Le processus fils %u se termine\n", getpid());
exit (0);
}
else
{
/* processus père */
sprintf (commande, "ps %u", pid);
system (commande);
sleep (1);
system (commande);
sleep (1);
system (commande);
sleep (1);
system (commande);
sleep (1);
system (commande);
sleep (1);
system (commande);
}
return (0);
}
Compilez et lancez le programme. Que se passe t il ?
Une fois le programme terminé, lancez la commmande ps. Le processus fils est il toujours présent ?
Réponse :
Le S dans le seconde colonne indique que le processus fils est endormi au début, puis il se termine et passe à l’état de
zombie Z. Lorque le processus père se termine, on invoque manuellement la commande ps, et on s’aperçoit que le fils
zombie a disparu.
L’exemple suivant décrit un autre phénomène. Le fichier source exemple_zombie_2.c est le suivant :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int
main (void)
{
pid_t pid;
if (pid != 0) {
/* processus père */
sleep (2);
fprintf (stdout, "Père : je me termine\n");
exit (0);
} else {
/* processus fils */
Le processus père se termine au bout de deux secondes, alors que le fils va continuer à afficher régulièrement le PID
du père. L’exécution montre bien que le processus 1 adopte le processus fils dès que le père se termine. Au passage
on remarquera que, aussitôt le processus père terminé, le shell reprend la main et affiche immédiatement son symbole
d’acceuil.
Pour lire le code de retour d’un processus fils, il existe quatre fonctions : wait(), waitpid(), wait3() et wait4(). Les
trois premières sont des fonctions de bibliothèque implémentées en invoquant wait4() qui est le seul véritable appel
système.
Nous n’étudierons que la fonction wait().
La fonction wait() est déclarée dans <sys/wait.h>, ainsi :
Lorsqu’on l’invoque, elle bloque le processus appelant jusqu’à ce qu’un de ses fils se termine. Elle renvoie alors le PID
du fils terminé. Si le pointeur status est non NULL, il est renseigné avec une valeur informant sur les circonstances
de la mort du fils. Si un processus fils était déjà en attente à l’état de zombie, wait() revient immédiatement. Si les
corconstances de la fin du processus ne nous intéressent pas, il est possible de fournir un argument NULL. La manière
dont sont organisées les informations au sein de l’entier status est opaque, et il faut utiliser les macros suivantes pour
analyser les circonstances de la fin d’un processus fils :
– WIFEXITED(status) est vraie si le processus s’est terminé de son propre chef en invoquant exit() ou en revenant
de main(). On peut alors obtenir le code de retour du fils en invoquant WEXITSTATUS(status) ;
– WIFSIGNALED(status) indique que le fils s’est terminé à cause d’un signal, y compris le signal SIGABRT, envoyé
lorsqu’il appelle abort(). Le numéro de signal ayant tué le processu fils est disponible en utilisant la macro
WTERMSIG(status). À ce moment,la macro WCOREDUMP(status) signal si une image mémoire core a été créée ;
– WIFSTOPPED(status) indique si le fils a ét stopppé temporairement . Le numéro de signal ayant stoppé le processus
fils est accessible en utilisant WSTOPSIG(status).
Dans l’exemple qui suit, le processus père va se dédoubler en une série de fils qui se termineront de manière variées.
Le processus père restera en bouble sur wait(), jusqu’à ce qu’il ne reste plus de fils. Voici le fichier source :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
return (0);
}
case 0 :
return (1);
case 1 :
exit (2);
case 2 :
abort ();
case 3 :
raise (SIGUSR1);
}
return (numero);
}
Il n’y a pas de différence entre un retour de la fonction main() et un appel exit(). De même l’appel abort() se
traduit bien par un envoi du signal SIGABRT. Le signal SIGUSR1 termine le processus sans crée d’image core.
Fichier fexec.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
if (argc != 2)
{printf(" Utilisation : %s fic. a executer ! \n", argv[0]);
exit(1);
}
case -1 :
printf (" Le fork n’a pas reussi ");
exit (3) ;
default :
/* le pere attend la fin du fils */
printf (" Pere numero %d attend\n ",(int) getpid());
Fils=wait (&Etat);
printf ( " Le fils etait : %d ", Fils);
printf (" ... son etat etait :%0x (hexa) \n",Etat);
exit(0);
}
}
Que se passe-t-il ? Pourquoi ?
Réponse :
12.1 Introduction
Ce TP est issu d’un article de Pierre Ficheux ([email protected]). Il s’agit d’une introduction à la programmation
multi-threads sous LINUX. Les exemples de programmation utilisent la bibliothèque LinuxThreads disponible en
standard sur la majorité des distributions LINUX récentes.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int i;
i = 1;
if ((pid = fork()) == 0) {
/* Dans le fils */
printf ("Je suis le fils, pid = %d\n", getpid());
sleep (2);
printf ("Fin du fils, i = %d !\n", i);
exit (0);
}
else if (pid > 0) {
/* Dans le pere */
printf ("Je suis le pere, pid = %d\n", getpid());
sleep (1);
/* Modifie la variable */
i = 2;
printf ("le pere a modifie la variable a %d\n", i);
sleep (3);
printf ("Fin du pere, i = %d !\n", i);
exit (0);
}
137
138 T.P. de Système d’Exploitation
else {
/* Erreur */
perror ("fork");
exit (1);
}
}
Pour le compiler ,utiliser la commande gcc, par exemple (premier exercice) : gcc fork.c -o exo1 ou gcc -Wall
fork.c -o exo1. Pour plus de détail taper la commande man gcc.
yann@yoda:~/tp$ ./fork
Je suis le fils, pid = 23052
Je suis le pere, pid = 23051
le pere a modifie la variable a 2
Fin du fils, i = 1 !
Fin du pere, i = 2 !
yann@yoda:~/tp$
Les limites du fork apparaissent d’ores et déjà lorsqu’il s’agit de partager des variables entre un processus père et son
fils. Comme on le voit dans le petit exemple ci-dessus, la variable globale i, modifiée par le père a toujours l’ancienne
valeur dans le fils. Ceci est le comportement normal du fork qui duplique le contexte courant lors de la création d’un
processus fils.
la création d’un nouveau contexte est pénalisante au niveau performances. Il en est de même pour le changement de
contexte (context switch), lors du passage d’un processus à un autre.
Un thread ressemble fortement à un processus fils classique à la différence qu’il partage beaucoup plus de données avec
le processus qui l’a créé :
– Les variables globales,
– Les variables statiques locales,
– Les descripteurs de fichiers (file descriptors).
Le multi-threading est donc une technique de programmation permettant de profiter des avantages (et aussi de certaines
contraintes) de l’utilisation des threads.
12.3 Le multithreading
Cette partie est tirée d’un article d’Éric Lacombe ([email protected]) du magazine Linux France Magazine du mois de
Juillet/Août 2004.
Le multithreading est l’alternative à la programmation multiprocessus. Il offre un parallélisme plus léger à gérer pour
le système.
Les threads créent un parallélisme intra-processus et facilitent le partage des tâches et leur communication au sein
d’un processus.
de l’UC pour pouvoir les restaurer lors de l’ordonnancement. En plus de ces informatons on trouve des informa-
tions relatives aux entrées/sorties, à la gestion mémoire, à l’ordonnancement et à la comptabilistion des ressources
conssomées.
Dasn un processus multithreadé, plusieurs flots d’exécution se partagent les mêmes ressources (descripteurs de fichiers,
espaces d’adressage des variables...). Cependant pour permettre l’exécution simultanée de ces flots, chacun possède un
identifiant unique, une pile d’exécution propore, des registres (pointeur de pile...), un état...
Chaque thread possède également des données privées (dans la norme POSIX).
On peut résumer en disant qu’un thread est une entité d’exécution , rattachée à un processus, chargée d’exécuter une
partie du processus. Ce dernier étatn vu comme un ensemble de ressources (espace d’adressage, fichiers, périphériques...)
que ses threads se partagent.
Quel que soit le modèle de multithreading, on retrouve toujous les mêmes états principaux pour les threads. Ils
permettent, tout comme ils le font pour les processus, de renseigner l’ordonnanceur pour qu’il sache quelle tâche
lancer.
On retrouve pour chaque thread trois états clés : running, ready et blocked. L’état suspend que peut avoir un
processus n’existe pas (l’état waiting est une suspension mais n’a pas la même sémantique) ; en effet suspendre un
unique thread revient à suspendre l’exécution cd tous les threads d’un même processus, puisque celui-ci se retrouve
swappé pour laisser la place à d’autres processusen mémoire vive.
L’état runningest attribué à un thread en cours d’exécution, c’est à dire quand il s’exécute réellemet sur le processeur.
Il faut tout de mêm faire attention, car il reta possible que l’état d’un thread soit différent de celui de son processus
suivant l’implémentation multithreading retenue (cas des green threads).
Tout thread pouvant être choisi par l’ordonnanceur est dans l’état ready.
L’état blocked intervient lorsqu’un thread fai tun appel d’E/S, par exemple, ou un quelconque autre appel système,
ou que sa rtanche de temps impartie est écoulée. Suivant le modèle utilisé, ce timeslice est calculé : soit au niveau de
l’ordonnancement au sein des processus, auquel cas il s’agit d’une décision de l’espace utilisateur ; soit dans l’espace
du noyau.
Il se peut également qu’un threadsoit dans l’état waiting. Cela se produit lorsqu’il essaie d’accéder à un mutex (permet
l’exlusion mutuelle des threads lors de l’accès à des données globales) qui est déjà pris auquel il est bloquésur ce verrou
jusqu’à ce qu’il se libère.
Notons que le thread réveillé n’est pas forcément exécuté de suite. Ceci dépend de l’implémentation. L’état sleep est
un état qui se retrouve dans les threads Java par exemple.
Ces trois derniers états ont la caractéristique d’attendre qu’un évènement se produise.
Lorqu’un thread parvient a son terme ou lorsqu’il est tué par un autre thread, deux situations peuvent se produire.
Soit ses ressources sont libérées directement par l’entité en charge (en général, il s’agit du kernel, mais dans le cas des
noyaux linux 2.4, un thread manager associé au processus relatif au thread à supprimer se charge de la libération). Ce
cas n’est possible que si le thread possède l’attribut detached.
Soit il passe d’abord par un état zombie, dans l’attente qu’un autre thread lise sa valeur de retour, avant d’être détruit
et que ses ressources personnelles soient libérées par l’entité chargée pour ces responsabilités.
Tout comme la destruction d’un thread, la création se fait par l’intermédiairedu thread manager du processus ou par
l’entité accréditée à ce travail.
On considère généralement trois modèles de multithreading. Leurs différences sont fonctions de la prise en charge plus
ou moins importante du kernel. Les threads ont toujours une existence dans l’espace utilisateur, ils sont reliés (en
fonction du modèle) à des threads systèmes qui sont l’entité de base du scheduler.
Ce sont ces threads qui vont être multiplexés sur les différents processeurs. La liaison entre ces deux sortes de threads
se fait par le biais des LWP (Light-Weight Process).
Les threads liés Analysons le modèles des threads liés (figure 12.3 P2). C’est l’implémentation la moins complexe
et s’avère être un choix judicieux lorsque l’ordonnanceur système est de complexité constante (O(1)). Il s’agit d’ailleurs
du modèle retenu pour linux dans la série des 2.5.x et 2.6.x, l’ordonnanceur aynat été grandement modifié et etant
dorénavant en (O(1)).
Il s’agit tout simplement d’assoscier à un thread utilisateur un thread unique système. On qualifie souvent ce modèle de
1-à-1. L’importantce d’voir un ordonnanceur de complexité constante se comprend car le nombre de threads systèmes
est exactement égal au nombre de threads utilisateurs ; par conséquent, on évite l’écroulement du systèmes si le temps
mis par l’ordonnanceur à faire son choix est indépendant du nombre de threads systèmes. Si ce n’est pas le cas, le
systèmes des threads liés mis en place se trouve fortement affecté lorsqu’un nombre de threads créés est trop important
(ce qui est caractéristique des applications Java en autres).
Dans ce modèle, il est compréhensible que la gestion de la librairie de threads se trouve fortement simplifiée de par
l’absence du multiplexage au niveau utilisateur . On a donc par processus autant de LWP que de threads utilisateurs.
Si un thread fait un appel système, alors le processus peut continuer de s’exécuter s’il contient des threads à l’état
ready et que sa tranche de temps impartie n’est pas écoulée. Le cas échéant , seul le LWP associé au thread ayant
effectué l’appel système est alors bloqué, laissant libre cours aux autres LWP de s’exécuter à sa place au sein du même
processus. Le blocage dû aus apples systèmes se fait au niveau des threads. Notons également que les routines du
kernel peuvent être multithreadées. Ce modèle est adopté par Linux 2.4 et 2.6 et Solaris 8.
Les threads multiplexés Le modèle des threads multiplexés (Figure 12.3 P3) est le plus compliqué à mettre en
œuvre, il s’agit du modèle retenu dans Solaris 2 et Windows XP entre autres. L’ordonnancement ici se fait à deux
niveaux. En effet, au niveau système, les LWP (associés au thread système) sont multiplexés et on ordonnance aussi les
threads utilisateurs sur les LWP. On obtient ici une flexibilité accrue, mais on perd en efficacité. Un autre intérêt vient
de la possibilté d’avoir une adaptation de la politique d’ordonancement au niveau utilisateur en fonction de l’application
considérée. Les temps dus aux changements de contexte lors de l’ordonnancement des threads sont fortement réduits ;
en effet , la majeure partie de la gestion peut se faire dans l’espace utilisateur (s’il n’y a pas d’ajout de LWP au
processus courant), le multiplexage des threads sur les différents LWP n’est pas connu du kernel, et donc il n’y a pas
de changement de contexte de sa part (pas de modification de registres...), d’où ule gain de temps. Cependant, en
contre-partie, il s’avère impossible de profiter entièrement du parallélisme de la machine, car seul le LWP ou les LWP
associés aux threads multiplexés ont une existence pour le kernel.
Le nombre de threads systèmes accordés aux processeurs ramenés au nombre total de threads utilisateurs, détermine
la capacité à profiter du parallélisme au niveau hardware. Si ce nombre avoisine la valeur 1, on profitera grandement
du parallélisme au détriment du nombre de commutations de contexte plus important dans le kernel. Tout dépend à
quoi est destiné le système. Si la machine n’a qu’un seul processeur, alors il paraı̂t avantageux d’opter pour peu de
Les threads liés et multiplexés Il est également possible d’avoir des processus hybrides (figure 12.3 P1) dans ce
modèle, c’est à dire contenant à la fois des threads liés et des threads multiplexés.
Les Users Level Threads ou Green Threads Le modèle des Users Level Threads ou encore Green Threads permet
une utilisation sur des systèmes ne comportant aucune fonctionnalité favorisant l’implémentation du multithreading.
Ils sont implémentés uniquement dans un espace utilisateur, d’ou une portabilité accrue. Ils ne sont pas connus du
noyau, leur existence est simulée uniquement dans une bibliothèque. Toutes la gestion des threads ne requiert aucun
appel au kernel, d’où un gain de temps. Cependant , il s’agit là d’une maigre compensation, car il est alors impossible
d’assigner à différents processeurs différents threads du même processus simultanément. En outre si un threads fait un
appel système, il y a de grandes chances que la kernel passe la main à un autre processus, car il n’a pas la connaissance
de la segmentation en threads du processus. Par conséquent, il modifiera l’état du processus si celui-ci effectue un
appell d’E/S par exemple, et fera appel au scheduler pour décider à qui passer la main. Le thread aura quant à lui
toujours un état running, son état est donc indépendant de l’état du processus. On perd ici la possibilité de profiter
pleinement du timeslice accordé au processus.
Notons que la commande ps -L permet de voir l’ensemble des LWP alloués par le kernel aux différents processus. On
constate alors aisément que le nombre de threads sous linux correspond au nombre de LWP : on se trouve bien en
présence d’un modèle 1-à-1.
Le système mis en place est un modèle 1-à-1, autrement dit les processus sont composés de threads liés uniquement. De
plus, le manque de support de la part du noyau oblige à l’utilisation d’un thread manager au sein de chaque processus.
Il a pour rôle de créer et de détruire les nouveaux threads, assure aussi l’implémentation correcte de la sémantique
des signaux, ainsi que d’autres parties de gestion. Mais il ajoute une lourdeur au système, et ralentit le processus de
création et de destruction.
L’absence de moyen pour la synchronisation au sein du noyau amène à l’utilisation des signaux dans l’implémentation
des threads.
Ce pendant la gestion des signaux au sein de la bibliothèque de threads est fragile et non conforme POSIX. Ceci est
dû à l’absence de concept de groupes de threads dans le noyau.
L’ABI (Application Binary Interface) de ELF (le format binaire couramment utilisé sous linux), n’est pas prévue pour
le stockage des données privées aux threads.
Pour remédier à cela, des relations fixes entre le pointeur de pile et la position du descripteur de thread sont utilisés.
A ceci s’ajoute un nombre limité de threads.
Concluons sur les problèmes de cette implémentation :
– Le thread manager est un goulot d’étranglement pour la création et la destruction de threads ; de plus le nettoyage
du processus appartient à ce thread qui, s’il se trouve tué, ne peut plus agir.
– On ne peut pas envoyer un signal à un processuscomme un tout (chaque thread à un PID différent).
– L’utilisation des signaux comme primitives de synchronisation aboutit à de gros problèmes. Des réveil hasardeux
peuvent se produire, ce qui engendre une pression supplémentaire sur le système de gestion des signaux du kernel.
– SIGSTOP et SIGCONT stoppent uniquement un thread et non l’ensemble du processus.
– Chaque thread possède un PID différent (problème avec les signaux). Une limite de (8192-1) threads sur l’IA32
(Intel Architecture. Le système de fichier /proc devient dur à utiliser, si trop de threads sont présents.
– Le manque d support du noyau empêche l’implémentation correcte des signaux. En outre, leur utilisation est une
approche lourde.
12.3.5 La librairie NTPL (Ingo Molinar et Ulrich Drepper) et les modifications appor-
tées au noyau Linux
Le but de cette nouvelle bibliothèque est d’assurer une compatibilité POSIX, une utilisation efficace des systèmes
multiprocesseurs (SMP), des coûts de création de threads faibles, une compatibilité binaire maximale avec l’implémen-
tation Linux Threads, un coût administratif non proportionnel au nombre de processeurs utilisés, le support NUMA
(Non-Uniform Memory Architecture), etc...
Notons également l’intégration dans C++ pour la gestion eds exceptions où le nettoyage des objets s’apparente à la
destruction de threads.
Le TID est renvoyé dans thread, on peut également spécifier une structure d’attributs (ou NULL) et la routine à
exécuter peut disposer d’un argument.
Permet de détacher un thread, il ne peut plus communiquer sa valeur de retour à un autre thread.
pthread_t pthread_self(void);
Compare deux TID et renvoie une valeur non nulle s’il sont égaux.
Assure que la fonction passée en paramètre ne s’exécute qu’une seule fois. Cette routine est typiquement utilisée pour
des fonctions d’initialisation comme l’allocation d’une structure globale. Parmi tous les threads appelant pthread_once
avec la même variable de contrôle, un seul l’exécute. On déclare la variable once_control de manière statique.
void pthread_yield(void);
Le thread appelant libère le processeur sur lequel il s’exécute, il est placer dans la file ready (runnable) du scheduler
qui va choisir un autre thread à exécuter (cette fonction est une extension GNU).
Lorsque le thread reçoit une demande d’annulation, les routines empilées sont exécutées. Ces deux fonctions s’utilisent
ensemble : on empile une fonction de nettoyage à l’entrée d’une section et on la dépile lorsqu’on sort. Lors d’un pop,
on peut soit exécuter la fonction empilée (execute=1) soit la dépiler sans l’exécuter (execute=0).
Le thread peut soit accepter (PTHREAD_CANCEL_ENABLE) ou refuser (PTHREAD_CANCEL_DISABLE) les demandes d’annu-
lation de la part d’autres threads.
Si on autorise l’annulation, elle peut soit prendre un effet immédiat (PTHREAD_CANCEL_ASYNCHRONOUS), soit lorsque
l’exécution arrive à un point d’annulation (PTHREAD_CANCEL_DEFERRED) : il s’agit de certaines fonctions systèmes
bloquantes, et quatre fonctions des pthreads.
void pthread_testcancel(void);
La première fonction récupère la politique d’ordonnancement et la seconde l’établit. Trois politiques sont prévues :
SCHED_FIFO (premier arrivé, premier servi), SCHED_RR (round robin, i.e. temps partagé, SCHED_OTHER (politique de-
pendant du constructeur). L’attribut param contient principalement la priorité du processus.
Cette fonction enregistre les trois routines passées en argument. Lors d’un appel fork(), avant duplication du processus,
la fonction prepare() est appelée.
Ensuite parent() est exécutée dans le processus père et child() est exécutée dans le processus fils.
Cette fonction est utile lorsqu’est mis en jeu un verrou au sein d’un processus entre plusieurs threads, et qu’un threads
appelle fork().
Lors de cet appel, un processus fils est créé mais avec seulement un flot d’exécution ; celui du threads appelant .
Si ce thread (dans le contexte du processus fils) veut récupérer le verrou acquis dans le processus père par un autre
thread, alors il va bloquer indéfiniment.
Pour remédier à cela prepare doit acquérir le verrou (mutex) en question, e,suite les deux autres routines devront
libérer ce verrou dansnle contexte du père pour l’une et du fils pour l’autre.
12.4.2 Synchronisation
12.4.2.1 Les mutex : Opérations sur un mutex
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex-
attr_t *mutexattr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
Permet de créer ou de détruire un mutex de manière dynamique. On alloue préallablement de la mémoire via malloc
avec pour argument sizeof(pthread_mutex_t). La création d’un mutex peit être paramétrée (sinon positionner le
2ème paramètre à NULL). Pour initialiser un mutex de manière statique on procède comme suit :
Le première bloque jusqu’à obtenir le mutex, la seconde le relâche, et la troisième tente d’obtenir le mutex et echoue
si elle ne le peut.
Permet la création ou la destruction d’une structure d’atrtibuts à passer lors de la création d’un mutex.
Le partage d’un mutex ebtre plusieurs processus se fait par l’intermédiairede ces fonctions. L’argument pshared peut
prendre la valeur PTHREAD_PROCESS_SHARED pour en activer l apartage ou la valeur PTHREAD_PROCESS_PRIVATE pour
en restreindre l’accès au processus l’ayant créé. Notons que la création d’un mutex doit se faire en mémoire partagée
si l’on souhaite activer l’option PTHREAD_PROCESS_SHARED.
Attention, sous linux cette fonctionnalité n’est disponible que depuis NTPL.
Les trois valeurs de protocol sont : PTHREAD_PRIO_NONE (pour aucun protocole), PTREAD_PRIO_INHERIT (protection
par héritage des priorités) et PTHREAD_PRIO_PROTECT (protection par inversion des priorités).
Le valeur prioceiling (de 1 à 127) doit correspondre à la priorité maximale des threads pouvant obtenir le mutex.
Cette valeur est ensuite utilisée par le protocole de protection d’inversion pour empêcher les problèmes.
Permet de créer ou dsétruire une variable de condition. En outre, sa création peut être paramétrée (sinon positionner
le 2ème argument à NULL). Pour initialiser une variable de condition de manière statique :
Ces deux fonctions permettent la synchronisation de plusieurs threads. Détaillons leur comportement.
La première débloque le mutex passé en paramètre (il faut que le thread appelant ait initialement le mutex), puis se
met en attente sur la variable de condition. C’est alors qu’un aure thread peut acquérir le mutex puis effectuer des
modifications sur une structure d’échange par exmeple, et finalement appelle pthread_cond_signal().
Le premier thread se réveillle et essaie d’obtenir le mutex (le réveil et le blocage sur le mutex se fait atomiquement
dans la fonction pthread_cond_wait()), en vain... Il faut d’abord que l’autre thread le relâche. Notons que si plusieurs
threads sont en attente sur une même variable de condition, alors la fonction pthread_cond_signal() n’en réveillera
qu’un . Pour réveiller tous les threads en attente, on utilise la fonction suivante :
Une alternative à la fonction d’attente est sa version temporisée. La fonction suivante attend jusqu’à la date précisée
en troisième argument. Pour obtenir la date actuelle on peut utiliser gettimeofday().
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t
*mutex, const struct timespec *abstime);
Notons qu’une demande d’annulation d’un thread en attente sur une variable de condition peut bloque indéfiniment.
Il faut d’abord que la fonction pthread_cond_wait() ait récupérer le mutex (état prévisible). Cependant, il ne faut
pas que le thread se termine avec le mutex bloqué. On utilise donc une fonction de nettoyage comme suit :
pthread_mutex_lock(&mutex);
pthread_cancel_push(pthread_mutex_unlock, (void *) &mutex);
while(condition_non_realisee)
pthread_cond_wait(&cond,&mutex);
pthread_cancel_pop(1);
On récupère ou on modifie soit la taille de la pile pour le thread que l’on souhaite créer, soit l’adresse de la base de la
pile.
L’attribut modifié ou récupéré concerne esssentiellement la priorité du thread. Le champ associé à la structure est
sched_priority, sa valeur peut aller de 1 (thread le moins favorisé) à 127 (thread le plus favorisé).
On choisit ou on récupère ici la politique d’ordonnancement. Trois politique sont prévues : SCHED_FIFO (premier arrivé,
premier servi), SCHED_RR (round robin, i.e. temps partagé), SCHED_OTHER (politique dépendant du constructeur).
On signale (ou on récupère) que l’ordonnancement est spécifique au thread créé (PTHREAD_EXPLICIT_SCHED), ou s’il
doit suivre la configuration du thread créateur (PTHREAD_INHERIT_SCHED).
Ces fonctions n’ont d’intérêt que pour un multithreading hybride. Le cas échéant, l’ordonnancement peut se faire au
niveau de chaque processus (PTHREAD_SCOPE_PROCESS), ou au niveau système (PTHREAD_SCOPE_SYSTEM) : la portée
du scheduler est donc globale.
12.5 Applications
Notons les point forts d’un programme multithreadé. En effet il permet :
– Sa décomposition en tâches n’ayant pas un rapport direct de causalité. La décomposition du programme en éléments
distincts facilite sa compréhension (au détriment d’une gestion plus compliquée), sa maintenabilité, sa flexibilité.
– Sa pleine utilisation du parallélisme d’une machine, ou à défaut du profit de la tranche totale de temps alloué au
processus par le kernel (utilistion au mieux de la CPU).
– Le partage de la mémoire et des fichiers ouverts du processus entre ses threads permet leur communication sans
recours au kernel. Les conséquences de ces rapports nous amènent trivialement à l’utilisation du multithreading dans
les serveurs.
Donnons un exemplede scénario possible. Un thread R s’occupe de la reception des requêtes émises par les clients. A
chacune d’entre elles un nouveau thread est créé pour la traiter, et peut même être dédié à une communication s’il y
a lieu avec le client. De cette manière, le thread R peut s’occuper de nouvelles demandes.
La connexion au serveur se fait toujours par le même canal de communcation (port). Les clients dialoguent ensuite
avec leurs threads dédiés par d’autres ports, lesquels sont discutés par les clients et le serveur avant le traitement de
leur requête.
Il est aussi possible de créer un pool (bassin, réserve) de threads, permetttant d’anticiper sur la demande et ainsi
de traiter plus rapidement les requêtes des clients. Un autre domaine dans lequel les threads sont courants est celui
des interfaces graphiques. Le dialogue avec l’utilisateur se fait par un GUI (graphique user interface). Le programme
associe aux actions de l’utilisateur (clic sur bouton, par exemple) une fonction appelée callback (fonction reflexe).
Il s’agit en fait d’un thread qui est créé lorsque l’évènement à lieu. De cette manière, l’utilisateur peut continuer à
interagir avec l’application (l’interface reste active, elle n’est pas gelée par l’exécution d’une requête). Le traitement
des demandes faites par l’utilisateur est donc encore une fois détaché de sa reception. Les bibliothèques graphiques
GTK+ et Qt utilisent ce principe.
Le multithreadiong est donc à envisager dans tous les systèmes concurrentiels, dès lors qu’il y a partage de ressources.
SYNOPSIS
#include <linux/sched.h>
#include <linux/unistd.h>
DESCRIPTION
clone is an alternate interface to fork, with more
options. fork is equivalent to clone(0, SIGCLD|COPYVM).
La bibliothèque LinuxThreads développée par Xavier Leroy ([email protected]) est une excellente implémen-
tation de la norme POSIX 1003.1c. Cette bibliothèque est basée sur l’appel système clone. Je ne saurais trop vous
conseiller d’utiliser ce produit, ce que nous ferons dans la suite des exemples présentés dans cet article.
yann@yoda:~/tp$ ./thread1
Thread 1: 0
Thread 2: 0
Thread 1: 1
Thread 2: 1
Thread 1: 2
Thread 2: 2
Thread 1: 3
Thread 2: 3
Thread 1: 4
Thread 2: 4
yann@yoda:~/tp$
yann@yoda:~/tp$ ./thread1_bis
Thread 1: 0
Thread 2: 0
Thread 1: 1
Thread 2: 2
Thread 1: 3
Thread 2: 4
yann@yoda:~/tp$
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
pthread_mutex_lock (&my_mutex);
for (i = 0 ; i != 5 ; i++)
printf ("read_process, tab[%d] vaut %d\n", i, tab[i]);
pthread_mutex_unlock (&my_mutex);
pthread_exit (0);
}
pthread_mutex_lock (&my_mutex);
for (i = 0 ; i != 5 ; i++) {
tab[i] = 2 * i;
printf ("write_process, tab[%d] vaut %d\n", i, tab[i]);
sleep (1); /* Relentit le thread d’ecriture... */
}
pthread_mutex_unlock (&my_mutex);
pthread_exit (0);
}
Compiler le programme.
Que se passe-t-il ? Pourquoi ?
Réponse :
La fonction pthread_mutex_lock verrouille le MUTEX pendant la durée du remplissage du tableau. Le thread de
lecture est contraint d’attendre l’appel à pthread_mutex_unlock pour verrouiller à son tour le MUTEX et lire le
tableau correct. A l’exécution on obtient :
yann@yoda:~/tp$ ./thread2
write_process, tab[0] vaut 0
write_process, tab[1] vaut 2
write_process, tab[2] vaut 4
write_process, tab[3] vaut 6
write_process, tab[4] vaut 8
read_process, tab[0] vaut 0
read_process, tab[1] vaut 2
read_process, tab[2] vaut 4
read_process, tab[3] vaut 6
read_process, tab[4] vaut 8
yann@yoda:~/tp$
yann@yoda:~/tp$ ./thread2
write_process, tab[0] vaut 0
read_process, tab[0] vaut 0
read_process, tab[1] vaut 0
read_process, tab[2] vaut 0
read_process, tab[3] vaut 0
read_process, tab[4] vaut 0
write_process, tab[1] vaut 2
write_process, tab[2] vaut 4
write_process, tab[3] vaut 6
write_process, tab[4] vaut 8
yann@yoda:~/tp$
L’activité appelante doit posséder le verrou mutex. L’activité est alors bloquée sur la variable condition après avoir
libéré le verrou. L’activité reste bloquée jusqu’à ce que la variable condition soit signalée et que l’activité ait réussi à
réacquérir le verrou.
Signale la variable condition : une activité bloquée sur la variable condition est réveillée. Cette activité tente alors de
réacquérir le verrou correspondant à son appel de cond_wait. Elle sera effectivement débloquée quand elle réussira à
réacquérir ce verrou. Il n’y a aucun ordre garanti pour le choix de l’activité réveillée. L’opération signal n’a aucun effet
s’il n’y a aucune activité bloquée sur la variable condition (pas de mémorisation).
Toutes les activités en attente sont réveillées, et tentent d’obtenir le verrou correspondant à leur appel de cond_wait.
Remarques : Les verrous sont par défaut des verrous d’exclusion mutuelle. En utilisant les attributs (non documentés
ici), on peut créer un verrou dit récursif qui peut être verrouillé plusieurs fois par la même activité (le verrou doit alors
être autant de fois déverrouillé avant qu’une autre activité puisse l’acquérir).
Par ailleurs, contrairement à la définition des moniteurs de Hoare, l’activité signalée n’a pas priorité sur le signaleur :
le signaleur ne perd pas l’accès au moniteur s’il le possédait, et le signalé reste bloqué tant qu’il n’obtient pas le
verrou. C’est pourquoi il est nécessaire d’utiliser une boucle d’attente réévaluant la condition d’exécution. En effet,
cette condition peut être invalidée entre le moment où l’activité est signalée et le moment où elle obtient effectivement
le verrou, par exemple si une autre activité obtient le mutex et pénètre dans le moniteur avant l’activité signalée.
Enfin, pour des raisons d’efficacité, il est courant de faire l’appel à cond_signal hors de la zone lock-unlock, de
sorte que l’activité signalée puisse acquérir plus facilement le verrou. Attention cependant à garantir l’atomicité des
opérations du moniteur !
Exemple : Considérons deux variables partagées x et y, protégée pat le mutex mu et la variable de condition cond
qui doit être signalée lorsque xdevient supérieure à y.
int x,y;
pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_lock(&mut);
while (x <= y)
{
pthread_cond_wait(&cond, &mut);
}
/* operate on x and y */
pthread_mutex_unlock(&mut);
Les modifications sur x et y qui pourraient faire quenx deviennent supérieure à y doivent signaler la condition si
nécessaire :
pthread_mutex_lock(&mut);
/* modify x and y */
if (x > y) pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mut);
S’il peut être prouvé qu’au moins un des threads appelant doit être réveillé (par exemple, s’il y a seulement 2
threads qui communique via x et y ; pthread_cond_signal peut être utilisée comme une alternative plus efficace
que pthread_cond_broadcast. Dans le doute on utilise pthread_cond_broadcast.
Pour attendre que x devienne supérieur à y avec un délai de 5 secondes, on fait :
struct timeval now;
struct timespec timeout;
int retcode;
pthread_mutex_lock(&mut);
gettimeofday(&now);
timeout.tv_sec = now.tv_sec + 5;
timeout.tv_nsec = now.tv_usec * 1000;
retcode = 0;
while (x <= y && retcode != ETIMEDOUT)
{
retcode = pthread_cond_timedwait(&cond, &mut, &timeout);
}
if (retcode == ETIMEDOUT)
{
/* timeout occurred */
}
else
{
/* operate on x and y */
}
pthread_mutex_unlock(&mut);
/*Compilation
gcc -D_REENTRANT -o exemple2 exemple2.c -lpthread
*/
#include <pthread.h>
#include <string.h>
/* Depose le message msg (qui est dupliqué). Bloque tant que le tampon est plein. */
void deposer (char *msg)
{
pthread_mutex_lock (&protect);
while (buffer != NULL)
pthread_cond_wait (&est_libre, &protect);
/* buffer = NULL */
buffer = strdup (msg); /* duplication de msg */
pthread_cond_signal (&est_plein);
pthread_mutex_unlock (&protect);
}
/* Renvoie le message en t^ ete du tampon. Bloque tant que le tampon est vide.
* La libération de la mémoire contenant le message est à la charge de l’appelant. */
char *retirer (void)
{
char *result;
pthread_mutex_lock (&protect);
while (buffer == NULL)
pthread_cond_wait (&est_plein, &protect);
/* buffer != NULL */
result = buffer;
buffer = NULL;
pthread_cond_signal (&est_libre);
pthread_mutex_unlock (&protect);
return result;
}
/* Initialise le producteur/consommateur. */
void init_prodcons (void)
{
pthread_mutex_init (&protect, NULL);
pthread_cond_init (&est_libre, NULL);
pthread_cond_init (&est_plein, NULL);
buffer = NULL;
}
/*-----------------------------------------------------------------
Fonction executee par les threads.
-----------------------------------------------------------------*/
void Thread_ret (void)
{
int i;
char message[25];
for (i=0;i<5;i++)
{
sprintf(message,"%d",i);
strcat(message, "--message--");
deposer(message);
printf("Depose message %d \n",i);
} pthread_exit(NULL);
}
/*Initialisation du producteur/consommateur*/
init_prodcons();
pthread_join(threads[1], NULL);
printf("------main thread %d (main) fin de : %d\n",
(int)thr_main, (int)threads[1]);
printf("------main Fin de main (%d)\n", (int)thr_main);
return 0;
}
Compiler le programme.
Que se passe-t-il ? Pourquoi ?
Réponse :
Correction
sem_wait suspend le thread appelant jusqu’à ce que le sémaphore pointé par sem ait un compteur non nul. Alors le
compteur du sémaphore est atomiquement décrémenté.
sem_trywait est la variante non bloquante de sem_wait. Si le sémaphore pointé par sem possède un compteur non
nul, le compteur est décrémenté atomiquement et sem_trywait retourne immédiatement 0. Si le sémaphore possède
un compteur nul, sem_trywait retourne immédiatement l’erreur EAGAIN.
sem_post incrémente atomiquement le compteur du sémaphore pointé par sem. Cette fonction n’est jamais bloquante
et peut être utilisée de manière sure par des gestionnaires de signaux asynchrones.
sem_getvalue stocke à l’endroit pointé par sval le compteur courant du sémaphore sem.
sem_destroy détruit un objet sémaphore, en libérant les ressources qu’il possédait. Aucun threads ne doit attendre le
sémaphore lorsque celui-ci est détruit. Dans l’implémentation LinuxThreads, aucune ressources n’est associée à l’objet
sémaphore, donc, pour l’instant, la fonction sem_destroy ne fait rien à part tester si aucun thread n’attend après
l’objet sémaphore.
L’utilisation de sémaphores permet aussi la synchronisation entre plusieurs threads. Voici un exemple simple :thread3.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
static sem_t my_sem;
int the_end;
void *thread1_process (void * arg)
{
while (!the_end) {
printf ("Je t’attend !\n");
sem_wait (&my_sem);
}
printf ("OK, je sors !\n");
pthread_exit (0);
}
void *thread2_process (void * arg)
{
register int i;
for (i = 0 ; i < 5 ; i++) {
printf ("J’arrive %d !\n", i);
sem_post (&my_sem);
sleep (1);
}
the_end = 1;
sem_post (&my_sem); /* Pour debloquer le dernier sem_wait */
pthread_exit (0);
}
main (int ac, char **av)
{
pthread_t th1, th2;
void *ret;
sem_init (&my_sem, 0, 0);
if (pthread_create (&th1, NULL, thread1_process, NULL) < 0) {
fprintf (stderr, "pthread_create error for thread 1\n");
exit (1);
}
if (pthread_create (&th2, NULL, thread2_process, NULL) < 0) {
fprintf (stderr, "pthread_create error for thread 2\n");
exit (1);
}
(void)pthread_join (th1, &ret);
(void)pthread_join (th2, &ret);
}
Compiler le programme.
Que se passe-t-il ? Pourquoi ?
Réponse :
Dans cet exemple, le thread numéro 1 attend le thread 2 par l’intermédiaire d’un sémaphore. Après compilation on
obtient la sortie suivante :
yann@yoda:~/tp$ ./thread3
Je t’attend !
J’arrive 0 !
Je t’attend !
J’arrive 1 !
Je t’attend !
J’arrive 2 !
Je t’attend !
J’arrive 3 !
Je t’attend !
J’arrive 4 !
Je t’attend !
OK, je sors !
yann@yoda:~/tp$
mémoire est libéré, ce qui termine les threads d’une manière violente. Il n’y a plus d’affichage. Une solution, vu que
les threads sont maintenant indépendant du programme principal, est de faire attendre ce dernier par l’intermédiaire
de la fonction sleep(6).
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
static sem_t my_sem;
int the_end;
void *thread1_process (void * arg)
{
while (!the_end) {
printf ("Je t’attend !\n");
sem_wait (&my_sem);
}
printf ("OK, je sors !\n");
pthread_exit (0);
}
void *thread2_process (void * arg)
{
register int i;
for (i = 0 ; i < 5 ; i++) {
printf ("J’arrive %d !\n", i);
sem_post (&my_sem);
sleep (1);
}
the_end = 1;
sem_post (&my_sem); /* Pour debloquer le dernier sem_wait */
pthread_exit (0);
}
main (int ac, char **av)
{
pthread_t th1, th2;
void *ret;
pthread_attr_t thread_attr;
sem_init (&my_sem, 0, 0);
if (pthread_attr_init (&thread_attr) != 0) {
fprintf (stderr, "pthread_attr_init error");
exit (1);
}
if (pthread_attr_setdetachstate (&thread_attr, PTHREAD_CREATE_DETACHED) != 0)
{
fprintf (stderr, "pthread_attr_setdetachstate error");
exit (1);
}
if (pthread_create (&th1, &thread_attr, thread1_process, NULL) < 0) {
fprintf (stderr, "pthread_create error for thread 1\n");
exit (1);
}
if (pthread_create (&th2, &thread_attr, thread2_process, NULL) < 0) {
fprintf (stderr, "pthread_create error for thread 1\n");
exit (1);
}
sleep(6);
}
yann@yoda:~/tp$ ./thread3_bis
Je t’attend !
J’arrive 0 !
Je t’attend !
Je t’attend !
J’arrive 1 !
Je t’attend !
J’arrive 2 !
Je t’attend !
J’arrive 3 !
Je t’attend !
J’arrive 4 !
Je t’attend !
OK, je sors !
yann@yoda:~/tp$
yann@yoda:~/tp$ ./thread4
Thread 1: 0
Thread 1: 1
yann@yoda:~/tp$
Pour éviter l’utilisation des cancellation points, on peut indiquer que la destruction est en mode asynchrone en modifiant
le code du thread de la manière suivante :
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *my_thread_process (void * arg)
{
int i;
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
for (i = 0 ; i < 5 ; i++) {
sleep (1); /*inversion des lignes permet de voir l’action*/
printf ("Thread %s: %d\n", (char*)arg, i); /* du PTHREAD_CANCEL_ASYNCHRONOUS*/
}
}
main (int ac, char **av)
{
pthread_t th1, th2;
void *ret;
if (pthread_create (&th1, NULL, my_thread_process, "1") < 0) {
fprintf (stderr, "pthread_create error for thread 1\n");
exit (1);
}
sleep (2);
if (pthread_cancel (th1) != 0) {
fprintf (stderr, "pthread_cancel error for thread 1\n");
exit (1);
}
(void)pthread_join (th1, &ret);
}
yann@yoda:~/tp$ ./thread5
Thread 1: 0
Thread 1: 1
yann@yoda:~/tp$
12.11 Excercice 1
Dans cet exercice, trois threads seront créés, et ils incrémenteront chacun une variable globale différente. La valeur de
chacune de ces variables est affichée par le programme initial.
Réponse :
#include <stdio.h>
#include <pthread.h>
int val[3];
void *thread(void *num)
{
while(1)
{
/* Incrementation de la valeur associée à cette thread */
val[(int)num]=val[(int)num]+1;
/*val[(int)num]++;*/
}
}
int main(int c, char *v[])
{
int num,i;
void *ret;
pthread_t pthread_id[3];
val[0]=-70000;val[1]=-1500;val[2]=20;
/* Création de 3 threads qui exécutent toutes la fonction thread
avec pour paramètre le numéro de la thread */
for(num=0;num<3;num++)
pthread_create(pthread_id+num,NULL,thread,(void *)num);
for(i=0;i<30;i++)
{/* affiche la valeur pour chacune des threads */
printf("%d %d %d\n",val[0],val[1],val[2]);
usleep(30);
}
exit(0);
}
yann@yoda:~/tp$ ./exercice
368193 27541 20
1625609 460719 387455
2718303 914582 829174
3873767 1383948 1218376
5031845 1815537 1649715
6215952 2233009 2089599
7243731 2618205 2509100
8425464 3080838 2898893
9528360 3554037 3350284
10577993 4003460 3830973
11736164 4469286 4228007
12807752 4933318 4668714
13955785 5384593 5095310
15116508 5824953 5509208
16208766 6260286 5971707
17285809 6643175 6436315
18407308 7060464 6838498
19526665 7477570 7298501
20607722 7930140 7740981
yann@yoda:~/tp$
12.12 Excercice 2
Dans cet exercice, il s’agit de mettre en pratique la communication entre threads via une variable de condition.
Dans notre exercice, 5 threads vont accéder à une ressource de 7 emplacements. Chacun des threads créés va demander
successivement 3 puis 2 puis 1 emplacements de la ressource. Après chaque accès aux ressources le thread attend une
seconde et libère le ou les emplacements accédés.
Il faut bien entendu mettre en attente active tout thread qui désire accéder à un ou plusieurs emplacement alors que
toutes les ressources sont accédées. Il faut donc mettre en place un mécanisme d’exlusion.
Réponse :
//compilation
// gcc -Wall partage_ressource.c -o partage -lpthread -D_REENTRANT
/*partage_ressource.c*/
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define MAX_RESSOURCE 7
#define NB_THREADS 5
pthread_mutex_unlock(&mutex);
}
int main()
{
int i;
void *retour;
pthread_t thread[NB_THREADS];
printf("Programme de gestion de ressources\n\n");
for(i=0;i<NB_THREADS;i++)
{
pthread_create(&thread[i],NULL, fonction, NULL);
}
for(i=0;i<NB_THREADS;i++)
{
/*on attend que les threads se terminent*/
/*on affiche ce qu’ils retournent*/
pthread_join(thread[i], &retour);
printf("thread main : le thread %d se termine
(result : %d)\n", (int) thread[i], (int) retour);
}
pthread_exit(NULL);
}
un petit exemple de session gdb sur le programme d’exemple thread1. La compilation s’effectue de la manière suivante :
gcc -ggdb -D_REENTRANT -o thread1 thread1.c
(gdb) b main
Breakpoint 1 at 0x8048622: file thread1.c, line 22.
On a posé un point d’arrêt dans le programme principal avant la création des threads.
(gdb) n
[New Thread 25572]
[New Thread 25571]
[New Thread 25573]
Thread 1: 0
Thread 1: 1
Thread 1: 2
Thread 1: 3
Thread 1: 4
(gdb) info threads
3 Thread 25573 0x4007a921 in __libc_nanosleep ()
* 2 Thread 25571 main (ac=1, av=0xbffffca0) at thread1.c:27
1 Thread 25572 0x4008b2de in __select ()
L’action sur next exécute la fonction pthread_create qui provoque la création du thread 1. La commande info threads
permet de connaı̂tre la liste des tous les threads associés à l’exécution du programme. Le thread courant est indiqué
par l’étoile, le numéro du thread est indiqué en deuxième colonne (ici 1, 2, 3).
(gdb) thread 1
[Switching to Thread 25654]
#0 0x4008b2de in __select ()
[Agi95] Axis & Agix. Unix Utilisation : Guide de Formation. EDITIONS LASER, 1995.
[Bla02a] CH. Blaess. Langages de scripts sous Linux. Eyrolles, 2002.
[Bla02b] CH. Blaess. Programmation Système en C sous Linux. Eyrolles, 2002.
[CH95] J.M. Champarnaud and G. Hansel. Passeport pour UNIX et C. Passeport pour l’informatique. Internationnal
Thomson Publishing, 1995.
[Cha94] P. Charman. Unix et X-Window : Guide Pratique. CÉPADUÈS ÉDITIONS, 1994.
[Pou94] T. Poulain. Cours unix. 1994.
[Sto92] Richard Stoeckel. Filtres et Utilitaires Unix. ARMAND COLIN, 1992.
171