SELECT_TUT(2) System Calls Manual SELECT_TUT(2) NOM select, pselect - Multiplexage d'entrees-sorties synchrones BIBLIOTHEQUE Bibliotheque C standard (libc, -lc) SYNOPSIS Voir select(2) DESCRIPTION Les appels systeme select() et pselect()) sont utilises pour superviser efficacement plusieurs descripteurs de fichiers pour verifier si l'un d'entre eux est ou devient << pret >> ; c'est-a-dire savoir si des entrees-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 systeme. Pour des details sur les parametres et la semantique de select() et de pselect(), voir select(2). Combinaison d'evenements de signaux et de donnees ***pselect() est utile si vous attendez un signal ou qu'un/des descripteur(s) de fichier deviennent prets pour des entrees-sorties. Les programmes qui recoivent des signaux utilisent generalement le gestionnaire de signal uniquement pour lever un drapeau global. Le drapeau global indique que l'evenement doit etre traite 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 verifiant 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 etait en fait en attente. Cette "race condition" est resolue par l'appel pselect(). Cet appel peut etre utilise afin de definir le masque des signaux qui sont censes n'etre recus que durant l'appel a pselect(). Par exemple, supposons que l'evenement en question est la fin d'un processus enfant. Avant le demarrage de la boucle principale, nous bloquerions SIGCHLD en utilisant sigprocmask(2). Notre appel pselect() debloquerait SIGCHLD en utilisant le masque de signaux vide. Le programme ressemblerait a 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 a pselect(). (Code omis.) */ r = pselect(nfds, &readfds, &writefds, &exceptfds, NULL, &empty_mask); if (r == -1 && errno != EINTR) { /* Handle error */ } if (got_SIGCHLD) { got_SIGCHLD = 0; /* Gerer les evenements signales ici; e.g., wait() pour que tous les enfants se terminent. (Code omis.) */ } /* corps principal du programme */ } } Pratique Quelle est donc la finalite de select() ? Ne peut on pas simplement lire et ecrire dans les descripteurs chaque fois qu'on le souhaite ? L'objet de select() est de surveiller de multiples descripteurs simultanement et d'endormir proprement le processus s'il n'y a pas d'activite. Les programmeurs UNIX se retrouvent souvent dans une situation dans laquelle ils doivent gerer des entrees-sorties 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(2) et write(2), 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 pret pour des entrees-sorties. select() gere efficacement cette situation. 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 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 a controler lors de l'utilisation de 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 general pas portable et difficile a deboguer. 2. La valeur nfds 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 le retour de select(), tous les descripteurs de fichier dans tous les ensembles devraient etre testes pour savoir s'ils sont prets. 5. Les fonctions read(2), recv(2), write(2) et send(2) ne lisent ou n'ecrivent pas forcement la quantite totale de donnees specifiee. 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 sur 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-dessous font 1024 octets bien qu'ils aient facilement pu etre rendus plus grands. 7. Les appels a read(2), recv(2), write(2), send(2) et select() peuvent echouer avec l'erreur EINTR et les appels a read(2), recv(2), write(2), write(2) et send(2) peuvent echouer avec errno positionne sur 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 entrees-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) echouent avec une erreur autre que celles indiquees en 7., ou si l'une des fonctions d'entree renvoie 0, indiquant une fin de fichier, vous ne devriez pas utiliser ce descripteur a nouveau pour un appel a select(). Dans l'exemple ci-dessous, le descripteur est immediatement ferme et ensuite est 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. Comme select() modifie ses ensembles de descripteurs de fichiers, si l'appel est effectue dans une boucle alors les ensembles doivent etre reinitialises avant chaque appel. VALEUR RENVOYEE Voir select(2). NOTES De facon generale, tous les systemes d'exploitation qui gerent les sockets proposent egalement select(). select() peut etre utilise pour resoudre de facon portable et efficace de nombreux problemes que des programmeurs naifs essaient de resoudre avec des threads, des forks, des IPC, des signaux, des memoires partagees et d'autres methodes peu elegantes. L'appel systeme poll(2) a les memes fonctionnalites que select(), tout en etant legerement plus efficace quand il doit surveiller des ensembles de descripteurs creux. Il est disponible sur la plupart des systemes de nos jours, mais etait historiquement moins portable que select(). L'API epoll(7) specifique a Linux fournit une interface plus efficace que select(2) et poll(2) lorsque l'on surveille un grand nombre de descripteurs de fichier. EXEMPLES 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 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 " " \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 a jour ci-dessous, lorsque fd1 est ajoute a 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 evenements des anciens descripteurs de fichier fermes. */ continue; } } /* NB : lecture des donnees 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; } /* Verifier si l'ecriture de donnees a rattrape 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; } exit(EXIT_SUCCESS); } 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 donnees bidirectionnels simultanes. Vous pourriez penser qu'il est plus efficace d'utiliser un appel fork(2) et de dedier une tache a chaque flux. Cela devient alors plus delicat que vous ne l'imaginez. Une autre idee est de configurer les entrees-sorties comme non bloquantes en utilisant fcntl(2). Cela pose egalement probleme puisque ca vous force a utiliser 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 nouvelles connexions provoquent l'abandon de la connexion courante. VOIR AUSSI accept(2), connect(2), poll(2), read(2), recv(2), select(2), send(2), sigprocmask(2), write(2), epoll(7) TRADUCTION La traduction francaise de cette page de manuel a ete creee par Christophe Blaess , Stephan Rafin , Thierry Vignaud , Francois Micaux, Alain Portal , Jean-Philippe Guerard , Jean-Luc Coulon (f5ibh) , Julien Cristau , Thomas Huriaux , Nicolas Francois , Florentin Duneau , Simon Paillard , Denis Barbier , David Prevot , Cedric Boutillier , Frederic Hantrais et Jean-Philippe MENGUAL Cette traduction est une documentation libre ; veuillez vous reporter a la GNU General Public License version 3 concernant les conditions de copie et de distribution. Il n'y a aucune RESPONSABILITE LEGALE. Si vous decouvrez un bogue dans la traduction de cette page de manuel, veuillez envoyer un message a . Pages du manuel de Linux 6.06 31 octobre 2023 SELECT_TUT(2)