INTRODUCTION AU SYSTEME UNIX

Chapîtres 4, 5, 6

4 Langage de Commande (Shell)
4.1 Commandes Externes et Commandes Internes
4.2 Shell, Login-Shell
4.3 Caractères Spéciaux
4.4 Expressions Rationnelles
4.4.1 Expressions Rationnelles Atomiques
4.4.2 Expressions Rationnelles
4.5 Le Login et l'Environnement Shell 4.6 Les Procédures et leurs Paramètres 4.7 Structures de Contrôle 5 Noyau Unix 6 Fichiers
6.1 Caractéristiques des Fichiers
6.2 Appels Systèmes Opérant sur les Fichiers
6.3 Communication entre Processus par Tubes
7 Processus
7.1 Caractéristiques des Processus
7.2 Gestion de Processus par le Système Unix
7.3 Appels Systèmes Opérant sur les Processus
7.3.1 Création de Processus
7.3.2 Fin d'un Processus
7.3.3 Attente de la Fin d'un Fils
7.3.4 Substitution
7.3.5 Quelques Primitives
7.4 Les Signaux
7.4.1 Caractéristiques des Signaux
7.4.2 Principaux Signaux
7.4.3 Appels Systèmes Associés aux Signaux

4 Langage de Commande (Shell)

4.2 Shell, Login-Shell

Le Shell est à la fois un langage de commandes et l'interpréteur de ce langage.

Il assure l'interface externe entre les utilisateurs et le système, mais ne fait pas partie du noyau.

Principaux langages de commandes sont disponibles sur les systèmes UNIX :

le Bourne shell (sh),
le C-shell (csh) basé sur sh,
tcsh, basé sur csh, qui est du domaine public.

.

.

suite

4.1 Commandes Externes et Commandes Internes

L'interpreteur du shell a la possibilité, soit d'exécuter lui même la commande demandée par l'utilisateur (commande interne), soit de lancer l'exécution de programmes existant par ailleurs pour ce faire (commandes externes).

À chacune des commandes externes correspond un fichier exécutable ayant comme nom le nom de la commande. Ce fichier se trouve souvent dans le répertoires /bin

L'exécution d'une commande externe entraîne la création d'un nouveau processus exécutant le programme correspondant à la commande.

Une commande externe peut être appelée à partir de n'importe lequel des langages de commandes disponibles, mais aussi à partir d'un programme utilisateur.

Chacun des langages de commandes dispose de ses propres commandes qui sont les commandes internes.

suite

4.3 Caractères Spéciaux

Certains caractères jouent un rôle particulier :

* représente toute chaîne de caractères ;
? désigne un caractère quelconque (. dans l'éditeur ed) ;
[...] désigne un caractère quelconque de l'ensemble spécifié dans .... Par exemple, [abc1] représente un caractère appartenant à {a, b, c, 1}. Des abréviations permettent de ne pas décrire explicitement tout l'ensemble : [0-9], [a-z], [A-Z] ;
> et < représentent des redirections d'entrées/sorties standard ;
| désigne un tube ;
; permet de construire une séquence ;
<CR> passe à la ligne de commandes suivante.

Si l'on désire utiliser ces caractères en tant que caractères normaux, il faut les faire précéder d'un caractère \ qui perd lui-même sa particularité lorsqu'il est doublé. Lorsqu'une référence de fichier commence par un ., ce premier caractère doit être spécifié explicitement car cette occurrence n'est pas couverte par la convention *.

suite

4.4 Expressions Rationnelles

Ces expressions permettent de désigner des ensembles de chaînes de caractères, en particulier dans des adressages et des substitutions. Elles peuvent donc être utilisées dans la plupart des commandes, à la place de chaînes de caractères.

4.4.1 Expressions Rationnelles Atomiques

Une expression rationnelle atomique est :

soit un caractère normal ;

soit un caractère spécial précédé de \ ;

soit l'expression ? représentant un caractère quelconque ;

soit une chaîne de caractères non vide placée entre [ et ].

.

suite

4.4.2 Expressions Rationnelles

Les commandes du shell n'utilisent que des expressions rationnelles atomiques. Les autres peuvent être utilisées par exemple pour effectuer des substitutions de chaînes de caractères avec l'éditeur sed.

Une expression rationnelle est :

une expression rationnelle atomique ;
une expression rationnelle atomique suivie du caractère d'itération * ;
la concaténation d'expressions rationnelles ;
une expression rationnelle atomique suivie de :
\{n\} avec 0 ¾ n < 256 pour exactement n occurrences de l'express;
\{n,\} pour au moins n occurrences de l'expression ;
{m,n\} pour un nombre d'occurrences de l'expression entre m et n ;
une expression comprise entre \( et \) pour délimiter des champs dans une ligne ;
\n désignant le n-ième champ défini par le mécanisme précédent.

Exemple :

La commande :

echo "traduction en javanais" | sed "s/\([aeiouy]\)/av\1/g"

renvoie :

travadavuctaviavon aven javavavanavaavis.

.

.

.

.

.

4.5 Le Login et l'Environnement Shell

Lorsqu'un utilisateur se loge, un processus shell est exécuté. De plus, certaines commandes, soit communes, soit propres à chacun, sont effectuées :

.profile,
.login
.cshrc

Ce type de commande permet l'initialisation de variables shell, soit pour leur donner une valeur différente de celle par défaut, soit pour en définir de nouvelles.

L'ensemble de ces variables constitue l'environnement shell.

Le nom d'une variable shell est une chaîne de caractères contenant des lettres, des chiffres ou le caractère _ et commençant toujours par une lettre.

La valeur d'une variable est une chaîne de caractères quelconque.

Les variables d'environnement existant toujours sont :

* PS1 : premier prompt ;

* PS2 : second prompt. Celui-ci est utilisé en particulier pour continuer une commande commencée mais pas terminée ;

* HOME : référence absolue du catalogue privé ;

* PATH : liste de chemins dans lesquels les commandes appelées vont être cherchées ;

* TERM : type du terminal utilisé.

Une affectation de la valeur val à la variable var s'effectue par :

var = val.

La valeur d'une variable var est obtenue en utilisant $var. Si la variable var n'a pas été définie, son contenu est la chaîne de caractères vide.

Des délimiteurs permettent d'effectuer des opérations à l'intérieur de chaînes de caractères :

* '...' : ... est pris tel quel, c'est-à-dire que si cette chaîne contient des appels à des variables, aucune substitution n'est effectuée ;

* "..." : les variables contenues dans ... sont substituées ;

* 'op' : évalue la commande shell op.

La commande echo permet d'afficher une chaîne de caractères. L'option -n évite le retour à la ligne après affichage de la chaîne de caractères.

.

.

.

.

4.6 Les Procédures et leurs Paramètres

Une procédure (ou script shell ) est une suite de commandes shell. Elle peut accepter des paramètres:

commande paramètre1 ... paramètren.

Le premier paramètre est référencé par $1, le second par $2, ..., le neuvième par $9.

$1, $2, ..., $9 : paramètres de la commande ;
$0 : nom de la commande appelée ;
$* : liste des paramètres ;
$# : nombre de paramètres ;
$$ : numéro du processus shell correspondant à la commande ;
$? : code de retour de la dernière commande exécutée.

Les variables utilisées dans une commande sont locales à celle-ci.

4.7 Structures de Contrôle

Affectation

set <chaîne de caractères> : la <chaîne de caractères> devient la nouvelle liste de paramètres.

read <liste de variables> : les variables prennent les valeurs fournies par l'entrée standard.

.

.

.

.

.

Conditionnelles :

if <liste de commandes 1>
then <liste de commandes 2>
else <liste de commandes 3>
fi

.

case <chaîne de caractères> in
<motif1> ) <liste de commandes 1> ;;
...
<motif n> ) < liste de commandes n> ;;
*) <liste de commandes pour les autres cas> ;;
esac

.

.

Répétitions
for <variable> in <liste de chaînes de caractères>
do <liste de commandes>
done
.
for <variable> in $*
do <liste de commandes>
done
.
while <liste de commandes 1>
do <liste de commandes 2>
done
.
until <liste de commandes 1>
do <liste de commandes 2>
done

.

Opérateurs logiques

Tests sur les chaînes de caractères

[ <chaîne de caractères 1> = <chaîne de caractères 2> ]

teste si <chaîne de caractères 1> et <chaîne de caractères 2> sont égales.

Attention : les [ et ] doivent impérativement être entourés de blancs ainsi que les primitives de comparaison.

Autres tests sur les chaînes de caractères

!= (différentes),

-n (non vide),

-z (vide)

.

Tests sur les valeurs numériques :
-eq (égales),
-ne (différentes),
-gt (strictement supérieure),
-ge (supérieure ou égale),
-lt (strictement inférieure),
-le (inférieure ou égale) ;

Tests sur les fichiers :

-d (catalogue),
-f (fichier),
-r (permissions de lire le fichier),
-w (permissions d'écrire le fichier),
-x (permissions d'exécuter le fichier).


* exit <valeur entière> termine le processus et renvoie la valeur passée en paramètre comme code de retour.

Exemple 1 : le fichier monscript1 a le contenu est le suivant :

set 'ls'
for i in $*
do
if [ -d $i ]
then echo "$i est un repertoire"
fi
if [ $i = "toto" ]
then echo "toto trouve. Voulez-vous voir son contenu ?"
read rep
case $rep in
o | O ) cat $i;;
n | N ) echo "pas de visualisation du contenu de toto";;
* ) echo "vous repondez vraiment n importe quoi"
esac
fi
done

Exemple 2 : monscript2 a le contenu est le suivant :

chaine=$1
ps | grep $chaine | grep -v grep |
while read pid reste
do
kill -9 $pid
done

L'exécution de ce fichier de commandes s'effectue en appelant monscript2 avec les paramètres souhaités.

par exemple : monscript2 pgm_qui_boucle.

Cette exécution tue tous les processus qui s'appellent pgm_qui_boucle.

.

.

5 Noyau Unix

Le système UNIX est avant tout une collection d'appels système sur les fichiers, processus, entrées/sorties, communications inter-processus, communications réseaux, etc. (documentés dans la section 2 du main)

Chaque appel se présente comme un appel de fonction.

Le traitement d'un appel systèmes provoque un passage en mode système (ou noyau).

Lorsque le traitement de l'exception se termine, le processeur repasse en mode utilisateur et on revient exécuter l'instruction se trouvant immédiatement derrière l'appel système.

La plupart des appels systèmes retournent une valeur qui indique le succès ou l'échec de l'opération demandée. En cas d'échec la cause exacte de l'échec peut être précisée par le contenu de la variable globale errno disponible en incluant le fichier <errno.h>. Celui-ci contient également la liste des codes d'erreur.

Le traitement demandé est réalisé par le processus lui-même ce qui évite de faire une commutation et simplifie beaucoup le passage de paramètres et de valeur de retour puisqu'on reste dans le même espace d'adresses.


* les appels systèmes sont exécutés en utilisant une pile d'exécution différente ;


* la priorité d'exécution est plus grande ;


* certaines interruptions peuvent être masquées.

.

.

.

6 Fichiers

6.1 Caractéristiques des Fichiers

Tout fichier est défini par un descripteur de fichier unique appelé i-noeud. Il contient les informations d'ordre général concernant le fichier :

taille ;
adresse des blocs utilisés sur le disque ;
identification du propriétaire ;
permissions d'accès ;
type de fichier (fichier ordinaire, catalogue, ...) ;
date de dernière modification ;
compteur de références à ce fichier dans un répertoire.

L'i-noeud ne contient aucun nom pour le fichier.

Un fichier n'est effectivement détruit (espace disque et i-noeud récupérés par le système) que si le compteur de références du i-noeud du fichier devient nul.

.

En plus des permissions d'accès, 3 autres bits ayant un rôle spécial sont utilisés pour chaque fichier :

* set-uid permet d'exécuter un programme avec les privilèges de son propriétaire et non pas ceux de l'utilisateur qui lance l'exécution.

Ce mécanisme est utilisé en particulier pour changer le mot de passe. Le bit set-uid se traduit lorsqu'il est positionné par une lettre s dans les permissions d'accès (affichées lors d'un ls -l) ;

* set-gid : même chose avec le groupe ;

* bit de collage (sticky bit) assure le maintien du programme en mémoire même lorsque aucun processus actif ne correspond à une exécution du programme.

Un ficher régulier UNIX peut être considéré comme un tableau de caractères et les opérations de lecture/écriture se font à partir d'un "pointeur de position" qui pointe sur le premier caractère lors de l'ouverture et est ensuite avancé au fur et à mesure des lectures et des écritures.

Tables des fichiers ouverts

Toute utilisation d'un fichier commence par une ouverture qui renvoie un "file descriptor" traduit ici par "numéro de fichier ouvert" qui n'est que le numéro de l'entrée qui contient un pointeur vers l'inoeud du fichier, dans la table des fichiers ouverts.

Après ouverture, un programme désigne un fichier non plus par une référence relative ou absolue mais par le numéro de l'entrée dans la table.

Avantage : inutile de retraduire le nom a chaque opération sur le fichier.

6.2 Appels Systèmes Opérant sur les Fichiers

* int open(char *référence ,int mode, int permis)

Si le fichier désigné par la référence relative ou absolue référence existe alors il est ouvert et permis peut être omis.

mode indique le type d'accès : O_RDONLY, O_WRONLY et O_RDWR pour respectivement : lecture seulement, écriture seulement et lecture/écriture.

La fonction open retourne le numéro de fichier ouvert en cas de succès et -1 sinon.

Le pointeur de position du fichier pointe sur le premier octet du fichier.

Exemple :

int nf;

nf = open("projet.pas", O_RDWR);

Appels Systèmes Opérant sur les Fichiers (suite)

* int close (int no_fichier);

Ferme le fichier de numéro no_fichier.

L'entrée dans la table des fichiers ouverts est libérée et peut être réutilisée ultérieurement lors de l'ouverture d'un autre fichier.

La fonction close retourne 0 si l'opération se termine normalement et -1 sinon.

Exemple :

int nf,ff;

nf = open("projet.pas", O_RDWR);

ff = close(nf);

.

Appels Systèmes Opérant sur les Fichiers (suite)

* int read(int no_fichier, char *adr_zone, int nb);

Essaye de lire nb octets à partir de la position indiquée par le pointeur de position du fichier désigné par no_fichier et les copie à l'emplacement désigné par adr_zone.

La valeur retournée est le minimum de nb et de la quantité effectivement disponible et est égale à zéro si la fin de fichier est atteinte.

Le pointeur de position du fichier est augmenté de la quantité lue.

Exemple :

int nb;
char c,tab[100];
nb = read(0,&c,1);
nb = read(nf,tab,sizeof tab);

Appels Systèmes Opérant sur les Fichiers (suite)

* int write(int no_fichier, char *adr_zone, int nb);

Transfère nb caractères depuis l'adresse désignée par adr_zone à l'endroit pointé par le pointeur de position du fichier no_fichier.

Retourne le nombre de caractères transférés dans le fichier et -1 en cas de problème.

Exemple :

int nb;

char c,tab[8]="bonjour";

nb = write(1,&c,1);

nb = write(nf,tab,sizeof tab);

Appels Systèmes Opérant sur les Fichiers (suite)

* long lseek(int no_fichier, long déplacement, int drapeau);

Déplace le pointeur de position du fichier désigné par no_fichier de déplacement octets par rapport à la position courante, par rapport au début du fichier, ou par rapport à la fin du fichier suivant que drapeau est égal respectivement à 0, 1, 2.

Retourne la valeur du pointeur de position après l'opération et -1 en cas d'erreur.

Le début du fichier correspond à la valeur 0.

.

.

.

Autres fonctions systèmes utilisées pour les manipulations sur les fichiers :

creat : création d'un fichier.

link : création d'un lien physique sur un fichier.

unlink : suppression d'un lien physique sur un fichier et suppression du fichier si c'était le dernier lien existant.

dup : duplication d'une entrée dans la table des fichiers ouverts dans la première entrée disponible.

chdir : changement de répertoire de travail.

chown : changement de propriétaire de fichier.

chmod : changement de permission d'accès.

flush : vide un tampon d'entrée/sortie.

stat : donne attributs d'un fichier (type, propriétaire, permission d'accès, etc).

umask : modification du "masque" des permissions d'accès utilisé lors des créations de fichiers.

mknod : création d'un répertoire.

mount : montage d'une arborescence d'un volume amovible.

tar : création, modification et lecture d'archives.

6.3 Communication entre Processus par Tubes

Un tube est un fichier sans nom géré par le système

int pipe (int no_fichier[2]);

créé et ouvre le tube à la fois en lecture et en écriture et rend deux numéros de fichiers ouverts, l'un correspondant à l'ouverture en lecture et l'autre à l'ouverture en écriture.

Retourne 0 si la création du tube a bien eu lieu et -1 sinon.

Exemple :

int retour, no_fichier[2];
retour = pipe(no_fichier);

Un tube ne peut être utilisé que par le processus qui l'a créé ou par ses descendants (du fait de son absence de nom).

L'écriture et la lecture dans un tube se font en utilisant les appels normaux read() et write() avec comme paramètre le numéro de fichier correspondant.

Tubes (suite)

Il n'y a qu'un seul pointeur de position pour les lectures, qui est partagé par tous les processus "lecteurs".

De même il n'y a qu'un seul pointeur de position en écriture qui est partagé par tous les processus "écrivains".

Pour assurer le caractère FIFO, les modifications de pointeur de position par lseek() sont interdites.

tube vide : pointeur de lecture est égal au pointeur d'écriture.
tube plein : pointeur d'écriture -pointeur de lecture = max.

Lors d'une lecture, le nombre d'octets effectivement lus est le minimum du nombre demandé et du nombre disponible. Le système se charge de suspendre tout processus qui tente de lire dans un tube vide jusqu'à l'introduction de nouveau octets ou la fermeture définitive du tube en écriture. De même pour l'écriture.

Tubes (suite)

Une "extrémité" d'un tube peut être fermée par appel de close() sur le numéro de fichier ouvert correspondant.

Un tube fermé à une extrémité par un processus ne peut être ré-ouvert par ce même processus (absence de nom de tube).

tube définitivement fermé en écriture : tous les processus qui possédaient le numéro de fichier ouvert en écriture l'ont fermé en écriture.

tube définitivement fermé en lecture : tous les processus qui possédaient le numéro de fichier ouvert en lecture l'ont fermé en lecture.

Écrire dans un tube définitivement fermé en lecture n'a pas de sens. Par conséquent toute écriture dans un tube définitivement fermé en lecture provoque l'envoi d'un signal "tube fermé" (SIGPIPE) au processus demandeur. Sauf mention contraire ce signal provoque sa mort (voir signaux).