select, pselect - Multiplexage d'entrées-sorties synchrones
Bibliothèque C standard (
libc,
-lc)
Voir
select(2)
Les appels système
select() et
pselect()) sont
utilisés 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.
Cette page fournit des informations de contexte et des tutoriels sur
l'utilisation de ces appels système. Pour des détails sur les
paramètres et la sémantique de
select() et de
pselect(), voir
select(2).
***
pselect() est utile si vous attendez un signal ou qu'un/des
descripteur(s) de fichier deviennent prêts 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
était 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 n'être reçus que durant l'appel à
pselect(). Par exemple, supposons 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 */
}
}
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.
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 appels à read(2), recv(2),
write(2), send(2) et select() peuvent échouer
avec l'erreur EINTR et les appels à read(2),
recv(2), write(2), write(2) et send(2) peuvent
échouer avec errno positionné sur 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 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 les fonctions read(2), recv(2),
write(2) et send(2) échouent 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.
Voir
select(2).
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.
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 <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <unistd.h>
static int forward_port;
#undef max
#define max(x, y) ((x) > (y) ? (x) : (y))
static int
listen_socket(int listen_port)
{
int lfd;
int yes;
struct sockaddr_in addr;
lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1) {
perror("socket");
return -1;
}
yes = 1;
if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR,
&yes, sizeof(yes)) == -1)
{
perror("setsockopt");
close(lfd);
return -1;
}
memset(&addr, 0, sizeof(addr));
addr.sin_port = htons(listen_port);
addr.sin_family = AF_INET;
if (bind(lfd, (struct sockaddr *) &addr, sizeof(addr)) == -1) {
perror("bind");
close(lfd);
return -1;
}
printf("on accepte les connexions sur le port %d\n", listen_port);
listen(lfd, 10);
return lfd;
}
static int
connect_socket(int connect_port, char *address)
{
int cfd;
struct sockaddr_in addr;
cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd == -1) {
perror("socket");
return -1;
}
memset(&addr, 0, sizeof(addr));
addr.sin_port = htons(connect_port);
addr.sin_family = AF_INET;
if (!inet_aton(address, (struct in_addr *) &addr.sin_addr.s_addr)) {
fprintf(stderr, "inet_aton() : mauvais format d'adresse IP\n");
close(cfd);
return -1;
}
if (connect(cfd, (struct sockaddr *) &addr, sizeof(addr)) == -1) {
perror("connect()");
shutdown(cfd, SHUT_RDWR);
close(cfd);
return -1;
}
return cfd;
}
#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 ready, nfds;
int fd1 = -1, fd2 = -1;
int buf1_avail = 0, buf1_written = 0;
int buf2_avail = 0, buf2_written = 0;
char buf1[BUF_SIZE], buf2[BUF_SIZE];
fd_set readfds, writefds, exceptfds;
ssize_t nbytes;
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 (;;) {
nfds = 0;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);
FD_SET(h, &readfds);
nfds = max(nfds, h);
if (fd1 > 0 && buf1_avail < BUF_SIZE)
FD_SET(fd1, &readfds);
/* Note: nfds est mis à jour ci-dessous, lorsque fd1
est ajouté à exceptfds. */
if (fd2 > 0 && buf2_avail < BUF_SIZE)
FD_SET(fd2, &readfds);
if (fd1 > 0 && buf2_avail - buf2_written > 0)
FD_SET(fd1, &writefds);
if (fd2 > 0 && buf1_avail - buf1_written > 0)
FD_SET(fd2, &writefds);
if (fd1 > 0) {
FD_SET(fd1, &exceptfds);
nfds = max(nfds, fd1);
}
if (fd2 > 0) {
FD_SET(fd2, &exceptfds);
nfds = max(nfds, fd2);
}
ready = select(nfds + 1, &readfds, &writefds, &exceptfds, NULL);
if (ready == -1 && errno == EINTR)
continue;
if (ready == -1) {
perror("select()");
exit(EXIT_FAILURE);
}
if (FD_ISSET(h, &readfds)) {
socklen_t addrlen;
struct sockaddr_in client_addr;
int fd;
addrlen = sizeof(client_addr);
memset(&client_addr, 0, addrlen);
fd = accept(h, (struct sockaddr *) &client_addr, &addrlen);
if (fd == -1) {
perror("accept()");
} else {
SHUT_FD1;
SHUT_FD2;
buf1_avail = buf1_written = 0;
buf2_avail = buf2_written = 0;
fd1 = fd;
fd2 = connect_socket(forward_port, argv[3]);
if (fd2 == -1)
SHUT_FD1;
else
printf("connexion depuis %s\n",
inet_ntoa(client_addr.sin_addr));
/* Passer les événements des anciens descripteurs de
fichier fermés. */
continue;
}
}
/* NB : lecture des données hors bande avant les lectures normales */
if (fd1 > 0 && FD_ISSET(fd1, &exceptfds)) {
char c;
nbytes = recv(fd1, &c, 1, MSG_OOB);
if (nbytes < 1)
SHUT_FD1;
else
send(fd2, &c, 1, MSG_OOB);
}
if (fd2 > 0 && FD_ISSET(fd2, &exceptfds)) {
char c;
nbytes = recv(fd2, &c, 1, MSG_OOB);
if (nbytes < 1)
SHUT_FD2;
else
send(fd1, &c, 1, MSG_OOB);
}
if (fd1 > 0 && FD_ISSET(fd1, &readfds)) {
nbytes = read(fd1, buf1 + buf1_avail,
BUF_SIZE - buf1_avail);
if (nbytes < 1)
SHUT_FD1;
else
buf1_avail += nbytes;
}
if (fd2 > 0 && FD_ISSET(fd2, &readfds)) {
nbytes = read(fd2, buf2 + buf2_avail,
BUF_SIZE - buf2_avail);
if (nbytes < 1)
SHUT_FD2;
else
buf2_avail += nbytes;
}
if (fd1 > 0 && FD_ISSET(fd1, &writefds) && buf2_avail > 0) {
nbytes = write(fd1, buf2 + buf2_written,
buf2_avail - buf2_written);
if (nbytes < 1)
SHUT_FD1;
else
buf2_written += nbytes;
}
if (fd2 > 0 && FD_ISSET(fd2, &writefds) && buf1_avail > 0) {
nbytes = write(fd2, buf1 + buf1_written,
buf1_avail - buf1_written);
if (nbytes < 1)
SHUT_FD2;
else
buf1_written += nbytes;
}
/* Vérifier 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.
accept(2),
connect(2),
poll(2),
read(2),
recv(2),
select(2),
send(2),
sigprocmask(2),
write(2),
epoll(7)
La traduction française de cette page de manuel a été
créée par Christophe Blaess
<
https://www.blaess.fr/christophe/>, Stéphan Rafin
<
[email protected]>, Thierry Vignaud
<
[email protected]>, François Micaux, Alain Portal
<
[email protected]>, Jean-Philippe Guérard
<
[email protected]>, Jean-Luc Coulon (f5ibh)
<
[email protected]>, Julien Cristau
<
[email protected]>, Thomas Huriaux <
[email protected]>,
Nicolas François <
[email protected]>, Florentin
Duneau <
[email protected]>, Simon Paillard
<
[email protected]>, Denis Barbier
<
[email protected]>, David Prévot <
[email protected]>,
Cédric Boutillier <
[email protected]>,
Frédéric Hantrais <
[email protected]> et Jean-Philippe
MENGUAL <
[email protected]>
Cette traduction est une documentation libre ; veuillez vous reporter
à la
GNU
General Public License version 3 concernant les conditions de copie
et de distribution. Il n'y a aucune RESPONSABILITÉ LÉGALE.
Si vous découvrez un bogue dans la traduction de cette page de manuel,
veuillez envoyer un message à
[email protected]