Blue Flower

Chercher

Génaralités sur les signaux



précédent suivant

Les processus UNIX

Introduction
Le système UNIX est un système multitâche (système capable d'exécuter plusieurs tâches à la fois) et classiquement on appelle processus l'instance d'un programme qui est en train de s'executer. Chaque processus UNIX possède donc ses ressources (espace mémoire, table des fichiers ouverts...). On voit bien que toute activité dans le système UNIX est un processus ; si c'est un processus relatif au système (swapper, crons, getty,...) on parle de processus système et si c'est une application utilisatrice i.e l'execuiton d'une commande ou d'une applicatrion on parle alors de processus utilisateur.
Classiquement un processus UNIX se crée avec la routine fork() par dédoublement d'un processus existant appelé communément processus père. Lorsque le processus fils est créé,le système lui attribue une copie des ressources du processus père et là s'ensuient deux problèmes :
1) Le premier est un problème de performance, puisque la duplication est un mécanisme coûteux
2) et le deuxième est un problème de communication entre les processus, qui ont des variables séparées.
Toutefois il existe des moyens d'atténuer ces problèmes : technique du copy-on-write dans le noyau pour ne dupliquer les pages mémoires que lorsque c'est strictement nécessaire), utilisation de segments de mémoire partagée (voir les IPC) pour mettre des données en commun.
Sur une machine uni-processeur, il n'y a qu'un seul processus qui s'exécute effectivement à un instant donné. Le noyau assure une commutation régulière entre tous les processus présents sur le système pour garantir un fonctionnement multitâche. Sur une machine multi-processeur, le principe est le même à la différence que plusieurs processus - mais rarement tous - peuvent s'exécuter réellement en parallèle.

Création d'un processus - fork()
Sous UNIX la création de processus est réalisée par l'appel système :

#include < sys/types.h >
#include <unistd.h>
int fork(void);

Tous les processus sauf le processus d'identification 0, sont créés par un appel à fork().Le processus qui appelle le fork() est appelé processus père. Le nouveau processus est appelé processus fils. Tout processus a un seul processus père. Tout processus peut avoir zéro ou plusieurs processus fils. Chaque processus est identifié par un numéro unique, son PID. Le processus de PID=0 est créé au démarrage de la machine, ce processus a toujours un rôle spécial1 pour le système, de plus pour le bon fonctionement des programmes utilisant fork() il faut que le PID zéro reste toujours utilisé. Le processus zéro crée, grâce à un appel de fork, le processus init de PID=1. Le processus de PID=1 de nom init est l'ancêtre de tous les autres processus (le processus 0 ne réalisant plus de fork()), c'est lui qui accueille tous les processus orphelins de père (ceci a fin de collecter les information à la mort de chaque processus).
Exemple de création de processus

exemple_fork.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>
int
main (void)
{
pid_t pid_fils;
do {
pid_fils = fork ( ) ;
} while ((pid_fils == -1) && (errno == EAGAIN));
if (pid_fils == -1) {
fprintf (stderr, "fork( ) impossible, errno=%d\n", errno);
return (1);
}
if (pid_fils == 0) {
fprintf (stdout, "Fils : PID=%d, PPID=%d\n",
getpid( ) , getppid( ));
return (0):
} else {
fprintf (stdout, "Père : PID=%d, PPID=%d\n",
getpid( ) , getppid( ));
wait (NULL);
return(0);
}
}

Lors de son exécution, ce programme fournit les informations suivantes :

Père : PID=31382, PPID=30550, PID fils=31383
Fils : PID=31383, PPID=31382

Il est clair que ces numéros sont liés au système sur lequel le programme est lancé. La signification de certaine routine présentes dans c programmes seront détaillées ultérieurement notamment wait()

Etat d'un processus
Plusieurs processus peuvent sembler s'exécuter en même temps mais le nombre maximum de processus qui peuvent s'exécuter simultanément est le nombre de processeurs du système. Le système d'exploitation fait fonctionner un à un les processus pendant un court laps de temps et les met en pause, chaque processus utilise tour à tour le processeur plusieurs fois par seconde ce qui donne l'impression de la simultanéité d'exécution.
Pour un processus, on distingue les états suivants.
* En cours d'exécution (running): soit disposant du processeur, soit en attente du processeur.
* En sommeil (sleeping): en attente d'une ressource.
* Stoppé (stopped): arrêté généralement par CTRL-S, par exemple, ou par un debogueur.
* Zombie: le processus est terminé mais le noyau conserve encore des informations à son sujet qui pourront être consultées par un autre processus avant suppression définitive.
Un processus est caractérisé par
* son code (le code binaire du programme qu'il exécute)
* ses données:
* statiques: variables globales, variables statiques des fonctions
* pile: variables automatiques (locales)
* tas: zones mémoire allouées dynamiquement,
* des données conservées par le noyau pour la gestion du processus: identificateur, utilisateur(s) attaché(s) au processus, fichiers en cours d'utilisation, etc

zone u et table des processus
Tous les processus sont associés à une entrée dans la table des processus qui est interne au noyau. De plus, le noyau alloue pour chaque processus une structure appelée zone u , qui contient des données privées du processus, uniquement manipulables par le noyau. La table des processus nous permet d'accéder à la table des régions par processus qui permet d'accéder à la table des régions. Ce double niveau d'indirection permet de faire partager des régions. Dans l'organisation avec une mémoire virtuelle, la table des régions est matérialisée logiquement dans la table de pages. Les structures de régions de la table des régions contiennent des informations sur le type, les droits d'accès et la localisation (adresses en mémoire ou adresses sur disque) de la région. Seule la zone u du processus courant est manipulable par le noyau, les autres sont inaccessibles. L'adresse de la zone u est placée dans le mot d'état du processus.


Gestion des processus

Le pid
Les processus sont identifiés par un numéro unique attribué par le noyau: le pid (Processus ID). Les appels-système qui concernent les processus utilisent le numéro pid pour désigner le processus qui fait l'objet de l'appel. Un processus peut obtenir son pid par l'appel système getpid().

#include <unistd.h>
pid_t getpid(void)

Le processus fils peut aisément accéder au PID de son père (noté PPID pour Parent PID) grâce à l'appel-système getppid( ), déclaré lui aussi dans <unistd.h> : pid_t getppid (void); Cette routine se comporte comme getpid( ) , mais renvoie le PID du père du processus appelant. Par contre, le processus père ne peut connaître le numéro du nouveau processus créé qu'au moment du retour du fork( ).

uid_t getuid (void);
uid_t geteuid(void);

Le type uid_t est défini dans <sys/types.h>. Selon les systèmes, il s'agit d'un unsigned int, unsigned short ou unsigned long. Nous utilisons donc la conversion %u pour fprintf( ) qui doit fonctionner dans la plupart des cas. L'UID effectif est différent de l'UID réel lorsque le fichier exécutable dispose d'un attribut particulier permettant au processus de changer d'identité au démarrage du programme. Considérons par exemple le programme suivant:

exemple_getuid.c :
#include <stdio.h>
#include <unistd.h>
int
main (void)
{
fprintf (stdout, " UID réel = %u, UID effectif = %u\n",
getuid( ), geteuid( ));
return (0);
}

Quand on compile ce programme, qu'on lance l'executable on obtient parfaitement:

UID réel = 500, UID effectif = 500

Le comportement est pour l'instant parfaitement normal. Imaginons maintenant que root passe par là, s'attribue le fichier exécutable et lui ajoute le bit « Set-UID» à l'aide de la commande chmod (chmod +s exemple_getuid ). Lorsqu'un utilisateur va maintenant exécuter exemple_getuid, le système va lui fournir l'UID effectif du propriétaire du fichier, à savoir root (qui a toujours l'UID 0 par définition); maintenant si on execute ce programme dans l'espace utilisateur autre que root, on verra que

UID réel = 500, UID effectif = 0

Set-UID
Un processus avec le bit Set-UID positionné démarre donc avec un UID effectif différent de celui de l'utilisateur qui l'a invoqué. Quand il désire effectuer une opération non privilégiée, il peut demander à remplacer son UID effectif par l'UID réel. Une copie de l'UID effectif est conservée dans l'UID sauvé. Il pourra donc à tout moment demander à remplacer à nouveau son UID effectif par son UID sauvé.
Un programme utilisant uniquement seteuid( ) ou setreuid( ) sera 100 % compatible avec BSD, un programme utilisant uniquement setuid( ) sera 100 % compatible avec Posix. Bien entendu, ils seront tous deux 100 % compatibles avec Linux. Nous allons étudier le comportement d'un programme Set-UID qui abandonne temporairement ses privilèges pour disposer des permissions de l'utilisateur l'ayant invoqué, puis qui reprend à nouveau ses autorisations originales.

exemple_setreuid.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int
main (void)
{
uid_t uid_reel;
uid_t uid_eff;
uid_reel = getuid( );
uid_eff = geteuid( );
fprintf (stdout, " UID-R = %u, UID-E = %u\n", getuid( ), geteuid( ));
fprintf (stdout, " setreuid (-1, %d) = %d\n", uid_reel,
setreuid (-1, uid_reel));
fprintf (stdout, " UID-R = %u, UID-E = %u\n", getuid( ), geteuid( )):
fprintf (stdout, " setreuid (-1, %d) = %d\n", uid_eff,
setreuid (-1, uid_eff));
fprintf (stdout, " UID-R = %u, UID-E = %u\n", getuid( ), geteuid( ));
fprintf (stdout, " setreuid (%d, -1) = %d\n", uid_eff,
setreuid (uid_eff, -1));
fprintf (stdout, " UID-R = %u, UID-E = %u\n", getuid( ), geteuid( ));
return (0);
}

à Venir....

Le GID
Chaque utilisateur du système appartient à un ou plusieurs groupes. Ces derniers sont définis dans le fichier /etc/groups. Un processus fait donc également partie des groupes de l'utilisateur qui l'a lancé. Comme nous l'avons vu avec les UID. un processus dispose donc de plusieurs GID (Group IDentifier) réel, effectif, sauvé, ainsi que de GID supplémentaires si l'utilisateur qui a lancé le processus appartient à plusieurs groupes. ATTENTION Il ne faut pas confondre les groupes d'utilisateurs auxquels un processus appartient, et qui dépendent de la personne qui lance le processus et éventuellement des attributs Set-GID du fichier exécutable, avec les groupes de processus, qui permettent principalement d'envoyer des signaux à des ensembles de processus. Un processus appartient donc à deux types de groupes qui n'ont rien à voir les uns avec les autres. Le GID réel correspond au groupe principal de l'utilisateur ayant lancé le programme (celui qui est mentionné dans /etc/passwd). Le GID effectif peut être différent du GID réel si le fichier exécutable dispose de l'attribut Set-GID (chmod g+s). C'est le GID effectif qui est utilisé par le noyau pour vérifier les autorisations d'accès aux fichiers. La lecture de ces GID se fait symétriquement à celle des UID avec les appels-système getgid( ) et getegid( ). La modification (sous réserve d'avoir les autorisations nécessaires) peut se faire à l'aide des appels setgid( ) , setegid( ) et setregid( ). Les fonctions getgid( ) et setgid( ) sont compatibles avec Posix.1, les autres avec BSD. Les prototypes de ces fonctions sont présents dans <unistd.h>, le type gid_t étant défini dans <sys/types.h> :

gid_t getgid (void);
gid_t getegid (void);
int setgid (gid_t egid);
int setegid (gid_t egid);
int setregid (gid_t rgid, gid_t egid);

Les deux premières fonctions renvoient le GID demandé. les deux dernières renvoient 0 si elle réussissent et -1 en cas d'échec. L'ensemble complet des groupes auxquels appartient un utilisateur est indiqué dans /etc/groups (en fait, c'est une table inversée puisqu'on y trouve la liste des utilisateurs appartenant à chaque groupe). Un processus peut obtenir cette liste en utilisant l'appelsystème getgroups( ) : int getgroups (int taille, gid_t liste []); Celui-ci prend deux arguments, une dimension et une table. Le premier argument indique la taille (en nombre d'entrées) de la table fournie en second argument. L'appel-système va remplir le tableau avec la liste des GID supplémentaires du processus. Si le tableau est trop petit, getgroups( ) échoue (renvoie -1 et remplit errno), sauf si la taille est nulle ; auquel cas,il renvoie le nombre de groupes supplémentaires du processus. La manière correcte d'utiliser getgroups ( ) est donc la suivante.

exemple_getgroups.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
int
main (void)
{
int taille;
gid_t * table_gid = NULL;
int i;
if ((taille = getgroups (0, NULL)) < 0) {
fprintf (stderr, "Erreur getgroups, errno =%d\n", errno);
return (1);
}
if ((table_gid = calloc (taille, sizeof (gid_t)))==NULL) {
fprintf (stderr, "Erreur calloc, errno = %d\n", errno);
return (1);
}
if (getgroups (taille, table_gid) < 0) {
fprintf (stderr, "Erreur getgroups, errno %d\n", errno);
return (1);
}
for (i = 0; i < taille; i ++)
fprintf (stdout, "%u " , table_gid [i]);
fprintf (stdout, "\n");
free (table_gid);
return (0);
}

Organisation mémoire

à suivre ....

Espace adressage virtuel
à suivre ....


Primitives de bas niveau

Généralités
à suivre ....


ordonnancement des processus

Généralités
à suivre ....


Fonctionnement multitâche, priorités

Généralités
à suivre ....

Modification de la priorité d'un autre processus
à suivre ....


Exemples sur les processus

Exemples :fork et pipe

1     /* biproc.c */
  2     /*
  3      * Illustration de fork() et pipe();
  4      * 
  5      * Exemple à deux processus reliés par un tuyau 
  6      - l'un envoie abcdef...z 10 fois dans le tuyau 
  7      - l'autre écrit ce qui lui arrive du tuyau sur la
  8      - sortie standard, en le formattant.
  9      */
 10     #include <unistd.h>
 11     #include <stdlib.h>
 12     #include <stdio.h>
 13     #include <sys/types.h>
 14     #include <sys/wait.h>
 15     #define MAXLIGNE 30
 16     void genere(int sortie)
 17     {
 18             char alphabet[26];
 19             int k;
 20             for (k = 0; k < 26; k++)
 21                     alphabet[k] = 'a' + k;
 22             for (k = 0; k < 10; k++)
 23                     if (write(sortie, alphabet, 26) != 26) {
 24                             perror("write");
 25                             exit(EXIT_FAILURE);
 26                     };
 27             close(sortie);
 28     }
 29     int lire(int fd, char *buf, size_t count)
 30     {
 31             /* lecture, en insistant pour remplir le buffer */
 32             int deja_lus = 0, n;
 33             while (deja_lus < count) {
 34                     n = read(fd, buf + deja_lus, count - deja_lus);
 35                     if (n == -1)
 36                             return (-1);
 37                     if (n == 0)
 38                             break;  /* plus rien à lire */
 39                     deja_lus += n;
 40             };
 41             return (deja_lus);
 42     }
 43     void affiche(int entree)
 44     {
 45             char ligne[MAXLIGNE + 1];
 46             int nb, numligne = 1;
 47             while ((nb = lire(entree, ligne, MAXLIGNE)) > 0) {
 48                     ligne[nb] = '\0';
 49                     printf("%3d %s\n", numligne++, ligne);
 50             };
 51             if (nb < 0) {
 52                     perror("read");
 53                     exit(EXIT_FAILURE);
 54             }
 55             close(entree);
 56     }
 57     int main(void)
 58     {
 59             int fd[2], status;
 60             pid_t fils;
 61             if (pipe(fd) != 0) {
 62                     perror("pipe");
 63                     exit(EXIT_FAILURE);
 64             }
 65             if ((fils = fork()) < 0) {
 66                     perror("fork");
 67                     exit(EXIT_FAILURE);
 68             }
 69             if (fils == 0) {        /* le processus fils */
 70                     close(fd[0]);
 71                     close(1);
 72                     genere(fd[1]);
 73                     exit(EXIT_SUCCESS);
 74             };
 75             /* le processus père continue ici */
 76             close(0);
 77             close(fd[1]);
 78             affiche(fd[0]);
 79             wait(&status);
 80             printf("status fils = %d\n", status);
 81             exit(EXIT_SUCCESS);
 82     }

précédent suivant

Communication entre processus (IPC System V)

Caractéristiques communes
Les IPC sont les mécanismes de communication inter-processus. Ce sont les files de messages, la mémoire partagée, et les sémaphores.
Ces mécanismes de communication (qui ne sont pas introduits dans la norme POSIX) ne sont pas identifiés par des descripteurs de fichiers et ne peuvent donc pas se manipuler en utilisant les appels système relatifs aux fichiers ou les fonctions de la bibliothèque standard d'E/S.

Ces outils de communication peuvent être partagés entre des processus n'ayant pas immédiatement d'ancêtre commun. Pour cela, les IPC introduisent le concept de clé. Dans chaque système UNIX on dispose d'une table spécifique qui gère les IPC System V. Chaque objet (Objet ici n'est pas vu comme l'instance d'une classe) possède une identification interne (nombre entier naturel) dont la connaissance est indispensable pour accéder à l'objet ; on obtient cette identification par héritage ou par interrogation du système à l'aide de routines comme msgget pour les files de messages, shmget() pour la mémoire partagée et semget() pour les sémaphores .

ftok(): constitution d'une clé
La fonction ftok() est définie comme suit dans <sys/ipc.h>

key_t ftok (char * nom_fichier, char projet);

Cette fonction crée une clé (un entier de 32 bits) à partir d'un chemin d'accès (un fichier - le numéro d'inode du fichier et son device -) et d'un caractère indiquant un «projet». si on change l'inode associée à la référence, cela change la valeur de la clé, donc il n'est plus possible de retrouver la clé originale.
La meilleure façon pour avoir une clé consiste à demander au système de la créer lui-même, en se basant sur des références communes pour tous les processus. De cette manière, tous les processus d'un ensemble donné pourront choisir de créer leur clé commune en utilisant le chemin d'accès du fichier exécutable de l'application principale, ainsi qu'un numéro de version par exemple. La constante symbolique IPC_PRIVATE, définie dans <sys/ipc.h> représente une clé privée, demandant sans condition la création d'une nouvelle ressource IPC.
Une fois qu'on a obtenu une clé ou qu'on a choisi d'utiliser une ressource privée avec IPC_PRIVATE, on peut demander l'accès à l'IPC proprement dite. l'emploi de IPC_PRIVATE n'empêche pas l'accès à la ressource par un autre processus, mais garantit uniquement qu'une nouvelle ressource sera créée. L'identifiant renvoyé par la routine d'ouverture n'aura rien d'exceptionnel; un autre processus pourra l'employer à condition d'avoir les autorisations d'accès nécessaires.

Ouverture d'un IPC
Chaque ressource IPC( file de message, sémaphore ou mémoire partagée) possède une routine qui permet de l'obtenir.
Pour les files de message, on peut envoyer ou recevoir un message en se servant respectivement de msgsnd() et msgget(). Ces fonctions demandent au système de créer éventuellement la ressource si elle n'existe pas, puis de renvoyer un numéro d'identification. Si la ressource existe déjà et si le processus appelant n'a pas les autorisations nécessaires pour y accéder, les routines échouent en renvoyant -1. On peut attacher puis de détacher un segment de mémoire partagée dans l'espace d'adressage du processus avec shmat( ) ou shmdt( ) et enfin lever de manière bloquante ou non un sémaphore, puis de le relâcher avec la fonction commune semop( ).

Contrôle : paramètres
Les IPC sont accompagnés aussi de fonctions qui permettent de positionner des options de paramètrages; ces fonctions msgctl( ), shmctl( ) et semctl( ) permettent d'atteindre et de modifier les attributs des options de paramamétrage qui se trouvent dans les trois structures msgid_ds, shmid_ds et semid_ds. Ces structures permettent l'accès à un objet de type

struct ipcperm {
_key key_t           ; 	/* Clé associée à la ressource IPC*/
_seq unsigned short  ; 	/* Numéro de séquence,*/
                      	 /* utilisé de manière interne par le*/
                       	/* système, à ne pas toucher*/
mode unsigned short  ; /* Autorisations d'accès à la ressource, comme pour les*/
                       	/* permissions des fichiers  */
uid uid_t          ;   /* UID effectif de la ressource IPC*/
gid gid_t          ;   /* GID effectif de la ressource IPC*/
cuid uid_t         ;   /* UID du créateur de la ressource*/
cgid gid_t         ;   /* GID du créateur de la ressource*/
}

défini dans <sys/ipc.h> . Les modifications de mode ne peuvent être réalisées que par le propriétaire,créateur de la ressource ou par un processus ayant la capacité CAP_IPC_OWNER. Les fonctions de contrôle permettent également de détruire une ressource IPC.


Mémoires partagées

Généralités
Ce mécanisme permet à plusieurs processus distincts de partager des segments de mémoire définis dynamiquement par l'un d'eux. Chaque segment de mémoire est identifié, au niveau du système, par une clé à laquelle correspond un identifiant appelé shmid (Shared Memory Identification) qui référence une entrée dans la table des segments de mémoire.
Chaque entrée dans cette table a une structure shmid_ds définie dans <sys/shm.h> qui contient notamment les champs :

  • Shm_perm : structure ipc_perm décrivant les permissions.
  • Shm_segsz : taille du segment.
  • Shm_lpid : pid du dernier processus utilisateur.
  • Shm_cpid : pid du processus créateur du segment.
  • Shm_natch : nombre d'attachements.
  • Shm_atime : date du dernier attachement.
  • Shm_dtime : date du dernier détachement.
  • Shm_ctime : date de la dernière opération de contrôle.

Création d'un segment
La routine shmget(key_t key, int size, int shmflag) permet de créer ou de retrouver un segment de mémoire partagée. En cas de succès, elle renvoie un shmid (entier > 0), sinon elle renvoie «-1» et «errno...» . Le paramètre key est une clé d'accès au segment. Pour réserver le segment au processus créateur et à sa descendance, on lui donne la valeur IPC_PRIVATE. Le paramètre size est la taille du segment. Le paramètre shmflag règle les droits d'accès et certains indicateurs de création et de recherche :

  • - IPC_CREAT : création d'un IPC s'il n'existe pas.
  • - IPC_ALLOC : recherche d'un IPC supposé exister.
  • - IPC_EXCL : erreur si l'IPC existe déjà.
  • - 0xyz : codage octal des droits d'accès.
  • Donc en un mot la fonction shmget() dont le prototype est "int shmget (key_t key, int size, int shmflg);"

donne l'identifiant du segment ayant la clé key. Un nouveau segment (de taille size) est créé si key est IPC_PRIVATE, ou bien si les indicateurs de shmflg contiennent IPC_CREAT. Combinées, les options IPC_EXCL | IPC_CREAT indiquent que le segment ne doit pas exister préalablement. Les bits de poids faible de shmflg indiquent les droits d'accès.
Exemples:
1 : IPC_CREAT|IPC_EXCL|0666 : création d'un s.m.p. avec rw, avec échec si le segment existe déjà.
2 : IPC_ALLOC : recherche de segment existant avec retour du shmid si existence et retour de –1 sinon.

Attachement et détachement à un segment
La fonction shmat() dont le protype est

char *shmat (int shmid, char *shmaddr, int shmflag );

attache le segment shmid en mémoire, avec les droits spécifiés dans shmflag (SHM_R, SHM_W, SHM_RDONLY). shmaddr précise où ce segment doit être situé dans l'espace mémoire (la valeur NULL demande un placement automatique).
shmat() renvoie l'adresse où le segment a été placé.
Lorsqu'un segment est attaché à un programme, les données qu'il contient sont accessibles en mémoire par l'intermédiaire d'un pointeur.
La routine shmat() rendre visible un segment de mémoire partagée dans l'espace d'adressage d'un processus. Elle retourne l'adresse d'attachement du segment, sinon elle retourne «-1» ;
le paramètre shmid est le descripteur du segment,
le paramètre shmaddr l'adresse d'attachement ( il est conseillé de laisser le système de choisir le point de greffe en passant zéro comme argument) et le paramètre shmflag est en général égal à zéro, ce qui signifie que le segment est attaché en lecture/écriture. On peut interdire toute écriture en passant SHM_RDONLY comme argument.
shmdt() permet de libérer le segment et en cas d'erreur il retourne «-1».

int shmdt (char *shmaddr);

Contrôle d'un segment
Une fois que le segement est créé ou retrouvé par la routine shmget(), on contrôle son utilisation par la routine shmctl() en tenant compte des fichiers «.h» à déclarer avant d'utiliser shmctl() dont le protype est

int shmctl(int shmid, int cmd, struct shmid_ds *buf) ;

Cette routine shmctl(int shmid, int cmd, struct shmid_ds *buf) permet de Consulter, modifier les caractéristiques d'un segment, ou même le supprimer. Elle retourne «0» en cas de succès, et «-1» sinon.
Le paramètre shmid est un descripteur de segment.
Le paramètre cmd précise l'opération de contrôle à réaliser :
- IPC_STAT : dans ce si à la place de cmd on IPC_STAT on a une Consultation,
- IPC_SET : entraîne une Modification du segment de mémoire et
- IPC_RMID : entre purement et simplement la Suppression du segment.
Le paramètre buf est l'adresse d'un objet de structure shmid_ds déclaré par l'utilisateur.

Exemples : producteur/consommateur
Le producteur :
Ce programme lit une suite de nombres, et effectue le cumul dans une variable en mémoire partagée.

Le producteur :
           /* prod.c */
           /*
              Ce programme lit une suite de nombres, et effectue le cumul dans une
              variable en mémoire partagée. 
	   */
           #include <sys/ipc.h>
           #include <sys/shm.h>
           #include <sys/types.h>
           #include <stdlib.h>
           #include <stdio.h>
          #include <errno.h>
          void abandon(char message[]){
              perror(message);
              exit(EXIT_FAILURE);
          }
          struct donnees {
              int nb;
              int total;
          };
          int main(void)
          {
              key_t cle;
              int id;
              struct donnees *commun;
              int reponse;
              cle = ftok(getenv("HOME"), 'A');
              if (cle == -1)
                  abandon("ftok");
              id = shmget(cle, sizeof(struct donnees),
                          IPC_CREAT | IPC_EXCL | 0666);
              if (id == -1) {
                  switch (errno) {
                  case EXIST:
                      abandon("Note: le segment existe déjà\n");
                  default:
                      abandon("shmget");
                  }
              }
              commun = (struct donnees *) shmat(id, NULL, SHM_R | SHM_W);
              if (commun == NULL)
                  abandon("shmat");
              commun->nb = 0;
              commun->total = 0;

              while (1) {
                  printf("+ ");
                  if (scanf("%d", &reponse) != 1)
                      break;
                  commun->nb++;
                  commun->total += reponse;
                  printf("sous-total %d= %d\n", commun->nb, commun->total);
              }
              printf("---\n");

              if (shmdt((char *) commun) == -1)
                  abandon("shmdt");
              /* suppression segment */
              if (shmctl(id, IPC_RMID, NULL) == -1)
                  abandon("shmctl(remove)");
              return EXIT_SUCCESS;
          }

Explications:
La création de la clé avec la fonction ftok();. Si ce qu'on retourne dans «cle» est égale à «-1» on sort sinon on continue c'est à dire qu'on a une bonne clé.
Avec cette clé (cle) on créé le segment de mémoire avec la routine shmget(). Si id vaut «-1 » on abandonne sinon on continue
en attachant ce segment de mémoire à une variable en l'occurrence ici "commun" qui est une adresse dont la taille peut contenir la structure «struct donnee » . La valeur de retour est l'adresse où l'attachement a été effectivement réalisé, c'est-à-dire celle attribuée au premier octet du segment. cette opération est réalisée par la routine shmat() ; une fois qu'on a réalisé l'attachement (on vérifie si l'adresse retournée n'est pas égale à NULL) on initialise les champs de la structure «commun» (commun->nb = 0; et commun->total = 0;)

Le consommateur :
Ce programme affiche périodiquement le contenu de la mémoire partagée. et pour l'arrêter on fait Contrôle-C

         /* cons.c */
           /*
              Ce programme affiche périodiquement le contenu de la 
              mémoire partagée. Arrêt par Contrôle-C
           */
           #include <sys/ipc.h>
           #include <sys/shm.h>
           #include <sys/types.h>
           #include <unistd.h>
          #include <stdlib.h>
          #include <stdio.h>
          #include <errno.h>
          #include <signal.h>
          #define DELAI 2
          void abandon(char message[])
          {
              perror(message);
              exit(EXIT_FAILURE);
          }
          struct donnees {
              int nb;
              int total;
          };
          int continuer_boucle = 1;
          void arreter_boucle(int signal)
          {
              continuer_boucle = 0;
          }
          int main(void)
          {
              key_t cle;
              int id;
              struct donnees *commun;
              struct sigaction a;
              cle = ftok(getenv("HOME"), 'A');
              if (cle == -1)
                  abandon("ftok");
              id = shmget(cle, sizeof(struct donnees), 0);
              if (id == -1) {
                  switch (errno) {
                  case ENOENT:
                      abandon("pas de segment\n");
                  default:
                      abandon("shmget");
                  }
              }
              commun = (struct donnees *) shmat(id, NULL, SHM_R);
              if (commun == NULL)
                  abandon("shmat");
              continuer_boucle = 1;
              a.sa_handler = arreter_boucle;
              sigemptyset(&a.sa_mask);
              a.sa_flags = 0;
              sigaction(SIGINT, &a, NULL);
              while (continuer_boucle) {
                  sleep(DELAI);
                  printf("sous-total %d= %d\n", commun->nb, commun->total);
              }
              printf("---\n");
              if (shmdt((char *) commun) == -1)
                  abandon("shmdt");
              return EXIT_SUCCESS;
          }

Eplications:
Ce programme va lire ce qui est écrit dans le segment de mémoire partagé ; par le programme consommateur; avec la fonction ftock() on va récupérer la clé "cle", s'en servir pour recupérer son "id" à travers la fonction shmget(),


Sémaphores

Sémaphores : présentation
Les sémaphores sont des variables partagées, dont l'accès ne se fait que grâce aux deux opérations atomiques( opérations totalement ininterruptibles, qui ont toujours lieu séquentiellement même sur une machine multi-processeurs) Pn et Vn . Ils permettent de réaliser l'accès en exclusion mutuelle à une ressource ; Cela veut dire que lorsque des processus ont accès à une ressource partagée, si un d'entre eux y accède, les autres ne pourront pas y accèder tant que celui qui est dessus n'ait fini .
Un sémaphore peut servir à l' accès et au contrôle non pas d'une simple ressource critique mais à plusieurs exemplaires d'une même ressource. Ainsi on a à faire à un compteur qu'on augmente ou qu'on diminue d'une valeur entière qui n'est pas nécessairement «1». L'opération Pn( ) est alors bloquante tant que le compteur du sémaphore est inférieur à la valeur n demandée, puis elle diminue le compteur de cette quantité. Parallèlement, l'opération Vn( ) doit incrémenter le compteur de la valeur «n» d'exemplaires libérés de la ressource. En C/C++ pour se servir des sémaphores on utilise les bibliothèques

 #include <sys/types.h>
 #include <sys/ipc.h>
 #include <sys/sem.h>

Les opérations System V travaillent en fait sur des tableaux de sémaphores généralisés (pouvant évoluer par une valeur entière quelconque).

Création de sémaphore
Un sémaphore est une variable dont les valeurs possibles sont des entiers positives ou nuls; le système associe à cette variable une structure du type «struct sem »définie comme suit:

struct sem{
ushort semval;  /* Valeur du sémaphore                  */
ushort sempid;  /* PID dernière opération               */
ushort semncnt; /* Nb proc. en attente incrément semval */
ushort semzcnt; /* Nb proc. en attente de semval = 0    */
}

La fonction semget(key_t key, int nsems, int semflg ) retourne l'identificateur de l'ensemble de sémaphores généralisé associé à la valeur de clé key; cet ensemble contient nsems sémaphores individuels. Le prototype de semget() est:

int semget(key_t key, int nsems, int semflg );

Le nouveau ensemble de sémaphores créé ou sollicité, a les droits donnés par les 9 bits de poids faible de semflg; si key est égale à IPC_PRIVATE, ou si semflg contient IPC_CREAT c'est automatiquement un nouveau ensemble de nsems sémaphores qui est créé. A chaque ensemble de sémaphores le noyau associe une structure d'informations semid_ds:

struct semid_ds {
	struct ipc_perm sem_perm;	/* operation permission struct */
	struct sem	*sem_base;	/* ptr to first semaphore in set */
	ushort_t	sem_nsems;	/* # of semaphores in set */
	time_t		sem_otime;	/* last semop time */
	long		sem_pad1;	/* reserved for time_t expansion */
	time_t		sem_ctime;	/* last change time */
	long		sem_pad2;	/* time_t expansion */
	long		sem_pad3[4];	/* reserve area */
};

Pendant la création, la structure de données semid_ds qui contrôle le jeu de sémaphores est initialisée comme suit :
sem_perm.cuid et sem_perm.uid contiennent l'UID effectif du processus appelant.
sem_perm.cgid et sem_perm.gid contiennent le GID effectif du processus appelant.
le 9 bits de poids faibles de sem_perm.mode sont remplis avec les 9 bits de poids faibles de semflg.
sem_nsems reçoit la valeur nsems.
sem_otime est mis a 0.
sem_ctime est rempli avec l'heure actuelle.
l'argument nsems peut valoir 0 (ignore) si l'appel système n'est pas une création d'ensemble de sémaphores. Autrement nsems doit être supérieur à 0 et inférieur ou égal au nombre maximal de sémaphores SEMMSL par ensemble.
Si le jeu de sémaphores existe déjà, les permissions d'accès sont contrôlées, et l'on vérifie si l'ensemble est sur le point d'être détruit.
L'entier retourné par semget() est le semid de l'ensemble de sémaphores ou «-1 » en cas d'erreur

synchronisation des opérations liées au sémaphore
La routine semop(int semid, struct sembuf *sops, unsigned nsops) agit sur l'ensemble de sémaphores identifié par semid en appliquant simultanément à plusieurs sémaphores individuels les actions décrites dans les nsops premiers éléments du tableau sops. Son prototype est :

int semop(int semid, struct sembuf *sops, unsigned nsops);

La routine semop( ) sert à la fois pour les opérations Pn( ) et Vn( ), sur de multiples sémaphores appartenant au jeu indiqué en premier argument. L'opération effectuée est déterminée ainsi :
-Lorsque le champ sem_op d'une structure sembuf est strictement positif, le noyau incrémente le compteur interne associé au sémaphore de la valeur indiquée et réveille les processus en attente.
Quand sembuf.sem_op = n, avec n > 0, alors l'opération est Vn( ).
-Lorsque le champ sem_op est strictement négatif, le noyau endort le processus jusqu'à ce que le compteur associé au sémaphore soit supérieur à sem_op, puis il décrémente le compteur de cette valeur avant de continuer l'exécution du processus.
Quand sembuf.sem_op = n, avec n < 0. alors l'opération est Pn( ).
-Lorsque le champ sem_op est nul, le noyau endort le processus jusqu'à ce que le compteur associé au sémaphore soit nul, puis il continue l'exécution du programme. Cette fonctionnalité permet de synchroniser les processus.
La routine semop( ) prend en second argument une table de structures sembuf. Le nombre d'éléments dans cette table est indiqué en dernière position. Le noyau garantit que les opérations seront atomiquement liées, ce qui signifie qu'elles seront toutes réalisées ou qu'aucune ne le sera. Bien entendu, il suffit qu'une seule opération avec sem_op négatif ou nul échoue avec l'attribut IPC_NOWAIT pour que toutes les modifications soient annulées.
Chaque sembuf est une structure de la forme

 struct sembuf{ 
  short sem_num;  /* semaphore number: 0 = first */
  short sem_op;   /* semaphore operation */
  short sem_flg;  /* operation flags */
  }

sem_flg est une combinaison d'indicateur qui peut contenir IPC_NOWAIT et SEM_UNDO . Ici nous supposons que sem_flg est 0.
sem_num indique le numéro du sémaphore individuel sur lequel porte l'opération.
sem_op est un entier destiné (sauf si il est nul) à être ajouté à la valeur courante semval du sémaphore. L'opération se bloque si «sem_op + semval < 0 ».
Cas particulier : si sem_op est 0, l'opération est bloquée tant que semval est non nul. Les valeurs des sémaphores ne sont mises à jour que lorsque aucun d'eux n'est bloqué.
Il existe deux options possibles pour le membre sem_flg :
- IPC_NOWAIT : l'opération ne sera pas bloquante, même si le champ sem_op est négatif ou nul, mais l'appel-système indiquera l'erreur EAGAIN dans errno si l'opération n'est pas réalisable.
- SEM_UNDO : pour être sûr que le sémaphore retrouvera un état correct même en cas d'arrêt intempestif du programme, le noyau va mémoriser l'opération inverse de celle qui a été réalisée et l'effectuera automatiquement à la fin du processus.

Contrôle de sémaphore
semctl(sem,n,SETVAL,val) permet de réaliser diverses opérations sur l'ensemble des sémaphores, selon la commande demandée et son prototype est :

int semctl(int semid, int semnum, int cmd, union semun arg )

En particulier, on peut fixer le n-ième sémaphore à la valeur val en faisant :
semctl(sem,n,SETVAL,val);


Exemples sur les sémaphores IPC V:

1) Exemple 1 :
Ce programme est une illustration des sémaphores dijkstra. Avec la routine fork() on créé un processus fils qui va donc s'éxecuter parallèlement avec le processus père ; chaque processus utilise un sémaphore pour communiquer; les deux processus font uniquement ce q'on leur demande. Quand Le processus notifie que le sémaphore track est libre quand il retourne 0; chaque processus doit modifier le sémaphore en conséquence.

 
#include <stdio.h>
 #include <sys/types.h>
 #include <sys/ipc.h>
 #include <sys/sem.h>
 union semun {
               int val;
               struct semid_ds *buf;
               ushort *array;
          };
main()
{ int i,j; 
  int pid;
  int semid; /* semid de l'ensemble de semaphores */
  key_t key = 1234; /* la clé associée  */
  int semflg = IPC_CREAT | 0666; /* semflg to pass to semget() */
  int nsems = 1; /* nsems to pass to semget() */
  int nsops; /* number of operations to do */
  struct sembuf *sops = (struct sembuf *) malloc(2*sizeof(struct sembuf)); 
  /* ptr to operations to perform */
  /* set up semaphore */  
  (void) fprintf(stderr, "\nsemget: Setting up seamaphore: semget(%#lx, %\
 %#o)\n",key, nsems, semflg);
   if ((semid = semget(key, nsems, semflg)) == -1) {
	perror("semget: semget failed");
	exit(1);
      } else 
	(void) fprintf(stderr, "semget: semget succeeded: semid =\
%d\n", semid);
  /* get child process */ 
   if ((pid = fork()) < 0) {
        perror("fork");
        exit(1);
    }   
if (pid == 0)
     { /* child */
       i = 0;      
       while (i  < 3) {/* allow for 3 semaphore sets */       
       nsops = 2;       
       /* wait for semaphore to reach zero */       
       sops[0].sem_num = 0; /* We only use one track */
       sops[0].sem_op = 0; /* wait for semaphore flag to become zero */
       sops[0].sem_flg = SEM_UNDO; /* take off semaphore asynchronous  */              
       sops[1].sem_num = 0;
       sops[1].sem_op = 1; /* increment semaphore -- take control of track */
       sops[1].sem_flg = SEM_UNDO | IPC_NOWAIT; /* take off semaphore */      
     /* Recap the call to be made. */       
    (void) fprintf(stderr,"\nsemop:Child  Calling semop(%d, &sops, %d) with:", semid, nsops);
       for (j = 0; j < nsops; j++)
	{
	  (void) fprintf(stderr, "\n\tsops[%d].sem_num = %d, ", j, sops[j].sem_num);
	  (void) fprintf(stderr, "sem_op = %d, ", sops[j].sem_op);
	  (void) fprintf(stderr, "sem_flg = %#o\n", sops[j].sem_flg);
	}	
       /* Make the semop() call and report the results. */
	if ((j = semop(semid, sops, nsops)) == -1) {
		perror("semop: semop failed");
		} 
	   else 
      {
		(void) fprintf(stderr, "\tsemop: semop returned %d\n", j);	
		(void) fprintf(stderr, "\n\nChild Process Taking Control of Track: %d/3 times\n", i+1);
		sleep(5); /* DO Nothing for 5 seconds */
		nsops = 1;     
       	/* wait for semaphore to reach zero */
       		sops[0].sem_num = 0;
           sops[0].sem_op = -1; /* Give UP COntrol of track */
           sops[0].sem_flg = SEM_UNDO | IPC_NOWAIT; /* take off semaphore, asynchronous  */
       	if ((j = semop(semid, sops, nsops)) == -1) {
			perror("semop: semop failed");
			} 
		   else
		   (void) fprintf(stderr, "Child Process Giving up Control of Track: %d/3 times\n", i+1);
       	   sleep(5); /* halt process to allow parent to catch semaphor change first */
     }
      ++i;
      }      
     }
  else /* parent */
     {  /* pid hold id of child */    
       i = 0;       
       while (i  < 3) { /* allow for 3 semaphore sets */       
       nsops = 2;       
       /* wait for semaphore to reach zero */
       sops[0].sem_num = 0;
       sops[0].sem_op = 0; /* wait for semaphore flag to become zero */
       sops[0].sem_flg = SEM_UNDO; /* take off semaphore asynchronous  */      
       sops[1].sem_num = 0;
       sops[1].sem_op = 1; /* increment semaphore -- take control of track */
       sops[1].sem_flg = SEM_UNDO | IPC_NOWAIT; /* take off semaphore */     
     /* Recap the call to be made. */     
     (void) fprintf(stderr,"\nsemop:Parent Calling semop(%d, &sops, %d) with:", semid, nsops);
      for (j = 0; j < nsops; j++)
	{
	  (void) fprintf(stderr, "\n\tsops[%d].sem_num = %d, ", j, sops[j].sem_num);
	  (void) fprintf(stderr, "sem_op = %d, ", sops[j].sem_op);
	  (void) fprintf(stderr, "sem_flg = %#o\n", sops[j].sem_flg);
	}
	/* Make the semop() call and report the results. */
	if ((j = semop(semid, sops, nsops)) == -1) {
		perror("semop: semop failed");
		} 
	   else 
      {
		(void) fprintf(stderr, "semop: semop returned %d\n", j);	
		(void) fprintf(stderr, "Parent Process Taking Control of Track: %d/3 times\n", i+1);
		sleep(5); /* Do nothing for 5 seconds */
      nsops = 1;       
      /* wait for semaphore to reach zero */
      sops[0].sem_num = 0;
      sops[0].sem_op = -1; /* Give UP COntrol of track */
      sops[0].sem_flg = SEM_UNDO | IPC_NOWAIT; /* take off semaphore, asynchronous  */       
      if ((j = semop(semid, sops, nsops)) == -1) {
			perror("semop: semop failed");
		} 
		else
		(void) fprintf(stderr, "Parent Process Giving up Control of Track: %d/3 times\n", i+1);
      sleep(5); /* halt process to allow child to catch semaphor change first */
       }
       ++i;       
      }      
     }
}

Explications :
- Après la création de l'ensemble de sémaphore avec la clé 1234 deux processus sont créés
- Chaque processus ( père et fils) produit les mêmes opérations:
- Chaque processus accède au même sémaphore track (sops[].sem_num = 0).
- Chaque processus attend que track soit libre et prend donc le contr&ocrc;le.
Cela prend fin par la mise de la bonne valeur dans sops[].sem_op.
- Une fois que le processus a le contrôle il attend 5 secondes (in reality some processing would take place in place of this simple illustration)
- Puis le processus abandonne le contrôle de track sops[1].sem_op = -1 .
- Un slep suplémentaire est réalisé pour être sûr qu'un autre n'ait pas le temps de prendre la main.
Remarque: Il n'y a pas de synchronisation ici; dans ce simple exemple seul l'OS positionne le processus qui doit prendre la main.

2 ) exemple :
C'est une implementation du probleme "producteur-consommateur" avec un buffer de capacite limitée. la synchronisation se fait au moyen de semaphores.

/*buffer.c*/
#include <stdio.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/ipc.h>
#include "../include/shmdef.h"
#include "../include/semdef.h"
#define BUFSIZE 10
int produce(){
  int n;
  n = rand() % 1000;
  fprintf(stdout,"producing %d\n",n);
  return(n);
}
consume(n)
     int n;
{
  fprintf(stdout,"            consuming %d\n",n);
}
append(n,b,in)
     int n,*b,*in;
{
  b[*in] = n;
  *in = (*in+1) % BUFSIZE;
}
int take(b,out)
     int *b,*out;
{
  int n;
  n = b[*out];
  *out = (*out+1) % BUFSIZE;
  return(n);
}
main(argc,argv)
     int argc;
     char *argv[];
{
  int shmid1, size, semid1, semid2, semid3, pid, fathersleep, sonsleep;
  int in, out;
  if (argc != 3) {
    fathersleep = 0;
    sonsleep = 0;
  }
  else {
    fathersleep = atoi(argv[1]);
    sonsleep = atoi(argv[2]);
  }
  size = BUFSIZE * sizeof(int);
  shmid1 = shmcreate(size);
  semid1 = screate();
  semid2 = screate();
  semid3 = screate();

  ssetval(semid1,1);
  ssetval(semid2,0);
  ssetval(semid3,BUFSIZE);
  in = 0;
  out = 0;
  if((pid=fork()) == 0) {
    int *addr, n, *b, i;
    addr = (int *)shmattach(shmid1);
    b = addr;
    for(i=1;i<=20;i++){
      swait(semid2);
      swait(semid1);
      n = take(b,&out);
      ssignal(semid1);
      ssignal(semid3);
      consume(n);
      sleep(sonsleep);
    }
  }
  else {
    if (pid != -1) {
      int *addr, n, *b, i, status;
      addr = (int *)shmattach(shmid1);
      b = addr;
      for(i=1;i<=20;i++) {
	sleep(fathersleep);
	n = produce();
	swait(semid3);
	swait(semid1);
	append(n,b,&in);
	ssignal(semid1);
	ssignal(semid2);
      }
      while(wait(&status) != -1);
      semremove(semid1);
      semremove(semid2);
      semremove(semid3);
      shmremove(shmid1);
    }
    else {
      fprintf(stderr,"No more procs\n");
      exit(1);
    }
  }
}
defintion des semdef.h
#ifndef	__semdef_h
#define	__semdef_h
#include <sys/sem.h>
#define PERMS 0666
extern int errno;
extern char *sys_errlist[];
union semun
{
  int val;
  struct semid_ds *buf;
  ushort *array;
};

/******************************************************************************
 * semcreate(i): creates a set of i semaphores and returns the associated     *
 * semid.                                                                     *
 ******************************************************************************/
int semcreate(i)
     int i;
{
  int semid;
  if ((semid = semget(IPC_PRIVATE,i,PERMS)) == -1)
    {
      fprintf(stderr,"error %d in semget: %s\n",errno,sys_errlist[errno]);
      exit(errno);
    };
  return(semid);
}
/******************************************************************************
 * screate(): creates a set of one semaphore and returns the associated semid *
 ******************************************************************************/
int screate()
{
  int semid;
  if ((semid = semget(IPC_PRIVATE,1,PERMS)) == -1)
    {
      fprintf(stderr,"error %d in semget: %s\n",errno,sys_errlist[errno]);
      exit(errno);
    };
  return(semid);
}
/******************************************************************************
 * semsetval(semid,n,i): sets to i the value of semaphore number n in the set *
 * of semaphores associated to semid.                                         *
 ******************************************************************************/
void semsetval(semid,n,i)
     int semid,n,i;
{
  union semun arg;
  arg.val=i;
  if (semctl(semid,n,SETVAL,arg) == -1)
    {
      fprintf(stderr,"error %d in semctl (semsetval): %s\n",
	      errno,sys_errlist[errno]);
      exit(errno);
    }
}
/******************************************************************************
 * ssetval(semid,i): sets to i the value of the semaphore associated to semid *
 ******************************************************************************/
void ssetval(semid,i)
int semid,i;
{
  union semun arg;
  arg.val=i;
  if (semctl(semid,0,SETVAL,arg) == -1)
    {
      fprintf(stderr,"error %d in semctl (ssetval): %s\n",
	      errno,sys_errlist[errno]);
      exit(errno);
    }
}
/******************************************************************************
 * semgetval(semid,n): returns the value of the semaphore number n in the set *
 * of semaphores associated to semid.                                         *
 ******************************************************************************/
int semgetval(semid,n)
     int semid,n;
{
  int nb;
  union semun arg;
  arg.val=0;

  if ((nb=semctl(semid,n,GETVAL,arg)) == -1)
    {
      fprintf(stderr,"error %d in semctl (semgetval): %s\n",
	      errno,sys_errlist[errno]);
      exit(errno);
    };
  return(nb);
}    
/******************************************************************************
 * sgetval(semid): returns the value of the semaphore associated to semid.    *
 ******************************************************************************/
int sgetval(semid)
     int semid;
{
  int nb;
  union semun arg;
  arg.val=0;

  if ((nb=semctl(semid,0,GETVAL,arg)) == -1)
    {
      fprintf(stderr,"error %d in semctl (sgetval): %s\n",
	      errno,sys_errlist[errno]);
      exit(errno);
    };
  return(nb);
}    
/******************************************************************************
 * semwanb(semid,n): returns the number of processes that are currently       *
 * suspended awaiting the value of semaphore number n associated to semid     *
 * to become greater than its current value.                                  *
 ******************************************************************************/
int semwanb(semid,n)
     int semid,n;
{
  int nb;
  union semun arg;
  arg.val=0;
  if ((nb = semctl(semid,n,GETNCNT,arg)) == -1)
    {
      fprintf(stderr,"error %d in semctl (semwanb): %s\n",errno,sys_errlist[errno]);
      exit(errno);
    };
  return(nb);
}    

/******************************************************************************
 * swanb(semid): returns the number of processes that are currently suspended *
 * awaiting the value of the semaphore associated to semid to become greater  *
 * than its current value.                                                    *
 ******************************************************************************/
int swanb(semid)
     int semid;
{
  int nb;
  union semun arg;
  arg.val=0;
  if ((nb = semctl(semid,0,GETNCNT,arg)) == -1)
    {
      fprintf(stderr,"error %d in semctl (swanb): %s\n",errno,sys_errlist[errno]);
      exit(errno);
    };
  return(nb);
}    
/******************************************************************************
 * semwait(semid,n,i): suspends the calling process until the value of        *
 * semaphore number n associated to semid becomes greater than or equal to i. *
 * Then, this value is decremented by i and the calling process resumes its   *
 * execution.                                                                 *
 ******************************************************************************/
void semwait(semid,n,i)
     int semid,n,i;
{
  struct sembuf *sops;
  sops=(struct sembuf *)malloc(sizeof(struct sembuf));
  sops[0].sem_num = n;
  sops[0].sem_op = -i;
  sops[0].sem_flg = 0;
  if (semop(semid,sops,1) == -1){
      fprintf(stderr,"pid %d, error %d in semop (semwait), semid %d: %s\n",
	     getpid(),errno,semid,sys_errlist[errno]);
      exit(errno);
    };
  free((char *)sops);
}
/******************************************************************************
 * swait(semid): suspends the calling process until the value of the          *
 * semaphore associated to semid becomes greater than or equal to 1. Then,    *
 * this value is decremented by 1 and the calling process resumes its         *
 * execution. This is the "classical" wait(s) operation.                      *
 ******************************************************************************/
void swait(semid)
     int semid;
{
  struct sembuf *sops;
  sops=(struct sembuf *) malloc(sizeof(struct sembuf));
  sops[0].sem_num = 0;
  sops[0].sem_op = -1;
  sops[0].sem_flg = 0;
  if (semop(semid,sops,1) == -1){
      fprintf(stderr,"pid %d, error %d in semop (swait), semid %d: %s\n",
	     getpid(),errno,semid,sys_errlist[errno]);
      exit(errno);
    };
  free((char *)sops);
}
/******************************************************************************
 * semsignal(semid,n,i): adds i to the value of semaphore number n associated *
 * to semid.                                                                  *
 ******************************************************************************/
void semsignal(semid,n,i)
     int semid,n,i;
{
  struct sembuf *sops;
  sops=(struct sembuf *) malloc(sizeof(struct sembuf));
  sops[0].sem_num = n;
  sops[0].sem_op = i;
  sops[0].sem_flg = 0;
  if (semop(semid,sops,1) == -1)
    {
      fprintf(stderr,"pid %d, error %d in semop (semsignal), semid %d: %s\n",
	     getpid(),errno,semid,sys_errlist[errno]);
      exit(errno);
    };
  free((char *)sops);
}
/******************************************************************************
 * ssignal(semid): adds 1 to the value of the semaphore associated to semid.  *
 * This is the "classical" signal(s) operation.                               *
 ******************************************************************************/
void ssignal(semid)
     int semid;
{
  struct sembuf *sops;
  sops=(struct sembuf *) malloc(sizeof(struct sembuf));
  sops[0].sem_num = 0;
  sops[0].sem_op = 1;
  sops[0].sem_flg = 0;
  if (semop(semid,sops,1) == -1)
    {
      fprintf(stderr,"pid %d, error %d in semop (ssignal),semid %d: %s\n",
	     getpid(),errno,semid,sys_errlist[errno]);
      exit(errno);
    };
  free((char *)sops);
}
/******************************************************************************
 * semremove(semid): removes the semaphore identifier specified by semid from   *
 * the system and destroys the set of semaphores and data structure           *
 *  associated with it.                                                       *
 ******************************************************************************/
void semremove(semid)
     int semid;
{
  union semun arg;
  arg.val=0;
  if (semctl(semid,0,IPC_RMID,arg) == -1){
      fprintf(stderr,"error %d in semctl (semremove): %s\n",
	      errno,sys_errlist[errno]);
      exit(errno);
  }
}
#endif /* !__semdef_h */

Files de messages

Files de messages
Une file de messages est une liste chaînées qui contient des données organisées sous forme d'un type suivi d'un bloc de messages. Cette liste chaînée est gérée par le noyau qui tient compte des priorités des différents processus accèdant à la même file de messages.
Pour communiquer, deux processus doivent partager au moins une file de messages et Une file de messages peut être paratgée par plus de deux processus. Il existe une différenciation a l'intérieur d'une même file de plusieurs type de messages. La structure d'une file de message est la suivante :

struct msqid_ds {
      struct ipc_perm  msg_perm;   /*  droits d'acces à l'objet */
      struct _msg      *msg_first; /*  pointeur sur le premier message */
      struct _msg      *msg_last;  /*  pointeur sur le dernier message */
      u_short          msg_qnum;   /*  nombre de messages dans la file */
      u_short          msg_bytes;  /*  nombre maximum d'octets */
      pid_t            msg_lspid;  /*  pid du dernier processus emetteur */
      pid_t            msg_lrpid;  /*  pid du dernier processus recepteur *
      time_t           msg_stime;  /*  date de derniere emission (msgsnd) *
      time_t           msg_rtime;  /*  date de derniere reception (msgrcv)
      time_t           msg_ctime;  /*  date de dernier changement (msgctl)
      u_short           msg_cbytes;/*  nombre total actuel d'octets */
   };

Ce mécanisme permet l'échange de messages par des processus. Chaque message possède un corps de longueur variable et un type (entier strictement positif) qui peut servir à préciser la nature des informations contenues dans le corps.
Un message est une structure regroupant au minimum deux champs. Le premier est le type du message. Ce type n'a aucun sens pour le système. Il sert juste à pouvoir partitionner les messages dans une file. Les autres champs forment le corps du message. Le système donne une définition générique d'un message.

struct msgbuf {
        long mtype;    /* type du message strictement positif*/
        char mtext[1]; /* texte du message */
   };

Au moment de la réception, on peut choisir de sélectionner les messages d'un type donné. Chaque utilisateur devra définir sa propre structure sur cette base.
Pour la création, l'envoi ou la reception on a besoin entre autre des bibliothèques <sys/types.h>, <sys/ipc.h> et <sys/msg.h>.
Le noyau gère un maximum de MSGMNI files indépendantes - 128 par défaut - chacune pouvant comporter des messages de tailles inférieures à MSGMAX, soit 4 056 octets (et pas 4 096). Pour accéder à une file existante ou en créer une nouvelle, on appelle la routine msgget( ).

Création de files de messages
la fonction msgget(key_t key, int msgflg) demande l'accès à (ou la création de) la file de messages avec la clé key. msgget() retourne la valeur de l'identificateur de la file ; son prototype est

 int msgget(key_t key, int msgflg);

S'il y a un problème elle retourne «-1». Ici le paramètre
key est la clé retournée par la fonction ftok() et le paramètre
msgflg la composition binaire des constantes IPC_CREAT et IPC_EXCL .
IPC_CREAT crée une nouvelle file s'il n'y en a aucune présente et associée à la clé transmise en premier argument et
IPC_EXCL crée une nouvelle file de messages . lLa fonction msgget( ) échoue si une file existe déjà avec la clé indiquée.

Envoi d'un message dans une file de messages
La fonction msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg) envoie un message dans une file de messages msqid . Le corps de ce message contient msgsz octets; il est placé et précédé par le type dans le tampon pointé par msgp. Ce tampon a la forme:

    struct msgbuf {
         long mtype;     /* message type, must be > 0 */
         char mtext[...] /* message data */
    };

et le le prototype de la fonction est msgsnd():

int msgsnd (int msqid, struct msgbuf *msgp, int msgsz, int msgflg); 

En cas d'erreur la fonction msgsnd (int msqid, struct msgbuf *msgp, int msgsz, int msgflg) retourne «-1».

Recevoir un message dans une files de messages
La fonction msgrcv() lit dans la file un message d'un type donné (si type > 0) ou indifférent (si type==0), et le place dans le tampon pointé par msgp. La taille du corps ne pourra excéder msgsz octets, sinon il sera tronqué. msgrcv() renvoie la taille du corps du message.

int msgrcv (int msqid, struct msgbuf *msgp, int msgsz,long msgtyp, int msgflg);

contrôle d'une file de message
le contrôle et le paramétrage d'une file de messages se font à l'aide de la fonction msgctl() dont le prototype est

int msgctl ( int msqid, int  cmd, struct msqid_ds *buf );

Il y a trois commandes possibles, qu'on passe en second argument :
1) IPC_STAT : pour obtenir les paramètres concernant la file de messages et les stocker dans la structure msqid_ds passée en dernière position. Cette structure sera détaillée plus bas. Il faut avoir l'autorisation de lecture sur la file de messages.
2)IPC_SET : pour configurer certains paramètres en utilisant la structure passée en troisième argument. Les paramètres qui sont mis à jour seront décrits ci-dessous. Pour pouvoir modifier ces éléments, il faut que le processus appelant soit le propriétaire ou le créateur de la file de messages, ou qu'il ait la capacité CAP_SYS_ADMIN.
3)IPC_RMID : pour supprimer la file de messages. Tous les processus en attente de lecture ou d'écriture sur la file seront réveillés. Les opérations ultérieures d'accès à cette file échoueront. Il y a toutefois un risque qu'une nouvelle file soit créée par la suite et que le noyau lui attribue le même identifiant. Si un processus attend longtemps avant d'accéder à la file supprimée, il risque de se trouver en face de la nouvelle file sans s'y attendre. Ce manque de fiabilité est l'un des arguments employés par les détracteurs des IPC Système V

Exemples :
Deux programmes, l'un pour envoyer des messages (lignes de texte) sur une file avec un type donné, l'autre pour afficher les messages reçus.
Envoi des messages par snd.c

          /* snd.c */
           /*
             envoi des messages dans une file (IPC System V)
           */
           #include <errno.h>
           #include <stdio.h>
           #include <stdlib.h>
           #include <stdio.h>

           #include <sys/types.h>
          #include <sys/ipc.h>
          #include <sys/msg.h>
          #define MAX_TEXTE 1000
          struct tampon {
              long mtype;
              char mtext[MAX_TEXTE];
          };
          void abandon(char message[]){
              perror(message);
              exit(EXIT_FAILURE);
          }
          int main(int argc, char *argv[]){
              int cle, id, mtype;
              if (argc != 3) {
                  fprintf(stderr, "Usage: %s clé type\n", argv[0]);
                  abandon("mauvais nombre de paramètres");
              }
              cle = atoi(argv[1]);
              mtype = atoi(argv[2]);
              id = msgget(cle, 0666);
              if (id == -1)
                  abandon("msgget");
              while (1) {
                  struct tampon m             int l, r;
                  printf("> ");             
fgets(msg.mtext, MAX_TEXTE, stdin); l = strlen(msg.mtext); msg.mtype = mtype; r = msgsnd(id, (struct msgbuf *) &msg, l + 1, 0); if (r == -1) abandon("msgsnd"); } }

Explications:

Reception des messages par rcv.c

           /* rcv.c */
           /*
              affiche les messages qui proviennent 
              d'une file (IPC System V)
           */
           #include <errno.h>
           #include <stdio.h>
           #include <stdlib.h>
           #include <stdio.h>

          #include <sys/types.h>
          #include <sys/ipc.h>
          #include <sys/msg.h>

          #define MAX_TEXTE 1000
          struct tampon {
              long mtype;
              char mtext[MAX_TEXTE];
          };
          int continuer_boucle = 1;
          void abandon(char message[]){
              perror(message);
              exit(EXIT_FAILURE);
          }
          int main(int argc, char *argv[]){
              int cle, id;
              if (argc != 2) {
                  fprintf(stderr, "Usage: %s cle\n", argv[0]);
                  abandon("Mauvais nombnre de paramètres");
           }
              cle = atoi(argv[1]);
              id = msgget(cle, IPC_CREAT | 0666);
              if (id == -1)
                  abandon("msgget");
              while (continuer_boucle) {
                  struct tampon msg;
                  int l = msgrcv(id, (struct msgbuf *) &msg, MAX_TEXTE, 0L, 0);
                  if (l == -1)
                      abandon("msgrcv");
                  printf("(type=%ld) %s\n", msg.mtype, msg.mtext);
              }
              return EXIT_SUCCESS;
          }

Explications:

précédent suivant

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

  1. 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.
  2. 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).
  3. 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.
  4. 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:

  1. 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.
  2. 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.
  3. 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);
        }

précédent suivant

I) Les sockets en C/C++

Généralités

Socket image

Une socket est un point d'accès aux couches réseau TCP/UDP. les sockets constituent un mécanisme qui permet la communication sur le réseau Internet  ou entre processus locaux tournant sur une même machine; ces processus locaux sont  les FIFOs (une extension du concept "pipe" du monde UNIX), les pipes et les files de messages IPC.

On crée une socket par la fonction "socket(int domaine, int type, int protocole);" qui retourne un "int" qui n'est rien d'autre que le  descripteur de la fonction "socket()" dans la table des descripteurs des processus. La socket s'utilise comme un fichier ordinaire et on se sert des routines classiques réservées aux fichiers (les routines "read", "write", ...) ou des opérations spécifiques aux communications (send, sendto, recv, recvfrom, ...).

Après sa création, une socket n’est connue que du processus qui l’a créée et par ses descendants. Elle doit être désignée par une adresse pour pouvoir être contactée de l’extérieur par d'autres processus locaux ou distants.
Les communications qu'on réalise avec les sockets sont soit  en mode connecté soit en mode datagramme.

Dans la routine socket (int domaine, int type, int protocole), le paramètre "domaine" précise si le socket est pour une communication réseau ou une communication inter-processus; si on est dans le premier cas,  on parle de socket internet et le paramètre domaine prend la valeur   "AF_INET". Si c'est le deuxième  cas, on a un socket UNIX c'est à dire une communication inter-processus et le paramètre "domaine" vaut AF_UNIX; toutefois quelque soit le domaine  choisi  on se retrouve  dans l'un des quatre modes  de communication suivantes:

  1. en mode connecté à travers Internet: TCP
  2. en mode connecté à travers Unix: TCP
  3. en mode datagramme à travers Internet: UDP
  4. en mode datagramme à travers Unix: UDP

II) Communication

Généralités

Les deux modes connectées (TCP) et les deux modes datagrammes (UDP),  sont deux modes de socket respectivement appelés  SOCK_STREAM et  SOCK_DGRAM.
Si le paramètre le domaine vau AF_INET,  ce sont les numéros de port qui donnent le point de rendez-vous des sockets et si il est égale à AF_UNIX, ce sont des noms de fichiers qui donnent le point de rendez-vous.
Les concepts fondamentaux des sockets ( adresses, communication par datagrammes et flots, client-serveur, etc) utilisés sont les mêmes pour les sockets locales (communications sur une même machine - AF_UNIX) que pour les échanges par internet. Les particularités viennent de l' élaboration des adresses.

Stream Sockets et Datagram Sockets
On fabrique une adresse à partir d'un nom de machine (résolution) et d'un numéro de port  ou on retrouve le nom d'une machine à partir d'une adresse (résolution inverse).
Il y a plusieurs type de sockets (exemples les "Raw Sockets") mais les deux types qui nous intéressent ici sont les sockets de flux ("Stream Sockets") et les sockets de paquets ("Datagram Sockets") qui sont référencées respectivement par SOCK_STREAM et SOCK_DGRAM dans la suite de ce document. Les "sockets de paquets" sont parfois appelées les "sockets sans connection".
Les sockets de flux ("Stream Sockets") sont deux voies de communications bi-directionnelles et fiables. Si on y envoie deux éléments  dans l'ordre "1", "2", ces deux éléments arriveront dans le même ordre "1", "2" à l'autre bout de la connexion  sans erreur.

Les protocoles utilisés lors d'une communication entre réseaux font partie d'une organisation multicouche. Les informations échangées lors d'une communication internet se font par paquets (paquet ici n'a rien à voir avec datagramme); chaque paquet est emballé (encapsulé) avec une en-tête par le protocole qui va prendre en charge le paquet. Exemple, si on utilise le protocole TFTP, l'ensemble en-tête protocole est encapsulé par le protocole qui suit (par exemple UDP si c'est lui qui va prendre le relais ou IP sinon)  et finalement par la couche du protocole matériel (ou physique) (disons Ethernet).
Quand à l'ordinateur qui reçoit le paquet, sa couche matérielle enlève l'entête Ethernet, le noyau enlève les en-têtes IP ou UDP, le programme TFTP enlève l'entête TFTP et récupère finalement les données envoyées c'est à dire l'information.
Tout ce qu'on doit faire si on utilise les sockets de flux est d'envoyer les données avec la routine  send() et pour les sockets de paquets d'encapsuler le paquet avec la méthode de votre choix et d'appeler sendto(). Le noyau appelle alors la couche Transport et Internet à partir de vos données et le matériel appelle la couche d'accès réseau.

Création d'un socket

La création d'une socket se fait par la routine socket (int domaine, int type, int protocole), et l'utilisation de cette routine nécessite la présence des fichiers en-tête "#include <sys/types.h>" et  "#include <sys/socket.h>".
Les arguments de la  routine socket (int domaine, int type, int protocole) sont:

  • Domaine
    Ce paramètre définit une famille de protocoles (protocol family):
    PF_INET  ou AF_INET: cette constante comme domaine désigne les protocoles internet IPv4 (protocole fondé sur IP à savoir TCP,  UDP,  ICMP),
    AF_UNIX: celle-ci comme domaine désigne les protocoles pour les processus résidents sur la même machine
    PF_INET6: protocoles internet IPv6,
    PF_IPX: protocoles Novel IPX,
    PF_X25: protocoles X25 (ITU-T X.25 / ISO-8208),
    PF_APPLETALKAppletalk  protocoles Apple, etc.
  • type
    le type indique le style de communication désiré entre les deux participants; le type est soit SOCK_DGRAM ou SOCK_STREAM
  • protocole
    Ce troisième argument permet de spécifier le protocole à utiliser. Il est du type UDP ou TCP en général et les types de protocoles les plus courants sont:
    IPPROTO_TCP pour TCP
    IPPROTO_SCTP pour SCTP
    IPPROTO_UDP pour UDP
    IPPROTO_RAW et IPPROTO_ICMP uniquement avec SOCK_RAW PROTOCOL; le protocole est souvent mis à zéro car l'association de la famille de protocole et du type de communication définit explicitement le protocole de transport.
    Exemple, si on a PF_INET  comme domaine et SOCK_STREAM pour type,  --------> TCP => c'est à dire que le paramètre protocole vaut IPPROTO_TCP et si on a PF_INET + SOCK_DGRAM --------> UDP => protocole vaut IPPROTO_UDP; ce paramètre protocole est une constante définie dans le fichier d'en-têtes "/usr/include/netinet/in.h" et qui reflète le contenu du fichier système "/etc/protocols".

III) Communication par le réseau

Généralités

La communication par le réseau en utilisant les sockets internet se fait par flots de données (TCP) ou par datagrammes (UDP) et l'adresse de chaque  socket est la combinaison d'un numéro IP et  d'un numéro de port. Dans un système d'adressage IPv4, on utilise des structures de type "struct sockaddr_In" de la famille  AF_INET ( ou PF_INET) pour gérer les adresses IP car elle a un champ de type  "struct in_addr"  réservé à l'adresse: 

 struct in_addr {
union {
struct {
u_char s_b1,s_b2,s_b3,s_b4;
} S_un_b;
struct {
u_short s_w1,s_w2;
} S_un_w;
u_long S_addr;
} S_un;
#define s_addr S_un.S_addr /* cette define doit être utilisée pour tout le code */
#define s_host S_un.S_un_b.s_b2 /* OBSOLETE: host on imp */
#define s_net S_un.S_un_b.s_b1 /* OBSOLETE: network */
#define s_imp S_un.S_un_w.s_w2 /* OBSOLETE: imp */
#define s_impno S_un.S_un_b.s_b4 /* OBSOLETE: imp # */
#define s_lh S_un.S_un_b.s_b3 /* OBSOLETE: logical host */
};

La structure struct sockaddr_In est donc:

struct sockaddr_in {
short sin_family; /* doit être AF_INET */
u_short sin_port;
struct in_addr sin_addr; /* adresse IP de la machine */
char sin_zero[8];
};

et les membres de la structure "sockaddr_in"  sont les  3 champs sin_family, sin_addr et sin_port:

  1. sin_family   donne la famille d'adresses, qui vaut AF_INET ( ou PF_INET)
  2. sin_addr, pour l'adresse IP
  3. sin_port, pour le numéro de port

Pour IPv6, on utilise des structures de type "struct sockaddr_in6"

struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};

Cette structure possède un membre de type  "struct in6_addr" qui gère les  adresses IPv6: 

struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};

Comme pour les IPV4 on a les 3 champs de sockaddr_in6 qui permettent de bien construire l'adresse d'une socket:

  1. sin6_family, valant AF_INET6
  2. sin6_addr, l'adresse IP sur 6 octets
  3. sin6_port, pour le numéro de port.

Les octets de l'adresse IP et le numéro de port sont stockés dans l'ordre réseau (big-endian); cela n'est pas forcément le cas de toutes les machines hôtes sur laquelle s'exécuteront tous les programmes s'appuyant sur les sockets internet d'où la nécessité de faire certaines conversions et adaptations pour construire une adresse de socket.

Préparation d'une adresse

Structure de type "struct sockaddr"
Pour accéder à une socket sur une machine il faut une adresse. Les opérations sur les adresses concernent un type d'adresse général abstrait de type struct sockaddr qui recouvre tous les types concrets particuliers.
Pour réaliser une communication par socket internet, on dispose du nom de la machine hôte et d'un numéro de port. La structure "struct hostent" retournée par le pointeur de fonction *gethostbyname(const char *name) (voir #include <netdb.h>) donne  accès à plusieurs informations sur la machine hôte. Cette structure hostent est définie dans <netdb.h> comme suit:

struct hostent {
   char    *h_name;       /* Nom officiel de l'hôte.   */
   char   **h_aliases;    /* Liste d'alias.            */
   int      h_addrtype;   /* Type d'adresse de l'hôte. */
   int      h_length;     /* Longueur de l'adresse.    */
   char   **h_addr_list;  /* Liste d'adresses.         */
}

Ses membres sont :

  • h_name : Nom officiel de la machine hôte
  • h_aliases Une table, terminée par zéro, d'alternatives au nom officiel de l'hôte.
  • h_addrtype: Le type d'adresse (actuellement, toujours AF_INET).
  • h_length: La longueur, en octets, de l'adresse.
  • h_addr_list: Une table, terminée par zéro, d'adresses réseau pour l'hôte, avec l'ordre des octets du réseau.
  • h_addr: La première adresse dans h_addr_list pour respecter la compatibilité ascendante.

Si une machine a plusieurs adresses, ce qui est le cas quand on a plusieurs cartes réseaux/interfaces présentes sur une machine, on précise la carte concernée   en remplissant correctement la structure sin_addr
struct sockaddr_in {
short sin_family; /* must be AF_INET */
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
avec
struct in_addr {
unsigned long s_addr; // charger avec la routine inet_pton()
};

L’adresse IP d’une machine ( de type "struct in_addr") est sur 4 octets écrits  sous la forme "xyz.xzy.zxf.sdv" et quand au port adéquat, sa valeur est celle du champ sin_port,   un entier court en ordre réseau; si on y met un entier ordinaire, il faut le convertir par htons() (Host TO Network Short).
Pour l'adresse de la machine locale (à ne pas confondre avec la notion de socket local - socket UNIX -) on utilise, dans le cas le plus fréquent, l'adresse INADDR_ANY (0.0.0.0); ainsi La socket est ouverte (avec le même numéro de port) sur toutes les adresses IP de toutes les interfaces de la machine.
On peut aussi se servir, de l'adresse INADDR_LOOPBACK qui correspond à l'adresse locale 127.0.0.1 (alias localhost). et dans ce cas la socket n'est accessible que depuis la machine elle-même, d'une des adresses IP de la machine et de l'adresse de diffusion générale (broadcast) INADDR_BROADCAST (255.255.255.255)

Construction de l'adresse
  Du Côté du Serveur, on remplit une structure de type "struct sock_addr"  et on indique sur quel port le serveur se met en attente de connexion; on définit une structure de  type "struct sock_addr" et on assigne à chaque champ de la structure  une valeur comme suit:
struct sock_addr soc_in; /* on déclare la structue*/
soc_in.sin_family = AF_INET; /* on assigne au champ sin_family la valeur AF_INET,*/
soc_in.sin_addr.s_addr = htonl (INADDR_ANY);
soc_in.sin_port = htons (port);
Le champ sin_addr de la structure sock_addr  est une structure; son champ sin_addr.s_addr reçoit la valeur de retour de la fonction htonl(INADDR_ANY).

Du côté client, ...

Utilisation d'une socket côté serveur

Pour travailler avec des sockets, du côté serveur,  on a besoin d'un certain nombres de routines telles que :

  1. socket(): cette permet de créer la socket (voir ci-dessus)
  2. bind: cette routine attache une adresse (port) à une socket, la socket créée par la routine socket(). Les adresses >= 1024 sont réservées au super-utilisateur. Pour l'utiliser il faut avoir les éléments qui suivent:
    struct sockaddr_in soc_in1;
    bzero(&soc_in1,sizeof(soc_in1));
    soc_in1.sin_family = AF_INET;
    soc_in1.sin_port = htons(7000);
    soc_in1.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(sockfd,&soc_in1,sizeof(soc_in1) < 0) /* error */
  3. listen(): cette routine donne la longueur maximale de la queue d'attente des demandes de connexions non servies (par exemple 5). ce qui est très utile car, en général le serveur fait un fork() (voir dans le cours sur les processus la définition de fork() )dès qu'il accepte une connexion.
  4. accept (sockfd, &soc_in1, &lg); // l'appel à cette fonction fournit une nouvelle socket qui servira pour  dialoguer avec le client; c'est une fonction bloquante qui attend les demandes de connexion et qui retourne un nouveau file descriptor qui permettra de faire les entrées/sorties sur la nouvelle connexion.
    struct sockaddr_in soc_in1; // soc_in1 permet de recevoir l'adresse client
    int lg = sizeof(soc_inc1);
    int newsockfd = socket(sockfd, &soc_in1,&lg); // sockfd est le descripteur retourner par socket() 
  5. A partir de là on travaille avec le socket ce qui donne la séquence ci-dessus côté serveur:

La séquence côté serveur:
int fd, newfd;
if ((fd = socket(...)) < 0)
erreur ("ouverture socket");
if (bind(fd, ...) < 0)
erreur ("bind");
if (listen (fd, 5) < 0)
erreur ("listen");
for (; ;) {
if ((newfd = accept (fd, ...)) < 0) /* blocant */
erreur ("accept");
if (fork() == 0) {
close (fd);
.... /* on continue avec newfd */
exit (0);
} close (newfd);
}

Utilisation d'une socket côté client

La séquence côté client:

fd = socket(domaine, type, protocole);
connect (fd, adresse_serveur, longueur_adresse_serveur);
read (fd, ..., ...);
write (fd, ..., ...);

........


Communication par le réseau: réalisation d'un serveur TCP

Les fonctions essentielles

Un serveur TCP traite des connexions venant d'un ou plusieurs clients. Après avoir créé et nommé une socket, le serveur spécifie qu'il accepte les communications entrant par la fonction listen(int s, int backlog), et se met effectivement en attente d'une connexion de client par la fonction accept(int s, struct sockaddr *addr,socklen_t *addrlen); ces routines nécessitent la présence des deux fichiers include "#include <sys/types.h>" et " #include <sys/socket.h>".

Le paramètre backlog de la fonction listen indique la taille maximale de la file des connexions qui peuvent être mise en attente. Sous Linux cette limite est donnée par la constante SOMAXCONN (qui vaut 128), sur d'autres systèmes elle est limitée à 5.
La fonction accept() renvoie une autre socket, qui servira à la communication avec le client. L'adresse du client peut être obtenue par les paramètres addr et addrlen.
En général les serveurs TCP traitent plusieurs connexions simultanément. La solution habituelle est de lancer, après l'appel à accept() un processus fils (par fork())qui traite la communication avec un seul client. Ceci induit une gestion des processus, donc des signaux liés à la terminaison des processus fils. Cette façon de gérer les serveurs TCP sera traitée dans la partie de l'article concernant les processus.

Réalisation d'un serveur TCP

Etude d'un serveur TCP (serveur web) qui renvoie les fichiers (textes) d'un répertoire sous forme de pages HTML.

 /* serveur-web.c
* Ce Serveur TCP est un
* 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
*/
#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 "webCServer.h"
void arreter_serveur(int numero_signal);
void attendre_sous_serveur(int numero_signal);
int fd_serveur; /* variable globale, pour partager avec traitement signal fin_serveur */

void demarrer_serveur(int numero_port, char repertoire[]){
int numero_client = 0;
int fd_client;
struct sigaction action_int, action_chld;
fd_serveur = serveur_tcp(numero_port);

/* arrêt du serveur si signal SIGINT */
action_int.sa_handler = arreter_serveur;
sigemptyset(&action_int.sa_mask);
action_int.sa_flags = 0;
sigaction(SIGINT, &action_int, NULL);

/* attente fils si SIGCHLD */
action_chld.sa_handler = attendre_sous_serveur;
sigemptyset(&action_chld.sa_mask);
action_chld.sa_flags = SA_NOCLDSTOP;
sigaction(SIGCHLD, &action_chld, NULL);

printf("> Serveur " VERSION
" (port=%d, répertoire de documents=\"%s\")\n",
numero_port, repertoire);
while (1) {
struct sockaddr_in a;
size_t l = sizeof a;
fd_client = attendre_client(fd_serveur);

getsockname(fd_client, (struct sockaddr *) &a, &l);
numero_client++;
printf("> client %d [%s]\n", numero_client,
inet_ntoa(a.sin_addr));
if (fork() == 0) {
/* le processus fils ferme le socket serveur et s'occupe du client */
close(0);
close(1);
close(2);
close(fd_serveur);
servir_client(fd_client,repertoire);
close(fd_client);
exit(EXIT_SUCCESS);
}
/* le processus père n'a plus besoin du socket client.
Il le ferme et repart dans la boucle */
close(fd_client);
}
}
/*
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 attendre_sous_serveur(int numero_signal){
/* cette fonction est appelée chaque fois qu'un signal SIGCHLD
indique la fin d'un processus fils _au moins_. */
while (waitpid(-1, NULL, WNOHANG) > 0) {
/* attente des fils arrêtés, tant qu'il y en a */
}
}
/* -------------------------------------------------------------*/
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);
}

Explications

La fonction "int getopt(argc, argv, optstring)"
Cette fonction analyse les arguments de la ligne de commande. Ses éléments argc et argv correspondent aux nombres et à la table d'arguments qui sont transmis à la fonction main() lors du lancement du programme. Un élément de argv qui commence par `-' (et qui ne soit pas uniquement "-" ou "--") est considéré comme une option. Les caractères à la suite du '-' initial sont les caractères de l'option. Si getopt() est appelée à plusieurs reprises, elle renverra successivement chaque caractère de chaque option.
Cette fonction permet de récupérer les options relatives à l'utilisation de argrc et argv dans un programme C/C++; elle retourne le prochain caractère d'option dans argv qui concorde le caractère dans optstring; optstring doit contenir les caractères d'option que la fonction getopt() doit reconnaître ; si le caratère est suivi par deux points, l'option s'attend à avoir un argument, ou un groupe d'arguments, qui doivent être séparés par un espaces blanc.
Une fois qu'on est dans le bon tempo, on appelle la fonction demarre_serveur() qui, à son tour, appelle la fonction serveur_tcp(numero_port); après cet appel il installe un certain nombre de signaux qui permettent de gérer le serveur .
La fonction serveur_tcp(numero_port) démarre un service TCP sur le port indiqué et retourne le descripteur de la socket si le démarrage s'est bien passé. Une fois le serveur démarré on entre dans une boucle infini et on attend les clients ( voir les fonctions serveur_tcp(int numero_port), attendre_client(fd_serveur) et le fichier "webCServer.h" ci-dessus).

Les fonctions réseau du projet

La fonction serveur_tcp (int numero_port)

 /*
  Projet serveur Web
   Fonctions réseau
 */
  #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 <signal.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include "declarations.h"
 /*----------------------------------------------------------
  Fonctions réseau
  -----------------------------------------------------------*/
  int serveur_tcp(int numero_port){
    
        int fd;
        /* démarre un service TCP sur le port indiqué */
        struct sockaddr_in addr_serveur;
        size_t lg_addr_serveur = sizeof addr_serveur;
        /* création de la prise */
        fd = socket(AF_INET, SOCK_STREAM, 0);
        if (fd < 0)
         FATAL("socket");
         /* nommage de la prise */
        addr_serveur.sin_family = AF_INET;
        addr_serveur.sin_addr.s_addr = INADDR_ANY;
        addr_serveur.sin_port = htons(numero_port);
        if (bind(fd, (struct sockaddr *) &addr_serveur, lg_addr_serveur) < 0)
             FATAL("bind");             
        /* ouverture du service */
        listen(fd, 4);
        return (fd);
     }

.......

La fonction attendre_client (int fd_serveur)

      int attendre_client(int fd_serveur)
     {
         int fd_client;
         /* A cause des signaux SIGCHLD, la fonction accept()
            peut etre interrompue quand un fils se termine.
            Dans ce cas, on relance accept().
          */
          while ((fd_client = accept(fd_serveur, NULL, NULL)) < 0) {
              if (errno != EINTR)
                  FATAL("Fin anormale de accept().");
          }
         return (fd_client);
     }
 

...

Le fichier webCServer.h

     /* Serveur Web - webCServer.h */
     #define CRLF "\r\n"
      #define VERSION "MegaSoft 0.0.7 pour Unix"
      #define PORT_PAR_DEFAUT 8000
      #define REPERTOIRE_PAR_DEFAUT "/tmp"
     #define FATAL(err) { perror((char *) err); exit(1);}
     extern void servir_client(int fd_client, char repertoire[]);
     extern void envoyer_document(FILE * out,
                                  char nom_document[], char repertoire[]);
     extern void document_non_trouve(FILE * out, char nom_document[]);
     extern void requete_invalide(FILE * out);
      extern int serveur_tcp(int numero_port);
     extern int attendre_client(int fd_serveur);

On verra dans l'étude des processus une autre implémentation du serveur TCP qui utilisera les threads; 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é puis,

ouvrir une socket côté 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

Les fonctions de dialogue avec les Clients: traitement-client.c

Dans l'étude du serveur ci-dessus on a défini un certain nombre de fonctions dans le fichier "traitement-client.c"; ces fonctions permettent de traiter l'échange et le dialogue avec les clients;

  • dialogue_client() : lecture et traitement de la requête d'un client
  • envoyer_document(),
  • document_non_trouve(),
  • requete_invalide().
  /*
traitement-client.c
projet serveur WEB
Communication avec un client
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.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 <signal.h>
#include "webCServer.h"

void dialogue_client(int fdClient, char repertoire[]){
FILE *in, *out;
char verbe[100], nom_document[100];
int fd2;

/* Ouverture de fichiers de haut niveau */
in = fdopen(fdClient, "r");
/* note : si on attache out au même descripteur que in,
la fermeture de l'un entraine la fermeture de l'autre */
fd2 = dup(fdClient);
out = fdopen(fd2, "w");

/* lecture de la requête, du genre
"GET quelquechose ..." */
fscanf(in, "%100s %100s", verbe, nom_document);
fclose(in);

if (strcmp(verbe, "GET") == 0)
envoyer_document(out, nom_document, repertoire);
else
requete_invalide(out);
fflush(out); /* utile ? */
fclose(out);
}
    
void envoyer_document(FILE * out, char nom_document[], char repertoire[]){
char nom_fichier[100];
FILE *fichier;
char ligne[100];

sprintf(nom_fichier, "%s%s", repertoire, nom_document);
if (strstr(nom_fichier, "/../") != NULL) {
/* tentative d'accès hors du répertoire ! */
document_non_trouve(out, nom_document);
return;
}
fichier = fopen(nom_fichier, "r");
if (fichier == NULL) {
document_non_trouve(out, nom_document);
return;
}
fprintf(out,
"HTTP/1.1 200 OK" CRLF
"Server: " VERSION CRLF
"Content-Type: text/html; charset=iso-8859-1" CRLF
CRLF);
fprintf(out,
"<html><head><title>Fichier %s</title></head>"
"<body bgcolor=\"white\"><h1>Fichier %s</h1>" CRLF
"<center><table><tr><td bgcolor=\"yellow\"><listing>"
CRLF, nom_document, nom_fichier);
/* le corps du fichier */
while (fgets(ligne, 100, fichier) > 0) {
char *p;
for (p = ligne; *p != '\0'; p++) {
switch (*p) {
case '<':
fputs("&lt;", out);
break;
case '>':
fputs("&gt;", out);
break;
case '&':
fputs("&amp;", out);
break;
case '\n':
fputs(CRLF, out);
break;
default:
fputc(*p, out);
};
};
}
/* balises de fin */
fputs("</listing></table></center></body></html>" CRLF, out);
}
    void document_non_trouve(FILE * out, char nom_document[]){
/* envoi de la réponse : entête */
fprintf(out,
"HTTP/1.1 404 Not Found" CRLF
"Server: MegaSoft 0.0.7 (CP/M)" CRLF
"Content-Type: text/html; charset=iso-8859-1" CRLF
CRLF);

/* corps de la réponse */
fprintf(out,
"<HTML><HEAD>" CRLF
"<TITLE>404 Not Found</TITLE>" CRLF
"</HEAD><BODY BGCOLOR=\"yellow\">" CRLF
"<H1>Pas trouvé !</H1>" CRLF
"Le document <font color=\"red\"><tt>%s</tt></font> "
"demandé<br>n'est pas disponible.<P>" CRLF
"<hr> Le webmaster"
"</BODY></HTML>" CRLF, nom_document);
fflush(out);
}
     void requete_invalide(FILE * out){
fprintf(out,
"<HTML><HEAD>" CRLF
"<TITLE>400 Bad Request</TITLE>" CRLF
"</HEAD><BODY BGCOLOR=\"yellow\">" CRLF
"<H1>Bad Request</H1>"
"Vous avez envoyé une requête que "
"ce serveur ne comprend pas." CRLF
"<hr> Le webmaster" "</BODY></HTML>" CRLF);
fflush(out);
}

  /* ---------------------------------------------------------
Fonctions réseau
---------------------------------------------------------*/
int serveur_tcp(int numero_port){
int fd;
/* démarre un service TCP sur le port indiqué */
struct sockaddr_in addr_serveur;
size_t lg_addr_serveur = sizeof addr_serveur;
/* création de la prise */
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
FATAL("socket");
/* nommage de la prise */
addr_serveur.sin_family = AF_INET;
addr_serveur.sin_addr.s_addr = INADDR_ANY;
addr_serveur.sin_port = htons(numero_port);
if (bind(fd, (struct sockaddr *) &addr_serveur, lg_addr_serveur) < 0)
FATAL("bind");

/* ouverture du service */
listen(fd, 4);
return (fd);
}
 int attendre_client(int fd_serveur){
int fd_client;
/* A cause des signaux SIGCHLD, la fonction accept()
peut etre interrompue quand un fils se termine.
Dans ce cas, on relance accept().
*/
while ((fd_client = accept(fd_serveur, NULL, NULL)) < 0) {
if (errno != EINTR)
FATAL("Fin anormale de accept().");
}
return (fd_client);
}
 void servir_client(int fdClient, char repertoire[]){
FILE *in, *out;
char verbe[100], nom_document[100];
int fd2;
/* Ouverture de fichiers de haut niveau */
in = fdopen(fdClient, "r");
/* note : si on attache out au même descripteur que in,
la fermeture de l'un entraine la fermeture de l'autre */
fd2 = dup(fdClient);
out = fdopen(fd2, "w");
/* lecture de la requête, du genre
"GET quelquechose ..." */
fscanf(in, "%100s %100s", verbe, nom_document);
fclose(in);
if (strcmp(verbe, "GET") == 0)
envoyer_document(out, nom_document, repertoire);
else
requete_invalide(out);
fflush(out); /* utile ? */
fclose(out);
}

....


Communication par le réseau:  réalisation d'un client TCP-IP

La socket d'un client TCP doit être reliée (par connect()) à celle  du serveur, et elle est utilisée ensuite par les routines read() et des write(), ou des entrées-sorties de haut niveau comme  fprintf(), fscanf()  si on a défini des flots par fdopen().

 /* web_client.c */
/*
Interrogation d'un serveur web
Usage:
web_client serveur port adresse-document
retourne le contenu du document d'adresse
http://serveur:port/adresse-document
Exemple:
web_client www.info.prive 80 /index.html
Fonctionnement:
- ouverture d'une connexion TCP vers serveur:port
- envoi de la requête GET adresse-document HTTP/1.0[cr][lf][cr][lf]
- affichage de la réponse
*/
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <netdb.h>
#include <string.h>
#include <stdlib.h>
#define CRLF "\r\n"
#define TAILLE_TAMPON 1000

void abandon(char message[]){
perror(message);
exit(EXIT_FAILURE);
}
/* -- connexion vers un serveur TCP --- */
int ouvrir_connexion_tcp(char nom_serveur[], int port_serveur){
struct sockaddr_in addr_serveur;
struct hostent *serveur;
int fd;
fd = socket(AF_INET, SOCK_STREAM, 0); /* création prise */
if (fd < 0)
abandon("socket");
serveur = gethostbyname(nom_serveur); /* recherche adresse serveur */
if (serveur == NULL)
abandon("gethostbyname");
addr_serveur.sin_family = AF_INET;
addr_serveur.sin_port = htons(port_serveur);
addr_serveur.sin_addr = *(struct in_addr *) serveur->h_addr;
if (connect(fd, /* connexion au serveur */
(struct sockaddr *) &addr_serveur,
sizeof addr_serveur)
< 0)
abandon("connect");
return (fd);
}

void demander_document(int fd, char adresse_document[]){
char requete[TAILLE_TAMPON];
int longueur;
/* constitution de la requête, suivie d'une ligne vide */
longueur = snprintf(requete, TAILLE_TAMPON,
"GET %s HTTP/1.0" CRLF CRLF,
adresse_document);
write(fd, requete, longueur); /* envoi */
}
void afficher_reponse(int fd){
char tampon[TAILLE_TAMPON];
int longueur;
while (1) {
longueur = read(fd, tampon, TAILLE_TAMPON); /* lecture par bloc */
if (longueur <= 0)
break;
write(1, tampon, longueur); /* copie sur sortie standard */
};
}
int main(int argc, char *argv[]){
char *nom_serveur, *adresse_document;
int port_serveur;
int fd;
if (argc != 4) {
printf("Usage: %s serveur port adresse-document\n", argv[0]);
abandon("nombre de paramètres incorrect");
}
nom_serveur = argv[1];
port_serveur = atoi(argv[2]);
adresse_document = argv[3];
fd = ouvrir_connexion_tcp(nom_serveur, port_serveur);
demander_document(fd, adresse_document);
afficher_reponse(fd);
close(fd);
return EXIT_SUCCESS;
}

Explications

Le coeur du programme est la fonction ouvrir_connexion_tcp(nom_serveur, port_serveur); au sein de cette fonction on crée la socket( avec les vérifications d'usage), on cherche l'adresse du serveur par la fonction gethostbyname() (dans la foulée on a les champs de la structure "sockaddr_in" qui sont remplis) et enfin on a la connexion au serveur qui est réalisée; une fois la connexion faite, la fonction ouvrir_connexion qui retourne le descripteur de la socket est créée.

.......


Communication par le réseau: datagramme (UDP)

Généralités

La communication par socket Internet commence par la création d'une socket par la routine socket(int domain, int type, int protocol). Cette fonction construit une socket et retourne un numéro de descripteur. On utilise la famille AF_INET, et comme on échange des datagrammes on se sert du type SOCK_DGRAM et le paramètre protocole par défaut vaut "0" (Si la routine socket() retourne "-1" on est dans un cas d'échec.
Avec une socket en mode connecté, Une fois la socket créée, la fonction connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen) met en relation la socket (de cette machine) avec une autre socket désignée, qui sera le « correspondant par défaut » pour la suite des opérations.
Dans le cas d'une socket connectée on peut envoyer des datagrammes (contenu dans un tampon "t" de longueur "n") en se servant de la fonction write(sockfd,t,n); sinon on peut se servir de la fonction send(int s, const void *msg, size_t len, int flags) qui permet de positionner des flags comme MSG_DONTWAIT pour une écriture non bloquante. Une autre façon de faire est d'utiliser la fonction sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) qui permet d'envoyer des datagrammes à une adresse spécifiée sur une socket connectée ou non. Pour la reception on peut se servir d'un simple read(), de recv(int s, void *buf, size_t len, int flags) (avec des flags) ou d' un recvfrom(int s, void *buf, size_t len, int flags,struct sockaddr *from, socklen_t *fromlen), qui permet de récupérer l'adresse "from" de l'émetteur. 
.......


Communication par le réseau: réalisation d'un serveur UDP

Le serveur ouvre une socket sur un port en mode non-connecté, affiche les messages(chaînes de caractères) qu'il reçoit par ce socket puis envoie une réponse:
......

         /*
         serveur-echo.c -  Réception de datagrammes
         Exemple de serveur qui
         - ouvre un socket sur un port en mode non-connecté
         - affiche les messages (chaînes de caractères)
         qu'il reçoit par ce socket.
         - envoie une réponse
         */
       #include <unistd.h>
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <netinet/in.h>
      #include <signal.h>
      #include <stdio.h>
      #include <stdlib.h>
      #include <arpa/inet.h>
      #include <ctype.h>
      #include <string.h>
      #define TAILLE_TAMPON 1000
      static int fd;
      void abandon(char message[])
      {
          printf("SERVEUR> Erreur fatale\n");
          perror(message);
          exit(EXIT_FAILURE);
      }
      void arreter_serveur(int signal)
      {
          close(fd);
          printf("SERVEUR> Arrêt du serveur (signal %d)\n", signal);
          exit(EXIT_SUCCESS);
      }
      int main(int argc, char *argv[])
      {
          struct sockaddr_in adresse_serveur;
          size_t taille_adresse_serveur;
          int numero_port_serveur;
          char *src, *dst;
          struct sigaction a;
          /* 1. réception des paramètres de la ligne de commande */
          if (argc != 2) {
              printf("usage: %s port\n", argv[0]);
              abandon("mauvais nombre de paramètres");
          }
          numero_port_serveur = atoi(argv[1]);
          /* 2. Si une interruption se produit, arrêt du serveur */
          /* signal(SIGINT, arreter_serveur); */
          a.sa_handler = arreter_serveur;
          sigemptyset(&a.sa_mask);
          a.sa_flags = 0;
          sigaction(SIGINT, &a, NULL);
          /* 3. Initialisation du socket de réception */
          /* 3.1 Création du socket en mode non-connecté 
             (datagrammes) */
          fd = socket(AF_INET, SOCK_DGRAM, 0);
          if (fd < 0)
              abandon("socket");
          /* 3.2 Remplissage de l'adresse de réception 
             (protocole Internet TCP-IP, réception acceptée sur toutes 
             les adresses IP du serveur, numéro de port indiqué)
           */
          adresse_serveur.sin_family = AF_INET;
          adresse_serveur.sin_addr.s_addr = INADDR_ANY;
          adresse_serveur.sin_port = htons(numero_port_serveur);
          /* 3.3 Association du socket au port de réception */
          taille_adresse_serveur = sizeof adresse_serveur;
          if (bind(fd,
                   (struct sockaddr *) &adresse_serveur,
                   taille_adresse_serveur) < 0)
              abandon("bind");
          printf("SERVEUR> Le serveur écoute le port %d\n",
                 numero_port_serveur);
          while (1) {
              struct sockaddr_in adresse_client;
              int taille_adresse_client;
              char tampon_requete[TAILLE_TAMPON],
                  tampon_reponse[TAILLE_TAMPON];
              int lg_requete, lg_reponse;
              /* 4. Attente d'un datagramme (requête) */
              taille_adresse_client = sizeof(adresse_client);
              lg_requete = recvfrom(fd, tampon_requete, TAILLE_TAMPON, 0,     /* flags */
                                    (struct sockaddr *) &adresse_client,
                                    (socklen_t *) & taille_adresse_client);
              if (lg_requete < 0)
                  abandon("recfrom");
              /* 5. Affichage message avec sa provenance et sa longueur */
              printf("%s:%d [%d]\t: %s\n",
                     inet_ntoa(adresse_client.sin_addr),
                     ntohs(adresse_client.sin_port), lg_requete,
                     tampon_requete);
              /* 6. Fabrication d'une réponse */
              src = tampon_requete;
              dst = tampon_reponse;
              while ((*dst++ = toupper(*src++)) != '\0');
              lg_reponse = strlen(tampon_reponse) + 1;
              /* 7. Envoi de la réponse */
              if (sendto(fd,
                         tampon_reponse,
                         lg_reponse,
                        0,
                        (struct sockaddr *) &adresse_client,
                        taille_adresse_client)
                 < 0)
                 abandon("Envoi de la réponse");
         }
         /* on ne passe jamais ici */
         return EXIT_SUCCESS;
     }

Explications

..........................


Communication par le réseau: réalisation d'un client UDP

     /*
       client-echo.c
       Envoi de datagrammes
       Exemple de client qui 
       - ouvre un socket 
       - envoie des datagrammes sur ce socket (lignes de textes
       de l'entrée standard)
       - attend une réponse
       - affiche la réponse
       */
     #include <unistd.h>
     #include <sys/types.h>
     #include <sys/socket.h>
     #include <netinet/in.h>
     #include <signal.h>
     #include <stdio.h>
     #include <netdb.h>
     #include <string.h>
     #include <stdlib.h>
     #define TAILLE_TAMPON 1000
     static int fd;
     void abandon(char message[])
     {
         printf("CLIENT> Erreur fatale\n");
         perror(message);
         exit(EXIT_FAILURE);
     }
     int main(int argc, char *argv[])
     {
         struct sockaddr_in adresse_socket_serveur;
         struct hostent *hote;
         int taille_adresse_socket_serveur;
         char *nom_serveur;
         int numero_port_serveur;
         char *requete, reponse[TAILLE_TAMPON];
         int longueur_requete, longueur_reponse;
         /* 1. réception des paramètres de la ligne de commande */
         if (argc != 4) {
             printf("Usage: %s hote port message\n", argv[0]);
             abandon("nombre de paramètres incorrect");
         }
         nom_serveur = argv[1];
         numero_port_serveur = atoi(argv[2]);
         requete = argv[3];
         /* 2. Initialisation du socket */
         /* 2.1 création du socket en mode datagramme */
         fd = socket(AF_INET, SOCK_DGRAM, 0);
         if (fd < 0)
             abandon("Creation socket");
         /* 2.2 recherche de la machine serveur */
         hote = gethostbyname(nom_serveur);
         if (hote == NULL)
             abandon("Recherche serveur");
         /* 2.3 Remplissage adresse serveur */
         adresse_socket_serveur.sin_family = AF_INET;
         adresse_socket_serveur.sin_port = htons(numero_port_serveur);
         adresse_socket_serveur.sin_addr =
             *(struct in_addr *) hote->h_addr;
         taille_adresse_socket_serveur = sizeof adresse_socket_serveur;
         /* 3. Envoi de la requête */
         printf("REQUETE> %s\n", requete);
         longueur_requete = strlen(requete) + 1;
         if (sendto(fd, requete, longueur_requete, 0,        /* flags */
                    (struct sockaddr *) &adresse_socket_serveur,
                    taille_adresse_socket_serveur)
             < 0)
             abandon("Envoi requete");
         /* 4. Lecture de la réponse */
         longueur_reponse =
             recvfrom(fd, reponse, TAILLE_TAMPON, 0, NULL, 0);
         if (longueur_reponse < 0)
             abandon("Attente réponse");
         printf("REPONSE> %s\n", reponse);
         close(fd);
         printf("CLIENT> Fin.\n");
         return EXIT_SUCCESS;
     } 

Explications

....


IV) Communication inter-processus 

Généralités

Les concepts utilisés pour les sockets UNIX/Linux (socket local) et les sockets internet sont  identiques; le Langage C fournit un certain nombre de routines (fonctions) qui permettent de les utiliser. En ce qui concerne Windows, Microsoft a rajouté quelques MACROs qui peuvent rendre les programmes non portables d'un environnement à un autre.
Comme pour les sockets Internet qu'on a vu ci-dessus, la création d'une socket UNIX est réalisée par la routine socket(int domain, int type, int protocol) qui retourne un entier appelé descripteur. Le paramètre "domaine" indique le «domaine de communication» utilisé; pour les communications locales c' est PF_LOCAL synonyme de PF_UNIX. Le paramètre "type" est l'un des types SOCK_DGRAM (communication par messages -blocs contenant des octets- appelés datagrammes) ou SOCK_STREAM (communication par flot -bidirectionnel- d'octets une fois que la connection est établie) et quand au paramètre "protocol" il indique le protocole selectionné; ce protocole est égal à zéro ce qui correspond au protocole par défaut pour le domaine et le type sélectionné.

La fonction socket(int domain, int type, int protocol) crée un socket anonyme. Pour qu'un autre processus puisse le désigner, il faut lui associer un nom par l'intermédiaire d'une adresse contenue dans une structure appelée sockaddr_un. L'association d'une adresse à une socket se fait par la routine C "bind(int descsock, struct sockaddr *ptlocsockaddr, int locsockaddrlen)"; ceci associe l’adresse locale "*ptlocsockaddr" à la socket "descsock"  et le paramètre "locsockaddrlen" représente la taille de l’adresse "*ptlocsockaddr". Cette adresse est un chemin d'accès dans l'arborescence des fichiers et des répertoires.
Pour utiliser cette structure "sockaddr_un" dans programme  il faut y inclure les lignes de code suivantes:
#include <sys/types.h >
#include <sys/socket.h >
#include <sys/un.h >
socklen_t longueur_adresse;
struct sockaddr_un adresse;
adresse.sun_family = AF_LOCAL;
strcpy(adresse.sun_path,"/tmp/xyz");
longueur_adresse = SUN_LEN (&dresse);

Dans ces quelques lignes d'instrution qui permettront d'utiliser les sockets UNIX, on voit bien les fonctions en-tête à utiliser et l'utilisation de la variable "adresse" de type "struct sockadd_un":

  struct sockaddr_un {
        sa_family_t  sun_family;               /* AF_UNIX  */
        char         sun_path[UNIX_PATH_MAX];  /* pathname */
};

et l'utilisation de la MACRO SUN_LEN() définie dans <sys/un.h> (le champ sun_path doit se terminer avec un caractère NULL).

......

On va illustrer l'utilisation des fonctions de l'API Socket en construisant des applications qui serviront de serveur et/ou de clients.


Communication inter-processus par datagramme:

Dans l'échange de données en mode non connectée (Datagrammes) chaque acteur (le serveur ou le client) suit une démarche précise.

Le client doit :

  • Créer la socket par la fonction socket(..)
  • Envoyer le message par la fonction sendto(int s, const void *msg, size_t len, int flags,const struct sockaddr *to, socklen_t tolen). sendto utilise le descripteur de socket "s" pour envoyer le message formé des "len" premiers octets de "msg" à l'adresse de longueur "tolen" pointée par "to". Le même descripteur peut être utilisé pour des envois à des adresses différentes.
  • Fermer la socket par la fonction close(..) une fois les message envoyées.

Quand au serveur il doit:

  • Créer la socket par la fonction socket(...)
  • Attacher une adresse à cette socket par la fonction bind(...)
  • Et par la fonction recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen) attendre l'arrivée d'un datagramme qui est stocké dans les "len" premiers octets du tampon "buff". Si "*from" n'est pas NULL, l'adresse de la socket émetteur qui peut servir à expédier une réponse est placée dans la structure pointée par "*from", dont la longueur maximale est contenue dans l'entier pointé par "fromlen". Si la lecture est réussie, la fonction retourne le nombre d'octets du message lu, et la longueur de l'adresse est mise à jour.

........


Communication inter-processus par datagramme: réalisation d'un serveur UDP

Le serveur reçoit des datagrammes par une socket du domaine local et les affiche; il s'arrête quand la donnée envoyée est "stop".

  /* serveur-dgram-local.c   */     
       #include <stdio.h>
       #include <unistd.h>
     #include <stdlib.h >
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <sys/un.h>
      #define TAILLE_MAX_DONNEE 1024
      void abandon(char message[]){
          perror(message);
          exit(EXIT_FAILURE);
      }
      int main(int argc, char *argv[]){
          socklen_t longueur_adresse;
          struct sockaddr_un adresse;
          int fd;
          if (argc != 2) {
              fprintf(stderr, "usage: %s chemin\n", argv[0]);
              abandon("mauvais nombre de parametres");
          }
          adresse.sun_family = AF_LOCAL;
          strcpy(adresse.sun_path, argv[1]);
          longueur_adresse = SUN_LEN(&adresse);
          fd = socket(PF_LOCAL, SOCK_DGRAM, 0);
          if (fd < 0)
              abandon("Création du socket serveur");
          if ( bind(fd, (struct sockaddr *) &adresse, longueur_adresse) < 0)
              abandon("Nommage du socket serveur");
          printf("> Serveur démarré sur socket local \"%s\"\n", argv[1]);
          while (1) {
              int lg;
              /* un caractère supplémentaire permet d'ajouter le
                 terminateur de chaîne, qui n'est pas transmis */
              char tampon[TAILLE_MAX_DONNEE + 1];
              lg = recvfrom(fd, tampon, TAILLE_MAX_DONNEE, 0, NULL, NULL);
              if (lg <= 0)
                  abandon("Réception datagramme");
              tampon[lg] = '\0';      /* ajout terminateur */
              printf("Reçu : %s\n", tampon);
              if (strcmp(tampon, "stop") == 0) {
                  printf("> arrêt demandé\n");
                  break;
              }
          }
          close(fd);
          unlink(argv[1]);
          return EXIT_SUCCESS;
      }

Détails

Comme il est dit un peu plus haut, le serveur commence par créer la socket  par la routine socket, attache la socket à une adresse et puis se met en attente du client; comme on échange des datagrammes, cette attente se fait par la fonction recevfrom:
le paramètre PF_LOCAL de la routine socket()  montre bien qu'on est entrain de créer une socket pour une communication inter_processus, SOCK_DGRAM  précise que ce qu'on échange ce sont des datagrammes; le dernier paramètre vaut zéro car on n'a pas besoin de préciser le protocol puisque au vu des deux premiers paramètres le système sait quel protocole est utilisé. La routine bind() attache la socket à une adresse et enfin  le serveur se met en attente l'arrivée d'un client.


Communication inter-processus par datagramme: réalisation d'un client UDP

Le client (client-dgram-local) envoie des datagrammes à une socket du domain local, et à charge au serveur de les afficher.

/* client-dgram-local.c */
/* 
          Usage: client-dgram-local chemin messages
          Ici chemin indique localhost si client 
          et serveur s'y trouvent.
          les autres des chaînes de caractères.
          Exemple : client-dgram-local un deux "trente et un" stop
  */
       #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <sys/un.h>
      #define TAILLE_MAX_DONNEE 1024
      void abandon(char message[])
      {
         perror(message);
          exit(EXIT_FAILURE);
      }
      int main(int argc, char *argv[])
      {
          socklen_t longueur_adresse;
          struct sockaddr_un adresse;
          int fd;
          int k;
          if (argc <= 2) {
              fprintf(stderr, "usage: %s chemin message\n", argv[0]);
              abandon("mauvais nombre de paramètres");
          }
          adresse.sun_family = AF_LOCAL;
          strcpy(adresse.sun_path, argv[1]);
          longueur_adresse = SUN_LEN(&adresse);
          fd = socket(PF_LOCAL, SOCK_DGRAM, 0);
          if (fd < 0)
              abandon("Création du socket client");
          for (k = 2; k < argc; k++) {
              int lg;
              lg = strlen(argv[k]);
              if (lg > TAILLE_MAX_DONNEE)
                  lg = TAILLE_MAX_DONNEE;
              /* le message est envoyé sans le terminateur '\0' */
              if (sendto(fd, argv[k], lg, 0,
                         (struct sockaddr *) &adresse,
                         longueur_adresse) < 0)
                  abandon("Expédition du message");
              printf(".");
              fflush(stdout);
              sleep(1);
          }
          printf("OK\n");
          close(fd);
          return EXIT_SUCCESS;
      } 

Détails

 On a la création de la socket par la routine socket(PF_Local, SOCK_DGRAM,0); il est précisé via les paramètres de l'appel de la fonction socket (...) qu'on a, utilisation de socket UNIX (voir PF_LOCAL), une échange de datagrammes (voir SOCK_DGRAM); une fois que les données sont prêtes on les envoie par la routine sendto 

.......


Communication inter-processus par flot de données: réalisation d'un client

L'application qui sert de Client suit un certain nombres d'étapes qui font appel aux fonctions suivantes de l'API Socket:

  1. la fonction socket() telle que:  fd= socket(PF_LOCAL, SOCK_STREAM,0) qui  créé la socket
  2. la fonction connect() telle que: connect(fd, (struct sockaddr *) &adresse, longueur_adresse )
  3. une boucle for(;;) par exemple)qui permettra de faire les actions suivantes:
    {
    write() pour émission 
    read() pour receptionr
    }
  4. la fonction close(fd) pour fermer la socket

Le client envoie (en mode connecté) des données par une socket locale : il ouvre une socket, envoie sur cette socket du texte lu à partir de l'entrée standard, attend et affiche la réponse.

Exemple de client TCP

/*     
         client-stream
         Envoi/réception de données par un socket local (mode connecté)
  */       
       #include <unistd.h>
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <sys/un.h>
      #include <signal.h>
      #include <stdio.h>
      #include <string.h>
      #include <stdlib.h>
      #define TAILLE_TAMPON 1000
      void abandon(char message[])
      {
          perror(message);
          exit(EXIT_FAILURE);
      }
      int main(int argc, char *argv[])
      {
          char *chemin;               /* chemin d'accès du socket serveur */
          socklen_t longueur_adresse;
          struct sockaddr_un adresse;
          int fd;
          /* 1. réception des paramètres de la ligne de commande */
          if (argc != 2) {
              printf("Usage: %s chemin\n", argv[0]);
              abandon("Mauvais nombre de paramètres");
          }
          chemin = argv[1];
          /* 2. Initialisation du socket */
          /* 2.1 création du socket   */
          fd = socket(PF_LOCAL, SOCK_STREAM, 0);
          if (fd < 0)
              abandon("Création du socket client");
          /* 2.2 Remplissage adresse serveur */
          adresse.sun_family = AF_LOCAL;
          strcpy(adresse.sun_path, chemin);
          longueur_adresse = SUN_LEN(&adresse);
          /* 2.3 connexion au serveur */
          if (connect(fd, (struct sockaddr *) &adresse, longueur_adresse)
              < 0)
              abandon("connect");
          printf("CLIENT> Connexion établie\n");
          /* 3. Lecture et envoi des données */
          for (;;) {
              char tampon[TAILLE_TAMPON];
              int nb_lus, nb_envoyes;
              nb_lus = read(0, tampon, TAILLE_TAMPON);
              if (nb_lus <= 0)
                  break;
              nb_envoyes = write(fd, tampon, nb_lus);
              if (nb_envoyes != nb_lus)
                  abandon("envoi données");
          }
          /* 4. Fin de l'envoi */
          shutdown(fd, 1);
          printf("CLIENT> Fin envoi, attente de la réponse.\n");
          /* 5. Réception et affichage de la réponse */
          for (;;) {
              char tampon[TAILLE_TAMPON];
              int nb_lus;
              nb_lus = read(fd, tampon, TAILLE_TAMPON - 1);
              if (nb_lus <= 0)
                  break;
              tampon[nb_lus] = '\0';  /* ajout d'un terminateur de chaîne */
              printf("%s", tampon);
          }
          /* et fin */
          close(fd);
          printf("CLIENT> Fin.\n");
          return EXIT_SUCCESS;
      }

Détails

.....


Communication interprocessus par flot de données: réalisation d'un Serveur

L'application qui joue le rôle de serveur  suit les étapes suivantes:

  1. socket()
  2. bind()
  3. listen()
  4. while (1) {
    1. accept()
    2. while (x) { read() ou write() }
    3. close()
    4. }
  5. close()

Le serveur monotâche (serveur-stream-monotache) envoie des données par une socket local; ce serveur monotâche gère plusieurs connexions; dès qu'il reçoit une demande de connexion, il lit le texte reçu et envoie une réponse.Comme il traîte une seule communication à la fois, Si le client fait traîner les choses, les autres clients en attente resteront bloqués longtemps.

Exemle de serveur TCP

 /*  
         serveur-stream-monotache
         Envoi/réception de données par un socket local (mode connecté)
         Exemple de serveur monotâche qui gère plusieurs connexions
         - attend une connexion
         - lit du texte
         - envoie une réponse
  */
       #include <unistd.h>
      #include <sys/types.h>
      #include <sys/socket.h>
      #include <sys/un.h>
      #include <signal.h>
      #include <stdio.h>
      #include <stdlib.h>
      #include <ctype.h>
      #include <assert.h>
      #define TAILLE_TAMPON             1000
      #define MAX_CONNEXIONS_EN_ATTENTE 4
      #define MAX_CLIENTS               10
      /* les données propres à chaque client */
      #define INACTIF -1
      struct {
          int fd;
          int numero_connexion;
          int compteur;
      } client[MAX_CLIENTS];
      void abandon(char message[])
      {
          perror(message);
          exit(EXIT_FAILURE);
      }
      int main(int argc, char *argv[])
      {
          int fd_serveur;
          struct sockaddr_un adresse;
          size_t taille_adresse;
          char *chemin;
          int nb_connexions = 0;
          int i;
          int nbfd;
          /* 1. réception des paramètres de la ligne de commande */
          if (argc != 2) {
              printf("usage: %s chemin\n", argv[0]);
              abandon("mauvais nombre de paramètres");
          }
          chemin = argv[1];
          /* 3. Initialisation du socket de réception */
          /* 3.1 création du socket   */
          fd_serveur = socket(PF_LOCAL, SOCK_STREAM, 0);
if (fd_serveur < 0)
abandon("Création du socket serveur");
/* 3.2 Remplissage adresse serveur */
adresse.sun_family = AF_LOCAL;
strcpy(adresse.sun_path, chemin);
taille_adresse = SUN_LEN(&adresse);
/* 3.3 Association de l'adresse au socket */
taille_adresse = sizeof adresse;
if (bind(fd_serveur, (struct sockaddr *) &adresse, taille_adresse)
< 0)
abandon("bind");
/* 3.4 Ce socket attend des connexions mises en file d'attente */
listen(fd_serveur, MAX_CONNEXIONS_EN_ATTENTE);
printf("SERVEUR> Le serveur écoute le socket %s\n", chemin);
/* 3.5 initialisation du tableau des clients */
for (i = 0; i < MAX_CLIENTS; i++) {
client[i].fd = INACTIF;
}
/* 4. boucle du serveur */
for (;;) {
fd_set lectures;
/* 4.1 remplissage des masques du select */
FD_ZERO(&lectures);
FD_SET(fd_serveur, &lectures);
for (i = 0; i < MAX_CLIENTS; i++) {
if (client[i].fd != INACTIF)
FD_SET(client[i].fd, &lectures);
}
/* 4.2 attente d'un événement (ou plusieurs) */
nbfd = select(FD_SETSIZE, &lectures, NULL, NULL, NULL);
assert(nbfd >= 0);
/* 4.3 en cas de nouvelle connexion : */
if (FD_ISSET(fd_serveur, &lectures)) {
/* si il y a de la place dans la table des clients,
on y ajoute la nouvelle connexion */
nbfd--;
for (i = 0; i < MAX_CLIENTS; i++) {
if (client[i].fd == INACTIF) {
int fd_client;
fd_client = accept(fd_serveur, NULL, NULL);
if (fd_client < 0)
abandon("accept");
client[i].fd = fd_client;
client[i].numero_connexion = ++nb_connexions;
client[i].compteur = 0;
printf
("SERVEUR> arrivée de connexion #%d (fd %d)\n",
client[i].numero_connexion, fd_client);
break;
}
if (i >= MAX_CLIENTS) {
printf("SERVEUR> trop de connexions !\n");
}
}
};
/* 4.4 traitement des clients actifs qui ont reçu des données */
for (i = 0; (i < MAX_CLIENTS) && (nbfd > 0); i++) {
if ((client[i].fd != INACTIF) &&
FD_ISSET(client[i].fd, &lectures)) {
int nb_lus;
char tampon[TAILLE_TAMPON];
nbfd--;
nb_lus =
read(client[i].fd, tampon, TAILLE_TAMPON - 1);
printf("SERVEUR> données reçues de #%d (%d octets)\n",
client[i].numero_connexion, nb_lus);
if (nb_lus > 0)
client[i].compteur += nb_lus;
else {
int nb_car;
printf
("SERVEUR> envoi de la réponse au client #%d.\n",
client[i].numero_connexion);
nb_car =
sprintf(tampon,
"*** Fin de la connexion #%d\n",
client[i].numero_connexion);
write(client[i].fd, tampon, nb_car);
nb_car =
sprintf(tampon,
"*** Vous m'avez envoyé %d caractères\n",
client[i].compteur);
write(client[i].fd, tampon, nb_car);
nb_car =
sprintf(tampon, "*** Merci et à bientôt.\n");
write(client[i].fd, tampon, nb_car);
close(client[i].fd);
/* enlèvement de la liste des clients */
client[i].fd = INACTIF;
}
}
}
assert(nbfd == 0); /* tous les descripteurs ont été traités */
}
/* on ne passe jamais ici (boucle sans fin) */
return EXIT_SUCCESS;
}

Détails

La technique utilisée ici permet de traiter plusieurs connexions par un processus unique : le serveur maintient une liste de descripteurs ouverts, fait une boucle autour d'un select(), en attente de données venant d'un des descripteurs des clients, et il traite alors les données venant de ce client (il l'enlève de la liste en fin de communication). Cette technique conduit à des performances nettement supérieures aux serveurs multiprocessus ou multithreads (pas de temps perdu à lancer des processus), au prix d'une programmation qui oblige le programmeur à gérer lui-même le ``contexte de déroulement'' de chaque processus.
A la ligne 80 on a la fonction select ("int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);") qui permet attendre que des données soient prêtes à être lues sur un des descripteurs de l'ensemble readfs, ou que l'un des descripteurs de writefds soit prêt à recevoir des écritures, que des exceptions se produisent (exceptfds), ou encore que le temps d'attente timeout soit épuisé. Lorsque select() se termine, readfds, writefds et exceptfds contiennent les descripteurs qui ont changé d'état. select() retourne le nombre de descripteurs qui ont changé d'état, ou -1 en cas de problème.
L'entier n doit être supérieur (strictement) au plus grand des descripteurs contenus dans les 3 ensembles (c'est en fait le nombre de bits significatifs du masque binaire qui représente les ensembles). On peut utiliser la constante FD_SETSIZE. Les pointeurs sur les ensembles (ou le délai) peuvent être NULL, ils représentent alors des ensembles vides (ou une absence de limite de temps). Les macros FD_CLR, FD_ISSET, FD_SET, FD_ZERO permettent de manipuler les ensembles de descripteurs.

.........


V) Exemples utilisant les sockets :

L'application Telnet (Tous les caractères que vous tapez doivent arriver dans le même ordre que celui dans lequel ils ont été tapés), les navigateurs Web et les serveurs exploitant le protocole HTTP utilisent les sockets de flux pour recevoir des pages. Cela se vérifie si vous exécutez telnet sur un serveur WWW en spécifiant le port 80 et que vous tapez "GET pagename", alors vous aurez en retour la page HTML!
Les sockets de flux utilisent un protocole appelé ""The Transmission Control Protocol" plus connu sous le nom de "TCP" (Voir RFC-793 pour beaucoup plus de détails sur TCP). TCP s'assure que vos données arrivent séquentiellement et sans erreurs. TCP s'associe au protocol IP et ce dernier traite uniquement le routage Internet.

précédent suivant