retour cours

Master Ingéniérie et Téchnologies - IPST ULP informatique

TCP19 2005-2006 cours génie informatique

Séance n° 5 : Programmation distribuée




table des matières :

  1. multi tâche - multi utilisateur
  2. fork
  3. threads (processus légers)
  4. dialogue entre tâches
    1. signaux
    2. shm, mémoire partagée
    3. tubes - pipes
    4. mutex
    5. semaphores
  5. application (Grafcet)

MULTI TACHE – MULTI UTILISATEUR

Un ordinateur (mono processeur) ne peut faire qu'une chose à la fois. Historiquement, on a débuté avec du mono tâche : une tâche tourne jusqu'à ce qu'elle soit terminée. Pour éviter les pertes de temps, on a créé le traitement par lots (batch) : toutes les réponses aux questions sont préparées (dans l'ordre) à l'avance. Le programme est mis en file d'attente. Quand c'est son tour il monopolise l'ordinateur, fait tous ses affichages (sur fichier ou imprimante) puis rend la main. Mais toutes les opérations ne vont pas à la même vitesse, en particulier les E/S sont lentes. Quand on veut en faire une, il serait intéressant que pendant le temps d'accès (de l'ordre de la ms par ex) on fasse quelques milliers d'instructions d'une tâche en attente. Pour que les utilisateurs ne s'impatientent pas, on attribue une durée maxi à chaque tâche, on les fait donc progresser en pseudo simultané (time sharing).

les tâches sont en 3 états (pour simplifier) : en cours, attend son tour, attend un évènement extérieur (E/S). L'ordonanceur (le programme central de l'OS) gère les priorités. Celles-ci peuvent être variables (une tâche en cours perd de la priorité, celles en attente en gagnent). Attention, le temps de changement de tâche n'est pas nul, deux tâches n'ayant rien à attendre on intérêt à être lancées successivement. De même faire autre chose sur l'ordinateur parce qu'on trouve qu'il "mouline" n'est pas toujours une bonne idée.

ps permet de voir les processus actifs.

Les tâches en attente peuvent être "swappées", c'est à dire mises sur disque dur pour libérer de la mémoire pour la tâche en cours.

Exemple de problème compliqué : il me faut un carnet et un crayon pour exécuter une tâche, j'ai trouvé un crayon et attend que quelqu'un rende un carnet, un autre a un carnet et attend un crayon.

FORK

           #include <unistd.h>
           pid_t   pid;

           pid = fork ();

           if (pid > 0) {
                /* Processus père      */
           } else if (pid == 0) {
                /* Processus fils      */
           } else {
                /* Traitement d'erreur */
           }

On crée un processus par copie de l'actuel (y compris variables globales). Les deux avancent à leur vitesse et se termineront quand ils auront fini leur travail (quel que soit celui qui finit en premier, le 2nd continue). Le fils (et le père) peuvent lancer d'autres fork.

Au démarrage du fork, tout est identique (variables...), puis chacun fait ce qu'il veut, s'arrête quand il veut... C'est donc une bonne solution quand les 2 processus sont indépendants. Cela ne l'est plus tellement quand on veut partager de la mémoire, attendre que l'autre soit terminé...

Un fork peut exécuter une commande (system ou exec).

Quand un processus veut accéder au clavier (cin), c'est lui qui le réserve. Tant qu'on n'a pas entré la valeur, ce processus est en attente. Les autres processus qui demanderaient le clavier se mettent en file d'attente (mais les cout qui précèdent sont faits immédiatement). Peut-être serait-il plus malin de créer un processus qui gère les E/S et utiliser de la communication inter-processus pour transmettre l'information ?

rq : pour fork, chaque processus a son buffer de clavier (si je tape plusieurs réponses sur la même ligne) alors qu'avec les threads il n'y en a qu'un (commun).

Idem pour un fichier. S'il a été ouvert avant le fork, les processus écriront dedans dans l'ordre où ils avancent (dépend donc surtout de l'ordonanceur). On évite les problèmes si chaque processus a ses propres fichiers.

Un père peut se bloquer et attendre la fin d'un fils :
waitpid (pid_du_fils, &val_retour, NULL);

wait(&val_retour); attend la fin d'un fils (n'importe lequel) et retourne son pid.

voir le man car il peut aussi être débloqué par des signaux.

exemple : tst-fork.cpp

THREADS

Contrairement au fork, un thread est un processus "léger". Un fork double tout : le code, les variables globales, la pile (variables locales, arguments). Le thread ne recrée que la pile. On gagne en temps et en place à chaque création (voire activation si on swape) de thread. Et surtout les variables globales sont communes. Mais tous les fils sont tués à la fin du père, même s'ils n'étaient pas terminés).

#include <pthread.h>
//ici on peut déclarer des variables globales partagées

void *mon_processus (void * arg)
{
  // ici on met un traitement
  pthread_exit (0);
}

main (void)
{
  pthread_t th1, th2;
  void *ret;
//lancer les thread
  pthread_create (&th1, NULL, mon_processus, (void*)"1") ;
  pthread_create (&th2, NULL, mon_processus, (void*)"2") ;

// ici le main peut travailler en même temps que les threads

// attendre la fin du thread (la fin du main tue les threads)
  pthread_join (th1, &ret);
  pthread_join (th2, &ret);
}

On le compile par "g++ tst.cpp -D_REENTRANT -o tst -lpthread"

Exemple : tst-thread.cpp

SIGNAUX

Divers processus tournant en même temps peuvent avoir besoin de communiquer. Une solution est possible par les signaux, gérés par l'OS (et l'ordonnanceur). Le principe est qu'un processus émet un signal en direction d'un autre processus, qui en général l'attendait.

exemples : KILL (tue un processus sans autre forme de procès), STOP met un processus en attente (CONT le relance), CHLD (le père repart quand le processus fils se termine), USR1 et USR2 libres... voir man 7 signal.

Les signaux permettent surtout de synchroniser des processus. Mais ils ne sont pas prévus pour transmettre une valeur (mais il peut prévenir qu'une donnée est dans de la mémoire partagée).

On envoie un signal à un processus par kill(num_process,num_signal);

Un processus qui doit recevoir des signaux doit prévoir une fonction qui sera appelée lors d'un signal (et préciser, avant à quel signal est associée quelle fonction grace à l'appel de signal(num_sig,nom_fonc)

pause() bloque un processus jusqu'à l'arrivée d'un signal.

(voir man 2 signal, man 2 pause et man 2 kill).

rq : alarm(délai) permet d'envoyer un signal SIGALARM après un délai donné en secondes.

Voici un exemple utilisant des signaux pour envoyés entre deux tâacirc;ches. C'est un jeu interactif : le jeu du plus ou moins : Soit une tâche A qui décide d'un nombre à découvrir, et ouvre un shm (zône de mémoire partagée). A tout autre processus (par ex B) qui y met une proposition, A rendra -1 si elle est trop petite, 1 si trop grande, 0 si c'est le nombre à découvrir. Pour prévenir A qu'une valeur est proposée (dans le shm), il faut lui envoyer un signal USR1 (et quand A y aura mis 1,-1 ou 0, il renverra aussi un USR1).
La tâche B met les propositions (données par le joueur) dans le shm, et regarde les réponses de A pour les afficher au joueur. La première version est ici : jeuA.c et jeuB.c. La seconde version rend les deux tâches équivalentes : chacune décide d'un nombre que l'autre devra découvrir (les deux joueurs jouent en même temps). En voici la solution : jeuA et jeuB. Les deux sont exactement identiques excepté la valeur de la clé du shm, qui est différente pour A et pour B.

MEMOIRE PARTAGEE

On peut créer une zone de mémoire partagée (shm : shared memory), et y accéder depuis plusieurs processus (pour les threads c'est aussi possible mais autant passer par les variables globales).

shmget(num_du_shm,taille,IPC_CREAT | droits_d_acces) permet de le créer (s'il n'existe pas encore),

shmat permet d'associer un shm existant à un pointeur local

shmdt permet de désintaller un shm (mais pas le supprimer ?)

voir exemple ecrivain.cpp/lecteur.cpp : lancez dans un shell « ./lecteur 1234 ». Il ouvre un shm de numéro 1234, et vous affichera son numéro de processus, par exemple 3456. Dans un ou plusieurs autres shells, lancez « ./ecrivain 1234 3456 message » et votre message sera affiché dans le shell du lecteur.

TUBES

On appelle tube un procédé proposé par le système pour transmettre des informations d'un processus vers un autre. Ils s'utilisent un peu comme des fichiers (ouverture par open, l'un écrit par write et l'autre lit par read, puis close (voire unlink). Un tube est unidirectionnel. Quand un processus ouvre un tube,il attend qu'un autre l'ait ouvert (à l'autre bout) pour continuer.

Les lectures "vident" le tube.

exemple écrivain

	mkfifo("montube",S_IRUSR|S_IWUSR|S_IRGRP);
	fic=open("montube",O_WRONLY);

exemple lecteur

	fic=open("montube",O_RDONLY);

MUTEX

un mutex est en réalité un sémaphore binaire. Mais ce qui importe, c'est qu'il permet de réserver l'accès à une donnée commune. Il y a bien sur d'autres moyens de gérer l'accès simultané (signaux ou sémaphores par exemple), mais dans le cas des threads (où les variables globales sont partagées), il faut cette gestion. C'est pourquoi des fonctions mutex (mutal exception) sont incluses dans pthread.h.

Exemple de cas où un mutex est obligatoire :
pile[sommet++]=valeur : si deux processus le font en même temps, ils peuvent tous les deux incrémenter sommet puis seulement mettre leur valeur (au même endroit).

	pthread_mutex_t mon_mutex;
	pthread_mutex_init(&mon_mutex,NULL);
	pthread_mutex_lock(&mon_mutex);
	pthread_mutex_trylock(&mon_mutex);
	pthread_mutex_unlock(&mon_mutex);
	pthread_mutex_destroy(&mon_mutex);

SEMAPHORES

Un sémaphore est une valeur entière (un code) qu'un processus montre à un moment, et que tous les autres peuvent regarder. En général, il sert à savoir combien sont en attente d'une ressource (et passeront avant nous).

#include <semaphore.h>
sem_t s;
int pshared=0;
unsigned int val=1;
// Les fonctions retournent 0 si ok, -1 sinon

// initialisation du semaphore s
sem_init(&s, pshared, val);
	/*pshared ne vaut PAS 0 si on veut partager sem entre plusieurs processus,
	mais man dit que seul 0 fonctionne sous Linux!
	val est la valeur initiale désirée. */
sem_wait(&s); //bloque le thread jusqu'à ce que s soit nul puis décrémente s
sem_post(&s); //incrémente s;

sem_destroy(&s) //libère le sémaphore

exemple

sem_init(&s,0,1);
//lancer les threads

//dans chacun des threads :
sem_wait(s);
	//faire l'opération critique
sem_post(&s);

autre utilisation : il y a 4 sandwich dispo, certains en font, d'autres en mangent.

APPLICATION

Dans les automatismes (au moins ceux qui nécessitent une commande évoluée), on effectue de nombreuses tâches en même temps. Le Grafcet est un outil adapté. Il est très facile de simuler une partie de Grafcet au déroulement séquentiel, où une seule étape est active à la fois, dans un processus classique. Il ne reste plus qu'à décomposer le Grafcet en plusieurs threads. On peut également utiliser un thread qui gère toutes les entrées-sorties, et communique avec les autres via une zone de mémoire partagée. L'idéal est de mettre au repos les threads en attente d'un capteur, et de les réveiller par un signal dès qu'un capteur change. Voir grafcet.cpp (simplifié : sans signaux, sans gestion des accès simultanés à la mémoire partagée).

LIENS

http://pficheux.free.fr/articles/lmf/threads/

http://www.estvideo.com/dew/index/page/programmation-systeme-linux

http://www.eisti.fr/~info/SDE/Cours/CoursSysteme.pdf


COPYRIGHT

retour sommaire cours Patrick TRAU, ULP - IPST nov 05