Comment empêcher les SIGPIPE (ou les manipuler correctement)

261

J'ai un petit programme serveur qui accepte les connexions sur un socket TCP ou UNIX local, lit une commande simple et, selon la commande, envoie une réponse. Le problème est que le client peut parfois ne pas avoir d'intérêt pour la réponse et se termine tôt, donc écrire sur ce socket provoquera un SIGPIPE et fera planter mon serveur. Quelle est la meilleure pratique pour éviter le plantage ici? Existe-t-il un moyen de vérifier si l'autre côté de la ligne lit toujours? (select () ne semble pas fonctionner ici car il dit toujours que le socket est accessible en écriture). Ou devrais-je simplement attraper le SIGPIPE avec un gestionnaire et l'ignorer?

jkramer
la source

Réponses:

255

Vous voulez généralement ignorer le SIGPIPEet gérer l'erreur directement dans votre code. En effet, les gestionnaires de signaux en C ont de nombreuses restrictions sur ce qu'ils peuvent faire.

La façon la plus portable de le faire est de définir le SIGPIPEgestionnaire sur SIG_IGN. Cela empêchera toute écriture de douille ou de tuyau de provoquer un SIGPIPEsignal.

Pour ignorer le SIGPIPEsignal, utilisez le code suivant:

signal(SIGPIPE, SIG_IGN);

Si vous utilisez l' send()appel, une autre option consiste à utiliser l' MSG_NOSIGNALoption, qui désactivera le SIGPIPEcomportement par appel. Notez que tous les systèmes d'exploitation ne prennent pas en charge l' MSG_NOSIGNALindicateur.

Enfin, vous pouvez également considérer l' SO_SIGNOPIPEindicateur de socket qui peut être défini setsockopt()sur certains systèmes d'exploitation. Cela évitera SIGPIPEd'être causé par des écritures uniquement sur les sockets sur lesquelles il est défini.

dvorak
la source
75
Pour rendre cela explicite: signal (SIGPIPE, SIG_IGN);
jhclark
5
Cela peut être nécessaire si vous obtenez un code retour de sortie de 141 (128 + 13: SIGPIPE). SIG_IGN est le gestionnaire de signal ignoré.
velcrow
6
Pour les sockets, il est vraiment plus facile d'utiliser send () avec MSG_NOSIGNAL, comme l'a dit @talash.
Pawel Veselov
3
Que se passe-t-il exactement si mon programme écrit sur un tube cassé (une socket dans mon cas)? SIG_IGN fait que le programme ignore le SIG_PIPE, mais cela fait-il que send () fait quelque chose de indésirable?
sudo
2
Il convient de le mentionner, car je l'ai trouvé une fois, puis je l'ai oublié plus tard et je me suis perdu, puis je l'ai découvert une deuxième fois. Les débogueurs (je sais que gdb le fait) remplacent votre gestion du signal, donc les signaux ignorés ne sont pas ignorés! Testez votre code en dehors d'un débogueur pour vous assurer que le SIGPIPE ne se produit plus. stackoverflow.com/questions/6821469/…
Jetski S-type
155

Une autre méthode consiste à changer le socket pour qu'il ne génère jamais de SIGPIPE sur write (). C'est plus pratique dans les bibliothèques, où vous ne voudrez peut-être pas de gestionnaire de signal global pour SIGPIPE.

Sur la plupart des systèmes basés sur BSD (MacOS, FreeBSD ...), (en supposant que vous utilisez C / C ++), vous pouvez le faire avec:

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

Avec cela en vigueur, au lieu du signal SIGPIPE généré, EPIPE sera retourné.

user55807
la source
45
Cela semble bien, mais SO_NOSIGPIPE ne semble pas exister sous Linux - ce n'est donc pas une solution largement portable.
nobar
1
salut à tous, pouvez-vous me dire où je dois mettre ce code? je ne comprends pas les remerciements
R. Dewi
9
Sous Linux, je vois l'option MSG_NOSIGNAL dans les drapeaux d' envoi
Aadishri
Fonctionne parfaitement sur Mac OS X
kirugan
116

Je suis super en retard à la fête, mais je ne suis SO_NOSIGPIPEpas portable et peut ne pas fonctionner sur votre système (cela semble être un problème BSD).

Une bonne alternative si vous êtes sur, disons, un système Linux sans SO_NOSIGPIPEserait de définir l' MSG_NOSIGNALindicateur sur votre appel send (2).

Exemple remplacé write(...)par send(...,MSG_NOSIGNAL)(voir le commentaire de nobar )

char buf[888];
//write( sockfd, buf, sizeof(buf) );
send(    sockfd, buf, sizeof(buf), MSG_NOSIGNAL );
sklnd
la source
5
En d'autres termes, utilisez send (..., MSG_NOSIGNAL) en remplacement de write () et vous n'obtiendrez pas SIGPIPE. Cela devrait bien fonctionner pour les sockets (sur les plates-formes prises en charge), mais send () semble être limité à une utilisation avec des sockets (pas des tuyaux), donc ce n'est pas une solution générale au problème SIGPIPE.
nobar
@ Ray2k Qui diable développe encore des applications pour Linux <2.2? C'est en fait une question semi-sérieuse; la plupart des distributions sont livrées avec au moins 2,6.
Parthian Shot
1
@Parthian Shot: Vous devriez repenser votre réponse. La maintenance des anciens systèmes embarqués est une raison valable de prendre soin des anciennes versions de Linux.
kirsche40
1
@ kirsche40 Je ne suis pas l'OP - j'étais juste curieux.
Parthian Shot du
@sklnd, cela a parfaitement fonctionné pour moi. J'utilise un QTcpSocketobjet Qt pour envelopper l'utilisation des sockets, j'ai dû remplacer l' writeappel de méthode par un système d'exploitation send(en utilisant la socketDescriptorméthode). Quelqu'un connaît une option plus propre pour définir cette option dans une QTcpSocketclasse?
Emilio González Montaña
29

Dans cet article, j'ai décrit une solution possible pour le cas Solaris lorsque ni SO_NOSIGPIPE ni MSG_NOSIGNAL n'est disponible.

Au lieu de cela, nous devons supprimer temporairement SIGPIPE dans le thread actuel qui exécute le code de bibliothèque. Voici comment procéder: pour supprimer SIGPIPE, nous vérifions d'abord s'il est en attente. Si c'est le cas, cela signifie qu'il est bloqué dans ce fil, et nous ne devons rien faire. Si la bibliothèque génère un SIGPIPE supplémentaire, il sera fusionné avec celui en attente, et c'est un no-op. Si SIGPIPE n'est pas en attente, nous le bloquons dans ce fil et vérifions également s'il était déjà bloqué. Ensuite, nous sommes libres d'exécuter nos écritures. Lorsque nous devons restaurer SIGPIPE à son état d'origine, nous procédons comme suit: si SIGPIPE était en attente à l'origine, nous ne faisons rien. Sinon, nous vérifions s'il est en attente maintenant. Si c'est le cas (ce qui signifie que les actions de sortie ont généré un ou plusieurs SIGPIPE), alors nous l'attendons dans ce fil, effaçant ainsi son statut en attente (pour ce faire, nous utilisons sigtimedwait () sans délai d'expiration; c'est pour éviter le blocage dans un scénario où un utilisateur malveillant a envoyé SIGPIPE manuellement à un processus entier: dans ce cas, nous le verrons en attente, mais d'autres threads peuvent manipulez-le avant d'avoir un changement pour l'attendre). Après avoir effacé le statut en attente, nous débloquons SIGPIPE dans ce fil, mais uniquement s'il n'a pas été bloqué à l'origine.

Exemple de code sur https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156

kroki
la source
22

Gérer le SIGPIPE localement

Il est généralement préférable de gérer l'erreur localement plutôt que dans un gestionnaire d'événement de signal global, car localement, vous aurez plus de contexte sur ce qui se passe et sur les recours à prendre.

J'ai une couche de communication dans l'une de mes applications qui permet à mon application de communiquer avec un accessoire externe. Lorsqu'une erreur d'écriture se produit, je lance et exception dans la couche de communication et le laisse bouillonner jusqu'à un bloc try catch pour le gérer là-bas.

Code:

Le code pour ignorer un signal SIGPIPE afin que vous puissiez le gérer localement est:

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

Ce code empêchera le signal SIGPIPE d'être élevé, mais vous obtiendrez une erreur de lecture / écriture lorsque vous essayez d'utiliser le socket, vous devrez donc vérifier cela.

Sam
la source
cet appel doit-il être effectué après avoir connecté une prise ou avant cela? où est-il préférable de placer. Merci
Ahmed
@Ahmed Je l' ai mis personnellement applicationDidFinishLaunching: . L'essentiel est qu'il devrait être avant d'interagir avec une connexion socket.
Sam
mais vous obtiendrez une erreur de lecture / écriture lorsque vous essayez d'utiliser le socket, vous devrez donc vérifier cela. - Puis-je demander comment cela est possible? signal (SIGPIPE, SIG_IGN) fonctionne pour moi mais en mode débogage, il renvoie une erreur est-il possible d'ignorer également cette erreur?
jongbanaag
@ Dreyfus15 Je crois que j'ai simplement encapsulé les appels en utilisant le socket dans un bloc try / catch pour le gérer localement. Ça fait un moment que je n'ai pas vu ça.
Sam
15

Vous ne pouvez pas empêcher le processus à l'extrémité d'un tuyau de se terminer, et s'il se termine avant que vous ayez fini d'écrire, vous obtiendrez un signal SIGPIPE. Si vous SIG_IGN le signal, alors votre écriture reviendra avec une erreur - et vous devez noter et réagir à cette erreur. Attraper et ignorer le signal dans un gestionnaire n'est pas une bonne idée - vous devez noter que le tuyau est maintenant éteint et modifier le comportement du programme afin qu'il n'écrive plus dans le tuyau (car le signal sera à nouveau généré et ignoré encore une fois, et vous essaierez à nouveau, et tout le processus pourrait durer longtemps et gaspiller beaucoup de puissance CPU).

Jonathan Leffler
la source
6

Ou devrais-je simplement attraper le SIGPIPE avec un gestionnaire et l'ignorer?

Je pense que c'est vrai. Vous voulez savoir quand l'autre extrémité a fermé son descripteur et c'est ce que SIGPIPE vous dit.

Sam

Sam Reynolds
la source
3

Le manuel Linux disait:

EPIPE L'extrémité locale a été arrêtée sur un socket orienté connexion. Dans ce cas, le processus recevra également un SIGPIPE, sauf si MSG_NOSIGNAL est défini.

Mais pour Ubuntu 12.04, ce n'est pas correct. J'ai écrit un test pour ce cas et je reçois toujours EPIPE withot SIGPIPE. SIGPIPE est généré si j'essaye d'écrire sur le même socket cassé une deuxième fois. Vous n'avez donc pas besoin d'ignorer SIGPIPE si ce signal se produit, cela signifie une erreur logique dans votre programme.

talash
la source
1
Je reçois très certainement SIGPIPE sans d'abord voir EPIPE sur un socket sous linux. Cela ressemble à un bug du noyau.
davmac
3

Quelle est la meilleure pratique pour éviter le crash ici?

Soit désactiver les sigpipes comme pour tout le monde, soit intercepter et ignorer l'erreur.

Existe-t-il un moyen de vérifier si l'autre côté de la ligne lit toujours?

Oui, utilisez select ().

select () ne semble pas fonctionner ici car il dit toujours que le socket est accessible en écriture.

Vous devez sélectionner les bits de lecture . Vous pouvez probablement ignorer les bits d' écriture .

Lorsque l'extrémité distante ferme son descripteur de fichier, sélectionnez vous indiquera que des données sont prêtes à être lues. Lorsque vous allez lire cela, vous récupérerez 0 octet, c'est ainsi que le système d'exploitation vous indique que le descripteur de fichier a été fermé.

La seule fois où vous ne pouvez pas ignorer les bits d'écriture, c'est si vous envoyez de gros volumes et qu'il y a un risque que l'autre extrémité soit en retard, ce qui peut entraîner le remplissage de vos tampons. Si cela se produit, alors essayer d'écrire dans le descripteur de fichier peut entraîner le blocage ou l'échec de votre programme / thread. Tester select avant l'écriture vous protégera de cela, mais cela ne garantit pas que l'autre extrémité est saine ou que vos données vont arriver.

Notez que vous pouvez obtenir un sigpipe à partir de close (), ainsi que lorsque vous écrivez.

Fermer vide toutes les données mises en mémoire tampon. Si l'autre extrémité a déjà été fermée, la fermeture échouera et vous recevrez un sigpipe.

Si vous utilisez TCPIP en mémoire tampon, une écriture réussie signifie simplement que vos données ont été mises en file d'attente pour l'envoi, cela ne signifie pas qu'elles ont été envoyées. Tant que vous n'avez pas réussi à appeler close, vous ne savez pas que vos données ont été envoyées.

Sigpipe vous dit que quelque chose s'est mal passé, il ne vous dit pas quoi ou ce que vous devez faire à ce sujet.

Ben Aveling
la source
Sigpipe vous dit que quelque chose s'est mal passé, il ne vous dit pas quoi ou ce que vous devez faire à ce sujet. Exactement. Le seul but de SIGPIPE est de supprimer les utilitaires de ligne de commande canalisés lorsque la prochaine étape n'a plus besoin de l'entrée. Si votre programme fait du réseautage, vous devez généralement bloquer ou ignorer SIGPIPE à l'échelle du programme.
DépriméDaniel
Précisément. SIGPIPE est destiné à des situations comme "find XXX | head". Une fois que head a fait correspondre ses 10 lignes, il est inutile de poursuivre la recherche. Alors la tête sort, et la prochaine fois que find essaie de lui parler, il obtient un sigpipe et sait qu'il peut aussi sortir.
Ben Aveling
Vraiment, le seul but de SIGPIPE est de permettre aux programmes d'être bâclés et de ne pas vérifier les erreurs d'écriture!
William Pursell
2

Sous un système POSIX moderne (c'est-à-dire Linux), vous pouvez utiliser la sigprocmask()fonction.

#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 vous souhaitez restaurer l'état précédent ultérieurement, assurez-vous d'enregistrer le old_statequelque part en sécurité. Si vous appelez cette fonction plusieurs fois, vous devez soit utiliser une pile, soit enregistrer uniquement la première ou la dernière old_state... ou peut-être avoir une fonction qui supprime un signal bloqué spécifique.

Pour plus d'informations, lisez la page de manuel .

Alexis Wilke
la source
Il n'est pas nécessaire de lire-modifier-écrire l'ensemble des signaux bloqués comme celui-ci. sigprocmaskajoute à l'ensemble des signaux bloqués, vous pouvez donc faire tout cela avec un seul appel.
Luchs
Je ne lis pas -modifie-écris , je lis pour sauvegarder l'état actuel dans lequel je garde old_stateafin de pouvoir le restaurer plus tard, si je le souhaite. Si vous savez que vous n'aurez jamais besoin de restaurer l'état, il n'est en effet pas nécessaire de le lire et de le stocker de cette manière.
Alexis Wilke
1
Mais vous le faites: le premier appel à sigprocmask()lire l'ancien état, l'appel à le sigaddsetmodifier, le deuxième appel à l' sigprocmask()écrire. Vous pouvez supprimer le premier appel, initialiser avec sigemptyset(&set)et remplacer le deuxième appel par sigprocmask(SIG_BLOCK, &set, &old_state).
Luchs
Ah! Ha ha! Vous avez raison. Je fais. Eh bien ... dans mon logiciel, je ne sais pas quels signaux j'ai déjà bloqués ou pas bloqués. Alors je le fais de cette façon. Cependant, je suis d'accord que si vous n'avez qu'un seul "set signaux de cette façon", alors un simple clear + set suffit.
Alexis Wilke