Czy istnieje sposób na współdzielenie gniazda nasłuchującego przez wiele procesów?

90

W programowaniu za pomocą gniazd tworzy się gniazdo nasłuchujące, a następnie dla każdego klienta, który się łączy, otrzymuje się normalne gniazdo strumieniowe, którego można użyć do obsługi żądań klienta. System operacyjny zarządza kolejką połączeń przychodzących w tle.

Dwa procesy nie mogą łączyć się z tym samym portem w tym samym czasie - w każdym razie domyślnie.

Zastanawiam się, czy istnieje sposób (w każdym dobrze znanym systemie operacyjnym, zwłaszcza Windows), aby uruchomić wiele instancji procesu, tak aby wszystkie łączyły się z gniazdem i efektywnie współdzieliły kolejkę. Każda instancja procesu może być wtedy jednowątkowa; po prostu blokowałby się przy akceptowaniu nowego połączenia. Gdy klient się połączył, jedna z bezczynnych instancji procesu zaakceptowałaby tego klienta.

Pozwoliłoby to każdemu procesowi mieć bardzo prostą, jednowątkową implementację, nie współużytkując niczego, chyba że poprzez jawną pamięć współdzieloną, a użytkownik byłby w stanie dostosować przepustowość przetwarzania, uruchamiając więcej instancji.

Czy taka funkcja istnieje?

Edycja: dla osób pytających „Dlaczego nie używać wątków?” Oczywiście wątki są opcją. Ale w przypadku wielu wątków w jednym procesie wszystkie obiekty można udostępniać i należy bardzo uważać, aby obiekty albo nie były udostępniane, albo były widoczne tylko dla jednego wątku na raz lub były absolutnie niezmienne, a najpopularniejsze języki i Środowiskom wykonawczym brakuje wbudowanej obsługi zarządzania tą złożonością.

Uruchamiając kilka identycznych procesów roboczych, można uzyskać system współbieżny, w którym domyślnie nie ma współużytkowania, co znacznie ułatwia zbudowanie poprawnej i skalowalnej implementacji.

Daniel Earwicker
źródło
2
Zgadzam się, wiele procesów może ułatwić stworzenie poprawnej i solidnej implementacji. Skalowalny, nie jestem pewien, zależy to od domeny, z którą masz problem.
MarkR

Odpowiedzi:

92

Możesz współdzielić gniazdo między dwoma (lub więcej) procesami w systemie Linux, a nawet Windows.

W Linuksie (lub systemie operacyjnym typu POSIX) użycie fork()spowoduje, że rozwidlone dziecko będzie miało kopie wszystkich deskryptorów plików rodzica. Wszystko, czego nie zamknie, będzie nadal udostępniane i (na przykład z gniazdem nasłuchującym TCP) może być używane do accept()nowych gniazd dla klientów. Tak działa wiele serwerów, w tym w większości przypadków Apache.

W systemie Windows to samo jest w zasadzie prawdą, z wyjątkiem tego, że nie ma fork()wywołania systemowego, więc proces nadrzędny będzie musiał użyć CreateProcesslub czegoś, aby utworzyć proces potomny (który może oczywiście używać tego samego pliku wykonywalnego) i musi przekazać mu dziedziczony uchwyt.

Uczynienie gniazda nasłuchującego uchwytem dziedzicznym nie jest całkowicie trywialną czynnością, ale też nie jest zbyt trudne. DuplicateHandle()musi być użyty do stworzenia zduplikowanego dojścia (jednak nadal w procesie nadrzędnym), który będzie miał ustawioną dziedziczoną flagę. Następnie możesz przekazać ten uchwyt w STARTUPINFOstrukturze procesowi potomnemu w CreateProcess jako uchwyt STDIN, OUTlub ERRuchwyt (zakładając, że nie chcesz go używać do niczego innego).

EDYTOWAĆ:

Czytając bibliotekę MDSN, okazuje się, że WSADuplicateSocketjest to bardziej solidny lub poprawny mechanizm robienia tego; jest to nadal nietrywialne, ponieważ procesy nadrzędne / potomne muszą wypracować, który uchwyt musi zostać zduplikowany przez jakiś mechanizm IPC (chociaż może to być tak proste, jak plik w systemie plików)

WYJAŚNIENIE:

Odpowiadając na pierwotne pytanie PO: nie, wiele procesów nie może bind(); tylko oryginalny proces rodzic nazwałby bind(), listen()etc, procesy potomne po prostu przetworzyć prośby accept(), send(), recv()itd.

MarkR
źródło
3
Wiele procesów można powiązać, określając opcję gniazda SocketOptionName.ReuseAddress.
Aaron Clauson
Ale o co chodzi? I tak procesy są cięższe niż wątki.
Anton Tykhyy
7
Procesy są cięższe niż wątki, ale ponieważ współużytkują tylko rzeczy jawnie udostępnione, wymagana jest mniejsza synchronizacja, co ułatwia programowanie, a w niektórych przypadkach może być nawet bardziej wydajne.
MarkR
11
Ponadto, jeśli proces dziecka ulegnie awarii lub zepsuje się w jakiś sposób, jest mniej prawdopodobne, że wpłynie to na rodzica.
MarkR
4
Warto również zauważyć, że w Linuksie możesz "przekazywać" gniazda do innych programów bez użycia fork () i nie ma relacji rodzic / dziecko, używając gniazd Unix.
Rahly,
35

Większość innych podała techniczne powody, dla których to działa. Oto kod w Pythonie, który możesz uruchomić, aby to zademonstrować:

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

Zauważ, że rzeczywiście nasłuchują dwa identyfikatory procesów:

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

Oto wyniki działania telnetu i programu:

$ 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
źródło
2
Tak więc w przypadku jednego połączenia otrzymuje go rodzic lub dziecko. Ale kto otrzymuje połączenie, jest nieokreślony, prawda?
Hot.PxL
1
tak, myślę, że to zależy od tego, jaki proces jest zaplanowany do uruchomienia przez system operacyjny.
Anil Vaitla
14

Chciałbym dodać, że gniazda mogą być współużytkowane w systemie Unix / Linux przez gniazda AF__UNIX (gniazda międzyprocesowe). Wydaje się, że zostaje utworzony nowy deskryptor gniazda, który jest w pewnym sensie aliasem do oryginalnego. Ten nowy deskryptor gniazda jest wysyłany przez gniazdo AFUNIX do innego procesu. Jest to szczególnie przydatne w przypadkach, gdy proces nie może fork () udostępniać swoich deskryptorów plików. Na przykład podczas korzystania z bibliotek, które zapobiegają temu z powodu problemów z wątkami. Powinieneś utworzyć gniazdo domeny Unix i użyć libancillary do przesłania deskryptora.

Widzieć:

Aby utworzyć gniazda AF_UNIX:

Na przykład kod:

zachthehack
źródło
13

Wygląda na to, że MarkR i zackthehack w pełni odpowiedzieli na to pytanie, ale chciałbym dodać, że Nginx jest przykładem modelu dziedziczenia gniazd nasłuchowych.

Oto dobry opis:

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

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

...

Przepływ procesu roboczego NGINX

Po tym, jak główny proces NGINX wczyta plik konfiguracyjny i rozwidli skonfigurowaną liczbę procesów roboczych, każdy proces roboczy wchodzi w pętlę, w której czeka na wszelkie zdarzenia w odpowiednim zestawie gniazd.

Każdy proces roboczy rozpoczyna się tylko od gniazd nasłuchujących, ponieważ nie ma jeszcze dostępnych połączeń. Dlatego deskryptor zdarzenia ustawiony dla każdego procesu roboczego zaczyna się od samych gniazd nasłuchujących.

(UWAGA) NGINX można skonfigurować do używania jednego z kilku mechanizmów odpytywania zdarzeń: aio / devpoll / epoll / eventpoll / kqueue / poll / rtsig / select

Gdy połączenie dociera do któregokolwiek z gniazd nasłuchujących (POP3 / IMAP / SMTP), każdy proces roboczy wyłania się z jego sondowania zdarzeń, ponieważ każdy proces roboczy NGINX dziedziczy gniazdo nasłuchujące. Następnie każdy proces roboczy NGINX będzie próbował uzyskać globalny mutex. Jeden z procesów roboczych uzyska blokadę, podczas gdy inne wrócą do odpowiednich pętli odpytywania zdarzeń.

W międzyczasie proces roboczy, który uzyskał globalny mutex, zbada wyzwalane zdarzenia i utworzy niezbędne żądania w kolejce roboczej dla każdego wyzwalanego zdarzenia. Zdarzenie odpowiada pojedynczemu deskryptorowi gniazda z zestawu deskryptorów, z którego obserwował pracownik.

Jeśli wyzwalane zdarzenie odpowiada nowemu połączeniu przychodzącemu, NGINX akceptuje połączenie z gniazda nasłuchującego. Następnie wiąże strukturę danych kontekstu z deskryptorem pliku. Ten kontekst zawiera informacje o połączeniu (czy jest to POP3 / IMAP / SMTP, czy użytkownik jest jeszcze uwierzytelniony itp.). Następnie to nowo utworzone gniazdo jest dodawane do deskryptora zdarzenia ustawionego dla tego procesu roboczego.

Pracownik porzuca teraz muteks (co oznacza, że ​​wszelkie zdarzenia, które dotarły do ​​innych pracowników, mogą zostać przetworzone) i rozpoczyna przetwarzanie każdego żądania, które było wcześniej w kolejce. Każde żądanie odpowiada zdarzeniu, które zostało zasygnalizowane. Z każdego sygnalizowanego deskryptora gniazda proces roboczy pobiera odpowiednią strukturę danych kontekstu, która była wcześniej skojarzona z tym deskryptorem, a następnie wywołuje odpowiednie funkcje wywołania zwrotnego, które wykonują działania na podstawie stanu tego połączenia. Na przykład, w przypadku nowo nawiązanego połączenia IMAP, pierwszą rzeczą, którą zrobi NGINX, jest napisanie standardowego komunikatu powitalnego IMAP do
podłączonego gniazda (* OK IMAP4 gotowy).

Po pewnym czasie każdy proces roboczy kończy przetwarzanie pozycji kolejki roboczej dla każdego zaległego zdarzenia i wraca do swojej pętli sondowania zdarzeń. Po ustanowieniu połączenia z klientem zdarzenia są zwykle szybsze, ponieważ zawsze, gdy podłączone gniazdo jest gotowe do odczytu, wyzwalane jest zdarzenie odczytu i należy podjąć odpowiednią akcję.

richardw
źródło
11

Nie jestem pewien, jak istotne jest to w stosunku do pierwotnego pytania, ale w jądrze Linuksa 3.9 jest łatka dodająca funkcję TCP / UDP: obsługa TCP i UDP dla opcji gniazda SO_REUSEPORT; Nowa opcja gniazda umożliwia powiązanie wielu gniazd na tym samym hoście z tym samym portem i ma na celu poprawę wydajności wielowątkowych aplikacji serwera sieciowego działających na systemach wielordzeniowych. więcej informacji można znaleźć w linku LWN LWN SO_REUSEPORT w Linux Kernel 3.9, jak wspomniano w odnośniku referencyjnym:

opcja SO_REUSEPORT jest niestandardowa, ale dostępna w podobnej formie w wielu innych systemach UNIX (w szczególności BSD, skąd pomysł). Wydaje się, że jest to użyteczna alternatywa dla wyciskania maksymalnej wydajności z aplikacji sieciowych działających w systemach wielordzeniowych, bez konieczności używania wzorca rozwidlenia.

Walid
źródło
Z artykułu LWN wygląda prawie na to, że SO_REUSEPORTtworzy pulę wątków, w której każde gniazdo znajduje się w innym wątku, ale tylko jedno gniazdo w grupie wykonuje accept. Czy możesz potwierdzić, że wszystkie gniazda w grupie otrzymują kopię danych?
jww
3

Miej jedno zadanie, którego jedynym zadaniem jest nasłuchiwanie połączeń przychodzących. Po odebraniu połączenia akceptuje połączenie - tworzy to oddzielny deskryptor gniazda. Zaakceptowane gniazdo jest przekazywane do jednego z dostępnych zadań roboczych, a zadanie główne wraca do nasłuchiwania.

s = socket();
bind(s);
listen(s);
while (1) {
  s2 = accept(s);
  send_to_worker(s2);
}
HUAGHAGUAH
źródło
W jaki sposób gniazdo jest przekazywane pracownikowi? Pamiętaj, że chodzi o to, że pracownik to oddzielny proces.
Daniel Earwicker,
fork () być może lub jeden z innych pomysłów powyżej. A może całkowicie oddzielisz wejście / wyjście gniazda od przetwarzania danych; wysyłanie ładunku do procesów roboczych za pośrednictwem mechanizmu IPC. OpenSSH i inne narzędzia OpenBSD używają tej metodologii (bez wątków).
HUAGHAGUAH
3

W systemie Windows (i Linux) jeden proces może otworzyć gniazdo, a następnie przekazać to gniazdo innemu procesowi, tak że ten drugi proces może również użyć tego gniazda (i przekazać je po kolei, jeśli chce to zrobić) .

Kluczowym wywołaniem funkcji jest WSADuplicateSocket ().

Spowoduje to wypełnienie struktury informacjami o istniejącym gnieździe. Ta struktura następnie, poprzez wybrany przez ciebie mechanizm IPC, jest przekazywana do innego istniejącego procesu (uwaga, mówię istniejący - kiedy wywołujesz WSADuplicateSocket (), musisz wskazać proces docelowy, który otrzyma wyemitowane informacje).

Proces odbierający może następnie wywołać WSASocket (), przekazując tę ​​strukturę informacji i otrzymać uchwyt do odpowiedniego gniazda.

Oba procesy mają teraz uchwyt do tego samego podstawowego gniazda.


źródło
2

Wygląda na to, że chcesz, aby jeden proces nasłuchiwał nowych klientów, a następnie przekazywał połączenie po uzyskaniu połączenia. Aby to zrobić między wątkami jest łatwe, a w .Net masz nawet metody BeginAccept itp., Aby zająć się dużą ilością instalacji hydraulicznych za Ciebie. Przekazywanie połączeń między granicami procesu byłoby skomplikowane i nie miałoby żadnych korzyści w zakresie wydajności.

Alternatywnie możesz mieć wiele procesów powiązanych i nasłuchujących w tym samym gnieździe.

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

Jeśli uruchomisz dwa procesy, z których każdy wykonuje powyższy kod, zadziała, a pierwszy proces wydaje się pobierać wszystkie połączenia. Jeśli pierwszy proces zostanie zabity, drugi pobierze połączenia. Przy takim udostępnianiu gniazd nie jestem pewien, w jaki sposób system Windows decyduje, który proces uzyskuje nowe połączenia, chociaż szybki test wskazuje, że najstarszy proces uzyskuje je jako pierwszy. Nie wiem, czy udostępnia, czy pierwszy proces jest zajęty, czy coś podobnego.

Aaron Clauson
źródło
2

Innym podejściem (które pozwala uniknąć wielu złożonych szczegółów) w systemie Windows, jeśli używasz protokołu HTTP, jest użycie protokołu HTTP.SYS . Umożliwia to wielu procesom nasłuchiwanie różnych adresów URL na tym samym porcie. Na serwerze 2003/2008 / Vista / 7 tak działają usługi IIS, więc możesz udostępniać mu porty. (W systemie XP SP2 obsługiwany jest protokół HTTP.SYS, ale IIS5.1 go nie używa).

Inne interfejsy API wysokiego poziomu (w tym WCF) korzystają z HTTP.SYS.

Richard
źródło