
                La programmation multitche (ou parallle)
                            en C sous Unix
             par Professor Falken <prfalken@freeshell.org>


chtit Disclaimer:
*****************

Ce T-phile n'est pas sens tre une rfrence complte sur tous les moyens
de programmer en multi-tache sous Unix, ou un manuel de rfrence sur
l'utilisation de ces moyens.
Je l'ai plutot crit pour donner un aperu des techniques les plus
courantes dans ce domaine, et des pointeurs vers les documentations
(man pages, info, LDP, bouquins...) adquates  une rfrence dtaille.
J'ai aussi tent d'attirer l'attention sur des exemples d'utilisation
pouvant entrainer des bugs et/ou des trous de scurit dans les programmes
lors de l'utilisation du multi-tache.

Je ne serais en aucun cas responsable d'une utilisation des informations
ci-dessous  des fins illgales ou mal-intentionnes
( ne surtout pas confondre :)), et je ne serais pas non-plus responsable
des prjudices (intrusions, trous de scurit...) causs par l'
(la mauvaise) utilisation de ces informations.
En fait, j'aurais pu ripper ce disclaimer dans n'importe quel bouquin sur
la programmation =))))

Une relativement bonne connaissance du language C et du systme Unix sont
prfrables pour la bonne comprhension de ce T-phile.
Il faut au moins tre capable de faire la diffrence entre une fonction
de librairie et un appel systme, savoir ce qu'est un fichier, savoir
lire et comprendre un programme en C, un algorigramme en ASCII :))),
avoir une notion des processus sous Unix (PID, PPID, child, parent...),
ne pas confondre un programme et un processus (un programme peut parfois etre
un ensemble de processus),
savoir ce qu'est un signal, avoir une ide aproximative de la gestion
d'un systme multitache sur un seul CPU, et tre capable de visualiser
clairement des venements simultans, sans repre temporel...
En gros, faut pas en tre  coder un "Hello World" avec stdio et se
demander comment on va le compiler =)))


Intro:
*****

    Unix, et tous les autres systmes qui en drivent,
(SysV, *BSD, SunOs, AIX, Linux, minix,...) ont comme principale
caractristique d'tre multitches.
Ca veut dire que, sur un mme systme (pour ne pas dire un mme processeur),
on peut excuter plusieurs processus (tches, applications, programmes...)
simultanment.
Cela se rvle extrement puissant lorsque on veut partager le mme
systeme entre plusieurs utilisateurs, ou alors qu'un seul utilisateur veut
se servir de plusieurs applications en mme temps
(ie: compilateur + editeur + reader de docs).
Cette utilisation du multitche ne ncessite pas obligatoirement de programmes
conus pour le multitche, car celui-ci est gr par le systme, et les
processus croient en gnral qu'ils sont seuls  fonctionner dans leur espace 
mmoire.

Mais il existe en standard, sous Unix, la possibilit de grer plusieurs
parties d'un mme programme en parallle.
Ceci s'avre avantageux dans le cas d'un programme serveur (FTP, HTTP...),
d'un cluster de machines formant une machine virtuelle (PVM), ou encore d'un
systme multi-processeurs (SMP), d'un systme temps rel
(automate industriel, pilote automatique...), ou plus simplement d'un
logiciel ncessitant une programmation venementielle (ie. GTK+).

Il existe principalement deux faons de grer le multi-tasking dans un
programme sous Unix: le multi-threading, et la communication inter-processus
(IPC), les deux pouvant bien sur tre mixs pour donner des algorythmes
d'excution plus ou moins complexes.



Le Multi-threading:
******************

L'API de multi-threading la plus rpandue est le standard POSIX 1003.1c, plus
connu sous le nom de POSIX-threads (ou pthreads). Elle est implmente sous
Linux, avec la librairie LinuxThreads, livre en standard avec la GNU libc 2.
On la trouve aussi sous Solaris 2.5, Digital Unix 4.0, SGI Irix 6, et sur les
dernires versions de AIX et HP-UX.

Le principe du multi-threading est que le programme utilisant ces fonctions
se divise en plusieurs branches (threads) d'excution, partageant le meme
espace d'adressage mmoire, les memes descripteurs de fichiers, les memes
gestionnaires de signaux (signal handlers), et, theoriquement, le meme PID
(quoique cela est faux sous Linux).

Ainsi, on peut facilement creer un programme adapt aux architectures
paralleles (MPP, SMP...) avec des branches d'executions pouvant accder aux
memes variables et aux memes fichiers.

exemple simple d'algorythme multi-thread:

                           /--------\
                           | Parent |       Processus parent
                           \--------/
                               |  division en 2 branches (threads)
                              / \
               thread 1 ---> /   \ <---- thread 2
                            /     \
                           /       \
                  +--->---v         v------<-----+
                  |       |         |            |
                  |   --------       \           ^
                  ^  (  a++   )  +--------+  N   |
                  |   --------  / a == k ? \--->-+
                  |       |     ++--------++
                  +----<--|           | Y
                                      v
                                      |
                                   /------\
                                   | FIN  |
                                   \------/

Avec POSIX threads, on ecrira le programme en C comme a:

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

#include <pthread.h>

int a;              /* variable a */
int k;              /* constante */

void * thread_1(void *arg)      /* thread # 1 */
{
    while (1)
        a++;                    /* operation sur la variable a */
}

void * thread_2(void *arg)      /* thread # 2 */
{
    while (1)
        if (a == k)             /* test sur la variable a */
            pthread_exit(NULL); /* si a est egale a k, on kill le thread */
}

int main(int argc, char *argv)
{
    pthread_t thread1;      /* identificateurs des */
    pthread_t thread2;      /* threads 1 et 2      */

    a = 0;
    k = 10;

    pthread_create(&thread1, NULL, thread_1, "thread 1"); /* on lance thread1 */
    pthread_create(&thread2, NULL, thread_2, "thread 2"); /* on lance thread2 */

    /* on note que pthread_create lance le thread concern en 'background',
       et retourne tout de suite aprs, thread_1 et thread_2 s'excutent donc
       en parallle
    */

    pthread_join(thread2, NULL);    /* on suspend l'execution jusqu'a ce que */
                                    /* thread2 soit kill */

    return 0;   /* le retour de la fonction principale cause la fin de tous */
                /* les threads encore en excution */
}

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

Les mutexes :
-----------
Mais il se pose alors un probleme... en effet, thread1 et thread2,
s'excutant en meme temps, accdent  la meme variable...
Que se passerait-il si thread2 testait la variable a au meme moment ou
thread1 incremente cette meme variable ??
Il serait en fait impossible de prvoir si le test aurait lieu avant ou
aprs l'incrmentation de a, et, de plus, un accs simultan  la meme
variable entraine la plupart du temps une erreur de segmentation (segfault).

Pour viter ce genre de situations, il existe des objets appels les mutex,
ou MUTual EXclusion device, dont le role est de "verrouiller" une partie du
code, par exemple pour protger une variable lors d'un accs.

Il existe principalement deux fonctions pour utiliser les mutexes:

    * pthread_mutex_lock(pthread_mutex_t *mutex) :
        Cette fonction verrouille le mutex point par *mutex.
        Si un autre thread tente de verrouiller ce mutex, il est suspendu
        jusqu' ce que le mutex soit dverrouill par le thread qui l'a
        verrouill en premier.

    * pthread_mutex_unlock(pthread_mutex_t *mutex) :
        Cette fonction dverrouille le mutex point par *mutex, librant
        ansi les ventuels threads qui ont tent de le verrouiller avant.

Plus, pour l'initialisation et la destruction :

    * pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *mutexattr) :
        Cette fonction initialise le mutex point par *mutex.
        Un pointeur NULL sur *mutexattr initialise *mutex avec les attributs
        par defaut.

    * pthread_mutex_destroy(pthread_mutex_t *mutex) :
        Dtruit le mutex point par *mutex.

Le prog d'exemple deviendra alors :
(les ... sont les parties identiques a ci-dessus)

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

#include <pthread.h>

...

pthread_mutex_t a_mutex;        /* declaration du mutex */

void * thread_1(void *arg)      /* thread # 1 */
{
    while (1) {
        pthread_mutex_lock(&a_mutex);   /* on verrouille a_mutex */

        a++;                        /* operation sur la variable a */

        pthread_mutex_unlock(&a_mutex); /* on deverrouille a_mutex, car on */
                                        /* n'accede plus a la variable a */
    }
}

void * thread_2(void *arg)      /* thread # 2 */
{
    while (1) {
        pthread_mutex_lock(&a_mutex);   /* on verrouille a_mutex */

        if (a == k)             /* test sur la variable a */
            pthread_exit(NULL); /* si a est egale a k, on kill le thread */

        pthread_mutex_unlock(&a_mutex); /* on deverrouille a_mutex, car on */
                                        /* n'accede plus a la variable a */
    }
}

int main(int argc, char *argv)
{
    pthread_t thread1;      /* identificateurs des */
    pthread_t thread2;      /* threads 1 et 2      */

    ...

    pthread_mutex_init(&a_mutex, NULL);     /* on initialise a_mutex */

    ...


    pthread_mutex_destroy(&a_mutex);        /* on dtruit a_mutex */

    return 0;   /* le retour de la fonction principale cause la fin de tous */
                /* les threads encore en excution */
}

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

Ainsi, le verrouillage du mutex fait en sorte que les deux threads ne
puissent pas toucher a la variable a en meme temps, car ils essayent de
verrouiller a_mutex avant chaque accs a cette variable.
Donc, si thread 1 tente d'incrmenter a pendant que thread 2 est en train de
la comparer avec k, il se suspend jusqu' la fin du test, car a_mutex tait
dja verrouill par thread 2.


Les semaphores :
--------------
Une autre fonctionnalit de Pthreads est l'usage des semaphores.
A ne pas confondre avec les semaphores du SysV IPC, quoique leur
fonctionnement est analogue.
Les semaphores sont des compteurs, partags entre plusieurs threads.
Les principales oprations effectuables sur une semaphore sont,
incrmenter la semaphore 'atomiquement' (ie un seul thread peut l'incrementer
 la fois), et attendre que la valeur soit suprieure a 0, puis la decrementer
atomiquement.

Cette fonctionnalit peut etre utile pour limiter la consommation en CPU
d'un programme multi-thread.
On va reprendre l'exemple ci-dessus; tel qu'il est cod, ce programme devrait
consommer environ 90% des ressources CPU  lui seul, car il utilise deux
threads en boucle, qui n'excutent que des oprations simples et rapides, donc
les boucles s'excutent trop rapidement pour laisser aux autres processus du
systme le temps de prendre leur part de ressources.
De plus, thread 2 execute le test arbitrairement, meme si a n'a pas chang
d'une itration  l'autre.

L'utilisation des smaphores permet de limiter ce comportement, en suspendant
thread 2 si a n'a pas chang.

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

#include <pthread.h>
#include <semaphore.h>
...

sem_t a_sem;        /* declaration de la semaphore */

void * thread_1(void *arg)      /* thread # 1 */
{
    while (1) {
        pthread_mutex_lock(&a_mutex);   /* on verrouille a_mutex */

        a++;                        /* operation sur la variable a */

        pthread_mutex_unlock(&a_mutex); /* on deverrouille a_mutex, car on */
                                    /* n'accede plus a la variable a */

        sem_post(&a_sem);       /* on incremente a_sem */
    }
}

void * thread_2(void *arg)      /* thread # 2 */
{
    while (1) {

        sem_wait(&a_sem);   /* suspend thread 2 jusqu' ce que a_sem != 0 */
                            /* puis decremente a_sem */

        pthread_mutex_lock(&a_mutex);   /* on verrouille a_mutex */

        if (a == k)             /* test sur la variable a */
            pthread_exit(NULL); /* si a est egale a k, on kill le thread */

        pthread_mutex_unlock(&a_mutex); /* on deverrouille a_mutex, car on */
                                        /* n'accede plus a la variable a */
    }
}

int main(int argc, char *argv)
{
    pthread_t thread1;      /* identificateurs des */
    pthread_t thread2;      /* threads 1 et 2      */

    ...

    sem_init(&a_sem, 0, 0);     /* on initialise a_sem */

    ...


    sem_destroy(&a_sem);        /* on dtruit a_mutex */

    return 0;   /* le retour de la fonction principale cause la fin de tous */
                /* les threads encore en excution */
}

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

On aura donc un algo equivalent  a :

             thread 1             thread 2
                |                    |
        +---->--+                    +-----------+
        |       |                    |           |
        |   /-------\          /-------------\   |
        |   |  a++  |          | a_sem > 0 ? |   |
        |   \-------/          \-------------/   |
        |       |                    |           |
        |  /-----------\         /--------\      |
        |  | a_sem = 1 |         | test a |      |
        ^  \-----------/         \--------/      |
        |       |                    |           |
        +--<----+               /-----------\    |
                                | a_sem = 0 |    |
                                \-----------/    |
                                     |           |
                                     |           |
                                     +---->------+



Les conditions :
--------------
Un autre moyen d'conomiser des ressources CPU est d'utiliser le mcanisme
des conditions.
C'est assez simple. Cela consiste a suspendre un thread, jusqu' ce qu'elle
recoive un signal, la condition, venant d'un autre thread.

Pour utiliser cette structure, plusieurs fonctions:
    * pthread_cond_init(pthread_cond_t *cond, pthread_cond_attr_t *cond_attr);
      Comme son nom l'indique, initialise la variable condition pointe par
      *cond. Si *cond_attr est NULL, la condition prend les attributs par
      defaut.

    * pthread_cond_signal(pthread_cond_t *cond);
      Libre un et un seul thread en attente de la condition pointe par
      *cond. Si aucun thread n'est en attente, aucune opration n'est
      effectue. Si plusieurs threads sont en attente, un seul thread est
      libr, mais il est impossible de prdire lequel. Pour cette dernire
      situation, il vaut mieux utiliser pthread_cond_broadcast();

    * pthread_cond_broadcast(pthread_cond_t *cond);
      Libre tous les threads en attente de la condition pointe par *cond.
      Si aucun thread n'est en attente, aucune opration n'est effectue.

    * pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
      Suspend le thread jusqu' la rception d'un signal sur cond.
      La fonction dverrouille mutex, pour que le thread envoyant le signal
      puisse le verrouiller, puis le dverrouiller aprs envoi du signal.
      Ceci a pour but d'viter les race conditions, dans le cas ou un thread
      se prpare  attendre un signal sur une condition, et o cette
      condition est signale juste avant que le premier thread ait commenc
       attendre.

    * pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
      const struct timespec *abstime);
      Meme chose que pthread_cond_wait, avec un timer qui libre le thread
      si un signal n'a pas t reu aprs la dure spcifie avec abstime.
      Si aucun signal n'a t reu aprs le timeout, la fonction retourne
      la valeur entire ETIMEDOUT.
      La valeur abstime est en fait la date  laquelle le timeout doit
      survenir, au format time(2). Par exemple abstime = 0 correspond 
      la date 00:00:00 GMT, 1 Janvier 1970, date d'origine sur les systemes
      Unix.
      Pour definir un timeout relatif, on rcupre la date actuelle avec
      gettimeofday(2), et on rajoute la valeur du timeout  cette date,
      avant de la passer  pthread_cond_timedwait();

    * pthread_cond_destroy(pthread_cond_t *cond);
      Dtruit la condition cond, en vrifiant qu'aucun thread n'est encore
      suspendu.

Voila, c'est  peu prs tout pour les fonctions de l'API POSIX threads.
Il reste encore les fonctions d'opration sur les attributs et les paramtres
de scheduling (priorits), mais elles sont moins souvent utilises et ne
ncessitent pas d'aprhension d'un concept particulier.


Il faut aussi noter qu'il est TRES FACILE d'obtenir des RACE CONDITIONS
dans un programme multi-thread, pouvant entrainer des TROUS DE SECURITE,
ou meme un CRASH DU SYSTEME, par puisement des ressources CPU et mmoire.
Il faut donc parfaitement maitriser les mutexes, conditions et smaphores
pour raliser un programme stable utilisant des threads.

Une autre note sur l'utilisation des threads dans les gestionnaires de
signaux (sighandler) : la stabilit de l'API avec des signaux asynchrones
varie selon les implmentations. Se rferer  la doc de l'implmentation
en question.

Rfrences:
    * Toutes les man pages (3th), qui traitent des fonctions de l'API
      POSIX 1003.1c
    * Sur les systmes Linux bas sur GNU libc 2.x (RedHat 5.x/6.x)
      /usr/doc/glibc-2.x.x/FAQ-threads.html, /usr/doc/glibc-2.x.x/README.threads
      /usr/doc/glibc-2.x.x/examples.threads/*.c ...
    * Le standard POSIX 1003.1c (j'ai pas l'url, mais a devrait pas tre trop
      dur a trouver), quoique les descriptions des standards POSIX ne sont pas
      trop adapts comme tutorials pour une API, ils sont plutot destins aux
      concepteurs des implmentations.



La Communication Inter-Processus (IPC):
**************************************

Cette mthode de programmation multi-tche utilise la capacit d'Unix 
"cloner" un processus, pour en faire un autre processus indpendant, et de
faire communiquer plusieurs processus par des moyens tels que les signaux,
tubes (pipes), FIFOS, sockets, mmoire partage, semaphores, les queues
de messages...
Sa particularit par rapport aux threads, c'est qu'elle utilise plusieurs
processus indpendants, qui ne partagent pas leur espace d'addressage.

Elle consiste en fait  utiliser les librairies standard d'Unix pour
effectuer des tches en parallle, sans avoir recours  une API spcialise.

On utilise ces techniques lorsqu'on programme sur un systme n'implmentant
pas les threads, ou pour crer des applications portables sur quasi tous
les Unix, ou lorsque le multi-threading n'est pas adapt  l'application
concerne (notamment problmes avec les signaux).


L'appel systme fork(2) :
------------------------
    Cet appel systme est l'un des plus importants, si c'est pas LE plus
importants des appels systmes d'UNIX. En effet, sans lui, le noyau n'aurait
pas le moyen de lancer d'autres processus, et serait donc monotache.
En fait, aprs le du dmarrage du noyau, celui-ci fork le programme init(8), qui
 son tour va fork'er les autres programmes, les daemons, les shells...

fork(2) est donc une des pierres angulaires de la prog multi-tache sous Unix.

Son principe est simple: quand on l'appele, le noyau va faire une copie
conforme du processus qui l'appele, et dont la seule diffrence sera le
PID (process ID) et le PPID (parent process ID). Tous les descripteurs de
fichiers ouverts, les gestionnaires de signaux (sig handlers), sockets...
sont hrits. Seuls les signaux en attente ne sont pas hrits.

Les deux processus ainsi spars continuent leur excution  l'instruction
suivant l'appel  fork(2).
Typiquement, un appel  fork est utilis ainsi:

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

#include <unistd.h>

int main(int argc, char *argv[])
{
    pid_t child;

    child = fork();     /* la variable child prend une valeur diffrente */
                        /* dans le processus parent et le processus fork */


    if (child == -1) {
        fprintf(stderr, "Erreur dans l'excution de fork(2)");
        exit(child);
    }


    if (child == 0) {   /* si child est gal  0, on se trouve dans le */
                        /* processus fork */

        /*  code  excuter dans le nouveau processus, on appele gnralement
            une fonction sans retour, pour faciliter la lecture du code */

    } else {            /* child est gal au PID du nouveau processus */

        /*  code  excuter dans le processus parent. */

    }

    exit(0);
}

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

Ainsi, mme si les deux processus sont totalement spars,  la diffrence
d'un programme multi-thread, ils peuvent toujours communiquer, car ils
possdent tous les deux le PID de l'autre: le processus parent connait le
PID du nouveau processus par la valeur retourne par fork(2), et le nouveau
processus peut connaitre le PID de son parent avec la fonction getppid(2).

Il est alors possible de les faire communiquer avec des pipes ou des signaux,
ou des mcanismes plus complexes que sont les SysV IPC.


Les pipes (tubes) et FIFOS (First In First Out) :
------------------------------------------------
Un truc assez intressant avec fork(2) est la duplication des descripteurs de
fichier.
Grace  cette caractristique, il est relativement ais de crer un pipe
entre les deux processus, sans avoir recours  un FIFO
(beaucoup moins de possibilits de race conditions).

On peut procder de deux faons diffrentes:
    * on cr un pipe bidirectionnel, grce  l'appel systme pipe(2),
avant le fork(2); aps le fork, les deux processus communiquent par ce pipe.

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----
#include <unistd.h>

int main(int argc, char *argv[])
{
    pid_t child;
    int pipe_descri[2];     /* paire de descripteurs de fichier du pipe */
    char buf[256];

    pipe(pipe_descri);      /* on cr le pipe bidirectionnel */

    child = fork();         /* on cr le nouveau processus */

    if (child == 0)
        read(pipe_descri[0], &buf, 256);
    else
        write(pipe_descri[1], &buf, 256);

    exit(0);
}
------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----
Dans ce cas, on peut changer des donnes dans le tableau buf, qui sera
similaire dans les deux processus. On simule donc une mmoire partage
unidirectionnelle, de la taille de buf, c'est  dire 256 octets, qu'il faudra
rafraichir rgulierement si on veut l'utiliser comme telle.
A vous d'imaginer comment on peut faire des combinaisons de pipes pour crer
une quasi illusion de mmoire partage bidirectionnelle entre deux ou plusieurs
processus.

    * il existe galement un moyen, plus complexe, de crer un quivalent
de la ligne de commande shell suivante:
    $ parent | child

on cr tout d'abord un pipe bidirectionnel, comme au dessus, et on remplace
l'entre standard du nouveau processus par le descripteur de lecture du pipe,
on fait de mme dans le processus parent avec la sortie standard et le
descripteur d'criture du pipe. On utilise pour cela l'appel systme dup2(2),
qui duplique un descripteur de fichier sur un nouveau, fermant celui-ci si
il est dja ouvert.
Pour la deuxime mthode on codera:
(j'ai strip l'error checking pour ne pas alourdir)

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----
#include <unistd.h>

int main(int argc, char *argv[])
{
    pid_t child;
    int pipe_descri[2];     /* paire de descripteurs de fichier du pipe */

    pipe(pipe_descri);      /* on cr le pipe bidirectionnel */

    child = fork();         /* on cr le nouveau processus */

    if (child == 0) {
        dup2(pipe_descri[0], 0);    /* dans le nouveau processus, on copie */
                                    /* pipe_descri[0], descripteur de lecture */
                                    /* du pipe, sur stdin */
                                    /* (descripteur de fichier # 0) */
        lire_dans_stdin();  /* ou execv("child", NULL); */
    } else {
        dup2(pipe_descri[1], 1);    /* on fait la meme chose dans le processus */
                                    /* parent, avec le descripteur d'criture */
                                    /* et stdout */
        ecrire_dans_stdout();   /* ou execv("parent", NULL);
    }

    exit(0);
}
------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----


Mais ATTENTION... la fermeture accidentelle (SIGPIPE) du pipe causera la mort
des deux processus ! Il convient donc, soit d'ignorer le signal SIGPIPE,
en insrant au dbut du code:

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----
#include <signal.h>

    signal(SIGPIPE, SIG_IGN);
------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

soit de faire en sorte que les deux processus ne perdent pas leurs bouts
respectifs du pipe.
Il faut galement remarquer que, une fois ferm, il n'ya aucun moyen de
rtablir une communication entre les deux processus par le mme pipe.
Si on veut rtablir une communication par pipe, on a alors recours  un
FIFO, ou named pipe.
C'est tout simplement un fichier qui se comporte comme un pipe.
Pour l'utiliser, il suffit d'ouvrir ce fichier, de lire ou d'crire des
donnes dedans, avec un autre processus qui envoie ou reoit les donnes
transitant par le FIFO.
On cr un fichier FIFO avec la fonction mkfifo(3):
------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *filename, mode_t mode);
------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----
ATTENTION: l'utilisation des FIFOS peut entrainer des race conditions
assez dangereuses si elle est mal gre, particulirement au niveau des
permissions sur le fichier FIFO.

Je ne parlerai pas de l'utilisation des sockets, qui sont en fait des pipes
utilisables sur un rzo, car leur utilisation ne relve pas de la
programmation multi-taches, mais plutot de la prog rzo, quoique les
Unix Domain Sockets ne sont utilisables qu'en local (principaux quivalents
d'IPC sous BSD).


Les signaux :
------------
    Les signaux sont galement trs utiles pour la communication
inter-processus.
Ils sont utiliss par le systme, notament pour communiquer  un processus
qu'il est l'objet d'une erreur (ie: SIGSEGV => segmentation fault;
SIGPIPE => pipe rompu; SIGFPE => erreur de floating point; ...), ou qu'il
doit tre interrompu suite  une requette externe (utilisateur, reboot,
changement de runlevel... ie. SIGKILL; SIGSTOP => ctrl-z; SIGINT => ctrl-c;
SIGQUIT ...). Toute la liste est dans la man page signal(7).

Le codeur a aussi la possibilit de produire tous ces signaux
"artificiellement", soit avec la fonction raise(3), pour envoyer un signal
au programme qui l'appele, ou avec l'appel systme kill(2), pour envoyer
un signal  autre un processus dont on connait le PID.

Dans toute la panoplie des signaux, on trouve deux signaux 'spciaux':
SIGUSR1 et SIGUSR2. Contrairement  la plupart des signaux --qui produisent
gnralement une interruption du processus-- ces signaux n'ont aucune
fonction prcise, si ce n'est qu'ils sont utilisables par le codeur sans
entrainer d'effets secondaires sur le droulement du programme.
Ils sont parfaits comme moyen d'informer un processus du droulement d'un
autre processus... exemple :

                /-----------\                   /-----------\
                | process 1 |                   | process 2 |
                \-----------/                   \-----------/
                      |                               |
            --------------------                 ------------------
            | si evenement X,  |                 | on place un    |
            | envoi de SIGUSR1 |                 | sighandler sur |
            | sur process 2    |                 | SIGUSR1        |
            --------------------                 ------------------
                      :                               :
                      :                               :
                      :                               :
                      : droulement normal,           : droulement normal
                      : jusqu' l'apparition          : jusqu' rception
                      : de l'evenement X              : de SIGUSR1
                      :                       ----------------
                      |                       | SIGUSR1 reu | => lancement
                      |                       ----------------    du sighandler
                      |
               --------------
               | evenement X | => kill(pid_de_process_2, SIGUSR1);
               ---------------

                  et process 2 reprend son excution normale, car il a reu
le signal SIGUSR1, signal qu'il attendait, en mode S (suspend).

Si on fait deux progs en C, a donne ceci:

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

/* process 1 */
#include <unistd.h>
#include <signal.h>

void main(void)
{
    /* ici, l'evenement X sera l'accession de la variable i  la valeur 10 */
    /* on assume aussi qu'on connait le PID de process 2, par un moyen */
    /* mystrieux ou magique, ou plus simplement parce qu'on a gnr */
    /* process 2 avec un fork(2) :)) */

    int i;
    pid_t proc_2;   /* le pid de process 2 */

    while (i != 10)
        i++;

    kill(proc_2, SIGUSR1);

    ....
}

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

/* process 2 */
#include <unistd.h>
#include <signal.h>

void sig_handler(int sig)
{
    /* code du sighandler */
    /* nota : un sighandler est une routine qui est lance lors */
    /* de la rception d'un signal donn */
    /* son but le plus frquent est de 'nettoyer' et de terminer le */
    /* programme quand celui reoit un signal d'interruption */
    /* par exemple, quand l'utilisateur appuye sur ctrl-c */
}

void main(void)
{
    (void)signal(SIGUSR1, sig_handler);     /* on associe la routine */
                                            /* sig_handler() au signal */
                                            /* SIGUSR1 */

    /* droulement normal du prog */
}

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

Un point qu'il faut pas ngliger quand on utilise des signaux, c'est que
la rception d'un signal pendant l'excution d'un appel systme
entraine la fin du programme, sans avertissement... le seul message
reu est la valeur dans errno, dont l'interprtation renvoie:
"Interrupted system call".
Il faut donc, pendant l'excution des appels systmes dans un programme,
bloquer l'arrive des signaux susceptibles d'arriver pendant le fonctionnement
normal, les mettre en attente, et les traiter quand l'appel systme est fini.
Pour a, on a recours  l'appel systme sigprocmask(2).

Donc, si on reprend le code de process 2:

------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

/* process 2 */
#include <unistd.h>
#include <signal.h>

void sig_handler(int sig)
{
    /* ... */
}

void main(void)
{
    sigset_t block;     /* le type sigset_t dsigne un groupe (set) */
                        /* de signaux */

    (void)signal(SIGUSR1, sig_handler);

    sigemptyset(&block);    /* on vide le groupe block, au cas ou le */
                            /* segment de mmoire ou il se trouve ne */
                            /* serait pas mis  zro */

    sigaddset(&block, SIGUSR1); /* on ajoute SIGUSR1  ce groupe */


    sigprocmask(SIG_BLOCK, &block, NULL);   /* on bloque l'arrive de SIGUSR1 */
                                            /* avant l'appel systme */
    system_call_quelconque();

    sigprocmask(SIG_UNBLOCK, &block, NULL); /* on dbloque SIGUSR1 aprs */
                                            /* l'excution de l'appel systme */
                                            /* si SIGUSR1 a t reu pendant */
                                            /* l'appel systme, il est mis en */
                                            /* attente et trait quand on dbloque */
                                            /* l'accs au signal */
}

C'est  peu prs tout ce que j'ai trouv d'utile sur les signaux, concernant
le multitache. Il existe aussi l'appel systme sigaction(2), mais je n'ai
pas encore eu le temps de me pencher dessus.

A noter aussi que l'utilisation de SIGUSR1 et SIGUSR2 n'est pas possible
si on utilise en mme temps l'API de multi-threading LinuxThreads.
Ceci est du  l'implmentation de LinuxThreads qui se sert de ces deux
signaux pour bloquer et dbloquer les threads en attente.
Ce comportement n'est pas du tout compatible POSIX, et il faut esprer que
a change dans les prochaines implmentations...


Les System V Inter-Process Communication mechanisms :
----------------------------------------------------

Alors l, on arrive au gros morceau...
Les SysV IPC sont rputs pour tre compliqus... et c'est vrai !!!

Un peu d'histoire:
Les SysV IPC ont t introduits pour la premire fois par AT&T dans
Unix System V (d'ou le nom SysV), et leur forme a t finalise dans
System V release 4 (SysV r4), je sais plus exactement quand
( la rigueur on s'en fout =)...
Un truc notable est que, avant la naissance de la norme POSIX, on ne trouvait
pas les SysV IPC sur les systmes BSD (mais a vous vous en foutez aussi =),
car on y utilise a la place les sockets (Unix Domain).

Un peu de srieux : 8)))
Il existe trois formes de SysV IPC:
    * Les "files d'attente de messages"
      (le terme anglais 'message queues' est moins chiant  crire :)
    * Les smaphores
    * Les segments de mmoire partage (Shared Memory Segment)

Quelques caractristiques communes aux trois IPC:
    * Leur subsistance ne dpend pas de l'excution d'un programme;
      c'est  dire qu'un objet IPC existe toujours s'il n'a pas t
      effac par le programme qui l'a cr ou par un programme externe,
      et ce mme aprs que le programme crateur soit termin.
      Les IPC sont en fait grs directement par le noyau.

    * L'accs  un objet IPC est soumis aux mmes types de permissions
      qu'un fichier Unix (sauf excution et bits spciaux genre suid, sgid,
      sticky bit...), ainsi qu'aux mmes rgles de proprit (user, group),
       la diffrence prs que le crateur et le propritaire de l'objet
      peuvent tre deux uid/gid diffrents.

    * Un objet IPC est identifi par trois composantes: le type d'objet
      (message queue, semaphore ou mmoire partage), l'identificateur
      (identifier) qui permet au noyau d'identifier l'objet en question,
      et la cl (key) qui a pour but de s'assurer que deux processus
      accdent bien au mme objet pour communiquer entre eux, en effet, on
      ne peut pas prvoir  l'avance quel identificateur prendra un objet
      quand on le cr, mais on doit lui assigner une cl, qui doit tre
      unique (aucun autre objet ne doit la possder avant la cration).
      Si la cl est dja utilise, la routine de cration renvoie une erreur
      et ne cr pas l'objet.

    * Il existe une diffrence entre crer un objet et pouvoir utiliser un
      objet. En effet, un processus qui a cr un objet ne peut pas
      forcment y accder; dans le cas des segments de mmoire partage,
      il doit d'abord attacher ce segment  un pointeur dont il connait
      l'addresse. Par contre, dans le cas des semaphores et des message
      queues, la meme fonction est utilise pour crer un objet et pour
      connaitre son identificateur, que l'on rcupre en fonction de la cl
      demande. Si cette cl existe, l'identificateur de l'objet existant
      est renvoy, si elle n'existe pas, l'objet est d'abord cr et son
      identificateur est renvoy.

    * On peut lister tous les objets IPC prsents sur le systme avec la
      commande ipcs(8), on obtient ainsi toutes les cls, identificateurs,
      propritaires, permissions et paramtres de tous les objets prsents.

    * On peut supprimer un objet IPC grce  la commande ipcrm(8), en
      fonction de son type et de son identificateur.


* La Mmoire partage:
    La mmoire partage permet, comme son nom l'indique, de partager un
segment de mmoire entre 2 ou plusieurs processus.
Du cot du programme en C, on accde au segment par l'intermdiaire d'un
pointeur, assign par la fonction shmat(2), qui attache le segment de
mmoire partage au segment de donnes du process.
Pour utiliser shmat(2), on doit d'abord rcuprer l'id du segment ou en
crer un avec shmget(2).
Une fois le segment attach, le process peut y accder comme de la mmoire
normale, via le pointeur.
Un chtit example:
------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

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

void main(void)
{
    int shmid;      /* l'identificateur du segment */
    char *shm_seg;

    shmid = shmget(0x666, 256, IPC_CREAT | 0600);
/*      la cl -----^      ^    ^            ^--- permission 'rw------'
  la taille du segment ----|    |-- on cre le segment si la cl est inutilise
*/

    shm_seg = shmat(shmid, 0, 0);
/*                         ^
                           |
 on donne 0 pour addresse pour que celle-ci soit dtermine automatiquement
 par le noyau
*/

    shmdt(shm_seg);     /* on dtache le segment du process avec shmdt(2) */
                        /* le segment est dtach mais PAS DETRUIT. */
                        /* ici, c'est inutile car le segment est */
                        /* automatiquement dtach quand le process se */
                        /* termine */
}
------>snip>------->snip>----->snip>------->snip>------->snip>----->snip>-----

On dispose alors de 256 octets utilisables comme de la mmoire normale,
 l'exception que celle-ci peut tre partage avec d'autres programmes.
Utile pour envoyer des struct ou toute autre donne qui serait chiante 
passer dans un pipe.

On peut changer les paramtres, obtenir des infos sur, et dtruire un
segment de mmoire partage avec l'appel systme shmctl(2). (voir la man page)

Il faut TOUJOURS dtruire un segment de mmoire partage quand on ne s'en
sert plus, car celui-ci reste en mmoire indfiniment, et garde les donnes
qui y taient stockes lors de sa dernire utilisation.
Un segment tant world ou group readable peut donc tre exploit pour
rcuperer des donnes utilises par un programme utilis antrieurement
si il n'est pas effac  temps.
Il faut aussi faire attention  la gestion des cls pour viter les races
conditions ou les exploits qui consistent  crer un segment avant
le programme qui doit le crer avec une cl donne, permettant de gagner
des accs sur la mmoire d'un programme excut par un user privilgi.
Il y a un moyen d'viter a en OR-ant le flag IPC_EXCL aux flags dans
shmget(2): ainsi, on s'assure que shmget retourne une erreur si un segment
existe dja avec la cl donne.
Un autre moyen est de donner IPC_PRIVATE comme cl pour s'assurer de la
cration d'un segment avec une cl originale, auquel cas il faudra
communiquer cette cl aux autres processus devant accder au segment.

L'effet d'une criture simultane d'un mme octet de mmoire partage
par deux ou plusieurs process est imprvisible, mais donne la plupart du
temps une erreur de segmentation; il faut donc aussi en protger l'accs, le
plus souvent avec des smaphores.

Un autre problme est qu'il est impossible d'empcher les autres processus
appartenant  un mme utilisateur d'utiliser un segment cr par celui-ci, car
ils ont tous la permission d'y accder, quelles que soient ses permissions.

En rsum, la mmoire partage est de loin le moyen le plus rapide de faire
communiquer plusieurs processus, mais elle peut tre trs dangereuse pour
l'intgrit du systme si elle est mal utilise (risques d'exploits, voir
de crashs ou de kernel panic, puisement de la mmoire virtuelle...)

* Les smaphores et les message queues:
    Ces deux types d'objets me sont beaucopu moins familiers, et j'ai pas
encore eu l'occasion de les utiliser... de plus, comme je manque de temps
pour finir cet article, j'en parlerai dans un prochain article.

En attendant, allez regarder les quelques rfrences sur le sujet:
    * Bien sur les man pages, commencez par ipc(5), fork(2), pipe(2)...
    * Les pages info de la glibc 2.1 contiennent une rfrence sur les
      SysV IPC
    * The Linux Programmer's Guide, ou lpg, faisant partie du Linux
      Documentation Project, traite (en anglais) des pipes et des SysV IPC,
      d'une manire vachement plus claire que dans les man pages.



The end:
*******
Voila, c'est a peu prs tout,  part en ce qui concerne les deux derniers
mcanismes d'IPC, que j'ai pas trait par manque de temps.

Pour le cot scu, il est vident que le traitement parallle de donnes
par plusieurs processus diffrents peut entrainer beaucoup plus de race
conditions et de trous de scurit qu'un programme traditionnel, il faut
donc faire tres attention au verrouillage des donnes.

Greetz:
    The CHX Hacking Crew: David Lightman (yopyoooop =))), neo, 
     CyberFranck (schlum box rulz :), Dark_Willy (free tibet... & dogs =), oRl,
     moi (Professor Falken), BigNose...,
    The OrganiKs Crew: Lionel, Tbh, RoBloChOn, cd13h, coder, [fred]...,
    Cryptel,
    rockme =), jacko (phreaking rulz), shado, Xgh0st (grosse lamouze :),
    #cryptel, #linuxfr, #organiks, et tous ceux que j'oublie...

Special greetz a:
    mon frangin qui me manque vraiment trop :(((((((( death sucks
    s***, qui m'a vraiment beaucoup aid cette anne, d'abord a tenir le choc,
     et ensuite  me motiver pour le bacho... T la meilleure
    la chienne de mon voisin, qui bouffe tellement qu'on a du la mettre au
     rgime :)
    Red Hot Chili Peppers, Jimi Hendrix, Led Zep, Pink Floyd,
     Rage Against The Machine, Ben Harper, Prodigy... pour leur excellente zique
    yopyop & tobozo, pour les dlires qu'on s'est tap dessus avec
     David Lightman [ yopyoooop ! uh-uh uhuh ! =))))) ]


----[  EOF
