¿Hay alguna forma de que varios procesos compartan un socket de escucha?

90

En la programación de sockets, creas un socket de escucha y luego, para cada cliente que se conecta, obtienes un socket de flujo normal que puedes usar para manejar la solicitud del cliente. El sistema operativo gestiona la cola de conexiones entrantes detrás de escena.

Dos procesos no se pueden vincular al mismo puerto al mismo tiempo, por defecto, de todos modos.

Me pregunto si hay una manera (en cualquier sistema operativo conocido, especialmente Windows) de iniciar múltiples instancias de un proceso, de modo que todas se unan al socket y, por lo tanto, compartan efectivamente la cola. Cada instancia de proceso podría ser de un solo subproceso; simplemente se bloquearía al aceptar una nueva conexión. Cuando un cliente se conecta, una de las instancias de proceso inactivas acepta a ese cliente.

Esto permitiría que cada proceso tuviera una implementación de un solo subproceso muy simple, sin compartir nada a menos que a través de una memoria compartida explícita, y el usuario podría ajustar el ancho de banda de procesamiento iniciando más instancias.

¿Existe tal característica?

Editar: Para aquellos que preguntan "¿Por qué no usar hilos?" Obviamente, los hilos son una opción. Pero con varios subprocesos en un solo proceso, todos los objetos se pueden compartir y se debe tener mucho cuidado para garantizar que los objetos no se compartan o que solo sean visibles para un subproceso a la vez, o que sean absolutamente inmutables, y la mayoría de los lenguajes y lenguajes populares los tiempos de ejecución carecen de soporte integrado para gestionar esta complejidad.

Al iniciar un puñado de procesos de trabajo idénticos, obtendría un sistema concurrente en el que el valor predeterminado es no compartir, lo que facilita mucho la creación de una implementación correcta y escalable.

Daniel Earwicker
fuente
2
Estoy de acuerdo, múltiples procesos pueden facilitar la creación de una implementación correcta y sólida. Escalable, no estoy seguro, depende de su dominio problemático.
MarkR

Respuestas:

92

Puede compartir un socket entre dos (o más) procesos en Linux e incluso Windows.

En Linux (o SO tipo POSIX), el uso fork()hará que el hijo bifurcado tenga copias de todos los descriptores de archivo del padre. Cualquiera que no se cierre continuará compartiéndose y (por ejemplo, con un socket de escucha TCP) se puede utilizar para accept()nuevos sockets para clientes. Así es como funcionan muchos servidores, incluido Apache en la mayoría de los casos.

En Windows, lo mismo es básicamente cierto, excepto que no hay una fork()llamada al sistema, por lo que el proceso principal deberá usar CreateProcesso algo para crear un proceso secundario (que, por supuesto, puede usar el mismo ejecutable) y debe pasarle un identificador heredable.

Convertir un socket de escucha en un identificador heredable no es una actividad completamente trivial, pero tampoco demasiado complicada. DuplicateHandle()debe usarse para crear un identificador duplicado (sin embargo, aún en el proceso principal), que tendrá el indicador heredable establecido en él. A continuación, puede dar esa mango en la STARTUPINFOestructura para el proceso hijo en CreateProcess como STDIN, OUTo ERRmango (asumiendo que no quiere usarlo para otra cosa).

EDITAR:

Al leer la biblioteca MDSN, parece que WSADuplicateSocketes un mecanismo más sólido o correcto para hacer esto; todavía no es trivial porque los procesos padre / hijo necesitan determinar qué identificador debe ser duplicado por algún mecanismo de IPC (aunque esto podría ser tan simple como un archivo en el sistema de archivos)

ACLARACIÓN:

En respuesta a la pregunta original del OP, no, múltiples procesos no pueden bind(); sólo el proceso padre original se llame bind(), listen()etc, los procesos hijo se acaba de procesar las solicitudes de accept(), send(), recv()etc.

MarkR
fuente
3
Se pueden vincular varios procesos especificando la opción de socket SocketOptionName.ReuseAddress.
sipwiz
Pero cual es el punto? De todos modos, los procesos son más pesados ​​que los hilos.
Anton Tykhyy
7
Los procesos son más pesados ​​que los subprocesos, pero como solo comparten cosas que se comparten explícitamente, se requiere menos sincronización, lo que facilita la programación e incluso podría ser más eficiente en algunos casos.
MarkR
11
Además, si un proceso hijo falla o se rompe de alguna manera, es menos probable que afecte al padre.
MarkR
3
También es bueno notar que, en Linux, puede "pasar" sockets a otros programas sin usar fork () y no tiene una relación padre / hijo, usando Unix Sockets.
Rahly
34

La mayoría de los demás han proporcionado las razones técnicas por las que esto funciona. Aquí hay un código de Python que puede ejecutar para demostrarlo usted mismo:

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

Tenga en cuenta que, de hecho, hay dos ID de proceso escuchando:

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

Estos son los resultados de ejecutar telnet y el programa:

$ 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
fuente
2
Entonces, para una conexión, el padre o el hijo lo obtienen. Pero quién consigue la conexión es indeterminista, ¿verdad?
Hot.PxL
1
sí, creo que depende de qué proceso esté programado para ejecutarse en el sistema operativo.
Anil Vaitla
14

Me gustaría agregar que los sockets se pueden compartir en Unix / Linux a través de sockets AF__UNIX (sockets entre procesos). Lo que parece suceder es que se crea un nuevo descriptor de socket que es algo así como un alias del original. Este nuevo descriptor de socket se envía a través del socket AFUNIX al otro proceso. Esto es especialmente útil en los casos en que un proceso no puede bifurcar () para compartir sus descriptores de archivo. Por ejemplo, cuando se utilizan bibliotecas que evitan esto debido a problemas de subprocesos. Debe crear un socket de dominio Unix y usar libancillary para enviar el descriptor.

Ver:

Para crear sockets AF_UNIX:

Por ejemplo código:

zachthehack
fuente
13

Parece que MarkR y zackthehack ya han respondido completamente esta pregunta, pero me gustaría agregar que Nginx es un ejemplo del modelo de herencia de socket de escucha.

Aquí hay una buena descripción:

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

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

...

Flujo de un proceso de trabajo NGINX

Después de que el proceso principal de NGINX lee el archivo de configuración y se bifurca en el número configurado de procesos de trabajo, cada proceso de trabajo entra en un ciclo donde espera cualquier evento en su respectivo conjunto de sockets.

Cada proceso de trabajo comienza con solo los sockets de escucha, ya que aún no hay conexiones disponibles. Por lo tanto, el descriptor de eventos establecido para cada proceso de trabajo comienza solo con los sockets de escucha.

(NOTA) NGINX se puede configurar para utilizar cualquiera de varios mecanismos de sondeo de eventos: aio / devpoll / epoll / eventpoll / kqueue / poll / rtsig / select

Cuando llega una conexión a cualquiera de los sockets de escucha (POP3 / IMAP / SMTP), cada proceso de trabajo emerge de su encuesta de eventos, ya que cada proceso de trabajo de NGINX hereda el socket de escucha. Luego, cada proceso de trabajo de NGINX intentará adquirir un mutex global. Uno de los procesos de trabajo adquirirá el bloqueo, mientras que los demás volverán a sus respectivos bucles de sondeo de eventos.

Mientras tanto, el proceso de trabajo que adquirió el mutex global examinará los eventos activados y creará las solicitudes de cola de trabajo necesarias para cada evento que se activó. Un evento corresponde a un descriptor de socket único del conjunto de descriptores desde los que el trabajador estaba observando eventos.

Si el evento disparado corresponde a una nueva conexión entrante, NGINX acepta la conexión desde la toma de escucha. Luego, asocia una estructura de datos de contexto con el descriptor de archivo. Este contexto contiene información sobre la conexión (ya sea POP3 / IMAP / SMTP, si el usuario aún está autenticado, etc.). Luego, este socket recién construido se agrega al conjunto de descriptores de eventos para ese proceso de trabajo.

El trabajador ahora renuncia al mutex (lo que significa que cualquier evento que llegó a otros trabajadores puede continuar) y comienza a procesar cada solicitud que se puso en cola anteriormente. Cada solicitud corresponde a un evento que fue señalado. De cada descriptor de socket que se señaló, el proceso de trabajo recupera la estructura de datos de contexto correspondiente que se asoció anteriormente con ese descriptor, y luego llama a las funciones de devolución de llamada correspondientes que realizan acciones basadas en el estado de esa conexión. Por ejemplo, en el caso de una conexión IMAP recién establecida, lo primero que hará NGINX es escribir el mensaje de bienvenida IMAP estándar en el
conector conectado (* OK IMAP4 listo).

Poco a poco, cada proceso de trabajo completa el procesamiento de la entrada de la cola de trabajo para cada evento pendiente y regresa a su ciclo de sondeo de eventos. Una vez que se establece cualquier conexión con un cliente, los eventos suelen ser más rápidos, ya que siempre que el enchufe conectado está listo para lectura, se dispara el evento de lectura y se debe realizar la acción correspondiente.

richardw
fuente
11

No estoy seguro de cuán relevante es esto para la pregunta original, pero en el kernel 3.9 de Linux hay un parche que agrega una característica TCP / UDP: compatibilidad con TCP y UDP para la opción de socket SO_REUSEPORT; La nueva opción de socket permite que varios sockets en el mismo host se unan al mismo puerto y está destinada a mejorar el rendimiento de las aplicaciones de servidor de red multiproceso que se ejecutan sobre sistemas multinúcleo. se puede encontrar más información en el enlace LWN LWN SO_REUSEPORT en Linux Kernel 3.9 como se menciona en el enlace de referencia:

la opción SO_REUSEPORT no es estándar, pero está disponible en una forma similar en varios otros sistemas UNIX (en particular, los BSD, donde se originó la idea). Parece ofrecer una alternativa útil para obtener el máximo rendimiento de las aplicaciones de red que se ejecutan en sistemas multinúcleo, sin tener que utilizar el patrón de bifurcación.

Walid
fuente
Desde el artículo de LWN, casi parece que SO_REUSEPORTcrea un grupo de subprocesos, donde cada socket está en un subproceso diferente pero solo un socket en el grupo realiza el accept. ¿Puede confirmar que todos los sockets del grupo obtienen una copia de los datos?
jww
3

Tenga una única tarea cuyo único trabajo sea escuchar las conexiones entrantes. Cuando se recibe una conexión, acepta la conexión; esto crea un descriptor de socket separado. El socket aceptado se pasa a una de sus tareas de trabajador disponibles y la tarea principal vuelve a escuchar.

s = socket();
bind(s);
listen(s);
while (1) {
  s2 = accept(s);
  send_to_worker(s2);
}
HUAGHAGUAH
fuente
¿Cómo se pasa el socket a un trabajador? Tenga en cuenta que la idea es que un trabajador es un proceso separado.
Daniel Earwicker
fork () tal vez, o una de las otras ideas anteriores. O quizás separe completamente la E / S del socket del procesamiento de datos; envíe la carga útil a los procesos de trabajo a través de un mecanismo de IPC. OpenSSH y otras herramientas de OpenBSD utilizan esta metodología (sin hilos).
HUAGHAGUAH
3

En Windows (y Linux) es posible que un proceso abra un socket y luego pase ese socket a otro proceso de modo que ese segundo proceso también pueda usar ese socket (y pasarlo a su vez, si así lo desea) .

La llamada de función crucial es WSADuplicateSocket ().

Esto llena una estructura con información sobre un socket existente. Luego, esta estructura, a través de un mecanismo IPC de su elección, se pasa a otro proceso existente (tenga en cuenta que digo existente: cuando llama a WSADuplicateSocket (), debe indicar el proceso de destino que recibirá la información emitida).

El proceso de recepción puede llamar a WSASocket (), pasar esta estructura de información y recibir un identificador al socket subyacente.

Ambos procesos ahora mantienen un identificador en el mismo socket subyacente.


fuente
2

Parece que lo que desea es un proceso que escuche a los nuevos clientes y luego entregue la conexión una vez que obtenga una conexión. Hacer eso a través de subprocesos es fácil y en .Net incluso tiene los métodos BeginAccept, etc. para encargarse de gran parte de la plomería por usted. Transferir las conexiones a través de los límites del proceso sería complicado y no tendría ninguna ventaja de rendimiento.

Alternativamente, puede tener varios procesos enlazados y escuchando en el mismo 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 + ".");
}

Si inicia dos procesos, cada uno ejecutando el código anterior, funcionará y el primer proceso parece obtener todas las conexiones. Si se mata el primer proceso, el segundo obtiene las conexiones. Con el uso compartido de sockets como ese, no estoy seguro de cómo Windows decide exactamente qué proceso obtiene nuevas conexiones, aunque la prueba rápida apunta al proceso más antiguo que las obtiene primero. En cuanto a si comparte si el primer proceso está ocupado o algo así, no lo sé.

sipwiz
fuente
2

Otro enfoque (que evita muchos detalles complejos) en Windows si está usando HTTP, es usar HTTP.SYS . Esto permite que varios procesos escuchen diferentes URL en el mismo puerto. En Server 2003/2008 / Vista / 7, así es como funciona IIS, por lo que puede compartir puertos con él. (En XP SP2, HTTP.SYS es compatible, pero IIS5.1 no lo usa).

Otras API de alto nivel (incluido WCF) utilizan HTTP.SYS.

Ricardo
fuente