Comment prévenir les SIGPIPEs (ou les gérer correctement)

J'ai un petit programme serveur qui accepte les connexions sur un socket TCP ou UNIX local, lit une commande simple et, en fonction de la commande, envoie une réponse. Le problème est que le client peut ne pas avoir d'intérêt dans la réponse parfois 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 crash ici? Existe-t-il un moyen de vérifier si l'autre côté de la ligne est toujours en lecture? (select () ne semble pas fonctionner ici comme ça dit toujours que le socket est accessible en écriture). Ou devrais-je simplement attraper le SIGPIPE avec un gestionnaire et l'ignorer?

216
demandé sur Aristotle Pagaltzis 2008-09-20 17:43:21

10 réponses

Vous voulez généralement ignorer le SIGPIPE et gérer l'erreur directement dans votre code. C'est parce que 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 gestionnaire SIGPIPE sur SIG_IGN. Cela empêchera toute écriture de socket ou de tuyau de provoquer un signal SIGPIPE.

Pour ignorer le signal SIGPIPE, utilisez le code suivant:

signal(SIGPIPE, SIG_IGN);

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

Enfin, vous pouvez également considérer l'indicateur de socket SO_SIGNOPIPE qui peut être défini avec setsockopt() sur certains systèmes d'exploitation. Cela empêchera SIGPIPE d'être causé par des Écritures uniquement sur les sockets sur lesquels il est défini.

205
répondu dvorak 2015-12-18 21:58:10

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

Sur la plupart des BSD (MacOS, FreeBSD...) systèmes, (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 ceci en effet, au lieu que le signal SIGPIPE soit généré, EPIPE sera retourné.

142
répondu user55807 2018-05-29 11:06:07

Je suis super en retard à la fête, mais SO_NOSIGPIPE n'est pas portable, et pourrait ne pas fonctionner sur votre système (il semble être une chose BSD).

Une bonne alternative si vous êtes sur, disons, un système Linux sans {[1] } serait de définir le drapeau MSG_NOSIGNAL sur votre appel send(2).

Exemple de remplacement de 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 );
107
répondu sklnd 2017-05-23 12:26:35

Dans ce post {[2] } j'ai décrit la 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 courant qui exécute le code de la bibliothèque. Voici comment faire: pour supprimer SIGPIPE, nous vérifions d'abord si elle 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, elle sera fusionnée avec un, et c'est un no-op. si SIGPIPE n'est pas en attente, nous le bloquons dans ce thread, 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 faisons ce qui suit: si SIGPIPE était en attente à L'origine, nous ne faisons rien. Sinon, nous vérifions si elle est en attente maintenant. Si c'est le cas (ce qui signifie que les actions out ont généré un ou plusieurs SIGPIPEs), alors nous l'attendons dans ce thread, effaçant ainsi son statut en attente (à faire cela nous utilisons sigtimedwait () avec zéro délai d'attente; c'est pour éviter le blocage dans un scénario où un utilisateur malveillant a envoyé SIGPIPE manuellement à tout un processus: dans ce cas, nous le verrons en attente, mais d'autres threads peuvent le gérer avant que nous ayons un changement pour l'attendre). Après avoir effacé le statut en attente, nous débloquons SIGPIPE dans ce thread, mais seulement s'il n'a pas été bloqué à l'origine.

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

28
répondu kroki 2014-08-15 08:48:42

Gérer SIGPIPE localement

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

J'ai une couche de communication dans l'une de mes applications qui permet à mon application de communiquer avec un accessoire externe. Quand 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 y.

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 déclenché, mais vous obtiendrez une erreur de lecture / écriture lorsque vous essayez d'utiliser le socket, vous devrez donc vérifier cela.

19
répondu Sam 2012-01-27 16:04:09

Vous ne pouvez pas empêcher le processus à l'extrémité d'un tuyau de quitter, et s'il se termine avant que vous ayez fini d'écrire, vous obtiendrez un signal SIGPIPE. Si vous SIG_IGN le signal, votre écriture retournera avec une erreur - et vous devez noter et réagir à cette erreur. Juste attraper et ignorer le signal dans un gestionnaire n'est pas une bonne idée - vous devez noter que le tuyau est maintenant disparu et modifier le comportement du programme afin qu'il n'écrive plus dans le tuyau (parce que le signal sera généré à nouveau, et ignoré à nouveau, et vous allez essayer à nouveau, et l'ensemble du processus pourrait continuer pendant un long temps et perdre beaucoup de puissance CPU).

13
répondu Jonathan Leffler 2008-09-20 14:45:34

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

Je crois que c'est à droite sur. Vous voulez savoir quand L'autre extrémité a fermé leur descripteur et C'est ce que SIGPIPE vous dit.

Sam

4
répondu Sam Reynolds 2008-09-20 13:45:23

Manuel Linux dit:

EPIPE L'extrémité locale a été arrêtée sur une connexion orientée socket. 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 juste. J'ai écrit un test pour ce cas et je reçois toujours EPIPE withot SIGPIPE. SIGPIPE est généré si j'essaie d'écrire sur le même socket cassé la deuxième fois. Donc vous n'avez pas besoin D'ignorer SIGPIPE si ce signal arrive signifie erreur logique dans votre programme.

2
répondu talash 2014-03-26 14:41:00

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

Désactivez les sigpipes selon tout le monde, ou attrapez et ignorez l'erreur.

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

Oui, utilisez select ().

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

Vous devez sélectionner sur lelire bits. Vous pouvez probablement ignorer l'écriture bit.

Lorsque l'extrémité éloignée ferme son handle de fichier, select vous indiquera qu'il y a des données prêtes à lire. Quand vous allez lire cela, vous récupérerez 0 octets, ce qui explique comment le système d'exploitation vous indique que le handle de fichier a été fermé.

La seule fois où vous ne pouvez pas ignorer les bits d'écriture est si vous envoyez de gros volumes, et il y a un risque de retard de l'autre extrémité, ce qui peut provoquer le remplissage de vos tampons. Si cela se produit, alors essayer d'écrire dans le handle de fichier peut provoquer votre programme / thread pour bloquer ou échouer. Tester select avant d'écrire vous en protégera, 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 signal de fermer(), ainsi que lorsque vous écrivez.

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

Si vous utilisez TCPIP tamponné, une écriture réussie signifie simplement vos données a été mis en file d'attente pour envoyer, cela ne signifie pas qu'il a été envoyé. Tant que vous n'appelez pas close avec succès, vous ne savez pas que vos données ont été envoyées.

Sigpipe vous dit que quelque chose a mal tourné, il ne vous dit pas quoi, ni ce que vous devriez faire à ce sujet.

1
répondu Ben Aveling 2015-09-15 23:57:28

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

#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 plus tard, assurez-vous d'enregistrer le old_state dans un endroit sûr. Si vous appelez cette fonction plusieurs fois, vous devez utiliser une pile ou enregistrer uniquement le premier ou le dernier old_state... ou peut-être avoir une fonction qui supprime un signal bloqué spécifique.

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

-1
répondu Alexis Wilke 2018-05-21 23:43:57