Date création : 27-03-2008 20:23:44
 Vous êtes dans : GNU/Linux Astuces / Pages man [Section2 - Appels système]
SELECT_TUT
Index
- NOM
- SYNOPSIS
- DESCRIPTION
- ARGUMENTS
- COMBINAISON D'ÉVÉNEMENTS DE SIGNAUX ET DE DONNÉES
- PRATIQUE
- EXEMPLE DE REDIRECTION DE PORT
- RÈGLES DE SELECT
- ÉMULATION DE USLEEP
- VALEUR RENVOYÉE
- NOTES
- VOIR AUSSI
- TRADUCTION
NOM
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - Multiplexage
d'entrées-sorties synchrones.
SYNOPSIS
#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);
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *ntimeout, sigset_t
*sigmask);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
DESCRIPTION
select() (ou pselect()) est la fonction pivot de la plupart des
programmes en C qui gèrent simultanément et de façon efficace plusieurs
descripteurs de fichiers (ou sockets). Ses principaux arguments sont trois
tableaux de descripteurs de fichiers : readfds, writefds, et
exceptfds. select() est généralement utilisé de façon à bloquer en
attendant un « changement d'état » d'un ou plusieurs descripteurs de
fichiers. Un « changement d'état » est signalé lorsque de nouveaux
caractères sont mis à disposition sur le descripteur de fichier ; ou
bien lorsque de l'espace devient disponible au niveau des tampons internes
du noyau permettant de nouvelles écritures dans le descripteur de fichier,
ou bien lorsqu'un descripteur de fichier rencontre une erreur (dans le
cas d'une socket ou d'un tube, une telle erreur est levée lorsque l'autre
extrémité de la connexion est fermée).
Pour résumer, select() surveille simplement de multiples descripteurs de
fichiers, et constitue l'appel Unix standard pour réaliser cette tâche.
Les tableaux de descripteurs de fichier sont appelés ensembles de
descripteurs de fichiers. Chaque ensemble est de type fd_set, et son
contenu peut être modifié avec les macros FD_CLR(), FD_ISSET(),
FD_SET(), et FD_ZERO(). On commence généralement par utiliser
FD_ZERO() sur un ensemble venant d'être créé. Ensuite, les descripteurs
de fichiers individuels qui vous intéressent peuvent être ajoutés un à un à
l'aide de FD_SET(). select() modifie le contenu de ces ensembles selon
les règles ci-dessous. Après un appel à select(), vous pouvez vérifier si
votre descripteur de fichier est toujours présent dans l'ensemble à l'aide
de la macro FD_ISSET(). FD_ISSET() renvoie zéro si le descripteur de
fichier est absent et une valeur non nulle sinon. FD_CLR() retire un
descripteur de fichier de l'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 fichiers. 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 via un appel recv() (pour les sockets) ou read() (pour les
tubes, fichiers et sockets).
- 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 fichiers. 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 via un appel à send() (pour les sockets) ou write() (pour
les tubes, fichiers et sockets).
- exceptfds
-
Cet ensemble est examiné pour les exceptions ou les erreurs survenues sur
les descripteurs de fichiers. Néanmoins, ceci n'est véritablement rien
d'autre qu'une rumeur. exceptfds est en fait utilisé afin de détecter
l'occurrence de données hors-bande (Out Of Band). Les données hors bande
sont celles qui sont envoyées sur une socket en utilisant le drapeau
MSG_OOB, ainsi exceptfds s'applique en réalité uniquement aux
sockets. Voir recv(2) et send(2) à ce sujet. Suite à un appel à
select(), exceptfds ne contient plus aucun de ses descripteurs de
fichiers à l'exception de ceux qui sont disponibles pour une lecture de
données hors-bande. Cependant, vous pouvez presque toujours lire uniquement
un octet de données hors bande (à l'aide de recv()), et l'écriture de
données hors bande (avec send()) peut être effectuée à n'importe quel
moment et n'est pas bloquante. Il n'y a donc pas de besoin d'un quatrième
ensemble afin de vérifier si une socket est disponible pour une écriture de
données hors bande.
- nfds
-
Il s'agit d'un entier valant un de plus que n'importe lequel des
descripteurs de fichiers de tous les ensembles. En d'autres termes, lorsque
vous ajoutez des descripteurs de fichiers à vos ensembles, vous devez
déterminer la valeur entière maximale de tous ces derniers, puis ajouter un
à cette valeur, et la passer en argument nfds à select().
- utimeout
-
-
Il s'agit du temps le plus long que select() doit 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
d'un événement. utimeout peut être positionné à zéro seconde, ce qui
provoque le retour immédiat de select(). La structure struct timeval
est définie comme
struct timeval {
long tv_sec; /* secondes */
long tv_usec; /* microsecondes */
};
- ntimeout
-
-
Cet argument 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 non bloqués pendant un appel
pselect() (voir 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. Il se comporte alors de façon
identique à select().
COMBINAISON D'ÉVÉNEMENTS DE SIGNAUX ET DE DONNÉES
pselect() doit être utilisé si vous attendez tout aussi bien un signal
que des données d'un descripteur de fichier. Les programmes qui reçoivent
les signaux comme des événements 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ébloquer des signaux qui ne sont pas censés être reçus si
ce n'est 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(). Notre
appel pselect() débloquerait SIGCHLD en utilisant le masque de signal
initial. Le programme ressemblerait à ceci :
int child_events = 0;
void child_sig_handler (int x) {
child_events++;
signal (SIGCHLD, child_sig_handler);
}
int main (int argc, char **argv) {
sigset_t sigmask, orig_sigmask;
sigemptyset (&sigmask);
sigaddset (&sigmask, SIGCHLD);
sigprocmask (SIG_BLOCK, &sigmask,
&orig_sigmask);
signal (SIGCHLD, child_sig_handler);
for (;;) { /* boucle principale */
for (; child_events > 0; child_events--) {
/* traiter les événements ici */
}
r = pselect (n, &rd, &wr, &er, 0, &orig_sigmask);
/* 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é. Il fait ceci
tout en vous permettant de gérer de multiples tubes et sockets
simultanément. Les programmeurs UNIX se retrouvent souvent dans une
situation dans laquelle ils doivent gérer des E/S 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() et
write(), 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 disponible pour lire/écrire des
données. select() gère efficacement cette situation.
Un exemple classique de select() se trouve dans la page de manuel de
select(2).
EXEMPLE DE REDIRECTION DE PORT
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;
if ((s = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
perror ("socket");
return -1;
}
yes = 1;
if (setsockopt
(s, SOL_SOCKET, SO_REUSEADDR,
(char *) &yes, sizeof (yes)) < 0) {
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)) < 0) {
perror ("bind");
close (s);
return -1;
}
printf ("accepting connections on port %d
",
(int) listen_port);
listen (s, 10);
return s;
}
static int connect_socket (int connect_port,
char *address) {
struct sockaddr_in a;
int s;
if ((s = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
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)) < 0) {
perror ("connect()");
shutdown (s, SHUT_RDWR);
close (s);
return -1;
}
return s;
}
#define SHUT_FD1 {
if (fd1 >= 0) {
shutdown (fd1, SHUT_RDWR);
close (fd1);
fd1 = -1;
}
}
#define SHUT_FD2 {
if (fd2 >= 0) {
shutdown (fd2, SHUT_RDWR);
close (fd2);
fd2 = -1;
}
}
#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
fwd <listen-port>
<forward-to-port> <forward-to-ip-address>
");
exit (1);
}
signal (SIGPIPE, SIG_IGN);
forward_port = atoi (argv[2]);
h = listen_socket (atoi (argv[1]));
if (h < 0)
exit (1);
for (;;) {
int r, n = 0;
fd_set rd, wr, er;
FD_ZERO (&rd);
FD_ZERO (&wr);
FD_ZERO (&er);
FD_SET (h, &rd);
n = max (n, h);
if (fd1 > 0 && buf1_avail < BUF_SIZE) {
FD_SET (fd1, &rd);
n = max (n, fd1);
}
if (fd2 > 0 && buf2_avail < BUF_SIZE) {
FD_SET (fd2, &rd);
n = max (n, fd2);
}
if (fd1 > 0
&& buf2_avail - buf2_written > 0) {
FD_SET (fd1, &wr);
n = max (n, fd1);
}
if (fd2 > 0
&& buf1_avail - buf1_written > 0) {
FD_SET (fd2, &wr);
n = max (n, fd2);
}
if (fd1 > 0) {
FD_SET (fd1, &er);
n = max (n, fd1);
}
if (fd2 > 0) {
FD_SET (fd2, &er);
n = max (n, fd2);
}
r = select (n + 1, &rd, &wr, &er, NULL);
if (r == -1 && errno == EINTR)
continue;
if (r < 0) {
perror ("select()");
exit (1);
}
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 < 0) {
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 < 0) {
SHUT_FD1;
} else
printf ("connexion de %s
",
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;
errno = 0;
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;
errno = 0;
r = recv (fd2, &c, 1, MSG_OOB);
if (r < 1) {
SHUT_FD1;
} 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 provoqué 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;
}
}
return 0;
}
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() 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 E/S comme non bloquantes en utilisant un appel
ioctl(). Cela pose également problème parce que vous finissez par avoir
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.
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 du tout ses descripteurs de fichiers en mode non bloquant (voir
ioctl(2)). Il est facile d'introduire des erreurs subtiles qui
annuleraient l'avantage de l'utilisation de select(), aussi, cette page
présente une liste de points essentiels à contrôler lors de l'utilisation de
l'appel 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 fichiers dans tous
les ensembles devraient être testés pour savoir s'ils sont prêts.
- 5.
-
Les fonctions read(), recv(), write(), et send() 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-dessus font 1024 octets bien qu'ils aient facilement pu être rendus plus
grands.
- 7.
-
Les fonctions read(), recv(), write(), et send() 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 E/S en mode non bloquant, vous n'obtiendrez pas de
EAGAIN. Néanmoins, vous devriez tout de même gérer ces erreurs dans un
souci de complétude.
- 8.
-
N'appelez jamais read(), recv(), write(), ou send() avec un
tampon de taille nulle.
- 9.
-
Si l'une des fonctions read(), recv(), write() et send() é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-dessus, 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.
-
Il paraît que la couche socket de Windows ne traite pas correctement les
données hors bande (OOB). Il ne gère pas non plus les appels select()
lorsqu'aucun descripteur de fichier n'est positionné. N'avoir aucun
descripteur de fichier positionné est un moyen utile afin d'endormir le
processus avec une précision inférieure à la seconde en utilisant le
timeout. (Voir plus loin.)
ÉMULATION DE USLEEP
Sur les systèmes qui ne possèdent pas la fonction usleep(), 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 fichiers.
En cas de timeout échu, alors les descripteurs de fichiers 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, les 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,
implantent également select(). De nombreux types de programmes deviennent
extrêmement compliqués sans cette fonction. 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
IPCs, 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 fichiers.
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)
TRADUCTION
Cette page de manuel a été traduite par
Stéphan Rafin <stephan DOT rafin AT laposte DOT net> en 2002,
puis a été mise à jour par
Alain Portal <aportal AT univ-montp2 DOT fr> jusqu'en 2006.
La traduction de cette page de manuel est basée sur les traductions
disponibles sur http://manpagesfr.free.fr/,
mais est gérée par l'équipe francophone de traduction de Debian
au travers de la liste de discussion debian-l10n-french.
Veuillez signaler toute erreur de traduction par un rapport de bogue sur
le paquet manpages-fr.
Vous pouvez toujours avoir accès à la version anglaise de ce document en
utilisant la commande
« man -L C <section> <page_de_man> ».
|