Cómo prevenir los SIGPIPE (o manejarlos adecuadamente)

261

Tengo un pequeño programa de servidor que acepta conexiones en un socket TCP o local de UNIX, lee un comando simple y, dependiendo del comando, envía una respuesta. El problema es que el cliente puede no tener interés en la respuesta a veces y sale temprano, por lo que escribir en ese socket provocará un SIGPIPE y hará que mi servidor se bloquee. ¿Cuál es la mejor práctica para evitar el accidente aquí? ¿Hay alguna manera de verificar si el otro lado de la línea todavía está leyendo? (select () no parece funcionar aquí, ya que siempre dice que el socket es grabable). ¿O debería atrapar el SIGPIPE con un controlador e ignorarlo?

jkramer
fuente

Respuestas:

255

En general, desea ignorar SIGPIPEy manejar el error directamente en su código. Esto se debe a que los manejadores de señales en C tienen muchas restricciones sobre lo que pueden hacer.

La forma más portátil de hacer esto es configurar el SIGPIPEcontrolador en SIG_IGN. Esto evitará que cualquier escritura de socket o tubería provoque una SIGPIPEseñal.

Para ignorar la SIGPIPEseñal, use el siguiente código:

signal(SIGPIPE, SIG_IGN);

Si está usando la send()llamada, otra opción es usar la MSG_NOSIGNALopción, que desactivará el SIGPIPEcomportamiento por llamada. Tenga en cuenta que no todos los sistemas operativos admiten la MSG_NOSIGNALbandera.

Por último, es posible que también desee considerar el SO_SIGNOPIPEindicador de socket que se puede configurar setsockopt()en algunos sistemas operativos. Esto evitará que SIGPIPEsea ​​causado por escrituras solo en los sockets en los que está configurado.

dvorak
fuente
75
Para hacer esto explícito: señal (SIGPIPE, SIG_IGN);
jhclark
55
Esto puede ser necesario si obtiene un código de retorno de salida de 141 (128 + 13: SIGPIPE). SIG_IGN es el controlador de señal de ignorar.
velcrow
66
Para los sockets, es realmente más fácil usar send () con MSG_NOSIGNAL, como dijo @talash.
Pawel Veselov
3
¿Qué sucede exactamente si mi programa escribe en una tubería rota (un socket en mi caso)? SIG_IGN hace que el programa ignore SIG_PIPE, pero ¿eso hace que send () haga algo indeseable?
sudo
2
Vale la pena mencionarlo, ya que lo encontré una vez, luego lo olvidé más tarde y me confundí, luego lo descubrí por segunda vez. Los depuradores (sé que gdb lo hace) anulan el manejo de su señal, por lo que las señales ignoradas no se ignoran. Pruebe su código fuera de un depurador para asegurarse de que SIGPIPE ya no ocurra. stackoverflow.com/questions/6821469/…
Jetski S-type
155

Otro método es cambiar el socket para que nunca genere SIGPIPE en write (). Esto es más conveniente en las bibliotecas, donde es posible que no desee un controlador de señal global para SIGPIPE.

En la mayoría de los sistemas basados ​​en BSD (MacOS, FreeBSD ...), (suponiendo que esté usando C / C ++), puede hacer esto con:

int set = 1;
setsockopt(sd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));

Con esto en efecto, en lugar de que se genere la señal SIGPIPE, se devolverá EPIPE.

usuario55807
fuente
45
Esto suena bien, pero SO_NOSIGPIPE no parece existir en Linux, por lo que no es una solución ampliamente portátil.
nobar
1
hola a todos, ¿pueden decirme en qué lugar tengo que poner este código? No entiendo gracias
R. Dewi
9
En Linux, veo la opción MSG_NOSIGNAL en las banderas de envío
Aadishri
Funciona perfecto en Mac OS X
kirugan
116

Llego súper tarde a la fiesta, pero SO_NOSIGPIPEno es portátil y es posible que no funcione en su sistema (parece ser algo BSD).

Una buena alternativa si está en, digamos, un sistema Linux sin SO_NOSIGPIPEsería establecer el MSG_NOSIGNALindicador en su llamada send (2).

Ejemplo reemplazado write(...)por send(...,MSG_NOSIGNAL)(ver el comentario de nobar )

char buf[888];
//write( sockfd, buf, sizeof(buf) );
send(    sockfd, buf, sizeof(buf), MSG_NOSIGNAL );
Sklnd
fuente
55
En otras palabras, use send (..., MSG_NOSIGNAL) como reemplazo de write () y no obtendrá SIGPIPE. Esto debería funcionar bien para los sockets (en plataformas compatibles), pero send () parece estar limitado al uso con sockets (no tuberías), por lo que esta no es una solución general al problema SIGPIPE.
nobar
@ Ray2k ¿Quién demonios sigue desarrollando aplicaciones para Linux <2.2? Esa es en realidad una pregunta semi-seria; la mayoría de las distribuciones se envían con al menos 2.6.
Parthian Shot
1
@Parthian Shot: Deberías repensar tu respuesta. El mantenimiento de sistemas embebidos antiguos es una razón válida para cuidar las versiones anteriores de Linux.
kirsche40
1
@ kirsche40 No soy el OP. Solo tenía curiosidad.
Parthian Shot
@sklnd esto me funcionó perfectamente. Estoy usando un QTcpSocketobjeto Qt para envolver el uso de sockets, tuve que reemplazar la writellamada al método por un sistema operativo send(usando el socketDescriptormétodo). ¿Alguien sabe una opción más limpia para establecer esta opción en una QTcpSocketclase?
Emilio González Montaña
29

En esto publicación describí una posible solución para el caso de Solaris cuando ni SO_NOSIGPIPE ni MSG_NOSIGNAL están disponibles.

En cambio, tenemos que suprimir temporalmente SIGPIPE en el hilo actual que ejecuta el código de la biblioteca. Aquí se explica cómo hacerlo: para suprimir SIGPIPE, primero verificamos si está pendiente. Si lo hace, esto significa que está bloqueado en este hilo, y no tenemos que hacer nada. Si la biblioteca genera SIGPIPE adicional, se fusionará con la pendiente, y eso es un no-op. Si SIGPIPE no está pendiente, lo bloqueamos en este hilo y también verificamos si ya estaba bloqueado. Entonces somos libres de ejecutar nuestras escrituras. Cuando debemos restaurar SIGPIPE a su estado original, hacemos lo siguiente: si SIGPIPE estaba pendiente originalmente, no hacemos nada. De lo contrario, verificamos si está pendiente ahora. Si lo hace (lo que significa que nuestras acciones han generado uno o más SIGPIPE), lo esperamos en este hilo, limpiando así su estado pendiente (para hacer esto usamos sigtimedwait () con tiempo de espera cero; esto es para evitar el bloqueo en un escenario donde el usuario malintencionado envió SIGPIPE manualmente a un proceso completo: en este caso lo veremos pendiente, pero otro hilo puede manejarlo antes de que tengamos un cambio para esperarlo). Después de borrar el estado pendiente, desbloqueamos SIGPIPE en este hilo, pero solo si no se bloqueó originalmente.

Código de ejemplo en https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156

kroki
fuente
22

Manejar SIGPIPE localmente

Por lo general, es mejor manejar el error localmente en lugar de en un controlador de eventos de señal global, ya que localmente tendrá más contexto sobre lo que está sucediendo y qué recurso tomar.

Tengo una capa de comunicación en una de mis aplicaciones que permite que mi aplicación se comunique con un accesorio externo. Cuando se produce un error de escritura, lanzo una excepción en la capa de comunicación y dejo que se forme un bloque de prueba para manejarlo allí.

Código:

El código para ignorar una señal SIGPIPE para que pueda manejarlo localmente es:

// We expect write failures to occur but we want to handle them where 
// the error occurs rather than in a SIGPIPE handler.
signal(SIGPIPE, SIG_IGN);

Este código evitará que se active la señal SIGPIPE, pero recibirá un error de lectura / escritura cuando intente usar el socket, por lo que deberá verificarlo.

Sam
fuente
¿Debería hacerse esta llamada después de conectar un enchufe o antes de eso? ¿Dónde es mejor colocar? Gracias
Ahmed
@Ahmed Yo personalmente puse en applicationDidFinishLaunching: . Lo principal es que debería ser antes de interactuar con una conexión de socket.
Sam
pero recibirá un error de lectura / escritura cuando intente usar el socket, por lo que deberá verificarlo. - ¿Puedo preguntar cómo es esto posible? la señal (SIGPIPE, SIG_IGN) funciona para mí, pero en el modo de depuración devuelve un error ¿es posible ignorar también ese error?
jongbanaag
@ Dreyfus15 Creo que acabo de terminar las llamadas usando el socket en un bloque try / catch para manejarlo localmente. Ha pasado un tiempo desde que vi esto.
Sam
15

No puede evitar que el proceso en el otro extremo de una tubería salga, y si sale antes de que termine de escribir, obtendrá una señal SIGPIPE. Si SIG_IGN la señal, entonces su escritura volverá con un error, y debe anotar y reaccionar ante ese error. Simplemente atrapar e ignorar la señal en un controlador no es una buena idea: debe tener en cuenta que la tubería ahora está desactivada y modificar el comportamiento del programa para que no vuelva a escribir en la tubería (porque la señal se generará nuevamente y se ignorará nuevamente, y lo intentará nuevamente, y todo el proceso podría continuar durante mucho tiempo y desperdiciar mucha energía de la CPU).

Jonathan Leffler
fuente
6

¿O debería atrapar el SIGPIPE con un controlador e ignorarlo?

Creo que eso es correcto. Desea saber cuándo el otro extremo ha cerrado su descriptor y eso es lo que SIGPIPE le dice.

Sam

Sam Reynolds
fuente
3

El manual de Linux dijo:

EPIPE El extremo local se ha apagado en un zócalo orientado a la conexión. En este caso, el proceso también recibirá un SIGPIPE a menos que se establezca MSG_NOSIGNAL.

Pero para Ubuntu 12.04 no está bien. Escribí una prueba para ese caso y siempre recibo EPIPE sin SIGPIPE. SIGPIPE se genera si intento escribir en el mismo socket roto por segunda vez. Por lo tanto, no necesita ignorar SIGPIPE si se produce esta señal, significa un error lógico en su programa.

talash
fuente
1
Definitivamente recibo SIGPIPE sin ver primero EPIPE en un socket en Linux. Esto suena como un error del kernel.
davmac
3

¿Cuál es la mejor práctica para evitar el accidente aquí?

O deshabilite los sigpipes según todos, o atrape e ignore el error.

¿Hay alguna manera de verificar si el otro lado de la línea todavía está leyendo?

Sí, use select ().

select () no parece funcionar aquí, ya que siempre dice que el socket es grabable.

Debe seleccionar en los bits de lectura . Probablemente pueda ignorar los bits de escritura .

Cuando el extremo lejano cierra su identificador de archivo, select le indicará que hay datos listos para leer. Cuando vaya y lea eso, obtendrá 0 bytes, que es cómo el sistema operativo le dice que el identificador de archivo se ha cerrado.

El único momento en que no puede ignorar los bits de escritura es si está enviando grandes volúmenes, y existe el riesgo de que el otro extremo se acumule, lo que puede hacer que se llenen sus memorias intermedias. Si eso sucede, intentar escribir en el identificador del archivo puede hacer que su programa / hilo se bloquee o falle. Probar select antes de escribir lo protegerá de eso, pero no garantiza que el otro extremo esté sano o que sus datos lleguen.

Tenga en cuenta que puede obtener un sigpipe de close (), así como cuando escribe.

Cerrar elimina todos los datos almacenados en el búfer. Si el otro extremo ya se ha cerrado, entonces cerrará fallará y recibirá un letrero.

Si está utilizando TCPIP almacenado, entonces una escritura exitosa solo significa que sus datos se han puesto en cola para enviar, no significa que se hayan enviado. Hasta que cierre correctamente, no sabe que sus datos han sido enviados.

Sigpipe te dice que algo salió mal, no te dice qué o qué debes hacer al respecto.

Ben Aveling
fuente
Sigpipe te dice que algo salió mal, no te dice qué o qué debes hacer al respecto. Exactamente. Prácticamente el único propósito de SIGPIPE es derribar las utilidades de línea de comandos canalizadas cuando la siguiente etapa ya no necesita la entrada. Si su programa está haciendo trabajo en red, generalmente debería bloquear o ignorar todo el programa SIGPIPE.
DepressedDaniel
Precisamente. SIGPIPE está destinado a situaciones como "encontrar XXX | cabeza". Una vez que la cabeza ha coincidido con sus 10 líneas, no tiene sentido continuar con el hallazgo. Entonces la cabeza sale, y la próxima vez que find intenta hablar con ella, recibe un sello y sabe que también puede salir.
Ben Aveling
¡Realmente, el único propósito de SIGPIPE es permitir que los programas sean descuidados y no verificar errores en las escrituras!
William Pursell
2

En un sistema POSIX moderno (es decir, Linux), puede usar la sigprocmask()función.

#include <signal.h>

void block_signal(int signal_to_block /* i.e. SIGPIPE */ )
{
    sigset_t set;
    sigset_t old_state;

    // get the current state
    //
    sigprocmask(SIG_BLOCK, NULL, &old_state);

    // add signal_to_block to that existing state
    //
    set = old_state;
    sigaddset(&set, signal_to_block);

    // block that signal also
    //
    sigprocmask(SIG_BLOCK, &set, NULL);

    // ... deal with old_state if required ...
}

Si desea restaurar el estado anterior más tarde, asegúrese de guardar el old_statelugar seguro. Si llama a esa función varias veces, debe usar una pila o guardar solo la primera o la últimaold_state ... o tal vez tener una función que elimine una señal bloqueada específica.

Para más información lea la página del manual .

Alexis Wilke
fuente
No es necesario leer, modificar y escribir el conjunto de señales bloqueadas como esta. sigprocmaskse agrega al conjunto de señales bloqueadas, por lo que puede hacer todo esto con una sola llamada.
Luchs
No leo-modifico-escribo , leo para guardar el estado actual que mantengo old_statepara poder restaurarlo más tarde, si así lo decido. Si sabe que no necesitará restaurar el estado, no hay necesidad de leerlo y almacenarlo de esta manera.
Alexis Wilke
1
Pero sí: la primera llamada para sigprocmask()leer el estado anterior, la llamada para sigaddsetmodificarlo, la segunda llamada para sigprocmask()escribirlo. Puede eliminar la primera llamada, inicializar sigemptyset(&set)y cambiar la segunda llamada a sigprocmask(SIG_BLOCK, &set, &old_state).
Luchs
Ah! ¡Jaja! Tienes razón. Hago. Bueno ... en mi software, no sé qué señales ya he bloqueado o no. Entonces lo hago de esta manera. Sin embargo, estoy de acuerdo en que si solo tiene un "conjunto de señales de esta manera", un simple conjunto claro + es suficiente.
Alexis Wilke