Dans ce chapitre, on va voir comment les périphériques communiquent avec le processeur ou la mémoire. On sait déjà que les entrées-sorties (et donc les périphériques) sont reliées au reste de l'ordinateur par un ou plusieurs bus. Pour communiquer avec un périphérique, le processeur a juste besoin de configurer ces bus avec les bonnes valeurs. Dans la façon la plus simple de procéder, le processeur se connecte au bus et envoie sur le bus les données et commandes à adéquates. Mais il existe cependant des contraintes temporelles quant à la communication entre périphérique et processeur. Les deux composants ne vont pas à la même vitesse, ce qui impose des méthodes d'accès particulières.
Les registres d’interfaçage libèrent le processeur lors de l'accès à un périphérique, mais seulement en partie. Ils sont très utiles pour les transferts du processeur vers les périphériques. Le processeur écrit dans ces registres et fait autre chose en attendant que le périphérique ait terminé. Mais les transferts dans l'autre sens sont plus problématiques. Par exemple, imaginons que le processeur souhaite lire une donnée depuis le disque dur : le processeur envoie l'ordre de lecture en écrivant dans les registres d’interfaçage, fait autre chose en attendant que la donnée soit lue, puis récupère la donnée quand elle est disponible. Mais comment fait-il pour savoir quand la donnée lue est disponible ? De même, le processeur ne peut pas (sauf cas particuliers) envoyer une autre commande au contrôleur de périphérique tant que la première commande n'est pas traitée, mais comment sait-il quand le périphérique en a terminé avec la première commande ? Pour résoudre ces problèmes, il existe globalement trois méthodes : le "pooling", l'usage d'interruptions, et le "Direct Memory Access".
Le "pooling".
La première solution est celle du "Pooling". Elle consiste à vérifier périodiquement si le périphérique a reçu ou envoyé quelque chose. Par exemple, après avoir envoyé un ordre au contrôleur, le processeur vérifie périodiquement si le contrôleur est prêt pour un nouvel envoi de commandes/données. Sinon le processeur vérifie régulièrement si le périphérique a quelque chose à lire dire, au cas où le périphérique veut entamer une transmission. Généralement, il suffit au processeur de lire le registre d'état du contrôleur : un bit spécial de celui-ci permet d'indiquer si le contrôleur est libre ou occupé. Pour la lecture, la situation est similaire : le processeur doit lire régulièrement son contenu pour voir si le périphérique ne lui a pas envoyé quelque chose.
Le "Pooling" permet de ne pas rester connecté en permanence durant le temps que met le périphérique pour effectuer une lecture ou une écriture, mais ce n'est pas parfait : ces vérifications périodiques sont autant de temps perdu pour le processeur. Pour solutionner ce problème, on a décidé d’utiliser des interruptions !
Les interruptions.
La vérification régulière des registres d’interfaçage prend du temps que le processeur pourrait utiliser pour autre chose. Pour réduire à néant ce temps perdu, certains processeurs supportent les interruptions. Il s'agit de fonctionnalités du processeur, qui interrompent temporairement l’exécution d'un programme pour réagir à un événement extérieur (matériel, erreur fatale d’exécution d'un programme…). L'interruption va effectuer un petit traitement (ici, communiquer avec un périphérique), réalisé par un programme nommé routine d'interruption. Avec ces interruptions, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail : il suffit que le contrôleur de périphérique prévienne le processeur avec une interruption.
Lorsqu'un processeur exécute une interruption, celui-ci :
L'appel d'une routine d'interruption est similaire à un appel de fonction, mais qui serait déclenché par des raisons toutes autres. Les manipulations à faire lors de l'exécution d'une routine d'interruption sont similaires à celles à faire lors d'un appel de fonction. Lors de l’exécution d'une interruption, il faut sauvegarder les registres du processeur, vu que la routine peuvent écraser des données dans les registres. Cette sauvegarde n'est pas toujours faite automatiquement par notre processeur : c'est parfois le programmeur qui doit coder lui-même la sauvegarde de ces registres dans la routine d'interruption elle-même. Certains processeurs fournissent des registres spécialement dédiés aux interruptions, qui ne sont accessibles que par les routines d'interruptions : cela évite d'avoir à sauvegarder les registres généraux. D'autres utilisent le fenêtrage de registres, avec une fenêtre pour les interruptions et une autre pour les programmes. Pour savoir s'il est dans une interruption, le processeur utilise une bascule.
Le vecteur d'interruptions.
Devant la multiplicité des périphériques, on se doute bien qu'il n'existe pas d'interruption à tout faire. Une routine d'interruption qui s'occupe de communiquer avec le disque dur sera différente d'une routine agissant sur la carte graphique. On a donc besoin de plusieurs routines d'interruption : au moins une par périphérique, souvent plusieurs par périphérique. Certains ordinateurs utilisent une partie de leur mémoire pour stocker les adresses de chaque routine : cette portion de mémoire s'appelle le vecteur d'interruption. Lorsqu'une interruption a lieu, le processeur va automatiquement aller chercher son adresse dans ce vecteur d'interruption. Une autre solution est simplement de déléguer cette gestion du choix de l’interruption au système d'exploitation : l'OS devra alors traiter l'interruption tout seul. Dans ce cas, le processeur contient un registre qui stockera des bits qui permettront à l'OS de 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 masquage d'interruptions.
Quand plusieurs interruptions se déclenchent en même temps, on ne peut en exécuter qu'une seule. Et certaines interruptions sont prioritaires sur les autres : par exemple, l'interruption qui gère l'horloge système est plus prioritaire qu'une interruption en provenance de périphériques lents comme le disque dur ou une clé USB. Quand plusieurs interruptions souhaitent s'exécuter en même temps, on choisit d'exécuter celle qui est la plus prioritaire. Les autres interruptions sont alors mises en attente : on parle de masquage d'interruption. Le masquage d'interruption permet de bloquer des interruptions temporairement, et de les exécuter ultérieurement, une fois le masquage d'interruption levé.
Certaines interruptions ne sont pas masquables. Il s'agit généralement d'erreurs matérielles importantes, comme une erreur de protection mémoire, une division par zéro au niveau du processeur, une surchauffe du processeur, etc. Ces interruptions sont systématiquement exécutées en priorité, et ne sont jamais masquées.
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.
Les différents types d'interruptions.
Il y a trois méthodes pour déclencher une interruption :
Les interruptions logicielles 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, avec cependant quelques différences. La première est que la routine d'interruption exécutée n'est pas fournie par le programme exécuté. Pour un appel de fonction, la fonction est inclue dans le programme exécuté et un simple branchement suffit pour faire l'appel de fonction. Par contre, avec les interruptions logicielles, la routine exécutée est une routine du système d'exploitation ou du BIOS. Et cela entraine beaucoup de conséquences. Par exemple, sur les systèmes à mémoire virtuelle, la routine d'interruption n'est pas forcément dans le même espace d'adressage. Toujours est-il que les interruptions logicielles permettent au programmeur d'utiliser des interruptions, quelle qu’en soit la raison. Ces interruptions logicielles sont surtout utilisées par les programmes ayant besoin d'accéder au matériel directement, à savoir les pilotes de périphériques ou les systèmes d'exploitation.
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... Pour pouvoir exécuter des exceptions matérielles, 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 modifier ses circuits de division pour ajouter de quoi 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.
Les IRQ sont des interruptions déclenchées par un périphérique. Dans une implémentation simple des IRQ, chaque périphérique envoie ses interruptions au processeur via une entrée IRQ : la mise à 1 de cette entrée déclenche une interruption bien précise au cycle suivant. Pour économiser des entrées, on a inventé le contrôleur d'interruptions, un circuit sur lequel on connecte tous les fils d'IRQ. Ce contrôleur va gérer les priorités et les masquages. Ce contrôleur envoie un signal d'interruption IRQ global au processeur et un numéro qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Parfois, ce numéro n'est pas envoyé au processeur directement, mais stocké dans un registre, accessible via le bus de données.
Le "Direct memory access".
Avec nos interruptions, seul le processeur gère l'adressage de la mémoire. Impossible par exemple, de permettre à un périphérique d'adresser la mémoire RAM ou un autre périphérique. Il doit donc forcément passer par le processeur, et le monopoliser durant un temps assez long, au lieu de laisser notre processeur exécuter son programme tranquille. Pour éviter cela, on a inventé le "bus mastering". Grâce au "bus mastering", le périphérique adresse la mémoire directement. Il est capable d'écrire ou lire des données directement sur les différents bus. Ainsi, un périphérique peut accéder à la mémoire, ou communiquer avec d’autres périphériques directement, sans passer par le processeur. Le direct memory access est une technologie de "bus mastering" qui permet aux périphériques d'accéder à la RAM sans passer par le processeur. Elle peut même servir à transférer des données de la mémoire vers la mémoire, pour effectuer des copies de très grosses données.
Le contrôleur DMA.
Avec la technologie DMA, l'échange de données entre le périphérique et la mémoire est intégralement géré par un circuit spécial, intégré au périphérique et relié au bus mémoire : le contrôleur DMA. Ce contrôleur DMA est capable de transférer un gros bloc de mémoire entre un périphérique et la mémoire. Il contient des registres dans lesquels le processeur pour initialiser un transfert de données. Ces registres contiennent, au minimum un registre pour l'adresse du segment de la mémoire, un autre pour la longueur du segment de mémoire. Le travail du contrôleur est assez simple. Celui-ci doit se contenter de placer les bonnes valeurs sur les bus, pour effectuer le transfert. Il va donc initialiser le bus d'adresse à l'adresse du début du bloc de mémoire. Puis, à chaque fois qu'une donnée est lue ou écrite sur le périphérique, il va augmenter l'adresse de ce qu'il faut pour sélectionner le bloc de mémoire suivant. Le transfert peut aller dans les deux sens : du périphérique vers la RAM, ou de la RAM vers le périphérique.
Le sens du transfert, ainsi que les informations sur le bloc de mémoire à transférer, sont précisés dans des registres interne au contrôleur DMA. On trouve aussi parfois un ou plusieurs registres de contrôle. Ces registres de contrôle peuvent contenir beaucoup de choses : avec quel périphérique doit-on échanger des données, les données sont-elles copiées du périphérique vers la RAM ou l'inverse, et bien d’autres choses encore. Lorsqu'un périphérique souhaite accéder à la mémoire ou qu'un programme veut envoyer des données à un périphérique, il déclenche l'exécution d'une interruption et configure le contrôleur DMA avec les données nécessaires pour démarrer le transfert de donnée.
Il existe trois façons de transférer des données entre le périphérique et la mémoire : le mode block, le mode cycle stealing, et le mode transparent.
DMA et cohérence des caches.
Le contrôleur DMA pose un problème sur les architectures avec une mémoire cache reliée au processeur. Le problème est que le contrôleur mémoire peut modifier n'importe quelle portion de la RAM, y compris une qui est mise en cache. Or, les changements dans la RAM ne sont pas automatiquement propagés au cache. Dans ce cas, le cache contient une copie de la donnée obsolète, qui ne correspond plus au contenu écrit dans la RAM par le contrôleur DMA. Pour résoudre ce problème, on interdit de charger dans le cache des données stockées dans les zones de la mémoire qui peuvent être modifiées par des périphériques ou des contrôleurs DMA. Toute lecture ou écriture dans ces zones de mémoire ira donc directement dans la mémoire RAM, sans passer par la ou les mémoires caches.