Interruption des appels système lorsqu'un signal est capté

29

En lisant les pages de manuel sur les appels read()et, write()il apparaît que ces appels sont interrompus par des signaux, qu'ils soient bloqués ou non.

En particulier, supposons

  • un processus établit un gestionnaire pour un certain signal.
  • un appareil est ouvert (disons un terminal) avec le O_NONBLOCK non réglé (c'est-à-dire fonctionnant en mode blocage)
  • le processus effectue ensuite un read()appel système pour lire à partir du périphérique et, par conséquent, exécute un chemin de contrôle du noyau dans l'espace noyau.
  • tandis que le prédécesseur exécute son read()dans l'espace noyau, le signal pour lequel le gestionnaire a été installé précédemment est envoyé à ce processus et son gestionnaire de signal est appelé.

En lisant les pages de manuel et les sections appropriées dans SUSv3 'System Interfaces volume (XSH)' , on constate que:

je. Si a read()est interrompu par un signal avant de lire des données (c'est-à-dire qu'il a dû bloquer car aucune donnée n'était disponible), il renvoie -1 avec errnola valeur [EINTR].

ii. Si a read()est interrompu par un signal après avoir lu avec succès certaines données (c'est-à-dire qu'il était possible de commencer à traiter la demande immédiatement), il renvoie le nombre d'octets lus.

Question A): Ai-je raison de supposer que dans les deux cas (bloc / pas de bloc) la livraison et la gestion du signal ne sont pas entièrement transparentes pour le read()?

Cas i. semble compréhensible car le blocage read()placerait normalement le processus dans l' TASK_INTERRUPTIBLEétat de sorte que lorsqu'un signal est délivré, le noyau place le processus dans l' TASK_RUNNINGétat.

Cependant, lorsque le read()n'a pas besoin de bloquer (cas ii.) Et traite la demande dans l'espace noyau, j'aurais pensé que l'arrivée d'un signal et sa gestion seraient transparentes, tout comme l'arrivée et la gestion appropriée d'un HW l'interruption serait. En particulier , j'ai supposé que lors de la livraison du signal, le processus serait placé temporairement en mode utilisateur pour exécuter son gestionnaire de signal à partir duquel il reviendrait finalement pour finir le traitement de l'interruption read()(dans l'espace du noyau) de sorte que le read()court son cours jusqu'à la fin, après quoi le processus revient au point juste après l'appel à read()(dans l'espace utilisateur), avec tous les octets disponibles lus en conséquence.

Mais ii. semble impliquer que le read()est interrompu, car les données sont disponibles immédiatement, mais il retourne ne renvoie que certaines des données (au lieu de toutes).

Cela m'amène à ma deuxième (et dernière) question:

Question B): Si mon hypothèse sous A) est correcte, pourquoi l' read()interruption est-elle interrompue, même si elle n'a pas besoin d'être bloquée car des données sont disponibles pour satisfaire immédiatement la demande? En d'autres termes, pourquoi le n'est-il read()pas repris après l'exécution du gestionnaire de signal, ce qui a finalement pour résultat de renvoyer toutes les données disponibles (qui étaient disponibles après tout)?

darbehdar
la source

Réponses:

29

Résumé: vous avez raison de dire que la réception d'un signal n'est pas transparente, ni dans le cas i (interrompu sans avoir rien lu) ni dans le cas ii (interrompu après une lecture partielle). Faire autrement au cas où j'aurais besoin d'apporter des changements fondamentaux à la fois à l'architecture du système d'exploitation et à l'architecture des applications.

La vue d'implémentation du système d'exploitation

Considérez ce qui se passe si un appel système est interrompu par un signal. Le gestionnaire de signaux exécutera le code en mode utilisateur. Mais le gestionnaire syscall est du code noyau et ne fait confiance à aucun code en mode utilisateur. Explorons donc les choix du gestionnaire syscall:

  • Mettez fin à l'appel système; signaler combien a été fait au code utilisateur. C'est au code de l'application de redémarrer l'appel système d'une manière ou d'une autre, si vous le souhaitez. Voilà comment fonctionne unix.
  • Enregistrez l'état de l'appel système et autorisez le code utilisateur à reprendre l'appel. Ceci est problématique pour plusieurs raisons:
    • Pendant l'exécution du code utilisateur, quelque chose pourrait arriver pour invalider l'état enregistré. Par exemple, si vous lisez un fichier, le fichier peut être tronqué. Le code du noyau aurait donc besoin de beaucoup de logique pour gérer ces cas.
    • L'état enregistré ne peut pas être autorisé à conserver un verrou, car il n'y a aucune garantie que le code utilisateur reprendra jamais l'appel système, puis le verrou serait maintenu pour toujours.
    • Le noyau doit exposer de nouvelles interfaces pour reprendre ou annuler les appels système en cours, en plus de l'interface normale pour démarrer un appel système. C'est beaucoup de complications pour un cas rare.
    • L'état enregistré devra utiliser des ressources (mémoire, au moins); ces ressources devraient être allouées et détenues par le noyau mais être imputées sur l'allocation du processus. Ce n'est pas insurmontable, mais c'est une complication.
      • Notez que le gestionnaire de signal peut effectuer des appels système qui eux-mêmes sont interrompus; vous ne pouvez donc pas simplement avoir une allocation de ressources statique qui couvre tous les appels système possibles.
      • Et si les ressources ne peuvent pas être allouées? Le syscall devrait alors échouer de toute façon. Ce qui signifie que l'application devrait avoir du code pour gérer ce cas, donc cette conception ne simplifierait pas le code de l'application.
  • Restez en cours (mais suspendu), créez un nouveau thread pour le gestionnaire de signal. Ceci, encore une fois, est problématique:
    • Les premières implémentations Unix avaient un seul thread par processus.
    • Le gestionnaire de signal risquerait de dépasser les chaussures du syscall. C'est un problème de toute façon, mais dans la conception actuelle d'Unix, il est contenu.
    • Des ressources devraient être allouées pour le nouveau thread; voir au dessus.

La principale différence avec une interruption est que le code d'interruption est fiable et très contraint. Il n'est généralement pas autorisé d'allouer des ressources, ou de s'exécuter indéfiniment, ou de prendre des verrous et de ne pas les libérer, ou de faire tout autre genre de choses désagréables; puisque le gestionnaire d'interruption est écrit par l'implémenteur du système d'exploitation lui-même, il sait qu'il ne fera rien de mal. D'un autre côté, le code d'application peut tout faire.

La vue de conception d'application

Lorsqu'une application est interrompue au milieu d'un appel système, l'appel système doit-il continuer jusqu'à la fin? Pas toujours. Par exemple, considérons un programme comme un shell qui lit une ligne du terminal, et l'utilisateur appuie sur Ctrl+C, déclenchant SIGINT. La lecture ne doit pas se terminer, c'est à cela que sert le signal. Notez que cet exemple montre que l' readappel système doit être interruptible même si aucun octet n'a encore été lu.

Il doit donc y avoir un moyen pour l'application de dire au noyau d'annuler l'appel système. Sous la conception unix, cela se produit automatiquement: le signal fait revenir le syscall. D'autres conceptions nécessiteraient un moyen pour l'application de reprendre ou d'annuler l'appel système à sa guise.

L' readappel système est ce qu'il est parce que c'est la primitive qui a du sens, étant donné la conception générale du système d'exploitation. Cela signifie, en gros, «lire autant que vous le pouvez, jusqu'à une limite (la taille du tampon), mais arrêtez si quelque chose d'autre se produit». Pour lire réellement un tampon complet, il faut exécuter readune boucle jusqu'à ce que le plus d'octets possible ait été lu; c'est une fonction de niveau supérieur, fread(3). Contrairement à read(2)ce qui est un appel système, freadc'est une fonction de bibliothèque, implémentée dans l'espace utilisateur au-dessus de read. Il convient à une application qui lit un fichier ou meurt en essayant; il ne convient pas à un interpréteur de ligne de commande ou à un programme en réseau qui doit limiter proprement les connexions, ni à un programme en réseau qui a des connexions simultanées et n'utilise pas de threads.

L'exemple de lecture en boucle est fourni dans la programmation du système Linux de Robert Love:

ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) {
  if (ret == -1) {
    if (errno == EINTR)
      continue;
    perror ("read");
    break;
  }
  len -= ret;
  buf += ret;
}

Il prend en charge case iet case iiet quelques autres.

Gilles 'SO- arrête d'être méchant'
la source
Merci beaucoup Gilles pour une réponse très concise et claire qui corrobore les opinions similaires avancées dans un article sur la philosophie de conception UNIX. Semble très convaincant pour moi que le comportement d'interruption syscall a à voir avec la philosophie de conception UNIX plutôt qu'avec des contraintes ou des obstacles techniques
darbehdar
@darbehdar C'est les trois: la philosophie de conception unix (ici principalement que les processus sont moins fiables que le noyau et peuvent exécuter du code arbitraire, aussi que les processus et les threads ne sont pas créés implicitement), les contraintes techniques (sur les allocations de ressources) et la conception d'applications (là sont des cas où le signal doit annuler l'appel système).
Gilles 'SO- arrête d'être méchant'
2

Pour répondre à la question A :

Oui, la livraison et le traitement du signal ne sont pas entièrement transparents pour le read().

La read()course à mi-chemin peut occuper certaines ressources pendant qu'elle est interrompue par le signal. Et le gestionnaire de signal du signal peut également en appeler un autre read()(ou tout autre appel système sécurisé de signal asynchrone ). Ainsi, l' read()interruption par le signal doit être arrêtée en premier afin de libérer les ressources qu'il utilise, sinon l' read()appel du gestionnaire de signaux accédera aux mêmes ressources et provoquera des problèmes réentrants.

Parce que les appels système autres que ceux qui read()pourraient être appelés à partir du gestionnaire de signaux peuvent également occuper un ensemble de ressources identique à celui utilisé read(). Pour éviter les problèmes réentrants ci-dessus, la conception la plus simple et la plus sûre consiste à arrêter l'interruption à read()chaque fois qu'un signal se produit pendant son exécution.

Justin
la source