C'è un modo per più processi di condividere un socket in ascolto?


90

Nella programmazione dei socket, crei un socket di ascolto e quindi per ogni client che si connette, ottieni un normale socket di flusso che puoi usare per gestire la richiesta del client. Il sistema operativo gestisce la coda delle connessioni in entrata dietro le quinte.

Due processi non possono collegarsi alla stessa porta contemporaneamente, per impostazione predefinita, comunque.

Mi chiedo se esiste un modo (su qualsiasi sistema operativo noto, in particolare Windows) per avviare più istanze di un processo, in modo tale che si leghino tutte al socket e quindi condividano efficacemente la coda. Ogni istanza di processo potrebbe quindi essere a thread singolo; si bloccherebbe solo quando si accetta una nuova connessione. Quando un client si connette, una delle istanze del processo inattivo accetta quel client.

Ciò consentirebbe a ciascun processo di avere un'implementazione molto semplice, a thread singolo, senza condividere nulla se non attraverso la memoria condivisa esplicita e l'utente sarebbe in grado di regolare la larghezza di banda di elaborazione avviando più istanze.

Esiste una tale caratteristica?

Modifica: per coloro che chiedono "Perché non usare i thread?" Ovviamente i thread sono un'opzione. Ma con più thread in un unico processo, tutti gli oggetti sono condivisibili e occorre prestare molta attenzione per garantire che gli oggetti non siano condivisi, o siano visibili solo a un thread alla volta, o siano assolutamente immutabili e le lingue e le lingue più diffuse i runtime mancano del supporto integrato per la gestione di questa complessità.

Avviando una manciata di processi di lavoro identici, si otterrebbe un sistema simultaneo in cui l' impostazione predefinita non è condivisione, rendendo molto più semplice creare un'implementazione corretta e scalabile.


2
Sono d'accordo, più processi possono rendere più facile creare un'implementazione corretta e robusta. Scalabile, non ne sono sicuro, dipende dal tuo dominio problematico.
MarkR

Risposte:


92

Puoi condividere un socket tra due (o più) processi in Linux e persino in Windows.

Sotto Linux (o OS di tipo POSIX), l'uso fork()farà sì che il figlio biforcuto abbia copie di tutti i descrittori di file del genitore. Tutto ciò che non chiude continuerà a essere condiviso e (ad esempio con un socket di ascolto TCP) può essere utilizzato per accept()nuovi socket per i client. Questo è il numero di server, incluso Apache nella maggior parte dei casi, funzionano.

Su Windows la stessa cosa è fondamentalmente vera, tranne che non c'è una fork()chiamata di sistema, quindi il processo genitore dovrà usare CreateProcesso qualcosa per creare un processo figlio (che può ovviamente utilizzare lo stesso eseguibile) e deve passargli un handle ereditabile.

Rendere un socket in ascolto un handle ereditabile non è un'attività del tutto banale ma nemmeno troppo complicata. DuplicateHandle()deve essere utilizzato per creare un handle duplicato (comunque nel processo genitore), che avrà il flag ereditabile impostato su di esso. Quindi puoi dare quell'handle nella STARTUPINFOstruttura al processo figlio in CreateProcess come un STDIN, OUTo ERRhandle (supponendo che tu non voglia usarlo per nient'altro).

MODIFICARE:

Leggendo la libreria MDSN, sembra che WSADuplicateSocketsia un meccanismo più robusto o corretto per farlo; non è ancora banale perché i processi genitore / figlio devono capire quale handle deve essere duplicato da qualche meccanismo IPC (sebbene questo potrebbe essere semplice come un file nel filesystem)

UNA PRECISAZIONE:

In risposta alla domanda originale del PO, no, più processi non possono bind(); solo il processo padre originale sarebbe chiamata bind(), listen()ecc, i processi figlio sarebbe solo elaborare le richieste da parte accept(), send(), recv()etc.


3
È possibile eseguire il binding più processi specificando l'opzione socket SocketOptionName.ReuseAddress.
sipwiz

Ma qual è il punto? I processi sono comunque più pesanti dei thread.
Anton Tykhyy

7
I processi sono più pesanti dei thread, ma poiché condividono solo cose esplicitamente condivise, è necessaria una minore sincronizzazione che rende la programmazione più semplice e potrebbe anche essere più efficiente in alcuni casi.
MarkR

11
Inoltre, se un processo figlio si arresta in modo anomalo o si interrompe in qualche modo, è meno probabile che influisca sul genitore.
MarkR

3
È bene anche notare che, in Linux, puoi "passare" i socket ad altri programmi senza usare fork () e non ha una relazione genitore / figlio, usando Unix Sockets.
Rahly

34

La maggior parte degli altri ha fornito le ragioni tecniche per cui funziona. Ecco un po 'di codice Python che puoi eseguire per dimostrarlo da solo:

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()

Nota che ci sono effettivamente due ID di processo in ascolto:

$ 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)

Ecco i risultati dell'esecuzione di telnet e del programma:

$ 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

2
Quindi, per una connessione, il genitore o il figlio lo ottengono. Ma chi ottiene la connessione è indeterminista, giusto?
Hot.PxL

1
sì, penso che dipenda da quale processo è pianificato per essere eseguito dal sistema operativo.
Anil Vaitla

14

Vorrei aggiungere che i socket possono essere condivisi su Unix / Linux tramite socket AF__UNIX (socket inter-process). Quello che sembra accadere è che viene creato un nuovo descrittore di socket che è in qualche modo un alias di quello originale. Questo nuovo descrittore di socket viene inviato tramite il socket AFUNIX all'altro processo. Ciò è particolarmente utile nei casi in cui un processo non può utilizzare fork () per condividere i suoi descrittori di file. Ad esempio, quando si utilizzano librerie che impediscono ciò a causa di problemi di threading. Dovresti creare un socket di dominio Unix e usare libancillary per inviare il descrittore.

Vedere:

Per creare socket AF_UNIX:

Ad esempio codice:


13

Sembra che questa domanda abbia già ricevuto una risposta completa da MarkR e zackthehack, ma vorrei aggiungere che Nginx è un esempio del modello di ereditarietà del socket in ascolto.

Ecco una buona descrizione:

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

                            June 6, 2007
             Md. Mansoor Peerbhoy <mansoor@zimbra.com>

...

Flusso di un processo di lavoro NGINX

Dopo che il processo NGINX principale ha letto il file di configurazione e si è inserito nel numero configurato di processi di lavoro, ogni processo di lavoro entra in un ciclo in cui attende eventuali eventi sul rispettivo set di socket.

Ogni processo di lavoro inizia con solo i socket di ascolto, poiché non sono ancora disponibili connessioni. Pertanto, il descrittore di eventi impostato per ogni processo di lavoro inizia con solo i socket in ascolto.

(NOTA) NGINX può essere configurato per utilizzare uno qualsiasi dei diversi meccanismi di polling degli eventi: aio / devpoll / epoll / eventpoll / kqueue / poll / rtsig / select

Quando una connessione arriva su uno qualsiasi dei socket di ascolto (POP3 / IMAP / SMTP), ogni processo di lavoro emerge dal suo poll degli eventi, poiché ogni processo di lavoro NGINX eredita il socket di ascolto. Quindi, ogni processo di lavoro NGINX tenterà di acquisire un mutex globale. Uno dei processi di lavoro acquisirà il blocco, mentre gli altri torneranno ai rispettivi loop di polling degli eventi.

Nel frattempo, il processo di lavoro che ha acquisito il mutex globale esaminerà gli eventi attivati ​​e creerà le richieste di coda di lavoro necessarie per ogni evento attivato. Un evento corrisponde a un singolo descrittore socket dall'insieme di descrittori da cui il lavoratore stava guardando gli eventi.

Se l'evento attivato corrisponde a una nuova connessione in entrata, NGINX accetta la connessione dal socket in ascolto. Quindi, associa una struttura di dati di contesto al descrittore di file. Questo contesto contiene informazioni sulla connessione (se POP3 / IMAP / SMTP, se l'utente è ancora autenticato, ecc.). Quindi, questo socket di nuova costruzione viene aggiunto al descrittore di eventi impostato per quel processo di lavoro.

Il worker ora rinuncia al mutex (il che significa che qualsiasi evento arrivato su altri worker può procedere) e inizia a elaborare ogni richiesta precedentemente accodata. Ogni richiesta corrisponde a un evento che è stato segnalato. Da ogni descrittore di socket segnalato, il processo di lavoro recupera la struttura di dati di contesto corrispondente precedentemente associata a quel descrittore e quindi chiama le corrispondenti funzioni di richiamata che eseguono azioni in base allo stato di quella connessione. Ad esempio, nel caso di una nuova connessione IMAP, la prima cosa che NGINX farà è scrivere il messaggio di benvenuto IMAP standard sul
socket connesso (* OK IMAP4 pronto).

A poco a poco, ogni processo di lavoro completa l'elaborazione della voce della coda di lavoro per ogni evento in sospeso e torna al ciclo di polling degli eventi. Una volta stabilita una connessione con un client, gli eventi di solito sono più rapidi, poiché ogni volta che il socket connesso è pronto per la lettura, viene attivato l'evento di lettura e deve essere intrapresa l'azione corrispondente.


11

Non sono sicuro di quanto sia rilevante per la domanda originale, ma nel kernel Linux 3.9 c'è una patch che aggiunge una funzionalità TCP / UDP: supporto TCP e UDP per l'opzione socket SO_REUSEPORT; La nuova opzione socket consente a più socket sullo stesso host di collegarsi alla stessa porta e ha lo scopo di migliorare le prestazioni delle applicazioni server di rete multithread in esecuzione su sistemi multicore. maggiori informazioni possono essere trovate nel link LWN LWN SO_REUSEPORT in Linux Kernel 3.9 come menzionato nel link di riferimento:

l'opzione SO_REUSEPORT non è standard, ma disponibile in una forma simile su un certo numero di altri sistemi UNIX (in particolare, i BSD, da cui è nata l'idea). Sembra offrire un'utile alternativa per spremere le massime prestazioni dalle applicazioni di rete in esecuzione su sistemi multicore, senza dover utilizzare il pattern fork.


Dall'articolo LWN sembra quasi che SO_REUSEPORTcrei un pool di thread, in cui ogni socket si trova su un thread diverso ma solo un socket nel gruppo esegue il accept. Potete confermare che tutti i socket del gruppo ottengono una copia dei dati?
jww


3

Avere un'unica attività il cui unico compito è ascoltare le connessioni in entrata. Quando viene ricevuta una connessione, accetta la connessione - questo crea un descrittore socket separato. Il socket accettato viene passato a una delle attività di lavoro disponibili e l'attività principale torna in ascolto.

s = socket();
bind(s);
listen(s);
while (1) {
  s2 = accept(s);
  send_to_worker(s2);
}

Come viene passato il socket a un lavoratore? Tieni presente che l'idea è che un lavoratore è un processo separato.
Daniel Earwicker

fork () forse, o una delle altre idee sopra. O forse si separa completamente l'I / O del socket dall'elaborazione dei dati; inviare il payload ai processi di lavoro tramite un meccanismo IPC. OpenSSH e altri strumenti OpenBSD utilizzano questa metodologia (senza thread).
HUAGHAGUAH

3

In Windows (e Linux) è possibile che un processo apra un socket e quindi passi quel socket a un altro processo in modo che anche quel secondo processo possa utilizzare quel socket (e passarlo a sua volta, se lo desidera) .

La chiamata di funzione cruciale è WSADuplicateSocket ().

Questo popola una struttura con informazioni su un socket esistente. Questa struttura quindi, tramite un meccanismo IPC di tua scelta, viene passata a un altro processo esistente (nota che dico esistente - quando chiami WSADuplicateSocket (), devi indicare il processo di destinazione che riceverà le informazioni emesse).

Il processo ricevente può quindi chiamare WSASocket (), passare questa struttura di informazioni e ricevere un handle al socket sottostante.

Entrambi i processi ora tengono un handle per lo stesso socket sottostante.


2

Sembra che quello che vuoi è un processo in ascolto di nuovi clienti e poi trasferire la connessione una volta ottenuta una connessione. Farlo attraverso i thread è facile e in .Net hai anche i metodi BeginAccept ecc. Per occuparti di gran parte dell'impianto idraulico per te. Trasferire le connessioni oltre i confini del processo sarebbe complicato e non avrebbe alcun vantaggio in termini di prestazioni.

In alternativa, è possibile associare più processi e ascoltarli sullo stesso socket.

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 + ".");
}

Se avvii due processi che eseguono ciascuno il codice sopra, funzionerà e il primo processo sembra ottenere tutte le connessioni. Se il primo processo viene interrotto, il secondo ottiene le connessioni. Con la condivisione dei socket in questo modo, non sono sicuro di come Windows decida quale processo ottiene nuove connessioni, sebbene il test rapido punti al processo più vecchio che le ottiene per prime. Non so se condivide se il primo processo è occupato o qualcosa del genere.


2

Un altro approccio (che evita molti dettagli complessi) in Windows se si utilizza HTTP, consiste nell'usare HTTP.SYS . Ciò consente a più processi di ascoltare diversi URL sulla stessa porta. Su Server 2003/2008 / Vista / 7 è così che funziona IIS, quindi puoi condividere le porte con esso. (Su XP SP2 HTTP.SYS è supportato, ma IIS5.1 non lo utilizza.)

Altre API di alto livello (incluso WCF) utilizzano HTTP.SYS.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.