Vous êtes sur la page 1sur 48

L'Environnement UNIX 

M. SELLAMI
Guides d’études
 
1. Présentation générale du système UNIX 
2. Le Système de fichiers 
3. Le langage de commande SHELL 
4. Les filtres (Recherche et Edition) 
5. Utilisation du noyau UNIX
6. La Communication sous UNIX
1. Présentation générale du système UNIX
1.1. Historique
Le système d'exploitation UNIX est apparu sur une machine PDP-7 (DEC) aux Laboratoires Bell en
1969. Ken Thompson, avec le concours actif de Rudd Canaday, Doug Mc Ilroy, Joe Ossanna, et
Dennis Ritchie, a écrit un noyau de système d'exploitation en temps partagé assez agréable à utiliser
pour susciter l'enthousiasme des utilisateur, et rendre crédible l'achat d'une machine plus importante
- un PDP-11/20.
Un des premiers utilisateurs fut Ritchie qui aida à porter le système sur le PDP-11 en 1970. Ritchie
a également conçu et réalisé un compilateur du langage C. En 1973, Thompson et Ritchie on écrit le
noyau d'UNIX en C.
Unix fut conçu à l'origine comme un système d'exploitation destiné à supporter un bon système de
fichiers, muni de nombreux outils de manipulation de fichiers.
Thompson et Ritchie, ses auteurs, n'ont pas cherché à innover, mais ils ont au contraire appliqué les
bonnes idées déjà développées dans d'autres systèmes (MULTICS à MIT).
Deux filières dominent le marché:
- AT&T avec la version System V
- l'Université de Berkeley, avec la version actuelle BSD4.4
Les raisons du succès d'UNIX sont diverses. D'abords il est écrit en C, et donc est facilement
portable sur une large gamme d'ordinateurs, ce qui constitue un avantage commercial indéniable.
Ensuite, les sources du système sont disponibles, ce qui permet des adaptations faciles aux besoins
propres des utilisateurs. Enfin , et c'est certainement le plus important, c'est un bon système
d'exploitation en particulier pour les programmeurs : l'environnement UNIX est particulièrement
riche et fécond. L'idée maîtresse d'UNIX est que la puissance d'un système provient plus des
relations entre programmes que des programmes eux-mêmes. Beaucoup de programmes UNIX
font, de façon isolée, des traitement triviaux; combinés avec d'autres, ils deviennent des outils
généraux et performants.

1.2. Le phénomène de standardisation


L'effort de normalisation de UNIX est la conséquence d'une pression sur les constructeurs par les
utilisateurs (/USR/GROUP). On a vu donc émerger des spécifications qui, sans être des normes au
sens classique du terme devaient tout au moins constituer une base de garantie de la portabilité
d'applications d'une machine à l'autre. Parmi ces spécifications on peut citer:
• POSIS (Portable Operating System for Computer Environnement) publiée par l'IEEE et issu
d'un document de l'association /usr/group produit 1983.
• X/OPEN (1984): spécification issues d'une association du même nom créée par les
constructeurs européens et dont tous les autres constructeurs sont membres.
• SVID (System V Interface Definition) dont la première version est sortie en 1984 et qui est
d'origine AT&T.
Pour des raisons industrielles et commerciales une normalisation internationale a été menée par
deux groupes : le groupe OSF (Open Software Foundation) qui regroupe les constructeurs (IBM,
DEC, BULL, HP...) impose comme standard UNIX, AIX/OSF (UNIX d'IBM), et le groupe
ARCHER (AT&T) ou UNIX International pour lequel le standard devait être UNIX SYSTEM V
Release 4 ( version de convergence 4.3 BSD, XENIX, SUNOS et UNIX SYSTEM V Release 3).
La tendance vers une version standard de UNIX et vers une uniformisation des environnements est
certaine. Elle est souhaitée par les sociétés de développement de logiciels et par les utilisateurs des
systèmes informatiques.

2
1.3. Caractéristiques essentielles
Le système Unix est organisé en couches: autour du matériel, la première couche fournit les pilotes
qui réalisent les échanges avec les périphériques et les opérations élémentaires pour gérer la
mémoire; la deuxième couche est composée essentiellement de 2 grandes parties : le système de
fichiers, incluant la gestion des entrées-sorties, et la système de gestion des taches, incluant la
gestion de la mémoire; la troisième couche est composée des différents outils accessibles par des
commandes. Les deux premières couches constituent le noyau du système. Petit dans les premières
versions le noyau tend à grossir au fur et à mesure des adjonctions faites, essentiellement dans le
système de gestion des tâches (nouveaux moyens de communication).
UNIX est un système multitâches : à un instant donné, plusieurs tâches s'exécutent logiquement en
parallèle . C'est un système multi-utilisateurs : pour pouvoir travailler sur un système UNIX, un
utilisateur doit être enregistré, et plusieurs utilisateurs peuvent travailler simultanément. La gestion
de l'unité centrale est réalisée selon le principe du temps partagé: chaque tâche reçoit à tour de rôle
un quantum de temps pour s'exécuter. Sur les matériels modernes , UNIX gère la mémoire par le
mécanisme de pagination et de chargement de pages à la demande. Cependant, il gère également un
cache, partie réservée de la mémoire pour stocker les derniers blocs résultant d'échanges avec le
disque , à des fins d'optimisation de ces échanges.
Les entrées-sorties sont banalisées: écrire sur un fichier, sur un périphérique, sur une prise réseau
ou sur l'entrée d'une tâche se fait toujours d'une façon identique. Cette unification est réalisée par
le système de fichiers, et les différences sont traitées au dernier moment, par la couche la plus
interne du noyau.
L'utilisateur d'un système UNIX a deux façons pour accéder aux services offerts par l e système :
soit il écrit des programmes qui font des appels au noyau (les sous-programmes du noyau ont une
interface décrite en C dans les manuels UNIX, mais on peut les appeler depuis quel langage
classique), soit il utilise l'interface-utilisateur fourni par le langage de commande. UNIX comprend
plusieurs langages de commande; on les appelle des shells (parcequ'ils entourent les outils
disponibles).C'est à l'usager de choisir son shell, mais il peut en charger en cours de session.
Le Bourne-Shell est le plus ancien: c'est aussi le plus universel. Souvent, c'est le seul qui soit
disponible ; on parle dans ce cas simplement de Shell. Il y a une variante , le Korn-Shell, qui est une
extension du Bourne-Shell. Le C-Shell adopte la langage C, d'où son nom. Historiquement, il est
né après le Bourne-Shell; ses facilités interactives ont repensées dans le Korn-Shell. Le C-Shell a
été développé par la filière BSD, mais on le trouve également sous System V. Sa variante, le T-shell,
est purement BSD.
Le shell est un véritable langage de programmation, mais il n'est pas compilé: on peut donc écrire
des programmes qui font appel aux commandes, mais ces programmes ne peuvent pas appeler les
sous-programmes du noyau. Un programme écrit en shell est interprété : chaque ligne du
programme est analysée puis immédiatement exécutée.
Un programme shell est interprété interactivement ou en mode détaché.
Il existe 2 sortes d'utilisateurs pour le système UNIX donné:
- l'administrateur du système: c'est un usager particulier qui a tous les droits, et qui est responsable
du bon état du système. Son nom est root;
- les autres utilisateurs : c'est l'administrateur qui enregistrent un nouvel usager. Les usagers sont
rassemblés en groupes. Les usagers d'un même groupe peuvent avoir des accès particuliers sur les
fichiers appartient aux utilisateurs du groupe. En général un groupe rassemble les personnes qui
travaillent sur un même projet. Pour changer de groupe, il faut le demander à l'administrateur.

3
1.4. Le Shell
Quand le système imprime le caractère d'appel ($), les commandes que vous tapez ne sont pas
traités directement par le noyau mais par un intermédiaire qu'on appelle interpréteur de commande
(le shell). Le shell est un programme ordinaire comme date ou who, bien qu'il puisse faire des
choses remarquables. Les 3 fonctions intéressantes sont :
• Les noms de fonctions abrégés: on peut désigner un ensemble de fichiers pour une commande
par une définition générique - le shell recherche lui-même la totalité des fichiers désignés.
• La redirection des entrées-sorties: on peut demander à ce que la sortie d'un programme aille
dans un fichier plutôt que sur l'écran, ou bien à ce que l'entrée d'une commande vienne d'un
fichier plutôt que du clavier. On peut également lier les entrées et les sorties de programmes
entre elles.
• Créer un environnement sur mesure: on peut définir ses propres commandes.
1.5. Les processus
1.5.1. Définitions
Une tâche est appelée un processus sous UNIX. Un processus est une activité autonome qui exécute
un programme : c'est une entité dynamique qui n'ait, qui vit en toute indépendance ou en
communiquant avec d'autres processus , qui à son tour peut créer des processus, et enfin qui meurt.
Le système crée automatiquement , à la connexion d'un usager, un processus qui exécute
l'interpréteur de commande choisi préalablement: ce processus va vivre jusqu'à ce que l'usager se
déconnecte, et il va créer durant la session autant de processus que de commandes que l'usager
demande à exécuter.
Un processus est caractérisé par un certain nombre d'informations stockées dans une table du
noyau; en particulier, un processus a un numéro(PID), un père (PPID), un propriétaire (UID), et un
groupe propriétaire (GID).
1.5.2. Fichiers standards
A sa création, tout processus est muni de trois organes de communication avec ce qui l'entoure: ce
sont les fichiers standards. Un processus lit ses données dans le fichier standard d'entrée
(caractérisé par le numéro 0), écrit ses résultats dans le fichier standard de sortie (numéro 1), et
envoie ses messages d'erreurs sur le fichier standard d'erreur (numéro 2). Le processus shell né à
la connexion d'un usager est telle que le fichier standard d'entrée est connecté au clavier de l'usager,
et que les fichiers standards de sortie et d'erreurs sont connectés à l'écran du terminal.
sortie standard
entrée standard 1 1
0
P1 2 écran
P2
erreur standard clavier 0 2

Processus Théorique Un processus fils du shell initial


Au moment de la création d'un processus, on peut rediriger ses entrées_sorties standards: cela
signifie que l'on connecte les fichiers standard vers d'autres organes, comme les fichiers.

1 1 fs
fs fi
P3 écran 0 P4 2
fe
clavier 0 2

sortie standard redirigé les entrées-sorties standards redirigées


La redirection est indiquée dans le shell par les caractères > et >.
$ echo ceci est mon message affiche un message sur la sortie standard
$ echo ceci est mon message >f le message sert à initialiser le fichier f.
Différentes formes de redirection sont possibles:

4
1>f ou >f redirection de la sortie standard sur f, avec création de f s'il n'existait pas, ou
écrasement préalable de f;
2>f idem, pour rediriger l'erreur standard;
1>>f ou >f redirection de la sortie standard sur f, avec création de f s'il n'existait pas;
2>>f idem, pour rediriger l'erreur standard;
0<f ou <f redirection de l'entrée standard à partir de f;
$ ls >fichier.liste écrie la liste des fichiers du répertoire dans le fichier fichier.liste
$ cat f1 f2 f3 >>temp copie f1 f2 f3 dans temp
$ mail marie eric phil marc <let envoye un message à des personnes provenant du fichier let
$ sort <temp trie le contenu du fichier temp
1.5.3. Composition de processus
Soient P1, P2 et P3 trois processus crées par le shell pour exécuter des programmes C1, C2 et C3.
On distingue trois moyens pour composer ces processus:
• La composition séquentielle (;): se note, C1;C2;C3
les processus relatifs à P1, P2 et P3, relatifs à C1 C2 et C3, seront enchaîner d'une façon
indépendante et en séquence, le déroulement du premier n'influe pas le second ...
$ date; wc <fff; who am i; echo fin de commande composee>toto
Mon Sep Jan 27 19:08:47 PST 1993
66 45 23
jmr tty9 Jan 27 17:55
$ cat toto
fin de commande composee
• La composition parallèle de processus indépendants (&): se note, C1&C2&C3
les trois processus P1, P2 et P3 sont crées simultanément et s'exécutent en parallèle. Cependant, les
processus P1 et P2 s'exécutent en arrière-plan: ils sont détachés du clavier, et l'utilisateur ne peut
pas leurs communiquer de données. Le processus P3 s'exécute lui en mode interactif. Si P1 et P2
sont privés du clavier , ils partagent avec P3 l'écran: s'ils ne sont pas redirigés, leurs sortie et erreurs
standards sont envoyés sur le même écran.
$ wc ch* >wc.out le commande wc s'exécutera pendant que l'usager à la main
2145
$ lp ch*& cc code.c les fichiers aynat pour racine ch seront imprimés en arrière plan, la
compilation en mode intéractif
• La composition parallèle de processus communicants (|): se note, C1|C2|C3
exprime qu'un tube relie la sortie de P1 avec l'entrée de P2, et qu'un autre tube relie la sortie de P2
avec l'entrée de P3. Les 3 processus sont lancés sont lancés en, parallèle, et se synchronisent
automatiquement par leur flots de d'entrées-sorties.
0
P1 1 0 P2 1 0 P3 1
2 2 2
Trois processus parallèles communicants
$ who|sort liste des utilisateurs par ordre alphabétique
$ who|wc -l nombre d'utilisateurs connéctés a usystème
$ ls | pr -3 liste des fichiers en 3 colonnes
1.5.4. Les variables
Pendant l'exécution d'un programme, un processus doit conserver des informations dans des
variables. Le segments de données d'un processus contient les variables globales (ou communs) et
les variables utilisées dans des programmes écrits en shell. Lorsqu'un processus crée un fils, celui-ci
hérite de ces deux types de variables jusqu'au moment où ils sont recouverts par les variables de la
commande que le fils doit exécuter. Une zone particulière, appelée l'environnement du processus
contient des variables qui seront héritées par le processus fils. L'environnement permet donc à un
processus père de stocker des informations qui seront connues du fils.

5
2. Le Système de fichiers
2.1. Notion de fichier
Un fichier est une suite non structurée, stockée sur une mémoire auxiliaire. Le système fournit 2
modes d'accès : l'accès séquentiel, pour accéder au caractère suivant, et l'accès direct, pour accéder
au caractère de numéro donné. Par contre les programmes UNIX peuvent construire des fichiers
structurés, des modes d'accès complexes: UNIX ne donne ici que le moyen de stockage de
l'information. Un fichier qui contient du binaire (données binaires, code machine), est toujours
considéré comme étant un fichier de caractères.
Cependant, le système distingue plusieurs sortes de fichiers:
• les fichiers ordinaires, ceux que les utilisateurs déclarent pour stocker leurs programmes et
leurs données;
• les répertoires, qui permettent de regrouper des fichiers et de les désigner de façon simple et
homogène;
• les fichiers qui sont des périphériques à accès par caractère, comme un terminal;
• les fichiers qui sont des périphériques à accès par bloc, comme un disque;
• les fichiers qui contient des références vers un autre volume et qu'on appelle des liens
symboliques
• les fichiers qui sont des tubes mettant en communication deux processus
• les fichiers qui sont des prises réseaux, pour accéder à des fichiers éloignés.
2.2. Caractéristiques d'un fichier
Les caractéristiques statiques d'un fichier (celles qui ne dépendent pas de son accès à un instant
donné) sont stockées dans un descripteur appelé un i-noeud. Un i-noeud (inode) est un tableau qui
contient les informations suivantes :
• le type de fichier (ordinaire, répertoire, ...);
• sa taille, en octets;
• l'identification de son propriétaire: le couple UID-GID qui donne le numéro du propriétaire et le
numéro de son groupe;
• trois dates : date de création, la date de dernière modification et la date de dernière consultation;
• un compteur de référence sur le i-noeud (liens);
• la liste des blocs contenant l'information sur le disque.
Un i-noeud peut être examiné par la commande ls: elle donne accès au type, à la taille , au
propriétaire, au groupe, aux droits d'accès, aux nombres de liens, au numéro du i-noeud et permet
des classement sur la date de modification ou d'utilisation. La commande du donne le nombre de
blocs utilisés par le fichier. Les attributs d'identifications et le droit d'accès peuvent être modifiés
par les commandes chown, chgrp et chmod, et les dates par la commande touch.

2.4. Désignation d'un fichier : les répertoires


La correspondance entre désignation externe (identificateur) et désignation interne (n° de i-noeud)
est réalisée par les répertoires . Un répertoire (catalogue) est un fichier particulier : c'est en fait un
tableau à deux colonnes, l'une contenant les noms des fichiers, l'autre leur numéro de i-noeud.
Ainsi, pour désigner de façon externe un fichier, il suffit de désigner le répertoire dans lequel son
nom se trouve, puis son nom. Comme le répertoire est lui-même un fichier, il possède une référence
interne et une désignation externe: on commence donc par nommer le répertoire qui contient le nom
du répertoire qui ... L'origine de cette chaîne de désignation est un répertoire appelé la racine du
système de fichiers; c'est le seul répertoire connu directement par le système. Son numéro de i-
noeud a pour valeur 2.

6
La méthode de désignation que l'on vient de décrire définit une arborescence, dans laquelle les
noeuds sont les répertoires, et les feuilles, les fichiers. Pour un système de fichiers donné, un fichier
a donc 2 repérages :
„ son numéro de i-noeud;
„ la liste des noms de répertoires qui conduisent à son propre nom: ce repérage est dit "nom
absolu" du fichier.
Les différents shells adoptent tous la même convention de nommage externes fichiers: la racine de
l'arborescence a pour nom la barre oblique (caractère /), et on établit la liste des répertoires
composant le nom en faisant suivre d'une barre oblique chaque nom de répertoire. Enfin, tout
répertoire contient par défaut 2 entrées: point (caractère .), désigne le répertoire lui-même, et deux
points (.. ), désigne le répertoire père

2.4. Les permissions


Chaque fichiers possède un ensemble de permissions, qui déterminent qui peut faire quoi avec le
fichier. Si vous désirez ranger vos lettres d'amour dans un ensemble hiérarchisé de répertoires, peut-
être ne souhaiterez-vous pas voir d'autres personnes les lire! Vous pourrez ainsi changer les
permissions sur chaque lettre pour éviter les commérages ou seulement sur quelques-unes pour les
encourager! Il est également possible de changer la permission du répertoire lui-même pour
décourager les curieux.
Attention ! Le super utilisateur peut lire et modifier tous les fichiers du système. La commande su
vous confère les droits du super utilisateur si vous connaissez le mot de passe de root.
Le fichier /etc/passwd est le fichier des mots de passe. C'est un fichier de texte ordinaire qui
contient toutes les informations de connexion de chaque utilisateur.
$ grep vous /etc/passwd
vous : gkmbCTrJ04COM:604:1:anonyme: /usr/vous:
$
Les champs de ce fichier sont séparés par deux points :
nom:mot-de-passe-crypté:uid:groupe-id:divers:répertoire:shell
Le noyau vous accorde le droit de lire /etc/passswd en regardant les permissions associés au fichier.
Il y a trois types de permissions associées au fichier : lecture (examen du contenu), écriture
(modification), et exécution (le faire exécuter comme un programme). De plus, différentes
permissions sont attribuées aux utilisateurs. Comme propriétaire du fichier, vous possédez un jeu
de droits de lecture, d'écriture, et d'exécution; votre "groupe" en possède également ; les "autres" en
possède un troisième. L'option -l de ls imprime les permissions :
$ ls -l /etc/passwd
-rw-r--r-- 1 root 5115 Aug 30 10 10:40 /etc/passwd
$ ls -lg /etc/passwd
-rw-r--r-- 1 root 5115 Aug 30 10 10:40 /etc/passwd
Le fichier /etc/passwd appartient au nom de connexion root, du groupe adm , il a une taille de 5115
octets, il a été modifié pour la dernière fois le 30 Août à 10h40, et possède un lien .
La chaine -rw-r--r-- est la représentation par ls des permissions. le premier - indique qu'il s'agit
d'un fichier ordinaire, d pour un répertoire. Les 3 caractères suivants représentent les droits du
propriétaire (uid). rw- signifie que root (le propriétaire) peut lire ou écrire mais pas exécuter ce
fichier. Un fichier exécutable possède un x à la place d'un -.
Les trois caractères suivants (r--) repésentent les permissions du groupe. Les membres du groupe
adm ne peuvent que lire ce fichier. Les trois derniers (r--) définissent les permissions des autres
utilisateurs. Sur cette machine, seul root peut changer les informations de connexion d'un
utilisateur, mais chacun peut lire le fichier. Le programme qui change les mots de passe est passwd;
il se trouve certainement dans /bin:
$ ls -l /bin/passwd
-rwsr-xr-x 1 root 5115 Aug 30 10 10:40 /etc/passwd
7
Les permissions indiquent ici que chacun peut exécuter ce fichier, mais que seul root peut le
changer. Le s à la place du x dans les permissions du propriétaire indique que, lorsque la commande
est exécutée, elle doit obtenir les permissions associées à son propriétaire (root). Comme
/bin/passwd s'exécutera avec l'uid de root, chacun peut utiliser passwd pour modifier /etc/passwd.
$ ls -ld . ls demande des informations sur le répertoire lui-même
drwxrwxr-x 1 root 5115 Aug 30 10 10:40 etc
Le premier d indique que '.' est bien un répertoire. Le r indique qu'il est possible de lire ce
répertoire (il est possible d'utiliser ls ou od, par exemple). Un w indique qu'il est possible de créer
ou de détruire des fichiers dans ce répertoire, puisque ces opérations impliquent des modifications
du répertoire.
La permission d'effacer un fichier est indépendante du fichier lui-même. Si vous possédez la
permission w dans un répertoire, il vous est possible de détruire des fichiers, même si ceux-ci sont
protégés en écriture.
La permission x pour un répertoire ne signifie pas exécution, mais autorise une recherche dans ce
répertoire. Il est possible de créer un répertoire avec les permissions --x pour les autres utilisateurs
qui pourront alors exécuter les fichiers de ce répertoire s'ils les connaissent, mais ne pourront pas
les connaître par les commandes du système telles que ls. De même avec les permissions r--, il est
possible de voir ce qu'il y a dans le répertoire mais impossible de l'utiliser.
Certaines versions de ls donnent le propriétaire et le groupe en un seul appel.
La commande chmod change les permissions d'un fichier.
$ chmod permissions fichiers ...
la syntaxe de permissions peuvent être données de 2 façons :
- soit un nombre octal , de la forme
chmod nnn fichiers 4:"read", 2:"write", 1:"execute"

$ chmod 666 essai # essai peut être lu, écrit par tout le monde
Les trois chiffres spécifient, comme dans ls, les permissions du propriétaire, du groupe, et des
autres.
- soit sous forme symbolique chmod [qui] position [droit]+ fichiers...

qui :: u|g|o|a
position :: +|-|=
droit :: r|w|x
$ chmod +x commande # permet à tout le monde d'exécuter commande
$ chmod -w fichier # empêche tous les utiulisateurs d'écrire le fichier.
Seul le propriétaire d'un fichier peut en changer les permissions (mis à part le super utilisateur, bien
sûr).

8
3. Le langage de commande SHELL
3.1. Introduction
Les services offerts par le système UNIX sont accessibles par l'une ou l'autre méthodes suivantes:
• ou bien le service se présente comme un sous-programme du noyau UNIX, et dans ce cas là, on
doit l'appeler depuis un programme (écrit en C, PASCAL ou dans un autre langage à compiler).
• ou bien le service se présente comme un programme exécutable, et on l'active en appelant le
chargeur du système. Ce chargeur est accessible à travers un interpréteur de commande .
Cet interpréteur qui entoure virtuellement l'ensemble des commandes, est appelé un shell. Il
interprète les commandes que lui fait l'utilisateur. Dans UNIX, le langage de commande
(interpréteur de commande) n'est pas unique: l'utilisateur peut choisir parmi plusieurs. Nous
présentons ici le Bourne-Shell.
Le langage Bourne-Shell est un langage algorithmique qui possède les structures de contrôles
classiques (itérations tantque et pour, sélections si et cas). Les objets qu'il permet de manipuler sont
essentiellement les fichiers et les répertoires de l'arborescence d'UNIX; parmi ceux-ci , certains
fichiers exécutables contiennent le code.

3.2. Eléments de base


Le shell interprète des instructions du langage de commande, séparés par des séparateurs
d'instruction. Les instructions peuvent être de sortes très différentes:
• une commande : appel d'un sous-programme externe: ls , rm, cp, ...
• une commande : appel d'un sous-programme interne : echo , read, ...
• une instruction : structure de contrôle : if , while, ...
• une liste de commandes ou phrases
3.2.1. Séparateurs
Les séparateurs de phrases sont le ® et le point-virgule.
<ret> ; séparateurs de phrases
<tab> espace séparateurs d'entités lexicales
3.2.2. Mots
Les mots du langage sont constitués avec les caractères lettres, chiffres et souligné: ils commencent
par une lettre ou un souligne. On peut mettre des caractères spéciaux dans les mots, à condition de
les déspécialiser. Les mots désignent les variables, les symboles du langage (mots-clé) et les noms
de fichiers.
Un commentaire est délimité par le caractère # en tête de mot et un ®.
3.2.3. Caractères génériques
On peut désigner un ensemble de fichiers pour une commande par une définition générique - le
shell recherche lui-même la totalité des fichiers désignés.
? remplace un caractère quelconque, sauf le ®;
* remplace n'importe quelle chaîne, même vide, de caractères ne comportant pas de ®.
[...] remplace un caractère parmi ceux qui sont énumérés entre les crochets, ou un caractère
parmi ceux qui ne sont pas énumérés entre les crochets si le premier d'entre eux est un
point d'exclamation.
$ ls
t1.p t2b.p t3.p tf.p tn.p tx.p
t1a.p t2.p t4.p tn tx
$ echo Mes versions de TP sont t?.p
Mes Versions de TP sont t1.p t2.p t3.p tf.p tn.p tx.p
$ echo les bonnes versions t[!0-9].p

9
les bonnes versions tf.p tn.p tx.p
$ echo Dans t1?.p les tableaux sont de la forme t\[nx] avec nx=12 \*P
Dans t1a.p t1b.p les tableaux sont de la forme t[nx] avec nx=12*P

- Caractères spéciaux
Les caractères spéciaux ont une signification dans le langage, à moins qu'ils ne soient ignorés. Un
caractère spécial devient un caractère normal s'il est précédé du caractère \ (contre-barre) ou s'il est
situé dans un parenthésage particulier. Ces caractères sont :
&|<>$()'"`{}\
3.2.4. Parenthésages
Servent à regrouper des séquences de mots ou à isoler des mots:
{...} exécute les commandes dans ... par le même shell; délimite la portée de l'opérateur de prise
de valeur $ (voir plus loin);
(...) exécute les commandes ... par un shell fils;
'...' prend littéralement ... ; aucun caractère spécial n'a de signification ;
"..." prend littéralement ... après interprétation de $, '...', et \
`...` exécute les commandes dans ... ; le résultat (sotie standard) remplace `...`
$ echo '***'
*** # idem \*\*\*
$ echo "Voici l'exemple"
Voici l'exemple
$ echo la variable 'as$V3' contient $HOME/as\$
la variable as$v3 contient /users/fc/tp1/as$ le shell remplace $HOME par /users/fc/tp1/as, v3 par vide
$ echo "la variable as$V3 contient $HOME/as\$"
la variable as contient /users/fc/tp1/as$
$ echo 'la variable as$V3 contient $HOME/as$'
la variable as$v3 contient $HOME/as$
$ (date ; who) # les sorties de date et de who sont mises bout à bout en un seul
flot de données
wed sep 28 09:11:09
vous tty2 sep 28 08:30
mhs tty4 sep 28 09:30
$(date;who)| tee sauve |wc # les sorties de date et de who seront copiées dans le fichier sauve
et vers la sortie standards
Remarques
- on ne peut pas banaliser le caractère quote dans un parenthésage quote, ni le caractère guillemet
dans un parenthésage guillemet;
- lorsque le Shell interprète les caractères génériques, il ne les remplace s'il ne trouve aucun nom de
fichier à mettre en correspondance; en particulier, il ne les remplace pas par une chaîne vide.
3.2.5. Paramètres des commandes
Les paramètres effectifs d'une commande est une chine de caractères.
Lorsqu'une commande est un sous-programme écrit en Shell, les paramètres formels sont désignés
par des chiffre, et ce sont des paramètres positionnels:
0 nom de la commande;
1 premier paramètre effectif;
2... deuxième paramètre effectif, ainsi de suite;
"$*" équivalent à "$1 $2 ..."
"$@" équivalent à "$1" "$2" ...

10
3.3. Variables; Arguments et paramètres de commandes
Les variables sont désignés par des mots; elles ne se déclarent pas, et leurs type (numérique ou
chaîne) dépend de leur interprétation par les commandes. Les variables et les fonctions dans un
même processus doivent avoir des noms différents.
- Initialisation variable=valeur (sans espaces des 2 côtés du =)
Une valeur peut être une valeur numérique, une chaîne de caractères, le résultat d'un commande,
une liste de commandes ...
$ N=1 # N est une variable entière
$ ACTIF = `who` # ACTIF est une chaîne, résultat en sortie par l'exécution de who
$ USAGE = "echo Syntaxe: $0 f1 F2; cat $ help" # USAGE est une liste de commandes
- Evaluation des variables
La prise de valeur d'une variable ou d'un paramètre est réalisée par l'opérateur $ placé devant le
nom de la variable ou du paramètre. Ainsi, $N est une expression de valeur 1, et $1 est une
expression dont la valeur est celle du paramètre effectif placé en première position d'une commande
simple. Les accolades {} peuvent être nécessaire autour du nom, ainsi:
{variable} l'interprète substituera dans le cas où la variable est définie sa valeur et dans le cas
contraire le mot (non évalué), la variable n'étant pas affectée.
$ REP=${REP}miage # Concaténer "miage" sans espace à la valeur de REP
Si la valeur de la variable contient un caractère générique, celui-ci est remplacé lors de l'expansion
de la variable.
La substitution d'une variable ou d'un paramètre permet d'évaluer conditionnellement la valeur de
la variable ou d'un paramètre. Ce mécanisme sert à donner des "valeurs par défaut" à des variables
dont on sait pas si elles sont initialisées.
{variable:-mot} l'interprète substituera dans le cas où la variable est définie sa valeur et dans le
cas contraire le mot (non évalué), la variable n'étant pas affectée.
$ t=${1:60} # t aura la valeur du premier argument de la commande, sinon, 60
{variable:=mot} même substitution avec de plus affectation de la variable si elle n'est pas définie
$ a='bonjour'
$ echo ${a:='au revoir'}
bonjour
$ echo ${b:='salut'} # b aura la valeur 'salut'
salut
{variable:?mot} si la variable est initialisé et non nulle, elle sera substituée; autrement, imprime
mot et termine le processus Shell en cours.
$ echo ${b:?Erreur} # b à pour valeur initiale 'salut'
salut
$ echo ${c:?Attention!} # c n'est pas initialisée
c : Attention!
{variable:+mot} si la variable est initialisé et non nulle, l'interprète substituera mot et sinon la
chaîne vide, la variable n'étant pas modifiée.
$ echo ${b:+bonjour}
bonjour
- Expression, lectures et affichages
Le calcul d'expression sur des variables n'est pas supporté par le Shell. Seules les lectures et les
écritures sont réalisées par des commandes internes du Shell: read et echo. Pour les calculs et les
tests, on utilise des commandes UNIX spécialisées, comme expr et test.
• read : admet en paramètre une ou plusieurs variables séparées par un séparateur de mot. Elle
initialise la 1ère variable avec la 1ère chaîne tapée, le séparateur au clavier étant aussi le blanc ou le

11
TAB, et elle initialise la dernière variable avec toutes les chaînes de fin de ligne. read délivre le
code retour 0, sauf si elle a rencontré la fin de fichier.
• echo: souvent une commande interne. Elle n'admet pas d'option et admet dans ces paramètres des
séquences d'échappement pour afficher des caractères spéciaux:
\t caractère TAB
\n fin de ligne, pour changer de ligne dans le message;
\f saut de page, pour changer de page si la sortie standard est redirigée vers un fichier à
imprimer
\c pour afficher le message sans le terminer par un retour de ligne
Sous BSD, echo est une commande et admet une option :
echo "-n message" est équivalent au echo "message \c"
# lecture au clavier des variables x, y et z
$ read x y z # si on tape "une deux trois quatre cinq"
# x vaut "une", y vaut "deux" et z vaut "trois quatre cinq"
# affichage par la commande interne echo
echo la valeur de x est : $x
la valeur de x est : une
# incrémentation de N par la commande UNIX expr
N=`expr $N + 1` # autres opérateurs : - * / %(mod)
# test de la valeur numérique N par la commande UNIX test
x=test $N -eq 2 # délivre vrai (0) si N=2
# autres opérateurs: -ne, -gt, -ge, -lt, -le
- Portée et environnement
La protée d'une variable est le processus qui l'a crée. Pour être visible dans les processus Shell qui
seront crées par la suite, une variable doit être exportée par la commande export: elle est alors
placée dans l'environnement du processus, environnement dont hériteront ses fils.
$ x=Bonjour
$ export x
$ sh # un autre shell
$ echo $x
Bonjour # x est connue dans le sous-shell

- Variables prédéfinies
# nombre de paramètres effectifs d'un commande (sous-programme);
? code de retour d'une commande;
$ numéro de du processus Shell en cours;
! numéro du dernier processus lancé en arrière plan;
HOME nom du répertoire personnel (de login), fournit le paramètre par défaut de cd;
PATH règles de recherches des commandes;
TERM type du terminal utilisé
TERM valeur du premier prompt ($ par défaut)
PS1 valeur de second prompt (> par défaut)
IFS ensemble de caractère interprétés comme séparateur de chaînes dans le Shell.
$ pS1=bonjour
bonjour:pwd
/usr/ens/vous
$ HOME=/
$ cd
$ pwd
/
$ HOME=/usr/ens/vous
$ cd
$ pwd
/usr/ens/vous
12
Une variable peut être détruite par la commande interne unset; cependant les variables PATH,
PS1, PS2 ne peuvent pas être détruites.

3.4. Les Commandes


Les commandes correspondent aux instructions du langage de programmation.
3.4.1. Structure de la ligne de commande
„ Commande simple: liste de mots séparés par des espaces. Le premier mot est le nom de la
commande, les autres sont les paramètres effectifs qui lui sont transmis. Une commande simple
délivre une valeur appelée code de retour. Un code de retour nul indique une exécution sans
erreur. Une valeur non nul à une signification propre à la commande qui l'a retournée.
„ Tube (pipe-line) : séquence de 2 ou plusieurs commandes séparées par le "|". Les commandes
sont exécutées par des processus différents fonctionnant en mode pipe-line (producteur-
consommateur).
„ Liste de commande: séquence de commandes simples ou de pipe-lines séparés par l'un des
caractères suivants :
; exécution séquentielle; de même priorité que le &;
& exécution asynchrone
&& exécution de la partie droite conditionnée par un code de retour nul de ce qui précède
(commande s'est bien déroulée); de priorité plus forte que & et ; ;
|| exécution de la suite conditionnée par un code de retour non nul de ce qui précède
(commande s'est bien déroulée); de même priorité plus forte que && .
3.4.2. Les Structures de contrôles
Le langage Shell dispose de structures de contrôle semblables à celles du langage PASCAL
(conditionnelles, itérations bornées, ou non bornées).
3.4.2.1. La commande test
Avant de voir les structures de contrôle du Shell, nous revenons sur la commande test dont nous
avons parlé ultérieurement.
Cette commande permet de tester des expressions ou des états de fichiers et délivre un code retour
Syntaxe test exp
[ exp ] les espaces sont indispensables entre les crochets
exp est une expression peut être un simple terme, ou une suite de termes combinés avec les
opérateurs logiques et, ou, non : -a, -o !. Les termes peuvent être parenthéser par le couple ( ), mais
il ne faut pas oublier de faire précéder chaque parenthèse d'une contre-barre (la parenthèse est un
caractère spécial du Shell).
Un terme peut être d'une des formes suivantes:
-r f vrai si f existe et a le droit r;
-w f vrai si f existe et a le droit w;
-x f vrai si f existe et a le droit x;
-f f vrai si f existe et est un fichier ordinaire;
-d r vrai si le répertoire r existe;
-u f vrai si f existe et a le bit set-UID;
-s f vrai si f existe et n'est pas de longueur non nulle;
-z ch vrai si la chaîne ch est de longueur nulle;
-n ch vrai si la chaîne ch est de longueur non nulle;
c1 = c2 vrai si la chaîne c1 et c2 sont égales;
c1 != c2 vrai si la chaîne c1 et c2 sont différentes;

13
n1 -op n2 op est un opérateur qui est eq (=), ne (différent), ge(>=), gt (>), lt (<) ou le (<=), et n1 et
n2 sont des entiers.
if [ $I -ge 10 -a $I -le 20] # I compris entre 10 et 20
then ...

if test -s resultat -o \( -n $R -a $N -gt $NN \) # le fichier résultat n'est pas vide, ou alors R
then ... # est initialisée et N est supérieure à NN

3.4.2.1. La boucle pour...


for <variable> [in chaine> ...]
do <liste de <commandes>
done

Exécuter une liste de commandes pour une liste de valeurs données.


La liste de commande est exécutée pour chaque valeur de la variable prise dans la suite des chaînes
derrière le in.
$ for prog in *.p # traite chaque fichier du répertoire courant
do
echo "\n\n$prog \n" # \n imprime un retour de ligne (SystemV, seulement)
head -1 $prog # head -n f imprime les n premières lignes de f
done
$ cat com1
for i # équivalent à for i in $*
echo $i # affiche la liste des paramètres
done
$ com1 a bc d
a
bc
ds

$ for i in 3 4 5 6 # copie le fichier f dans f3 f4 f5 et f6


do cp f f$i
done

3.4.2.2. La boucle tantque...


Le Shell dispose de structures itératives du type tant que et jusquà dont la syntaxe:

while <liste de commandes >


do <liste de commandes>
done

Tant que le code de retour de la dernière commande de la première <liste de commandes> est 0
(vrai), la deuxième <liste de commandes> est exécutée et la boucle continue.
# lire un choix numérique
echo Votre choix (1-4)
while echo "?\c"; read choix;
[ $choix -lt 1 -o $choix -gt 4 ] # appel à la commande test
do echo Recommencer (1-4)
done

14
3.4.2.3. La sélection si ...

if <liste de commnades>
then <liste de commandes>
[elif <liste de commandes> ]*
then <liste de commandes>
[else <liste de commandes> ]
fi
La liste de commande de then est exécuté si le code de retour de la dernière commande de la liste
située derrière le then est nul, sinon , la liste de commandes de else. elif est mis à la place de else if.
L'alternative else peut être omise.
Le code retour de if est 0 quand la liste après then est exécutée, sinon c'est celui de la dernière
commande de la liste située aprés le else.
$ cat com2
if test -f $1
then echo $1 existe
else echo $1 n\'existe pas
fi
$ com2 p1
p1 n'existe pas
$ com2 f3
f3 existe
Il est possible d'enchaîner en cascade des sélections sous la forme:
if ... if ...
then then
else if ... elif ...
then .... then ....
else if ... ou encore elif ...
.... then
fi ...
fi fi
fi

3.4.2.3. La sélection cas ...


Cette structure permet de réaliser des aiguillages. La forme générale en est :
case <chaîne> in
<motif> ) <liste de commandes>;;
<motif> ) <liste de commandes>;;
- ...
- ...
esac
L'interpréteur teste si la chaîne contrôlant l'instruction case correspond aux motifs successifs dans
l'ordre où ils apparaissent et exécute la liste de commandes correspondant au premier motif
satisfaisant, puis l'exécution du case est terminée.
# aiguillage après la lecture du choix
case $choix in
1) sh choix1;;
2) sh choix2;;
*) echo "Pas encore au point";;
esac

15
3.4.3. Commandes Internes
Une commande interne est une commande prédéfinie dans le Shell, pour laquelle il n'existe pas de
fichier exécutable ayant son nom.
- structure de contrôle
break [n] sortir d'une boucle (tant que, pour), ou de n emboîtements de boucles;
continue [n] continuer à l'itération suivante d'une boucle, ou à la nème boucle englobante;
exit [n] quitter le programme avec en code retour celui de la dernière commande exécutée,
où n s'il est présent. C'est la commande qui permet à un sous-programme Shell de
rendre un code-retour no nul;
return [n] idem pour quitter une fonction;
. fichier exécute un fichier Shell dans le Shell courant et non dans un sous Shell;
: commande vide du Shell. Son code de retour est 0 et peut donc être utilisée
comme constante vraie; pratique pour écrire des boucles infinies!
while :; do date; done
- Commandes relatives aux varaibles et paramètres
eval mot le contenu de la variable mot est considéré comme une liste de commande Shell.
Ces commandes sont exécutées. Cela sert à forcer une nouvelle évaluation par le
shell des variables ou des commandes (ressemble à un sous-programme).
# fichier de démarrage /etc/profile
MESSFIN="echo `logname` de \'connecte\' le ; date;"
QUOTAT='TAILLE= ` du -s $HOME | cut -f1`'
MESQUIT=' echo Test des quotas en cours"
eval $QUOTAS
if [ $TAILLE -gt $MAXQUOTAS ]
then
eval $MESSQUOT
exec sh
fi
trap "eval $MESSFIN; eval $QUITTER" 0
# suite
set [arg1]+ fournit les valeurs des paramètres effectifs correspondant aux paramètres formels
1, 2, ...Permet ainsi de paramétrer les sous-programmes réalisés par eval: il suffit
que les commandes contenues dans mot soient paramétrées, en faisant référence à
$1, $2, ...
set `date` # range le jour de la semaine dans $1 mois dans $2 ...
set -u [mot] permet la production d'une erreur si la variable mot (ou toute variable) n'est pas
initialisée lors de son utilisation.
shift [n] les paramètres formels à partir de $n + 1 (n=1 par défaut) sont renommés à partir
de 1. Nécessaire quand une commande écrite en shell a plus de 9 paramètres, et
utile par exemple pour traiter dans une boucle tant que une liste d'arguments
optionnels.
3.5. Redirection des E/S
>fichier dirige la sortie standard vers fichier;
>> fichier ajoute la sortie standard à fichier;
< fichier prend l'entrée standard depuis fichier;
n> fichier dirige la sortie du descripteur n vers fichier;
n>> fichier ajoute la sortie du descripteur n vers fichier;
<&m le fichier associé au descripteur m est utilisé à la place de l'entrée standard.
Utilisable quand on connaît les numéros des descripteurs;
>&m idem pour la sortie standard;
n> &m fusionne la sortie du descripteur n à celle de m ;

16
n< &m fusionne l'entrée du descripteur n à celle de m ;
n> fichier dirige la sortie du descripteur n vers fichier
<< [-]mot l'entrée du Shell est lue jusqu'à une ligne contenant mot (ou une fin de fichier).
Le signe (-) permet d'ignorer les blancs de début de ligne du texte soumis
redirection;
<< [-]\mot idem mais sans interprétation
<< [-]'mot' idem mais sans interprétation
$ (echo Message normal
> echo message pour signaler une erreur 1>&2
> echo Autre message normal
) > sortie 2>erreur
$ cat sortie
Message normal
Autre message normal
$ cat erreur
Message pour signaler une erreur

3.6. Sous-programmes
Un texte en Shell et stocké dans un fichier constitue une procédure Shell. Dans le texte, les
paramètres formels ont pour noms 1, 2, ...,9 et leurs valeurs sont donc désignés par $1 pour le
premier, $2 pour le second, etc. Les paramètres sont positionnels ; le nombre de paramètres effectifs
n'a pas besoin d'être égal au nombre de paramètres formels (qui ne sont pas déclarés).
L'activation de la procédure peut se faire en exécutant la commande sh suivie du nom du fichier et
de ses paramètres effectifs, ou bien en appelant directement le nom du fichier suivi de ses
paramètres si ce fichier est exécutable: il suffit donc de mettre le droit x à un fichier contenant un
sous-programme Shell pour fabriquer une nouvelle commande.

3.7. Gestion des processus et des événements


• Lorsque des commandes sont lancées en arrière-plan, et qu'il est nécessaire de synchroniser la
suite d'un programme Shell avec la fin de ces commandes, le programme Shell doit se bloquer en
attente de la fin de ces commandes par la commande interne wait:
wait [n] suspend l'exécution jusqu'à ce que le processus de numéro n, ou tous les processus
lancés en arrière-plan, se terminent. Le code retour est celui du processus attendu.

• Les signaux d'UNIX qui peuvent être reçus durant l'exécution du Shell sont traités de façon
standard, à moins qu'un traitement spécifique ne soit indiqué par la commande trap.
trap [arguments] [n]+ l'argument est une commande qui sera exécutée par le Shell lorsque le
signal (peut être plusieurs) n survient;
trap [n] + les trappes sont remises à leur état primitif (restaurer)
trap donne la liste des commandes associés à chaque signal.
Les arguments de trap sont examinés deux fois:
- lors de l'établissement des trapes
- à l'arrivée du signal correspondant
• La commande interne exec commande lance la commande nommée par recouvrement du
processus shell: celui-ci disparaît complètement
$ exec ksh # remplacement du Shell par un processus Korn-Shell
$ exec login # recommencer une session, en gardant la ligne
$ exec >session # pour ce shell, la sortie standard est le fichier session

17
4. Les filtres (Recherche et Edition)
Il existe une grande famille de programmes qui lisent une entrée, réalisent une transformation et
écrivent un résultat.
Ce chapitre présente les filtres les plus utilisés. Nous commençons par ceux qui effectuent des
recherches dans les fichiers (la famille grep) ou des recherches de fichiers (find).
Le reste du chapitre sera réservé à l'étude de deux outils de "transformation de texte"
programmables et universels. Ils sont programmables, en ce sens que les transformation s à réaliser
sont définies dans un langage de programmation simple. Ces programmes sont sed et awk qui sont
des généralisations de grep.

4.1. Recherche d'une chaîne dans un fichier- la famille GREP-


Unix offre trois commandes de même famille ; grep, egrep et fgrep, pour afficher les lignes de
fichiers donnés en paramètre et qui contiennent un motif donné. Leurs syntaxes est le même:
grep [option] <motif> [<fichier> ... ]

Le motif est une expression régulière dans laquelle certains caractères ont une signification
particulière.
grep est très utile pour trouver les occurrences d'une variable dans un programme, ou des mots dans
un document, ou pour sélectionner des parties des sorties d'un programme.
Sauf option contraire, chaque ligne ayant cette propriété est écrite sur la sortie standard. Il est
recommandé d'entourer d'apostrophes les expressions contenant des caractères spéciaux du langage
de commandes utilisé.
Options
-v seules les lignes ne contenant pas le motif sont affichées;
-x n'affiche que les lignes qui ne contiennent que le motif (fgrep seulement);
-c seul le nombre de lignes satisfaisantes est affiché;
-i ignore la distinction minuscule/majuscule dans les comparaisons (grep seulement)
-l seules les noms de fichiers contenant le motif sont affichés;
-n numérote les lignes affichées;
-e motif lorsque le motif commence par un tiret;
-f fichier le motif est dans le fichier (egrep et fgrep)
Le code de retour est 0 si un motif a été trouvé, 1 sinon, et 2 s'il y a eu des erreurs.
$ grep -n variable *.ch cherche variable dans les source
$ grep From $MAIL affiche les en-têtes des messages
$ grep From $MAIL| grep -v marie ... autre que marie
$ grep -c marie $HOME/lib/annuaire trouve le numéro de marie dans l'annuaire
$ who | grep marie est-elle connectée?
$ ls | grep -v temp les noms de fichiers ne contenant temp

expressions régulières de grep et fgrep (par ordre décroissant de précédence)


c tout caractère non spécial
\c ignore la signification spéciale de c
^ début de ligne
$ fin de ligne
. n'importe quel caractère unique
[...] n'importe quel caractère unique parmi ....; des suites comme a-z sont possibles
[^...] n'importe quel caractère unique ne figurant pas parmi ....

18
\n ce qui a été identifié par la nième sous expression \(...\) (grep seulement)
r* zéro ou plus occurrences de r
r+ une ou plus occurrences de r
r? zéro ou une occurrence de r
r1r2 r1 suivi de r2
r1| r2 r1 ou r2 (egrep seulement)
\( r \) sous expression r repérée (grep seulement); voir \n; peut être imbriqué
(r) expression régulière r (egrep seulement); peut être imbriqué
$ grep '^From' $MAIL imprime toutes les lignes commençant par From
$ ls -l | grep '^d' imprime seulement les lignes décrivant un répertoire
$ ls -l | grep '^.......rw' imprime les fichiers qui peuvent être lus ou écrits par tous
$ grep '^[^:]*::' /etc/passwd trouver les utilisateurs qui n'ont pas de mots de passe
grep est le plus ancien membre d'une famille de programmes qui contient également egrep et
fgrep. Leur comportement est à peu près le même. fgrep recherche plusieurs expressions
simultanément et egrep traite de vraies expressions régulières - les mêmes que grep, mais avec un
opérateur 'ou' et des parenthèses pour grouper les expressions.

4.2. Recherche d'un fichier: find


find descend récursivement des sous-hiérarchies de répertoires données par leur racine, en
cherchant à appliquer à des fichiers précisés par un ou plusieurs critères se sélection (nom, type,
date) une commande donnée.
find [répertoire]+ [expression]+

répertoire est la liste des racines des sous-hiérarchies à parcourir. expression est une suite de
primitives exprimant à la fois les critères de sélection des fichiers et les actions à leur appliquer.
Lorsque le critère est vrai, l'action est exécutée. Ces primitives sont :
-name <motif> vrai si le motif s'applique sur le nom du fichier courant;
-user <nom> vrai si le fichier courant appartient à l'utilisateur nom;
-atime <n> vrai si le fichier a été utilisé dans les n derniers jours; peu utilisable;
-mtime <n> vrai si le fichier a été modifié dans les n derniers jours;
-newer <fichier> vrai si le fichier courant a été modifié plus récemment que fichier;
-type <x> vrai si le type du fichier est x avec :
b pour fichier spécial bloc,
c pour fichier spécial caractère,
d pour fichier répertoire,
f pour fichier ordinaire,
p pour tube nommé (System III/V);
-perm <octal> vrai si les permissions du fichier correspondent au nombre octal donné;;
- links <n> vrai si le fichier a n liens ;
-group <nom< vrai si le groupe propriétaire du fichier est nom;
- size <n> vrai si le fichier contient <n> blocs;
- exec commande vrai si la commande restitue une valeur de sortie nulle; la fin de la commande
doit être ponctuée par \; et le nom du fichier examiné est représenté par {};
- print le nom de fichier doit être imprimé;
- inum <n> vrai si le i-noeud du fichier à pour numéro n;
-\(expression\) a pour valeur booléenne celle de l'expression parenthésée.
Toutes ces primitives peuvent être composées avec les opérateurs booléens: de négation !, de
conjonction (concaténation des expressions), de disjonction -o , et les parenthèses \(\).
find . \( -name *.p -o -name *.f \) -mtime -30 -exec ls -l {} \;

19
find applique la commande ls -l à tous les fichiers qui sont des sources PASCAL ou FORTRAN
modifiés depuis moins d'un mois, et qui appartiennent à la hiérarchie dont la racine est le répertoire
courant (.):
find $HOME -newer .datesauv -print
affiche les fichiers de l'arborescence définie par le répertoire personnel de l'usage modifiés après le
fichier .datesauv

4.3. Tri et fusion


4.3.1 Tri sort [options] [clé [option]]+ [<fichier> ]+
trie les lignes et écrit les résultats sur la sortie standard sauf si l'option -o est utilisée. Le tri sur les
ligne est lexicographique sur l'ensemble des fichiers.
options
-c (check) vérifie que le fichier est trié; la sortie n'existe que si le fichier n'est pas trié;
-u (unique) ne garde qu'une ligne parmi toutes celles qui ont la même clé;
-o <fichier> (output) envoie la sortie sur <fichier>
-d (dictionary) les comparaisons ne se font qu'avec les lettres, les chiffres et les blancs;
-f lettre minuscules et majuscules sont confondues;
-n clé numérique (signée éventuellement);
-i les caractères dont le code ASCII est extérieur à l'intervalle octal 040-0176 sont ignorés;
-r renverser l'ordre du tri;
-b ignorer les blancs en début de champ;
-t<x> le délimiteur de champ dans les lignes est le caractère <x> et nom le TAB
clé limite le champ des comparaisons aux portions de lignes, elle est de la forme +debut - fin.
debut est le premier champ servant à faire la clé (numérotation à partir de zéro), fin est le numéro
du premier champ qui n'appartient pas à la clé.
sort +1 -2 f trie f sur une clé formaté par le deuxième champ de chaque ligne
Une clé peut être préciser des portions de champs. Dans ce cas , debut et fin sont de la forme m.n
(éventuellement l'une des options b d f i n ou r) où m donne le nombre de champs à sauter depuis
le début de ligne et n le nombre de caractères à sauter en début de champ avant de commencer la
comparaison.
$ sort +2.3 -5.0 f trie f sur une clé qui commence au 4ème caractère du troisième
champ, et qui se termine à la fin du cinquième champ.
$ ls | sort trie les noms de fichiers en ordre alphabétique
$ ls -s | sort -nr trie par ordre de taille décroissante
$ ls -l | sort +3nr trie par taille décroissant; après avoir sauter les 3 premiers champs
$ who | sort +4n trie les plus anciennes connexions
Soit f.t un fichier d'étudiants qui la format suivant:
nom, prénom, jj/mm/aa, codeBac, codeDiplôme
$ sort -t, +4bf -5bf +0f -1f f.t >diplome.t tri sur diplôme, puis alphabétiquement, sortie sur diplome.t
$ sort -t, +2.7b -2.9b +2.4b -2.6b +2.b -2.2b f.t >age.t
tri par âge, la date étant la date de naissance, sortie sur age.t.
Dupont, Marie, 01/03/70, c, UIT GEA
champs : 0 1 2 3 4
car. dans ch2: 0123456789
+2.7 signifie que la clé commence sur le 7ème caractère du champ 3 (soit le premier a de ...
jj/mm/aa, le chiffre 7), et -2.9 signifie qu'elle s'arrête au 9ème caractère après la fin du deuxième
champ (0 de 70, sur l'exemple). Le b collé sur la clé pour ignorer les éventuels blancs entre la
virgule et la date.

20
4.3.2 Fusion
sort -m [options] [clé [option]]+ [<fichier1> ]+

Les fichiers doivent préalablement être triés sur les clés de fusion.

4.4. L'éditeur de flot sed


sed est un éditeur non interactif qui permet le traitement de fichiers texte. Il est utilisé surtout dans
les sous-programmes shell, et comme filtre dans les listes de commandes.
sed [-n] [<requête>][-e <requête>]* [ -f <script>][<fichier>]*

sed recopie le ou les <fichier> (l'entrée standard, par défaut) sur la sortie standard, en appliquant à
chaque ligne les requêtes qui suivent l'option -e (un par requête), ou les requêtes qui se trouve dans
le fichier <script>, ou la requête s'il y en a une.
4.4.1. Forme générale des requêtes de sed
Une requête sed est de la forme:
[adresse1 [,adresse2]] commande [arguments]

L'option -n demande à sed de n'afficher sur la sortie que les lignes sur lesquelles on applique la
commande p.
adresse est le numéro absolu d'un ligne de texte ( dans le cas où il y a plusieurs fichiers sources,
sed considère qu'il y a en un formé de la concaténation des différents fichiers), ou le $ pour dernière
ligne, ou une expression régulière palace entre deux barres (/.../) et définissant un motif figurant
dans la ligne adressé.
4.4.2. Mode de fonctionnement
sed ne travaille pas sur une image du fichier en mémoire: son tampon ne contient qu'une ou
quelques lignes du fichier à un instant donné. Toutes les requêtes que l'on fournit à sed sont
appliquées à chacune des lignes défilant dans le tampon, en fonction de l'adressage de ces lignes
dans les requêtes . Un tampon auxiliaire permet les déplacements de blocs de texte dans le fichier.

tampon d'édition (1 ligne)

tampon des tampon


requêtes auxiliaire
fichier à éditer fichier résultat

4.4.3. Les commandes de manipulation de l'espace de travail


commande est une lettre suivi d'arguments. Toutes les requêtes peuvent avoir zéro, une ou
plusieurs adresses sauf a et i (zéro ou une).
a\ <texte> ajoute des lignes à la sortie jusqu'à la première ligne ne se terminant pas par \
b <etiq> saut à la commande :<etiq>
c\ remplace des lignes comme pour a
d détruit la ligne courante; lit la suivante
i\ insère le texte avant la prochain sortie
l imprime la ligne en visualisant tous les caractères (même ceux qui non
imprimables)
p imprime la ligne
q termine
r fichier lit fichier, et copie son contenu en sortie
s/<anc>/<nouv>/f substitue l'expression régulière <anc> par la chaîne de substitution nouv. f est un
indicateur :

21
g : remplace toutes occurrences traitées;
p : le contenu du tampon est écrit si une substitution est effectuée;
w <fichier> : écrit dans <fichier> si une substitution est effectuée
t <etiq> branchement à :<etiq> si la dernière substitution a réussi
w <fichier> écrit dans <fichier>
y/<ch1>/<ch2>/ toutes les occurrences du tampon des caractères figurant dans la chaîne1 sont
remplacés par le caractère correspondant de la chaîne2.
= imprime le numéro de la ligne
! commande-sed la commande est effectuée sur les lignes non sélectionnées
{ et } associe à un espace d'adressage une liste de commandes
N la ligne courante est ajoutée au tampon qui en contient alors deux
D détruit le début du tampon jusqu'à la première fin de ligne incluse
P affiche le début du tampon jusqu'à la première fin de ligne incluse
h le tampon est copié dans le tampon auxiliaire dont le contenu est détruit
H le tampon est ajouté dans le tampon auxiliaire; le séparateur est la fin de ligne
g le tampon auxiliaire est copié dans le tampon dont le contenu est détruit
G le tampon auxiliaire est ajouté dans le tampon; ; le séparateur est la fin de ligne
x échange entre les contenus du tampon et du tampon auxiliaire.
$ sed 's/UNIX/UNIX(TM)/g' f1 f2 remplace la chaîne UNIX par UNIX (TM) dans f1 f2
$ sed '10,/{# .*#}/s/ */ /g' f >>f remplace les espaces qui se suivent par un seul blanc de la dixième
ligne jusqu'à la première ligne (après la dixième) qui contient une
chaîne de la forme {#...#}
$ who | sed 's/ .* / /' remplace un espace suivi d'un nombre quelconque de caractères
jusqu'au dernier espace la ligne par un seul espace
$ sed '/fin/ q' imprime son entrée sur la sortie jusqu'à la première ligne contenant
le mot fin
$ sed '/^***/d' détruit les lignes commençant par ***
$ sed 's/$/\n/' ajoute une fin de ligne à la fin de chaque ligne
$ sed '1,/^$/d détruit jusqu'à la première ligne vide
$ sed -n '/^$/,/^fin/p imprime chaque groupe lignes depuis une ligne vide jusqu'à une ligne
débutant par fin
$ sed -n '/exp/w fichier1 écrit les lignes vérifiant exp sur fichier1, sinon sur fichier2
> /exp/!w fichier2'

# exemple : Remplacer une fin de ligne par le séparateur :, sauf s'il est suivi par !.
# debut de la boucle
: debut
# ajout de la suivante dans le tampon
N
# remplacer la fin de ligne par : s'il n'y a pas de !
s/ \n \([^!]\) / :\1 /
# si la substitution a réussi , recommencer
t debut
# sinon , afficher la ligne d'adresse complète
P
# et détruire le ! qui reste dans le tampon
D

22
4.5. Outil de transformation de texte AWK
awk est un outil non interactif pour traiter des textes, les transformer , les éditer , faire des calculs
élémentaires sur le contenu. Il manipule les fichiers ligne par ligne et adresse les champs à
l'intérieur des lignes. Pour traiter les champs, le programmateur dispose de variables et des
instructions classiques des langages de programmation.
Quelques-unes des limitations de sed sont levées par awk. L'idée de base d'awk est la même que
celle de sed, mais les détails dérivent plutôt du langage C que d'un éditeur de texte.
awk [-F<car>] {[-f <fprog>| <prog>]} [param] [<fichier>]*

traite les fichiers référencés ligne par ligne, et chaque ligne est soumise à une programme awk
(<fprog>, <prog>) ; si la ligne courante satisfaisant à une ou plusieurs sélections proposées dans le
programme les actions correspondants sont exécutés. Une ligne d'un programme awk a le format
suivant :
sélection { actions }
Les sélections sont des expressions les mêmes que dans egrep, ou peuvent être encore plus
compliquées. awk fournit deux expressions particulières, BEGIN et END. Les actions associées à
BEGIN sont réalisées avant que la première ligne soit lue; ce sont couramment des actions
d'initialisation de variables.
$ awk '/<expression régulière>/ {print}' <fichier> ... fait la même chose que egrep
$ awk ' {print}' <fichier> ... fait la même chose que cat
L'option -F permet de définir le nouveau séparateur de champs, si ce n'est pas le <espace> ou le
caractère <tab>. Les fichiers sont traités dont l'ordre où ils sont données en paramètres dans les
lignes. Dans le cas où aucun fichier n'est signalé, c'est l'entrée standard qui le sera.
Les fichiers sont traités dont l'ordre où ils sont données en paramètres dans les lignes. Dans le cas
où aucun fichier n'est donné, c'est l'entrée standard qui le sera.
4.5.1. Traitement des champs
awk découpe le fichier qui lui est soumis en enregistrements logiques (par défaut les lignes) eux-
mêmes découpés en champs. Un enregistrement (resp. un champs) est une suite de caractères
délimitée par un délimiteur d'enregistrement qui par défaut est le caractère <newligne> (resp. un
délimiteur de champ qui par défaut le caractère <espace>).
De façon générale $1, $2, ..., $NF désignent le premier champ, le second champ, .... de
l'enregistrement courant noté $0. NF est une variable dont la valeur est le nombre de champs de la
ligne courante.
$ who
rim tty2 Sep 29 11:53
slim tty4 Sep 29 12:15
$ who | awk '{print $1, $5} imprime les noms et heurs de connexions
rim 11:53
slim 12:15
4.5.2. Les variables
- les variables utilisateurs
Les identificateurs sont définis comme dans le langage C. La déclaration et le type sont implicites.
On distingue les variables numériques (nombres flottants) et les variables chaînes. Les variables
sont toues initialisées à la chaîne vide '', cette chaîne ayant comme valeur 0 dans les expressions
arithmétiques. La conversion de type est automatique au cours de opérations. Il est enfin possible
d'utiliser les tableaux, l'indexation étant réalisée avec les crochets comme en c.
- les variables prédéfinies
awk maintient des informations intéressantes en dehors du nombre de champs dans différentes
variables:
FILENAME nom du fichier courant d'entrée

23
FS séparateur de champs (<espace> par défaut)
NF nombre de champs dans l'enregistrement courant
NR numéro de l'enregistrement courant
OFMT format de sortie des nombres (défaut %g, voir printf ...)
OFS séparateur de champs de sortie(par défaut, un espace)
ORS séparateur de lignes en sortie (par défaut, fin de ligne)
RS séparateur de lignes en entrée (par défaut de ligne)
$ awk '{print NR, $0}' affiche le fichier avec le n° de ligne
$ awk '{ printf "%4d %s\n", NR, $0 }' même chose mais le n° de ligne sur 4 caractères
4.5.3. Les sélections
Une sélection a l'une des formes suivantes:
- combinaison quelconque au moyen des opérateurs: ! || && (et) telles qu'elles sont définies
pour l'utilitaire grep.
- expression régulière de la forme
<expression> ~ <expression régulière>
<expression> !~ <expression régulière>
<expression> <operateur> <expression> avec <opérateur> est < <= > >= == !=
$2 == "" 2ème champ vide
$2 ~ /$/ 2ème champ identique à la chaîne vide
$2 !~ /./ 2ème champ ne contient aucun caractère
length($2) == 0 2ème champ de longueur nulle

$ awk -F: '$2 == ""' /etc/passwd affiche les gens qui n'ont pas de mot de passe
$ awk 'BEGIN {FS = ":" } idem
> $2 == "" ' /etc/passwd
- intervalle de la forme /motif/, /motif/
Les actions spécifiés sont exécutées pour toutes les lignes comprises entre la première ligne
contenant le premier motif et la première ligne suivante contenant le second motif.
4.5.4. Les actions
4.5.4.1. Les opérateurs (par ordre de priorité croissante)
= += -= *= /= %= opérateurs d'affectation
|| opérateur booléens OU; “exp1” || “exp2” est vraie si l'une des deux l'est. “exp2”
n'est pas évaluée si “exp1” est vraie
&& opérateur booléens ET; “exp1” && “exp2” est vraie si les deux le sont
! négation
> >= < <= == != ~ !~ opérateurs relationnels ~ et !~ sont des opérateurs de correspondance.
<espace> opérateur de concaténation de chaînes
+ - plus, moins
*/% multiplication, division, reste
++ -- incrémentation, décrémentation (préfixé ou postfixé)
4.5.4.2. Les fonctions
cos(expr) cosinus de expr
exp(expr) exponentielle de expr
getline() lit la ligne suivante; retourne 0 si la fin de fichier, 1 sinon
index(s1, s2) position de la chaîne s1 dans la chaîne s2
int(expr) partie entière de expr
length(s) longueur de la chaîne s. Sans argument, $0 est pris comme argument.
log(expr) logarithme naturel de expr
sin(expr) sinus de expr
split(s, a, c) découpe s en a[1],a[2], ..., a[n] suivant le délimiteur c (par défaut la valeur de FS)
24
sprintf(fmt,...) formate ... en accord avec la spécification fmt
substr(s, m, n) sous-chaîne de s; début au caractère m, longueur maximale n
4.5.4.3. Les instructions
Elles apparaissent dans les actions et se terminent soit par un ; , soit par un caractère de fin de ligne,
soit par } ou encore par # (début d'un commentaire qui se termine à la fin de ligne).
- Instructions d'impression
print [<expression>, <expression> ...]
imprime les valeurs des expressions séparés par le caractère valeur de la variable OFS. Sans
argument, il y a impression de l'enregistrement complet ($0). L'impression se termine par une fin de
ligne.
print (<format>, [<expression>, <expression> ...])
imprime les expressions dans le format demandé.
Ces impressions peuvent être redirigées en les faisant suivre de >fichier ou >>fichier.
- Instruction conditionnelle
if (<expression>)
<instruction>
else
<instruction>
- Instructions conditionnelles
while (<expression>)
<instruction>

for (<var>; <expr>; <expr>)


<instruction>
ou
for (<var> in <tableau>)
<instruction>
- les autres instructions
break sortie de la boucle courant
continue poursuivre à l'itération suivante de la boucle
exit sortie du programme en cas d'exécution de la section END et sinon passage à cette
section
next abondan de l'enregistrement courant et passage au suivant
• utilisation des tableaux
$ cat envers
# envers : imprime son entrée à l'envers
awk ' { ligne[NR]=$0 }
END {for (i=NR; i>0; i--) ligne[i]}
' $*
$
• utilisation des tableaux associatifs
$ cat freqmot
# freqmot : compter les apparitions de mots dans un texte
awk ' { for (i=1; i<=NF; i++) num[$i]++ }
END {for (mot in num) print mot, num[mot]}
' $*
$
• un exemple plus complexe
$ cat double
# recherche les mots doubles adjacents dans plusieurs fichiers
awk '
FILENAME != prevfile { # nouveau fichier
25
NR=1
prevfile=FILENAME
}
NF>0 {
if ($1 == dernmot)
printf "double %s, fichier %s, ligne %s ", $1, FILENAME, NR
for (i=2; i<=NF; i++)
if ($i == $(i-1))
printf "double %s, fichier %s, ligne %s \n", $i, FILENAME, NR
if (NF >0)
dernmot=$NF
} ' $*
$

26
5. Utilisation du noyau UNIX
Ce chapitre traite les interactions de niveau bas avec le système UNIX; ce sont les appels systèmes
qui sont des points d'entrée dans le noyau. il s'agit des services que fournit le système d'exploitation.
D'une autre façon, l'utilisation de la bibliothèque standard accroît la portabilité des programmes
écrits mais pas le temps d'exécution.
Nous examinerons dans ce chapitre plusieurs domaines. D'abord;, le système de gestion des entrées-
sorties; où nous détaillerons un peu plus les répertoires et les inodes. Ensuite, nous présenterons le
processus, qui permettent d'exécuter un programme . Enfin nous traiterons des interruptions ou
signaux.

5.1. Les entées sortie de base


Le plus bas niveau des entrées sorties consiste en des appels du système d'exploitation. Un
programme lit des fichiers par morceaux de taille arbitraire. Le noyau gère les données par blocs de
taille compatible avec les caractéristiques physiques des périphériques, et réparti les ressources
entre les différents utilisateurs.
5.1.1. Les descripteurs de fichiers
Toute entrée ou sortie est une lecture ou une écriture d'un fichier, car tous les périphériques, et
même votre terminal, sont des fichiers pour UNIX. Cela signifie qu'une interface unique prend en
charge les communication entre un programme et son environnement.
A ce niveau, on trouve deux types d'appels au système : d'une part quelques appels permettant de
manipuler des fichiers (création, ou suppression ...), et d'autre part des primitives permettant d'agir
sur le contenu d'un fichier ou d'un i-noeud.
Dans le cas général, avant d'entreprendre une opération de lecture ou d'écriture , il est nécessaire
d'en informer le système : il s'agit de l'ouverture du fichier. Dans le cas d'une écriture, il peut être
nécessaire de créer le fichier. Le système vérifie vos droits à réaliser de telles opérations, et, si tout
est correct, retourne un entier non nul, appelé descripteur de fichier (ou nom local). Chaque fois
qu'une E/S sera faite vers un fichier, ce descripteur servira à le désigner en lieu et place de son
nom. Toute information connue sur un fichier ouvert est tenue à jour par le système; un programme
ne peut traiter un fichier qu'à travers ce descripteur de fichier.
Un programme , lorsqu'il est lancé par le shell, hérite de trois fichiers ouverts dont les descripteurs
0, 1 et 2; il s'agit de l'entrée standard, de la sortie standard et de la sortie d'erreur standard. Ces
descripteurs, s'il n'y pas eu de redirection désigne le terminal. Si le système ouvre d'autres fichiers,
les descripteurs seront 3,4 etc...
Si les E/S sont redirigées vers des fichiers ou des tubes, le shell modifie l'assignation par défaut des
descripteurs 0 et 1. En général , le descripteur 2 reste connecté au terminal, pour que les messages
d'erreur y apparaissent. Des écritures comme 2 > fichier ou comme 2>&1 modifient également ces
assignations au niveau du shell et non pas du programme.
int read(fd, buf, n) int write(fd, buf, n)
int fd; /* descripteur du fichier*/
char buf[SIZE] /* tampon de transfert */
int n; /*nombre d'octets à transférer */

5.1.2. Les E/S sur fichier -read et -write


Toutes E/S est réalisée par deux appels du système qui sont read et write que l'on utilise en C à
l'aide de fonctions de même nom.
Chaque appel fournit en retour le nombre d'octets réellement transférés. Zéro indique une fin de
fichier, et -1 indique une erreur. En lecture, le nombre d'octets à lire peut être inférieur à celui
demandé s'il reste moins de n octets à lire dans le fichier. En écriture, la valeur retournée est le
27
nombre d'octets réellement écrits; une erreur a eu lieu si ce nombre est inférieur à la valeur
demandée.
Bien qu'il n'y ait pas de restriction sur les nombres de caractères à lire ou écrire, les deux valeurs les
plus communes sont 1 (un caractère à la fois), le plus souvent 512 ou 1024 (paramètre BUFSIZE
de <stdio.h>)
/* cat : version primitive */
/* copie son entrée sur sa sortie */
# define SIZE 512 /* arbitraire */
main()
{ char buf[SIZE];
int n;
while((n=read(0, buf, sizeof(buf)) > 0)
write(1, buf, n);
exit(0);
}
Soit le programme readslow qui continue à lire son entrée même après une détection de fin de
fichier. readslow lit son entrée jusqu'à une fin de fichier, puis s'en dort avant d'essayer à nouveau.
/* readslow: lecture continue */
# define SIZE 512 /* arbitraire */
main()
{ char buf[SIZE];
int n;
for (;;) {
while((n=read(0, buf, sizeof(buf)) > 0)
write(1, buf, n);
sleep(10); /* la fonction sleep suspend l'exécution d'un programme pendant une
durée indiquée en secondes
}
}
5.1.3. La création de fichier -open, creat, close, unlink
Il est possible d'utiliser d'autres fichiers que ceux ouvert par défaut, il faut alors donner
explicitement leurs noms. Il existe 2 fonctions pour cela:
int open(nom, mode, droits)
char *nom; /* nom du fichier*/
int mode; /* mode d'ouverture */
int droits; /* droits, permissions d'accès */
open retourne un descripteur (ou nom local) de fichier qui est un entier (fopen de la bibliothèque
standard retourne un pointeur), -1 en cas d'erreur. Le nom de fichier est une chaîne de caractère
pointée par nom.
L'ouverture du fichier est donnée par le mode précisée par l'entier mode: 0 lecture, 1 écriture et 2
lecture écriture. Le troisième paramètre droits, indique les droits d'accès, est seulement est pris en
compte que dans le mode de création.
Il est important de noter que l'on peut ouvrir un fichier que s'il existe et le mode d'ouverture doit
être compatible avec les droits d'accès du fichier pour le propriétaire effectif du processus
demandeur.
L'utilisation de cette primitive est facilitée par l'inclusion du fichier /usr/include/fcntl.h dans lequel
sont définies un certain nombre de noms symboliques de constantes correspondant à des modes
d'ouverture particuliers :
O_RDONLY(0) ouverture en lecture seulement;
O_WRONLY(1) ouverture en écriture seulement;
O_RDWR(2) ouverture en lecture et écriture;
D'autres bits peuvent êtres combinés (OU) avec les bits précédents:

28
O_NEDELAY(04) ouverture non bloquante (tubes nommés ou fichiers spéciaux seulement);
O_APPEND(010) écriture uniquement en fin de fichier
O_CREAT(0400) création de fichier s'il n'existe pas, l'appelant doit préciser droits;
O_TRUNC(01000) effacer le contenu du fichier s'il existe.
O_EXCL(02000) associé au bit O_CREAT, provoque un echec de l'ouverture si le fichier existe
déjà
creat doit être utilisée pour créer de nouveau fichier ou pour en réécrire d'anciens.
int creat (nom,droits)
char *nom; /* nom du fichier*/
int droits; /* droits d'accès */
creat retourne un descripteur de fichier s'il a été possible de créer le fichier appelée nom, et -1 dans
le cas contraire. Si le fichier n'existe pas, il est créé avec les droits d'accès indiquées dans droits. Si
le fichier existe déjà , sa longueur est ramené à zéro et les droits ne seront pas changés.
Dans les deux cas, creat ouvre le fichier en écriture et retourne un descripteur utilisé lors des
opérations futures sur le fichier.
open retourne -1 si une erreur a eu lieu, ou un descripteur.
/* cp: version minimale */
# include <stdio.h>
# include <stdlib.h>
# define PERM 0644 /* rw-r--r-- */
char *nompg;
main(int argc, char *argv) /* cp :
copie f1 vers f2 */
{ int f1, f2, n;
char buf[BUFSIZ];
nompg = argv[0];
if (argc != 3)
error ("Utilisation: %s depuis vers", nompg);
if ((f1=open(argv[1], 0)) == -1) /* O_RDONLY */
error("Ouverture impossible de %s", argv[1]);
if ((f2=creat(argv[2], PERMS)) == -1)
error( "Céation impossible de %s", argv[2]);
while ((n=read(f1, buf, BUFSIZ)) > 0)
if (write(f2, buf, n) != n)
error("Erreur en écriture ");
exit(0);
}
error(char *s1, char *s2) /* impression du message d'erreur et fin */
{/* errno : entier externe positionné par le système en cas d'erreur */
extern int errno, sys_nerr;
/* sys_errlist: tableau de chaine, indexé par errno, traduit les nombres en message clair ou #
include <errno.h> */
extern char *sys_errlist[]; if (nompg)
fprintf(stderr,"%s: ", nompg);
fprintf(stderr, s1, s2);
if (errno > 0 && eerno < sys_nerr)
fprintf(stderr,"\n(%s)", sys_errlist[errno]);
fprintf(stderr,"\n");
exit(0);
}
$ cp essai toto
cp : Ouverture impossible de essai
(No such file or directory)
$ date > essai; chmod 0 essai
$ cp essai toto
cp: Ouverture impossible de essai
(Permission denied)

La fonction close rompt le lien entre un fichier et un descripteur, rendant celui-ci libre pour une
autre utilisation. Les fichiers sont fermés “automatiquement” en fin de processus (exit); mais ,
étant donné qu'un même processus ne peut ouvrir qu'un nombre limité de fichiers simultanément, il
29
peut s'avérer utile de fermer un ou plusieurs fichiers dans un processus pour “récupérer” des
descripteurs. Une autre utilisation de close est dans la redirection des entrées sorties standard.
int close(fd)
int fd; /* descripteur du fichier */
Terminer un programme par un appel de exit ou par une fin de la fonction main() ferme tous les
fichiers ouverts.
La fonction unlink supprime un fichier du système de fichier.
5.1.4. L'accès aléatoire -lssek
Les E/S sont normalement séquentielles: chaque read ou write se situe juste après le précédent.
Lorsque cela est nécessaire, il est néanmoins possible d'accéder à un fichier dans un ordre arbitraire.
La fonction lseek permet de se déplacer dans un fichier sans effectuer de lecture ou d'écriture.
int lseek(fd, depl, origine)
int fd; /* descripteur de fichier */
long depl; /* déplacement par apport à l'origine */
int origine; /* 0 : début,1: courant, 2: fin */
La position courante dans le fichier de descripteur fd devient depl qui est considéré par rapport à
l'origine. Ainsi, origine peut être 0, 1 ou 2 pour indiquer que depl est relatif au début, à la position
courante ou à la fin du fichier. La valeur retournée est la nouvelle position courante ou -1 en cas
d'erreur.
lseek(fd, 0L, 2); /* pour ajouter à la fin de fichier,il suffit de se positionner à la fin avant d'écrire*/
lseek(fd, 0L, 0); /* pour retourner au début du fichier */
pos = lseek(fd, 0L, 1); /* position courante */

get(fd, pos, buf, n) /* lit n octets depuis pos */


int fd, n;
long pos;
char *buf;
{ if (lseek(fd, pos, 0) == -1)
return -1;
else
return read(fd, buf, n);
}

5.1.5 Duplication d'un descripteur de fichier


Il est possible de définir des synonymes de descripteurs
int dup(fd)
int fd; /* descripteur de fichier */
fournit un nouveau descripteur ayant exactement les mêmes caractéristiques physiques que le
descripteur fd (qui doit nécessairement exister ). La primitive garantit que la valeur restituée est la
plus petite possible, celle-ci valant -1 en cas d'erreur. Ce mécanisme est essentiel pour rediriger les
fichiers standards sur un fichier ordinaire.
d=creat("toto", 0666);
close((2);
dup(d);
redirige la sortie erreur standard (descripteur 2) sur le fichier toto. dup(d) restitue la valeur 2; le
résultat n'est pas utilisé, mais l'important c'est que 2 aura les mêmes caractéristiques du descripteur
d et en fait donc un vrai synonyme.
Sur certaines versions du système, on trouve une primitive dup2 qui permet de forcer des
synonymies entre deux descripteurs donnés.
int dup2(d1, d2)

30
force d2 comme synonyme de d1. Si d2 était un descripteur existant, il est préalablement fermé. La
valeur de retour est -1 en cas d'impossibilité.

5.2. Le système de fichiers: les répertoires


Un répertoire est un fichier contenant une liste des noms de fichiers et une indication sur leur
localisation. La localisation est en réalité un index vers une table appelée tables des inodes. L'inode
d'un fichier est l'endroit où toute information le concernant (à l'exception de son nom) est rangée.
Une entrée du répertoire contient donc seulement deux informations: un numéro d'inode et un nom
de fichier. La description précise se trouve dans le fichier <sys/dir.h>:
$ cat /usr/include/sys/dir.h
# define DIRSIZ 14 /* long. max. du nom du fichier */
struct direct { /* structure d'une entrée du répertoire */
ino_t d_ino; /* numéro du inode */
char d_name[DIRSIZ]; /* nom du fichier */
};
Une description complète des types utilisés par le système se trouve dans <sys/types.h> qui doit
être inclue avant <sys/dir.h>.
Bien que un répertoire est un fichier, un certain nombre d'opérations sur le fichier sont interdites sur
les répertoires: ce sont toutes celles qui permettent d'y écrire directement. Le noyau les interdit afin
de maintenir l'intégrité du système de fichiers.
En ce qui concerne la création d'un catalogue, elle ne peut se faire que par l'intermédiaire de la
primitive mknod et sa suppression par la primitive unlink et dans le cas à condition d'être le super-
utilisateur.
# include <sys/direct.h>
catalogue(dir) /* affiche le répertoire de nom dir */
char *dir;
{ int fd;
struct direct nbuf;
if (!dir)
dir = ".";
if ((fd=open(dir,0)) == -1)
return -1;
while (read(fd, (char *) &nbuf, sizeof(struct direct)) > 0)
printf("%s \n",nbuf.name);
close(fd);
return 0;
}
Les fonctions POSIX
Pour des problèmes de portabilités utilisez les fonctions POSIX readdir, opendir, closedir. La
fonction readdir :
int readdir(unsigned int fd, struct dirent *dirp, unsigned int
count);
lit une structure dirent depuis le répertoire pointé par fd et la place en mémoire dans la zone pointée
par dirp. Le paramètre count est ignoré, au plus une structure dirent est lue. La structure dirent est
déclarée comme suit :
struct dirent
{
long d_ino; /* inode number */
off_t d_off; /* offset to this dirent */
unsigned short d_reclen; /* length of this d_name */
char d_name [NAME_MAX+1]; /* file name (nullterminated)*/
}
Pour lire le contenu d'un répertoire, les étapes suivantes sont nécessaires:

31
- Appelez opendir en lui passant le chemin du répertoire que vous souhaitez explorer. opendir
renvoie un descripteur DIR*, dont vous aurez besoin pour accéder au contenu du répertoire. Si une
erreur survient, l'appel renvoie NULL;
- Appelez readdir en lui passant le descripteur DIR* que vous a renvoyé opendir. À chaque appel,
readdir renvoie un pointeur vers une instance de struct dirent correspondant à l'entrée suivante dans
le répertoire. Lorsque vous atteignez la fin du contenu du répertoire, readdir renvoie NULL. La
struct dirent que vous optenez via readdir dispose d'un champ d_name qui contient le nom de
l'entrée.
- Appelez closedir en lui passant le descripteur DIR* à la fin du parcours.
Le programme suivant listdir affiche le contenu d'un répertoire. Celui-ci peut être spécifié sur la
ligne de commande mais, si ce n'est pas le cas, le répertoire courant est utilisé.
Exemple : listdir.c Affiche le Contenu d'un Répertoire
#include <assert.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main (int argc, char* argv[])
{
char* dir_path;
DIR* dir;
struct dirent* entry;
char entry_path[PATH_MAX + 1];
size_t path_len;
if (argc >= 2)
/* répertoire spécifié sur la ligne de commande s'il y a lieu. */
dir_path = argv[1];
else /* Sinon, utilise le répertoire courant. */
dir_path = ".";
/* Copie le chemin du répertoire dans entry_path. */
strncpy (entry_path, dir_path, sizeof (entry_path));
path_len = strlen (dir_path);
/* Si le répertoire ne se termine pas par un slash, l'ajoute. */
if (entry_path[path_len - 1] != '/') {
entry_path[path_len] = '/';
entry_path[path_len + 1] = '\0';
++path_len;
}
/* Démarre l'affichage du contenu du répertoire. */
dir = opendir (dir_path);
/* Boucle sur les entrées du répertoire. */
while ((entry = readdir (dir)) != NULL) {
const char* type; /* chemin complet=répertoire+nom de l'entrée. */
strncpy(entry_path+path_len,entry->d_name,sizeof(entry_path)-path_len);
printf ("%s\n",entry_path);
}

closedir (dir);
return 0;
}

32
5.3. Le système de fichiers: les inodes
Le fichier /usr/include/sys/stat.h contient la prédéfinition d'une structure C correspondant à la
structure physique d'un inode (i-noeud). Cette structure est définie de la manière suivante:
struct stat
{ dev_t st_dev; /* périphérique du inode */
ino_t st_ino; /*numéro du inode */
ushort st_mode /* bits de mode; bits 15 à 12: type, bits 11 à 0:droits d'accès*/
short st_nlink; /* nombre de liens */
ushort st_uid; /* numéro du propriétaire */
ushort st_gid; /* numéro du groupe */
dev_t st_rdev; /* numéro de la ressource pour un fichier spécial */
off_t st_size; /* taille du fichier en octets */
time_t st_atime; /* date de dernier accès */
time_t st_mtime; /* date de derniere modification (écriture) */
time_t st_ctime; /* date de dernière modification de caractéristique*/
};
Tous les champs employés dans la structure sont mémorisés en seconde depuis le 1er janvier 1970.
Les types utilisés sont prédéfinis dans le fichier /usr/include/types.h. Le champs st_mode contient
un ensemble d'indicateurs décrivant le fichier; pour simplifier, la définition de ces indicateurs fait
partie de <sys/stat.h >:
S_IFMT 0170000 /* type de fichier */
S_IFDIR 0040000 /* catalogue */
S_IFCHR 0020000 /* fichier special caractère */
S_IFBLK 0060000 /* fichier bloc special */
S_IFREG0100000 /* fichier ordinaire */
S_FIFO 0010000 /* tube ordinaire ou tube nommé (fifo) */
S_ISUID 04000 /* “set-uid” bit positionné (permission s utilisateur) */
S_ISGID 02000 /* “set-gid” bit positionné (permission s groupe) */
S_ISVTX 01000 /* “sticky” bit positionné*/
S_IREAD 00400 /* permission en lecture pour le propriétaire */
S_IWRITE 00200 /* permission en écrite pour le propriétaire */
S_IEXEC 00100 /* permission en éxecution/recherche pour le propriétaire */
st_mode : la valeur retourné dans ce champ sont des bits combinés indiquant le type de fichier, les
bits d'attributs et un résumé des droits d'accès.
15 11 10 9 8 0
type setuid setgid sticky r w x r w x r w x
type
15 14 13 12 type de fichier
0 0 0 0 donnée
0 1 0 0 répertoire
0 0 1 0 spécial caractère
1 0 0 0 spécial bloc
• Le bit STICKY (Save Text Image after execution) indique que le code doit être conservé en fin
d'exécution pour éviter de refaire appel au S.G.F. qu'il à nouveau exécuter.
• Les bits SetUid (resp. SetGid) permette de demander a ce qu'un utilisateur qui exécute le
fichier, acquière, temporairement, les droits d'accès du propriétaire (resp. le groupe).
L'inode d'un fichier est traité par deux fonctions système qui sont stat et fstat. stat utilise un nom de
fichier et retourne l'information de l'inode de ce fichier ou -1 s'il eu une erreur. fstat fait le même
travail depuis un descripteur de fichier.

33
int stat(nom, stbuf) int fstat(fd, stbuf)
char *nom; /* nom du fichier */
int fd; /* descripteur du fichier */
struct stat * stbuf; /* buffer de transfert */
/*verifmail: surveillance de la boite à lettres */
#include <stdio.h>
#include <sys/types.h>
#include <sys/types.h>
chhar *nompg;
char *maildir = "usr/spool/mail"
main(argc, argv)
int argc;
char *argv[];
{ struct stat buf;
char *nom, *getlogin();
int dertaille = 0;
nompg = argv[0];
if ((nom = getlogin()) == NULL)
error("Nom de connexion inconnu", (char *) 0);
if (chdir(maidir) == -1 )
erreor("cd impossible vers %s", maildir);
for (;;) {
if (stat(nom, &buf) == -1) /* pas de b. a. l. */
buf.st_size = 0;
if (buf.st_size > dertaille) /* pas de b. a. l. */
fprintf(stderr, "\nVous avez du courrier\007\n");
dertaille = buf.st_size;
sleep(60);
}
}

5.4. Gestion des processus


Ce chapitre est consacré aux différents appels au noyau qui permettent la gestion des processus à
savoir:
- la création de processus;
- les mécanismes de recouvrement;
- la synchronisation des processus.
5.4.1. Le processus UNIX
Un processus est défini comme étant une exécution d'un programme. Cette exécution nécessite des
ressources système (mémoire, cpu, routines, structures de données, ...) et s'effectue dans un
environnement déterminé. Les différents processus d'un système s'exécutent en parallèle (pseudo-
parallélisme) tout en échangeant des données et des signaux et peuvent donc être confrontés à des
problèmes de conflits d'accès à des ressources partagées (fichiers par exemple). Pour chaque
processus il y a deux modes d'exécutions possibles :
- le mode utilisateur: le moins privilégié. Dans ce cas le processus exécute ses instructions et utilise
ses propres ressources sans accéder à celles du noyau.
- le mode système : mode privilégié dans lequel un processus peut exécuter du code système. Un
processus en mode système n'est pas interruptible car le noyau est non préemptif (solution au
problème d'exclusion mutuelle en ce qui concerne l'accès au code noyau). Le passage en mode
système est provoqué par certains événements:
• événement internes : Synchrones à l'exécution d'un processus: appels systèmes, trappes
permettent un traitement exceptionnel (violation du segment, bus erreur, virgule flottante, ...).
• événement externes : pouvant se produire à tout moment pendant l'exécution d'un processus : se
sont les interruptions.
5.4.1.1. Structure d'un processus

34
Dans le système , l'existence d'un processus est matérialisée par l'allocation d'un descripteur ou
image mémoire du processus ("Process Control Block") qui définit totalement le processus. Cette
image est constitué de:
- un programme exécutable
sa structure est indiquée dans /usr/include/a.out
magic number
en-tête
du fichier
texte(code)
données
initialisée
données
non initialisée
(bss)
table
des symbole
L'en-tête vient après le "magic number" entier indiquant le type de fichier exécutable : paginé, texte
partageable, texte et données dans le même segment, texte et données séparés, ...; viennent ensuite
les tailles des différentes zones du programme.
- un contexte d'exécution
contient toutes les informations permettant de démarrer ou de poursuivre l'exécution du processus.
II est constitué d'un ensembles de trois environnements nécessaires pour l'exécution du processus:
• environnement utilisateur ou espace adresse utilisateur du processus, constitué du code, des
données, de la pile utilisateur et des mémoires partagées.
• environnement machine ou registres constitué des registres spéciaux, des registres de travail et
des registres de traduction des adresses virtuelles en adresses physiques.
• environnement système ou espace adresse constitué du code, de la pile système et des
différentes structures de données (table des processus, structure U, table des prérégions, table
des textes ...).
5.4.1.2. L'environnement d'un processus
L'environnement d'exécution d'un processus est précisé par un ensemble de chaînes de caractères.
Chacune des chaînes de caractères définissant cet environnement est de la forme nom=val où nom
est le nom d'une variable environnement (TERM, HOME,...) et val la valeur de cette variable. Cet
environnement est accessible via la variable externe environ, définie comme suit:
extern char **environ;
et utilisée par exemple, par les fonctions getenv qui retourne la valeur d'une variable et putenv qui
(re)définit une variable de l'environnement.
char *getenv(char *nom) /* nom de la variable */

char *putenv(char *chaine) /* pointe une chaîne de la forme nom=val */


5.4.1.3. Les désignations associées à un processus
Les processus vont avoir à se désigner entre eux. Dans ce but, un processus se voit attribuer, à sa
création, un numéro (entier positif) qui l'identifie sans ambiguïté. Ce numéro est dit processus-id.
- identification de processus
int getpid() int getppid()
fournissent respectivement le numéro du processus et celui du processus père.
- identification de groupe de processus
Un processus appartient à un groupe de processus identifié par un numéro. Lorsqu'un processus est
crée, il appartient au même groupe que son père mais il peut, par la suite, fonder son propre groupe.

35
La primitive getpgrp retourne le numéro de groupe de l'appelant. La primitive setpgrp affecte au
numéro de groupe de l'appelant la valeur du processus courant.
int setpgrp()
int getpgrp()

- identification de propriétaires
int getuid() int geteuid() int getgid() int getegid()

fournissent respectivement le propriétaire réel (uid), le propriétaire effectif (euid), le groupe réel
(gid) et le groupe effectif (egid) du processus.
Les numéros réels d'utilisateur et de groupe d'utilisateurs sont les numéros d'utilisateur et de
groupe d'utilisateurs pour le compte duquel le processus a été créé.
$ cat ident.c
main()
{ rpintf("uid=%d euid=%d gid=%d egid=%d\n", getuid(), geteuid(),
getgid(), getegid());}
$ ls -l ident
-rwxr-xr-x 1 mahdi 5896 Sep 26 09:65 ident
$ ident
uid=401 euid=401 gid=50 egid=50
$ ls -l ident
-rws--x--x 1 mahdi 5896 Sep 26 09:65 ident
$ ident
uid=401 euid=10 gid=50 egid=50
Le propriétaire effectif du processus est le propriétaire du fichier exécutable ident puisque le “Set-
uid bit” est positionné pour le fichier.
5.4.2. Création de processus: fork()
C'est cette primitive qui permet la création dynamique d'un nouveau processus qui s'exécute
concurrente avec le processus qui l'a créé. C'est la seule manière sous UNIX de créer un processus
à l'exception du processus 0 (swapper) créé à la “main” par le noyau au démarrage du système.
int fork()
entraîne la création d'un nouveau processus qui est une copie exacte du processus appelant. Cela
signifie que le processus ainsi créé, que nous baptiserons processus fils, hérite du processus qui l'a
créé que nous baptiserons processus père d'un certain nombre de ces attributs: le même code, une
copie de la zone de données, l'environnement, les différents propriétaires, la priorité, les
descripteurs de fichiers courant, et le traitement des signaux.
Le seul moyen de distinguer le processus fils du processus père est que la valeur de retour de la
fonction est 0 dans le processus fils créé et est égale dans le processus père au numéro du
processus fils nouvellement créé.

int n;
if ((n=fork()) == -1) {
fprintf(stderr," le fork est en échec \n");
exit(1);
}
else if (n == 0) {
printf("dans le processus fils valeur de fork = %d\n", n); /*processus fils */
printf("identification du processus fils: %d\n", getpid());
}
else {
printf("dans le processus père valeur de fork = %d\n", n); /*processus père */
printf("identification du processus père: %d\n", getpid());
printf("fin du processus père");
}

36
un tel programme entraîne la création de deux processus. Pratiquement après la fourche
(fork)deux processus vont se dérouler de manière concurrente sur le même code et processus fils va
recevoir une copie physique de la zone des données du processus père.
5.4.3. Synchronisation des processus
5.4.3.1. Terminaison d'un processus: exit()
void exit(int *status) /*status : compte rendu */

Un processus est terminé lorsque son image mémoire (parties non partageables) est désallouée. Un
processus peut se terminer de deux manières : soit volontairement par l'appel système exit, soit
involontairement par réception d'un signal, auquel cas le noyau fait appel à la routine exit().
Principales actions:
• ignorer tous les signaux pendant son exécution.
• fermer tous les fichiers ouverts, désallouer l'image mémoire du processus.
• si le processus père attend la mort de son fils par un wait, il est notifié par le signal SIGCLD de
la mort du fils et peut récupérer les 8 bits de poids faibles du status.
• si le processus père n'attend pas la mort de son fils (absence de wait), le processus (fils)
appelant devient zombie: il ne peut plus être actif; il dispose uniquement d'une entrée dans la
table des processus dont certains champs contiennent des informations de comptabilité.
• mise à 1 de l'identificateur (PPID) du processus père de tous les fils vivants ou zombies
(adoption par le processus init).
• libération des éléments d'IPC (mémoires partagées, sémaphores...).
5.4.3.2. Synchronisation de processus: wait()
La primitive wait permet de remédier à ce problème :
int wait(int *n) /* n :compte rendu */
provoque la suspension du processus appelant jusqu'à la terminaison de l'un de ses fils. Si un fils
s'est déjà terminé, l'appelant reprend immédiatement son exécution. wait retourne le PID du
processus fils. Le père (appelant) récupère, s'il le désire, une valeur entière précisant les
circonstances dans lesquelles ce fils s'est terminé:
• Processus stoppé (mode trace)
15 14 13 12 11 10 9 8 7 0
N° signal 0177
• Processus terminé volontairement par exit
15 14 13 12 11 10 9 8 7 0
argument de exit 0
• Processus terminé à la suite d'un signal
15 14 13 12 11 10 9 8 7 0
0 N° signal
$ cat sync1.c
main() /* fichier exécutable : sync1 */
{ if (fork() == 0) { /* processus fils */
printf("processus fils: %d\n", getpid());
}
else { /* processus père */
int m,n;
m=wait(&n);
printf("fin du processus %d avec valeur de retour de wait %d\n",n,m);
}
5.4.4. Exécution d'un autre programme : exec
Le but de la création d'un nouveau processus n'est pas de lui faire exécuter le même code que le
processus père. Bien au contraire, il s'agit de lui faire exécuter une tâche particulière. C'est bien par
exemple le cas du shell, qui pour exécuter une commande d'un utilisateur créé un fils (sous-shell),
37
qui se charge de l'exécution du fichier binaire de la commande tapée. Le chargement et l'exécution
de ce programme se traduisent par un recouvrement (overlay) de l'ancienne image mémoire héritée
du processus père.
En fait, avant le chargement et l'exécution du nouveau programme , le noyau libère la mémoire
associé à l'ancienne image. En conséquence, on ne revient jamais d'un exec qui a réussi.
La famille est divisé en deux “clans”: les execl pour lesquels le nombre des arguments du
programme lancé est connu, et les execv où il ne l'est pas. Il y a trois types d'appels exec dont
chacun peut s'exécuter dans un environnement particulier:
int execl(char *nom, *arg0, *arg1, ..., *argn, 0)
/* nom est un mchemin et arguments de la commande */
name est le chemin (pathname) du fichier contant le programme à exécuter, arg0 indique le nom
du fichier. Les autres arguments sont les éléments de argv (arguments du programme). La fin de la
liste est marquée par un argument 0.
execl("bin/cat,"cat", "/etc/inittab", 0);
fprintf(stderr, "impossible d'exécuter 'cat'\n");

int execle(char *name, *arg0, *arg1, ..., *argn, **envp, 0)


/* nom est un chemin , arguments et environnement */
variante associé à execl qui suppose toujours un nombre limité de paramètres, mais en plus transmet
un environnement déterminé au programme à exécuter.
char *envp[]={"X= bonjour","Y= merci"} ;
execle("/bin/cat", "cat", "/etc/motd", 0, envp);

int execv(char *nom, **argv)


/* nom :chemin, arguments pointés dans le tableau argv */
lancement de l'exécution du programme dont le nom est name et les arguments sont les différentes
chaînes pointées dans le tableau argv.
char *argv[]={« cat","/etc/inittab",} ;
execv("/bin/cat", argv);
execve suppose l'utilisation d'un environnement particulier.
int execlp(char *nom, *arg0, *arg1, ..., *argn, 0)
/* nom de la commande, éléments de argv */
Le programme à exécuter est recherché dans la variable shell PATH (similaire à execl).
execlp("date", "date", (char *) 0);
L'appel execvp est similaire à execv mais avec recherche du programme à exécuter dans PATH.

38
5.5. Signaux et interruptions
Le mécanisme des signaux permet de prendre en compte des événements pouvant survenir pendant
l'exécution d'un processus. Un événement à pour origine un incident matériel ou une erreur
provoquée par le processus (références mémoire incorrectes, erreur sur des opérations flottantes,
tentatives d'exécution d'instruction privilégiées)mais peut être également volontairement déclenché
pour contrôler l'exécution des processus. son déclenchement se traduit par l'émission d'un signal
vers ce processus. C'est un mécanisme fondamental de communication inter-processus.
Les signaux les plus communs sont les interruptions , qui sont générés par la frappe du caractère
DEL, par la frappe de FS (ctrl\), par la coupure de la ligne, ou par la commande kill. Le fichier
/usr/include/signal.h contient la liste des signaux accessibles, citons quelques uns gèrés par UNIX
System V:
SIGHUP 1 signal émis à tous les processus associés à un terminal lorsque se terminal se déconnecte
SIGINT 2 signal d'interruption simple: émis à tous les processus lorsque le caractère <INTR>
(DEL) est frappée (abondan du programme)
SIGQUIT 3 abondan, lorsque le caractère <QUIT> est frappée, avec création d'un fichier “core” qui
sera analysé grâce au metteur au point
SIGILL 4 émis en cas d'instruction illégale
SIGIOT 6 émis en cas de problème matériel
SIGKILL 9 destruction du processus
SIGBUS 10 en cas d'erreur sur le bus
SIGPIPE 13 en cas d'écriture sur un tube dans lecteur
SIGALARM 14 événement déclenché par l'horloge pour signaler l'expiration d'un délai
SIGIUSR1 16 signal à la disposition de utilisateur pour faire communiquer des processus
SIGIUSR2 17 comme SIGUSR1
..
5.5.1. Emission d'un signal
La primitive kill permet d'émettre un signal vers un ou plusieurs processus; ces processus doivent
avoir le numéro réel ou effectif d'utilisateur de l'appelant.
int kill(pid_t pid, int sig); /*pid :n° du processus ,sig: n° du signal */
• pid est nul, le signal est envoyé aux processus appartenant au même groupe que l'appelant.
• sig est nul, aucun signal n'est envoyé mais la valeur de retour de la fonction kill si pid est un
numéro de processus ou non (0 en cas de succès et -1 sinon).
5.5.1. Interception d'un signal
La réception d'un signal par un processus entraîne par défaut la terminaison de ce processus, cette
terminaison dans le cas de certains signaux entraîne la création d'un fichier core. La réception d'un
signal par un processus peut être provoquée par une erreur de programme (émission par le système),
une interruption de utilisateur depuis son terminal (SIGINT, SIGQUIT) ou un envoi par un
processus utilisateur (primitive kill).
Pour certains processus, il arrive quel traitement effectué , par défaut, lorsqu'un signal est reçu ne
soit pas le mieux adapté. Un processus, qui se trouve être dans ce cas là, peut appeler la primitive
signal :
void (*signal(int signum, void (*handler)(int)))(int);

int (*signal(sig, handler))


int sig; /* n° du signal
int (*handler)(); /* traitement en cas d'interception
du signal */
afin de demander :
• qu'un signal soit ignoré (se masquer contre l'événement);
• qu'un traitement défini par le processus lui-même soit exécuté lors de la prochaine interception
du signal (handler).
39
L'appel système signal installe un nouveau gestionnaire pour le signal numéro signum. Le
gestionnaire de signal est handler qui peut être soit une fonction spécifique de l'utilisateur, soit l'une
des constantes suivantes :
- SIG_IGN : Ignorer le signal
- SIG_DFL : Reprendre le comportement par défaut pour lesignal
L'argument entier qui est passé au gestionnaire de signal est le numéro du signal. Ceci permet
d'utiliser un même gestionnaire pour plusieurs signaux.
signal renvoie la valeur précédente du gestionnaire de signaux, ou SIG_ERR en cas d'erreur.
Le fichier <signal.h> contient la définition des différents arguments. Ainsi,
rétablit le comportement par défaut(terminaison des processus).
Si le second argument est le nom d'une fonction utilisateur (déclarée précédemment dans le même
fichier) celle-ci sera appelée lors de l'apparition du signal associé. Cette possibilité est souvent
utilisée pour permettre aux programmes de terminer proprement un travail avant de s'achever, par
exemple, en détruisant un fichier temporaire:
# define <signal.h>
char *tmpfile= "temp.XXXXXX";
main()
{ if (signal(SIGINT, SIG_IGN) != SIG_IGN)
signal(SIGINT, onintr);
mktemp(tempfile); /* créé un fichier temporaire */
/* le programme .....*/
exit(0);
}
onintr() /* nettoyage si interruption */
{ unlink(tempfile);
exit(1);
}
Une manière plus sophistiquée de traiter les interruptions , est de les interpréter comme des requêtes
de fin de travail en cours pour ne pas perdre le travail fait précédemment.
# define <signal.h>
# define <setjmb.h>
jmp_buf sjbuf;
main()
{
if (signal, SIG_IGN) != SIG_IGN)
signal(SIGINT, onintr);
setjmp(sjbuf); /* sauvegarde pile courante */
for (;; ) {
/* boucle de traitememnt courant.....*/
}
}

onintr() /* remise à l'état initial */


{
signal(SIGINT, onintr); /* pour la prochaine fois */
printf("\nInterruption\n");
longjmp(sjbuf, 0); /* retour etat initial */
}

5.5.2. Les signaux POSIX

Les signaux System V présentent les problèmes suivants :


- ne sont pas reliables, les signaux pouvant être perdus.
- leur sémantique est différente de la séemantique des signaux BSD.
POSIX reprends le méecanisme des signaux BSD.
40
La fonction sigaction peut être utilisée pour paramétrer l'action à effectuer en réponse à un signalLe
premier paramètre est le numéro du signal. Les deux suivants sont des pointeurs vers des structures
sigaction; la première contenant l'action à effectuer pour ce numéro de signal, alors que la seconde
est renseignée avec l'action précédente
int sigaction(int signum, const struct sigaction *act, struct sigaction
*oldact);
La structure sigaction est définie comme suit :
struct sigaction {
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
Si act est non nul, la nouvelle action pour le signal signum est définie par act. Si oldact est non nul,
l'ancienne action est sauvegardée dans oldact.
Le programme suivant utilise une fonction de gestion de signal pour compter le nombre de fois où
le programme reçoit le signal SIGUSR1, un des signaux utilisables par les applications.
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
sig_atomic_t sigusr1_count = 0;
void handler (int signal_number)
{
++sigusr1_count;
}
int main ()
{
struct sigaction sa;
memset (&sa, 0, sizeof (sa));
sa.sa_handler = &handler;
sigaction (SIGUSR1, &sa, NULL);
/* Faire quelque chose de long ici. */
/* ... */
printf ("SIGUSR1 a été reçu %d fois\n", sigusr1_count);
return 0;
}

Comme les signaux sont asynchrones, le programme principal peut être dans un état très
fragile lorsque le signal est traité et donc pendant l'exécution du gestionnaire de signal.
C'est pourquoi vous devriez éviter d'effectuer des opérations d'entrées/sorties ou d'appeler
la plupart des fonctions système ou de la bibliothèque C depuis un gestionnaire de signal.

Un gestionnaire de signal doit effectuer le minimum nécessaire au traitement du signal,


puis repasser le contrôle au programme principal (ou terminer le programme). Dans la
plupart des cas, cela consiste simplement à enregistrer que le signal est survenu. Le
programme principal vérifie alors périodiquement si un signal a été reçu et réagit en
conséquence.

L'appel sigprocmask est utilisé pour changer la liste des signaux actuellement bloqués.
int sigprocmask(int how, const sigset_t *set, sigset_t*oldset);

L'appel sigpending permet l'examen des signaux en attente (qui se sont déclenchés en
étant bloqués). Le masque de signaux en attente est stocké dans set.

41
int sigpending(sigset_t *set);
L'appel sigsuspend remplace temporairement le masque de signaux bloqués par celui
fourni dans mask puis endort le processus jusqu'à arrivée d'un signal.
int sigsuspend(const sigset_t *mask);

5.5.3. Attente d'un signal


int pause(void)
Un processus suspend son exécution en appelant la primitive pause. Cette suspension dure jusqu'à
la réception d'un signal non masqué. Après exécution de la fonction associée au signal reçu, le
processus, s'il n'a pas été détruit, reprend son traitement à l'instruction qui suit l'appel de la
primitive. pause renvoie toujours -1, et errno est positionné à la valeur EINTR.

5.5.4. Gestion de délais: les alarmes


La fonction alarm(n) a pour effet d'envoyer un signal SIGALARM au programme qui l'exécute, n
secondes plus tard. Cette fonction peut être utilisée pour s'assurer qu'un événement a bien eu lieu
dans un laps de temps limitée. Si l'événement se produit, le signal peut être supprimé, sinon, le
programme peut récupérer le contrôle en interceptant l'interruption.
unsigned int alarm(unsigned int nb_sec); /* nb_sec :durée du délais */
alarm(0) peut désactiver l'ancien appel de la fonction. La valeur de retour est le temps restant dans
l'horloge; si le paramètre sec est nul.
Le programme timeout qui exécute une commande; si cette commande ne se termine pas en un
temps prédéfini, sa terminaison est forcée.
$ cat timeout
#define <signal.h>
#define <stdio.h>
int pid; /* n° processus */
char *nompg;
main()
int argc,
char **argv;
{ int sec = 10, status, onalarm();
nompg = argv[0];
if (argc > 1 && argv[1][0] == '-') {
sec = atoi(&argv[1][1]);
argc--;
argv++;
}
if (argc < 2)
error("Utilisation : %s [-10] cmd", nompg);
if ((pid = fork()) == 0) {
execvp(argv[1], &argv[1]);
error("Exécution impossible de %s", argv[1]);
}
signal(SIGALARM, onalarrm);
alarm(sec);
if (wait(&status) == -1 || (status & 0177) != 0)
error("Arret de %s", argv[1]);
exit((status >> 8) & 0377);
}
onalarm() /* tue le fils si alarm a lieu */
{
kill(pid, SIGKILL);
}
timeout, dans l'exemple suivant, force l'exécution de watchfor dans un délai d'une heure.
$ timeout -3600 watchfor dmg &

42
L'exemple suivant montre l'utilisation de cette primitive pour forcer la lecture au clavier dans un
délai donné (ici 15 secondes).

43
# define <stdio.h>
# define <stdio.h>
# define <setjmb.h>
jmp_buf savebuf;
int c;
main()
{ int insist();
signal(SIGALARM, insist);
alarm(15);
while (getcfhar() != EOF) {
printf('caractère recu\n");
ecrire();
setjm(savebuf);
alarm(15);
}
alarm(0);
c = getchar();
signal(SIGALARM, insist);
longjmp(savebuf);
}
insist()
{
signal(SIGALARM, SIG-DFL);
printf("attention\n");
alarm(10);
c=getchar();
signal(SIGALARM, insist);
longjmp(savebuf);
}
ecrire()
{ printf("15 secondes\n");
}

La bibliothèque standard Unix System V contient une fonction sleep qui suspend l'exécution de
l'appelant pour une durée déterminée. Cette fonction est réalisée en interceptant le signal
SIGALRM et en appelant les primitives système alarm puis pause.
unsigned int sleep (unsigned int nb_sec);/* nb_sec: la durée */

6. La Communication Sous UNIX


6.1 Tubes et Tubes Nommés
Les tubes sont un mécanisme de communication qui permet de réaliser des communications entre
processus sous forme d'un flot continu d'octets. Les tubes sont un des éléments de l'agrément
d'utilisation d'UNIX. C'est ce mécanisme qui permet l'approche filtre de la conception sous UNIX.
Mécanisme de communication lié au système de gestion de fichier, les tubes nommés ou non sont
des paires d'entrées de la table des fichiers ouverts, associées à une inode en mémoire gérée par un
driver spécifique. Une entrée est utilisée par les processus qui écrivent dans le tube, une entrée pour
les lecteurs du tube.
L'opération de lecture y est destructive !
L'ordre des caractères en entrée est conservé en sortie (premier entré premier sortie).
Un tube a une capacité finie : en général le nombre d'adresses directes des inodes du SGF (ce
qui peut varier de 5 à 80 Ko).
6.1.1. Les tubes ordinaires (pipe)
Un tube est matérialisé par deux entrées de la table des ouvertures de fichiers, une de ces entrées est
ouverte en écriture (l'entrée du tube), l'autre en lecture (la sortie du tube). Ces deux entrées de la
table des fichiers ouverts nous donnent le nombre de descripteurs qui pointent sur elles. Ces valeurs
peuvent être traduites comme :

44
nombre de lecteurs= nombre de descripteurs associés à l'entrée ouverte en lecture. On ne peut pas
écrire dans un tube sans lecteur.
nombre d'écrivains= nombre de descripteurs associés à l'entrée ouverte en écriture. La nullité de
ce nombre définit le comportement de la primitive read lorsque le tube est vide.
6.1.1.1. Création de tubes ordinaires
Un processus ne peut utiliser que les tubes qu'il a créés lui-même par la primitive pipe ou qu'il a
hérités de son père grâce à l'héritage des descripteurs à travers fork et exec.
#include <unistd.h>
int pipe(int p[2]);

Figure 1 : Ouverture d'un tube


On ne peut pas manipuler les descripteurs de tubes avec les fonctions et primitives : lseek,
ioctl, tcsetattr et tcgetattr, comme il n'y a pas de périphérique associé au tube (tout
est fait en mémoire).
Héritage d'un tube dans la figure2 : le processus B hérite des descripteurs ouverts par son père A et
donc, ici, du tube.

Figure 2 : Héritage d'un tube


Dans la Figure3 les descripteurs associés aux tubes sont placés comme descripteurs 0 et 1 des
processus A et B, c'est à dire la sortie de A et l'entrée de B. Les autres descripteurs sont fermés pour
assurer l'unicité du nombre de lecteurs et d'écrivains dans le tube.

45
Figure 3 : Redirection de la sortie standard de A dans le tube et de l'entrée standard de B dans le
tube, et fermeture des descripteurs inutiles
6.1.1.2. Lecture dans un tube
On utilise l'appel système read.
int nb_lu;
nb_lu = read(p[0], buffer, TAILLE_READ);
Remarquer que la lecture se fait dans le descripteur p[0].
Comportement de l'appel :
Si le tube n'est pas vide et contient taille caractères :
lecture de nb_lu = min(taille, TAILLE_READ) caractères.
Si le tube est vide
Si le nombre d'écrivains est nul
alors c'est la fin de fichier et nb_lu est nul.
Si le nombre d'écrivains est non nul
Si lecture bloquante alors sommeil
Si lecture non bloquante alors en fonction de l'indicateur
O_NONBLOCK nb_lu= -1 et errno=EAGAIN.
O_NDELAY nb_lu = 0.

6.1.1.3. Ecriture dans un tube


nb_ecrit = write(p[1], buf, n);
L'écriture est atomique si le nombre de caractères à écrire est inférieur à PIPE_BUF, la taille du
tube sur le système. (cf <limits.h>).
Si le nombre de lecteurs est nul
envoi du signal SIGPIPE à l'écrivain.
Sinon
Si l'écriture est bloquante, il n'y a retour que quand
les n caractères ont été écrits dans le tube.
Si écriture non bloquante
Si n > PIPE_BUF, retour avec un nombre inférieur à n
éventuellement -1 !
Si n PIPE_BUF
et si n emplacements libres, écriture nb_ecrit = n
sinon retour -1 ou 0.

6.1.1.4. Interblocage avec des tubes


Un même processus a deux accès à un tube, un accès en lecture, un accès en écriture et essaie de
lire sur le tube vide en mode bloquant ⎯→ le processus est bloqué indéfiniment dans la primitive
read.
Avec deux processus : deux tubes entre les deux processus, tous les deux bloqués en lecture ou tous
les deux bloqués en écriture, tous les deux en attente d'une action de l'autre processus.

46
6.1.2. Les tubes nommés
Les tube nommés sont des tubes (pipe) qui existent dans le système de fichiers, et donc peuvent être
ouverts grâce à une référence.
Il faut préalablement créer le tube nommé dans le système de fichiers, grâce à la primitive mknod
(mkfifo), avant de pouvoir l'ouvrir avec la primitive open.

int mknod(reference, mode | S_IFIFO,0);


est construit comme le paramètre de mode de la fonction open.
En POSIX, un appel simplifié :
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *ref, mode_t mode);

On peut créer des FIFOs à partir du shell grâce à


mkfifo [-p] [-m mode] ref ...
L'ouverture d'un tube nommé se fait exclusivement soit en mode O_RDONLY soit en mode
O_WRONLY, ainsi le nombre de lecteur et d'écrivain peut être comptabilisé.

6.1.2.1 Ouverture et synchronisation des ouvertures de tubes nommés


Il y a automatiquement synchronisation des processus qui ouvrent en mode bloquant un tube
nommé.
L'opération d'ouverture sur un tube nommé est bloquante en lecture.
Le processus attend qu'un autre processus ouvre la fifo en écriture.
L'ouverture en écriture est aussi bloquante, avec attente qu'un autre processus ouvre la fifo en
lecture. L'ouverture bloquante se termine de façons synchrone pour les deux processus.
Ainsi un unique processus ne peut ouvrire à la fois en lecture et écriture un tube nommé.
En mode non bloquant (O_NONBLOCK, O_NDELAY), seule l'ouverture en lecture réussit dans
tous les cas. L'ouverture en écriture en mode non bloquant d'un tube nommé ne fonctionne que si un
autre processus a déjà ouvert en mode non bloquant le tube en lecture, ou bien qu'il est bloqué dans
l'appel d'une ouverture en lecture en mode bloquant. Ceci pour éviter que le processus qui vient
d'ouvrir le tube nommé, n'écrive dans le tube avant qu'il n'y ait de lecteur (qu'un processus ait
ouvert le tube en lecture) et ce qui engendrerait un signal SIGPIPE (tube détruit), ce qui n'est pas
vrai car le tube n'a pas encore été utilisé.

6.1.2.2. Suppression d'un tube nommé


L'utilisation de rm ou unlink ne fait que détruire la référence, le tube n'est réellement détruit que
lorsque son compteur de liens internes et externes est nul.
Une fois que tous les liens par référence sont détruits, le tube nommé devient un tube ordinaire.

6.1.2..3. les appels popen et pclose


Une interface plus facile pour lancer un co-processus est proposée avec les primitives popen et
pclose.

47
FILE *popen (const char *commande, const char *type);

int pclose (FILE *stream);

La fonction popen() engendre un processus en créant un tube (pipe), exécutant un fork(), et en


invoquant le shell. Comme un tube est unidirectionnel par définition, l'argument type doit indiquer
seulement une lecture ou une écriture, et non pas les deux. Le flux correspondant sera ouvert en
lecture seule ou écriture seule.

6.2 les Sockets

48