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 (voir
feature_test_macros(7)) :
pselect() : _POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >=600
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
pseudo terminaux 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() (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. 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;
if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
return -1;
}
yes = 1;
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
(char *) &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;
if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -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_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 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.23 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
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, et mise à
disposition sur http://manpagesfr.free.fr/.
Les mises à jour et corrections de la version présente dans Debian sont
directement gérées par Julien Cristau <jcristau@debian.org> et l’équipe
francophone de traduction de Debian.
Veuillez signaler toute erreur de traduction en écrivant à
<debian-l10n-french@lists.debian.org> ou 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> ».