Aller au contenu principal

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 nfds vaut le numéro du plus grand descripteur qui nous intéresse plus un, et trois ensembles de type fd_set dé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 (NULL pour 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.