Na ja, oder fast alle ...

Ich glaube, dass das Problem im modernen Internet eine Überfülle an Informationen unterschiedlicher Qualität ist. Das Finden von Material zu einem interessanten Thema ist kein Problem. Das Problem besteht darin, gutes Material von schlechtem Material zu unterscheiden, wenn Sie wenig Erfahrung auf diesem Gebiet haben. Ich beobachte ein Bild, wenn es viele Übersichtsinformationen "oben" gibt (fast auf der Ebene einer einfachen Auflistung), sehr wenige ausführliche Artikel und keine Übergangsartikel von einfach zu komplex. Dennoch ist es die Kenntnis der Merkmale eines bestimmten Mechanismus, die es uns ermöglicht, während der Entwicklung eine fundierte Entscheidung zu treffen.
In dem Artikel werde ich versuchen aufzuzeigen, was der grundlegende Unterschied zwischen Epoll und anderen Mechanismen ist, was es einzigartig macht, und Artikel zitieren, die Sie nur lesen müssen, um die Möglichkeiten und Probleme von Epoll besser zu verstehen.
Jeder kann eine Axt führen, aber es braucht einen wahren Krieger, um eine Nahkampfmelodie zu singen.
Ich gehe davon aus, dass der Leser mit epoll vertraut ist, zumindest die Manpage lesen. Es wurde genug über epoll , poll , select geschrieben, damit jeder, der sich unter Linux entwickelt, mindestens einmal davon gehört hat.
Viel fd
Wenn Leute über Epoll sprechen, höre ich im Grunde die These, dass seine "Leistung besser ist, wenn es viele Dateideskriptoren gibt".
Ich möchte nur eine Frage stellen - wie viel ist wie viel? Wie viele Verbindungen werden benötigt und vor allem unter welchen Bedingungen wird epoll anfangen, spürbare Leistungssteigerungen zu erzielen?
Für diejenigen, die Epoll studiert haben (es gibt viel Material, einschließlich wissenschaftlicher Artikel), liegt die Antwort auf der Hand - es ist genau dann besser, wenn die Anzahl der Verbindungen, die auf ein Ereignis warten, die Anzahl der verarbeitungsfertigen Verbindungen erheblich übersteigt. Wenn der Gewinn so signifikant wird, dass es einfach keinen Urin gibt, der diese Tatsache ignoriert, werden 10k-Verbindungen berücksichtigt [4].
Die Annahme, dass die meisten Verbindungen anstehen, beruht auf einer soliden Logik und Lastüberwachung von Servern, die aktiv verwendet werden.
Wenn die Anzahl der Wirkstoffe die Gesamtzahl anstrebt, Es wird keinen Gewinn geben Es wird keinen signifikanten Gewinn geben, ein signifikanter Gewinn ist auf und nur zurückzuführen, weil epoll nur Deskriptoren zurückgibt, die Aufmerksamkeit erfordern, und poll alle Deskriptoren zurückgibt, die zur Beobachtung hinzugefügt wurden.
Im letzteren Fall verbringen wir natürlich Zeit damit, alle Deskriptoren + den Aufwand für das Kopieren eines Arrays von Ereignissen aus dem Kernel zu durchlaufen.
Tatsächlich wird dieser Punkt bei der anfänglichen Leistungsmessung, die dem Patch beigefügt war [9], nicht unterstrichen, und man kann nur anhand des im Artikel erwähnten Dienstprogramms deadcon raten (leider geht der Dienstprogrammcode pipetest.c verloren). Andererseits ist es in anderen Quellen [6, 8] sehr schwierig, es nicht zu bemerken, da diese Tatsache praktisch herausragt.
Die Frage stellt sich sofort, aber was ist nun, wenn nicht geplant ist, sozusagen eine solche Anzahl von Epoll- Dateideskriptoren zu bedienen, und dies nicht erforderlich ist?
Trotz der Tatsache, dass epoll ursprünglich speziell für solche Situationen erstellt wurde [5, 8, 9], ist dies bei weitem nicht der einzige Unterschied zwischen epoll .
EPOLLET
Zunächst werden wir uns den Unterschied zwischen flankengetriggerten und pegelgetriggerten Triggern ansehen. Eine gute Aussage zu diesem Thema finden Sie im Artikel Edge Triggered Vs Level Triggered Interrupts - Venkatesh Yadav :
Unterbrechung des Levels, es ist wie bei einem Kind. Wenn das Baby weint, müssen Sie alles aufgeben, was Sie getan haben, und zum Baby laufen, um es zu füttern. Dann legst du das Baby wieder in die Krippe. Wenn er wieder weint, werden Sie ihn nirgendwo lassen, aber Sie werden versuchen, ihn zu beruhigen. Und während das Kind weint, werden Sie es keinen Moment verlassen und erst dann zur Arbeit zurückkehren, wenn es sich beruhigt. Aber nehmen wir an, wir gingen in den Garten (Unterbrechung ausgeschaltet), als das Kind anfing zu weinen, und als Sie nach Hause zurückkehrten (Unterbrechung eingeschaltet), war das erste, was Sie tun, das Kind zu überprüfen. Aber Sie werden nie erfahren, dass er geweint hat, als Sie im Garten waren.
Eine Unterbrechung an der Front ist wie ein elektronisches Kindermädchen für gehörlose Eltern. Sobald das Kind auf dem Gerät zu weinen beginnt, leuchtet ein rotes Licht auf und leuchtet auf, bis Sie die Taste drücken. Selbst wenn das Kind anfing zu weinen, aber schnell stehen blieb und einschlief, werden Sie immer noch wissen, dass das Kind weinte. Aber wenn er anfing zu weinen und Sie den Knopf gedrückt haben (Bestätigung der Unterbrechung), leuchtet das Licht nicht auf, selbst wenn er weiter weint. Der Schallpegel im Raum sollte abfallen und dann wieder ansteigen, damit das Licht aufleuchtet.
Wenn der Epoll (sowie Poll / Select ) im Level-Trigger-Verhalten entsperrt ist, wenn sich der Deskriptor im angegebenen Status befindet und bis zum Löschen dieses Status als aktiv betrachtet wird, wird der Flanken-Trigger nur durch Ändern des aktuell angegebenen geordneten Status entsperrt.
Auf diese Weise können Sie das Ereignis später und nicht sofort nach dem Empfang behandeln (fast eine direkte Analogie zur oberen und unteren Hälfte des Interrupt-Handlers).
Spezifisches Beispiel mit epoll:
Level ausgelöst
- Griff zum Epoll mit Flag EPOLLIN hinzugefügt
- epoll_wait () blockiert, während auf ein Ereignis gewartet wird
- Schreiben Sie 19 Bytes in den Dateideskriptor
- epoll_wait () wird mit dem EPOLLIN- Ereignis entsperrt
- Wir machen nichts mit den Daten, die kamen
- epoll_wait () wird mit dem EPOLLIN- Ereignis erneut entsperrt
Dies wird so lange fortgesetzt, bis wir die Daten aus dem Deskriptor vollständig gezählt oder zurückgesetzt haben.
Flanke ausgelöst
- Handle mit EPOLLIN- Flags zu epoll hinzugefügt | EPOLLET
- epoll_wait () blockiert, während auf ein Ereignis gewartet wird
- Schreiben Sie 19 Bytes in den Dateideskriptor
- epoll_wait () wird mit dem EPOLLIN- Ereignis entsperrt
- Wir machen nichts mit den Daten, die kamen
- epoll_wait () ist blockiert und wartet auf ein neues Ereignis
- Schreiben Sie weitere 19 Bytes in den Dateideskriptor
- epoll_wait () wird mit dem neuen EPOLLIN- Ereignis entsperrt
- epoll_wait () ist blockiert und wartet auf ein neues Ereignis
einfaches Beispiel: epollet_socket.c
Dieser Mechanismus soll die Rückgabe von epoll_wait () aufgrund eines Ereignisses verhindern, das bereits verarbeitet wird.
Wenn im Fall von level beim Aufrufen von epoll_wait () der Kernel prüft, ob sich fd in diesem Zustand befindet, überspringt edge diese Prüfung und versetzt den aufrufenden Prozess sofort in den Ruhezustand.
EPOLLET selbst macht Epoll O (1) zu einem Multiplexer für Ereignisse.
Es ist notwendig, über EAGAIN und EPOLLET zu erklären - die Empfehlung mit EAGAIN lautet , Byte-Stream nicht zu behandeln, die Gefahr im letzteren Fall entsteht nur, wenn Sie den Deskriptor nicht bis zum Ende gelesen haben und keine neuen Daten gekommen sind. Dann hängt der Schwanz im Deskriptor, aber Sie erhalten keine neue Benachrichtigung. Mit accept () ist die Situation einfach anders. Dort müssen Sie fortfahren, bis accept () EAGAIN zurückgibt . Nur in diesem Fall ist die korrekte Operation garantiert.
// 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) { // // } }
Mit dieser Eigenschaft ist nur Hunger genug:
- Pakete kommen zum Deskriptor
- Pakete in den Puffer lesen
- Ein weiteres Paket kommt
- Pakete in den Puffer lesen
- kommt eine kleine Portion
- ...
Daher werden wir EAGAIN nicht bald erhalten, aber wir werden es möglicherweise überhaupt nicht erhalten.
Daher erhalten andere Dateideskriptoren keine Zeit für die Verarbeitung, und wir sind damit beschäftigt, ständig ankommende kleine Datenmengen zu lesen.
donnern Nerd Herde
Um zur letzten Flagge zu gelangen, müssen Sie verstehen, warum sie tatsächlich erstellt wurde und eines der Probleme, die Entwickler bei der Entwicklung von Technologie und Software hatten.
Donnerndes Herdenproblem
Problem mit der Donnerherde
Stellen Sie sich eine Vielzahl von Prozessen vor, die auf ein Ereignis warten. Wenn ein Ereignis eintritt, werden sie geweckt und der Kampf um Ressourcen beginnt, obwohl nur ein Prozess erforderlich ist, der sich mit der weiteren Verarbeitung des Ereignisses befasst. Der Rest der Prozesse wird wieder schlafen.
IT-Terminologie - Vasily Alekseenko
In diesem Fall interessiert uns das Problem von accept () und read (), die in Verbindung mit epoll über Streams verteilt sind.
akzeptieren
Tatsächlich gab es bei einem blockierenden Aufruf von accept () lange Zeit keine Probleme. Der Kernel sorgt dafür, dass nur ein Prozess für dieses Ereignis entsperrt wurde und alle eingehenden Verbindungen serialisiert werden.
Aber mit epoll dieser Trick funktioniert nicht. Wenn listen () auf einem nicht blockierenden Socket ausgeführt wurde, warten alle epoll_wait () beim Herstellen der Verbindung auf das Ereignis dieses Deskriptors.
Natürlich kann accept () nur einen Thread ausführen , der Rest erhält EAGAIN , aber dies ist eine Verschwendung von Ressourcen.
Darüber hinaus hilft uns EPOLLET auch nicht, da wir nicht genau wissen, wie viele Verbindungen sich in der Verbindungswarteschlange ( Backlog ) befinden. Wie wir uns erinnern, sollte bei Verwendung von EPOLLET die Socket-Verarbeitung fortgesetzt werden, bis sie mit dem EAGAIN- Fehlercode zurückgegeben wird. Daher besteht die Möglichkeit, dass alle accept () von einem Thread verarbeitet werden und der Rest nicht funktioniert.
Und dies führt uns wieder zu einer Situation, in der der benachbarte Strom vergeblich geweckt wurde.
Wir können auch eine andere Art von Hunger bekommen - wir werden nur einen Thread laden und der Rest wird keine Verbindungen zur Verarbeitung erhalten.
EPOLLONESHOT
Vor Version 4.5 bestand die einzig richtige Möglichkeit, den verteilten Epoll beim nächsten Aufruf von accept () in einen nicht blockierenden listen () - Deskriptor zu verarbeiten, darin, das EPOLLONESHOT- Flag zu setzen, was wiederum dazu führte, dass accept () jeweils nur in einem Thread verarbeitet wurde.
Kurz gesagt: Wenn EPOLLONESHOT verwendet wird , wird das einem bestimmten Deskriptor zugeordnete Ereignis nur einmal ausgelöst . Danach müssen die Flags mit epoll_ctl () erneut gespannt werden .
EPOLLEXKLUSIV
Hier hilft uns EPOLLEXCLUSIVE und Level-Triggered.
EPOLLEXCLUSIVE entsperrt jeweils ein ausstehendes epoll_wait () für ein Ereignis.
Das Schema ist recht einfach (eigentlich nicht):
- Wir haben N Threads, die auf ein Verbindungsereignis warten
- Der erste Kunde verbindet sich mit uns
- Thread 0 wird gestreut und die Verarbeitung gestartet, andere Threads bleiben blockiert
- Ein zweiter Client stellt eine Verbindung zu uns her. Wenn Thread 0 noch mit der Verarbeitung beschäftigt ist, wird Thread 1 entsperrt
- Wir fahren weiter fort, bis der Thread-Pool erschöpft ist (niemand erwartet ein Ereignis auf epoll_wait () )
- Ein anderer Kunde verbindet sich mit uns
- Und dessen Verarbeitung empfängt einen ersten Strom, der Ursache epoll_wait ()
- Der zweite Thread empfängt den zweiten Client, der epoll_wait () aufruft.
Somit ist die gesamte Wartung gleichmäßig auf die Flüsse verteilt.
$ ./epollexclusive --help -i, --ip=ADDR specify ip address -p, --port=PORT specify port -n, --threads=NUM specify number of threads to use
Beispielcode: epollexclusive.c (funktioniert nur mit der Kernel-Version ab 4.5)
Wir bekommen ein Pre-Fork-Modell auf Epoll. Dieses Schema eignet sich gut für kurzzeitige TCP-Verbindungen.
lesen
Aber mit read () im Fall von Byte-Streaming hilft uns EPOLLEXCLUSIVE wie EPOLLET nicht weiter.
Aus offensichtlichen Gründen können wir ohne EPOLLEXCLUSIVE überhaupt kein Level- Trigger verwenden. Mit EPOLLEXCLUSIVE ist nicht alles besser, da wir ein Paket über Streams verteilen können, außer mit einer unbekannten Reihenfolge der eingetroffenen Bytes.
Bei EPOLLET ist die Situation dieselbe.
Und hier wird EPOLLONESHOT mit Reinitialisierung nach Abschluss der Arbeiten der Ausweg sein. Sobald also ein Thread mit diesem Dateideskriptor und Puffer funktioniert:
- Griff mit EPOLLONESHOT- Flags zu epoll hinzugefügt | EPOLLET
- Warten auf epoll_wait ()
- Lesen Sie vom Socket zum Puffer, bis read () EAGAIN zurückgibt
- mit EPOLLONESHOT- Flags neu initialisieren | 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_data_t data; };
Dieser Artikel ist vielleicht der einzige in meinem Artikel, der meiner persönlichen Meinung nach ist. Die Möglichkeit, einen Zeiger oder eine Zahl zu verwenden, ist hilfreich. Wenn Sie beispielsweise einen Zeiger verwenden, wenn Sie epoll verwenden, können Sie einen Trick wie den folgenden ausführen:
#define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );}) struct epoll_client { 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);
Ich denke, jeder weiß, woher diese Technik stammt.
Fazit
Ich hoffe, dass wir das Thema Epoll eröffnen konnten. Wer diesen Mechanismus bewusst nutzen möchte, muss nur die Artikel in der Referenzliste lesen [1, 2, 3, 5].
Basierend auf diesem Material (und noch besser durchdachtes Lesen der Materialien aus der Referenzliste) können Sie einen sperrfreien (ohne Blockierung) Pre-Fork-Server (frühe Prozessgenerierung) mit mehreren Threads erstellen oder vorhandene Strategien basierend auf den speziellen Eigenschaften von epoll () überarbeiten.
epoll ist einer der einzigartigen Mechanismen, die Benutzer, die ihre Linux-Programmierpfade ausgewählt haben, kennen müssen, da sie einen ernsthaften Vorteil gegenüber anderen Betriebssystemen bieten. Möglicherweise lehnen sie plattformübergreifendes Lernen für einen bestimmten Fall ab (lassen Sie es funktionieren) nur unter Linux, wird es aber gut machen).
Argumentation über die "Spezifität" des Problems
Bevor jemand über die Spezifität dieser Flags und Verwendungsmuster spricht, möchte ich eine Frage stellen:
"Aber ist es nichts, was wir versuchen, die Spezifität des Mechanismus zu diskutieren, der ursprünglich für bestimmte Aufgaben erstellt wurde [9, 11]? Oder warten wir sogar 1k-Verbindungen, ist eine tägliche Aufgabe für einen Programmierer?"
Ich verstehe das Konzept der "Aufgabenspezifität" nicht, es erinnert mich an alle möglichen Schreie über die Nützlichkeit und Sinnlosigkeit der verschiedenen gelehrten Disziplinen. Indem wir uns erlauben, auf diese Weise zu argumentieren, geben wir uns das Recht, für andere zu entscheiden, welche Informationen für sie nützlich und welche nutzlos sind, während wir wohlgemerkt nicht am gesamten Bildungsprozess teilnehmen.
Für Skeptiker ein paar Links:
Leistungssteigerung mit SO_REUSEPORT in NGINX 1.9.1 - VBart
Vom Einhorn lernen: die donnernde Herde akzeptieren () kein Problem - Chris Siebenmann
Serializing accept (), AKA Donnerherde, AKA das Zeeg-Problem - Roberto De Ioris
Wie interagiert der EPOLLEXCLUSIVE-Modus von epoll mit dem Level-Triggering?
Referenzliste
- Select ist grundlegend kaputt - Marek
- Epoll ist grundsätzlich kaputt 1/2 - Marek
- Epoll ist grundlegend gebrochen 2/2 - Marek
- Das C10K-Problem - Dan Kegel
- Nochmals Umfrage gegen Epoll - Jacques Mattheij
- epoll - Benachrichtigungsfunktion für E / A-Ereignisse - The Mann
- Die Methode zu Epolls Wahnsinn - Cindy Sridharan
Benchmarks
- https://www.kernel.org/doc/ols/2004/ols2004v1-pages-215-226.pdf
- http://lse.sourceforge.net/epoll/index.html
- https://mvitolin.wordpress.com/2015/12/05/endurox-testing-epollexclusive-flag/
Die Entwicklung des Epolls
- https://lwn.net/Articles/13918/
- https://lwn.net/Articles/520012/
- https://lwn.net/Articles/520198/
- https://lwn.net/Articles/542629/
- https://lwn.net/Articles/633422/
- https://lwn.net/Articles/637435/
Nachtrag
Vielen Dank an Sergey ( dlinyj ) und Peter Ovchenkov für wertvolle Diskussionen, Kommentare und Hilfe!