Lors de la conception d'applications réseau hautes performances avec des sockets non bloquants, il est important de décider quelle méthode de surveillance des événements réseau nous utiliserons. Il y en a plusieurs, et chacun est bon et mauvais à sa manière. Le choix de la bonne méthode peut être critique pour l'architecture de votre application.
Dans cet article, nous considérerons:
- sélectionnez ()
- poll ()
- epoll ()
- libevent
Utilisation de select ()
L'ancien, éprouvé au fil des ans, le travailleur acharné select () a été créé à l'époque où les «prises» étaient appelées «
prises Berkeley ». Cette méthode n'était pas incluse dans la toute première spécification de ces sockets Berkeley elles-mêmes, car à l'époque il n'existait toujours pas de concept d'E / S non bloquantes. Mais quelque part dans les années 80, elle est apparue, et avec elle, sélectionnez (). Depuis lors, rien n'a changé de manière significative dans son interface.
Pour utiliser select (), le développeur doit initialiser et remplir plusieurs structures fd_set avec des descripteurs et des événements qui doivent être surveillés, puis appeler select (). Un code typique ressemble à ceci:
fd_set fd_in, fd_out; struct timeval tv;
Lorsque select () a été conçu, personne ne s'attendait à ce qu'à l'avenir, nous devions écrire des applications multithreads desservant des milliers de connexions. Select () présente plusieurs inconvénients importants qui le rendent mal adapté pour travailler sur de tels systèmes. Les principaux sont:
- select modifie les structures fd_sets qui lui sont transmises, afin qu'aucune d'entre elles ne puisse être réutilisée. Même si vous n'avez rien à changer (par exemple, après avoir reçu une donnée, vous voulez en obtenir plus), les structures fd_sets devront être réinitialisées. Eh bien, ou copiez à partir d'une sauvegarde précédemment enregistrée à l'aide de FD_COPY. Et cela devra être fait encore et encore, avant chaque appel de sélection.
- Pour savoir exactement quel descripteur a généré l'événement, vous devez tous les interroger manuellement avec FD_ISSET. Lorsque vous surveillez 2000 descripteurs et que l'événement s'est produit uniquement pour l'un d'entre eux (qui, selon la loi de la méchanceté, sera le dernier de la liste) - vous gaspillerez beaucoup de ressources processeur.
- Est-ce que je viens de mentionner 2000 descripteurs? J'en ai été excité. select ne prend pas beaucoup en charge. Eh bien, au moins sur Linux ordinaire, avec le noyau habituel. Le nombre maximal de descripteurs observés simultanément est limité par la constante FD_SETSIZE, qui est rigidement égale à 1024 sous Linux. Certains systèmes d'exploitation vous permettent d'implémenter un piratage en remplaçant la valeur FD_SETSIZE avant d'inclure le fichier d'en-tête sys / select.h, mais ce piratage ne fait pas partie d'un standard commun. Le même Linux l'ignorera.
- Vous ne pouvez pas travailler avec des descripteurs d'un ensemble observable d'un autre thread. Imaginez un thread exécutant le code ci-dessus. Il a donc démarré et attend les événements dans son select (). Imaginez maintenant que vous avez un autre thread qui surveille la charge globale du système, et maintenant il a décidé que les données du socket sock1 n'étaient pas arrivées trop longtemps et qu'il était temps de rompre la connexion. Comme ce socket peut être réutilisé pour servir de nouveaux clients, il serait bon de le fermer correctement. Mais le premier thread observe ce descripteur en ce moment. Que se passera-t-il si nous le fermons tout de même? Oh, la documentation a une réponse à cette question et vous ne l'aimerez pas: "Si le handle observé avec select () est fermé par un autre thread, vous obtiendrez un comportement indéfini."
- Le même problème apparaît lorsque vous essayez d'envoyer des données via sock1. Nous n'enverrons rien tant que select n'aura pas terminé son travail.
- Le choix des événements que nous pouvons surveiller est assez limité. Par exemple, pour déterminer qu'un socket distant a été fermé, vous devez, premièrement, surveiller les événements d'arrivée de données dessus, et deuxièmement, essayer de lire ces données (read renverra 0 pour le socket fermé). Cela peut toujours être considéré comme acceptable lors de la lecture de données à partir d'un socket (lire 0 - le socket est fermé), mais que se passe-t-il si notre tâche actuelle envoie des données à ce socket et aucune lecture de données à partir de ce moment?
- select vous impose un fardeau inutile pour calculer le «plus grand descripteur» et le passer comme paramètre séparé
Bien sûr, tout ce qui précède n'est pas une nouvelle. Les développeurs de systèmes d'exploitation sont depuis longtemps conscients de ces problèmes et nombre d'entre eux ont été pris en compte lors de la conception de la méthode d'interrogation. À ce stade, vous pouvez vous demander, pourquoi étudions-nous même l'histoire ancienne maintenant, et y a-t-il aujourd'hui des raisons d'utiliser l'ancienne sélection? Oui, il y a deux raisons. Pas le fait qu'ils vous seront utiles un jour, mais pourquoi ne pas les découvrir.
La première raison est la portabilité. select () est avec nous depuis un million d'années. Peu importe ce que la jungle des plates-formes matérielles et logicielles vous apporte, s'il y a un réseau là-bas, il y en aura. Il n'y a peut-être pas d'autres méthodes, mais la sélection sera presque garantie. Et ne pensez pas que je tombe maintenant dans la sénilité sénile et me souviens de quelque chose comme des cartes perforées et ENIAC, non. Il n'y a pas de méthode d'interrogation plus moderne
, par exemple, dans Windows XP . Mais sélectionnez est.
La deuxième raison est plus exotique et liée au fait que select peut (théoriquement) fonctionner avec des délais d'attente de l'ordre de la nanoseconde (si le matériel le permet), tandis que poll et epoll ne prennent en charge qu'une précision en millisecondes. Cela ne devrait pas jouer un rôle spécial sur les ordinateurs de bureau ordinaires (ou même les serveurs), où vous n'avez toujours pas de minuterie matérielle de précision en nanosecondes. Mais toujours dans le monde, il existe des systèmes en temps réel qui ont de tels temporisateurs. Donc, je vous en prie, lorsque vous écrivez le firmware d'un réacteur nucléaire ou d'une fusée - ne soyez pas trop paresseux pour mesurer le temps en nanosecondes. Tu sais, je veux vivre.
Le cas décrit ci-dessus est probablement le seul dans lequel vous n'avez vraiment pas le choix de ce que vous devez utiliser (seule la sélection est appropriée). Cependant, si vous écrivez une application régulière pour travailler sur du matériel ordinaire, et que vous fonctionnerez avec un nombre adéquat de sockets (des dizaines, des centaines - et pas plus), alors la différence de sondage et de performances sélectionnées ne sera pas perceptible, donc le choix sera basé sur d'autres facteurs.
Sondage avec poll ()
poll est une nouvelle méthode d'interrogation des sockets, créée après que les gens ont commencé à écrire des services réseau volumineux et fortement chargés. Il est beaucoup mieux conçu et ne souffre pas de la plupart des inconvénients de la méthode de sélection. Dans la plupart des cas, lors de l'écriture d'applications modernes, vous choisirez entre l'utilisation de poll et epoll / libevent.
Pour utiliser poll, un développeur doit initialiser les membres de la structure pollfd avec des descripteurs et des événements observables, puis appeler poll ().
Un code typique ressemble à ceci:
Le sondage a été créé pour résoudre les problèmes de la méthode select, voyons comment cela s'est avéré:
- Il n'y a pas de limite au nombre de descripteurs observés; plus de 1024 peuvent être surveillés
- La structure pollfd n'est pas modifiée, ce qui permet de la réutiliser entre les appels à poll () - il suffit de réinitialiser le champ revents.
- Les événements observés sont mieux structurés. Par exemple, vous pouvez déterminer si un client distant est déconnecté sans avoir à lire les données du socket.
Nous avons déjà évoqué les lacunes de la méthode de sondage: elle n'est pas disponible sur certaines plateformes, comme Windows XP. Depuis Vista, il existe, mais s'appelle WSAPoll. Le prototype est le même, donc pour le code indépendant de la plate-forme, vous pouvez écrire un remplacement, comme:
#if defined (WIN32) static inline int poll( struct pollfd *pfd, int nfds, int timeout) { return WSAPoll ( pfd, nfds, timeout ); } #endif
Eh bien, la précision des délais d'attente est de 1 ms, ce qui ne suffira pas très rarement. Cependant, le sondage présente d'autres inconvénients:
- Comme pour l'utilisation de select, il est impossible de déterminer quels descripteurs ont généré les événements sans passer par toutes les structures observées et vérifier les champs revents qui s'y trouvent. Pire encore, il est également implémenté dans le noyau du système d'exploitation.
- Comme avec select, il n'y a aucun moyen de modifier dynamiquement l'ensemble d'événements observé
Cependant, tout ce qui précède peut être considéré comme relativement insignifiant pour la plupart des applications clientes. L'exception est probablement uniquement les protocoles p2p, où chacun des clients peut être associé à des milliers d'autres. Ces problèmes peuvent être ignorés même par la plupart des applications serveur. Par conséquent, le sondage devrait être votre préférence par défaut par rapport à la sélection, sauf si l'une des deux raisons ci-dessus vous limite.
Pour l'avenir, je dirai que le sondage est préférable même par rapport à l'epoll plus moderne (discuté ci-dessous) dans les cas suivants:
- Vous voulez écrire du code multiplateforme (epoll est uniquement sur Linux)
- Vous n'avez pas besoin de surveiller plus de 1000 sockets (epoll ne vous donnera rien de significatif dans ce cas)
- Vous devez surveiller plus de 1000 sockets, mais le temps de connexion avec chacun d'eux est très faible (dans ces cas, les performances de poll et epoll seront très proches - le gain de l'attente de moins d'événements dans epoll sera barré par le surcoût de leur ajout / suppression)
- Votre application n'est pas conçue pour modifier les événements d'un thread pendant qu'un autre les attend (ou vous n'en avez pas besoin)
Sondage avec epoll ()
epoll est la méthode la plus récente et la meilleure pour attendre des événements sur Linux (et uniquement sur Linux). Eh bien, ce n'est pas que le "plus récent" est direct - il est au cœur depuis 2002. Il diffère du sondage et sélectionne en ce qu'il fournit une API pour ajouter / supprimer / modifier la liste des descripteurs et événements observés.
L'utilisation d'epoll nécessite des préparations un peu plus approfondies. Le développeur doit:
- Créez un descripteur epoll en appelant epoll_create
- Initialisez la structure epoll_event avec les événements nécessaires et les pointeurs vers les contextes de connexion. Le «contexte» ici peut être n'importe quoi, epoll passe juste cette valeur dans les événements retournés
- Appelez epoll_ctl (... EPOLL_CTL_ADD) pour ajouter un handle à la liste des observables
- Appelez epoll_wait () pour attendre les événements (nous indiquons exactement combien d'événements nous voulons recevoir à la fois, par exemple 20). Contrairement aux méthodes précédentes, nous obtenons ces événements séparément, et non dans les propriétés des structures d'entrée. Si nous observons 200 descripteurs et que 5 d'entre eux ont reçu de nouvelles données - epoll_wait ne renverra que 5 événements. Si 50 événements se produisent, les 20 premiers nous seront retournés et les 30 restants attendront le prochain appel, ils ne seront pas perdus
- Traiter les événements reçus. Ce sera un traitement relativement rapide, car nous ne regardons pas les descripteurs où rien ne s'est passé
Un code typique ressemble à ceci:
Commençons par les défauts d'epoll - ils sont évidents d'après le code. Cette méthode est plus difficile à utiliser, vous devez écrire plus de code, elle fait plus d'appels système.
Les avantages sont également évidents:
- epoll renvoie une liste des seuls descripteurs pour lesquels les événements observés se sont réellement produits. Vous n'avez pas besoin de parcourir des milliers de structures à la recherche de celle, peut-être celle où l'événement attendu a fonctionné.
- Vous pouvez associer un contexte significatif à chaque événement observé. Dans l'exemple ci-dessus, nous avons utilisé un pointeur vers un objet de la classe de connexion pour cela - cela nous a sauvé une autre recherche potentielle pour un tableau de connexions.
- Vous pouvez ajouter ou supprimer des sockets de la liste à tout moment. Vous pouvez même modifier les événements observés. Tout fonctionnera correctement, cela est officiellement pris en charge et documenté.
- Vous pouvez démarrer plusieurs threads en attente d'événements à partir de la même file d'attente à l'aide de epoll_wait. Quelque chose qui ne peut en aucun cas être fait avec select / poll.
Mais vous devez également vous rappeler qu'epoll n'est pas «un sondage amélioré à tous les niveaux». Il présente des inconvénients par rapport au sondage:
- La modification des drapeaux d'événements (par exemple, le passage de READ à WRITE) nécessite un appel système epoll_ctl supplémentaire, tandis que pour le sondage, vous changez simplement le masque de bits (complètement en mode utilisateur). Passer de 5 000 sockets de lecture à écriture nécessitera 5 000 appels système et commutateurs de contexte pour epoll, tandis que pour interrogation, ce sera une opération de bits triviale dans une boucle.
- Pour chaque nouvelle connexion, vous devez appeler accept () et epoll_ctl () sont deux appels système. Si vous utilisez le sondage, il n'y aura qu'un seul appel. Avec une durée de vie de connexion très courte, cela peut faire la différence.
- epoll n'est disponible que sur Linux. D'autres systèmes d'exploitation ont des mécanismes similaires, mais toujours pas complètement identiques. Vous ne pourrez pas écrire de code avec epoll pour qu'il se compile et fonctionne, par exemple, sur FreeBSD.
- Il est difficile d'écrire du code parallèle très chargé. De nombreuses applications n'ont pas besoin d'une telle approche fondamentale, car leur niveau de charge est facilement traité à l'aide de méthodes plus simples.
Par conséquent, epoll ne doit être utilisé que lorsque toutes les conditions suivantes sont remplies:
- Votre application utilise un pool de threads pour gérer les connexions réseau. Le gain d'epoll dans une application monothread sera négligeable, et vous ne devriez pas vous soucier de l'implémentation.
- Vous vous attendez à un nombre relativement important de connexions (de 1000 et plus). Sur un petit nombre de sockets observées, epoll ne donnera pas de gain de performances, et s'il y a littéralement quelques sockets, il peut même ralentir.
- Vos connexions vivent relativement longtemps. Dans une situation où une nouvelle connexion transfère seulement quelques octets de données et se ferme juste là - l'interrogation fonctionnera plus rapidement, car elle devra effectuer moins d'appels système pour la traiter.
- Vous avez l'intention d'exécuter votre code sous Linux et uniquement sous Linux.
Si un ou plusieurs éléments échouent, envisagez d'utiliser poll ou libevent.
libevent
libevent est une bibliothèque qui encapsule les méthodes d'interrogation répertoriées dans cet article (ainsi que certaines autres) dans une API unifiée. L'avantage ici est qu'une fois que vous avez écrit le code, vous pouvez le construire et l'exécuter sur différents systèmes d'exploitation. Néanmoins, il est important de comprendre que libevent n'est qu'un wrapper, à l'intérieur duquel toutes les méthodes ci-dessus fonctionnent, avec tous leurs avantages et inconvénients. libevent ne forcera pas select à écouter plus de 1024 sockets, et epoll ne modifiera pas la liste des événements sans appel système supplémentaire. Il est donc toujours important de connaître les technologies sous-jacentes.
La nécessité de prendre en charge différentes méthodes d'interrogation rend l'API de la bibliothèque libevent plus complexe. Mais encore, son utilisation est plus facile que d'écrire manuellement deux moteurs de sélection d'événement différents pour, par exemple, Linux et FreeBSD (en utilisant epoll et kqueue).
Pensez à utiliser libevent lorsque vous combinez deux événements:
- Vous avez examiné les méthodes de sélection et d'interrogation et elles n'ont certainement pas fonctionné pour vous.
- Vous devez prendre en charge plusieurs systèmes d'exploitation