web-dev-qa-db-fra.com

Comment prévenir les SIGPIPE (ou les manipuler correctement)

J'ai un petit programme serveur qui accepte les connexions sur un socket UNIX TCPou local, lit une commande simple et, selon la commande, envoie une réponse. Le problème est que le client n'a parfois aucun intérêt dans la réponse et qu'il quitte tôt, donc écrire dans ce socket provoquera un SIGPIPE et fera planter mon serveur. Quelle est la meilleure pratique pour empêcher l'accident ici? Existe-t-il un moyen de vérifier si l’autre côté de la ligne lit encore? (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?

239
jkramer

Vous voulez généralement ignorer la SIGPIPE et 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.

Pour ce faire, la méthode la plus portable consiste à définir le gestionnaire SIGPIPE sur SIG_IGN. Cela empêchera toute écriture de socket ou de tuyau de générer 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 par appel. Notez que tous les systèmes d'exploitation ne prennent pas en charge l'indicateur MSG_NOSIGNAL.

Enfin, vous pouvez également envisager l’indicateur de socket SO_SIGNOPIPE qui peut être défini avec setsockopt() sur certains systèmes d’exploitation. Cela évitera que SIGPIPE soit provoqué par des écritures uniquement sur les sockets sur lesquels il est configuré.

233
dvorak

Une autre méthode consiste à changer le socket pour qu'il ne génère jamais SIGPIPE sur write (). Cela 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 utilisiez C/C++), vous pouvez le faire avec:

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

Ainsi, au lieu de générer le signal SIGPIPE, EPIPE sera renvoyé.

149
user55807

Je suis super en retard pour la fête, mais SO_NOSIGPIPE n'est pas portable et pourrait ne pas fonctionner sur votre système (il semble que ce soit un problème BSD).

Une bonne alternative si vous êtes sur, par exemple, un système Linux sans SO_NOSIGPIPE 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 );
112
sklnd

Dans ce post , 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 si celui-ci est en attente. Si c'est le cas, cela signifie qu'il est bloqué dans ce fil et que nous ne devons rien faire. Si la bibliothèque génère un SIGPIPE supplémentaire, il sera fusionné avec celui qui est en attente, ce qui n’est pas une opération. 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 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 out ont généré un ou plusieurs SIGPIPE), nous l'attendons dans ce fil, effaçant ainsi son statut en attente (pour ce faire, nous utilisons sigtimedwait () avec un délai d'expiration égal; ceci afin d'éviter tout blocage un scénario dans lequel un utilisateur malveillant a envoyé SIGPIPE manuellement à un processus complet: dans ce cas, nous le verrons en attente, mais un autre thread peut le gérer avant que nous n'ayions à le modifier). Après avoir effacé le statut en attente, nous débloquons SIGPIPE dans ce fil, mais uniquement s’il n’avait pas été bloqué à l’origine.

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

29
kroki

Manipuler 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, car localement, vous aurez plus de contexte pour ce qui se passe et quels recours utiliser.

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 jette une exception dans la couche de communication et la laisse bouillonner jusqu'à un bloc catch catch pour la gérer.

Code:

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

// 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 lors de la tentative d'utilisation du socket. Vous devrez donc vérifier cela.

22
Sam

Vous ne pouvez pas empêcher le processus situé à l'extrémité distante d'un canal de se terminer. Si ce processus se termine avant d'avoir terminé 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. Attraper et ignorer le signal dans un gestionnaire n'est pas une bonne idée - vous devez noter que le canal est maintenant obsolète et modifier le comportement du programme pour qu'il n'écrit plus dans le canal (car le signal sera généré à nouveau et ignoré encore une fois, et vous allez essayer à nouveau, et le processus entier pourrait durer un long temps et gaspiller beaucoup de puissance de calcul).

15
Jonathan Leffler

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

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

Sam

6
Sam Reynolds

Quelle est la meilleure pratique pour empêcher l'accident ici?

Désactivez les sigpipes comme tout le monde ou interceptez et ignorez l'erreur.

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

Oui, utilisez select ().

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

Vous devez sélectionner le lire bits. Vous pouvez probablement ignorer les bits write.

Lorsque l'extrémité distante ferme son descripteur de fichier, select vous indiquera qu'il existe des données prêtes à être lues. Lorsque vous lirez cela, vous obtiendrez 0 octet, ce qui explique comment 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 que l’autre extrémité risque d’être en retard de traitement, ce qui peut entraîner le remplissage de vos mémoires tampons. Si cela se produit, alors essayer d'écrire dans le descripteur de fichier peut bloquer ou échouer votre programme/thread. Tester select avant d'écrire vous protégera contre 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 de close (), ainsi que lorsque vous écrivez.

Close efface 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 être envoyées, cela ne signifie pas pour autant qu'elles ont été envoyées. Jusqu'à ce que vous appeliez close, 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 faire ou quoi faire.

3
Ben Aveling

Le manuel Linux disait:

EPIPE L'extrémité locale a été fermée sur une prise orientée 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 sans SIGPIPE. SIGPIPE est généré si j'essaie d'écrire sur le même socket cassé une seconde fois. Donc, vous n'avez pas besoin d'ignorer SIGPIPE si ce signal se produit, cela signifie une erreur de logique dans votre programme.

3
talash

Sous un système POSIX moderne (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 ultérieurement, veillez à enregistrer le old_state dans un endroit sûr. Si vous appelez cette fonction plusieurs fois, vous devez soit utiliser une pile, soit ne sauvegarder que le premier ou le dernier old_state... ou peut-être une fonction qui supprime un signal bloqué spécifique.

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

2
Alexis Wilke