[sockets] 여러 프로세스가 청취 소켓을 공유하는 방법이 있습니까?

소켓 프로그래밍에서 청취 소켓을 만든 다음 연결하는 각 클라이언트에 대해 클라이언트의 요청을 처리하는 데 사용할 수있는 일반 스트림 소켓을 얻습니다. OS는 백그라운드에서 들어오는 연결 대기열을 관리합니다.

기본적으로 두 프로세스는 동시에 같은 포트에 바인딩 할 수 없습니다.

잘 알려진 OS, 특히 Windows에서 프로세스의 여러 인스턴스를 시작하여 모두 소켓에 바인딩되어 효과적으로 대기열을 공유하는 방법이 있는지 궁금합니다. 그러면 각 프로세스 인스턴스는 단일 스레드가 될 수 있습니다. 새 연결을 수락하면 차단됩니다. 클라이언트가 연결되면 유휴 프로세스 인스턴스 중 하나가 해당 클라이언트를 수락합니다.

이를 통해 각 프로세스는 명시적인 공유 메모리를 통하지 않는 한 아무것도 공유하지 않는 매우 간단한 단일 스레드 구현을 가질 수 있으며 사용자는 더 많은 인스턴스를 시작하여 처리 대역폭을 조정할 수 있습니다.

그러한 기능이 있습니까?

편집 : “왜 스레드를 사용하지 않습니까?” 분명히 스레드는 옵션입니다. 그러나 단일 프로세스에 여러 스레드가있는 경우 모든 개체를 공유 할 수 있으며 개체가 공유되지 않거나 한 번에 하나의 스레드에만 표시되는지 또는 절대적으로 변경 불가능하며 가장 널리 사용되는 언어 및 런타임에는 이러한 복잡성을 관리하기위한 기본 제공 지원이 없습니다.

소수의 동일한 작업자 프로세스를 시작하면 기본값 이 공유되지 않는 동시 시스템을 얻을 수 있으므로 정확하고 확장 가능한 구현을 훨씬 쉽게 구축 할 수 있습니다.



답변

Linux와 Windows에서도 두 개 이상의 프로세스간에 소켓을 공유 할 수 있습니다.

Linux (또는 POSIX 유형 OS)에서를 사용 fork()하면 분기 된 자식이 모든 부모 파일 설명 자의 복사본을 갖게됩니다. 닫히지 않은 모든 항목은 계속 공유되며 (예 : TCP 수신 소켓 사용) accept()클라이언트의 새 소켓에 사용될 수 있습니다 . 이것은 대부분의 경우 Apache를 포함하여 작동하는 서버 수입니다.

Windows에서는 fork()시스템 호출이 없기 때문에 부모 프로세스가 CreateProcess자식 프로세스 (물론 동일한 실행 파일을 사용할 수 있음)를 만들거나 상속 가능한 핸들을 전달해야한다는 점을 제외하면 기본적으로 동일합니다.

청취 소켓을 상속 가능한 핸들로 만드는 것은 완전히 사소한 작업은 아니지만 너무 까다 롭지는 않습니다. DuplicateHandle()상속 가능한 플래그가 설정된 중복 핸들을 만드는 데 사용해야합니다 (하지만 여전히 상위 프로세스에 있음). 그런 다음 STARTUPINFO구조의 해당 핸들을 CreateProcess의 하위 프로세스에 STDIN, OUT또는 ERR핸들 (다른 용도로 사용하고 싶지 않다고 가정)으로 지정할 수 있습니다.

편집하다:

MDSN 라이브러리를 읽으면 WSADuplicateSocket이 작업을 수행하는보다 강력하거나 올바른 메커니즘 인 것으로 보입니다 . 부모 / 자식 프로세스가 어떤 핸들이 일부 IPC 메커니즘에 의해 복제되어야하는지 알아 내야하기 때문에 여전히 중요하지 않습니다 (파일 시스템의 파일처럼 간단 할 수 있음).

설명:

OP의 원래 질문에 대한 대답으로, 아니요, 여러 프로세스는 할 수 없습니다 bind(). 단지 원래의 부모 프로세스는 부를 것이다 bind(), listen()등, 자식 프로세스는 단지의 요청 처리하는 것 accept(), send(), recv()


답변

대부분의 다른 사람들은 이것이 작동하는 기술적 이유를 제공했습니다. 다음은이를 직접 입증하기 위해 실행할 수있는 몇 가지 Python 코드입니다.

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

실제로 두 개의 프로세스 ID가 수신하고 있습니다.

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

다음은 텔넷과 프로그램을 실행 한 결과입니다.

$ 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


답변

AF__UNIX 소켓 (프로세스 간 소켓)을 통해 Unix / Linux에서 소켓을 공유 할 수 있음을 추가하고 싶습니다. 발생하는 것처럼 보이는 것은 원래의 별칭과 다소 유사한 새 소켓 설명자가 생성되는 것입니다. 이 새 소켓 설명자는 AFUNIX 소켓을 통해 다른 프로세스로 전송됩니다. 이것은 프로세스가 파일 설명자를 공유하기 위해 fork () 할 수없는 경우에 특히 유용합니다. 예를 들어, 스레딩 문제로 인해이를 방지하는 라이브러리를 사용할 때. Unix 도메인 소켓을 만들고 libancillary 를 사용 하여 설명자를 보내야합니다.

보다:

AF_UNIX 소켓 생성 :

예를 들어 코드 :


답변

이 질문은 MarkR과 zackthehack에 의해 이미 완전히 답변 된 것처럼 보이지만 Nginx가 청취 소켓 상속 모델의 예라고 추가하고 싶습니다.

다음은 좋은 설명입니다.

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

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

NGINX 작업자 프로세스의 흐름

기본 NGINX 프로세스가 구성 파일을 읽고 구성된 작업자 프로세스 수로 분기 한 후 각 작업자 프로세스는 각 소켓 집합에서 이벤트를 기다리는 루프에 들어갑니다.

아직 사용 가능한 연결이 없기 때문에 각 작업자 프로세스는 청취 소켓으로 시작됩니다. 따라서 각 작업자 프로세스에 대해 설정된 이벤트 설명자는 청취 소켓으로 시작됩니다.

(참고) NGINX는 여러 이벤트 폴링 메커니즘 중 하나를 사용하도록 구성 할 수 있습니다. aio / devpoll / epoll / eventpoll / kqueue / poll / rtsig / select

연결이 수신 소켓 (POP3 / IMAP / SMTP)에 도착하면 각 NGINX 작업자 프로세스가 수신 소켓을 상속하므로 각 작업자 프로세스가 이벤트 폴에서 나타납니다. 그런 다음 각 NGINX 작업자 프로세스가 전역 뮤텍스를 획득하려고 시도합니다. 작업자 프로세스 중 하나는 잠금을 획득하고 다른 프로세스는 각각의 이벤트 폴링 루프로 돌아갑니다.

한편, 글로벌 뮤텍스를 획득 한 작업자 프로세스는 트리거 된 이벤트를 검사하고 트리거 된 각 이벤트에 대해 필요한 작업 대기열 요청을 생성합니다. 이벤트는 작업자가 이벤트를 감시하는 설명자 집합의 단일 소켓 설명자에 해당합니다.

트리거 된 이벤트가 새로 들어오는 연결에 해당하는 경우 NGINX는 청취 소켓에서 연결을 수락합니다. 그런 다음 컨텍스트 데이터 구조를 파일 설명자와 연결합니다. 이 컨텍스트는 연결에 대한 정보 (POP3 / IMAP / SMTP 여부, 사용자가 아직 인증되었는지 여부 등)를 보유합니다. 그런 다음 새로 구성된 소켓이 해당 작업자 프로세스에 대한 이벤트 설명자 세트에 추가됩니다.

이제 작업자는 뮤텍스를 포기하고 (다른 작업자에 도착한 모든 이벤트가 처리 될 수 있음을 의미 함) 이전에 대기열에 있던 각 요청을 처리하기 시작합니다. 각 요청은 신호를받은 이벤트에 해당합니다. 신호를받은 각 소켓 설명자에서 작업자 프로세스는 해당 설명자와 이전에 연결되었던 해당 컨텍스트 데이터 구조를 검색 한 다음 해당 연결 상태에 따라 작업을 수행하는 해당 콜백 함수를 호출합니다. 예를 들어, 새로 설정된 IMAP 연결의 경우 NGINX가 가장 먼저 수행하는 작업은
연결된 소켓 에 표준 IMAP 환영 메시지를 쓰는 것입니다 (* OK IMAP4 준비).

각 작업자 프로세스는 미해결 이벤트마다 작업 대기열 항목 처리를 완료하고 이벤트 폴링 루프로 돌아갑니다. 클라이언트와 연결이 설정되면 연결된 소켓이 읽을 준비가 될 때마다 읽기 이벤트가 트리거되고 해당 작업을 수행해야하기 때문에 이벤트는 일반적으로 더 빠릅니다.


답변

이것이 원래 질문과 얼마나 관련이 있는지는 모르겠지만 Linux 커널 3.9에는 TCP / UDP 기능을 추가하는 패치가 있습니다. SO_REUSEPORT 소켓 옵션에 대한 TCP 및 UDP 지원; 새로운 소켓 옵션을 사용하면 동일한 호스트의 여러 소켓을 동일한 포트에 바인딩 할 수 있으며 멀티 코어 시스템에서 실행되는 멀티 스레드 네트워크 서버 응용 프로그램의 성능을 향상시킬 수 있습니다. 자세한 내용은 참조 링크에 언급 된대로 Linux Kernel 3.9 의 LWN 링크 LWN SO_REUSEPORT에서 찾을 수 있습니다 .

SO_REUSEPORT 옵션은 비표준이지만 여러 다른 UNIX 시스템 (특히 아이디어가 시작된 BSD)에서 유사한 형식으로 사용할 수 있습니다. 포크 패턴을 사용하지 않고도 멀티 코어 시스템에서 실행되는 네트워크 애플리케이션에서 최대 성능을 끌어낼 수있는 유용한 대안을 제공하는 것 같습니다.


답변

Linux 3.9부터 소켓에 SO_REUSEPORT를 설정 한 다음 여러 비 관련 프로세스가 해당 소켓을 공유하도록 할 수 있습니다. 그것은 prefork 체계보다 더 간단하고, 더 이상 신호 문제, 하위 프로세스로의 fd 누출 등이 없습니다.

Linux 3.9는 소켓 서버를 작성하는 새로운 방법을 도입했습니다.

SO_REUSEPORT 소켓 옵션


답변

들어오는 연결을 수신하는 것이 유일한 작업 인 단일 작업이 있습니다. 연결이 수신되면 연결을 수락합니다. 이것은 별도의 소켓 설명자를 생성합니다. 수락 된 소켓은 사용 가능한 작업자 작업 중 하나로 전달되고 기본 작업은 다시 수신 상태로 돌아갑니다.

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