Man Select_tut en français
SELECT_TUT(2) Manuel du programmeur Linux SELECT_TUT(2)
NOM
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - Multiplexage d'E/S
synchrones
SYNOPSIS
#include
#include
#include
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *utimeout);
int pselect(int n, 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 pro-
grammes en C qui gerent simultanement et de facon efficace plusieurs
descripteurs de fichiers (ou sockets). Ses principaux arguments sont
trois tableaux de descripteurs de fichiers : readfds, writefds, et
exceptfds. select() est generalement utilise de facon a bloquer en
attendant un changement d'etat d'un ou plusieurs descripteurs de
fichiers. Un changement d'etat est signale lorsque de nouveaux carac-
teres sont mis a disposition sur le descripteur de fichier ; ou bien
lorsque de l'espace devient disponible au niveau des tampons internes
du noyau permettant de nouvelles ecritures 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 levee
lorsque l'autre extremite de la connexion est fermee).
Pour resumer, select() surveille simplement de multiples descripteurs
de fichiers, et constitue l'appel Unix standard pour realiser cette
tche.
Les tableaux de descripteurs de fichier sont appeles ensembles de
descripteurs de fichiers. Chaque ensemble est de type fd_set, et son
contenu peut etre modifie avec les macros FD_CLR(), FD_ISSET(),
FD_SET(), et FD_ZERO(). On commence generalement par utiliser FD_ZERO()
sur un ensemble venant d'etre cree. Ensuite, les descripteurs de
fichiers individuels qui vous interessent peuvent etre ajoutes un a un
a l'aide de FD_SET(). select() modifie le contenu de ces ensembles
selon les regles ci-dessous. Apres un appel a select(), vous pouvez
verifier si votre descripteur de fichier est toujours present dans
l'ensemble a l'aide de la macro FD_ISSET(). FD_ISSET() renvoie zero 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 examine afin de determiner si des donnees sont
disponibles en lecture a partir d'un de ses descripteurs de
fichiers. Suite a un appel a select(), readfds ne contient plus
aucun de ses descripteurs de fichiers a l'exception de ceux qui
sont immediatement disponibles pour une lecture via un appel
recv() (pour les sockets) ou read() (pour les tubes, fichiers et
sockets).
writefds
Cet ensemble est examine afin de determiner s'il y a de l'espace
afin d'ecrire des donnees dans un de ses descripteurs de
fichiers. Suite a un appel a select(), writefds ne contient plus
aucun de ses descripteurs de fichiers a l'exception de ceux qui
sont immediatement disponibles pour une ecriture via un appel a
send() (pour les sockets) ou write() (pour les tubes, fichiers
et sockets).
exceptfds
Cet ensemble est examine pour les exceptions ou les erreurs sur-
venues sur les descripteurs de fichiers. Neanmoins, ceci n'est
veritablement rien d'autre qu'une rumeur. exceptfds est en fait
utilise afin de detecter l'occurrence de donnees hors-bande (Out
Of Band). Les donnees hors bande sont celles qui sont envoyees
sur une socket en utilisant le drapeau MSG_OOB, ainsi exceptfds
s'applique en realite uniquement aux sockets. Voir recv(2) et
send(2) a ce sujet. Suite a un appel a select(), exceptfds ne
contient plus aucun de ses descripteurs de fichiers a l'excep-
tion de ceux qui sont disponibles pour une lecture de donnees
hors-bande. Cependant, vous pouvez presque toujours lire unique-
ment un octet de donnees hors bande (a l'aide de recv()), et
l'ecriture de donnees hors bande (avec send) peut etre effectuee
a n'importe quel moment et n'est pas bloquante. Il n'y a donc
pas de besoin d'un quatrieme ensemble afin de verifier si une
socket est disponible pour une ecriture de donnees hors bande.
n 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 ter-
mes, lorsque vous ajoutez des descripteurs de fichiers a vos
ensembles, vous devez determiner la valeur entiere maximale de
tous ces derniers, puis ajouter un a cette valeur, et la passer
en argument n a select().
utimeout
Il s'agit du temps le plus long que select() doit attendre avant
de rendre la main, meme si rien d'interessant n'est arrive. Si
cette valeur est positionnee a NULL, alors, select() bloque
indefiniment dans l'attente d'un evenement. utimeout peut etre
positionne a zero seconde, ce qui provoque le retour immediat de
select(). La structure struct timeval est definie comme
struct timeval {
long tv_sec; /* secondes */
long tv_usec; /* microsecondes */
};
ntimeout
Cet argument a la meme signification que utimeout mais struct
timespec a une precision a la nanoseconde comme explicite ci-
dessous :
struct timespec {
long tv_sec; /* secondes */
long tv_nsec; /* nanosecondes */
};
sigmask
Cet argument renferme un ensemble de signaux non bloques 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 bloques a l'entree et la sortie de la fonction.
Il se comporte alors de facon identique a select().
COMBINAISON D'EVENEMENTS DE SIGNAUX ET DE DONNEES
pselect() doit etre utilise si vous attendez tout aussi bien un signal
que des donnees d'un descripteur de fichier. Les programmes qui
recoivent les signaux comme des evenements utilisent generalement le
gestionnaire de signal uniquement pour lever un drapeau global. Le dra-
peau global indique que l'evenement doit etre traiter dans la boucle
principale du programme. Un signal provoque l'arret de l'appel select()
(ou pselect()) avec errno positionnee a EINTR. Ce comportement est
essentiel afin que les signaux puissent etre traites dans la boucle
principale du programme, sinon select() bloquerait indefiniment. Ceci
etant, la boucle principale implante quelque part une condition verifi-
ant le drapeau global, et l'on doit donc se demander : que se passe
t'il si un signal est leve apres la condition mais avant l'appel a
select() ? La reponse est que select() bloquerait indefiniment, meme si
un signal est en fait en attente. Cette "race condition" est resolue
par l'appel pselect(). Cet appel peut etre utilise afin de debloquer
des signaux qui ne sont pas censes etre recus si ce n'est durant
l'appel a pselect(). Par exemple, disons que l'evenement en question
est la fin d'un processus fils. Avant le demarrage de la boucle princi-
pale, nous bloquerions SIGCHLD en utilisant sigprocmask(). Notre appel
pselect() debloquerait SIGCHLD en utilisant le masque de signal ini-
tial. Le programme ressemblerait a 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 (;;) { /* main loop */
for (; child_events > 0; child_events--) {
/* do event work here */
}
r = pselect (n, &rd, &wr, &er, 0, &orig_sigmask);
/* corps principal du programme */
}
}
PRATIQUE
Quelle est donc la finalite de select() ? Ne puis-je pas simplement
lire et ecrire dans les descripteurs chaque fois que je le souhaite ?
L'objet de select() est de surveiller de multiples descripteurs simul-
tanement et d'endormir proprement le processus s'il n'y a pas
d'activite. Il fait ceci tout en vous permettant de gerer de multiples
tubes et sockets simultanement. Les programmeurs UNIX se retrouvent
souvent dans une situation dans laquelle ils doivent gerer des E/S
provenant de plus d'un descripteur de fichier et dans laquelle le flux
de donnees est intermittent. Si vous deviez creer une sequence d'appels
read() et write(), vous vous retrouveriez potentiellement bloque sur un
de vos appels attendant pour lire ou ecrire des donnees a partir/vers
un descripteur de fichier, alors qu'un autre descripteur de fichier est
inutilise bien qu'il soit disponible pour lire/ecrire des donnees.
select() gere efficacement cette situation.
Un exemple simple de l'utilisation de select() peut etre trouve dans la
page de manuel select(2).
EXEMPLE DE REDIRECTION DE PORT
Voici un exemple qui montre mieux l'utilite reelle de select(). Le
code ci-dessous consiste en un programme de TCP forwarding qui
redirige un port TCP vers un autre.
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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\n",
(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\n\tfwd \
\n");
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\n",
inet_ntoa
(client_address.sin_addr));
}
}
/* NB : lecture des donnees 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;
}
/* Verifie si l'ecriture de donnees a provoque la lecture de donnees */
if (buf1_written == buf1_avail)
buf1_written = buf1_avail = 0;
if (buf2_written == buf2_avail)
buf2_written = buf2_avail = 0;
/* une extremite a ferme la connexion, continue
d'ecrire vers l'autre extremite jusqu'a 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 donnees hors bande OOB transmis
par les serveurs telnet. Il gere le probleme epineux des flux de don-
nees bidirectionnels simultanes. Vous pourriez penser qu'il est plus
efficace d'utiliser un appel fork() et de dedier une tche a chaque
flux. Cela devient alors plus delicat que vous ne l'imaginez. Une autre
idee est de configurer les E/S comme non bloquantes en utilisant un
appel ioctl(). Cela pose egalement probleme parce que vous finissez par
avoir des timeouts inefficaces.
Le programme ne gere pas plus d'une connexion a la fois bien qu'il soit
aisement extensible a une telle fonctionnalite en utilisant une liste
chainee de tampons - un pour chaque connexion. Pour l'instant, de nou-
velles connexions provoquent l'abandon de la connexion courante.
REGLES DE SELECT
De nombreuses personnes qui essaient d'utiliser select() obtiennent un
comportement difficile a comprendre et produisent des resultats non
portables ou des effets de bord. Par exemple, le programme ci-dessus
est ecrit avec precaution afin de ne bloquer nulle part, meme s'il ne
positionne pas du tout ses descripteurs de fichiers en mode non blo-
quant (voir ioctl(2)). Il est facile d'introduire des erreurs subtiles
qui annuleraient l'avantage de l'utilisation de select(), aussi, vais-
je presenter une liste de points essentiels a controler lors de l'util-
isation de l'appel select().
1. Vous devriez toujours essayer d'utiliser select() sans timeout.
Votre programme ne devrait rien avoir a faire s'il n'y a pas de
donnees disponibles. Le code dependant de timeouts n'est en gen-
eral pas portable et difficile a deboguer.
2. La valeur n doit etre calculee correctement pour des raisons
d'efficacite comme explique plus haut.
3. Aucun descripteur de fichier ne doit etre ajoute a un quelconque
ensemble si vous ne projetez pas de verifier son etat apres un
appel a select(), et de reagir de facon adequate. Voir la regle
suivante.
4. Apres qu'un appel select() ait rendu la main, tous les descrip-
teurs de fichiers de tous les ensembles devraient etre verifies
pour voir s'ils sont prets.
5. Les fonctions read(), recv(), write(), et send() ne lisent ou
n'ecrivent pas forcement la quantite totale de donnees speci-
fiee. Si elles lisent/ecrivent la quantite 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 gerer le cas ou vos
fonctions traitent seulement l'envoi ou la reception d'un unique
octet.
6. Ne lisez/N'ecrivez jamais seulement quelques octets a la fois a
moins que vous ne soyez absolument sr de n'avoir qu'une faible
quantite de donnees a traiter. Il est parfaitement inefficace de
ne pas lire/ecrire autant de donnees que vous pouvez en stocker
a chaque fois. Les tampons de l'exemple ci-dessus font 1024
octets bien qu'ils aient facilement pu etre rendus plus grands.
7. Les fonctions read(), recv(), write(), et send() tout comme
l'appel select() peuvent renvoyer -1 avec errno valant EINTR ou
EAGAIN (EWOULDBLOCK). Ces resultats doivent etre correctement
geres (cela n'est pas fait correctement ci-dessus.) Si votre
programme n'est pas cense recevoir de signal, alors, il est
hautement improbable que vous obteniez EINTR. Si votre programme
n'a pas configure les E/S en mode non bloquant, vous n'obtien-
drez pas de EAGAIN. Neanmoins, vous devriez tout de meme gerer
ces erreurs dans un soucis de completude.
8. N'appelez jamais read(), recv(), write(), ou send() avec un
tampon de taille nulle.
9. Si les fonctions read(), recv(), write() et send() echouent avec
des erreurs autres que celles indiquees en7., ou si l'une des
fonctions d'entree renvoie 0, indiquant la fin de fichier, vous
ne devriez pas passer ce descripteur de fichier a select().
again. Dans l'exemple ci-dessus, je ferme le descripteur imme-
diatement, et ensuite, je le positionne a -1 afin qu'il ne soit
pas inclus dans un ensemble.
10. La valeur de timeout doit etre initialisee a chaque nouvel appel
a select(), puisque des systemes d'exploitation modifient la
structure. Cependant, pselect() ne modifie pas sa structure de
timeout.
11. J'ai entendu que le niveau socket Windows ne traite pas cor-
rectement les donnees hors bande (OOB). Il ne gere pas non plus
les appels select() lorsqu'aucun descripteur de fichier n'est
positionne. N'avoir aucun descripteur de fichier positionne est
un moyen utile afin d'endormir le processus avec une precision
inferieure a la seconde en utilisant le timeout. (Voir plus
loin.)
EMULATION DE USLEEP
Sur les systemes qui ne possedent pas la fonction usleep(), vous pouvez
appeler select() avec un timeout a valeur finie et sans descripteur de
fichier de la facon 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 systemes Unix.
VALEUR RENVOYEE
En cas de succes, select() renvoie le nombre total de descripteurs de
fichiers encore presents dans les ensembles de descripteurs de
fichiers.
En cas de timeout echu, la valeur de retour sera zero. Les descrip-
teurs de fichiers devraient tous etre vides (mais peuvent ne pas l'etre
sur certains systemes).
Une valeur de retour egale a -1 indique une erreur, errno est alors
positionne de facon adequate. En cas d'erreur, les ensembles renvoyes
et le contenu de la structure de timeout sont indefinis et ne
devraient pas etre exploites. pselect() ne modifie cependant jamais
ntimeout.
NOTES
De facon generale, tous les systemes d'exploitation qui gerent les
sockets, implantent egalement select(). De nombreux types de pro-
grammes deviennent extremement compliques sans cette fonction.
select() peut etre utilise pour resoudre de facon portable et efficace
de nombreux problemes que des programmeurs naifs essaient de resoudre
de maniere plus compliquee avec des threads, des forks, des IPCs, des
signaux, des memoires partagees et ainsi de suite.
L'appel systeme poll(2) a les memes fonctionnalites que select(), et
est quelque peu plus efficace lors de la surveillance d'ensembles de
descripteurs de fichiers parsemes. Il est aujourd'hui largement
disponible mais etait considere historiquement comme moins portable que
select().
L'API epoll(7), specifique a Linux, fournit une interface plus efficace
que select(2) et poll(2) pour la surveillance d'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
Ce document est une traduction realisee par Stephan Rafin le 16 juin 2002 et revisee le 7 decem-
bre 2006.
L'equipe de traduction a fait le maximum pour realiser une adaptation
francaise de qualite. La version anglaise la plus a jour de ce document
est toujours consultable via la commande : LANG=C man 2 select_tut .
N'hesitez pas a signaler a l'auteur ou au traducteur, selon le cas,
toute erreur dans cette page de manuel.
Linux 2.4 21 octobre 2001 SELECT_TUT(2)
|