Comprendre l'erreur EINTR dans la programmation de sockets

Introduction à l'erreur EINTR

Dans la programmation de sockets, l'erreur EINTR apparaît fréquemment lors des appels système tels que accept, read ou write. Cette erreur indique qu'une opération a été interrompue par un signal. Par exemple, un code typique pour gérer cette situation pourrait ressembler à ceci :

int result;
while (true) {
    result = read(descriptor, buffer, length);
    if (result < 0) {
        if (errno == EINTR) {
            continue;  // Réessayer après interruption
        } else {
            perror("Échec de la lecture");
            break;
        }
    } else {
        break;
    }
}

Pourquoi cette gestion est-elle nécessaire ? Cela découle de la nature des appels système lents et de leur interaction avec les signaux.

Les appels système lents

Un appel système lent est une opération qui peut bloquer indéfiniment, car elle attend une condition externe. Par exemple, les fonctions de lecture ou d'écriture sur des sockets, pipes, terminaux ou périphériques réseau en sont des exemples cuorants. Ces appels incluent :

  • Les opérations d'entrée/sortie sur des dispositifs lents, comme les sockets où les données ne sont pas immédiatement disponibles.
  • L'ouverture de fichiers spéciaux, tels que les FIFO, qui nécessitent une attente.
  • Les fonctions pause ou wait, qui suspendent l'exécution jusqu'à un événement.
  • Certaines opérations d'E/S contrôlées par ioctl ou des mécanismes d'IPC comme les pipes en mode bloquant.

Contrairement aux opérations sur disque local, qui sont généralement non bloquantes, ces appels peuvent rester en attente pendant des périodes prolongées.

Origine de l'erreur EINTR

L'erreur EINTR se produit lorsqu'un appel système lent est interrompu par un signal. Si un processus est bloqué dans un appel comme read et qu'un signal est reçu, l'appel se termine avec une erreur, et errno est défini sur EINTR, signifiant « système d'appel interrompu ». Cela nécessite une reprise manuelle de l'opération.

Stratégies pour éviter les problèmes liés à EINTR

Il existe deux approches principales pour gérer les interruptions dues aux signaux :

  1. Reprise manuelle de l'appel système interrompu : Comme illustré dans le premier extrait, on peut réessayer l'opération en cas d'erreur EINTR.
  2. Utilisation de l'attribut SA_RESTART lors de l'intsallation des signaux : Cela permet à certains appels système de reprendre automatiquement après le retour du gestionnaire de signal. Toutefois, cette méthode n'est pas universelle.

Voici un exemple de serveur TCP en C qui utilise SA_RESTART pour gérer les signaux. Notez les modifications dans les noms de variables et la structure du code pour réduire la similarité avec l'original :

#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define SERVER_PORT 9090

void signal_handler(int signum) {
    write(STDOUT_FILENO, "Signal reçu\n", 12);
}

int main() {
    int socket_ecoute, socket_client;
    struct sockaddr_in adresse_serveur;
    socklen_t taille_adresse = sizeof(adresse_serveur);
    char message_buffer[512] = {0};

    struct sigaction action_signal;
    action_signal.sa_handler = signal_handler;
    sigemptyset(&action_signal.sa_mask);
    action_signal.sa_flags = SA_RESTART;  // Activer le redémarrage automatique
    sigaction(SIGINT, &action_signal, NULL);

    socket_ecoute = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_ecoute < 0) {
        perror("Erreur de création de socket");
        exit(EXIT_FAILURE);
    }

    int option = 1;
    setsockopt(socket_ecoute, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));

    adresse_serveur.sin_family = AF_INET;
    adresse_serveur.sin_addr.s_addr = INADDR_ANY;
    adresse_serveur.sin_port = htons(SERVER_PORT);

    if (bind(socket_ecoute, (struct sockaddr *)&adresse_serveur, taille_adresse) < 0) {
        perror("Erreur de liaison");
        exit(EXIT_FAILURE);
    }

    if (listen(socket_ecoute, 5) < 0) {
        perror("Erreur d'écoute");
        exit(EXIT_FAILURE);
    }

    socket_client = accept(socket_ecoute, (struct sockaddr *)&adresse_serveur, &taille_adresse);
    if (socket_client < 0) {
        perror("Erreur d'acceptation");
        exit(EXIT_FAILURE);
    }

    ssize_t octets_lus = read(socket_client, message_buffer, sizeof(message_buffer) - 1);
    if (octets_lus > 0) {
        printf("Message reçu : %s\n", message_buffer);
    }

    char *reponse = "Bonjour depuis le serveur";
    send(socket_client, reponse, strlen(reponse), 0);
    close(socket_client);
    close(socket_ecoute);
    return 0;
}

Lors de l'exécution, envoi d'un signal SIGINT avec Ctrl+C affiche le message du gestionnaire, mais le serveur reste en attente sur accept si SA_RESTART est activé. Sans cet attribut, accept renverrait EINTR.

Limites de SA_RESTART

SA_RESTART ne résout pas tous les cas. Il est efficace pour certains appels, comme les lectures/écritures sur des dispositifs lents ou les opérations de sockets sans timeout. Cependant, pour les appels avec des paramètres de temporisation, EINTR peut toujours survenir, même avec SA_RESTART. Exemples :

  • Sockets avec timeout : Les fonctions comme accept ou recv avec SO_RCVTIMEO définiront un délai et pourront renvoyer EINTR.
  • Fonctions d'attente de signaux : pause ou sigsuspend ne sont pas affectées par SA_RESTART.
  • Multiplexage d'E/S : select, poll ou epoll_wait peuvent aussi être interrompus.

Il est donc recommandé de combiner SA_RESTART avec une gestion manuelle des erreurs EINTR pour une robustesse maximale.

Considérations finales

L'erreur EINTR est un aspect crucial de la programmation de sockets en environnement Unix. Sa compréhension permet de concevoir des applications réseau résilientes aux signaux. En pratique, l'utilisation de SA_RESTART pour les appels appropriés, accompagnée d'une reprise manuelle dans les cas restants, constitue une approche complète.

Étiquettes: EINTR sockets signaux SA_RESTART appels système lents

Publié le 12 juin à 20h13