Fonctionnement d'un ordinateur/Les interruptions et exceptions

Un livre de Wikilivres.

Les interruptions sont des fonctionnalités du processeur qui ressemblent beaucoup aux appels de fonctions, mais avec quelques petites différences. Les interruptions, comme leur nom l'indique, interrompent temporairement l’exécution d'un programme pour effectuer un sous-programme nommé routine d'interruption. Lorsqu'un processeur exécute une interruption, celui-ci :

  • arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
  • exécute la routine d'interruption ;
  • restaure l'état du programme sauvegardé afin de reprendre l'exécution de son programme là ou il en était.
Interruption processeur

L'appel d'une routine d'interruption est très similaire à un appel de fonction et implique les mêmes chose : sauvegarder les registres du processeur, l'adresse de retour, etc. Tout ce qui a été dit pour les fonctions marche aussi pour les interruptions. La différence est que la routine d'interruption appartient au système d'exploitation ou à un pilote de périphérique, mais pas au programme en cours d'exécution.

Les interruptions sont classées en trois types distincts, aux utilisations très différentes : les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Les deux premières sont des interruptions générés par un évènement extérieur au programme, alors que les interruptions logicielles sont déclenchées quand le programme éxecute une instruction précise pour s'interrompre lui-même, afin d'éxecuter du code appartenant au système d'exploitation ou à un pilote de périphérique.

Les interruptions et les exceptions matérielles[modifier | modifier le wikicode]

Les exceptions matérielles et les interruptions matérielles permettent de réagir à un événement extérieur : communication avec le matériel, erreur fatale d’exécution d'un programme. Le programme en cours d'exécution est alors stoppé pour réagir, avant d'en reprendre l'exécution. Elles sont initiés par un évènement extérieur au programme, contrairement aux interruptions logicielles.

Déroulement d'une interruption.

Les exceptions matérielles[modifier | modifier le wikicode]

Une exception matérielle est une interruption déclenchée par un évènement interne au processeur, par exemple une erreur d'adressage, une division par zéro... Le processeur intègre des circuits qui détectent l'évènement déclencheur, ainsi que des circuits pour déclencher l'exception matérielle. Prenons l'exemple d'une exception déclenchée par une division par zéro : le processeur doit détecter les divisions par zéro. Lorsqu'une exception matérielle survient, la routine exécutée corrige l'erreur qui a été la cause de l'exception matérielle, et prévient le système d'exploitation si elle n'y arrive pas. Elle peut aussi faire planter l'ordinateur, si l'erreur est grave, ce qui se traduit généralement par un écran bleu soudain.

Pour donner un exemple d'utilisation, sachez qu'il existe une exception matérielle qui se déclenche quand on souhaite exécuter une instruction non-reconnue par le processeur. Rappelons que les instructions sont codées par des suites de bits en mémoire, codées sur quelques octets. Mais cela ne signifie pas que toutes les suites de bits correspondent à des instructions : certaines suites ne correspondent pas à des instructions et ne sont pas reconnues par le processeur. Dans ce cas, le chargement dans le processeur d'une telle suite de bit déclenche une exception matérielle "Instruction non-reconnue".

Et cela a été utilisé pour émuler des instructions sur les nombres flottants sur des processeurs qui ne les géraient pas. Pour cela, on modifiait la routine de l'exception "Instruction non-reconnue" de manière à ce qu'elle reconnaisse les suites de bits correspondant à des instructions flottantes et exécute une suite d'instruction entière équivalente.

Les interruptions matérielles[modifier | modifier le wikicode]

Les interruptions matérielles, aussi appelées IRQ, sont des interruptions déclenchées par un périphérique. La routine d'interruption est alors fournie par le pilote du périphérique. Du moins, c'est comme ça sur le matériel moderne, les anciens PC utilisaient des routines d'interruption fournies par le BIOS. Ce sont celles qui vont nous intéresser dans le chapitre sur la communication avec les périphériques, mais nous n'en parlerons pas dans le détail avant quelques chapitres.

Un exemple d'utilisation des interruptions matérielles la gestion de certains périphériques. Par exemple, quand vous tapez sur votre clavier, celui-ci émet une interruption à chaque appui/relevée de touche. Ainsi, le processeur est prévenu quand une touche est appuyée, le système d'exploitation qu'il doit regarder quelle touche est appuyée, etc.

Un autre exemple est la gestion des timers. Par exemple, imaginons que vous voyiez à un jeu vidéo, et qu'il vous reste 2 minutes 45 secondes pour sortir d'un laboratoire de recherche avant que l'auto-destruction ne s'active. La durée de 2 minutes 45 est programmée dans un timer, un circuit compteur qui permet de compter une durée. Le jeu vidéo programme le timer pour qu'il compte durant 2 minutes 45 secondes, puis attend que ce dernier ait finit de compter. Une fois la durée atteinte, le timer déclenche une interruption, pour stopper l'exécution du jeu vidéo. La routine d'interruption prévient le système d'exploitation que le timer a finit de compter, le jeu vidéo est alors prévenu, et fait ce qu'il a à faire.

Les interruptions logicielles[modifier | modifier le wikicode]

Les interruptions logicielles sont différentes des deux précédentes dans le sens où elles ne sont pas déclenchées par un évènement extérieur. A la place, elles sont déclenchées par un programme en cours d'exécution, via une instruction d'interruption. On peut les voir comme des appels de fonction un peu particuliers, si ce n'est que la routine d'interruption exécutée n'est pas fournie par le programme exécuté, mais par le système d'exploitation, un pilote de périphérique ou le BIOS. Le code éxecuté ne fait pas partie du programme éxecuté, mais en est extérieur, et cela change beaucoup de choses.

Sur les PC anciens, le BIOS fournissait les routines de base et le système d'exploitation se contentait d’exécuter les routines fournies par le BIOS. Mais de nos jours, les routines d'interruptions du BIOS sont utilisées lors du démarrage de l'ordinateur, mais ne sont plus utilisées une fois le système d'exploitation lancé. Le système d'exploitation fournit ses propres routines et n'a pas plus besoin des routines du BIOS.

Les interruptions du BIOS et des autres firmwares[modifier | modifier le wikicode]

Le BIOS fournit des routines d'interruption pour gérer les périphériques et matériels les plus courants. Ce n'est pas pour rien que « BIOS » est l'abréviation de Basic Input Output System, ce qui signifie « programme basique d'entrée-sortie ». Ces routines sont standardisées de façon à assurer la compatibilité des programmes sur tous les BIOS existants. Ce standard, malgré sa simplicité, était extrêmement puissant. Il était possible de créer un OS complet en utilisant juste des appels de routine du BIOS. Par exemple, le DOS, ancêtre de Windows, utilisait exclusivement les routines du BIOS !

Certaines routines peuvent notamment effectuer plusieurs traitements : par exemple, la routine qui permet de communiquer avec le disque dur peut aussi bien lire un secteur, l'écrire, etc. Pour spécifier le traitement à effectuer, on doit placer une certaine valeur dans le registre AH du processeur : la routine est programmée pour déduire le traitement à effectuer uniquement à partir de la valeur du registre AH. Mais certaines routines ne font pas grand-chose : par exemple, l'interruption 0x12h ne fait que lire la taille de la mémoire conventionnelle, qui est mémorisée à un endroit bien précis en mémoire RAM.

Voici une description assez succincte de ces routines. Vous remarquerez que je n'ai pas vraiment détaillé ce que font ces interruptions, ni comment les utiliser. Il faut dire que de nos jours, ce n'est pas franchement utile. Mais si vous voulez en savoir plus, je vous invite à lire la liste des interruptions du BIOS de Ralf Brown, disponible via ce lien : Liste des interruptions du BIOS, établie par Ralf Brown.

Adresse de la routine dans le vecteur d'interruption Description succinte
10h Si aucune ROM vidéo n'est détectée, le BIOS peut quand même communiquer directement avec la carte graphique grâce à cette routine. Elle a plusieurs fonctions différentes et peut tout aussi bien envoyer un caractère à l'écran que renvoyer la position du curseur.
13h Cette routine du BIOS permet de lire ou d'écrire sur le disque dur ou sur une disquette. Plus précisément, cette routine lui sert à lire les premiers octets d'un disque dur afin de pouvoir charger le système d'exploitation. Elle était aussi utilisée par les systèmes d'exploitation du style MS-DOS pour lire ou écrire sur le disque dur.
14h La routine 14h était utilisée pour communiquer avec le port série RS232 de notre ordinateur.
15h La routine 15h a des fonctions diverses et variées, toutes plus ou moins rattachées à la gestion du matériel. Le BIOS était autrefois en charge de la gestion de l'alimentation de notre ordinateur : il se chargeait de la mise en veille, de réduire la fréquence du processeur, d'éteindre les périphériques inutilisés. Pour cela, la routine 15h était utilisée. Ses fonctions de gestion de l'énergie étaient encore utilisées jusqu'à la création de Windows 95. De nos jours, avec l'arrivée de la norme ACPI, le système d'exploitation gère tout seul la gestion de l'énergie de notre ordinateur et cette routine est donc obsolète. À toute règle, il faut une exception : cette routine est utilisée par certains systèmes d'exploitation modernes à leur démarrage afin d'obtenir une description correcte et précise de l'organisation de la mémoire de l'ordinateur. Pour cela, nos OS configurent cette routine en plaçant la valeur 0x0000e820 dans le registre EAX.
16h La routine 16h permet de gérer le clavier et de le configurer. Cette routine est utilisée tant que le système d'exploitation n'a pas démarré, c'est pour cela que vous pouvez utiliser le clavier pour naviguer dans l'écran de configuration de votre BIOS. En revanche, aucune routine standard ne permet la communication avec la souris : il est impossible d'utiliser la souris dans la plupart des BIOS. Certains BIOS possèdent malgré tout des routines capables de gérer la souris, mais ils sont très rares.
17h Cette routine permet de communiquer avec une imprimante sur le port parallèle de l'ordinateur. Comme les autres, on la configure avec le registre AH.
19h Cette routine est celle qui s'occupe du démarrage du système d'exploitation. Elle sert donc à lancer le système d'exploitation lors du démarrage d'un ordinateur, mais elle sert aussi en cas de redémarrage.

Les routines du BIOS étaient parfois recopiées dans la mémoire RAM afin de rendre leur exécution plus rapide. Certaines options du BIOS, souvent nommées BIOS memory shadowing, permettent justement d'autoriser ou d'interdire cette copie du BIOS dans la RAM.

Les appels systèmes des systèmes d'exploitation[modifier | modifier le wikicode]

Tout OS fournit un ensemble de routines d'interruptions spécifiques qui servent à manipuler la mémoire, gérer des fichiers, etc. Les interruptions en question sont appelées des appels système. Par exemple, linux fournit les appels systèmes open, read, write et close pour manipuler des fichiers, ou encore les appels brk, sbrk, pour allouer et désallouer de la mémoire. Évidemment, ceux-ci ne sont pas les seuls : linux fournit environ 380 appels systèmes distincts.

Les appels systèmes permettent aux programmes d'exécuter des interruptions pré-programmées, mais ne peuvent pas demander n'importe quoi au système d'exploitation. La communication entre OS et programmes est donc standardisée, limitée par une interface, ce qui limite les problèmes de sécurité et simplifie la programmation des applications.

Les appels systèmes sont un concept des systèmes d'exploitation, qui peuvent se mettre en œuvre de plusieurs manières. On peut les implémenter avec des interruptions logicielles, des instructions de commutation en mode noyau, éventuellement d'autres. Assimiler interruptions logicielles et appels systèmes est en soi une erreur, mais même si les deux sont très liés.

L'espace noyau et l'espace utilisateur[modifier | modifier le wikicode]

La différence entre une interruption et un appel de fonction est notable sur les systèmes d'exploitation modernes, où le système d'exploitation a des privilèges que les autres programmes n'ont pas. Rappelons que le système d'exploitation sert d'intermédiaire entre les autres logiciels et le matériel. Les programmes ne sont pas censés accéder d'eux-mêmes au matériel, pour des raisons de portabilité, de sécurité, et bien d'autres. Ils ne peuvent pas accéder directement au disque dur, au clavier, à la carte son, etc. À la place, ils demandent au système d'exploitation de le faire à leur place et de leur transmettre les résultats.

Les niveaux de privilèges[modifier | modifier le wikicode]

Pour forcer la séparation entre OS et programmes, les processeurs modernes intègrent des sécurités qui portent le nom de niveaux de privilèges, ou encore d'anneaux mémoires. Tous les processeurs des PC modernes (x86 64 bits) gèrent seulement deux niveaux de privilèges : un mode noyau pour le système d'exploitation et un mode utilisateur pour les applications. Tout est autorisé en mode noyau, alors le mode utilisateur a de nombreuses restrictions quant à l'accès au matériel et la mémoire. Un programme en mode utilisateur ne peut pas accéder aux périphériques, ni gérer certaines portions protégées de la mémoire. C'est un mécanisme qui force à déléguer la gestion du matériel au système d'exploitation.

Au passage, tout l'OS n'est pas en mode noyau, seule une petite partie l'est et elle porte d'ailleurs le nom de noyau du système d’exploitation.

Outre l'accès aux périphériques, l'accès à la mémoire est aussi restreint par divers mécanismes dits de protection mémoire, mais seulement en mode utilisateur. Un programme en mode utilisateur se voit attribuer une certaine portion de la mémoire RAM, et ne peut accéder qu'à celle-ci. En clair, les programmes sont isolés les uns des autres : un programme ne peut pas aller lire ou écrire dans la mémoire d'un autre, les programmes ne se marchent pas sur les pieds, les bugs d'un programme ne débordent pas sur les autres programmes, etc.

Niveaux de privilèges sur les processeurs x86.

Les anneaux mémoire/niveaux de privilèges étaient initialement gérés uniquement par des mécanismes purement logiciels, mais sont actuellement gérés par le processeur. Le processeur contient un registre qui précise si le programme en cours est en espace noyau ou en espace utilisateur. À chaque accès mémoire ou exécution d'instruction, le processeur vérifie si le niveau de privilège permet l'opération demandée. Lorsqu'un programme effectue une instruction interdite en mode utilisateur, une exception matérielle est levée. Généralement, le programme est arrêté sauvagement et un message d'erreur est affiché.

Sur certains processeurs, on trouve des niveaux de privilèges intermédiaires entre l'espace noyau et l'espace utilisateur. Les processeurs x86 des PC 32 bits contiennent 4 niveaux de privilèges. Le système Honeywell 6180 en possédait 8. À l'origine, ceux-ci ont été inventés pour faciliter la programmation des pilotes de périphériques. Mais force est de constater que ceux-ci ne sont pas vraiment utilisés, seuls les espaces noyau et utilisateur étant pertinents.

Les interruptions basculent en mode noyau[modifier | modifier le wikicode]

Toute interruption bascule automatiquement le processeur dans l'espace noyau. C'est une nécessité : on passe d'un programme en espace utilisateur à une routine qui est en espace noyau. L'accès au matériel n'étant possible qu'en mode noyau, les interruptions matérielles doivent faire la transition en espace noyau, pareil pour les appels systèmes qui manipulent le matériel. Même sans accès aux périphériques, le passage en mode noyau est nécessaire pour passer outre la protection mémoire. A cause de la protection mémoire, impossible de manipuler certaines structures de données du système d'exploitation, chose seulement possible en espace noyau. De plus, le code du système d'exploitation est inaccessible pour les autres programmes. Exécuter une routine du système d'exploitation avec un branchement vers celle-ci ne marchera pas, car le branchement pointera une instruction présente dans une portion de mémoire réservée à l'OS, auquel le programme exécutant n'a pas accès. Cela se traduira par un joli plantage de l'application.

Le passage en mode noyau n'est cependant pas gratuit, de même que l'interruption qui lui est associée. Ainsi, les interruptions sont généralement considérées comme lentes, très lentes. Elles sont beaucoup plus lentes que les appels de fonction normaux, qui sont beaucoup plus simples. Les raisons à cela sont multiples, mais la principale est la suivante : les mémoires caches doivent être vidés lors des transferts entre mode noyau et mode utilisateur. Alors attention : diverses optimisations font que seuls certains caches spécialisés dont nous n'avons pas encore parlé, comme les TLB, doivent être vidés. Mais malgré tout, cela prend beaucoup de temps.

Divers processeurs incorporent des techniques pour rendre les appels systèmes plus rapides, en remplaçant les interruptions logicielles par des instructions spécialisées (SYSCALL/SYSRET et SYSENTER/SYSEXIT d'AMD et Intel). D'autres techniques similaires tentent de faire la même chose, à savoir changer le niveau de privilège sans utiliser d'interruptions : les call gate d’Intel, les Supervisor Call instruction des IBM 360, etc.

L'implémentation des interruptions[modifier | modifier le wikicode]

Toutes les interruptions, qu'elles soient logicielles ou matérielles, ne s'implémentent pas exactement de la même manière. Mais certaines choses sont communes à toutes les interruptions et à toutes leurs mises en œuvre. Par exemple, on s'attend à ce que la majeure partie des processeurs qui supportent les interruptions disposent des fonctionnalités que nous allons voir dans ce qui suit, à savoir : un vecteur d'interruption, une pile dédiée aux interruptions, la possibilité de désactiver les interruptions, etc. Elles ne sont pas tout le temps présentes, mais leur absence est plus une exception que la régle.

Le vecteur d'interruption[modifier | modifier le wikicode]

Vu le grand nombre d'interruptions logicielles/appels système, on se doute bien qu'il y a a peu-près autant de routines d'interruptions différentes. Et celles-ci sont placées à des endroits différents en mémoire RAM. Une solution simple serait de placer chaque routine d'interruption systématiquement au même endroit en mémoire. Les appels systèmes se résumeraient alors à des appels de fonctions basiques, avec un branchement inconditionnel vers l'adresse de la routine, connue à l'avance. Mais elle n'est pas très pratique, surtout quand on ne connait pas à l'avance l'emplacement des pilotes de périphériques en mémoire. Autant on peut prévoir tout cela à l'avance pour les routines du système d'exploitation, autant on ne peut pas le faire pour les routines des pilotes de périphériques dont on ne connait pas à l'avance ni le nombre, ni la taille, ni la fonction. De plus, sur les systèmes modernes, les adresses des routines peuvent changer lors de l'exécution des programmes pour de sombres raisons impliquant de la mémoire virtuelle et que nous verrons dans les chapitres à la fin de ce wikilivre.

Pour résoudre ce problème, les systèmes d'exploitation modernes font autrement. Ils numérotent les interruptions, à savoir qu'ils leur attribuent un numéro en commençant par 0. Un PC X86 moderne gére 256 interruptions, numérotées de 0 à 255. Un appel système ne précise pas l'adresse vers laquelle faire un branchement, mais précise le numéro de l'interruption à exécuter. Le système d'exploitation s'occupe ensuite de retrouver l'adresse de la routine à partir du numéro de l'interruption. Pour cela, ils dispersent un petit peu les routines dans la mémoire, mais mémorisent l'emplacement de chaque routine. Pour cela, le système d'exploitation utilise un tableau qui contient les adresses de chaque routine : la case numéro X du tableau contient l'adresse de l'interruption numéro X. Ce tableau s'appelle le vecteur d'interruption.

Pour ceux qui connaissent la programmation, le vecteur d'interruption est un tableau de pointeurs sur fonction, les fonctions étant les routines à exécuter.

Il faut préciser que le vecteur d'interruption mémorise les adresses pour toutes les routines, sans exceptions. Non seulement il mémorise celles des appels systèmes, mais aussi les routines des exceptions matérielles, ainsi que les routines des interruptions matérielles. Sur les PC modernes, le vecteur d'interruption est stocké dans les 1024 premiers octets de la mémoire. Il gére 256 interruptions, et les 32 premières sont réservées aux exceptions matérielles.

Une interruption logicielle se déroule donc comme suit : le programme exécute une instruction d'interruption et précise le numéro de l'interruption logicielle à exécuter, puis l'OS utilise le numéro pour lire dans le vecteur d'interruption, ce qui permet de récupérer l'adresse de la routine adéquate, et effectue un branchement vers celle-ci. Avec cette méthode, l'adresse de la routine n'a pas à être précisée à l'avance, lors de la conception de l'OS, et elle peut même changer lors de l'exécution d'un programme !

Le fait que les interruptions soient désignées non pas par une adresse, mais par un numéro explique pourquoi un appel système n'est pas qu'un simple appel de fonction. Il y a un niveau d'indirection en plus. C'est une des raisons qui fait qu'il est préférable d'avoir une instruction spécifique pour le processeur, séparée de l'instruction d'appel de fonction normale.

La conversion d'un numéro d'interruption en adresse peut se faire au niveau matériel ou logiciel. S'il est fait au niveau matériel, le processeur lit l'adresse automatiquement dans le vecteur d'interruption. Avec la solution logicielle, on délègue ce choix au système d'exploitation. Dans ce cas, le processeur contient un registre qui stocke le numéro de l'interruption, ou du moins de quoi déterminer la cause de l'interruption : est-ce le disque dur qui fait des siennes, une erreur de calcul dans l'ALU, une touche appuyée sur le clavier, etc.

Le vecteur d'interruption peut être mis à jour, ce qui permet de remplacer à la volée les routines d'interruptions utilisées. Il suffit de remplacer les adresses des routines à mettre à jour par les adresses des routines adéquates. On dit qu'on déroute ou qu'on détourne le vecteur d'interruption. Tous les systèmes d'exploitation modernes le font après le démarrage de l'ordinateur, pour remplacer les interruptions du BIOS par les interruptions fournies par le système d'exploitation et les pilotes. Cette mise à jour est effectuée par le système d'exploitation, une fois que le BIOS lui a laissé les commandes.

La pile dédiée aux interruptions[modifier | modifier le wikicode]

La plupart des systèmes d'exploitation utilisent une pile d'appel dédiée, séparée, pour les interruptions. Les raisons à cela sont multiples. La principale est que sur les systèmes d'exploitation capables de gérer plusieurs programmes en même temps, c'est une solution assez évidente. Chaque programme a sa propre pile d'appel, séparée des autres. Et la routine d'interruption est un programme comme un autre, qui doit donc avoir sa propre pile d'appel.

Désactiver les interruptions[modifier | modifier le wikicode]

Il faut noter qu'il est possible de désactiver temporairement l’exécution des interruptions, quelle qu’en soit la raison. C'est beaucoup utilisé sur les systèmes multiprocesseurs, afin d'éviter des problèmes lors de lecture/écriture d'une donnée manipulée par plusieurs processeurs. Mais nous verrons cela dans le chapitre adéquat, quand nous parlerons des systèmes multicœurs/multi-processeurs.

C'est aussi utilisé dans certains systèmes dit temps réels, où les concepteurs ont besoin de garanties assez fortes pour le temps d’exécution. Dans ces systèmes, chaque morceau de code doit s’exécuter en un temps définit à l'avance, qu'il ne doit pas dépasser. Pour garantir cela, certaines portions de code ne doivent pas être interrompues par des interruptions. Par exemple, prenons le cas d'une portion de code devant s’exécuter en moins de 300 millisecondes. Imaginons aussi que le code en question prend 200 ms sans interruption. Ce code peut parfaitement dépasser son temps attitré si plusieurs interruptions surviennent avec un timing problématique : il suffit que plus de 2 interruptions de 50 ms surviennent pendant que le code s’exécute. Désactiver les interruptions pendant le temps d’exécution du code permet d'éviter cela.