Fonctionnement d'un ordinateur/L'adressage des périphériques

Dans le chapitre précédent, nous avons vu que les périphériques, leurs registres d’interface et leurs contrôleurs, ont chacun une adresse bien précise. Nous avions vu comment le contrôleur de périphérique adresse les périphériques et comment les contrôleurs de périphériques eux-mêmes ont des adresses. Mais nous n'avons pas vu comment le processeur utilise ces adresses. Il nous reste à voir comment le mélange entre adresses mémoires et adresses de périphérique peut se faire, comment le processeur évite les confusions entre adresses de périphériques et adresses mémoire. Et pour cela, il y a plusieurs manières. La plus simple revient à séparer les adresses mémoire et les adresses périphériques, qui ne sont pas transmises sur les mêmes bus. L'autre méthode revient à utiliser un seul ensemble d'adresse, certaines étant allouées à la mémoire, d'autres aux périphériques. Les deux techniques portent des noms assez clairs : l'espace d'adressage séparé pour la première, l'espace d'adressage unifié pour la seconde. Voyons dans le détail ces deux techniques.

L'espace d’adressage séparé modifier

Avec la première technique, mémoire et entrées-sorties sont adressées séparément, comme illustré dans le schéma ci-dessous. La mémoire et les entrées-sorties ont chacune un ensemble d'adresse, qui commence à 0 et va jusqu’à une adresse maximale. On dit que la mémoire et les entrées-sorties ont chacune leur propre espace d'adressage.

 
Espaces d'adressages séparés entre mémoire et périphérique.

Avec cette technique, le processeur doit avoir des instructions séparées pour gérer les périphériques et adresser la mémoire. Il a des instructions de lecture/écriture pour lire/écrire en mémoire, et d'autres pour lire/écrire les registres d’interfaçage. L'existence de ces instructions séparées permet de positionner le bit IO correctement et de faire la différence entre mémoire et périphérique. Sans cela, le processeur ne saurait pas si une adresse est destinée à un périphérique ou à la mémoire et ne pourrait pas déterminer le bit IO associé.

L'implémentation matérielle de l'espace d'adressage séparé modifier

Avec un espace d'adressage séparé, on s'attend à avoir deux bus séparés : un pour la communication avec les périphériques et un autre pour la mémoire. Cela arrive et c'est un cas assez fréquent. Il faut dire que cette méthode est intuitive et permet de bien séparer les espaces d'adressage? Si la même adresse est à interpréter différemment selon qu'on l'envoie à un périphérique ou une mémoire, cela va bien avec le fait d'avoir deux bus d'adresse séparés. Outre son aspect intuitif, cette méthode est simple à implémenter, surtout au niveau du processeur. un autre avantage est qu'elle permet, en théorie, d'effectuer des accès à la mémoire pendant qu'on accède à un périphérique. Je dis en théorie, car le processeur doit être conçu pour, ce qui n'est pas gagné. Le défaut principal est lié à l'usage de deux bus : cela double le nombre de fils nécessaires, sans compter que le processeur doit avoir deux plus de broches qu'avec un bus unique.

 
Espace d'adressage séparé, implémentation avec deux bus séparés

Mais il est possible de mutualiser le bus d'adresse et de données. Par contre, autant bus d'adresse et de données sont partagés, autant on a bien deux bus de commandes, un pour le périphérique et un pour la mémoire.

 
Espace d'adressage séparé.

Si on mutualise le bus d'adresse, on doit indiquer la destination de l'adresse, périphérique ou mémoire. Une même adresse peut donc adresser soit une entrée-sortie, soit une case mémoire. La seule solution est d'utiliser un mécanisme de décodage d'adresse quelconque. La solution la plus simple et la plus utilisée consiste faire la différence à partir du bit de poids fort de l'adresse. Le bit de poids fort de l'adresse, appelé le bit I/O, vaut 0 pour une adresse mémoire, et 1 pour une adresse de périphérique. L'adresse envoyée sur le bus est formée en récupérant l'adresse à lire/écrire et en positionnant le bit I/O à sa bonne valeur. Tout cela est réalisé par l'instruction adéquate : une instruction d'accès mémoire positionnera ce bit à 0, alors qu'une instruction d'accès aux périphériques le positionnera à 1. Un défaut de cette solution est qu'elle impose d'avoir deux espaces d'adressage de même taille, un pour la/les mémoires, un autre pour les périphériques. Pas question d'avoir un espace d'adressage plus petit pour les périphériques, alors que ce serait possible avec deux bus séparés.

 
Bit IO.

L'espace d'adressage des périphériques contourne le cache modifier

Sur les processeurs disposant de mémoire cache, un problème dit de cohérence des caches peut survenir. Pour rappel, le cache est une petite mémoire censée accélérer les accès à la mémoire RAM, dans laquelle on stocke des copies des données en RAM. Un périphérique peut à tout instant modifier son état ou sa mémoire interne, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache. Si les accès aux périphériques passaient par l'intermédiaire du cache, on aurait droit à des problèmes. Il arriverait qu'un périphérique modifie une donnée en RAM, tandis que le cache continuerait de mémoriser une copie périmée de la donnée. Pour éviter tout problème, l'espace d'adressage des périphériques n'est pas caché : aucun accès dans cet espace d'adressage ne passe par le cache. La mémoire cache n'est utilisée que pour l'espace d'adressage des mémoires, rien d'autre.

Les entrées-sorties mappées en mémoire modifier

La dernière technique que nous allons voir s'appelle l'espace d'adressage unifie, ou encore les entrées-sorties mappées en mémoire. Avec cette technique, certaines adresses mémoires sont redirigées automatiquement vers les périphériques. Le périphérique se retrouve inclus dans l'ensemble des adresses utilisées pour manipuler la mémoire : on dit qu'il est mappé en mémoire.

 
IO mappées en mémoire

Le cas des périphériques avec une RAM intégrée modifier

Certains périphériques intègrent une RAM dans leurs circuits : cartes graphiques, cartes sons, etc. Notons que certains de ces périphériques intègrent une RAM et un processeur pour diverses raisons, notamment les cartes son et les cartes graphiques. Ce processeur intégré exécute des programmes dédiés, souvent fournis par les pilotes du périphérique ou le système d'exploitation, parfois par des applications spécialisées (les shaders des jeux vidéos en sont un bon exemple). Ces programmes sont recopiés dans la mémoire du périphérique, par le processeur principal ou via un contrôleur DMA. Cela peut paraitre bizarre, mais sachez que c'est exactement ce qui se passe pour votre carte graphique si celle-ci a moins de vingt ans. Leur mémoire est directement accessible directement par le processeur en détournant suffisamment d'adresses mémoires normales : elle est ainsi en partie (voire totalement) partagée entre le processeur principal de l'ordinateur et le périphérique (et plus globalement avec tout périphérique pouvant adresser la mémoire).

Évidemment, les adresses utilisées pour les périphériques ne sont plus disponibles pour la mémoire RAM. C'est ce qui causait autrefois un problème assez connu sur les ordinateurs 32 bits. Certaines personnes installaient 4 gigaoctets de mémoire sur leur ordinateur 32 bits et se retrouvaient avec « seulement » 3,5 à 3,8 gigaoctets de mémoire, les périphériques prenant le reste. Ce « bug » apparaissait sur les processeurs x86 32 bits, avec un système d'exploitation 32 bits. On remarque ainsi le défaut inhérent à cette technique : on ne peut plus adresser autant de mémoire qu'avant. Et mine de rien, quand on a une carte graphique avec 512 mégaoctets de mémoire intégrée, une carte son, une carte réseau PCI, des ports USB, un port parallèle, un port série, des bus PCI Express ou AGP, et un BIOS à stocker dans une EEPROM/Flash, ça part assez vite.

Notons qu'il est possible que toute la RAM du périphérique soit mappée en RAM, mais qu'il arrive souvent que seule une partie le soit. Un exemple classique est celui des cartes graphiques. Seule une portion de la mémoire vidéo (celle de la carte 3D) est mappée en mémoire RAM. Le processeur a donc accès à une partie de la mémoire vidéo, dans laquelle il peut lire ou écrire comme bon lui semble. Le reste de la mémoire vidéo est invisible du point de vue du processeur, mais manipulable par le GPU à sa guise. Il est possible pour le CPU de copier des données dans la portion invisible de la mémoire vidéo, mais cela se fait de manière indirecte en passant par le GPU d'abord. Il faut typiquement envoyer une commande spéciale au GPU, pour lui dire de charger une texture en mémoire vidéo, par exemple. Le GPU effectue alors une copie de la mémoire système vers la mémoire vidéo, en utilisant un contrôleur DMA intégré au GPU. Pour résumer, tout se passe comme si la mémoire partagée entre CPU et GPU était coupée en deux : une portion sur le GPU et une autre dans la RAM système.

 
Interaction du GPU avec la mémoire vidéo et la RAM système sur une carte graphique dédiée

De nos jours, la portion invisible de la mémoire vidéo a disparue. Pour accéder à un périphérique PCI-Express, il faut configurer des registres spécialisés, appelés les Base Address Registers (BARs). La configuration des registres précise quelle portion de mémoire vidéo est adressable par le processeur, quelle est sa taille, sa position en mémoire vidéo, etc. Avant 2008, les BAR permettaient d’accéder à seulement 256 mégaoctets, pas plus. La gestion de la mémoire vidéo était alors difficile. Les échanges entre portion visible et invisible de la mémoire vidéo étaient complexes, demandaient d’exécuter des commandes spécifiques au GPU et autres. Après 2008, la spécification du PCI-Express ajouta un support de la technologie resizable bar, qui permet au processeur d’accéder directement à plus de 256 mégaoctets de mémoire vidéo, voire à la totalité de la mémoire vidéo. De nombreux fabricants de cartes graphiques commencent à incorporer cette technologie, qui demande quelques changements au niveau du système d'exploitation, des pilotes de périphériques et du matériel.

L'implémentation matérielle des entrées-sorties mappées en mémoire modifier

La mise en œuvre des entrées-sorties mappées en mémoire peut se faire de plusieurs manières différentes. Plus haut, nous avons vu que l'usage d'un espace d'adressage séparé se marie assez bien avec l'existence de deux bus distincts, un pour la mémoire, un pour les périphériques. On s'attend donc à ce que ce soit l'inverse avec un espace d'adressage unique. Après tout, qui dit un seul espace d'adressage dit un seul bus d'adresse, donc un seul bus. Mais dans les faits, les choses sont plus compliquées. Si l'usage de deux espaces d'adressage séparés pouvait s'implémenter avec un bus partagé couplé à un bit IO, l'inverse est aussi possible avec les entrées-sorties mappées en mémoire. On peut très bien les implémenter soit avec un bus unique partagé entre mémoire et périphériques, soit avec deux bus séparés. Voyons voir ce qu'il en est.

L'usage d'un bus partagé entre RAM et I/O modifier

Dans son implémentation la plus simple, les entrées-sorties mappées en mémoire utilisent un bus unique sur lequel on connecte la RAM et les contrôleurs de périphériques. L'avantage est qu'il n'y a plus besoin de dupliquer les bus, ce qui économise beaucoup de fils. De plus, le bit IO disparait et on n'a pas besoin d'instructions différentes pour accéder aux périphériques et à la mémoire. Tout peut être fait par une seule instruction, qui n'a pas besoin de positionner un quelconque bit IO qui n'existe plus. Le processeur possède donc un nombre plus limité d'instructions machines, et est donc plus simple à fabriquer. Par contre, impossible d'accéder à la fois à la mémoire et à un contrôleur d'entrées-sorties : si le bus est utilisé par un périphérique, le processeur ne peut pas accéder à la mémoire et doit attendre que le périphérique ait fini sa transaction (et vice-versa).

 
Bus unique avec entrées mappées en mémoire.
 
Exemple détaillé.

Comme dit plus haut, une partie des adresses pointe alors vers un périphérique, d'autres vers la RAM ou la ROM. La redirection vers le bon destinataire est faite par décodage partiel d'adresse. Le circuit de décodage partiel d'adresse va ainsi placer le bit CS de la mémoire à 1 pour les adresses invalidées, l’empêchant de répondre à ces adresses.

 
Décodage d'adresse avec entrées-sorties mappées en mémoire.

L'usage d'un bus séparé pour les I/O et la mémoire modifier

Il est cependant possible d'utiliser des entrées-sorties mappées en mémoire sans utiliser un bus unique. On peut adapter cette technique sur des ordinateurs où le bus mémoire est séparé du bus pour les périphériques. Il existe plusieurs manières d'implémenter le tout. La première, de loin la plus simple, consiste à accéder à la RAM d'abord, puis aux périphériques si elle ne répond pas. Une tentative d'accès en RAM fonctionnera du premier coup si l'adresse en question est attribuée à la RAM. Mais si l'adresse est associée à un périphérique, la RAM ne répondra pas et on doit retenter l'accès sur le bus pour les périphériques. L'implémentation est cependant compliquée, sans compter que les performances sont alors réduites, du fait des deux tentatives consécutives.

Les autres solutions font communiquer les deux bus pour que la RAM ou les périphériques détectent précocement les accès qui leur sont dédiés. La première solution de ce type consiste à ajouter un dispositif qui transmet les accès du bus mémoire vers le bus des périphériques. Mais le bus pour les périphériques est souvent moins rapide que le bus mémoire et l'adaptation des vitesses pose des problèmes.

 
IO mappées en mémoire avec séparation des bus

Une autre solution est d'intercaler, entre le processeur et les deux bus, un circuit répartiteur. Il récupère tous les accès mémoire et distribue ceux-ci soit sur le bus mémoire, soit sur le bus des périphériques. C'était ce qui était fait du temps où les chipsets des cartes mères existaient encore, à l'époque des premiers Pentium. A l'époque, la puce de gestion du bus PCI faisait office de répartiteur. Elle mémorisait des plages mémoires entières, certaines étant attribuées à la RAM, les autres aux périphériques mappés en mémoire. Elles utilisaient ces plages pour faire la répartition. De nos jours, le répartiteur est généralement intégré au processeur, avec l'intégration du contrôleur mémoire dans le CPU.

 
IO mappées en mémoire avec séparation des bus, usage d'un répartiteur

La cohérence des caches avec des entrées mappées en mémoire modifier

Un défaut de cette méthode apparait sur les processeurs disposant de mémoire cache. Le problème est similaire à celui rencontré pour l'espace d'adressage séparé, mais la solution est différente. Avec des entrées-sorties mappées en mémoire, rien ne ressemble plus à une adresse mémoire qu'une autre adresse mémoire. Le cache ne sait pas si l'adresse correspond à un périphérique ou non, et il est censé mettre en cache son contenu automatiquement. Mais le problème est que les adresses liées aux périphériques, qui correspondent à des registres ou à la mémoire des périphériques, peuvent être modifiés sans que le cache soit mis au courant. Le périphérique peut à tout instant modifier son état ou sa mémoire interne, ce qui se répercute automatiquement dans l'espace d'adressage, mais rien de tout cela n'est transmis au cache.

La solution est que les accès aux périphériques ne doivent pas passer par l’intermédiaire du cache, si on veut qu'ils marchent comme ils le doivent. Cela demande d'adapter le cache et le matériel pour que accès aux périphériques mappés en mémoire contournent le cache. Des adresses, voire des zones entières de la mémoire, sont marquées comme étant non-cachables. 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.