Multiplexed Ein- / Ausgang

Vorwort


In diesem Artikel möchte ich auf wichtige Aspekte der Programmierung von Webanwendungen eingehen, die viele Benutzer gleichzeitig bedienen sollen. Das bedeutet, dass wir bereits alle langweiligen asynchronen E / A, Multiplexing usw. analysieren werden.
Folgende Ziele werden verfolgt:
  1. Erörtern Sie zur Systematisierung des Materials in diesem Bereich einige terminologische Inkonsistenzen
  2. Zerlegen Sie das Fundament, auf dem viele Kundendienstanwendungen basieren, vollständig.
  3. Entwickeln Sie eine Strategie für eine zukünftige Python-Anwendung, die viele Clients bedienen soll
  4. Erstelle ein klares Bild in deinem Kopf (nicht ohne Grund wird gesagt, dass du weißt, wann du es erklären kannst)



Warum?


Die Jahre der PHP-Programmierung haben sich ausgezahlt - ich habe mich nie gefragt, was sich hinter den Kulissen abspielt. Aber als die Projekte langsam, aber sicher langsamer wurden, beschloss ich, dass es an der Zeit war, mich mit Python zu beschäftigen (was ich jetzt erfolgreich mache). Aber da ich definitiv alle möglichen Wörter wie „Fork“, „Socket“, „Eventloop“, „Multiprocessing“, „Epoll“ usw. satt hatte Ich beschloss tiefer zu graben. Was dabei herauskam, liegt bei Ihnen. Trotzdem ist eine gewisse Verwirrung vorhanden. Eine Bestätigung hierzu finden Sie in den Referenzen [ 6 , 7 ].

int main ()


Dieser Artikel beschreibt Linux I / O auf Basis von Linux 2.6 und höher. Da sich das Material hauptsächlich auf Menschen wie mich konzentriert (das Gerät zum Erhitzen von Wasser ist optional mit einer akustischen Anzeige ausgestattet), müssen Sie eine Menge Grundlagen ausmachen. Überspringen Sie also einfach die Elemente, die Sie nicht benötigen.

Grundlegende Konzepte

Datei ist eine grundlegende Abstraktion in Linux. Linux folgt der Philosophie „Alles ist eine Datei“, was bedeutet, dass der größte Teil der Interaktion durch Lesen und Schreiben von Dateien realisiert wird. Dateioperationen werden mit einem eindeutigen Deskriptor ausgeführt - einem Dateideskriptor oder fd. Die meiste Linux-Systemprogrammierung umfasst die Arbeit mit Dateideskriptoren.
Es gibt reguläre Dateien (reguläre Datei) - das ist, was wir gewohnt sind (die gewöhnlichste "Datei" im üblichen Sinne) und spezielle Dateien sind einige Objekte, die als Dateien dargestellt werden. Linux unterstützt 4 Arten von Spezialdateien:
  • Gerätedateien blockieren
  • Character I / O-Gerätedateien
  • Named Pipes (Named Pipe oder FIFO)
  • Steckdosen

Letzteres ist genau der Bereich unserer Interessen, da Sockets die Kommunikation zwischen zwei verschiedenen Prozessen ermöglichen, die sich auf verschiedenen Computern befinden können (Client-Server). Tatsächlich basiert die Netzwerkprogrammierung und Programmierung für das Internet auf Sockets.
In erster Näherung genügt es, nur normale Dateien und Sockets zu berücksichtigen.


Eingabe / Ausgabe-Modelle

Insgesamt stehen in Unix-ähnlichen Systemen 5 + 1 verschiedene E / A-Modelle zur Verfügung. "Plus eins" werde ich etwas später erklären, aber im Moment betrachten wir jedes Modell genauer.

Blockieren von E / A (Blockieren von E / A)

Standardmäßig werden alle Eingaben in einem blockierenden Stil ausgegeben. Betrachten Sie eine schematische Darstellung der Prozesse, die beim Blockieren der Eingabe / Ausgabe auftreten.



In diesem Fall führt der Prozess den Systemaufruf recvfrom aus . Infolgedessen wird der Prozess blockiert (in den Ruhezustand versetzt), bis die Daten eingehen und der Systemaufruf sie in den Anwendungspuffer schreibt.
Danach endet der Systemaufruf (Return OK) und wir können unsere Daten verarbeiten.
Offensichtlich hat dieser Ansatz einen großen Nachteil: Während wir auf Daten warten (und dies kann aufgrund der Qualität der Verbindung usw. sehr lange dauern), wird der Prozess unterbrochen und reagiert nicht auf Anforderungen.

Nicht blockierende E / A (nicht blockierende E / A)

Wir können den nicht blockierenden Modus einstellen, wenn wir mit Sockets arbeiten, und dem Kernel Folgendes mitteilen: „Wenn die gewünschte Eingabe / Ausgabe nicht möglich ist, ohne den Prozess in eine Sperre zu versetzen (Schlafmodus), geben Sie den Fehler an mich zurück, dass Sie dies nicht tun können, ohne zu blockieren.“ Überlegen Sie schematische Darstellung von Vorgängen bei nicht blockierender Ein- / Ausgabe.



Die ersten drei Male, die wir einen Systemaufruf zum Lesen senden, geben kein Ergebnis zurück, weil Der Kernel erkennt, dass keine Daten vorhanden sind und gibt nur einen EWOULDBLOCK-Fehler zurück.
Der letzte Systemaufruf wird da erfolgreich sein Daten sind bereit zum Lesen. Infolgedessen schreibt der Kernel Daten in den Prozesspuffer und diese werden für die Verarbeitung verfügbar.
Auf dieser Basis können Sie eine Schleife erstellen, die für Sockets, die im nicht blockierenden Modus geöffnet sind, ständig recvfrom aufruft (Daten anfordert). Dieser Modus wird als Abruf bezeichnet. Die Anwendung fragt ständig den Kern des Systems nach der Datenverfügbarkeit ab. Grundsätzlich sehe ich keine Einschränkungen, um mehrere Sockets nacheinander abzufragen und dementsprechend von dem ersten zu lesen, in dem sich Daten befinden. Dieser Ansatz führt zu einem großen Overhead (Prozessor-Overhead).


Multiplexing-E / A (Multiplexing-E / A)

Im Allgemeinen bedeutet das Wort Multiplexing „Verdichtung“. Ich denke, es lässt sich mit dem Motto Zeitmanagement erfolgreich beschreiben - "mehr lernen". Beim Multiplexen von E / A wenden wir uns einem der im Betriebssystem verfügbaren Systemaufrufe zu (einem Multiplexer wie select, poll, pselect, dev / poll, epoll (empfohlen für Linux), kqueue (BSD)) und blockieren ihn, anstatt ihn zu blockieren tatsächlicher I / O-Aufruf. Der Vorgang des Multiplexens ist im Bild schematisch dargestellt: Die



Anwendung wird beim Aufruf von select blockiert und wartet auf die Lesbarkeit des Sockets. Dann gibt der Kernel den lesbaren Status zurück und Sie können Daten mit recvfrom abrufen. Auf den ersten Blick - eine völlige Enttäuschung. Das gleiche Sperren, Warten und zwei weitere Systemaufrufe (auswählen und erneut anfordern) - hoher Overhead. Anders als bei der Blockierungsmethode können Sie mit select (und jedem anderen Multiplexer) Daten nicht von einem, sondern von mehreren Dateideskriptoren erwarten. Ich muss sagen, dass dies die vernünftigste Methode ist, um viele Kunden zu bedienen, insbesondere wenn die Ressourcen sehr begrenzt sind. Warum ist das so? Weil der Multiplexer Ausfallzeiten (Ruhezustand) reduziert. Ich werde versuchen, das folgende Bild zu erklären:



Ein Pool von Deskriptoren für Sockets wird erstellt. Auch wenn die EINPROGRESS-Antwort während der Verbindung bei uns eingegangen ist, bedeutet dies, dass die Verbindung hergestellt wird, was uns nicht stört, weil Der Multiplexer nimmt während des Tests immer noch den zuerst freigegebenen.
Jetzt aufgepasst! Das Wichtigste!
Beantworten Sie die Frage: Welches Ereignis ist wahrscheinlicher? Für Ereignis A, dass die Daten für einen bestimmten Socket bereit sind, oder für Ereignis B, dass die Daten für mindestens einen Socket bereit sind ? . Antwort: B
Beim Multiplexen werden ALLE Sockets in einer Schleife geprüft und die erste ist bereit. Während wir mit ihm arbeiten, können andere auch pünktlich ankommen, dh wir verkürzen die Wartezeit für Leerlaufzeiten (das erste Mal können wir lange warten, aber die Ruhezeiten - viel weniger).
Wenn wir das Problem auf die übliche Weise lösen (mit Blockierung), müssen wir raten, von welcher Verbindung die erste Sekunde gelesen werden soll usw. d.h. wir irren uns zu 100% und werden warten, und obwohl wir diese Zeit nicht verschwenden konnten

E / A in Threads / untergeordneten Prozessen (Eine Dateibeschreibung pro Thread oder Prozess)

Zu Beginn wurde gesagt, dass es eine 5 + 1-Methode gibt. Dies war genau der Ansatz, wenn mehrere Streams oder Prozesse verwendet werden, in denen jeweils blockierende E / A-Vorgänge ausgeführt werden. Es ähnelt dem E / A-Multiplexing, weist jedoch mehrere Nachteile auf. Jeder weiß, dass Threads unter Linux ziemlich teuer sind (mit den sogenannten Systembefehlen), daher führt die Verwendung von Threads zu einer Erhöhung des Overheads. Wenn Sie Python als Programmiersprache betrachten, ist außerdem eine GIL enthalten, und dementsprechend kann zu jedem Zeitpunkt nur ein Thread innerhalb eines Prozesses ausgeführt werden. Eine weitere Option besteht darin, untergeordnete Prozesse zu erstellen, um E / A in einem blockierenden Stil zu verarbeiten. Dann ist es jedoch notwendig, über die Interaktion zwischen Prozessen nachzudenken (IPC - Interprozesskommunikation), die einige Schwierigkeiten hat. Wenn außerdem die Gesamtzahl der Kerne die Einheit nicht überschreitet, hat dieser Ansatz einen zweifelhaften Gewinn. Meines Wissens arbeitet Apache übrigens genauso (MPM-Prefork oder -Threads) und bedient den Client entweder in einem Thread oder in einem separaten Prozess.

Eingangssignal-gesteuerter Ausgang (signalgesteuerte E / A)

Es ist möglich, Signale zu verwenden, die den Kernel zwingen, uns ein Signal der Form SIGIO zu senden, wenn es möglich ist, Daten ohne Blockierung zu lesen (der Handle ist bereit zum Lesen). Schematisch ist dieser Ansatz im Bild dargestellt.



Zunächst müssen Sie die Socket-Parameter für die Arbeit mit Signalen festlegen und einen Signal-Handler mithilfe des Sigaction-System-Aufrufs zuweisen. Das Ergebnis wird sofort zurückgegeben und die Anwendung wird daher nicht blockiert. In der Tat kümmert sich der Kern um die ganze Arbeit, wie Es überwacht, wann die Daten bereit sind und sendet uns ein SIGIO-Signal, das den darauf installierten Handler aufruft (Callback-Funktion, Callback). Dementsprechend kann der Aufruf von recvfrom selbst entweder im Signalhandler oder im Hauptprogrammstrom erfolgen. Soweit ich das beurteilen kann, gibt es hier ein Problem - es kann nur ein Signal für jeden Prozess dieses Typs geben. Das heißt wir können immer nur mit einem fd gleichzeitig arbeiten (obwohl ich nicht sicher bin)


Asynchrone E / A (asynchrone E / A)

Die asynchrone Ein- / Ausgabe erfolgt über spezielle Systemaufrufe. Es basiert auf einer einfachen Idee: Der Kernel erhält den Befehl, die Operation zu starten und uns (unter Verwendung von Signalen oder etwas anderem) zu benachrichtigen, wenn die E / A-Operation vollständig abgeschlossen ist (einschließlich des Kopierens von Daten in den Prozesspuffer). Dies ist der Hauptunterschied zwischen dieser Implementierung und der Implementierung auf Signalen. Schematisch sind die Prozesse der asynchronen Eingabe und Ausgabe im Bild dargestellt.



Wir machen einen Systemaufruf aio_read und spezifizieren alle notwendigen Parameter. Den Rest der Arbeit erledigt der Kern für uns. Natürlich muss es einen Mechanismus geben, der den Prozess darüber informiert, dass die E / A abgeschlossen ist. Und hier treten möglicherweise viele Probleme auf. Aber dazu ein anderes Mal mehr.
Im Allgemeinen gibt es viele Probleme mit diesem Begriff, Beispiele für Links wurden bereits gegeben. Oft gibt es eine Verwechslung von Konzepten zwischen asynchroner, nicht blockierender und gemultiplexter Eingangsausgabe, anscheinend, weil das eigentliche Konzept von "asynchron" auf verschiedene Arten interpretiert werden kann. Asynchron bedeutet nach meinem Verständnis zeitunabhängig. Das heißt, wenn er einmal gestartet ist, lebt er sein Leben, bis es erfüllt ist, und dann erhalten wir einfach das Ergebnis

In der Praxis

In der Praxis eine Kombination verschiedener Modelle je nach Aufgabenstellung. Gehen Sie wie folgt vor:
  • Verwenden Sie einen Thread / Prozess für jede Operation mit einer Sperre - nicht rentabel
  • Viele Clients in unterschiedlichen Threads / Prozessen. Jeder Thread / Prozess verwendet einen Multiplexer
  • Viele Clients in unterschiedlichen Threads / Prozessen. Verwendete asynchrone Ein- / Ausgabe (aio)
  • Betten Sie den Server einfach in den Kernel ein

Weitere Details im C10K-Problem


Zusammenfassung


Ich hoffe, dass jetzt zumindest ein wenig Klarheit über die Unterschiede in den Dingen entsteht, in denen es leicht ist, sich zu verwechseln.
Naja und ja, Multiplexing-Regeln (bis sie fertig sind, denke ich).
Als ich die Referenzliteratur studierte, kam ich zu dem Schluss, dass reguläre Dateien im Gegensatz zu Sockets nicht in den nicht blockierenden Modus übertragen werden können. Aio scheint für sie verfügbar zu sein, was hier diskutiert wird: Asynchrone I / O unter Linux oder Willkommen in der Hölle

Literatur und Referenzen


  1. Robert Love „Linux. Systemprogrammierung
  2. W. Richard Stevens, Bill Fenner und Andrew M. Rudoff
  3. Stevens R., Rago S. Unix. Professionelle Programmierung, 2. Auflage
  4. Jedermanns Liebling Das C10K-Problem
  5. Asynchrone E / A unter Linux oder Willkommen in der Hölle
  6. Vergleichen von zwei Hochleistungs-E / A-Entwurfsmustern
  7. Asynchron vs nicht blockierend
  8. Blockieren vs. Nicht blockierende Steckdosen

Jetzt auch beliebt: