Fonctionnement d'un ordinateur/Les méthodes de synchronisation entre processeur et périphériques

Un livre de Wikilivres.

Dans ce chapitre, nous allons 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 et les interruptions de type IRQ[modifier | modifier le wikicode]

La solution la plus simple, appelée Pooling, est de vérifier périodiquement si le périphérique a 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 à dire, au cas où le périphérique veut entamer une transmission. Pour faire cette vérification, le processeur a juste à lire le registre d'état du contrôleur : un bit de celui-ci indique si le contrôleur est libre ou occupé. Le Pooling est une solution logicielle très imparfaite, car ces vérifications périodiques sont du temps de perdu pour le processeur. Aussi, d'autres solutions ont été inventées.

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. Pour rappel, 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…). Lors d'une interruption, le processeur suit la procédure suivante :

  • arrête l'exécution du programme en cours et sauvegarde l'état du processeur (registres et program counter) ;
  • exécute un petit programme nommé 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

Dans le chapitre sur les fonctions et la pile d'appel, nous avions vu qu'il existait plusieurs types d'interruptions différents. Les interruptions logicielles sont déclenchées par une instruction spéciale et sont des appels de fonctions spécialisés. Les exceptions matérielles se déclenchent quand le processeur rencontre une erreur : division par zéro, problème de segmentation, etc. Les interruptions matérielles, aussi appelées IRQ, sont des interruptions déclenchées par un périphérique et ce sont celles qui vont nous intéresser dans ce qui suit.

Avec ces IRQ, le processeur n'a pas à vérifier périodiquement si le contrôleur de périphérique a fini son travail. A la place, le contrôleur de périphérique prévient le processeur avec une interruption. 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. Pas besoin d'utiliser du pooling, pas besoin de vérifier sans cesse si un périphérique a quelque chose à signaler. A la place, le périphérique déclenche une interruption quand il a quelque chose à dire.

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

Implémenter les interruptions matérielles demande d'ajouter des circuits à la fois sur le processeur et sur la carte mère. On peut distinguer deux cas : soit on fournit une interruption matérielle unique, soit on permet à plusieurs périphériques de déclencher une interruption. Nous allons voir ces deux cas dans cet ordre, histoire de voir le cas le plus simple (et peu utilisé), avant de voir le cas plus complexe.

L'entrée d'interruption[modifier | modifier le wikicode]

Dans le cas le plus simple, le processeur n'est relié qu'un seul périphérique capable de générer des interruptions. On peut par exemple imaginer le cas d'un thermostat, basé sur un couple processeur/RAM/ROM, relié à un capteur de mouvement, qui commande une alarme. Le processeur n'a pas besoin d'interruptions pour gérer l'alarme, mais le capteur de mouvement fonctionne avec des interruptions. Dans ce cas, on a juste besoin d'ajouter une entrée sur le processeur, appelée l'entrée d'interruption, souvent notée INTR ou INT.

L'entrée d'interruption peut fonctionner de deux manières différentes, qui portent le nom d'entrée déclenchée par niveau logique et d'entrée déclenchée par front montant/descendant. Les noms sont barbares mais recouvrent des concepts très simples.

Les entrées d'interruption déclenchées par niveau logique[modifier | modifier le wikicode]

Le plus simple est le cas de l'entrée déclenchée par niveau logique : la mise à 1/0 de cette entrée déclenche une interruption au cycle d'horloge suivant. En général, il faut mettre l'entrée INT à 0 pour déclencher une interruption, mais nous allons considérer l'inverse dans ce qui suit. Le processeur vérifie au début de chaque cycle d'horloge si cette entrée est mise à 0 ou 1 et agit en conséquence.

Dans le cas le plus basique, le processeur reste en état d'interruption tant que l'entrée n'est pas remise à 0, généralement quand le processeur prévient le périphérique que la routine d'interruption est terminée. Cette solution est très simple pour détecter les interruptions, mais pose le problème de la remise à zéro de l'entrée.

Une autre solution consiste à utiliser des signaux d'interruption très brefs, qui mettent l'entrée à 1 durant un cycle d'horloge, avant de revenir à 0 (ou l'inverse). Le signal d'interruption ne dure alors qu'un cycle d'horloge, mais le processeur le mémorise dans une bascule que nous nommerons INT#BIT dans ce qui suit. La bascule INT#BIT permet de savoir si le processeur est en train de traiter une interruption ou non. Elle est mise à 1 quand on présente un 1 sur l'entrée d'interruption, mais elle est remise à 0 par le processeur, quand celui-ci active l'entrée Reset de la bascule à la fin d'une routine d'interruption.

Les entrées d'interruption déclenchées par front montant/descendant[modifier | modifier le wikicode]

A l'opposé, avec une entrée déclenchée par front montant/descendant, on doit envoyer un front montant ou descendant sur l'entrée pour déclencher une interruption. La remise à zéro de l'entrée est plus simple qu'avec les entrées précédentes. Si l'entrée détecte aussi bien les fronts montants que descendants, il n'y a pas besoin de remettre l'entrée à zéro.

Le problème de ces entrées est que le signal d'interruption arrive pendant un cycle d'horloge et que le processeur ne peut pas le détecter facilement. Pour cela, il faut ajouter quelques circuits qui détectent si un front a eu lieu pendant un cycle, et indique le résultat au processeur. Ces circuits traduisent l'entrée par front en entrée par niveau logique, si on peut dire. Il s'agit le plus souvent d'une bascule déclenchée sur front montant/descendant, rien de plus.

L'implémentation des IRQ multiples avec une entrée d'interruption[modifier | modifier le wikicode]

Le cas avec plusieurs périphériques est plus compliqué. Dans une implémentation simple des IRQ, chaque périphérique envoie ses interruptions au processeur via une entrée dédiée. Mais cela demande de brocher plusieurs entrées d'interruption au processeur, une par périphérique, ce qui fait beaucoup et limite le nombre de périphériques supportés. Et c'est dans le cas où chaque périphérique n'a qu'une seule interruption, mais un périphérique peut très bien utiliser plusieurs interruptions. Par exemple, un disque dur peut utiliser une interruption pour dire qu'une écriture est terminée, une autre pour dire qu'il est occupé et ne peut pas accepter de nouvelles demandes de lecture/écriture, etc.

Entrées d'interruptions séparées pour chaque périphérique

Une autre possibilité est de connecter tous les périphériques à l'entrée d'interruption à travers une porte OU ou un OU câblé, mais elle a quelques problèmes. Déjà, cela suppose que l'entrée d'interruption est une entrée déclenchée par niveau logique. Mais surtout, elle ne permet pas de savoir quel périphérique a causé l'interruption, et le processeur ne sait pas quelle routine exécuter.

Entrée d'interruption partagée

Le contrôleur d'interruption[modifier | modifier le wikicode]

Pour résoudre ce problème, il est possible de modifier la solution précédente en ajoutant un numéro d'interruption qui précise quel périphérique a envoyé l'interruption, qui permet de savoir quelle routine exécuter. Au lieu d'avoir une entrée par interruption possible, on code l'interruption par un nombre et on passe donc de entrées à entrées. Le processeur récupére ce numéro d'interruption, qui est généré à l'extérieur du processeur.

Pour implémenter cette solution, on a inventé le contrôleur d'interruptions. C'est un circuit qui récupère toutes les interruptions envoyées par les périphériques et qui en déduit : le signal d'interruption et le numéro de l'interruption. Le numéro d'interruption est souvent mémorisé dans un registre interne au contrôleur d'interruption. Il dispose d'une entrée par interruption/périphérique possible et une sortie de 1 bit qui indique si une interruption a lieu. Il a aussi une sortie pour le numéro de l'interruption.

Pour récupérer le numéro d'interruption, le processeur doit communiquer avec le contrôleur d'interruption. Et cette communication ne peut pas passer par l'entrée d'interruption, mais passe par un mécanisme dédié. Dans le cas le plus simple, le numéro d'interruption est envoyé au processeur sur un bus dédié. Le défaut de cette technique est qu'elle demande d'ajouter des broches d'entrée sur le processeur. Avec l'autre solution, le contrôleur d'interruption est mappé en mémoire, il est connecté au bus et est adressable comme tout périphérique mappé en mémoire. Le numéro d'interruption est alors toujours mémorisé dans un registre interne au contrôleur d'interruption, et le processeur lit ce registre en passant par le bus de données.

Contrôleur d'interruptions IRQ

L'intérieur d'un contrôleur d'interruption n'est en théorie pas très compliqué. Déterminer le signal d'interruption demande de faire un simple OU entre les entrées d'interruptions. Déduire le numéro de l'interruption demande d'utiliser un simple encodeur, de préférence a priorité. Pour gérer le masquage, il suffit d'ajouter un circuit de masquage en amont de l'encodeur, ce qui demande quelques portes logiques ET/NON.

Les contrôleurs d'interruption en cascade[modifier | modifier le wikicode]

Il est possible d'utiliser plusieurs contrôleurs d'interruption en cascade. C'était le cas sur les premiers processeurs d'Intel, notamment le 486, où on avait un contrôleur d'interruption maitre et un esclave. Les deux contrôleurs étaient identiques : c'était des Intel 8259, qui géraient 8 interruptions avec 8 entrées IRQ et une sortie d'interruption. Le contrôleur esclave gérait les interruptions liées au bus ISA, le bus pour les cartes d'extension utilisé à l'époque, et le contrôleur maitre gérait le reste.

Le fonctionnement était le suivant. Si une interruption avait lieu en-dehors du bus ISA, le contrôleur maitre gérait l'interruption. Mais si une interruption avait lieu sur le bus ISA, le contrôleur esclave recevait l'interruption, générait un signal transmis au contrôleur maitre sur l'entrée IRQ 2, qui lui-même transmettait le tout au processeur. Lee processeur accédait alors au bus qui le reliait aux deux contrôleurs d'interruption, et lisait le registre pour récupérer le numéro de l'interruption.

Contrôleurs d'interruptions IRQ du 486 d'Intel.
Intel 8259

En théorie, jusqu'à 8 contrôleurs 8259 peuvent être mis en cascade, ce qui permet de gérer 64 interruptions. Il faut alors disposer de 8 contrôleurs esclave et d'un contrôleur maitre. La mise en cascade est assez simple sur le principe : il faut juste envoyer la sortie INTR de l'esclave sur une entrée d'IRQ du contrôleur maitre. Il faut cependant que le processeur sache dans quel 8259 récupérer le numéro de l'interruption.

Pour cela, l'Intel 8259 disposait de trois entrées/sorties pour permettre la mise en cascade, nommées CAS0, CAS1 et CAS2. Sur ces entrées/sorties, on trouve un identifiant allant de 0 à 8, qui indique quel contrôleur 8259 est le bon. On peut le voir comme si chaque 8259 était identifié par une adresse codée sur 3 bits, ce qui permet d'adresser 8 contrôleurs 8259. Les 8 valeurs permettent d'adresser aussi bien le maitre que l'esclave, sauf dans une configuration : celles où on a 1 maitre et 8 esclaves. Dans ce cas, on considère que le maitre n'est pas adressable et que seuls les esclaves le sont. Cette limitation explique pourquoi on ne peut pas dépasser les 8 contrôleurs en cascade.

Les entrées/sorties CAS de tous les contrôleurs 8259 sont connectées entre elles via un bus, le contrôleur maitre étant l'émetteur, les autres 8259 étant des récepteurs. Le contrôleur maitre émet l'identifiant du contrôleur esclave dans lequel récupérer le numéro de l'interruption sur ce bus. Les contrôleurs esclaves réagissent en se connectant ou se déconnectant du bus utilisé pour transmettre le numéro d'interruption. Le contrôleur adéquat, adressé par le maitre, se connecte au bus alors que les autres se déconnectent. Le processeur est donc certain de récupérer le bon numéro d'interruption. Lorsque c'est le maitre qui dispose du bon numéro d'interruption, il se connecte au bus, mais envoie son numéro aux 8259 esclaves pour qu'ils se déconnectent.

Les Message Signaled Interrupts[modifier | modifier le wikicode]

Les interruptions précédentes demandent d'ajouter du matériel supplémentaire, relié au processeur : une entrée d'interruption, un contrôleur d'interruption, de quoi récupérer le numéro de l'interruption. Et surtout, il faut ajouter des fils pour que chaque périphérique signale une interruption. Prenons l'exemple d'une carte mère qui dispose de 5 ports ISA, ce qui permet de connecter 5 périphériques ISA sur la carte mère. Chaque port ISA a un fil d'interruption pour signaler que le périphérique veut déclencher une interruption, ce qui demande d'ajouter 5 fils sur la carte mère, pour les connecter au contrôleur de périphérique. Cela n'a pas l'air grand-chose, mais rappelons qu'une carte mère moderne gère facilement une dizaine de bus différents, donc certains pouvant connecter une demi-dizaine de composants. Le nombre de fils à câbler est alors important.

Il existe cependant un type d'interruption qui permet de se passer des fils d'interruptions : les interruptions signalées par message, Message Signaled Interrupts (MSI) en anglais. Elles sont utilisées sur le bus PCI et son successeur, le PCI-Express, deux bus très importants et utilisés pour les cartes d'extension dans presque tous les ordinateurs modernes et anciens.

Les interruptions signalées par message sont déclenchées quand le périphérique écrit dans une adresse allouée spécifiquement pour, appelée une adresse réservée. Le périphérique écrit un message qui donne des informations sur l'interruption : le numéro d'interruption au minimum, souvent d'autres informations. L'adresse réservée est toujours la même, ce qui fait que le processeur sait à quelle adresse lire ou récupérer le message, et donc le numéro d'interruption. Le message est généralement assez court, il faut rarement plus, ce qui fait qu'une simple adresse suffit dans la majorité des cas. Il fait 16 bits pour le port PCI ce qui tient dans une adresse, le bus PCI 3.0 MSI-X alloue une adresse réservée pour chaque interruption.

Le contrôleur d'interruption détecte toute écriture dans l'adresse réservée et déclenche une interruption si c'est le cas. Sauf que le contrôleur n'a pas autant d'entrées d'interruption que de périphériques. A la place, il a une entrée connectée au bus et il monitore en permanence le bus. Si une adresse réservée est envoyée sur le bus d'adresse, le contrôleur de périphérique émet une interruption sur l'entrée d'interruption du processeur. Les gains en termes de fils sont conséquents. Au lieu d'une entrée par périphérique (voire plusieurs si le périphérique peut émettre plusieurs interruptions), on passe à autant d'entrée que de bits sur le bus d'adresse. Et cela permet de supporter un plus grand nombre d'interruptions différentes par périphérique.

Un exemple est le cas du bus PCI, qui possédait 2 fils pour les interruptions. Cela permettait de gérer 4 interruptions maximum. Et ces fils étaient partagés entre tous les périphériques branchés sur les ports PCI, ce qui fait qu'ils se limitaient généralement à un seul fil par périphérique (on pouvait brancher au max 4 ports PCI sur une carte mère). Avec les interruptions par message, on passait à maximum 32 interruptions par périphérique, interruptions qui ne sont pas partagées entre périphérique, chacun ayant les siennes.

Les généralités sur les interruptions matérielles[modifier | modifier le wikicode]

Peu importe que les interruptions soient implémentées avec une entrée d'interruption ou avec une signalisation par messages, certaines fonctionnalités sont souvent disponibles. Par exemple, il est possible de prioriser certaines interruptions sur les autres, ce qui permet de gérer le cas où plusieurs interruptions ont lieu en même temps. De même, il est possible de mettre en attente certaines interruptions, voire de les désactiver. Autant désactiver les interruptions est possible autant pour les IRQ que les interruptions logicielles et exceptions, certaines fonctionnalités sont spécifiques aux interruptions matérielles.

Les priorités d'interruption[modifier | modifier le wikicode]

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 prioritaire sur les interruptions 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 exécute d'abord celle qui est la plus prioritaire, les autres sont alors mises en attente.

La gestion des priorités est gérée par le contrôleur d'interruption, ou alors par le processeur si le contrôleur d'interruption est absent. Pour gérer les priorités, l'encodeur présent dans le contrôleur de périphérique doit être un encodeur à priorité, et cela suffit. On peut configurer les priorités de chaque interruption, à condition que l'encodeur à priorité soit configurable et permette de configurer les priorités de chaque entrée.

Le masquage d'interruptions[modifier | modifier le wikicode]

Le masquage d'interruption permet de bloquer des interruptions temporairement, et de les exécuter ultérieurement, une fois le masquage d'interruption levé. Il est pris en charge soit par le contrôleur d'interruption, soit le processeur (surtout si le contrôleur d'interruption est absent). Il est utile quand on veut temporairement supprimer les interruptions, ce qui est utile dans certaines situations assez complexes, notamment quand le système d'exploitation en a besoin.

Le masquage peut être sélectif, à savoir qu'on peut décider d'ignorer certaines interruptions mais pas d'autres. Par exemple, on peut ignorer l'interruption numéro 5 provenant du disque dur, mais pas l'interruption numéro 0 du watchdog timer. Pour cela, les processeurs disposent d'un registre appelé l'interrupt mask register. Chaque bit de ce registre est associé à une interruption : le bit numéro 0 est associé à l'interruption numéro 0, le bit 1 à l'interruption 1, etc. Si le bit est mis à 1, l'interruption correspondante est ignorée. Inversement, si le bit est mis à 0 : l'interruption n'est pas masquée.

Certaines interruptions ne sont pas masquables et sont systématiquement exécutées en priorité. Il s'agit généralement d'erreurs matérielles importantes, qui peuvent se ranger dans deux cas de figures : une défaillance matérielle, et un watchdog timer.

Pour rappel, un watchdog timer est un timer qui sert à détecter les dysfonctionnements matériel, notamment les plantages ou les freezes. Pour cela, le 'watchdog timer génère régulièrement un signal d'interruption non-masquable. La routine d'interruption réinitialise le 'watchdog timer, si tout se passe bien. On part du principe que si l'ordinateur ne réinitialise par le watchdog timer, alors c'est qu'il a planté.

Les défaillances matérielles regroupent des situations très variées : une perte de l'alimentation, une erreur de parité mémoire, une surchauffe du processeur, etc. Le résultat de telles interruptions est que l'ordinateur est arrêté de force, ou alors affiche un écran bleu. Les défaillances matérielles sont généralement détectées par un paquet de circuits dédiés, mais il est généralement difficile de savoir d'où provient l'erreur, quel est le matériel ou périphérique responsable. C'est possible s'il s'agit d'une erreur de mémoire RAM, comme une lecture dont l'ECC détecte une corruption de mémoire, ou d'un problème de surchauffe, d'alimentation. Mais dans les autres cas, difficile de savoir quel est le problème.

Il faut noter que certains processeurs ont deux entrées d'interruption séparées : une pour les interruptions masquables, une autre pour les interruptions non-masquables. C'est le cas des premiers processeurs x86 des PCs, qui disposent d'une entrée INTR pour les interruptions masquables, et une entrée NMI pour les interruptions non-masquables.

Le Direct memory access[modifier | modifier le wikicode]

Avec les interruptions, seul le processeur gère l'adressage de la mémoire. Impossible à un périphérique d'adresser la mémoire RAM ou un autre périphérique, il doit forcément passer par l'intermédiaire du processeur. Pour éviter cela, on a inventé le bus mastering, qui permet à un périphérique de lire/écrire sur le bus système. C'est suffisant pour leur permettre d'adresser la mémoire directement ou de communiquer avec d’autres périphériques directement, sans passer par le processeur.

Le Direct Memory Access, ou DMA, est une technologie de bus mastering qui permet de copier un bloc de mémoire d'une source vers la destination. La source et la destination peuvent être la mémoire ou un périphérique, ce qui permet des transferts mémoire -> mémoire (des copies de données, donc), mémoire -> périphérique, périphérique -> mémoire, périphérique -> périphérique.

Le bloc de mémoire commence à une adresse appelée adresse de départ, et a une certaine longueur. Soit il est copié dans un autre bloc de mémoire qui commence à une adresse de destination bien précise, soit il est envoyé sur le bus à destination du périphérique. Ce dernier le reçoit bloc pièce par pièce, mot mémoire par mot mémoire. Sans DMA, le processeur doit copier le bloc de mémoire mot mémoire par mot mémoire, byte par byte. Avec DMA, la copie se fait encore mot mémoire par mémoire, mais le processeur n'est pas impliqué.

Le contrôleur DMA[modifier | modifier le wikicode]

Avec le 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 : le contrôleur DMA. Il est généralement intégré au périphérique, plus rarement placé sur la carte mère, parfois intégré au processeur, mais est toujours connecté au bus mémoire.

Le contrôleur DMA contient des registres d'interfaçage dans lesquels le processeur écrit pour initialiser un transfert de données. Un transfert DMA s'effectue sans intervention du processeur, sauf au tout début pour initialiser le transfert, et à la fin du transfert. Le processeur se contente de configurer le contrôleur DMA, qui effectue le transfert tout seul. Une fois le transfert terminé, le processeur est prévenu par le contrôleur DMA, qui déclenche une interruption spécifique quand le transfert est fini.

Le contrôleur DMA contient généralement deux compteurs : un pour l'adresse de la source, un autre pour le nombre de bytes restants à copier. Le compteur d'adresse est initialisé avec l'adresse de départ, celle du bloc à copier, et est incrémenté à chaque envoi de données sur le bus. L'autre compteur est décrémenté à chaque copie d'un mot mémoire sur le bus. Le second compteur, celui pour le nombre de bytes restant, est purement interne au contrôleur mémoire. Par contre, le contenu du compteur d'adresse est envoyé sur le bus d'adresse à chaque copie de mot mémoire.

Outre les compteurs, le contrôleur DMA contient aussi des registres de contrôle. Ils mémorisent des informations très variées : 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.

Controleur DMA

Le contrôleur DMA contient aussi des registres internes pour effectuer la copie de la source vers la destination. La copie se fait en effet en deux temps : d'abord on lit le mot mémoire à copier dans l'adresse source, puis on l'écrit dans l'adresse de destination. Entre les deux, le mot mémoire est mémorisé dans un registre temporaire, de même taille que la taille du bus. Mais sur certains bus, il existe un autre mode de transfert, où le controleur DMA ne sert pas d'intermédiaire. Le périphérique et la mémoire sont tous deux connectés directement au bus mémoire, la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts. Les deux modes sont différents, le premier étant plus lent mais beaucoup plus simple à mettre en place. Il est aussi très facile à mettre en palce quand le périphérique et la mémoire n'ont pas la même vitesse.

Les modes de transfert DMA[modifier | modifier le wikicode]

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.

Dans le mode block, le contrôleur mémoire se réserve le bus mémoire, et effectue le transfert en une seule fois, sans interruption. Cela a un désavantage : le processeur ne peut pas accéder à la mémoire durant toute la durée du transfert entre le périphérique et la mémoire. Alors certes, ça va plus vite que si on devait utiliser le processeur comme intermédiaire, mais bloquer ainsi le processeur durant le transfert peut diminuer les performances. Dans ce mode, la durée du transfert est la plus faible possible. Il est très utilisé pour charger un programme du disque dur dans la mémoire, par exemple. Eh oui, quand vous démarrez un programme, c'est souvent un contrôleur DMA qui s'en charge !

Dans le mode cycle stealing, on est un peu moins strict : cette fois-ci, le contrôleur ne bloque pas le processeur durant toute la durée du transfert. En cycle stealing, le contrôleur va simplement transférer un mot mémoire (un octet) à la fois, avant de rendre la main au processeur. Puis, le contrôleur récupérera l'accès au bus après un certain temps. En gros, le contrôleur transfère un mot mémoire, fait une pause d'une durée fixe, puis recommence, et ainsi de suite jusqu'à la fin du transfert.

Et enfin, on trouve le mode transparent, dans lequel le contrôleur DMA accède au bus mémoire uniquement quand le processeur ne l'utilise pas.

Les limitations des controleurs DMA[modifier | modifier le wikicode]

Les contrôleurs DMA sont parfois limités sur quelques points, ce qui a des répercussions dont il faut parler. Les limitations que nous voir ne sont pas systématiques, mais elles sont fréquentes, aussi il vaut mieux les connaitres.

Les limitations en terme d'adressage[modifier | modifier le wikicode]

La première limitation est que le contrôleur DMA n'a pas accès à tout l'espace d'adressage du processeur. Par exemple, le contrôleur DMA du bus ISA, que nous étudierons plus bas, avait accès à seulement aux 16 mébioctets au bas de l'espace d'adressage, à savoir les premiers 16 mébioctets qui commencent à l'adresse zéro. Le processeur pouvait adresser bien plus de mémoire. La chose était courante sur les systèmes 32 bits, où les contrôleurs DMA géraient des adresses de 20, 24 bits. Le problème est systématique sur les processeurs 64 bits, où les contrôleurs DMA ne gèrent pas des adresses de 64 bits, mais de bien moins.

Typiquement, le contrôleur DMA ne gére que les adresses basses de l'espace d'adressage. Le pilote de périphérique connait les limitations du contrôleur DMA, et prépare les blocs aux bons endroits, dans une section adressable par le contrôleur DMA. Le problème est que les applications qui veulent communiquer avec des périphériques préparent le bloc de données à transférer à des adresses hautes, inaccessibles par le contrôleur DMA.

La solution la plus simple consiste à réserver une zone de mémoire juste pour les transferts DMA avec un périphérique, dans les adresses basses accessibles au contrôleur DMA. La zone de mémoire est appelée un tampon DMA ou encore un bounce buffer. Si une application veut faire un transfert DMA, elle copie les données à transmettre dans le tampon DMA et démarre le transfert DMA par l'intermédiaire du pilote de périphérique. Le transfert en sens inverse se fait de la même manière. Le périphérique copie les données transmises dans le tampon DMA, et son contenu est ensuite copié dans la mémoire de l'application demandeuse. Il peut y avoir des copies supplémentaires vu que le tampon DMA est souvent géré par le pilote de périphérique en espace noyau, alors que l'application est en espace utilisateur. Diverses optimisations visent à réduire le nombre de copies nécessaires, elles sont beaucoup utilisées par le code réseau des systèmes d'exploitation.

Les limitations en terme d'alignement[modifier | modifier le wikicode]

La seconde limitation est que certains contrôleurs DMA ne gèrent que des transferts alignés, c'est-à-dire que les adresses de départ/fin du transfert doivent être des multiples de 4, 8, 15, etc. La raison est que le contrôleur DMA effectue les transferts par blocs de 4, 8, 16 octets. En théorie, les blocs pourraient être placés n'importe où en mémoire, mais il est préférable qu'ils soient alignés pour simplifier le travail du contrôleur DMA et économiser quelques circuits. Les registres qui mémorisent les adresses sont raccourcis, on utilise moins de fils pour le bus mémoire ou la sortie du contrôleur DMA, etc.

À noter que cet alignement est l'équivalent pour le contrôleur DMA de l'alignement mémoire du processeur. Mais les deux sont différents : il est possible d'avoir un processeur avec un alignement mémoire de 4 octets, couplé à un contrôleur DMA qui gère des blocs alignés sur 16 octets.

Un défaut de cet alignement est que les blocs à transférer via DMA doivent être placés à des adresses bien précises, ce qui n'est pas garanti. Si le pilote de périphérique est responsable des transferts DMA, alors rien de plus simple : il dispose de mécanismes logiciels pour allouer des blocs alignés correctement. Mais si le bloc est fourni par un logiciel (par exemple, un jeu vidéo qui veut copier une texture en mémoire vidéo), les choses sont tout autres. La solution la plus simple est de faire une copie du bloc incorrectement aligné vers un nouveau bloc correctement aligné. Mais les performances sont alors très faibles, surtout pour de grosses données. Une autre solution est de transférer les morceaux non-alignés sans DMA? et de copier le reste avec le DMA.

DMA et cohérence des caches[modifier | modifier le wikicode]

Le contrôleur DMA pose un problème sur les architectures avec une mémoire cache. Le problème est que le contrôleur DMA 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.

Cohérence des caches avec DMA.

Pour résoudre ce problème, la solution la plus simple interdit de charger dans le cache des données stockées dans les zones de la mémoire attribuées aux transferts DMA, 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. L'interdiction est assez facile à mettre en place, vu que le processeur est en charge de la mise en place du DMA. Mais sur les systèmes multi-coeurs ou multi-processeurs, les choses sont plus compliquées, comme on le verra dans quelques chapitres.

Une autre solution est d'invalider le contenu des caches lors d'un transfert DMA. Par invalider, on veut dire que les caches sont remis à zéro, leurs données sont effacées. Cela force le processeur à aller récupérer les données valides en mémoire RAM. L'invalidation des caches est cependant assez couteuse, comme nous le verrons dans le chapitre sur les caches. Elle est plus performante si les accès à la zone de mémoire sont rares, ce qui permet de profiter du cache 90 à 99% du temps, en perdant en performance dans les accès restants.

La technologie Data Direct I/O d'Intel permettait à un périphérique d'écrire directement dans le cache du processeur, sans passer par la mémoire. Si elle résout le problème de la cohérence des caches, son but premier était d'améliorer les performances des communications réseaux, lorsqu'une carte réseau veut écrire quelque chose en mémoire. Au lieu d'écrire le message réseau en mémoire, avant de le charger dans le cache, l'idée était de passer outre la RAM et d'écrire directement dans le cache. Cette technologie était activée par défaut sur les plateformes Intel Xeon processor E5 et Intel Xeon processor E7 v2.

Le 8237 et son usage dans les PC[modifier | modifier le wikicode]

Le 8237 d'Intel était un contrôleur DMA présents dans les premiers PC. Il a d'abord été utilisé avec les processeurs 8086, puis avec le 8088. Le processeur 8086 était un processeur 8 bits, mais qui avait des adresses de 16 bits. Le 8237 avait les mêmes propriétés : adressage sur 16 bits, copies octet par octet. Le processeur 8088 avait lui des adresses de 20 bits, ce qui était incompatible avec les capacités 16 bits du 8237. Cependant, cela n'a pas empéché d'utiliser le 8237 sur les premiers PC, mais avec quelques ruses.

L'usage du 8237 sur les premiers IBM PC[modifier | modifier le wikicode]

Pour utiliser un 8237 avec un adressage de 16 bits sur un bus d'adresse de 20 bits, il faut fournir les 4 bits manquants. Ils sont mémorisés dans un registre de 4 bits, placés dans un circuit 74LS670. Ce registre est configuré par le processeur, mais il n'est pas accessible au contrôleur DMA. Les architectures avec un bus de 24 bits utilisaient la même solution, sauf que le registre de 4 bits est devenu un registre de 8 bits. Cette solution ressemble pas mal à l'utilisation de la commutation de banque (bank switching), mais ce n'en est pas vu que le processeur n'est pas concerné et que seul le contrôleur DMA l'est. Le contrôleur DMA ne gère pas des banques proprement dit car il n'est pas un processeur, mais quelque chose d'équivalent que nous appellerons "pseudo-banque" dans ce qui suit. Les registres de 4/8 ajoutés pour choisir la pseudo-banque sont appelés des registres de pseudo-banque DMA.

Un défaut de cette solution est que le contrôleur DMA gère 4 pseudo-banques de 64 Kibioctets, au lieu d'un seul de 256 Kibioctets. S'il démarre une copie, celle-ci ne peut pas passer d'une pseudo-banque à un autre. Par exemple, si on lui demande de copier 12 Kibioctets, tout se passe bien si les 12 Kb sont tout entier dans une pseudo-banque. Mais si jamais les 3 premiers Kb sont dans une pseudo-banque, et le reste dans la suivante, alors le contrôleur DMA ne pourra pas faire la copie. Dans un cas pareil, le compteur d'adresse est remis à 0 une fois qu'il atteint la fin de la pseudo-banque, et la copie reprend au tout début de la pseudo-banque de départ. Ce défaut est resté sur les générations de processeurs suivantes, avant que les faibles performances du 8237 ne forcent à mettre celui-ci au rebut.

Le 8237 peut gérer 4 transferts DMA simultanés, 4 copies en même temps. On dit aussi qu'il dispose de 4 canaux DMA, qui sont numérotés de 0 à 3. La présence de plusieurs canaux DMA fait que les registres de pseudo-banque de 4/8 bits doivent être dupliqués : il doit y en avoir un par canal DMA. Le 8237 avait quelques restrictions sur ces canaux. Notamment, seuls les canaux 0 et 1 pouvaient faire des transferts mémoire-mémoire, les autres ne pouvant faire que des transferts mémoire-périphérique. Le canal 0 était utilisé pour le rafraichissement mémoire, plus que pour des transferts de données.

Les deux 8237 des bus ISA[modifier | modifier le wikicode]

Le bus ISA utilise des 8237 pour gérer les transferts DMA. Les registres de pseudo-banques sont élargis pour adresser 16 mébioctets, mais la limitation des pseudo-banques de 64 Kibioctets est encore présente. Les PC avec un bus ISA géraient 7 canaux DMA, qui étaient gérés par deux contrôleurs 8237 mis en cascade. La mise en cascade des deux 8237 se fait en réservant le canal 0 du 8237 esclave pour gérer le 8237 esclave. le canal 0 du maitre n'est plus systématiquement utilisé pour le rafraichissement mémoire, ce qui libère la possibilité de faire des transferts mémoire-mémoire. Voici l'utilisation normale des 7 canaux DMA :

Premier 8237 (maitre) :

  • Rafraichissement mémoire ;
  • Hardware utilisateur, typiquement une carte son ;
  • Lecteur de disquette ;
  • Disque dur, port parallèle, autres ;

Second 8237 (esclave) :

  • Utilisé pour mise en cascade ;
  • Disque dur (PS/2), hardware utilisateur ;
  • Hardware utilisateur ;
  • Hardware utilisateur.

Le bus ISA est un bus de données de 16 bits, alors que le 8237 est un composant 8 bits, ce qui est censé poser un problème. Mais le bus ISA utilisait des transferts spéciaux, où le contrôleur DMA n'était pas utilisé comme intermédiaire. En temps normal, le contrôleur DMA lit le mot mémoire à copier et le mémorise dans un registre interne, puis adresse l'adresse de destination et connecte ce registre au bus de données. Mais avec le bus ISA, le périphérique est connecté directement au bus mémoire et ne passe pas par le contrôleur DMA : la copie est directe, le contrôleur DMA s'occupe juste du bus d'adresse et n'accède pas au bus de données lors des transferts.

L'usage du DMA pour les transferts mémoire-mémoire[modifier | modifier le wikicode]

Le DMA peut être utilisé pour faire des copies dans la mémoire, avec des transferts mémoire-mémoire. Les performances sont correctes tant que le contrôleur DMA est assez rapide, sans compter que cela libère le processeur du transfert. Le processeur peut faire autre chose en attendant, tant qu'il n'accède pas à la mémoire, par exemple en manipulant des données dans ses registres ou dans son cache. La seule limitation est que les blocs à copier ne doivent pas se recouvrir, sans quoi il peut y avoir des problèmes, mais ce n'est pas forcément du ressort du contrôleur DMA.

Un exemple de ce type est celui du processeur CELL de la Playstation 2. Le processeur était en réalité un processeur multicœur, qui regroupait plusieurs processeurs sur une même puce. Il regroupait un processeur principal et plusieurs co-processeurs auxiliaires, le premier étant appelé PPE et les autres SPE. Le processeur principal était un processeur PowerPC, les autres étaient des processeurs en virgule flottante au jeu d'instruction beaucoup plus limité (ils ne géraient que des calculs en virgule flottante).

Le processeur principal avait accès à la mémoire RAM, mais les autres non. Les processeurs auxiliaires disposaient chacun d'un local store dédié, qui est une petite mémoire RAM qui remplace la mémoire cache, et ils ne pouvaient accéder qu'à ce local store, rien d'autre. Les transferts entre PPE et SPE se faisaient via des transferts DMA, qui faisaient des copies de la mémoire RAM principale vers les local stores. Chaque SPE incorporait son propre contrôleur DMA.

Schema du processeur Cell