Toute la vérité sur Linux Epoll

Eh bien, ou presque tous ...



Je crois que le problème sur l'Internet moderne est une surabondance d'informations de qualité différente. Trouver du matériel sur un sujet d'intérêt n'est pas un problème; le problème est de distinguer le bon matériel du mauvais matériel si vous avez peu d'expérience dans ce domaine. J'observe une image quand il y a beaucoup d'informations de synthèse "au sommet" (presque au niveau d'une simple énumération), très peu d'articles approfondis et pas d'articles de transition du simple au complexe. Néanmoins, c'est la connaissance des caractéristiques d'un mécanisme particulier qui nous permet de faire un choix éclairé lors du développement.


Dans l'article, j'essaierai de révéler quelle est la différence fondamentale entre epoll et d'autres mécanismes, ce qui le rend unique, ainsi que de citer des articles que vous avez juste besoin de lire pour mieux comprendre les possibilités et les problèmes d' epoll .


Tout le monde peut manier une hache, mais il faut un vrai guerrier pour lui faire chanter la mélodie de mêlée.

Je suppose que le lecteur connaît bien epoll , au moins lisez la page de manuel. On en a assez écrit sur epoll , poll , select pour que tous ceux qui développent sous Linux en aient entendu parler au moins une fois.


Beaucoup de fd


Lorsque les gens parlent d' epoll, j'entends essentiellement la thèse selon laquelle «ses performances sont meilleures quand il y a beaucoup de descripteurs de fichiers».


Je veux juste poser une question - combien coûte combien? Combien de connexions sont nécessaires et, surtout, dans quelles conditions epoll commencera-t-il à apporter des gains de performances tangibles?


Pour ceux qui ont étudié epoll (il y a beaucoup de matériel, y compris des articles scientifiques), la réponse est évidente - il vaut mieux si et seulement si le nombre de composés "en attente d'un événement" dépasse considérablement le nombre de "prêts pour le traitement". La marque de la quantité, lorsque le gain devient si important qu'il n'y a tout simplement pas d'urine pour ignorer ce fait, les composés 10k sont considérés [4].


L'hypothèse selon laquelle la plupart des connexions seront en attente provient de la logique sonore et de la surveillance de la charge des serveurs en cours d'utilisation.


Si le nombre de composés actifs vise le nombre total, il n'y aura aucun gain il n'y aura pas de gain significatif, un gain significatif est dû et uniquement au fait que epoll ne retourne que les descripteurs nécessitant une attention, et poll renvoie tous les descripteurs qui ont été ajoutés pour observation.


Évidemment, dans ce dernier cas, nous passons du temps à parcourir tous les descripteurs + les frais généraux de copie d'un tableau d'événements à partir du noyau.


En effet, dans la mesure de performance initiale, qui était attachée au patch [9], ce point n'est pas souligné et on ne peut que le deviner par la présence de l'utilitaire deadcon mentionné dans l'article (malheureusement, le code de l'utilitaire pipetest.c est perdu). En revanche, dans d'autres sources [6, 8], il est très difficile de ne pas le remarquer, car ce fait se prolonge pratiquement.


La question se pose immédiatement, mais que se passe-t-il maintenant s'il n'est pas prévu de desservir un tel nombre de descripteurs de fichiers epoll , pour ainsi dire, et n'est pas nécessaire?


Malgré le fait qu'epoll ait été initialement créé spécifiquement pour de telles situations [5, 8, 9], c'est loin d'être la seule différence entre epoll .


EPOLLET


Pour commencer, nous allons examiner la différence entre les déclencheurs déclenchés par front et les déclencheurs déclenchés par niveau. Il y a une très bonne déclaration à ce sujet dans l'article Interruptions déclenchées par niveau déclenchées contre Edge déclenchées - Venkatesh Yadav :


Interruption de niveau, c'est comme un enfant. Si le bébé pleure, vous devez abandonner tout ce que vous avez fait et courir vers le bébé pour le nourrir. Ensuite, vous remettez le bébé dans le berceau. S'il pleure encore, vous ne le laisserez nulle part, mais vous essayerez de le calmer. Et pendant que l'enfant pleure, vous ne le quitterez pas un instant, et vous ne retournerez au travail que lorsqu'il se calmera. Mais disons que nous sommes sortis dans le jardin (interruption désactivée) lorsque l'enfant a commencé à pleurer, puis lorsque vous êtes rentré chez vous (interruption activée) la première chose que vous faites est d'aller voir l'enfant. Mais vous ne saurez jamais qu'il pleurait pendant que vous étiez dans le jardin.

L'interruption sur le devant est comme une nounou électronique pour parents sourds. Dès que l'enfant commence à pleurer sur l'appareil, un voyant rouge s'allume et s'allume jusqu'à ce que vous appuyiez sur le bouton. Même si l'enfant a commencé à pleurer, mais s'est rapidement arrêté et s'est endormi, vous saurez toujours que l'enfant pleurait. Mais s'il a commencé à pleurer et que vous avez appuyé sur le bouton (confirmation de l'interruption), la lumière ne s'allumera pas même s'il continue de pleurer. Le niveau sonore dans la pièce doit baisser puis remonter pour que la lumière s'allume.

Si l' epoll (ainsi que poll / select ) est déverrouillé dans le comportement déclenché par le niveau si le descripteur est dans l'état spécifié et sera considéré comme actif jusqu'à ce que cet état soit effacé, le déclenchement sur front est déverrouillé uniquement en changeant l'état ordonné donné actuel.


Cela vous permet de gérer l'événement plus tard, et pas immédiatement après sa réception (presque une analogie directe avec la moitié supérieure et la moitié inférieure du gestionnaire d'interruption).


Exemple spécifique avec epoll:


Niveau déclenché


  • poignée ajoutée à epoll avec drapeau EPOLLIN
  • epoll_wait () bloque en attendant un événement
  • écrire dans le descripteur de fichier 19 octets
  • epoll_wait () se déverrouille avec l'événement EPOLLIN
  • nous ne faisons rien avec les données qui sont venues
  • epoll_wait () se déverrouille à nouveau avec l'événement EPOLLIN

Et cela continuera jusqu'à ce que nous comptions ou réinitialisions complètement les données du descripteur.


Déclenchement sur front


  • nouvelle poignée ajoutée à epoll avec les drapeaux EPOLLIN | EPOLLET
  • epoll_wait () bloque en attendant un événement
  • écrire dans le descripteur de fichier 19 octets
  • epoll_wait () se déverrouille avec l'événement EPOLLIN
  • nous ne faisons rien avec les données qui sont venues
  • epoll_wait () est bloqué en attendant un nouvel événement
  • écrire encore 19 octets dans le descripteur de fichier
  • epoll_wait () se déverrouille avec le nouvel événement EPOLLIN
  • epoll_wait () est bloqué en attendant un nouvel événement

exemple simple: epollet_socket.c


Ce mécanisme est conçu pour empêcher le retour de epoll_wait () en raison d'un événement qui est déjà en cours de traitement.


Si, dans le cas du niveau, lors de l'appel à epoll_wait (), le noyau vérifie si fd est dans cet état, alors edge saute cette vérification et met immédiatement le processus appelant en état de veille.


EPOLLET lui - même est ce qui fait d' epoll O (1) un multiplexeur d'événements.


Il est nécessaire d'expliquer EAGAIN et EPOLLET - la recommandation avec EAGAIN n'est pas de traiter le flux d'octets, le danger dans ce dernier cas ne survient que si vous n'avez pas lu le descripteur à la fin, et de nouvelles données ne sont pas arrivées. Ensuite, la queue se bloque dans le descripteur, mais vous ne recevrez pas de nouvelle notification. Avec accept (), la situation est juste différente, vous devez continuer jusqu'à ce que accept () renvoie EAGAIN , seulement dans ce cas le bon fonctionnement est garanti.


// TCP socket (byte stream) //  fd    EPOLLIN      int len = read(fd, buffer, BUFFER_LEN); if(len < BUFFER_LEN) { //   } else { //         //  -       epoll_wait, //      } 

  // accept //  listenfd    EPOLLIN      event.events = EPOLLIN | EPOLLERR; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event); sleep(5); //       >1  //   while(epoll_wait()) { newfd = accept(listenfd, ...); //      //        //  epoll_wait    listenfd    } //   while(epoll_wait()) { while((newfd = accept(...)) > 0) { //  -  } if(newfd == -1 && errno = EAGAIN) { //       //       } } 

Avec cette propriété, la famine suffit:


  • les paquets arrivent au descripteur
  • lire les paquets dans le tampon
  • un autre paquet vient
  • lire les paquets dans le tampon
  • vient une petite portion
  • ...

Ainsi , nous ne recevrons pas EAGAIN prochainement, mais nous ne le recevrons peut-être pas du tout.


Ainsi, d'autres descripteurs de fichiers ne reçoivent pas de temps pour le traitement, et nous sommes occupés à lire de petites portions de données qui arrivent constamment.


tonnerre nerd troupeau


Pour passer au dernier indicateur, vous devez comprendre pourquoi il a été créé et l'un des problèmes rencontrés par les développeurs avec l'évolution de la technologie et des logiciels.


Problème de troupeau tonnerre


Problème de troupeau de tonnerre

Imaginez un grand nombre de processus en attente d'un événement. Si un événement se produit, ils seront réveillés et la lutte pour les ressources commencera, bien qu'un seul processus soit nécessaire pour gérer le traitement ultérieur de l'événement. Le reste des processus dormira à nouveau.

Terminologie informatique - Vasily Alekseenko

Dans ce cas, nous nous intéressons au problème de l' acceptation () et de la lecture () distribuées sur les flux conjointement avec epoll .


accepter


En fait, avec un appel bloquant pour accepter (), il n'y a pas eu de problème depuis longtemps. Le noyau veillera à ce qu'un seul processus ait été déverrouillé pour cet événement et que toutes les connexions entrantes soient sérialisées.


Mais avec epoll, une telle astuce ne fonctionnera pas. Si nous avons écouté () sur une socket non bloquante, lorsque la connexion est établie, tous les epoll_wait () attendront l'événement de ce descripteur.


Bien sûr, accept () ne pourra faire qu'un seul thread, les autres recevront EAGAIN , mais c'est un gaspillage de ressources.


De plus, EPOLLET ne nous aide pas non plus, car nous ne savons pas exactement combien de connexions se trouvent dans la file d'attente de connexions ( backlog ). Comme nous nous en souvenons, lorsque vous utilisez EPOLLET , le traitement des sockets doit continuer jusqu'à ce qu'il revienne avec le code d' erreur EAGAIN , il y a donc une chance que tous accept () soient traités par un seul thread et que les autres ne fonctionnent pas.


Et cela nous conduit à nouveau à une situation où le ruisseau voisin a été réveillé en vain.


Nous pouvons également obtenir un autre type de famine - nous n'aurons qu'un seul thread chargé, et le reste ne recevra pas de connexions pour le traitement.


EPOLLONESHOT


Avant la version 4.5, la seule façon correcte de traiter le epoll distribué en un descripteur listen () non bloquant avec le prochain appel accept () était de définir l'indicateur EPOLLONESHOT , ce qui nous a conduit à nouveau à accepter () uniquement en cours de traitement dans un thread à la fois.


En bref - si EPOLLONESHOT est utilisé , l' événement associé à un descripteur particulier ne se déclenchera qu'une seule fois, après quoi il sera nécessaire de réarmer les drapeaux en utilisant epoll_ctl () .


EPOLLEXCLUSIVE


Ici EPOLLEXCLUSIVE et déclenché par niveau vient à notre aide.


EPOLLEXCLUSIVE déverrouille un epoll_wait () à la fois pour un événement.


Le schéma est assez simple (en fait non):


  • Nous avons N threads en attente d'un événement de connexion
  • Le premier client se connecte à nous
  • Le thread 0 sera dispersé et commencera le traitement, les autres threads resteront bloqués
  • Un deuxième client se connecte à nous, si le thread 0 est toujours occupé par le traitement, alors le thread 1 est déverrouillé
  • Nous continuons jusqu'à ce que le pool de threads soit épuisé (personne ne s'attend à un événement sur epoll_wait () )
  • Un autre client se connecte à nous
  • Et son traitement recevra le premier thread, qui appellera epoll_wait ()
  • Le deuxième thread recevra le deuxième client, qui appellera epoll_wait ()

Ainsi, toute la maintenance est répartie uniformément sur les flux.


 $ ./epollexclusive --help -i, --ip=ADDR specify ip address -p, --port=PORT specify port -n, --threads=NUM specify number of threads to use #    -  n*8 -t, --thunder not adding EPOLLEXCLUSIVE #     thunder herd -h, --help prints this message $ sudo taskset -c 0-7 ./epollexclusive -i 10.56.75.201 -p 40000 -n 8 2>&1 

exemple de code: epollexclusive.c (ne fonctionnera qu'avec la version du noyau à partir de 4.5)


Nous obtenons un modèle de pré-fourche sur epoll. Ce schéma est bien applicable pour les connexions TCP de courte durée .


lire


Mais avec read () dans le cas du streaming d'octets, EPOLLEXCLUSIVE , comme EPOLLET, ne nous aidera pas.


Pour des raisons évidentes, sans EPOLLEXCLUSIVE, nous ne pouvons pas utiliser du tout déclenché par niveau. Avec EPOLLEXCLUSIVE, tout ne va pas mieux, car nous pouvons obtenir un package réparti sur les flux, en plus avec un ordre inconnu d'octets arrivés.


Avec EPOLLET, la situation est la même.


Et ici, EPOLLONESHOT avec réinitialisation à la fin des travaux sera la solution . Ainsi, dès qu'un thread fonctionnera avec ce descripteur de fichier et ce tampon:


  • poignée ajoutée à epoll avec les drapeaux EPOLLONESHOT | EPOLLET
  • en attente sur epoll_wait ()
  • lire du socket au tampon jusqu'à ce que read () renvoie EAGAIN
  • réinitialiser avec les drapeaux EPOLLONESHOT | EPOLLET

struct epoll_event


 typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; 

Cet article est peut-être le seul dans mon article mon IMHO personnel. La possibilité d'utiliser un pointeur ou un nombre est utile. Par exemple, l'utilisation d'un pointeur lors de l'utilisation d'epoll vous permet de faire une astuce comme celle-ci:


 #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );}) struct epoll_client { /** some usefull associated data...*/ struct epoll_event event; }; struct epoll_client* to_epoll_client(struct epoll_event* event) { return container_of(event, struct epoll_client, event); } struct epoll_client ec; ... epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ec.e); ... epoll_wait (efd, events, 1, -1); struct epoll_client* ec_ = to_epoll_client(events[0].data.ptr); 

Je pense que tout le monde sait d'où vient cette technique.


Conclusion


J'espère que nous avons pu ouvrir le sujet d' epoll . Ceux qui veulent utiliser ce mécanisme consciemment, ont juste besoin de lire les articles de la liste de références [1, 2, 3, 5].


Sur la base de ce matériel (ou, mieux encore, en lisant attentivement les matériaux des références), vous pouvez créer un serveur pré-fork multi-thread (génération avancée du processus) sans verrouillage (sans blocage) ou réviser les stratégies existantes en fonction des propriétés spéciales d' epoll () ).


epoll est l' un des mécanismes uniques que les gens qui ont choisi leurs chemins de programmation Linux doivent connaître, car ils donnent un sérieux avantage sur les autres systèmes d'exploitation), et, peut-être, refuseront la multiplateforme pour un cas particulier (laissez-le fonctionner uniquement sous Linux mais le fera bien).


Raisonnement sur la "spécificité" du problème


Avant que quelqu'un ne parle de la spécificité de ces indicateurs et modèles d'utilisation, je veux poser une question:


"Mais n'est-ce rien que nous essayons de discuter de la spécificité du mécanisme qui a été créé initialement pour des tâches spécifiques [9, 11]? Ou est-ce que nous entretenons même des connexions 1k est une tâche quotidienne pour un programmeur?"


Je ne comprends pas le concept de «spécificité des tâches», cela me rappelle toutes sortes de cris sur l'utilité et la futilité des différentes disciplines enseignées. En nous laissant ainsi raisonner, nous nous réservons le droit de décider pour autrui quelles informations leur sont utiles et lesquelles sont inutiles, tout en ne participant pas au processus éducatif dans son ensemble.


Pour les sceptiques, quelques liens:


Augmentation des performances avec SO_REUSEPORT dans NGINX 1.9.1 - VBart
Apprendre de la licorne: le troupeau tonnerre accepte () sans problème - Chris Siebenmann
Sérialisation accepter (), AKA Thundering Herd, AKA le problème Zeeg - Roberto De Ioris
Comment le mode EPOLLEXCLUSIVE d'epoll interagit-il avec le déclenchement de niveau?


Les références


  1. Select est fondamentalement cassé - Marek
  2. Epoll est fondamentalement cassé 1/2 - Marek
  3. Epoll est fondamentalement brisé 2/2 - Marek
  4. Le problème du C10K - Dan Kegel
  5. Poll vs Epoll, encore une fois - Jacques Mattheij
  6. epoll - Fonction de notification d'événements d'E / S - The Mann
  7. La méthode de la folie d'Epoll - Cindy Sridharan

Repères


  1. https://www.kernel.org/doc/ols/2004/ols2004v1-pages-215-226.pdf
  2. http://lse.sourceforge.net/epoll/index.html
  3. https://mvitolin.wordpress.com/2015/12/05/endurox-testing-epollexclusive-flag/

L'évolution d'Epoll


  1. https://lwn.net/Articles/13918/
  2. https://lwn.net/Articles/520012/
  3. https://lwn.net/Articles/520198/
  4. https://lwn.net/Articles/542629/
  5. https://lwn.net/Articles/633422/
  6. https://lwn.net/Articles/637435/

Postscript


Un grand merci à Sergey ( dlinyj ) et Peter Ovchenkov pour leurs précieuses discussions, commentaires et aide!

Source: https://habr.com/ru/post/fr416669/


All Articles