Threads ou processus légers
Processus système
Un processus système est un ensemble de ressources physique et logique fourni pour l'exécution d'un programme; chaque processus système est composé de :
- Heap (le tas) : partie de la mémoire vive où on instancie tous les objets.
- Registers pour gérer l'exécution de code
- Stack: partie de la mémoire vive où on place tous les types de base.
- L'environnement d'information, incluant les répertoires de travail et les descripteurs des fichiers
- Les IDs des processus, les IDs des groupes de processus, les IDs des utilisateurs
- Les outils de communication d'inter-processus et les libraires partagées
Un thread
Un thread est un processus léger (par opposition au processus lourd du système) qui partage les mêmes données statiques et dynamiques avec d'autres processus légers qui s'exécutent, indépendamment les uns des autres, de façon concurrentielle dès fois et parallèlement (sur les ordinateur multiprocesseur). Chaque thread dispose d'une pile, d'un contexte d'exécution contenant les registres du processeur et d'un compteur d'instructions. Les communications entre les threads sont alors moins coûteuses en temps mémoire que les communications entre processus systèmes (appelés processus lourds). L'accès concurrentiel aux mêmes données oblige le développeur à bien gérer la synchronisation pour éviter toute interférence ou incohérence au niveau des résultats sur certaines portions de code.
Les fonctions de gestion des threads se lancent en même temps que le processus principal main; cela permet de faire de la programmation multitâche c'est à dire des programmes capables de réaliser plusieurs actions au même moment.
Les threads peuvent se voir affecter des priorités. Ces processus légers ont vocation à communiquer entre eux et de ce fait la norme Posix 1003.1c (POSIX -Portable Operating System Interface uniX) définit également des mécanismes de synchronisation qui sont, l'exclusion mutuelle (mutex), les sémaphores, et les conditions d'exécution.
Implémentation
Pour implémenter les fonctionnalités de la norme Posix.1c, il existe essentiellement deux possibilités :
L'implémentation dans l'espace du noyau, et celle dans l'espace de l'utilisateur.
- Dans une implémentation noyau, chaque thread est représenté par un processus indépendant, partageant son espace d'adressage avec les autres threads de la même application.
- Dans une implémentation utilisateur, l'application n'est constituée que d'un seul processus, et la répartition en différents threads est assurée par une bibliothèque indépendante du noyau.
Chaque implémentation a ses avantages et ses défauts qu'on peut voir sur le tableau ci-dessus :
Point de vue | Implémentation dans l'espace du noyau | Implémentation dans l'espace de l'utilisateur |
Implémentation des fonctionnalités Posix.1c | Nécessite la présence d'appels-système spécifiques, qui n'existent pas nécessairement sur toutes les versions du noyau. | Portable de système Unix en système Unix sans modification du noyau. |
Création d'un thread. | Nécessite un appel-système | Ne nécessite pas d'appel-système, est donc moins coûteuse en ressource que l'implémentation dans le noyau |
Commutation entre deux threads | Commutation par le noyau avec changement de contexte | Commutation assurée dans la bibliothèque sans changement de contexte, est donc plus légère |
Ordonnancement des threads. | Chaque thread dispose des mêmes ressources CPU que les autres processus du système | Utilisation globale des ressources CPU limitée à celle du processus d'accueil |
Priorités des tâches. | Chaque thread peut s'exécuter avec une priorité indépendante des autres, éventuellement en ordonnancement temps-réel. | Les threads ne peuvent s'exécuter qu'avec des priorités inférieures à celle du processus principal |
Parallélisme. | Le noyau peut accessoirement répartir les threads sur différents processeurs pour profiter du parallélisme d'une machine SMP | Les threads sont condamnés à s'exécuter sur un seul processeur puisqu'ils sont contenus dans un unique processus |
Création et exécution des threads
La routine pthread_create() crée un processus léger qui exécute la fonction "start_routine" avec l’argument arg et les attributs attr. Les attributs permettent de spécifier la taille de la pile, la priorité, la politique de planification, etc. Il y a plusieurs formes de modification des attributs. la routine pthread_create fait partie de la librairie Pthreads qui est un API de la norme POSIX. Avec cette libraire on crée un thread comme suit:
int pthread_create (pthread_t * thread, pthread_attr_t * attr, void * (* start_routine) (void *), void * arg);
Cette routine crée et lance le thread immédiatement en exécutant la fonction passée en troisième argument. L'exécution du thread se fait soit jusqu'à la fin de l’exécution de sa fonction ou bien jusqu'à son annulation. Il est possible d'utiliser une fonction avec plusieurs threads !
Si la création du thread réussit, la fonction pthread_create retourne 0 (zéro) et l'identifiant du thread nouvellement créé est stocké à l'adresse fournie en premier argument. En cas d'erreur, la valeur EAGAIN est retournée par la fonction pthread_create() s'il n'y a pas assez de ressources système pour créer un nouveau thread ou bien si le nombre maximum de threads définit par la constante PTHREAD_THREADS_MAX est atteint !
les paramètres de la fonction pthread_create
- Le paramètre *thread est un pointeur de type pthread_t: le type pthread est un type opaque, sa valeur réelle dépend de l'implémentation; sous Linux il s'agit en générale du type unsigned long. Ce type correspond à l'identifiant du thread créé; tout comme les processus Unix, chaque thread a son propre identifiant.
- Le paramètre *attr_t est un pointeur de type pthread_attr_t: ce type est lui aussi un type opaque permettant de définir des attributs spécifiques pour chaque thread. Il faut savoir qu'on peut changer le comportement de la gestion des threads; exemple, on peut les régler pour qu'ils tournent sur un système temps réel ; en générale on se contente des attributs par défaut et cet argument vaut NULL (valeur par défaut).
- Chaque thread dispose d'une fonction à exécuter, c'est en même temps sa raison d'être; Cet argument de la fonction pthread_creat est un pointeur de fonction; il permet de transmettre un pointeur sur la fonction que le thread devra exécuter.
- Le dernier argument représente un argument que l'on peut passer à la fonction que le thread doit exécuter.
Exemple
/ * FILE: hello.c * DESCRIPTION: * A "hello world" Pthreads program. Demonstrates thread creation and * termination. * AUTHOR: Omara Aly * LAST REVISED: ******************************************************************************/ #include <pthread.h> #include <stdio.h> #include <stdlib.h> #define NUM_THREADS 5 void *PrintHello(void *threadid){ long tid; tid = (long)threadid; printf("Hello World! It's me, thread #%ld!\n", tid); pthread_exit(NULL); } int main(int argc, char *argv[]){ pthread_t threads[NUM_THREADS]; int rc; long t; for(t=0;t<NUM_THREADS;t++){ printf("In main: creating thread %ld\n", t); rc = pthread_create(&threads[t], NULL, PrintHello, (void *)t); if (rc){ printf("ERROR; return code from pthread_create() is %d\n", rc); exit(-1); } } pthread_exit(NULL); }
Explication et détails
La première chose à avoir en tête est que le programme i.e le "main" est un thread; donc il va s'exécuter parallèlement au thread créé et lancé par la routine pthread_create(). Ce dernier, comme on l'a vu ci-dessus, a quatre paramètres; Le premier paramètre est utilisé pour fournir des informations au thread. Le second est utilisé pour donner des valeurs à quelques attributs du thread; dans notre cas ce paramètre vaut "NULL" (pointeur sur une valeur NULL) pour signifier à la routine thread_creat() de se servir des valeurs par défaut de tous ses paramètres. Le troisième paramètre est le nom de la fonction qui sera exécutée par le thread et le quatrième paramètre est un argument de la fonction exécutée par le thread.
Une fois compilée et exécutée ce programme donne les résultats suivants:
In main: creating thread 0 In main: creating thread 1 Hello World! It's me, thread #0! In main: creating thread 2 Hello World! It's me, thread #1! Hello World! It's me, thread #2! In main: creating thread 3 In main: creating thread 4 Hello World! It's me, thread #3! Hello World! It's me, thread #4!
pthread_exit
On a utilisé dans le programme la fonction «pthread_exit(NULL)». Cette fonction sert à terminer un thread. Il faudra utiliser cette fonction à la place de «exit» afin d'éviter d’arrêter tout le processus.L'appel à une telle fonction à la fin de la fonction «main» est utile pour éviter que le programme arrête les autres threads en cours.
void pthread_exit (void * retval);
pthread_cancel
int pthread_cancel (pthread_t thread);
pthread_setcancelstate
int pthread_setcancelstate (int state, int * etat_pred);
pthread_join
La fonction «pthread_join» sert à mettre en attente le programme; tant que le thread créé par «pthread_create» n'a pas terminé de s'exécuter. La fonction «pthread_join» est une des formes de synchronisation entre les threads. Par défaut dans le standard POSIX, tous les threads sont créés dans l'état «joignable» par opposition à l'état «détaché».
int pthread_join (pthread_t th, void ** thread_return);
La fonction «pthread_join» attend que le thread «th» se termine. La valeur retournée par la fonction est «0» si tout est correct. Le pointeur «thread_return» pointe l'emplacement où est enregistré la valeur de retour du thread «th». Cette valeur de retour est spécifiée à l'aide de la méthode «pthread_exit».
Exemples
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #if defined (Win32) # include <windows.h> # define psleep(sec) Sleep ((sec) * 1000) #elif defined (Linux) # include <unistd.h> # define psleep(sec) sleep ((sec)) #endif #define INITIAL_STOCK 20 #define NB_CLIENTS 5 /* Structure stockant les informations des threads clients et du magasin. */ typedef struct { int stock; pthread_t thread_store; pthread_t thread_clients [NB_CLIENTS]; } store_t; static store_t store = { .stock = INITIAL_STOCK, }; /* Fonction pour tirer un nombre au sort entre 0 et max. */ static int get_random (int max) { double val; val = (double) max * rand (); val = val / (RAND_MAX + 1.0); return ((int) val); } /* Fonction pour le thread du magasin. */ static void * fn_store (void * p_data) { while (1) { if (store.stock <= 0) { store.stock = INITIAL_STOCK; printf ("Remplissage du stock de %d articles !\n", store.stock); } } return NULL; } /* Fonction pour les threads des clients. */ static void * fn_clients (void * p_data) { int nb = (int) p_data; while (1) { int val = get_random (6); psleep (get_random (3)); store.stock = store.stock - val; printf ( "Client %d prend %d du stock, reste %d en stock !\n", nb, val, store.stock ); } return NULL; } int main (void) { int i = 0; int ret = 0; /* Creation du thread du magasin. */ printf ("Creation du thread du magasin !\n"); ret = pthread_create ( & store.thread_store, NULL, fn_store, NULL ); /* Creation des threads des clients si celui du magasin a reussi. */ if (! ret) { printf ("Creation des threads clients !\n"); for (i = 0; i < NB_CLIENTS; i++) { ret = pthread_create ( & store.thread_clients [i], NULL, fn_clients, (void *) i ); if (ret) { fprintf (stderr, "%s", strerror (ret)); } } } else { fprintf (stderr, "%s", strerror (ret)); } /* Attente de la fin des threads. */ i = 0; for (i = 0; i < NB_CLIENTS; i++) { pthread_join (store.thread_clients [i], NULL); } pthread_join (store.thread_store, NULL); return EXIT_SUCCESS; }
Synchronisation des threads
Généralités
Comme les threads partagent les mêmes ressources (mémoire, variables du processus etc.), dans certaines situations, il est nécessaire de les synchroniser pour obtenir un résultat cohérent. Prenons l'exemple d'un avion où il reste une seule place de disponible et deux clients se présentant à deux différents guichets. Si la place est proposée au premier client et que ce dernier prend tout son temps pour réfléchir, nous n'allons pas attribuer cette place au second client qui en a fait la demande. De ce fait, nous synchronisons l'accès à la méthode de réservation de telle manière à ne pas attribuer la même place aux deux voyageurs ou bien au second voyageur avant que le premier ne prenne sa décision de la prendre ou pas.
Monitor
Le moniteur est utilisé pour synchroniser l'accès à une ressource partagée. Cette ressource peut-être un segment d'un code donné. Un thread accède à cette ressource par l'intermédiaire de ce moniteur. Ce moniteur est attribué à un seul thread à la fois (comme dans un relais 4x100m où un seul coureur tient le témoin dans sa main pour le passer au coureur suivant dès qu'il a terminé de courir sa distance). Pendant que le thread exécute la ressource partagée aucun autre thread ne peut y accéder. Le thread libère le monitor dès qu'il a terminé l'exécution du code synchronisé.
Sémaphores
Les sémaphores sont des compteurs; on peut les incrémenter ou décrémenter. Ils peuvent être utilisés dans des situations où il existe une limite finie à une ressource et un mécanisme est nécessaire pour imposer cette limite. Prenons comme exemple un tampon qui a une taille fixe. Chaque fois qu'un élément est ajouté à ce tampon, le nombre de positions disponibles diminue et Chaque fois qu'un élément est retiré, le nombre disponible est augmenté.
Les sémaphores, qui font partie de la norme POSIX, ne sont pas implémentés dans toutes les bibliothèques de threads.
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); int sem_destroy(sem_t * sem); int sem_wait(sem_t * sem); int sem_post(sem_t * sem); int sem_trywait(sem_t * sem); int sem_getvalue(sem_t * sem, int * sval);
Les sémaphores sont créés par la routine sem_init, qui place l'identificateur du sémaphore à l'endroit pointé par le paramète "sem". La valeur initiale du sémaphore est dans "value". Si le paramètre pshared est nul, le sémaphore est local au processus lourd (le partage de sémaphores entre plusieurs processus lourds n'est pas implémenté dans la version courante de linuxthreads). Les routines sem_wait et sem_post sont les équivalents respectifs des primitives P et V de Dijkstra. La fonction sem_trywait échoue (au lieu de bloquer) si la valeur du sémaphore est nulle. Enfin, sem_getvalue consulte la valeur courante du sémaphore.
Pour assurer que chaque accès aux données partagées est bien « mutuellement exclusif » on utilise les «mutex» une abréviation pour «Mutual Exclusion» ou «exlusion mutuelle».
Mutex
Le(s) mutex (mutual exclusion ou zone d'exclusion mutuelle), est (sont) un système de verrou donnant ainsi une garantie sur la viabilité des données manipulées par les threads. En effet, il arrive même très souvent que plusieurs threads doivent accéder en lecture et/ou en écriture aux mêmes variables. Si un thread possède le verrou, seulement celui-ci peut lire et écrire sur les variables présentes dans la portion de code protégée (aussi appelée zone critique). Lorsque le thread a terminé, il libère le verrou et un autre thread peut le prendre à son tour.
Un mutex est donc un verrou possédant deux états : déverrouillé (pour signifier qu'il est disponible) et verrouillé (pour signifier qu'il n'est pas disponible).Un Mutex est donc une variable d'exclusion mutuelle. Pour créer un mutex, il faut tout simplement déclarer une variable du type pthread_mutex_t et l'initialiser avec la constante PTHREAD_MUTEX_INITIALIZER comme suit:
static pthread_mutex_t mutex_stock = PTHREAD_MUTEX_INITIALIZER;
On peut initialiser aussi un mutex à l'aide de la fonction «pthread_mutex_init».
int pthread_mutex_init (pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr);
Le premier argument correspond au mutex à initialiser. Le second argument représente les attributs à utiliser lors de l'initialisation. Nous pouvons utiliser la valeur «NULL».
La fonction retourne toujours la valeur 0.
Pour détruire un mutex, nous utilisons la fonction « pthread_mutex_destroy ».
int pthread_mutex_destroy (pthread_mutex_t* mutex);
A mutex is a lock that guarantees three things:
- Atomicité - Locking a mutex is an atomic operation, meaning that the operating system (or threads library) assures you that if you locked a mutex, no other thread succeeded in locking this mutex at the same time.
- Singularité - If a thread managed to lock a mutex, it is assured that no other thread will be able to lock the thread until the original thread releases the lock.
- Non-Busy Wait - If a thread attempts to lock a thread that was locked by a second thread, the first thread will be suspended (and will not consume any CPU resources) until the lock is freed by the second thread. At this time, the first thread will wake up and continue execution, having the mutex locked by it.
Verrouillage / déverrouillage
Un thread peut verrouiller le mutex s'il a besoin d'un accès exclusif aux données protégées.
- Pour verrouiller un mutex, nous utilisons la fonction « pthread_mutex_lock ».
int pthread_mutex_lock (pthread_mutex_t* mutex);
Dans ce cas de figure, la fonction retourne soit un message d'erreur pour signifier qu'une erreur s'est produite, zéro (0) dans le cas contraire pour signifier que le mutex a été correctement verrouillé. Le thread en question est le seul à détenir un «verrou» sur le mutex
Si le mutex est déjà verrouillé par un autre thread, la fonction bloque tant que le verouillage sur le mutex n'est pas obtenu.
Il existe une autre façon pour verrouiller un mutex.
Nous pouvons utiliser aussi la fonction «pthread_mutex_trylock»
int pthread_mutex_trylock (pthread_mutex_t* mutex);
Cette fonction ne bloque pas si le mutex est déjà verrouillé par un autre thread.
Elle retourne 0 en cas de succès. Dans le cas contraire, le mutex est verrouillé, elle retourne «EBUSY»
Quand un thread a fini de travailler avec les données «protégées», il doit libérer le mutex. Pour cela, il doit le déverrouiller pour qu'un autre thread puisse l'utiliser. La fonction «pthread_mutex_unlock» est utilisée pour réaliser une telle opération.
int pthread_mutex_unlock (pthread_mutex_t* mutex);
Exemples
Le programme qui suit peut-être compilé de deux manières indépendamment de l'utilisation ou non de l'option «MUTEX».
Si l'utilisation des mutex n'est pas activée, il faudra utiliser cette commande de compilation :
« g++ -Wall -pedantic -Os exemple.cpp -o exemple.exe -lpthreadGCE2 »
Si l'utilisation des mutex est activée, il faudra utiliser cette commande de compilation :
« g++ -Wall -pedantic -DMUTEX -Os exemple.cpp -o exemple.exe -lpthreadGCE2 »
#include <pthread.h> #include <iostream> using namespace std; const int N_PROCESS=10; // Pour éviter de faire un lock en trop const int NM_PROCESS= N_PROCESS - 1; int nParam; #ifdef MUTEX pthread_mutex_t mutex; #endif void * Thread (void * arg) { int ParamLocal; ParamLocal = nParam; cout << "ParamLocal: " << ParamLocal << ", arg: " << (int)arg\ << ", " << ((ParamLocal == (int)arg) \ ? "ok" : "bug !!!!!") << "\n"; #ifdef MUTEX pthread_mutex_unlock(&mutex); #endif pthread_detach(pthread_self()); return NULL; } int main (int argc, char *argv[]) { int i; pthread_t ecrivain_id; #ifdef MUTEX cout << "\nExemple de MUTEX et passage de parametres.\n\n"; #else cout << "\nExemple de bug-MUTEX et passage de parametres.\n\n"; #endif #ifdef MUTEX pthread_mutex_init(&mutex, NULL); pthread_mutex_lock(&mutex); #endif for (i = 0; i < N_PROCESS; i++) { nParam = i; pthread_create (&ecrivain_id, NULL, Thread, (void*)i); #ifdef MUTEX if (i != NM_PROCESS) pthread_mutex_lock(&mutex); #endif } #ifdef MUTEX pthread_mutex_destroy(&mutex); #endif pthread_exit (NULL); return 0; }
Variables de condition
Les conditions servent à mettre en attente des processus légers derrière un mutex. Une primitive permet de débloquer d'un seul coup tous les threads bloqués par un même condition.
#include <pthread.h> int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr); int pthread_cond_destroy(pthread_cond_t *cond); int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond);
Les conditions sont créées par phtread_cond_init, et détruites par phtread_cond_destroy. Un processus se met en attente en effectuant un phtread_cond_wait (ce qui bloque au passage un mutex). La primitive phtread_cond_broadcast débloque tous les processus qui attendent sur une condition, phtread_cond_signal en débloque un seul.
Les variables offrent une autre manière aux threads pour se synchroniser. Au lieu de se synchroniser sur « l'accès à la donnée » comme dans le cas des mutex, les variables de condition permettent aux threads de synchroniser en fonction de la valeur de la donnée. à l'aide de ces variables de condition, un thread peut informer d'autres threads de l'état d'une donnée partagée. Une variable de condition est toujours utilisée conjointement avec « mutex_lock ».
Une variable de condition est une instance de « pthread_cond_t ». - Cette variable peut-être initialisée de manière statique :
pthread_cond_t condvar = PTHREAD_COND_INITIALIZER;
Ou bien à travers une fonction d'initialisation « pthread_cond_init » :
int pthread_cond_init(pthread_cond_t* pCond,pthread_condattr_t* pCondattr);
Pour libérer une variable de condition, nous allons utiliser « pthread_cond_destroy »:
int pthread_cond_destroy(pthread_cond_t* pCond);
Les variables de condition sont associées de facto à un mutex. Ce dernier va bloquer le thread jusqu'à qu'il y ait notification ou que le temps d'attente s'est écoulé. - Nous utilisons les deux fonctions suivantes pour la mise en attente :
int pthread_cond_wait(pthread_cond_t* pCond,pthread_mutex_t* pMutex); int pthread_cond_timewait(pthread_cond_t* pCond,pthread_mutex_t* pMutex, struct timespec* tempsattente);
La première fonction associe la condition au mutex. La seconde en plus de faire cette association ajoute une contrainte de temps comme 3e argument. La première fonction ne rend la main que lorsque la notification est reçue ou qu'une erreur s'est produite. Alors que la seconde ajoute aussi un temps d'expiration. Si ce temps est écoulé, une valeur différente de 0 est retournée par la fonction. Si tout ce passe bien, les deux fonctions retournent la valeur 0. Dans le cas contraire la valeur retournée est non nulle.
Elle retourne 0 en cas de succès. Dans le cas contraire, le mutex est verrouillé, elle retourne « EBUSY ». Ces fonctions bloquent le thread appelant tant que la condition spécifiée est signalée. Ces fonctions doivent être appelées quand un mutex est verrouillé. Elles vont automatiquement relâcher le mutex quand elles sont en mode attente. Après qu'un signal est reçu et que le thread est réveillé, le mutex sera automatiquement verrouillé pour être utilisé par le thread. Il faudra penser à relâcher le mutex quand le thread n'en a plus besoin. Pour réveiller un thread, nous utilisons la fonction « pthread_cond_signal » :
int pthread_cond_signal(pthread_cond_t* pCond);
Pour réveiller plus d'un thread qui sont dans en mode d'attente d'un signal, nous utilisons la fonction « pthread_cond_broadcast » :
int pthread_cond_broadcast(pthread_cond_t* pCond);
Il faudra appeler « pthread_cond_wait » avant « pthread_cond_signal ». On ne réveille pas un thread avant de l'avoir mis en attente !
Serveur Web (avec threads):Principe et pseudo-code
Les processus légers permettent une autre approche : on crée préalablement un « pool » de processus que l'on bloque. Lorsqu'un client se présente, on confie la communication à un processus inoccupé.
ouvrir socket serveur (socket/bind/listen)
créer un pool de processus
répéter indéfiniment
attendre l'arrivée d'un client (accept)
trouver un processus libre, et lui
confier la communication avec le client
fin-répeter
Code du serveur /* serveur-web.c */ /* ------------------------------------------------ Serveur TCP serveur web, qui renvoie les fichiers (textes) d'un répertoire sous forme de pages HTML usage : serveur-web port repertoire exemple: serveur-web 8000 /usr/doc/exemples Version basée sur les threads. Au lieu de créer un processus par connexion, on gère un pool de tâches (sous-serveurs). - au démarrage du serveur les sous-serveurs sont créees, et bloqués par un verrou - quand un client se connecte, la connexion est confiée à un sous-serveur inactif, qui est débloqué pour l'occasion. --------------------------------------------------*/ #include <pthread.h> #include <unistd.h> #include <sys/types.h> #include <sys/errno.h> #include <sys/socket.h> #include <sys/wait.h> #include <sys/stat.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include "declarations.h" void arreter_serveur(int numero_signal); #define NB_SOUS_SERVEURS 5 struct donnees_sous_serveur { pthread_t id; /* identificateur de thread */ pthread_mutex_t verrou; int actif; /* 1 => sous-serveur occupé */ int fd; /* socket du client */ char *repertoire; }; struct donnees_sous_serveur pool[NB_SOUS_SERVEURS]; int fd_serveur; /* variable globale, pour partager avec traitement signal fin_serveur */ /* ------------------------------------------------------- sous_serveur ------------------------------------------------------- */ void *executer_sous_serveur(void *data) { struct donnees_sous_serveur *d = data; while (1) { pthread_mutex_lock(&d->verrou); servir_client(d->fd, d->repertoire); close(d->fd); d->actif = 0; } return NULL; /* jamais exécuté */ } /* ------------------------------------------------------- */ void creer_sous_serveurs(char repertoire[]) { int k; for (k = 0; k < NB_SOUS_SERVEURS; k++) { struct donnees_sous_serveur *d = pool + k; d->actif = 0; d->repertoire = repertoire; pthread_mutex_init(&d->verrou, NULL); pthread_mutex_lock(&d->verrou); pthread_create(&d->id, NULL, executer_sous_serveur, (void *) d); } } /* ----------------------------------------------------- demarrer_serveur: crée le socket serveur et lance des processus pour chaque client ----------------------------------------------------- */ int demarrer_serveur(int numero_port, char repertoire[]) { int numero_client = 0; int fd_client; struct sigaction action_fin; printf("> Serveur " VERSION "+threads " "(port=%d, répertoire de documents=\"%s\")\n", numero_port, repertoire); /* signal SIGINT -> arrèt du serveur */ action_fin.sa_handler = arreter_serveur; sigemptyset(&action_fin.sa_mask); action_fin.sa_flags = 0; sigaction(SIGINT, &action_fin, NULL); /* création du socket serveur et du pool de sous-serveurs */ fd_serveur = serveur_tcp(numero_port); creer_sous_serveurs(repertoire); /* boucle du serveur */ while (1) { struct sockaddr_in a; size_t l = sizeof a; int k; fd_client = attendre_client(fd_serveur); getsockname(fd_client, (struct sockaddr *) &a, &l); numero_client++; /* recherche d'un sous-serveur inoccupé */ for (k = 0; k < NB_SOUS_SERVEURS; k++) if (pool[k].actif == 0) break; if (k == NB_SOUS_SERVEURS) { /* pas de sous-serveur libre ? */ printf("> client %d [%s] rejeté (surcharge)\n", numero_client, inet_ntoa(a.sin_addr)); close(fd_client); } else { /* affectation du travail et déblocage du sous-serveur */ printf("> client %d [%s] traité par sous-serveur %d\n", numero_client, inet_ntoa(a.sin_addr), k); pool[k].fd = fd_client; pool[k].actif = 1; pthread_mutex_unlock(&pool[k].verrou); } } } /* ------------------------------------------------------------- Traitement des signaux --------------------------------------------------------------- */ void arreter_serveur(int numero_signal) { printf("=> fin du serveur\n"); shutdown(fd_serveur, 2); /* utile ? */ close(fd_serveur); exit(EXIT_SUCCESS); } /*-------------------------------------------------------------*/ void usage(char prog[]) { printf("Usage : %s [options\n\n", prog); printf("Options :" "-h\tcemessage\n" "-p port\tport du serveur [%d]\n" "-d dir \trépertoire des documents [%s]\n", PORT_PAR_DEFAUT, REPERTOIRE_PAR_DEFAUT); } /*-------------------------------------------------------------*/ int main(int argc, char *argv[]) { int port = PORT_PAR_DEFAUT; char *repertoire = REPERTOIRE_PAR_DEFAUT; /* la racine des documents */ char c; while ((c = getopt(argc, argv, "hp:d:")) != -1) switch (c) { case 'h': usage(argv[0]); exit(EXIT_SUCCESS); break; case 'p': port = atoi(optarg); break; case 'd': repertoire = optarg; break; case '?': fprintf(stderr, "Option inconnue -%c. -h pour aide.\n", optopt); break; } demarrer_serveur(port, repertoire); exit(EXIT_SUCCESS); }