Gibt es eine Möglichkeit für mehrere Prozesse, einen Listening-Socket gemeinsam zu nutzen?

89

Bei der Socket-Programmierung erstellen Sie einen Listening-Socket und erhalten dann für jeden Client, der eine Verbindung herstellt, einen normalen Stream-Socket, mit dem Sie die Client-Anforderung bearbeiten können. Das Betriebssystem verwaltet die Warteschlange eingehender Verbindungen hinter den Kulissen.

Zwei Prozesse können nicht gleichzeitig an denselben Port gebunden werden - jedenfalls standardmäßig.

Ich frage mich, ob es eine Möglichkeit gibt (unter einem bekannten Betriebssystem, insbesondere Windows), mehrere Instanzen eines Prozesses zu starten, sodass sie alle an den Socket gebunden sind und die Warteschlange effektiv gemeinsam nutzen. Jede Prozessinstanz könnte dann ein einzelner Thread sein. es würde nur blockieren, wenn eine neue Verbindung akzeptiert wird. Wenn ein Client verbunden ist, akzeptiert eine der inaktiven Prozessinstanzen diesen Client.

Dies würde es jedem Prozess ermöglichen, eine sehr einfache Single-Threaded-Implementierung zu haben, die nichts gemeinsam nutzt, außer durch expliziten gemeinsamen Speicher, und der Benutzer könnte die Verarbeitungsbandbreite anpassen, indem er mehr Instanzen startet.

Gibt es eine solche Funktion?

Bearbeiten: Für diejenigen, die fragen "Warum nicht Threads verwenden?" Offensichtlich sind Threads eine Option. Bei mehreren Threads in einem einzigen Prozess sind jedoch alle Objekte gemeinsam nutzbar, und es muss sorgfältig darauf geachtet werden, dass Objekte entweder nicht gemeinsam genutzt werden oder nur für jeweils einen Thread sichtbar sind oder absolut unveränderlich sind und die beliebtesten Sprachen und Sprachen verwenden Die Laufzeiten bieten keine integrierte Unterstützung für die Verwaltung dieser Komplexität.

Wenn Sie eine Handvoll identischer Worker-Prozesse starten, erhalten Sie ein gleichzeitiges System, in dem standardmäßig keine Freigabe erfolgt, was das Erstellen einer korrekten und skalierbaren Implementierung erheblich vereinfacht.

Daniel Earwicker
quelle
2
Ich stimme zu, mehrere Prozesse können es einfacher machen, eine korrekte und robuste Implementierung zu erstellen. Skalierbar, ich bin mir nicht sicher, es hängt von Ihrer Problemdomäne ab.
MarkR

Antworten:

91

Sie können einen Socket zwischen zwei (oder mehr) Prozessen unter Linux und sogar Windows gemeinsam nutzen.

Unter Linux (oder Betriebssystem vom Typ POSIX) führt die Verwendung fork()dazu, dass das gegabelte Kind Kopien aller Dateideskriptoren des Elternteils hat. Alles, was nicht geschlossen wird, wird weiterhin freigegeben und kann (z. B. mit einem TCP-Listening-Socket) für accept()neue Sockets für Clients verwendet werden. So viele Server, in den meisten Fällen einschließlich Apache, funktionieren.

Unter Windows gilt im Grunde das Gleiche, außer dass es keinen fork()Systemaufruf gibt, sodass der übergeordnete Prozess CreateProcesseinen untergeordneten Prozess (der natürlich dieselbe ausführbare Datei verwenden kann) oder etwas anderes verwenden muss und ihm ein vererbbares Handle übergeben muss.

Einen Listening-Socket zu einem vererbbaren Handle zu machen, ist keine völlig triviale Aktivität, aber auch nicht zu schwierig. DuplicateHandle()muss verwendet werden, um ein doppeltes Handle zu erstellen (das sich jedoch noch im übergeordneten Prozess befindet), auf dem das vererbbare Flag gesetzt ist. Dann können Sie diesen Griff in die geben STARTUPINFOStruktur , um das Kind Prozess in Createprocess als eine STDIN, OUToder ERRGriff (vorausgesetzt , Sie wollten es nicht für irgendetwas anderes verwenden).

BEARBEITEN:

Beim Lesen der MDSN-Bibliothek scheint dies WSADuplicateSocketein robusterer oder korrekterer Mechanismus zu sein. Es ist immer noch nicht trivial, da die übergeordneten / untergeordneten Prozesse herausfinden müssen, welches Handle von einem IPC-Mechanismus dupliziert werden muss (obwohl dies so einfach sein kann wie eine Datei im Dateisystem).

KLÄRUNG:

Als Antwort auf die ursprüngliche Frage des OP können mehrere Prozesse nicht bind(); nur der ursprünglichen Eltern - Prozess nennen würde bind(), listen()usw., würden die Child - Prozesse nur Anfragen verarbeiten durch accept(), send(), recv()usw.

MarkR
quelle
3
Mehrere Prozesse können durch Angabe der Socket-Option SocketOptionName.ReuseAddress gebunden werden.
Sipwiz
Aber worum geht es? Prozesse sind sowieso schwerer als Threads.
Anton Tykhyy
7
Prozesse sind schwerer als Threads, aber da sie nur explizit gemeinsam genutzte Dinge gemeinsam nutzen, ist weniger Synchronisation erforderlich, was die Programmierung erleichtert und in einigen Fällen sogar effizienter sein kann.
MarkR
11
Wenn ein untergeordneter Prozess abstürzt oder auf irgendeine Weise unterbrochen wird, ist es außerdem weniger wahrscheinlich, dass er sich auf den übergeordneten Prozess auswirkt.
MarkR
3
Beachten Sie auch, dass Sie unter Linux Sockets ohne Verwendung von fork () an andere Programme "übergeben" können und mit Unix Sockets keine Eltern-Kind-Beziehung haben.
Rahly
34

Die meisten anderen haben die technischen Gründe angegeben, warum dies funktioniert. Hier ist ein Python-Code, den Sie ausführen können, um dies selbst zu demonstrieren:

import socket
import os

def main():
    serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serversocket.bind(("127.0.0.1", 8888))
    serversocket.listen(0)

    # Child Process
    if os.fork() == 0:
        accept_conn("child", serversocket)

    accept_conn("parent", serversocket)

def accept_conn(message, s):
    while True:
        c, addr = s.accept()
        print 'Got connection from in %s' % message
        c.send('Thank you for your connecting to %s\n' % message)
        c.close()

if __name__ == "__main__":
    main()

Beachten Sie, dass tatsächlich zwei Prozess-IDs abhören:

$ lsof -i :8888
COMMAND   PID    USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
Python  26972 avaitla    3u  IPv4 0xc26aa26de5a8fc6f      0t0  TCP localhost:ddi-tcp-1 (LISTEN)
Python  26973 avaitla    3u  IPv4 0xc26aa26de5a8fc6f      0t0  TCP localhost:ddi-tcp-1 (LISTEN)

Hier sind die Ergebnisse von Telnet und dem Programm:

$ telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Thank you for your connecting to parent
Connection closed by foreign host.
$ telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Thank you for your connecting to child
Connection closed by foreign host.
$ telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Thank you for your connecting to parent
Connection closed by foreign host.

$ python prefork.py 
Got connection from in parent
Got connection from in child
Got connection from in parent
Anil Vaitla
quelle
2
Für eine Verbindung erhalten entweder Eltern oder Kind diese. Aber wer die Verbindung bekommt, ist unbestimmt, oder?
Hot.PxL
1
Ja, ich denke, es hängt davon ab, welcher Prozess vom Betriebssystem ausgeführt werden soll.
Anil Vaitla
13

Ich möchte hinzufügen, dass die Sockets unter Unix / Linux über AF__UNIX-Sockets (Interprozess-Sockets) gemeinsam genutzt werden können. Was zu passieren scheint, ist, dass ein neuer Socket-Deskriptor erstellt wird, der in gewisser Weise ein Alias ​​zum ursprünglichen ist. Dieser neue Socket-Deskriptor wird über den AFUNIX-Socket an den anderen Prozess gesendet. Dies ist besonders nützlich, wenn ein Prozess seine Dateideskriptoren nicht mit fork () teilen kann. Zum Beispiel, wenn Bibliotheken verwendet werden, die dies aufgrund von Threading-Problemen verhindern. Sie sollten einen Unix-Domain-Socket erstellen und libancillary verwenden , um den Deskriptor zu senden.

Sehen:

So erstellen Sie AF_UNIX-Sockets:

Zum Beispiel Code:

zachthehack
quelle
13

Diese Frage wurde anscheinend bereits vollständig von MarkR und zackthehack beantwortet, aber ich möchte hinzufügen, dass Nginx ein Beispiel für das Listening-Socket-Vererbungsmodell ist.

Hier ist eine gute Beschreibung:

         Implementation of HTTP Auth Server Round-Robin and
                Memory Caching for NGINX Email Proxy

                            June 6, 2007
             Md. Mansoor Peerbhoy <[email protected]>

...

Ablauf eines NGINX-Worker-Prozesses

Nachdem der Haupt-NGINX-Prozess die Konfigurationsdatei gelesen und die konfigurierte Anzahl von Worker-Prozessen eingegeben hat, tritt jeder Worker-Prozess in eine Schleife ein, in der er auf Ereignisse in seinem jeweiligen Socket-Satz wartet.

Jeder Worker-Prozess beginnt nur mit den Listening-Sockets, da noch keine Verbindungen verfügbar sind. Daher beginnt der für jeden Arbeitsprozess festgelegte Ereignisdeskriptor nur mit den Listening-Sockets.

(HINWEIS) NGINX kann so konfiguriert werden, dass einer von mehreren Ereignisabfragemechanismen verwendet wird: aio / devpoll / epoll / eventpoll / kqueue / poll / rtsig / select

Wenn eine Verbindung an einem der Überwachungssockets (POP3 / IMAP / SMTP) eintrifft, geht jeder Arbeitsprozess aus seiner Ereignisabfrage hervor, da jeder NGINX-Arbeitsprozess den Überwachungssocket erbt. Anschließend versucht jeder NGINX-Worker-Prozess, einen globalen Mutex abzurufen. Einer der Arbeitsprozesse erhält die Sperre, während die anderen zu ihren jeweiligen Ereignisabfrageschleifen zurückkehren.

In der Zwischenzeit überprüft der Arbeitsprozess, der den globalen Mutex erfasst hat, die ausgelösten Ereignisse und erstellt die erforderlichen Anforderungen für die Arbeitswarteschlange für jedes ausgelöste Ereignis. Ein Ereignis entspricht einem einzelnen Socket-Deskriptor aus dem Satz von Deskriptoren, von denen der Worker nach Ereignissen gesucht hat.

Wenn das ausgelöste Ereignis einer neuen eingehenden Verbindung entspricht, akzeptiert NGINX die Verbindung vom Listening-Socket. Anschließend wird dem Dateideskriptor eine Kontextdatenstruktur zugeordnet. Dieser Kontext enthält Informationen zur Verbindung (ob POP3 / IMAP / SMTP, ob der Benutzer noch authentifiziert ist usw.). Anschließend wird dieser neu erstellte Socket dem Ereignisdeskriptorsatz für diesen Arbeitsprozess hinzugefügt.

Der Worker gibt nun den Mutex auf (was bedeutet, dass alle Ereignisse, die bei anderen Workern aufgetreten sind, verarbeitet werden können) und beginnt mit der Verarbeitung jeder Anforderung, die zuvor in der Warteschlange stand. Jede Anfrage entspricht einem signalisierten Ereignis. Von jedem Socket-Deskriptor, der signalisiert wurde, ruft der Worker-Prozess die entsprechende Kontextdatenstruktur ab, die zuvor diesem Deskriptor zugeordnet war, und ruft dann die entsprechenden Rückruffunktionen auf, die Aktionen basierend auf dem Status dieser Verbindung ausführen. Wenn beispielsweise eine neu hergestellte IMAP-Verbindung hergestellt wird, schreibt NGINX zunächst die Standard-IMAP-Begrüßungsnachricht auf den
angeschlossenen Socket (* OK IMAP4 bereit).

Nach und nach schließt jeder Arbeitsprozess die Verarbeitung des Arbeitswarteschlangeneintrags für jedes ausstehende Ereignis ab und kehrt zu seiner Ereignisabfrageschleife zurück. Sobald eine Verbindung mit einem Client hergestellt wurde, sind die Ereignisse normalerweise schneller, da das Leseereignis immer dann ausgelöst wird, wenn der verbundene Socket zum Lesen bereit ist, und die entsprechende Aktion ausgeführt werden muss.

richardw
quelle
11

Ich bin mir nicht sicher, wie relevant dies für die ursprüngliche Frage ist, aber im Linux-Kernel 3.9 gibt es einen Patch, der eine TCP / UDP-Funktion hinzufügt: TCP- und UDP-Unterstützung für die SO_REUSEPORT-Socket-Option; Die neue Socket-Option ermöglicht die Bindung mehrerer Sockets auf demselben Host an denselben Port und soll die Leistung von Multithread-Netzwerkserveranwendungen verbessern, die auf Multicore-Systemen ausgeführt werden. Weitere Informationen finden Sie im LWN-Link LWN SO_REUSEPORT in Linux Kernel 3.9, wie im Referenzlink angegeben:

Die Option SO_REUSEPORT ist nicht Standard, steht jedoch in ähnlicher Form auf einer Reihe anderer UNIX-Systeme zur Verfügung (insbesondere auf den BSDs, aus denen die Idee stammt). Es scheint eine nützliche Alternative zu sein, um die maximale Leistung von Netzwerkanwendungen, die auf Multicore-Systemen ausgeführt werden, zu nutzen, ohne das Gabelmuster verwenden zu müssen.

Walid
quelle
Aus dem LWN-Artikel geht fast hervor, dass SO_REUSEPORTein Thread-Pool erstellt wird, in dem sich jeder Socket in einem anderen Thread befindet, aber nur ein Socket in der Gruppe das ausführt accept. Können Sie bestätigen, dass alle Sockets in der Gruppe jeweils eine Kopie der Daten erhalten?
JWW
3

Haben Sie eine einzige Aufgabe, deren einzige Aufgabe es ist, auf eingehende Verbindungen zu warten. Wenn eine Verbindung empfangen wird, akzeptiert sie die Verbindung. Dadurch wird ein separater Socket-Deskriptor erstellt. Der akzeptierte Socket wird an eine Ihrer verfügbaren Worker-Aufgaben übergeben, und die Hauptaufgabe wird wieder abgehört.

s = socket();
bind(s);
listen(s);
while (1) {
  s2 = accept(s);
  send_to_worker(s2);
}
HUAGHAGUAH
quelle
Wie wird die Steckdose an einen Arbeiter weitergegeben? Denken Sie daran, dass die Idee ist, dass ein Arbeiter ein separater Prozess ist.
Daniel Earwicker
fork () vielleicht oder eine der anderen Ideen oben. Oder Sie trennen die Socket-E / A vollständig von der Datenverarbeitung. Senden Sie die Nutzdaten über einen IPC-Mechanismus an Worker-Prozesse. OpenSSH und andere OpenBSD-Tools verwenden diese Methode (ohne Threads).
HUAGHAGUAH
3

Unter Windows (und Linux) ist es möglich, dass ein Prozess einen Socket öffnet und diesen Socket dann an einen anderen Prozess weitergibt, sodass dieser zweite Prozess diesen Socket ebenfalls verwenden kann (und ihn nacheinander weitergibt, falls er dies wünscht). .

Der entscheidende Funktionsaufruf ist WSADuplicateSocket ().

Dadurch wird eine Struktur mit Informationen zu einem vorhandenen Socket gefüllt. Diese Struktur wird dann über einen IPC-Mechanismus Ihrer Wahl an einen anderen vorhandenen Prozess übergeben (ich sage vorhanden - wenn Sie WSADuplicateSocket () aufrufen, müssen Sie den Zielprozess angeben, der die ausgegebenen Informationen erhält).

Der empfangende Prozess kann dann WSASocket () aufrufen, diese Informationsstruktur übergeben und ein Handle an den zugrunde liegenden Socket empfangen.

Beide Prozesse halten jetzt ein Handle für denselben zugrunde liegenden Socket.


quelle
2

Es hört sich so an, als ob Sie einen Prozess wünschen, der auf neue Clients wartet und die Verbindung dann abgibt, sobald Sie eine Verbindung erhalten. Dies über Threads hinweg zu tun ist einfach und in .Net haben Sie sogar die BeginAccept usw. Methoden, um einen Großteil der Installation für Sie zu erledigen. Das Übergeben der Verbindungen über Prozessgrenzen hinweg wäre kompliziert und hätte keine Leistungsvorteile.

Alternativ können Sie mehrere Prozesse an denselben Socket binden und abhören.

TcpListener tcpServer = new TcpListener(IPAddress.Loopback, 10090);
tcpServer.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
tcpServer.Start();

while (true)
{
    TcpClient client = tcpServer.AcceptTcpClient();
    Console.WriteLine("TCP client accepted from " + client.Client.RemoteEndPoint + ".");
}

Wenn Sie zwei Prozesse starten, die jeweils den obigen Code ausführen, funktioniert dies und der erste Prozess scheint alle Verbindungen zu erhalten. Wenn der erste Prozess beendet wird, erhält der zweite die Verbindungen. Bei einer solchen Socket-Freigabe bin ich mir nicht sicher, wie Windows genau entscheidet, welcher Prozess neue Verbindungen erhält, obwohl der Schnelltest darauf hinweist, dass der älteste Prozess diese zuerst erhält. Ob es teilt, ob der erste Prozess beschäftigt ist oder so etwas, weiß ich nicht.

sipwiz
quelle
2

Ein anderer Ansatz (der viele komplexe Details vermeidet) in Windows, wenn Sie HTTP verwenden, ist die Verwendung von HTTP.SYS . Auf diese Weise können mehrere Prozesse unterschiedliche URLs am selben Port abhören. Unter Server 2003/2008 / Vista / 7 funktioniert IIS so, sodass Sie Ports für IIS freigeben können. (Unter XP SP2 wird HTTP.SYS unterstützt, IIS5.1 verwendet es jedoch nicht.)

Andere High-Level-APIs (einschließlich WCF) verwenden HTTP.SYS.

Richard
quelle