:: News .:. Documents .:. Forum .:. Downloads .:. Bibliographie .:. Liens .:. Contact  :: 


Home
  :. News
  .: Documents
    .: Notions
    .: Protocoles
    .: Sécurité
    .: Architecture
    .: Prog
    .: Systèmes
  :. Forum
  .: Downloads
  :. Bibliographie
  .: Liens
  :. Contact

Chat

  Nickname:


irc: #guill.net

Forum



 
Programmation Unix : Signaux, processus et tubes  
 

 

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 ?!?''

Voir page des solutions

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 ++)
...

Voir page des solutions

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 */
...

Voir la page des solutions

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.

Voir la page des solutions

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.

Voir page des solutions

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!

Voir page des solutions

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...

Daniel Schang
 




Sondage

Quel est votre connexion à Internet aujourd'hui ?
 
RTC 56Kbps
ADSL simple de 128 à 2048 Kbps
ADSL + Téléphonie (+TV) de 128 à 2048 Kbps
ADSL simple jusqu'à 20Mbps
ADSL + Téléphonie (+TV) jusqu'à 20Mbps
Autres (RNIS, Satellites bi-directionnel...)
Total :
3241

Recherche


Docs
   Pflogsumm (Analyseur de log mail pour Postfix)
   Proftpd (Mise en service d'un serveur FTP avec proftpd sous Linux)
   Openldap (Mise en service d'un serveur LDAP sous Linux)
   Gestion des périphériques en c++ builder (Communication RS232 en C++ Builder)
   Les sockets windows (Windows Sockets : un cours accéléré)

guill.net©1999-2024