SELECT_TUT
Section : Manuel du programmeur Linux (
2)
Mise à jour de la version anglaise : 18 décembre 2007
Index
Menu principal
NOM
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - Multiplexage d'entrées-sorties synchrones
SYNOPSIS
/* Selon POSIX.1-2001 */
#include <sys/select.h>
/* Selon les normes précédentes */
#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 (voir
feature_test_macros(7)) :
pselect() :
_POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600
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
descripteur de fichiers (ou sockets). Ses principaux arguments sont
trois tableaux de descripteurs de fichier :
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 fichier.
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 fichier.
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 fichier 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 fichier.
Suite à un appel à
select(),
readfds
ne contient plus aucun de ses descripteurs de fichier à l'exception
de ceux qui sont immédiatement disponibles pour une lecture via un appel
recv(2)
(pour les sockets) ou
read(2)
(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 fichier.
Suite à un appel à
select(),
writefds ne contient plus aucun de ses descripteurs de fichier
à l'exception de ceux qui sont immédiatement disponibles
pour une écriture via un appel à
send(2)
(pour les sockets) ou
write(2)
(pour les tubes, fichiers et sockets).
- exceptfds
-
Cet ensemble est examiné pour les exceptions ou les erreurs survenues
sur les descripteurs de fichier.
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 fichier
à 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(2)),
et l'écriture de données hors bande (avec
send(2))
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 fichier de tous les ensembles.
En d'autres termes, lorsque vous ajoutez des descripteurs de fichier
à 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
renseignée avec
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(2).
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(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 disponible pour lire/écrire des données.
select()
gère efficacement cette situation.
Un exemple simple de l'utilisation de
select()
peut être trouvé dans la page de manuel
select().
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 fichier 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 qu'un appel
select()
ait rendu la main, 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-dessus 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
valant
EINTR
ou
EAGAIN
(EWOULDBLOCK).
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(2),
recv(2),
write(2)
ou
send(2)
avec un tampon de taille nulle.
- 9.
-
Si les fonctions
read(2),
recv(2),
write(2)
et
send(2)
échouent
avec des erreurs autres que celles indiquées en7.,
ou si l'une des fonctions d'entrée renvoie 0, indiquant une fin de fichier,
vous ne devriez pas passer ce descripteur de fichier à
select().
à nouveau.
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 parî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(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
fichiers.
En cas de timeout échu, la valeur de retour sera zéro.
Les descripteurs de fichier devraient tous
être vides (mais peuvent ne pas l'être sur certains systèmes).
Une valeur de retour égale à -1 indique une erreur,
errno
est alors renseignée de façon adéquate.
En cas d'erreur, le contenu des ensembles renvoyés et de la structure
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
de manière plus compliquée
avec des threads, des forks, des IPCs, des signaux, des mémoires partagées
et ainsi de suite.
L'appel système
poll(2)
a les mêmes fonctionnalités que
select(),
et est quelque peu plus efficace lors de la surveillance d'ensembles
de descripteurs de fichier parsemés.
Il est aujourd'hui largement disponible mais était considéré
historiquement comme moins portable que
select().
L'API
epoll(7),
spécifique à Linux, fournit une interface plus efficace que
select(2)
et
poll(2)
pour la surveillance d'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;
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", 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 <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 < 0)
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);
n = 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 < 0) {
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 < 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 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;
}
}
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 E/S comme non bloquantes
en utilisant un appel
ioctl(2).
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.
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 réalisée par Stéphan Rafin
<stephan DOT rafin AT laposte DOT net> le 16 juin 2002
et révisée le 24 juin 2008.
L'équipe de traduction a fait le maximum pour réaliser une adaptation
française de qualité. La version anglaise la plus à jour de ce document est
toujours consultable via la commande : « LANG=C man 2 select_tut ».
N'hésitez pas à signaler à l'auteur ou au traducteur, selon le cas, toute
erreur dans cette page de manuel.
Index
- NOM
-
- SYNOPSIS
-
- DESCRIPTION
-
- Arguments
-
- Combinaison d'événements de signaux et de données
-
- Pratique
-
- Règles de select
-
- Émulation de usleep
-
- VALEUR RENVOYÉE
-
- NOTES
-
- EXEMPLE
-
- VOIR AUSSI
-
- TRADUCTION
-
Dernière mise à jour : 24 juin 2008