Multiplexage des entrées-sorties
Dans ce chapitre, nous voulons présenter le problème de l'attente sur plusieurs descripteurs. L'exemple le plus fréquent est celui d'un serveur web : le serveur doit gérer simultanément un très grand nombre de flux d'entrée, de flux de sortie et de flux de contrôle.
Solution avec le mode non bloquant
Il est possible d'utiliser des entrées-sorties non bloquantes, mais c'est loin d'être la solution optimale, car notre processus va réaliser de nombreux appels système inutiles (attente active), d'autant plus dans le cas d'un serveur avec des comportements de clients très aléatoires. Le coût en ressources de cette attente active est extrêmement élevé et doit être évité, en particulier sur une machine en temps partagé.
Utiliser les mécanismes asynchrones
On peut utiliser des entrées-sorties asynchrones et demander au noyau de nous
prévenir par un signal de l'arrivée de données sur un descripteur. Ce signal est
SIGIO ; il s'active avec fcntl (commande F_SETOWN pour désigner le
processus destinataire, puis drapeau O_ASYNC via F_SETFL). Il n'est
disponible que pour certains types de descripteurs (terminaux, pseudo-terminaux,
sockets, et sous Linux tubes et FIFO), pas pour les fichiers ordinaires. De plus,
par défaut, ce mécanisme ne désigne pas le descripteur sur lequel s'est faite
l'arrivée de données, d'où de nouvelles pertes de temps dues aux appels réalisés
inutilement en mode non bloquant.
Les outils de sélection
La solution la plus efficace vient d'appels système de sélection, qui prennent
en paramètre un ensemble de descripteurs et permettent de tester si l'un de ces
descripteurs est prêt à satisfaire un appel système read ou write sans
bloquer. Cet appel est bloquant jusqu'à ce qu'un des descripteurs de l'ensemble
soit prêt (ou jusqu'à l'expiration d'un délai). Ainsi, il n'y a pas de
consommation inutile de temps processeur : le travail est fait à un niveau plus
bas (dans le noyau), de façon plus économique en ressources.
La primitive select
La première implémentation d'un outil de sélection sous UNIX est l'appel système
select (BSD). Malheureusement, son interface est devenue inadaptée aux
situations où le nombre de descripteurs utilisés par le programme est très grand,
ce qui peut arriver facilement avec un serveur de fichiers : les ensembles
fd_set sont limités à FD_SETSIZE descripteurs (1024 sous Linux) et doivent
être reconstruits à chaque appel. Nous fournissons à la primitive select :
- les descripteurs que nous voulons scruter : le premier paramètre
nfdsvaut le numéro du plus grand descripteur qui nous intéresse plus un, et trois ensembles de typefd_setdésignent les descripteurs surveillés ; - les conditions de réveil sur chaque descripteur (prêt en lecture, prêt en écriture, condition exceptionnelle comme l'arrivée de données hors bande) ;
- combien de temps nous voulons attendre (
NULLpour une attente infinie).
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_ZERO(fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
La fonction modifie les ensembles passés en paramètre pour indiquer, pour chaque
descripteur, s'il est prêt en lecture, en écriture ou si la condition
exceptionnelle a eu lieu, et retourne le nombre de descripteurs prêts (0 en cas
d'expiration du délai, -1 en cas d'erreur). Cette information nous permet
ensuite d'appeler read ou write sur les bons descripteurs.
La primitive poll
La primitive poll fournit un service proche de select avec une autre forme
d'interface : un tableau de structures struct pollfd (descripteur, événements
demandés, événements obtenus). Cette interface est adaptée quand le nombre de
descripteurs ouverts par le processus est très grand mais que l'on ne s'intéresse
qu'à un petit nombre de ceux-ci, et elle n'a pas de limite liée à FD_SETSIZE.
#include <poll.h>
struct pollfd {
int fd; /* descripteur */
short events; /* événements demandés : POLLIN, POLLOUT... */
short revents; /* événements obtenus */
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout); /* timeout en ms, -1 : infini */
Au-delà de select et poll
select et poll ont un coût proportionnel au nombre de descripteurs surveillés
à chaque appel. Pour les serveurs gérant des milliers de connexions, Linux
propose epoll (epoll_create1, epoll_ctl, epoll_wait) et les BSD
proposent kqueue : l'ensemble surveillé est enregistré une fois dans le noyau,
qui ne renvoie ensuite que les descripteurs prêts.