Il était une fois, pour écrire un assembleur x86, par exemple, vous auriez des instructions indiquant "charger le registre EDX avec la valeur 5", "incrémenter le registre EDX", etc.
Avec les processeurs modernes qui ont 4 cœurs (ou même plus), au niveau du code machine, semble-t-il simplement qu'il y a 4 processeurs distincts (c'est-à-dire qu'il n'y a que 4 registres "EDX" distincts)? Si tel est le cas, lorsque vous dites "incrémenter le registre EDX", qu'est-ce qui détermine le registre EDX du processeur incrémenté? Existe-t-il maintenant un concept de «contexte CPU» ou de «thread» dans l'assembleur x86?
Comment fonctionne la communication / synchronisation entre les cœurs?
Si vous écriviez un système d'exploitation, quel mécanisme est exposé via le matériel pour vous permettre de planifier l'exécution sur différents cœurs? S'agit-il d'instructions privilégiées spéciales?
Si vous écriviez une machine virtuelle de compilation / bytecode d'optimisation pour un processeur multicœur, que devriez-vous savoir spécifiquement sur, disons, x86 pour lui faire générer du code qui s'exécute efficacement sur tous les cœurs?
Quelles modifications ont été apportées au code machine x86 pour prendre en charge la fonctionnalité multicœur?
Réponses:
Ce n'est pas une réponse directe à la question, mais c'est une réponse à une question qui apparaît dans les commentaires. Essentiellement, la question est de savoir quel support le matériel donne au fonctionnement multi-thread.
Nicholas Flynt avait raison , du moins en ce qui concerne x86. Dans un environnement multithread (Hyper-threading, multicœur ou multiprocesseur), le thread Bootstrap (généralement le thread 0 dans le noyau 0 dans le processeur 0) démarre la récupération du code à partir de l'adresse
0xfffffff0
. Tous les autres threads démarrent dans un état de veille spécial appelé Wait-for-SIPI . Dans le cadre de son initialisation, le thread principal envoie une interruption inter-processeur (IPI) spéciale sur l'APIC appelée SIPI (Startup IPI) à chaque thread se trouvant dans WFS. Le SIPI contient l'adresse à partir de laquelle ce thread doit commencer à récupérer le code.Ce mécanisme permet à chaque thread d'exécuter du code à partir d'une adresse différente. Tout ce qui est nécessaire est un support logiciel pour chaque thread pour configurer ses propres tables et files d'attente de messagerie. Le système d' exploitation utilise les faire la programmation multithread réelle.
En ce qui concerne l'assemblage réel, comme l'écrivait Nicholas, il n'y a pas de différence entre les assemblages pour une application à thread unique ou à threads multiples. Chaque thread logique a son propre ensemble de registres, écrivant ainsi:
mettra à jour uniquement
EDX
pour le thread en cours d'exécution . Il n'y a aucun moyen de modifierEDX
sur un autre processeur à l'aide d'une seule instruction d'assemblage. Vous avez besoin d'une sorte d'appel système pour demander au système d'exploitation de dire à un autre thread d'exécuter du code qui mettra à jour le sienEDX
.la source
Exemple de baremetal exécutable minimal Intel x86
Exemple de métal nu exécutable avec toutes les plaques chauffantes requises . Toutes les parties principales sont couvertes ci-dessous.
Testé sur Ubuntu 15.10 QEMU 2.3.0 et sur le véritable invité matériel Lenovo ThinkPad T400 .
Le Guide de programmation du système Intel Manual Volume 3 - 325384-056F septembre 2015 couvre SMP dans les chapitres 8, 9 et 10.
Tableau 8-1. "Broadcast INIT-SIPI-SIPI Sequence and Choice of Timeouts" contient un exemple qui fonctionne simplement:
Sur ce code:
La plupart des systèmes d'exploitation rendront la plupart de ces opérations impossibles depuis l'anneau 3 (programmes utilisateur).
Vous devez donc écrire votre propre noyau pour jouer librement avec lui: un programme Linux utilisateur ne fonctionnera pas.
Au début, un seul processeur s'exécute, appelé le processeur d'amorçage (BSP).
Il doit réveiller les autres (appelés Processeurs d'application (AP)) par le biais d'interruptions spéciales appelées Interruptions de processeur (IPI) .
Ces interruptions peuvent être effectuées en programmant le contrôleur d'interruption programmable avancé (APIC) via le registre de commande d'interruption (ICR)
Le format de l'ICR est documenté à: 10.6 "ÉMISSION D'INTERRUPTIONS D'INTERPROCESSEUR"
L'IPI se produit dès que nous écrivons à l'ICR.
ICR_LOW est défini à 8.4.4 "Exemple d'initialisation MP" comme:
La valeur magique
0FEE00300
est l'adresse mémoire de l'ICR, comme indiqué dans le tableau 10-1 "Carte d'adresse du registre APIC local"La méthode la plus simple possible est utilisée dans l'exemple: elle configure l'ICR pour envoyer des IPI de diffusion qui sont délivrés à tous les autres processeurs à l'exception du processeur actuel.
Mais il est également possible, et recommandé par certains , d'obtenir des informations sur les processeurs via des structures de données spéciales configurées par le BIOS comme les tables ACPI ou la table de configuration MP d'Intel et de ne réveiller que celles dont vous avez besoin une par une.
XX
en000C46XXH
code l'adresse de la première instruction que le processeur exécutera comme:N'oubliez pas que CS multiplie les adresses par
0x10
, donc l'adresse mémoire réelle de la première instruction est:Donc, si par exemple
XX == 1
, le processeur démarre à0x1000
.Nous devons ensuite nous assurer qu'il y a du code en mode réel 16 bits à exécuter à cet emplacement mémoire, par exemple avec:
L'utilisation d'un script de l'éditeur de liens est une autre possibilité.
Les boucles de retard sont une partie gênante pour se mettre au travail: il n'y a pas de moyen super simple de faire de telles nuits avec précision.
Les méthodes possibles incluent:
Connexe: Comment afficher un nombre à l'écran et dormir pendant une seconde avec l'assemblage DOS x86?
Je pense que le processeur initial doit être en mode protégé pour que cela fonctionne lorsque nous écrivons à l'adresse
0FEE00300H
qui est trop élevée pour 16 bits.Pour communiquer entre les processeurs, nous pouvons utiliser un verrou tournant sur le processus principal et modifier le verrou à partir du deuxième cœur.
Nous devons nous assurer que la réécriture de la mémoire est effectuée, par exemple via
wbinvd
.État partagé entre les processeurs
8.7.1 "État des processeurs logiques" dit:
Le partage de cache est discuté à:
Les hyperthreads Intel ont plus de cache et de partage de pipeline que les cœurs séparés: /superuser/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858
Noyau Linux 4.2
La principale action d'initialisation semble être à
arch/x86/kernel/smpboot.c
.Exemple de baremetal exécutable minimal ARM
Ici, je fournis un exemple exécutable ARMv8 aarch64 minimal pour QEMU:
GitHub en amont .
Assemblez et exécutez:
Dans cet exemple, nous plaçons le CPU 0 dans une boucle de verrou tournant, et il ne se termine que lorsque le CPU 1 libère le verrou tournant.
Après le verrou tournant, le CPU 0 effectue ensuite un appel de sortie semi - hôte qui fait quitter QEMU.
Si vous démarrez QEMU avec un seul processeur
-smp 1
, la simulation se bloque pour toujours sur le verrou tournant.Le CPU 1 est réveillé avec l'interface PSCI, plus de détails sur: ARM: Start / Wakeup / Bringup the other CPU cores / APs and pass execution start address?
La version en amont a également quelques ajustements pour la faire fonctionner sur gem5, vous pouvez donc également expérimenter les caractéristiques de performance.
Je ne l'ai pas testé sur du vrai matériel, donc je ne sais pas à quel point c'est portable. La bibliographie Raspberry Pi suivante pourrait être intéressante:
Ce document fournit des conseils sur l'utilisation des primitives de synchronisation ARM que vous pouvez ensuite utiliser pour faire des choses amusantes avec plusieurs cœurs: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf
Testé sur Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0.
Prochaines étapes pour une programmabilité plus pratique
Les exemples précédents réveillent le processeur secondaire et effectuent une synchronisation de base de la mémoire avec des instructions dédiées, ce qui est un bon début.
Mais pour rendre les systèmes multicœurs faciles à programmer, par exemple comme POSIX
pthreads
, vous devrez également aborder les sujets suivants plus impliqués:l'installation interrompt et exécute un minuteur qui décide périodiquement quel thread s'exécutera maintenant. C'est ce que l'on appelle le multithreading préemptif .
Ce système doit également enregistrer et restaurer les registres de threads au démarrage et à l'arrêt.
Il est également possible d'avoir des systèmes multitâches non préemptifs, mais ceux-ci peuvent vous obliger à modifier votre code afin que chaque thread cède (par exemple avec une
pthread_yield
implémentation), et il devient plus difficile d'équilibrer les charges de travail.Voici quelques exemples simplifiés de minuterie en métal nu:
gérer les conflits de mémoire. Notamment, chaque thread aura besoin d'une pile unique si vous souhaitez coder en C ou dans d'autres langages de haut niveau.
Vous pouvez simplement limiter les threads pour avoir une taille de pile maximale fixe, mais la meilleure façon de gérer cela est avec la pagination qui permet des piles de "taille illimitée" efficaces.
Voici un exemple de baremetal naïf aarch64 qui exploserait si la pile devenait trop profonde
Ce sont de bonnes raisons d'utiliser le noyau Linux ou un autre système d'exploitation :-)
Primitives de synchronisation de la mémoire Userland
Bien que le démarrage / arrêt / gestion des threads dépasse généralement la portée de l'espace utilisateur, vous pouvez cependant utiliser les instructions d'assemblage des threads utilisateur pour synchroniser les accès à la mémoire sans appels système potentiellement plus coûteux.
Vous devriez bien sûr préférer utiliser des bibliothèques qui enveloppent de manière portative ces primitives de bas niveau. Le standard C ++ lui-même a fait de grandes avancées sur les en
<mutex>
-<atomic>
têtes et, et en particulier avecstd::memory_order
. Je ne sais pas si cela couvre toutes les sémantiques de mémoire possibles, mais c'est possible.La sémantique plus subtile est particulièrement pertinente dans le contexte des structures de données sans verrouillage , qui peuvent offrir des avantages en termes de performances dans certains cas. Pour les implémenter, vous devrez probablement en apprendre un peu plus sur les différents types de barrières mémoire: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/
Boost, par exemple, propose des implémentations de conteneurs sans verrouillage sur: https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html
Ces instructions utilisateur semblent également être utilisées pour implémenter l'
futex
appel système Linux , qui est l'une des principales primitives de synchronisation sous Linux.man futex
4.15 se lit comme suit:Le nom du syscall lui-même signifie "Fast Userspace XXX".
Voici un exemple C ++ x86_64 / aarch64 minimal inutile avec un assemblage en ligne qui illustre l'utilisation de base de ces instructions principalement pour le plaisir:
main.cpp
GitHub en amont .
Sortie possible:
De cela, nous voyons que l'
LDADD
instruction de préfixe x86 LOCK / aarch64 a rendu l'addition atomique: sans elle, nous avons des conditions de concurrence sur de nombreux ajouts, et le nombre total à la fin est inférieur au 20000 synchronisé.Voir également:
Testé sous Ubuntu 19.04 amd64 et avec le mode utilisateur QEMU aarch64.
la source
#include
(prend cela comme un commentaire), NASM, FASM, YASM ne connaissent pas la syntaxe AT&T donc ça ne peut pas être eux ... alors qu'est-ce que c'est?gcc
,#include
provient du préprocesseur C. Utilisez leMakefile
fourni comme expliqué dans la section de démarrage: github.com/cirosantilli/x86-bare-metal-examples/blob/… Si cela ne fonctionne pas, ouvrez un problème GitHub.Si je comprends bien, chaque "cœur" est un processeur complet, avec son propre ensemble de registres. Fondamentalement, le BIOS vous démarre avec un cœur en cours d'exécution, puis le système d'exploitation peut "démarrer" d'autres cœurs en les initialisant et en les pointant sur le code à exécuter, etc.
La synchronisation est effectuée par le système d'exploitation. Généralement, chaque processeur exécute un processus différent pour le système d'exploitation, de sorte que la fonctionnalité multithread du système d'exploitation est chargée de décider quel processus doit toucher quelle mémoire et quoi faire en cas de collision de mémoire.
la source
La FAQ non officielle de SMP
Il était une fois, pour écrire un assembleur x86, par exemple, vous auriez des instructions indiquant "charger le registre EDX avec la valeur 5", "incrémenter le registre EDX", etc. Avec les processeurs modernes qui ont 4 cœurs (ou même plus) , au niveau du code machine, semble-t-il simplement qu'il y a 4 CPU séparés (c'est-à-dire qu'il n'y a que 4 registres "EDX" distincts)?
Exactement. Il existe 4 jeux de registres, dont 4 pointeurs d'instructions distincts.
Si tel est le cas, lorsque vous dites "incrémenter le registre EDX", qu'est-ce qui détermine le registre EDX du processeur incrémenté?
Le CPU qui a exécuté cette instruction, naturellement. Considérez-le comme 4 microprocesseurs entièrement différents qui partagent simplement la même mémoire.
Existe-t-il maintenant un concept de «contexte CPU» ou de «thread» dans l'assembleur x86?
Non. L'assembleur traduit simplement les instructions comme il l'a toujours fait. Aucun changement là-bas.
Comment fonctionne la communication / synchronisation entre les cœurs?
Puisqu'ils partagent la même mémoire, c'est principalement une question de logique de programme. Bien qu'il y ait maintenant une interruption inter-processeur mécanisme d' , il n'est pas nécessaire et n'était pas présent à l'origine dans les premiers systèmes x86 à double processeur.
Si vous écriviez un système d'exploitation, quel mécanisme est exposé via le matériel pour vous permettre de planifier l'exécution sur différents cœurs?
L'ordonnanceur ne change pas, sauf qu'il porte un peu plus attention aux sections critiques et aux types de verrous utilisés. Avant SMP, le code du noyau finissait par appeler le planificateur, qui regardait la file d'attente d'exécution et choisissait un processus à exécuter en tant que thread suivant. (Les processus vers le noyau ressemblent beaucoup à des threads.) Le noyau SMP exécute exactement le même code, un thread à la fois, c'est juste que le verrouillage des sections critiques doit maintenant être protégé par SMP pour être sûr que deux cœurs ne peuvent pas accidentellement choisir le même PID.
S'agit-il d'instructions privilégiées spéciales?
Non. Les cœurs fonctionnent tous dans la même mémoire avec les mêmes anciennes instructions.
Si vous écriviez une machine virtuelle de compilation / bytecode d'optimisation pour un processeur multicœur, que devriez-vous savoir spécifiquement sur, disons, x86 pour lui faire générer du code qui s'exécute efficacement sur tous les cœurs?
Vous exécutez le même code que précédemment. C'est le noyau Unix ou Windows qui devait changer.
Vous pouvez résumer ma question comme suit: "Quelles modifications ont été apportées au code machine x86 pour prendre en charge la fonctionnalité multicœur?"
Rien n'était nécessaire. Les premiers systèmes SMP utilisaient exactement le même jeu d'instructions que les monoprocesseurs. Maintenant, il y a eu beaucoup d'évolution de l'architecture x86 et des millions de nouvelles instructions pour accélérer les choses, mais aucune n'était nécessaire pour SMP.
Pour plus d'informations, consultez le spécification Intel Multiprocessor .
Mise à jour: toutes les questions de suivi peuvent être répondues en acceptant complètement qu'un processeur multicœur à n voies est presque 1 exactement la même chose que n processeurs séparés qui partagent juste la même mémoire. 2 Une question importante n'a pas été posée: comment un programme est-il écrit pour s'exécuter sur plusieurs cœurs pour plus de performances? Et la réponse est: il est écrit en utilisant une bibliothèque de threads comme Pthreads. Certaines bibliothèques de threads utilisent des «threads verts» qui ne sont pas visibles par le système d'exploitation, et ceux-ci n'obtiendront pas de cœurs séparés, mais tant que la bibliothèque de threads utilise des fonctionnalités de thread du noyau, votre programme de thread sera automatiquement multicœur.
1. Pour des raisons de compatibilité ascendante, seul le premier noyau démarre lors de la réinitialisation, et quelques tâches de type pilote doivent être effectuées pour activer les autres.
2. Ils partagent également tous les périphériques, naturellement.
la source
En tant que personne qui écrit l'optimisation de machines virtuelles de compilation / bytecode, je peux peut-être vous aider ici.
Vous n'avez besoin de rien savoir spécifiquement sur x86 pour lui faire générer du code qui s'exécute efficacement sur tous les cœurs.
Cependant, vous devrez peut-être connaître cmpxchg et ses amis pour écrire du code qui s'exécute correctement sur tous les cœurs. La programmation multicœur nécessite l'utilisation de la synchronisation et de la communication entre les threads d'exécution.
Vous devrez peut-être connaître quelque chose sur x86 pour lui faire générer du code qui s'exécute efficacement sur x86 en général.
Il y a d'autres choses qu'il vous serait utile d'apprendre:
Vous devez en savoir plus sur les fonctionnalités du système d'exploitation (Linux ou Windows ou OSX) pour vous permettre d'exécuter plusieurs threads. Vous devriez en apprendre davantage sur les API de parallélisation telles que OpenMP et Threading Building Blocks, ou le prochain "Grand Central" d'OSX 10.6 "Snow Leopard".
Vous devez déterminer si votre compilateur doit être auto-parallélisant ou si l'auteur des applications compilées par votre compilateur doit ajouter une syntaxe spéciale ou des appels d'API dans son programme pour tirer parti des multiples cœurs.
la source
Chaque Core s'exécute à partir d'une zone mémoire différente. Votre système d'exploitation pointera un noyau vers votre programme et le noyau exécutera votre programme. Votre programme ne saura pas qu'il existe plusieurs cœurs ou sur quel cœur il s'exécute.
Il n'y a également aucune instruction supplémentaire disponible uniquement pour le système d'exploitation. Ces cœurs sont identiques aux puces à cœur unique. Chaque noyau exécute une partie du système d'exploitation qui gérera la communication avec les zones de mémoire communes utilisées pour l'échange d'informations afin de trouver la prochaine zone de mémoire à exécuter.
Il s'agit d'une simplification, mais elle vous donne une idée de base de la façon dont cela est fait. Plus d'informations sur les multicœurs et les multiprocesseurs sur Embedded.com contient de nombreuses informations sur ce sujet ... Ce sujet se complique très rapidement!
la source
Le code assembleur se traduira en code machine qui sera exécuté sur un cœur. Si vous voulez qu'il soit multithread, vous devrez utiliser plusieurs fois les primitives du système d'exploitation pour démarrer ce code sur différents processeurs ou différents morceaux de code sur différents cœurs - chaque cœur exécutera un thread distinct. Chaque thread ne verra qu'un seul noyau sur lequel il s'exécute actuellement.
la source
Cela ne se fait pas du tout dans les instructions de la machine; les cœurs prétendent être des processeurs distincts et n'ont pas de capacités spéciales pour communiquer entre eux. Il y a deux façons de communiquer:
ils partagent l'espace d'adressage physique. Le matériel gère la cohérence du cache, donc une CPU écrit dans une adresse mémoire qu'une autre lit.
ils partagent un APIC (contrôleur d'interruption programmable). Il s'agit de mémoire mappée dans l'espace d'adressage physique et peut être utilisée par un processeur pour contrôler les autres, les activer ou les désactiver, envoyer des interruptions, etc.
http://www.cheesecake.org/sac/smp.html est une bonne référence avec une URL idiote.
la source
La principale différence entre une application simple et une application multithread est que la première a une pile et que la seconde en a une pour chaque thread. Le code est généré quelque peu différemment car le compilateur supposera que les registres de données et de segments de pile (ds et ss) ne sont pas égaux. Cela signifie que l'indirection via les registres ebp et esp qui par défaut au registre ss ne sera pas également par défaut à ds (car ds! = Ss). Inversement, l'indirection via les autres registres qui par défaut est ds ne sera pas par défaut ss.
Les threads partagent tout le reste, y compris les zones de données et de code. Ils partagent également des routines lib alors assurez-vous qu'ils sont thread-safe. Une procédure qui trie une zone en RAM peut être multithread pour accélérer les choses. Les threads accèderont, compareront et ordonneront des données dans la même zone de mémoire physique et exécuteront le même code mais en utilisant différentes variables locales pour contrôler leur partie respective du tri. C'est bien sûr parce que les threads ont des piles différentes où les variables locales sont contenues. Ce type de programmation nécessite un réglage minutieux du code afin de réduire les collisions de données entre les cœurs (dans les caches et la RAM), ce qui entraîne à son tour un code plus rapide avec deux threads ou plus qu'avec un seul. Bien sûr, un code non réglé sera souvent plus rapide avec un processeur qu'avec deux ou plus. Déboguer est plus difficile car le point d'arrêt standard "int 3" ne sera pas applicable car vous voulez interrompre un thread spécifique et pas tous. Les points d'arrêt du registre de débogage ne résolvent pas non plus ce problème, sauf si vous pouvez les définir sur le processeur spécifique exécutant le thread spécifique que vous souhaitez interrompre.
D'autres codes multithread peuvent impliquer différents threads s'exécutant dans différentes parties du programme. Ce type de programmation ne nécessite pas le même type de réglage et est donc beaucoup plus facile à apprendre.
la source
Ce qui a été ajouté sur chaque architecture multiprocesseur par rapport aux variantes mono-processeur qui les ont précédées, ce sont les instructions de synchronisation entre les cœurs. De plus, vous avez des instructions pour gérer la cohérence du cache, vider les tampons et les opérations de bas niveau similaires qu'un système d'exploitation doit gérer. Dans le cas d'architectures multithreads simultanées comme IBM POWER6, IBM Cell, Sun Niagara et Intel "Hyperthreading", vous avez également tendance à voir de nouvelles instructions pour hiérarchiser les threads (comme définir des priorités et céder explicitement le processeur lorsqu'il n'y a rien à faire) .
Mais la sémantique de base sur un seul thread est la même, vous ajoutez simplement des fonctionnalités supplémentaires pour gérer la synchronisation et la communication avec d'autres cœurs.
la source