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;
if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -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;
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_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.44 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 à
<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> ».