Como evitar SIGPIPEs (ou manipulá-los adequadamente)

261

Eu tenho um pequeno programa de servidor que aceita conexões em um soquete TCP ou UNIX local, lê um comando simples e, dependendo do comando, envia uma resposta. O problema é que o cliente pode não ter interesse na resposta algumas vezes e sai mais cedo; portanto, gravar nesse soquete causará um SIGPIPE e causará uma falha no servidor. Qual é a melhor prática para evitar a falha aqui? Existe uma maneira de verificar se o outro lado da linha ainda está lendo? (select () parece não funcionar aqui, pois sempre diz que o soquete é gravável). Ou devo apenas pegar o SIGPIPE com um manipulador e ignorá-lo?

jkramer
fonte

Respostas:

255

Em geral, você deseja ignorar SIGPIPEe manipular o erro diretamente no seu código. Isso ocorre porque os manipuladores de sinal em C têm muitas restrições sobre o que podem fazer.

A maneira mais portátil de fazer isso é definir o SIGPIPEmanipulador para SIG_IGN. Isso impedirá que qualquer gravação de soquete ou tubo cause um SIGPIPEsinal.

Para ignorar o SIGPIPEsinal, use o seguinte código:

signal(SIGPIPE, SIG_IGN);

Se você estiver usando a send()chamada, outra opção é usá-la MSG_NOSIGNAL, o que SIGPIPEdesativará o comportamento por chamada. Observe que nem todos os sistemas operacionais suportam o MSG_NOSIGNALsinalizador.

Por fim, você também pode considerar o SO_SIGNOPIPEsinalizador de soquete que pode ser definido setsockopt()em alguns sistemas operacionais. Isso evitará que SIGPIPEseja causado por gravações apenas nos soquetes em que está definido.

dvorak
fonte
75
Para tornar isso explícito: signal (SIGPIPE, SIG_IGN);
Jhclark
5
Isso pode ser necessário se você estiver obtendo um código de retorno de saída 141 (128 + 13: SIGPIPE). SIG_IGN é o manipulador de sinal de ignorar.
Velcrow
6
Para soquetes, é realmente mais fácil usar send () com MSG_NOSIGNAL, como disse @talash.
Pawel Veselov
3
O que exatamente acontece se meu programa gravar em um cano quebrado (um soquete no meu caso)? SIG_IGN faz com que o programa ignore o SIG_PIPE, mas isso resulta em send () fazendo algo indesejável?
Sudo
2
Vale mencionar, já que eu o encontrei uma vez, esqueci depois e fiquei confuso, depois descobri uma segunda vez. Depuradores (eu sei que o gdb) substituem o tratamento de sinais, portanto, os sinais ignorados não são ignorados! Teste seu código fora de um depurador para garantir que o SIGPIPE não ocorra mais. stackoverflow.com/questions/6821469/…
Jetski S-type
155

Outro método é alterar o soquete para que nunca gere SIGPIPE em write (). Isso é mais conveniente em bibliotecas, nas quais você pode não querer um manipulador de sinal global para o SIGPIPE.

Na maioria dos sistemas baseados em BSD (MacOS, FreeBSD ...), (supondo que você esteja usando C / C ++), você pode fazer isso com:

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

Com isso, em vez de o sinal SIGPIPE ser gerado, o EPIPE será retornado.

user55807
fonte
45
Parece bom, mas o SO_NOSIGPIPE parece não existir no Linux - portanto, não é uma solução amplamente portátil.
dec2
1
oi tudo, você pode me dizer onde lugar que eu tenho que colocar esse código? Eu não entendo o obrigado 's
R. Dewi 12/12
9
No Linux, vejo a opção MSG_NOSIGNAL nos sinalizadores de envio
Aadishri
Funciona perfeito no Mac OS X
kirugan
116

Estou muito atrasado para a festa, mas SO_NOSIGPIPEnão é portátil e pode não funcionar no seu sistema (parece ser uma coisa do BSD).

Uma boa alternativa se você estiver, digamos, em um sistema Linux sem, SO_NOSIGPIPEseria definir o MSG_NOSIGNALsinalizador na sua chamada send (2).

Exemplo de substituição write(...)por send(...,MSG_NOSIGNAL)(consulte o comentário de nobar )

char buf[888];
//write( sockfd, buf, sizeof(buf) );
send(    sockfd, buf, sizeof(buf), MSG_NOSIGNAL );
sklnd
fonte
5
Em outras palavras, use send (..., MSG_NOSIGNAL) como um substituto para write () e você não obterá o SIGPIPE. Isso deve funcionar bem para soquetes (em plataformas suportadas), mas o send () parece ser limitado ao uso com soquetes (não pipes), portanto, essa não é uma solução geral para o problema do SIGPIPE.
Nobar
@ Ray2k Quem diabos ainda está desenvolvendo aplicativos para Linux <2.2? Essa é realmente uma pergunta semi-séria; a maioria das distribuições é fornecida com pelo menos 2,6.
Tiro parta
1
@Parthian Shot: Você deve repensar sua resposta. A manutenção de sistemas embarcados antigos é um motivo válido para cuidar de versões mais antigas do Linux.
precisa saber é o seguinte
1
@ kirsche40 Eu não sou o OP- eu estava apenas curioso.
Parthian Shot
@ sklnd isso funcionou perfeitamente para mim. Estou usando um QTcpSocketobjeto Qt para quebrar o uso de soquetes, tive que substituir a writechamada de método por um sistema operacional send(usando o socketDescriptormétodo). Alguém conhece uma opção mais limpa para definir essa opção em uma QTcpSocketclasse?
Emilio González Montaña
29

Neste post , descrevi uma possível solução para o caso Solaris quando nem SO_NOSIGPIPE nem MSG_NOSIGNAL estão disponíveis.

Em vez disso, precisamos suprimir temporariamente o SIGPIPE no encadeamento atual que executa o código da biblioteca. Veja como fazer isso: para suprimir o SIGPIPE, primeiro verificamos se ele está pendente. Caso isso aconteça, isso significa que está bloqueado neste segmento, e não precisamos fazer nada. Se a biblioteca gerar SIGPIPE adicional, ela será mesclada com a pendente, e isso não funcionará. Se o SIGPIPE não estiver pendente, o bloquearemos neste segmento e também verificaremos se ele já estava bloqueado. Então, somos livres para executar nossas gravações. Quando devemos restaurar o SIGPIPE ao seu estado original, fazemos o seguinte: se o SIGPIPE estava pendente originalmente, não fazemos nada. Caso contrário, verificamos se está pendente agora. Se sim (o que significa que as ações externas geraram um ou mais SIGPIPEs), aguardamos por isso neste segmento, limpando assim seu status pendente (para fazer isso, usamos sigtimedwait () com tempo limite zero; isso evita o bloqueio em um cenário em que um usuário mal-intencionado enviou o SIGPIPE manualmente a todo um processo: nesse caso, o veremos pendente, mas outro encadeamento pode lidar com isso antes de termos uma mudança para esperar). Após limpar o status pendente, desbloqueamos o SIGPIPE neste segmento, mas apenas se ele não tiver sido bloqueado originalmente.

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

kroki
fonte
22

Manipular SIGPIPE localmente

Geralmente, é melhor lidar com o erro localmente, em vez de em um manipulador de eventos de sinal global, pois localmente você terá mais contexto sobre o que está acontecendo e que recurso tomar.

Eu tenho uma camada de comunicação em um dos meus aplicativos que permite que ele se comunique com um acessório externo. Quando ocorre um erro de gravação, lanço uma exceção na camada de comunicação e deixo borbulhar até um bloco try catch para lidar com isso.

Código:

O código para ignorar um sinal SIGPIPE para que você possa manipulá-lo localmente é:

// 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);

Esse código impedirá que o sinal SIGPIPE seja gerado, mas você receberá um erro de leitura / gravação ao tentar usar o soquete, portanto, será necessário verificar isso.

Sam
fonte
essa ligação deve ser feita depois de conectar um soquete ou antes disso? onde é o melhor lugar Obrigado
Ahmed
@Ahmed Eu, pessoalmente, colocá-lo em applicationDidFinishLaunching: . O principal é que deve ser antes de interagir com uma conexão de soquete.
Sam
mas você receberá um erro de leitura / gravação ao tentar usar o soquete, portanto precisará verificar isso. - Posso perguntar como isso é possível? sinal (SIGPIPE, SIG_IGN) funciona para mim, mas no modo de depuração ele retorna um erro, é possível também ignorar esse erro?
precisa saber é o seguinte
@ Dreyfus15 Eu acredito que acabei de fazer chamadas usando o soquete em um bloco try / catch para lidar com isso localmente. Já faz um tempo desde que eu vi isso.
Sam
15

Você não pode impedir que o processo na extremidade oposta de um pipe saia e, se ele sair antes de terminar a gravação, você receberá um sinal SIGPIPE. Se você assinar o sinal, sua gravação retornará com um erro - e você precisará observar e reagir a esse erro. Apenas capturar e ignorar o sinal em um manipulador não é uma boa idéia - você deve observar que o canal agora está desativado e modifica o comportamento do programa para que ele não grave novamente no canal (porque o sinal será gerado novamente e ignorado e tente novamente, e todo o processo pode durar muito tempo e desperdiçar muita energia da CPU).

Jonathan Leffler
fonte
6

Ou devo apenas pegar o SIGPIPE com um manipulador e ignorá-lo?

Eu acredito que está certo. Você quer saber quando a outra extremidade fechou o descritor e é isso que o SIGPIPE diz.

Sam

Sam Reynolds
fonte
3

Manual do Linux disse:

EPIPE A extremidade local foi desligada em um soquete orientado à conexão. Nesse caso, o processo também receberá um SIGPIPE, a menos que MSG_NOSIGNAL esteja definido.

Mas para o Ubuntu 12.04 não está certo. Eu escrevi um teste para esse caso e sempre recebo o EPIPE sem o SIGPIPE. SIGPIPE é gerado se eu tentar escrever no mesmo soquete quebrado pela segunda vez. Portanto, você não precisa ignorar o SIGPIPE. Se esse sinal ocorrer, significa erro lógico no seu programa.

talash
fonte
1
Definitivamente, recebo o SIGPIPE sem ver o EPIPE em um soquete no linux. Isso soa como um bug do kernel.
davmac
3

Qual é a melhor prática para evitar a falha aqui?

Desative o sigpipes de acordo com todos ou capture e ignore o erro.

Existe uma maneira de verificar se o outro lado da linha ainda está lendo?

Sim, use select ().

select () parece não funcionar aqui, pois sempre diz que o soquete é gravável.

Você precisa selecionar os bits de leitura . Você provavelmente pode ignorar os bits de gravação .

Quando a extremidade remota fecha seu identificador de arquivo, selecione informará que existem dados prontos para leitura. Ao ler isso, você receberá 0 bytes, e é assim que o sistema operacional informa que o identificador do arquivo foi fechado.

O único momento em que você não pode ignorar os bits de gravação é se estiver enviando grandes volumes, e existe o risco de a outra extremidade ficar acumulada, o que pode fazer com que seus buffers sejam preenchidos. Se isso acontecer, tentar gravar no identificador de arquivo pode causar um bloqueio ou falha no seu programa / thread. O teste de seleção antes da gravação o protegerá disso, mas não garante que o outro lado esteja íntegro ou que seus dados cheguem.

Observe que você pode obter um sigpipe em close (), assim como quando você escreve.

Fechar libera todos os dados em buffer. Se a outra extremidade já estiver fechada, o fechamento falhará e você receberá um sinal.

Se você estiver usando TCPIP em buffer, uma gravação bem-sucedida significa que seus dados foram colocados na fila para envio, não significa que foram enviados. Até você fechar com êxito, você não sabe que seus dados foram enviados.

Sigpipe diz que algo deu errado, não diz o que ou o que você deve fazer sobre isso.

Ben Aveling
fonte
Sigpipe diz que algo deu errado, não diz o que ou o que você deve fazer sobre isso. Exatamente. Virtualmente, o único objetivo do SIGPIPE é derrubar os utilitários de linha de comando quando o próximo estágio não precisar mais da entrada. Se o seu programa estiver trabalhando em rede, geralmente você deve bloquear ou ignorar o SIGPIPE em todo o programa.
DepressedDaniel
Precisamente. O SIGPIPE destina-se a situações como "encontre XXX | cabeça". Uma vez que a cabeça corresponde às suas 10 linhas, não faz sentido continuar com a descoberta. Então, a cabeça sai e, da próxima vez que o find tenta falar com ele, ele recebe um sinal e sabe que também pode sair.
Ben Aveling
Realmente, o único objetivo do SIGPIPE é permitir que os programas sejam desleixados e não verifique se há erros nas gravações!
William Pursell
2

Sob um sistema POSIX moderno (ou seja, Linux), você pode usar a sigprocmask()função

#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 ...
}

Se você deseja restaurar o estado anterior posteriormente, salve o local old_stateseguro. Se você chamar essa função várias vezes, precisará usar uma pilha ou salvar apenas o primeiro ou o último old_state... ou talvez ter uma função que remova um sinal bloqueado específico.

Para mais informações, leia a página de manual .

Alexis Wilke
fonte
Não é necessário ler, modificar, escrever o conjunto de sinais bloqueados como este. sigprocmaskadiciona ao conjunto de sinais bloqueados, para que você possa fazer tudo isso com uma única chamada.
Luchs
Não leio-modifico-escrevo , leio para salvar o estado atual em que mantenho, old_statepara que possa restaurá-lo mais tarde, se assim o desejar. Se você sabe que nunca precisará restaurar o estado, de fato não há necessidade de ler e armazenar dessa maneira.
Alexis Wilke #:
1
Mas você faz: a primeira chamada para sigprocmask()lê o estado antigo, a chamada para sigaddsetmodificá-la, a segunda chamada para sigprocmask()escreve. Você pode remover para a primeira chamada, inicializar com sigemptyset(&set)e alterar a segunda chamada para sigprocmask(SIG_BLOCK, &set, &old_state).
21818 Luchs
Ah! Ha ha! Você está certo. Eu faço. Bem ... no meu software, não sei quais sinais já bloqueei ou não. Então eu faço dessa maneira. No entanto, concordo que, se você tiver apenas um "conjunto de sinais desta maneira", um simples conjunto claro + será suficiente.
Alexis Wilke #