select_tut

SELECT_TUT(2)             Manuel du programmeur Linux            SELECT_TUT(2)



NOM
       select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - Multiplexage
       d'entrées-sorties synchrones

SYNOPSIS
       /* D'après POSIX.1-2001 */
       #include <sys/select.h>

       /* D'après les standards précédents */
       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

       int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *utimeout);

       void FD_CLR(int fd, fd_set *set);
       int  FD_ISSET(int fd, fd_set *set);
       void FD_SET(int fd, fd_set *set);
       void FD_ZERO(fd_set *set);

       #include <sys/select.h>

       int pselect(int nfds, fd_set *readfds, fd_set *writefds,
                   fd_set *exceptfds, const struct timespec *ntimeout,
                   const sigset_t *sigmask);

   Exigences de macros de test de fonctionnalités pour la glibc (consultez
   feature_test_macros(7)) :

       pselect() : _POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600

DESCRIPTION
       select() (ou pselect()) est utilisé pour superviser efficacement
       plusieurs descripteurs de fichiers pour vérifier si l'un d'entre eux
       est ou devient « prêt » ; c'est-à -dire savoir si des
       entrées-sorties deviennent possibles ou si une « condition
       exceptionnelle » est survenue sur l'un des descripteurs.

       Ses paramètres principaux sont trois « ensembles » de descripteurs
       de fichiers : readfds, writefds et exceptfds. Chaque ensemble est de
       type fd_set, et son contenu peut être manipulé avec les macros
       FD_CLR(), FD_ISSET(), FD_SET(), et FD_ZERO(). Un ensemble nouvellement
       déclaré doit d'abord être effacé en utilisant FD_ZERO(). select()
       modifie le contenu de ces ensembles selon les règles ci-dessous.
       Après un appel à select(), vous pouvez vérifier si un descripteur de
       fichier est toujours présent dans l'ensemble à l'aide de la macro
       FD_ISSET(). FD_ISSET() renvoie une valeur non nulle si un descripteur
       de fichier indiqué est présent dans un ensemble et zéro s'il ne
       l'est pas. FD_CLR() retire un descripteur de fichier d'un ensemble.

   Arguments
       readfds
              Cet ensemble est examiné afin de déterminer si des données
              sont disponibles en lecture à partir d'un de ses descripteurs
              de fichier. Suite à un appel à select(), readfds ne contient
              plus aucun de ses descripteurs de fichiers à l'exception de
              ceux qui sont immédiatement disponibles pour une lecture.

       writefds
              Cet ensemble est examiné afin de déterminer s'il y a de
              l'espace afin d'écrire des données dans un de ses descripteurs
              de fichier. Suite à un appel à select(), writefds ne contient
              plus aucun de ses descripteurs de fichiers à l'exception de
              ceux qui sont immédiatement disponibles pour une écriture.

       exceptfds
              Cet ensemble est examiné pour des « conditions
              exceptionnelles ». En pratique, seule une condition
              exceptionnelle est courante : la disponibilité de données
              hors-bande (OOB : Out Of Band) en lecture sur une socket TCP.
              Consultez recv(2), send(2) et tcp(7) pour plus de détails sur
              les données hors bande. Un autre cas moins courant dans lequel
              select(2) indique une condition exceptionnelle survient avec des
              pseudoterminaux en mode paquet ; consultez tty_ioctl(4).) Suite
              à un appel à select(), exceptfds ne contient plus aucun de ses
              descripteurs de fichier à l'exception de ceux pour lesquels une
              condition exceptionnelle est survenue.

       nfds   Il s'agit d'un entier valant un de plus que n'importe lequel des
              descripteurs de fichier de tous les ensembles. En d'autres
              termes, lorsque vous ajoutez des descripteurs de fichier Ã
              chacun des ensembles, vous devez déterminer la valeur entière
              maximale de tous ces derniers, puis ajouter un à cette valeur,
              et la passer comme paramètre nfds.

       utimeout
              Il s'agit du temps le plus long que select() pourrait attendre
              avant de rendre la main, même si rien d'intéressant n'est
              arrivé. Si cette valeur est positionnée à NULL, alors,
              select() bloque indéfiniment dans l'attente qu'un descripteur
              de fichier devienne prêt. utimeout peut être positionné Ã
              zéro seconde, ce qui provoque le retour immédiat de select(),
              en indiquant quels descripteurs de fichiers étaient prêts au
              moment de l'appel. La structure struct timeval est définie
              comme :

                  struct timeval {
                      time_t tv_sec;    /* secondes */
                      long tv_usec;     /* microsecondes */
                  };

       ntimeout
              Ce paramètre de pselect() a la même signification que
              utimeout, mais struct timespec a une précision à la
              nanoseconde comme explicité ci-dessous :

                  struct timespec {
                      long tv_sec;    /* secondes */
                      long tv_nsec;   /* nanosecondes */
                  };

       sigmask
              Cet argument renferme un ensemble de signaux que le noyau doit
              débloquer (c'est-à -dire supprimer du masque de signaux du
              thread appelant) pendant que l'appelant est bloqué par
              pselect() (consultez sigaddset(3) et sigprocmask(2)). Il peut
              valoir NULL et, dans ce cas, il ne modifie pas l'ensemble des
              signaux non bloqués à l'entrée et la sortie de la fonction.
              Dans ce cas, pselect() se comporte alors de façon identique Ã
              select().

   Combinaison d'événements de signaux et de données
       pselect() est utile si vous attendez un signal ou qu'un descripteur de
       fichier deviennent prêt pour des entrées-sorties. Les programmes qui
       reçoivent des signaux utilisent généralement le gestionnaire de
       signal uniquement pour lever un drapeau global. Le drapeau global
       indique que l'événement doit être traité dans la boucle principale
       du programme. Un signal provoque l'arrêt de l'appel select() (ou
       pselect()) avec errno positionnée à EINTR. Ce comportement est
       essentiel afin que les signaux puissent être traités dans la boucle
       principale du programme, sinon select() bloquerait indéfiniment. Ceci
       étant, la boucle principale implante quelque part une condition
       vérifiant le drapeau global, et l'on doit donc se demander : que se
       passe-t-il si un signal est levé après la condition mais avant
       l'appel à select() ? La réponse est que select() bloquerait
       indéfiniment, même si un signal est en fait en attente. Cette "race
       condition" est résolue par l'appel pselect(). Cet appel peut être
       utilisé afin de définir le masque des signaux qui sont censés être
       reçus que durant l'appel à pselect(). Par exemple, disons que
       l'événement en question est la fin d'un processus fils. Avant le
       démarrage de la boucle principale, nous bloquerions SIGCHLD en
       utilisant sigprocmask(2). Notre appel pselect() débloquerait SIGCHLD
       en utilisant le masque de signaux vide. Le programme ressemblerait Ã
       ceci :

       static volatile sig_atomic_t got_SIGCHLD = 0;

       static void
       child_sig_handler(int sig)
       {
           got_SIGCHLD = 1;
       }

       int
       main(int argc, char *argv[])
       {
           sigset_t sigmask, empty_mask;
           struct sigaction sa;
           fd_set readfds, writefds, exceptfds;
           int r;

           sigemptyset(&sigmask);
           sigaddset(&sigmask, SIGCHLD);
           if (sigprocmask(SIG_BLOCK, &sigmask, NULL) == -1) {
               perror("sigprocmask");
               exit(EXIT_FAILURE);
           }

           sa.sa_flags = 0;
           sa.sa_handler = child_sig_handler;
           sigemptyset(&sa.sa_mask);
           if (sigaction(SIGCHLD, &sa, NULL) == -1) {
               perror("sigaction");
               exit(EXIT_FAILURE);
           }

           sigemptyset(&empty_mask);

           for (;;) {          /* main loop */
               /* Initialiser readfds, writefds et exceptfds
                  avant l'appel à pselect(). (Code omis.) */

               r = pselect(nfds, &readfds, &writefds, &exceptfds,
                           NULL, &empty_mask);
               if (r == -1 && errno != EINTR) {
                   /* Gérer les erreurs */
               }

               if (got_SIGCHLD) {
                   got_SIGCHLD = 0;

                   /* Gérer les événements signalés ici; e.g., wait() pour
                      que tous les fils se terminent. (Code omis.) */
               }

               /* corps principal du programme */
           }
       }

   Pratique
       Quelle est donc la finalité de select() ? Ne peut on pas simplement
       lire et écrire dans les descripteurs chaque fois qu'on le souhaite ?
       L'objet de select() est de surveiller de multiples descripteurs
       simultanément et d'endormir proprement le processus s'il n'y a pas
       d'activité. Les programmeurs UNIX se retrouvent souvent dans une
       situation dans laquelle ils doivent gérer des entrées-sorties
       provenant de plus d'un descripteur de fichier et dans laquelle le flux
       de données est intermittent. Si vous deviez créer une séquence
       d'appels read(2) et write(2), vous vous retrouveriez potentiellement
       bloqué sur un de vos appels attendant pour lire ou écrire des
       données à partir/vers un descripteur de fichier, alors qu'un autre
       descripteur de fichier est inutilisé bien qu'il soit prêt pour des
       entrées-sorties. select() gère efficacement cette situation.

   Règles de select
       De nombreuses personnes qui essaient d'utiliser select() obtiennent un
       comportement difficile à comprendre et produisent des résultats non
       portables ou des effets de bord. Par exemple, le programme ci-dessus
       est écrit avec précaution afin de ne bloquer nulle part, même s'il
       ne positionne pas ses descripteurs de fichier en mode non bloquant.Il
       est facile d'introduire des erreurs subtiles qui annuleraient
       l'avantage de l'utilisation de select(), aussi, voici une liste de
       points essentiels à contrôler lors de l'utilisation de select().

       1.  Vous devriez toujours essayer d'utiliser select() sans timeout.
           Votre programme ne devrait rien avoir à faire s'il n'y a pas de
           données disponibles. Le code dépendant de timeouts n'est en
           général pas portable et difficile à déboguer.

       2.  La valeur nfds doit être calculée correctement pour des raisons
           d'efficacité comme expliqué plus haut.

       3.  Aucun descripteur de fichier ne doit être ajouté à un quelconque
           ensemble si vous ne projetez pas de vérifier son état après un
           appel à select(), et de réagir de façon adéquate. Voir la
           règle suivante.

       4.  Après le retour de select(), tous les descripteurs de fichier dans
           tous les ensembles devraient être testés pour savoir s'ils sont
           prêts.

       5.  Les fonctions read(2), recv(2), write(2) et send(2) ne lisent ou
           n'écrivent pas forcément la quantité totale de données
           spécifiée. Si elles lisent/écrivent la quantité totale, c'est
           parce que vous avez une faible charge de trafic et un flux rapide.
           Ce n'est pas toujours le cas. Vous devriez gérer le cas où vos
           fonctions traitent seulement l'envoi ou la réception d'un unique
           octet.

       6.  Ne lisez/n'écrivez jamais seulement quelques octets à  la fois Ã
           moins que vous ne soyez absolument sûr de n'avoir qu'une faible
           quantité de données à traiter. Il est parfaitement inefficace de
           ne pas lire/écrire autant de données que vous pouvez en stocker
           Ã  chaque fois. Les tampons de l'exemple ci-dessous font 1024
           octets bien qu'ils aient facilement pu être rendus plus grands.

       7.  Les fonctions read(2), recv(2), write(2) et send(2) tout comme
           l'appel select() peuvent renvoyer -1 avec errno positionné Ã
           EINTR ou EAGAIN (EWOULDBLOCK) ce qui ne relève pas d'une erreur.
           Ces résultats doivent être correctement gérés (cela n'est pas
           fait correctement ci-dessus). Si votre programme n'est pas censé
           recevoir de signal, alors, il est hautement improbable que vous
           obteniez EINTR. Si votre programme n'a pas configuré les
           entrées-sorties en mode non bloquant, vous n'obtiendrez pas de
           EAGAIN.

       8.  N'appelez jamais read(2), recv(2), write(2) ou send(2) avec un
           tampon de taille nulle.

       9.  Si l'une des fonctions read(2), recv(2), write(2) et send(2)
           échoue avec une erreur autre que celles indiquées en 7., ou si
           l'une des fonctions d'entrée renvoie 0, indiquant une fin de
           fichier, vous ne devriez pas utiliser ce descripteur à nouveau
           pour un appel à select(). Dans l'exemple ci-dessous, le
           descripteur est immédiatement fermé et ensuite est positionné Ã
           -1 afin qu'il ne soit pas inclus dans un ensemble.

       10. La valeur de timeout doit être initialisée à chaque nouvel appel
           à select(), puisque des systèmes d'exploitation modifient la
           structure. Cependant, pselect() ne modifie pas sa structure de
           timeout.

       11. Comme select() modifie ses ensembles de descripteurs de fichiers,
           si l'appel est effectué dans une boucle alors les ensembles
           doivent être réinitialisés avant chaque appel.

   Ãmulation de usleep
       Sur les systèmes qui ne possèdent pas la fonction usleep(3), vous
       pouvez appeler select() avec un timeout à valeur finie et sans
       descripteur de fichier de la façon suivante :

           struct timeval tv;
           tv.tv_sec = 0;
           tv.tv_usec = 200000;  /* 0.2 secondes */
           select(0, NULL, NULL, NULL, &tv);

       Le fonctionnement n'est cependant garanti que sur les systèmes UNIX.

VALEUR RENVOYÃE
       En cas de succès, select() renvoie le nombre total de descripteurs de
       fichiers encore présents dans les ensembles de descripteurs de
       fichier.

       En cas de timeout échu, alors les descripteurs de fichier devraient
       tous être vides (mais peuvent ne pas l'être sur certains systèmes).
       Par contre, la valeur renvoyée est zéro.

       Une valeur de retour égale à -1 indique une erreur, errno est alors
       positionné de façon adéquate. En cas d'erreur, le contenu des
       ensembles renvoyés et le contenu de la structure de timeout sont
       indéfinis et ne devraient pas être exploités. pselect() ne modifie
       cependant jamais ntimeout.

NOTES
       De façon générale, tous les systèmes d'exploitation qui gèrent les
       sockets proposent également select(). select() peut être utilisé
       pour résoudre de façon portable et efficace de nombreux problèmes
       que des programmeurs naïfs essaient de résoudre avec des threads, des
       forks, des IPC, des signaux, des mémoires partagées et d'autres
       méthodes peu élégantes.

       L'appel système poll(2) a les mêmes fonctionnalités que select(),
       tout en étant légèrement plus efficace quand il doit surveiller des
       ensembles de descripteurs creux. Il est disponible sur la plupart des
       systèmes de nos jours, mais était historiquement moins portable que
       select().

       L'API epoll(7) spécifique à Linux fournit une interface plus efficace
       que select(2) et poll(2) lorsque l'on surveille un grand nombre de
       descripteurs de fichier.

EXEMPLE
       Voici un exemple qui montre mieux l'utilité réelle de select(). Le
       code ci-dessous consiste en un programme de « TCP forwarding » qui
       redirige un port TCP vers un autre.

       #include <stdlib.h>
       #include <stdio.h>
       #include <unistd.h>
       #include <sys/time.h>
       #include <sys/types.h>
       #include <string.h>
       #include <signal.h>
       #include <sys/socket.h>
       #include <netinet/in.h>
       #include <arpa/inet.h>
       #include <errno.h>

       static int forward_port;

       #undef max
       #define max(x,y) ((x) > (y) ? (x) : (y))

       static int
       listen_socket(int listen_port)
       {
           struct sockaddr_in a;
           int s;
           int yes;

           s = socket(AF_INET, SOCK_STREAM, 0);
           if (s == -1) {
               perror("socket");
               return -1;
           }
           yes = 1;
           if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
                   &yes, sizeof(yes)) == -1) {
               perror("setsockopt");
               close(s);
               return -1;
           }
           memset(&a, 0, sizeof(a));
           a.sin_port = htons(listen_port);
           a.sin_family = AF_INET;
           if (bind(s, (struct sockaddr *) &a, sizeof(a)) == -1) {
               perror("bind");
               close(s);
               return -1;
           }
           printf("accepting connections on port %d\n", listen_port);
           listen(s, 10);
           return s;
       }

       static int
       connect_socket(int connect_port, char *address)
       {
           struct sockaddr_in a;
           int s;

           s = socket(AF_INET, SOCK_STREAM, 0);
           if (s == -1) {
               perror("socket");
               close(s);
               return -1;
           }

           memset(&a, 0, sizeof(a));
           a.sin_port = htons(connect_port);
           a.sin_family = AF_INET;

           if (!inet_aton(address, (struct in_addr *) &a.sin_addr.s_addr)) {
               perror("bad IP address format");
               close(s);
               return -1;
           }

           if (connect(s, (struct sockaddr *) &a, sizeof(a)) == -1) {
               perror("connect()");
               shutdown(s, SHUT_RDWR);
               close(s);
               return -1;
           }
           return s;
       }

       #define SHUT_FD1 do {                                \
                            if (fd1 >= 0) {                 \
                                shutdown(fd1, SHUT_RDWR);   \
                                close(fd1);                 \
                                fd1 = -1;                   \
                            }                               \
                        } while (0)

       #define SHUT_FD2 do {                                \
                            if (fd2 >= 0) {                 \
                                shutdown(fd2, SHUT_RDWR);   \
                                close(fd2);                 \
                                fd2 = -1;                   \
                            }                               \
                        } while (0)

       #define BUF_SIZE 1024

       int
       main(int argc, char *argv[])
       {
           int h;
           int fd1 = -1, fd2 = -1;
           char buf1[BUF_SIZE], buf2[BUF_SIZE];
           int buf1_avail, buf1_written;
           int buf2_avail, buf2_written;

           if (argc != 4) {
               fprintf(stderr, "Utilisation\n\tfwd <listen-port> "
                        "<forward-to-port> <forward-to-ip-address>\n");
               exit(EXIT_FAILURE);
           }

           signal(SIGPIPE, SIG_IGN);

           forward_port = atoi(argv[2]);

           h = listen_socket(atoi(argv[1]));
           if (h == -1)
               exit(EXIT_FAILURE);

           for (;;) {
               int r, nfds = 0;
               fd_set rd, wr, er;

               FD_ZERO(&rd);
               FD_ZERO(&wr);
               FD_ZERO(&er);
               FD_SET(h, &rd);
               nfds = max(nfds, h);
               if (fd1 > 0 && buf1_avail < BUF_SIZE) {
                   FD_SET(fd1, &rd);
                   nfds = max(nfds, fd1);
               }
               if (fd2 > 0 && buf2_avail < BUF_SIZE) {
                   FD_SET(fd2, &rd);
                   nfds = max(nfds, fd2);
               }
               if (fd1 > 0 && buf2_avail - buf2_written > 0) {
                   FD_SET(fd1, &wr);
                   nfds = max(nfds, fd1);
               }
               if (fd2 > 0 && buf1_avail - buf1_written > 0) {
                   FD_SET(fd2, &wr);
                   nfds = max(nfds, fd2);
               }
               if (fd1 > 0) {
                   FD_SET(fd1, &er);
                   nfds = max(nfds, fd1);
               }
               if (fd2 > 0) {
                   FD_SET(fd2, &er);
                   nfds = max(nfds, fd2);
               }

               r = select(nfds + 1, &rd, &wr, &er, NULL);

               if (r == -1 && errno == EINTR)
                   continue;

               if (r == -1) {
                   perror("select()");
                   exit(EXIT_FAILURE);
               }

               if (FD_ISSET(h, &rd)) {
                   unsigned int l;
                   struct sockaddr_in client_address;

                   memset(&client_address, 0, l = sizeof(client_address));
                   r = accept(h, (struct sockaddr *) &client_address, &l);
                   if (r == -1) {
                       perror("accept()");
                   } else {
                       SHUT_FD1;
                       SHUT_FD2;
                       buf1_avail = buf1_written = 0;
                       buf2_avail = buf2_written = 0;
                       fd1 = r;
                       fd2 = connect_socket(forward_port, argv[3]);
                       if (fd2 == -1)
                           SHUT_FD1;
                       else
                           printf("connexion de %s\n",
                                   inet_ntoa(client_address.sin_addr));
                   }
               }

               /* NB : lecture des données hors bande avant les lectures normales */

               if (fd1 > 0)
                   if (FD_ISSET(fd1, &er)) {
                       char c;

                       r = recv(fd1, &c, 1, MSG_OOB);
                       if (r < 1)
                           SHUT_FD1;
                       else
                           send(fd2, &c, 1, MSG_OOB);
                   }
               if (fd2 > 0)
                   if (FD_ISSET(fd2, &er)) {
                       char c;

                       r = recv(fd2, &c, 1, MSG_OOB);
                       if (r < 1)
                           SHUT_FD2;
                       else
                           send(fd1, &c, 1, MSG_OOB);
                   }
               if (fd1 > 0)
                   if (FD_ISSET(fd1, &rd)) {
                       r = read(fd1, buf1 + buf1_avail,
                                 BUF_SIZE - buf1_avail);
                       if (r < 1)
                           SHUT_FD1;
                       else
                           buf1_avail += r;
                   }
               if (fd2 > 0)
                   if (FD_ISSET(fd2, &rd)) {
                       r = read(fd2, buf2 + buf2_avail,
                                 BUF_SIZE - buf2_avail);
                       if (r < 1)
                           SHUT_FD2;
                       else
                           buf2_avail += r;
                   }
               if (fd1 > 0)
                   if (FD_ISSET(fd1, &wr)) {
                       r = write(fd1, buf2 + buf2_written,
                                  buf2_avail - buf2_written);
                       if (r < 1)
                           SHUT_FD1;
                       else
                           buf2_written += r;
                   }
               if (fd2 > 0)
                   if (FD_ISSET(fd2, &wr)) {
                       r = write(fd2, buf1 + buf1_written,
                                  buf1_avail - buf1_written);
                       if (r < 1)
                           SHUT_FD2;
                       else
                           buf1_written += r;
                   }

               /* Vérifie si l'écriture de données a rattrapé la lecture de données */

               if (buf1_written == buf1_avail)
                   buf1_written = buf1_avail = 0;
               if (buf2_written == buf2_avail)
                   buf2_written = buf2_avail = 0;

               /* une extrémité a fermé la connexion, continue
                  d'écrire vers l'autre extrémité jusqu'à ce
                  que ce soit vide */

               if (fd1 < 0 && buf1_avail - buf1_written == 0)
                   SHUT_FD2;
               if (fd2 < 0 && buf2_avail - buf2_written == 0)
                   SHUT_FD1;
           }
           exit(EXIT_SUCCESS);
       }

       Le programme ci-dessus redirige correctement la plupart des types de
       connexions TCP y compris les signaux de données hors bande OOB
       transmis par les serveurs telnet. Il gère le problème épineux des
       flux de données bidirectionnels simultanés. Vous pourriez penser
       qu'il est plus efficace d'utiliser un appel fork(2) et de dédier une
       tâche à chaque flux. Cela devient alors plus délicat que vous ne
       l'imaginez. Une autre idée est de configurer les entrées-sorties
       comme non bloquantes en utilisant fcntl(2). Cela pose également
       problème puisque ça vous force à utiliser des timeouts inefficaces.

       Le programme ne gère pas plus d'une connexion à la fois bien qu'il
       soit aisément extensible à une telle fonctionnalité en utilisant une
       liste chaînée de tampons — un pour chaque connexion. Pour l'instant,
       de nouvelles connexions provoquent l'abandon de la connexion courante.

VOIR AUSSI
       accept(2), connect(2), ioctl(2), poll(2), read(2), recv(2), select(2),
       send(2), sigprocmask(2), write(2), sigaddset(3), sigdelset(3),
       sigemptyset(3), sigfillset(3), sigismember(3), epoll(7)

COLOPHON
       Cette page fait partie de la publication 3.70 du projet man-pages
       Linux. Une description du projet et des instructions pour signaler des
       anomalies peuvent être trouvées à l'adresse
       http://www.kernel.org/doc/man-pages/.

TRADUCTION
       Depuis 2010, cette traduction est maintenue à l'aide de l'outil po4a
       <http://po4a.alioth.debian.org/> par l'équipe de traduction
       francophone au sein du projet perkamon
       <http://perkamon.alioth.debian.org/>.

       Stéphan Rafin (2002), Alain Portal
       <http://manpagesfr.free.fr/> (2006).  Julien Cristau et l'équipe
       francophone de traduction de Debian (2006-2009).

       Veuillez signaler toute erreur de traduction en écrivant Ã
       <perkamon-fr@traduc.org>.

       Vous pouvez toujours avoir accès à la version anglaise de ce document
       en utilisant la commande « LC_ALL=C man <section> <page_de_man> ».



Linux                          30 décembre 2013                 SELECT_TUT(2)