Programmation
Unix : Signaux, processus et tubes
Merci
à l'auteur : Daniel Schang
UNIX
et langage C
Rappel: la commande cc
-o prog prog.c correspond à la demande de construction d'un
binaire exécutable de nom prog à partir du fichier
source prog.c.
Au vu de la très forte
imbrication qui existe entre le langage C et UNIX il est parfois très
utile de pouvoir passer à un programme C des paramètres.
Quand on appelle le maindu
programme C, deux arguments lui sont passés en fait:
le premier, baptisé
conventionnellement argc signifiant nombre d'arguments (en anglais:
argument
count) représente le nombre d'arguments de la ligne de commande
qui a appelé le programme,
le second, baptisé
conventionnellement argv signifiant vecteur d'arguments (en anglais:
argument
vector) est un pointeur sur un tableau de chaînes de caractères
qui contiennent les arguments, à raison de 1 par chaîne.
L'entête du programme
sera donc celle-ci:
main (int argc, char
*argv[]) {
}
Prenons un exemple, imaginons
que votre programme s'appelle essai.c et qu'il a été
appelé de la manière suivante sous UNIX: essai bonjour,
maître. Par convention, argv[0] est le nom par lequel
le programme a été appelé (ici: essai) et, par conséquent,
argc
vaut au moins 1. Si argc vaut 1, cela signifie qu'il n'y a aucun
argument après le nom du programme sur la ligne de commande. Dans
l'exemple ci-dessus, argc vaut 3 et argv[0], argv[1]
et argv[2] valent respectivement ``essai'', ``bonjour,'' et ``maître''.
Le premier argument optionnel est argv[1] et le dernier est argc-1;
de plus, la norme spécifie que argv[argc] doit être
un pointeur nul.
Tout ceci étant précisé,
on vous demande d'écrire un tel programme C qui affiche la liste
des arguments qui lui sont passés en paramètres. Si le programme
ne reçoit aucun paramètre, le programme renverra un message
du type ``pourriez-vous me donner quelques paramètres s.v.p ?!?''
Solution
Les
signaux sous UNIX
Présentation
des signaux
Sous UNIX, les processus
peuvent s'envoyer des messages appelés signaux. Ces derniers ont
des origines diverses, ils peuvent être :
retransmis par le noyau
: division par zéro, overflow, instruction interdite,
envoyés depuis le
clavier par l'utilisateur ( touches : CTRL+Z, CTRL+C,... )
émis par la commande
kill depuis le shell ou depuis le C par l'appel à la primitive kill.
Pour voir la totalité
des messages envoyables sous UNIX, faites:
kill -l
On peut remarquer que tous
les signaux ont leur nom qui commence par ``SIG''. Descriptif de quelques
signaux:
SIGFPE : une erreur
arithmétique est survenue.
SIGKILL : signal
de terminaison. Le processus qui reçoit ce signal doit se terminer
immédiatement.
SIGINT : frappe du
caractère int sur le clavier du terminal: (CTRL + C).
SIGUSR1 et SIGUSR2
: signaux qui peuvent être émis par un processus utilisateur.
SIGSTP : frappe du
caractère de suspension sur le clavier: (le fameux CTRL +Z).
SIGCONT : signal
de continuation d'un processus stoppé: lorsque vous tapez bg
après avoir fait un CTRL +Z
SIGHUP : le processus
leader de votre session est terminé: typiquement, lorsque vous vous
loggez, un processus vous concernant est crée; les autres processus
qui sont crées ensuite dpéendent de celui-ci. Une fois que
le processus leader de votre session est terminé (par un logout
par exemple), le message SIGHUP est envoyé à tous
ses fils qui vont se terminer à leur tour.
L'envoi de signaux
{En shell: kill
-num_du_signal num_du_processus
{En langage C:
#include <signal.h>
int kill (pid_t pid,
int sig);
valeur de pid: si
>0, le signal ira en direction du processus de numéro pid
(nous laisserons de côté ici les cas pid =0, pid<0...)
retour du kill:
renvoie 0: l'appel s'est
bien déroulé
renvoie -1: l'appel s'est
bien déroulé
valeur de sig:
<0 ou >NSIG: valeur incorrecte
0: pas de signal envoyé
mais test d'erreur: sert à tester l'existence d'un processus de
numéro pid par exemple
sinon: signal de numéro
sig
Le traitement des
signaux
Le destinataire du signal
peut exécuter une fonction à la réception d'un signal
donné. Ainsi, un appel à signal(num_du_signal,fonction)
récupère un signal de numéro num_du_signal.
{Remarque: sur les
UNIX de la famille Berkeley, par exemple Solaris de Sun, après exécution
de la fonction spécifique définie par l'utilisateur, l'option
par défaut est rétablie. Si on veut conserver l'option spécifique
il faut rappeler signal(num_sig, fonction) dans fonction. Voici
un exemple que vous pouvez tester en:
lançant le programme
puis en pressant sur (CTRL + C),
supprimant la ligne : /*
ligne 1 */ et retester le programme pour bien comprendre la remarque.
#include <signal.h>
void trait_sig_int ()
{
printf("Bien reçu SIGINT, mais je m'en moque \n");
signal (SIGINT,trait_sig_int); /* ligne 1 */
}
main () {
signal (SIGINT,trait_sig_int);
while (1);
}
Exercice
1 : écrire un programme qui ignore tous les signaux.
Piste pour l'écrire:
...
printf("Coucou,
recu signal %d ! mais je l'ignore !\n", Numero);
...
for (Nb_Sig
= 1; Nb_Sig < NSIG ; Nb_Sig ++)
...
Solution
Lui envoyer des signaux depuis
une autre fenêtre par la commande UNIX:
kill
-num_signal num_processus.
Ce programme les ignore-t-il
tous ?
Exercice
2 : écrire un programme qui :
Affiche son numéro
(pid) via l'appel à getpid(),
Traite tous les signaux
par une fonction fonc qui se contente d'afficher le numéro du signal
reçu.
Traite le signal SIGUSR1
par une fonction fonc1 et le signal SIGUSR2 par fonc2.
fonc1 affiche le numéro
du signal reçu et la liste des utilisateurs de la machine (appel
à who par system("who"))
fonc2 affiche le numéro
du signal reçu et l'espace disque utilisé sur la machine
(appel à df . par system("df ."))
Lancer le programme et lui envoyer
des signaux, dont SIGUSR1 et SIGUSR2, depuis une autre fenêtre, à
l'aide de la commande kill.
Le cas de la fonction
"alarm''
La
primitive alarm
unsigned int
alarm (unsigned int secondes)
demande au système d'envoyer
au processus courant le signal SIGALRM après la durée secondes.
Le comportement du processus à la réception du signal SIGALRM
est le suivant:
if secondes=0
then
annuler la demande antérieure
if handler associé
à SIGALRM non défini then terminer le processus
Exercice
1
Ecrire un programme alarm
qui demande à l'utilisateur de taper un nombre. Afin de ne pas attendre
indéfiniment, vous utiliserez le signal SIGALRM pour que le programme
ne soit pas bloqué plus de 5 secondes.
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
/* pour la fonction alarm */
...
Solution
Exercice
2
Écrire un programme
qui affiche la succession des lettres de l'alphabet de telle sorte qu'une
nouvelle lettre apparaisse toutes les secondes.
Solution
Processus
Rappels
La commande ps affiche
la liste de vos processus en cours.
La commande ps -x
détaille davantage cette liste.
La commande top effectue,
en temps réel un affichage encore plus précis de l'ensemble
des processus en cours d'exécution.
Créer le script essai
dont le contenu est le suivant:
date
sleep 2
essai
Lancer le script essai
par essai &. Le caractère & permet de lancer le processus
en arrière-plan (background), c'est-à-dire qu'il se déroulera
en tâche de fond. Essayez pour vous amuser à taper d'autres
commandes telles que ls -l, etc. Vous verrez alors, qu'imperturbable,
le script essai vous affichera toutes les 2 secondes la date et
l'heure.
Pour terminer le processus
essai qui ne s'arrête avec aucune combinaison de touches du clavier,
voyons une solution:
Taper fg
le processus revient alors
en avant plan (fg pour foreground).
il suffit ensuite de faire
CTRL + C pour arrêter le processus.
A présent, tapez:
ps
puis ps -x. Enfin, relancez le processus essai en background.
On pourrait dès lors
imaginer une autre solution pour tuer le processus essai qui consisterait
à:
1°) retrouver
le numéro de PID (processus identity) associé au processus
essai,
2°) tuer ce processus
par kill -9 <bon numéro de processus>.
!!! Attention, ici, on a malheureusement
du mal à retrouver le bon processus à tuer. Aussi ne faites
pas de kill à la légère car si vous vous trompez de
numéro de PID, vous risquez d'être purement et simplement
coincé... si cela arrive, appelez nous. Bref, il semble plus sage
de revenir à la solution du passage en avant-plan pour venir à
bout de ce processus.
La création
de processus
La
primitive fork
Afin de créer des
applications plus évoluées reposant sur le système
d'exploitation, il plus facile d'écrire un programme en C
qui réalise éventuellement des appels système.
La primitive fork
permet la création dynamique d'un nouveau processus qui s'exécute
de façon concurrente avec le processus qui l'a crée. Tout
processus UNIX, excepté le processus originel (de numéro
0), est crée par un appel à cette primitive.
Relisez le détail
du cours portant sur la primitive fork.
Rappels
de base pour l'écriture de programmes en C
Programme de saisie et
de réaffichage d'un double:
main()
{
double d;
scanf("%d'',&d); /* bien passer une adresse avec le &*/
printf("%d",d);
}
Premier
programme
Afin de bien comprendre le
fonctionnement de la primitive fork, tapez l'exemple suivant dans
le fichier prog.c et executez le (rajoutez les anti-slash ``n''
où il faut car ils se sont perdus sous html):
#include <stdlib.h>
#include <stdio.h>
main() {
pid_t pid;
switch(pid=fork()){
case -1: perror("Création
de processus");
exit(2);
case 0:
/* on est dans le processus fils */
printf("FILS valeur de fork =%d ",pid);
printf("FILS je suis le processus %d de père %d",getpid(), getppid());
printf("FILS fin du processus fils");
exit(0);
default:
/* on est dans le processus père */
printf("PERE valeur de fork = %d ",pid);
printf("PERE je suis le processus %d de père %d",getpid(), getppid());
printf("PERE fin du processus père");
}
}
Quelques commentaires:
perror : sert à
renvoyer un message d'erreur plus explicite que celui qui serait généré
avec un simple printf.
Dans certains cas, le processus
fils renvoie 1 comme valeur de numéro père alors que visiblement
le numéro de son père n'est pas 1. La raison en est simple:
si le père a fini son exécution avant le fils, le système
octroie alors le numéro du premier processus crée qui vaut
1.
Compilez ce programme. Exécutez
le.
Synchronisation
du père et du fils
Etant donné que le
processus père et le processus fils se déroulent de manière
concurrente, il se peut très bien que le fils termine son exécution
avant celle du père.
1°) Modifier le précédent
programme en introduisant une temporisation afin que le fils termine après
le père quoi qu'il arrive.
2°) A présent,
on souhaite que quoi qu'il advienne, le processus père doit finir
après le fils tout en conservant la temporisation qui se trouve
dans le ``case'' du fils.
Pour ce faire, nous allons
utiliser la commande wait:
pid_t wait(int *pointeur_status);
pid_t est un type qui assure la portabilité d'une plateforme UNIX
à l'autre (dans la pratique son type se confond souvent avec le
type int).
La sémantique de cette
primitive est la suivante:
si le processus ne possède
aucun fils, la primitive renvoie la valeur -1,
si le processus appelant
possède des fils mais aucun fils zombi, le processus est alors bloqué
jusqu'à ce que l'un de ses fils devienne zombi.
A l'aide de cette nouvelle commande
modifier votre programme afin que le père termine après le
fils quoiqu'il se produise dans la partie ``case'' du fils.
Solution
Signaux
et fork
Exercice: réaliser
un programme reposant sur les fork tel que le processus père
envoie un signal SIGUSR1 à son processus fils après
avoir testé son existence.
N'oubliez pas d'#include!
Solution
Primitives du
système
Il est possible depuis un
programme C de demander l'exécution d'une commande shell par un
appel à la fonction standard:
system (const
char *chaine_commande);
Créez un tel programme
qui lancera la commande nedit ou lancera le script top.
Les tubes
Introduction
Les tubes ordinaires (ou
non nommés) constituent un mécanisme permettant à
des processus de se communiquer des informations d'un volume significatif.
Le principe est le suivant:
on crée un tube,
ce tube fournit un mécanisme
de communication unidirectionnel entre deux processus. Ainsi, une
fois le tube crée entre deux processus, il sera possible à
un processus A d'écrire à une extrémité du
tube tandis qu'un autre processus B pourra lire à l'autre extrémité
ce qui a été écrit.
Ainsi qu'on va le voir tout
de suite, les tubes permettent une synchronisation entre deux processus:
le processus B devant attendre par exemple que le processus A ait écrit
dans le tube.
La
primitive de création : pipe
Un appel à la primitive
int pipe (int
p[2]);
crée un tube p : p[1]
correspond au descripteur pour écrire dans le tube et p[0] permettra
de lire dans le tube. Si le tube a bien été crée,
la valeur de retour de la fonction pipe sera 0, dans le cas contraire,
la fonction renverra -1.
Voici un programme qui illustre
le fonctionnement des tubes:
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int p[2];
char ch[10]; /* buffer
qui servira à envoyer ou recevoir des données
main() {
if (pipe(p) == -1) {
perror("Creation du tube");
exit(2);}
printf("Ecrivons dans le tube");
write(p[1],"1234",4); /* 4 : longueur de ce qui est écrit dans le
pipe */
read(p[0],&ch,4); /* lire lau maximum 4 caractères dans le pipe
*/
printf("A l'autre extremite, chaine recue : %s",ch);
}
write(p[1],"1234",4);
Remarques:
on lit/écrit dans
le tube des caractères ou chaines de caractères,
read(p[0],&buf,TAILLE_BUF);
correspond à une demande de lecture d'au plus TAILLE_BUF
caractères,
write(p[0],buf,TAILLE_BUF);
écriture des TAILLE_BUF premiers caractères de buf.
Ce qui vous est demandé:
Tester cet exemple.
Modifier le programme précédent.
Il s'agit d'utiliser la notion de pipe et de fork pour faire
en sorte qu'un processus père envoie à son fils des informations,
le fils affiche alors à l'écran ce qu'il a reçu.
Utiliser la notion de pipe
et de fork pour faire en sorte que le jeu de morpion joue tout seul
contre lui même. On peut ainsi imaginer créer 2 processus:
un processus père, un processus fils. Ces deux processus joueront
ensemble en s'informant mutuellement des coups joués par le biais
de deux pipe.
Attention aux phénomènes
de blocage. En effet, un processus qui tente de lire dans un tube où
rien ne se trouve sera bloqué jusqu'à ce que des informations
à lire s'y trouvent...
Suite