Alice rédige un compte-rendu pour un projet informatique. Elle à «ouvert» un logiciel de traitement de texte pour écrire le rapport.
Son navigateur web est aussi ouvert avec divers onglets, l'un pointant vers Wikipedia, l'autre vers un moteur de recherche et un troisième vers un site de réseau social dont elle se sert pour
partager son humeur avec ses camarades.
Elle utilise un logiciel de dessin afin d'ajouter des illustrations à son compte-rendu.
Son projet étant en Python, elle dispose aussi d'une fenêtre avec l'environnement Idle dans lequel elle exécute son programme afin d'en vérifier les résultats.
Enfin, pour ne pas être perturbée, elle a mis des écouteurs sur ses oreilles et écoute de la musique grâce au lecteur de musique de son ordinateur.
Tous ces programmes s'exécutent en même temps.
Pourtant, si on se souvient de la façon dont sont construits les ordinateurs, ils ne disposent que d'un nombre limité de processeurs.
Or, comme on le sait, un programme n'est qu'une suite d'instructions en langage machine, ces dernières étant exécutées une à une par le processeur.
Comment le processeur peut-il donc exécuter en même temps les instructions du programme de traitement de texte et celles du lecteur de musique?
Cette exécution concurrente de programmes est l'une des fonctionnalités de base offertes par les systèmes d'exploitation modernes.
On parle alors de systèmes d'exploitation multitâches.
Nous rappelons d'abord brièvement comment un programme est exécuté par le système d'exploitation, puis nous introduisons le principe de fonctionnement de l'ordonnanceur de processus, la partie du système d'exploitation permettant l'exécution concurrente des Programmes.
","title":"Gestion des processus et des ressources","tagtitle":"h1"},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Rappel sur l'exécution d'un programme
Un exécutable est un fichier (par exemple stocké sur le disque dur) contenant une suite d'instructions en langage machine.
MOVR0,#10\nMOVR1,#20\nADDR2,R1,R0\nADDR2,R2,#5
C'est donc une suite d'octets que le processeur est capable de décoder et exécuter.
Concrètement, lorsque l'on exécute un programme (par exemple en cliquant sur l'icône du fichier exécutable ou en renseignant son chemin dans un terminal), le système d'exploitation effectue les actions suivantes:
le fichier contenant le programme (exécutable) est copié dans la mémoire RAM, à une certaine adresse a;
le système d'exploitation écrit la valeur a dans le registre IP (instruction pointer).
Au prochain cycle d'horloge du processeur, ce dernier va alors lire l'instruction se trouvant à l'adresse a et l'exécuter.
Une fois cela fait, il exécutera ensuite la seconde instruction et ainsi de suite.
On rappelle que l'exécution d'une instruction se décompose elle-même en plusieurs sous-étapes effectuées au sein du processeur:
le chargement (récupérer l'instruction en mémoire), le décodage (déterminer dans la suite d'octets chargés quelle instruction ils encodent) et l'exécution proprement dite.
Même si elle est correcte, la description que nous avons faite de l'exécution d'un programme est incomplète.
En effet, si rien de plus n'est fait, alors la seule chose que l'on peut attendre, c'est que le programme en question s'exécute jusqu'à sa dernière instruction, puis rende la main au système d'exploitation.
Impossible alors de l'interrompre!
Impossible aussi de pouvoir exécuter deux programmes en même temps.
Pour pallier ce problème, les systèmes d'exploitation utilisent une fonctionnalité importante des processeurs modernes:
la notion d'interruption.
Une interruption est un signal envoyé au processeur lorsqu'un événement se produit.
Il existe plusieurs types d'interruptions.
Certaines sont générées par le matériel (par exemple, un disque dur signale qu'il à fini décrire des octets, une carte réseau signale que des paquets de données arrivent, etc.)
Lorsque le processeur reçoit une interruption, il interrompt son exécution à la fin de l'instruction courante et exécute un programme se trouvant à une adresse prédéfinie.
Ce programme reçoit en argument une copie des valeurs courante des registres, ainsi qu'un code numérique lui permettant de savoir à quel type d'interruption il fait face.
Ce programme spécial s'appelle le gestionnaire d'interruption.
Il est installé à une certaine adresse mémoire par le système d'exploitation, très tôt après le démarrage de la machine.
Parmi les interruptions matérielles, on retrouve les interruptions d'horloge.
Le processeur génère de lui-même une interruption matérielle à intervalles de temps fixe.
Historiquement, sur les processeurs Intel, cette interruption était levée toutes les 55 ms (environ 18 fois par seconde).
Le gestionnaire d'interruption était donc appelé au moins toutes les 55 ms.
De nos jours, les processeurs disposent d'horloges de haute précision capables d'émettre des interruptions avec une fréquence de 10 Mhz, donc toutes les 100ns.
Ces interruptions d'horloges, alliées au gestionnaire d'interruption, sont les pièces essentielles permettant d'exécuter des programmes de façon concurrente.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Vocabulaire
Certains termes sont particulièrement importants pour la suite et doivent être définis précisément:
Exécutable : un fichier binaire contenant des instructions machines directement exécutables par le processeur de la machine.
Processus : «programme en cours d'exécution». Un processus est le phénomène dynamique qui correspond à l'exécution d'un programme particulier. Le système d'exploitation identifie généralement les processus par un numéro unique.
Un processus est décrit par:
l'ensemble de la mémoire allouée par le système pour l'exécution de ce programme (ce qui inclut le code exécutable copié en mémoire et toutes les données manipulées par le programme, sur la pile ou dans le tas;
l'ensemble des ressources utilisées par le programme (fichiers ouverts, connexions réseaux, etc.);
les valeurs stockées dans tous les registres du processeur.
Thread ou tâche: exécution d'une suite d'instructions démarrée par un processus.
Deux processus sont l'exécution de deux programmes (par exemple, un traitement de texte et un navigateur web).
Deux threads sont l'exécution concurrente de deux suites d'instructions d'un même
processus.
Par exemple, pour un navigateur web, il peut y avoir un thread dont le rôle est de dessiner la page web dans une fenêtre et un autre thread dont le rôle est de télécharger un fichier sur lequel l'utilisateur à cliqué.
La différence fondamentale entre processus et thread est que les processus ne partagent pas leur mémoire, alors que
les threads, issus d'un même processus, peuvent accéder aux variables globales du programme et occupent le même espace en mémoire.
Exécution concurrente: deux processus ou tâches s'exécutent de manière concurrente si les intervalles de temps entre le début et la fin de leur exécution ont une partie commune.
Exécution parallèle: deux processus où tâches s'exécutent en parallèle s'ils s'exécutent au même instant. Pour que deux processus s'exécutent en parallèle, il faut donc plusieurs processeurs sur la machine.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Ordonnanceur du système d'exploitation
Comme nous l'avons vu, le système d'exploitation peut configurer une horloge et le gestionnaire d'interruption pour «reprendre la main», c'est-à-dire exécuter du code qui lui est propre, à intervalles réguliers.
Lorsqu'il s'exécute, il peut, entre autres choses, décider à quel programme en cours d'exécution il va rendre la main.
Dans le scénario d'utilisation donné en
introduction, l'ordonnanceur fonctionnera donc de la façon suivante:
Le programme «traitement de texte» est en cours d'exécution (l'utilisateur saisit du texte qui est affiché à l'écran, sauvegarde dans un fichier, etc.).
Une interruption d'horloge se déclenche.
Le code du gestionnaire d'interruption est appelé. Il reçoit en argument les valeurs qu'ont tous les registres avant le déclenchement de l'interruption (donc tout l'état «interne» du traitement de texte).
Le gestionnaire d'interruption sauvegarde ces registres à un endroit particulier de la mémoire.
Il choisit dans la liste des processus un autre processus, par exemple celui correspondant au navigateur web.
Il restaure les valeurs de tous les registres du processeur qu'il a sauvegardées la dernière fois qu'il a interrompu le navigateur web. Parmi ces registres sauvegardés, il y a notamment IP, l'adresse de la prochaine instruction à exécuter. Elle pointait alors vers une instruction du programme «navigateur web».
Le gestionnaire d'interruption rend la main. La prochaine instruction à exécuter est celle du processus navigateur web, qui reprend son exécution, jusqu'à ce qu'il soit mis en pause par la prochaine interruption d'horloge.
Le fait que l'ordonnanceur interrompe un processus et sauve son état s'appelle une commutation de conteste.
Afin de pouvoir choisir parmi tous les
processus lequel exécuter lors de la prochaine interruption, le système d'exploitation conserve pour chaque processus une structure de dounées nommée
PCB (pour l'anglais Process Control Bloc ou bloc de contrôle du processus).
Le PCB est simplement une zone mémoire dans laquelle sont stockées diverses informations sur le processus.
Nom
Description
PID
Process ID, l'identifiant numérique du processus
État
l'état dans lequel se trouve le processus
Registres
la valeur des registres lors de sa dernière interruption
Mémoire
zone mémoire (plage 'adresses) allouée par le processus lors de son exécution
Ressources
liste des fichiers ouverts, connexions réseaux en cours d'utilisation, etc.
Pour choisir parmi les processus celui auquel il va donner la main, l'ordonnanceur conserve les PCB dans une structure de donnée, par exemple une file (telles que celles décrites sut les Piles et les Files).
Le premier processus dans la file reprend son exécution.
Lors de la prochaine interruption, il est mis en bout de file.
Cette stratégie simple permet d'éviter qu'un processus monopolise tout le temps de calcul.
En effet, avant qu'un processus mis en bout de file puisse s'exécuter de nouveau, il aura laissé une chance à tous les autres d'avoir la main.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
États des processus
Les interruptions d'horloges ne sont pas les seuls événements permettant d'interrompre les processus.
Considérons le petit programme Python ci-dessous:
texte = input(\"Saisir une phrase : \")
print (\"Votre phrase en majuscules : \", texte.upper())
Ce programme utilise la fonction input pour demander à l'utilisateur de saisir une phrase.
Tant que l'utilisateur ne saisit rien, le programme ne peut pas avancer à l'instruction suivante.
Le système d'exploitation (qui gère aussi
le matériel et en particulier l'accès au clavier) peut suspendre le processus et le mettre en attente.
De plus, il sait aussi qu'il est inutile de réveiller ce processus tant que l'utilisateur n'a pas interagi avec le clavier.
Une telle interaction avec le clavier est signalée au processeur par une interruption, similaire aux interruptions d'horloges.
De manière générale, lorsque des périphériques (carte réseau, disque dur, souris, clavier, etc.) veulent signaler
au processeur qu'un événement est survenu, ils le feront au moyen d'une interruption.
Le système d'exploitation aura alors l'occasion «de réveiller» l'un des processus parmi ceux qui attendaient un tel événement.
On voit donc que les processus peuvent être dans différents états.
La plupart des systèmes d'exploitation utilisent principalement les états suivants:
Nouveau : état d'un processus en cours de création. Le système d'exploitation vient de copier l'exécutable en mémoire et d'initialiser le PCB.
Prêt: le processus peut être le prochain à s'exécuter, Il est dans la file des processus qui «attendent» leur tour et peuvent être choisis par l'ordonnanceur.
En exécution: le processus est en train de s'exécuter.
En attente: le processus est interrompu et en attente d'un événement externe (entrée/sortie, allocation mémoire, etc.).
Terminé: le processus s'est terminé, le système d'exploitation est en train de désallouer les ressources que le processus utilisait,
Les états nouveau et terminé sont éphémères.
En temps normal, l'état d'un processus variera entre prêt, en attente et en exécution.
La figure ci-desssous résume le cycle de vie d'un processus.
Figure 1 — Cycle de vie d'un processus.
On peut noter que quel que soit l'état dans lequel se trouve un processus, il peut se terminer de façon anormale.
S'il est en exécution, cela peut être dû à une erreur provoquée par le programme
(lecture d'une adresse mémoire invalide, division par 0, etc.).
Si le processus est en attente d'une entrée/sortie, il est possible qu'il se produise une erreur matérielle (disque dur défectueux par exemple).
Enfin, si le processus est dans l'état prêt, il peut quand même se terminer de façon anormale si l'utilisateur qui à lancé le programme ou l'administrateur système décide de l'interrompre manuellement.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Dans les systèmes POSIX (Unix, Linux, Mac OSX, Android,...) la commande ps
(pour l'anglais process status ou état des processus) permet d'obtenir des informations sur les processus en cours d'exécution.
$ ps -a -u -x
Connecter vous au vps 217.182.207.90 à l'aide du logiciel Putty sous windows ou avec le terminal sur Linux et Mac à l'aide de la commande :
ssh nomUtilisateur@217.182.207.90
Attention, il faut demander au professeur le nom d'utilisateur et le mot de passe.
Une fois connecté au serveur vps, lancer la commande ps -a -u -x et copier le résultat ci-dessous.
","title":"Commandes Unix de gestion des processus"},{"edit":"
Mettre ici le résultat de la commande ps -a -u -x
"},{"text":"
Les options -a, -u et -x permettent respectivement d'afficher tous les processus (et pas seulement ceux de l'utilisateur qui lance la commande), d'afficher le nom des utilisateurs (plutôt que leur identifiant numérique) et de compter aussi les processus n'ayant pas été lancés depuis un terminal (comme les daemon ou les processus lancés depuis une interface graphique).
Le détaille des colonnes les plus importantes sont ci-dessous. Pour en savoir plus avec la commande ps, il faut aller voir le manuel avec la commande man ps.
La colonne USER indique le nom de l'utilisateur qui a lancé le processus.
La colonne PID donne l'identifiant numérique du processus.
Les colonnes %CPU et %MEM indiquent respectivement le taux d'occupation du processeur et de la mémoire par le processus. Par exemple, dans l'affichage précédant, on peut voir que le processus 6966 occupe 9,8% du temps de calcul du processeur et 2% de la mémoire. En simplifiant un peu, on peut dire que sur les dernières 100 secondes d'utilisation du système, 9,8 secondes ont été passées à exécuter des instruction du processus 6966.
La colonne TTY indique l'identifiant du terminal où le processus à été lancé. Un caractère ? indique que le processus n'a pas été lancé depuis un terminal.
La colonne STAT indique l'état du processus (là première lettre en majuscule).
Sur la plupart des systèmes Unix, les états sont:
R : running ou runnable, le processus est dans l'état prêt ou en exécution (la commande ps ne différencie pas ces deux états);
S : sleeping, le processus est en attente.
Les colonnes START et TIME indiquent respectivement l'heure ou la date à la-
quelle le programme a été lancé et le temps cumulé d'exécution du processus correspondant (c'est-à-dire le temps total pendant lequel le processus était dans l'état « en exécution »).
Enfin, la colonne COMMAND indique la ligne de commande utilisée pour lancer le programme.
"}],[{"text":"
La commande top
Une commande un peu plus conviviale est la commande top. Cette dernière affiche en temps réel des informations similaires à celles affichées par ps.
Lancer la commande top et mettre le résultat ci-dessous.
Attention, pour sortir de la commande top, il faut utiliser la combinaison de touches Ctrl+C.
Ces informations sont rafraîchies toutes les secondes.
Cette commande est particulièrement précieuse pour essayer de déterminer quels processus occupent le plus le processeur ou ont alloué le plus de mémoire,
L'affichage de la commande top est donné à la figure 2.
","title":""},{"edit":"
Mettre le résultat ici de la commande top.
"}],[{"text":"
La commande kill
Si on souhaite interrompre un processus dont on connaît le PID, on peut utiliser la commande kill, en lui spécifiant les numéros des processus que l'on souhaite terminer.
$ kill 69663537
La commande ci-dessus va envoyer un signal de terminaison aux deux processus listés.
Pour les applications graphiques, ce signal est globalement équivalent à fermer la fenêtre principale de l'application.
Pour les applications en ligne de commande (qui s'exécutent dans un terminal en bloquant celui-ci), cela correspond à exécuter la combinaison de touche Ctr1-C.
Ce signal peut être intercepté par l'application et géré par cette dernière.
Par exemple, un logiciel de traitement de texte peut, comme lorqu'on ferme la fenêtre, proposer à l'utilisateur de sauvegarder ses fichiers avant de quitter.
Si on souhaite terminer immédiatement le processus sans que ce dernier puisse intercepter le signal, on peut passer l'option -9 à la commande ki11.
$ kill -9 69663537
******
Figure 22.2 — L'affichage de la commande top.
Dans ce cas, les processus seront immédiatement terminés, sans qu'ils puissent exécuter la moindre instruction supplémentaire. En particulier, les fichiers non écrits seront perdus. Cette option est à utiliser en dernier recours, par exemple lorsque l'application ne «répond plus» et se comporte de manière anarchique (utilisation importante du processeur où consommation de mémoire excessive).
Remarque : Sous le système Microsoft Windows, le gestionnaire de tâches joue un rôle similaire. Il est accessible par la combinaison de touche Ctrl+Alt+Sup.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Processus concurrents
Comme nous l'avons vu, les processus d'un système s'exécutent de manière concurrente:
leurs exécutions sont entrelacées.
De plus, l'ordre dans lequel ils s'exécutent est hors de leur contrôle, car il est décidé par l'ordonnanceur de processus du système d'exploitation.
Cette façon de fonctionner possède de nombreux avantages. Elle permet l'exécution d'un très grand nombre de programmes sur une machine monoprocesseur.
Elle permet aussi d'optimiser les ressources de la machine.
Par exemple, si un processus est en attente d'entrées-sorties, il est simplement mis en pause et le système peut utiliser le processeur pour effectuer un calcul utile. Et même si tous les processus sont en attente d'un événement, le système peut alors décider
dans ce cas de réduire la fréquence du processeur ou de le mettre partiellement en veille, ce qui permet d'économiser de l'énergie.
Cet aspect est particulièrement important pour les systèmes mobiles ou embarqués.
Cependant, l'utilisation de systèmes multitâches n'est pas sans problème.
En effet. lorsqu'un processus est interrompu, il ne s'en «rend pas compte».
Dit autrement, lorsqu'un processus est interrompu, il reprendra son exécution exactement dans l'état où il s'était arrêté. Tant que ce processus manipule des objets visibles de lui seul (par exemple, des variables allouées sur la pile ou dans le tas), tout va bien.
Mais si le processus accède à une ressource partagée, comme un fichier ou un périphérique matériel, alors de nombreux problèmes peuvent se produire.
Considérons le programme ci-dessous.
Programme — Écriture dans un fichier
from os import getpid
pid = str(getpid())
with open (\"test.txt\", \"w\") as fichier:
for i in range (1000):
fichier.write(pid + \" : \" + str(i) + \"\\n\")
fichier.flush()
Ce programme importe la fonction utilitaire getpid du module os. Celle-ci ne prend pas d'argument et renvoie simplement l'identifiant du processus dans lequel on se trouve.
Le programme stocke dans une variable globale son identifiant de processus, converti en chaîne de caractères. Puis il ouvre le fichier test.txt en écriture (mode \"w\", qui autorise l'écriture et vide le fichier s'il existe, plaçant le curseur interne en début de fichier).
Le programme écrit ensuite 1000 lignes de la forme:
12345 : O
12345 : 1
...
12345 : 999
où 12345 est l'identifiant de processus déterminé au début du programme.
La méthode .flush() de l'objet fichier assure que les caractères écrits avec la méthode write sont effectivement écrits sur le disque.
Enregistrer le programme Ecrire dans un fichier dans ecrire_fichier.py.
Exécuter le dans l'Idle python :
>>> ecrire_fichier.py
Ouvrer le fichier test.txt et mettre le numéro du processus ci-dessous.
Lancer dans l'Idle python l'instruction ci-dessus et mettre le résultat ci-dessous.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Comme l'écriture sur un disque dur étant coûteuse en temps, les fonctions d'écriture gardent en interne un tableau de caractères 4 tampon » dans lequel sont accumulés les caractères écrits au moyen de -write(). Seulement lorsque ce tableau est plein, les données sont écritent dans le fichier.
L'utilisation du caractère & à la suite d'une commande bascule directement ce programme en arrière-plan et rend directement la main au terminal.
On a donc lancé trois fois le programme ecrire_fichier.py.
Le shell a indiqué dans la console les identifiants des processus correspondants.
Les trois lignes contenant Done sont l'écrites lorsque les programmes se terminent.
Observer le contenu du fichier test.txt et conclure.
","title":""},{"edit":"
Conclure ici.
"}],[{"text":"
En observant le contenu du fichier test.txt, vous avez vu des lignes telles que:
11104 : 150
11103 : 151
11104 : 152
11105 : 153
11103 : 154
11103 : 155
11105 : 156
11105 : 157
11105 : 158
11105 : 159
11104 : 160
11104 : 161
11104 : 162
11104 : 163
11105 : 164
11105 : 165
11105 : 166
11105 : 167
11104 : 168
11104 : 169
11104 : 170
11104 : 171
11104 : 172
11104 : 173
11103 : 174
11103 : 175
11103 : 176
11103 : 177
11104 : 178
11104 : 179
11104 : 180
On a donc l'impression que certaines lignes ont été écrites par certains processus uniquement.
La réalité est plus complexe. Lors de l'ouverture du fichier, chaque processus initialise dans sa mémoire une variable de position (le curseur) contentant le décalage par rapport au début du fichier.
Lorsque des caractères sont écrits au moyen de .write() le curseur est avancé d'autant de caractères. Un enchaînement d'exécutions de processus provoquant les écritures est représenté à la figure 3.
La figure 3 représente dans trois colonnes les états des trois processus, en particulier les valeurs internes du curseur dans le fichier test.txt et les chaînes qui sont écrites lorsque le processus à la main.
La figure ci-dessous représente l'instant où le processus 11103 a la main,
Ce dernier est à une certaine position dans le fichier (1726) et y écrit deux lignes. Il est alors interrompu par l'ordonnanceur qui provoque un changement de contexte et donne la main au processus 11105.
Ce dernier se trouvait dans la position du curseur 1714 dans le fichier, et donc à indice inférieur (car les trois processus sont partis de l'indice 0 et écrivent exactement les mêmes nombres de caractères dans le fichier).
Le processus 11105 écrit une ligne avant d'être interrompu.
Le processus 11104 passe alors en exécution et peut écrire une ligne.
Comme c'est le dernier processus à écrire le ligne « 143 » qui se retrouve dans le fichier final.
L'ordonnanceur rend la main au processus 11105 qui se retrouve dans l'état où sa variable de curseur vaut 1726 et sa variable i de boucle vaut 153.
Il va donc procéder à l'écriture de la chaîne 11005 : 153\\ndans le fichier, écrasant par là même ligne qui avait été écrite à cette même position par le processus 11103.
Il est important de noter que cet «entrelacement» des instructions des trois processus est «aléatoire» ou plus
exactement non déterministe.
Figure 3 — Commutations de contexte entre processus.
Si on ré-exécute les trois programmes, l'ordonnanceur peut décider de changer de contexte à d'autres moments, en fonction de divers paramètres (nombre de processus total en cours d'exécution, valeur des horloges, etc.) et on obtiendra un fichier différent.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Interblocage
Un tel accès concurrent à une même ressource est souvent problématique.
Certains périphériques matériels, en particulier, nécessitent un accès exclusif.
Nous illustrons ce phénomène complexe avec les deux programmes suivant:
enregistrer_micro: acquiert la carte son en accès exclusif (pour accéder au micro) et écrit les sons enregistrés sous un certain format (par exemple mp3 ou wav) sur sa sortie standard et s'arrête lorsqu'il a écrit l'équivalent de 10 secondes de son.
jouer_son: acquiert la carte son en accès exclusif (pour accéder au haut-parleur) et joue le contenu qu'il reçoit sur son entrée standard.
Même si nous ne donnons pas les détails du code (d'assez bas niveau) permettant de réaliser ces programmes, ils n'en restent pas moins assez réalistes.
Les cartes son sont typiquement des périphériques ne pouvant être utilisés
que par au plus un processus.
Ces deux programmes peuvent être exécutés
séquentiellement de la façon suivante:
$ enregistrer_micro > message.mp3
$ cat message.mp3 | jouer_son
La première ligne de commande appelle le programme enregistrer_micro et enregistre (au moyen d'une redirection) les octets émis sur sa sortie standard dans le fichier message.mp3.
La deuxième commande utilise l'utilitaire cat pour afficher le contenu du fichier message.mp3 sur la sortie standard puis redirige cette dernière au moyen de l'opérateur «|» sur l'entrée standard de la commande jouer_son.
Que se passe-t-il maintenant si on enchaîne directement les deux commandes?
$ enregistrer_micro | jouer_son
Le premier programme ouvre la carte son.
Il commence ensuite à envoyer des données sur sa sortie standard.
Le second programme tente alors d'ouvrir la carte son et se retrouve bloqué.
Il ne peut continuer son exécution tant que la carte son n'est pas disponible.
Comme le programme jouer_son est bloqué, il ne lit aucun octet sur son entrée standard.
Comme il ne lit aucun octet, le premier programme se retrouve lui aussi bloqué lorsqu'il tente d'écrire sur sa sortie standard (puisque personne ne lit, les octets s'accumulent dans une zone mémoire tampon et le programme est bloqué quand cette dernière est pleine).
Les deux processus correspondant à ces deux programmes sont en inter-blocage:
le processus de jouer_son attend que la carte son soit libre pour progresser et lire son entrée standard;
le processus d'enregistrer_micro attend que quelqu'un consomme son entrée standard pour progresser et libérer la carte son.
L'inter-blocage est le grand danger de la programmation concurrente.
Dans notre exemple, l'utilisateur constaterait juste que les deux programmes ne font rien et ne rendent pas la main.
Il existe quatre conditions nécessaires
à la présence d'un inter-blocage, appelées conditions de Coffman, du nom d'Edward Grady Coffman Jr. (1934-), informaticien américain qui les a décrites en premier en 1971.
Exclusion mutuelle: au moins une ressource du système doit être en accès exclusif.
Rétention et attente: un processus détient une ressource et demande une autre ressource détenue par un autre processus.
Non préemption: une ressource ne peut être rendue que par un processus qui la détient (et ne peut pas être «préemptée» ou acquise de force par un autre processus).
Attente circulaire: l'ensemble des processus bloqués P1,...,Pn sont tels que P1 attend une ressource tenue par P2,...,Pn et P2,...,Pn attend une ressource détenue par P1.
Dans notre exemple précédent, les quatre conditions sont remplies.
Il existe une ressource à accès exclusif, la carte son(1).
Le processus du programme enregistrer_micro détient la carte son et veut en plus pouvoir écrire sur sa sortie (2).
Nous avons fait l'hypothèse que jouer_son est bloqué et ne peut pas acquérir la carte son (3).
Le programme enregistrer_micro attend
le déblocage de son entrée standard bloquée par jouer_son et ce dernier
attend le déblocage de la carte son détenue par enregistrer_micro (4).
Il existe plusieurs stratégies permettant d'éviter les inter-blocages où de les détecter et de les résoudre.
Dans le cadre générique des systèmes d'exploitation, et des programmes utilisateurs, la solution souvent retenue est la plus simple, à savoir interrompre les programmes (par exemple au moyen de
la commande kill).
","title":""},{"edit":"
"}],[{"text":"
Programmation concurrente en Python
Afin d'illustrer les problématiques d'inter-blocage dans un cadre plus
contrôlé que dans un système d'exploitation, nous donnons ici une introduction à la programmation multithread en Python.
Comme nous l'avons déjà expliqué, un thread est un «sous-processus» démarré par un processus et s'exécutant de manière concurrente avec le reste du programme.
Le module threading de la bibliothèque standard Python permet de démarrer des threads.
Nous illustrons son utilisation au moyen du programme ci-dessous:
*******57
Ce programme définit une fonction hello prenant en argument un entier représentant l'identifiant du thread dans lequel on se trouve.
Cette fonction effectue ensuite une boucle pour i allant de 0 à 4 et écrit à chaque tour de boucle la valeur de n et de i.
La fonction imprime ensuite un message indiquant qu'elle a terminé la boucle puis se termine.
Le programme principal effectue une boucle et appelle quatre fois (pour n entre 0 et 3) l'expression:
threading.Thread(target=hello, args=[n]).
Cette dernière crée un objet de type Thread.
L'argument nominé target doit être une fonction et l'argument args un tableau des arguments qui seront passés à la fonction.
La variable t contient l'objet Thread créé.
La méthode .start() lance l'exécution de la fonction en tâche de fond.
Cette méthode rend directement la main et le programme principal continue de s'exécuter de façons concurrente au thread démarré.
Programme — Comptage en parallèle
import threading
def hello(n):
for i in range(5):
print (\"Je suis le thread\", n, \"et ma valeur est\", i)
print (\"------ Fin du Thread \", n)
for n in range(4):
t = threading.Thread(target=hello, args=[n])
t.start()
Le programme ci-dessus, une fois exécuté, comporte alors cinq threads:
ceux démarrés par .start() et le
thread principal.
Tester 2 fois le programme et mettre le résultat ci-dessous. Comparer les 2 résultats et conclure sur les threads.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"},{"text":"
Comme pour les processus, les threuds alternent leur exécution au gré des commutations de contexte.
Deux exécutions successives donnent des affichages différents.
L'ordre dans lequel sont démarrés les threads ne donne aucune indication sur l'ordre dans lequel ils peuvent se terminer (dans l'exemple ci-dessus, le thread 0, démarré en premier se termine le dernier).
On peut s'étonner que les threads ne s'interrompent pas les un les autres au milieu d'une ligne.
Nous donnerons l'explication de ce phénomène plus loin.
"}],[{"text":"
Les threads peuvent servir à illustrer les problèmes de concurrence et d'interblocage.
Considérons le programme ci-dessous.
Programme — Compteur global partagé
import threading
COMPTEUR = 0
def incrc():
global COMPTEUR
for c in range(100000):
v = COMPTEUR
COMPTEUR = v + 1
th = []
for n in range(4):
t = threading.Thread(target=incrc, args=[])
t.start()
th.append(t)
for t in th:
t.join()
print (\"valeur finale\", COMPTEUR)
Ce dernier définit une variable globale COMPTEUR.
La fonction incre est similaire à la fonction hel1o du programme précédent.
Elle ne prend pas d'argument mais exécute 100 000 itérations d'une boucle qui incrémente la variable globale COMPTEUR.
Le programme principal déclare un tableau vide th.
Il démarre ensuite quatre threads et stocke les objets correspondants dans le tableau th, après les avoir démarrés.
Enfin, pour chacun des objets Thread stockés, la méthode .join() est appelée.
Cette dernière permet d'attendre que le thread auquel on l'applique soit terminé.
Si le thread est déjà terminé, la méthode se termine immédiatement.
Enfin, le programme imprime la valeur finale contenue dans le compteur.
Comme on a démarré quatre threads, et que chacun incrémente la valeur 100 000 fois, on s'attend à ce que l'affichage final soit 400 000.
Cependant, si on exécute le programme plusieurs fois, on peut constater qu'il n'affiche pas toujours le nombre attendu.
Tester plusieurs fois le programme pb_threads.py et conclure.
Que se passe-t-il?
Considérons les quatre threads t0 à t3.
Supposons que t0 soit en exécution et que la valeur de COMPTEUR soit 42. Si t0 est interrompu juste après avoir exécuté
v = COMPTEUR,
alors sa variable locale v contient la valeur 42.
La commutation de contexte donne la main à 41, qui exécute v = COMPTEUR suivi de COMPTEUR = v + 1 avant d'être lui-même interrompu.
La valeur de compteur continue d'augmenter lors des commutations de contexte suivantes avec 2 et t3 jusqu'à avoir COMPTEUR valant 50.
Lorsque ta reprend enfin la main, il continue là où il s'était arrêté et exécute donc COMPTEUR = v + 1, où la valeur de v est 42!
Le thread t0 va donc écraser la valeur 50 avec la valeur 43.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Pour corriger ce problème, il nous faut donc garantir l'accès exclusif à la variable COMPTEUR entre sa lecture et son écriture.
On peut pour cela utiliser un verrou.
Un verrou est un objet que l'on peut essayer d'acquérir.
Si on est le premier à faire cette demande, on acquiert le verrou.
On peut le rendre à tout moment.
Si en revanche quelqu'un d'autre tient le verrou, alors on est bloqué jusqu'à ce qu'il soit libéré.
Des verrous munis de ces deux opérations
sont disponibles dans le module threading avec le constructeur Lock.
Une fois l'objet verrou construit, on peut tenter de l'acquérir avec la méthode
.acquire() et on peut le rendre avec la méthode .release().
Une manière de corriger le programme précédent est la suivante:
import threading
verrou = threading.Lock()
def hello(n):
for c in range(5):
verrou.acquire()
v = COMPTEUR
COMPTEUR = v + 1
verrou.release()
Avant toute tentative de lecture, on essaye d'acquérir le verrou.
Une fois ce dernier acquis, le thread courant a la garantie qu'il est le seul à exécuter son code, jusqu'à l'instruction verrou.release(}).
Une telle portion de code protégée par un verrou s'appelle une section critique.
Attention, cela ne signifie pas que le thread ne peut pas être interrompu entre les lignes
v = COMPTEUR
et
COMPTEUR = v+1.
Cela signifie seulement que les autres threads, s'il reprennent la main, ne sont pas eux-mêmes en section critique
(ils sont forcément ailleurs dans leur code, probablement bloqués sur l'instruction verrou.acquire()).
On remarque qu'il est important que tous les thronds manipulent le même verrou.
C'est pour cela qu'il a été défini dans une variable globale accessible depuis tous les threads.
En dernière remarque, on peut ajouter que le programme aurait été tout aussi faux si on avait écrit COMPTEUR = COMPTEUR + 1 ou même COMPTEUR += 1. En effet, une instruction d'un langage de haut niveau comme Python cst en fait décomposée en de
nombreuses instructions machines:
lecture de la valeur du compteur en mémoire, addition et écriture en mémoire de la nouvelle valeur.
Le programme aurait donc pu être interrompu entre deux de ces instructions machines.
L'utilisation de plusieurs verrons rend les interblocages possibles.
Il conviendra donc d'être très prudent lorsque l'on manipule deux verrous à la fois.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
On illustre le problème des 2 verrous avec le programme suivant:
Programme - Interblocage
import threading
verrou1 = threading.Lock()
verrou2 = threading.Lock()
def f1():
verrou1.acquire()
print (\"Section critique 1.1\")
verrou2.acquire()
print (\"Section critique 1.2\")
verrou2.release()
verrou1.release()
def f2():
verrou2.acquire()
print (\"Section critique 2.1\")
verrou1.acquire()
print (\"Section critique 2.2\")
verrou1.release()
verrou2.release()
t1 = threading.Thread(target=f1, args=[])
t2 = threading.Thread(target=f2, args=[])
t1.start ()
t2.start ()
Tester plusieurs fois le programme ci-dessous et conclure.
Ce dernier déclare deux verrous, utilisés de façon symétrique par deux fonctions f1 et f2.
La fonction f1 essaye d'acquérir d'abord verrou1 puis verrou2, alors que f2 essaye de les acquérir dans l'ordre inverse.
Si on exécute ce programme, il a de grandes chances de se retrouver bloqué.
Considérons l'exécution suivante:
Le thread t1 a la main. Il s'exécute jusqu'à son premier affichage (avant la tentative d'acquisition de verrou2).
Le thread t2 prend la main. Il s'exécute, acquiert verrou2 qui est toujours libre, puis bloque sur l'acquisition de verrou1.
Le thread t1 reprend la main, il bloque alors sur l'acquisition de verrou2 (tenu par t2).
Chaque thread détient un verrou et attend l'autre. Ils sont en interblocage.
Cependant, le problème ne se manifeste que si les exécutions se font dans cet ordre.
Si la commutation de contexte intervient après que f1 a acquis verrou2, alors t1 peut se terminer sans bloquer.
Dans des programmes complexes, les situations d'interblocage sont particulièrement difficile à tester et
à corriger.
En effet, à cause du non déterminisme de l'ordonnancement des threads et des processus, il se peut que le programme se comporte bien lors de la phase de test et ne se bloque que lorsqu'il est exécuté en conditions réelles.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Verrous cachés
Considérons de nouveau le programme ci-dessous.
Programme — Comptage en parallèle
import threading
def hello(n):
for i in range(5):
print (\"Je suis le thread\", n, \"et ma valeur est\", i)
print (\"------ Fin du Thread \", n)
for n in range(4):
t = threading.Thread(target=hello, args=[n])
t.start()
Puisque les commutations de contextes peuvent intervenir à tout moment, il peut
sembler étrange que les affichages des threads s'entrelacent «proprement». En effet, l'affichage du programme donne l'impression que les threads ne sont jamais interrompus au milieu d'une ligne.
Ce comportement est du au fait que par défaut, la sortie standard de Python possède un buffer (ou zone tampon).
Ce dernier n'est rien d'autre qu'un tableau d'octets.
Chaque appel à print n'écrit pas directernent dans la console, ce qui serait inefficace, mais ajoute les caractères à la suite dans le buffer.
Lorsque ce dernier est plein, il est affiché d'un seul coup en utilisant une instruction de bas niveau du système d'exploitation.
Pour les fichiers textes (comme la sortie standard) le buffer est vidé à chaque retour chariot.
Hors, ce buffer n'est rien d'autre qu'un tableau d'octets, accompagnés d'entiers représentant sa taille, et la position du dernier caractère écrit,
Pour éviter le phénomène illustré dans le programme COMPTEUR les mises à jour du buffer ainsi que les modifications de ces entiers sont protégées par un verrou. En effet, il est naturel de vouloir utiliser print depuis plusieurs threads différents.
Si on écrit une chaîne se terminant avec retour chariot (comportement par défaut de print) il se produit donc le phénomène
suivant:
acquisition d'un verrou associé au buffer;
écriture de tous les caractères dans le buffer;
arrivé au retour chariot, envoyer le contenu du buffer dans la console, puis le vider;
relacher le verrou.
Deux threads ne peuvent donc pas mélanger leur ligne, puisque l'écriture d'une ligne devient une section critique.
On peut obtenir un comportement plus «réaliste» en désactivant les buffers d'écriture globalement, avec l'option -u de l'interprète Python:
$ python -u pr_thread.py
Tester le programme ci-dessous avec la commande ci-dessus et mettre le résultat ci-dessous. Conclure.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Les systèmes d'exploitation multitâches sont la norme.
Ils permettent d'exécuter de façon concurrente plusieurs programme.
L'exécution d'un programme s'appelle un processus.
C'est le système d'exploitation, et en particulier l'ordonnanceur, qui détermine quel processus s'exécute à un instant donné.
Le fait pour un processus d'être interrompu s'appelle une commutation de contexte.
Plusieurs processus s'exécutant de façon concurrente peuvent s'interbloquer s'ils
attendent de pouvoir accéder à un même ensemble de ressources en accès exclusif.
Les threads ou processus légers sont des «sous-processus» s'exécutant de manière concurrente.
L'accès à des ressources par plusieurs threads peut être protégé par des verrous.
Une portion de code comprise entre l'acquisition et le relâchement d'un verrou s'appelle une section critique.
","title":"Conclusion","tagtitle":"h1"},{"edit":"
"}],[{"text":"
On suppose qu'Alice exécute dans son terminal la commande
$ ps -a -u -x
Le processus correspondant à cette commande fera partie des processus affichés dans la sortie.
Dire quel sera l'état de ce processus (R ou S) et justifier.
","title":"Exercice"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":"
Le processus correspondant à la commande ps -a -u -x sera toujours dans l’état R. En effet, comme ce processus est, par définition, en train de s’exécuter lorsqu'il demande au système la liste des processus, il est forcément dans l’état R.
"}],[{"text":"
import threading
COMPTEUR = 0
def incrc():
global COMPTEUR
for c in range(100000):
v = COMPTEUR
COMPTEUR = v + 1
th = []
for n in range(4):
t = threading.Thread(target=incrc, args=[])
t.start()
th.append(t)
for t in th:
t.join()
print (\"valeur finale\", COMPTEUR)
On considère le programme ci-dessus (qui n'utilise pas de verrou).
Pour chacune des affirmations suivantes, dire si elle est vraie ou fausse et
justifier.
1. Le programme affiche toujours 400 000.
2. Le programme peut afficher un nombre plus petit que 400 000.
3. Le programme peut afficher un nombre plus grand que 400 000.
4. Si on ajoute t.join() après t.start(), le programme affiche toujours 400 000.
5. Si on transforme le corps de la boucle de la fonction incrc() par
verrou.acquire()
v = COMPTEUR
verrou.release()
verrou.acquire()
COMPTEUR = v + i
verrou.release()
en ayant définie une variable globale verrou = threading.Lock(), alors le programme affiche toujours 400 000.
Solution page 499 D
","title":"Exercice"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":"
1. Faux. Comme on l’a vu, certains incréments peuvent être perdus car écrasés par des valeurs plus anciennes lors d’une commutation de contexte.
2. Vrai. Même justification.
3. Faux. Les seules valeurs que l’on peut écrire dans la variable COMPTEUR sont des valeurs que la variable avait avant un changement de contexte plus 1. On ne peut donc pas dépasser le nombre total qu’on aurait obtenu dans le meilleur des cas.
4. Vrai. Si on ajoute t.join() à cet endroit, alors on attend qu’un thread se termine avant de passer au suivant. Ils ne s’exécutent donc plus de façon concurrente mais séquentielle et ne peuvent plus s’interrompre les uns les autres.
5. Faux. Le thread peut toujours être interrompu entre la lecture du compteur et l'écriture.
"}],[{"text":"
On considère la situation de la figure ci-dessous dans laquelle quatre voitures sont bloquées à une intersection.
Montrer qu'il s'agit d'un interblocage, c'est-à-dire que les quatre conditions de Coffman sont réunies.
On indiquera précisément quelles sont les ressources et les processus dans cette
situation.
On fera l'hypothèse que les conducteurs sont raisonnables, et qu'ils ne veulent pas provoquer d'accident.
Figure 4 — Quatre voitures bloquées à une intersection.
","title":"Exercice"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":"
On identifie quatre processus, les quatre voitures V1, V2, V3 et V4 (V1 en haut à gauche, puis numérotées dans le sens anti-
horaire).
Les quatre ressources sont les quatre portions de routes se trouvant devant chaque voiture (R1 à R4).
Les quatre conditions de Coffman sont bien respectées.
1. Les ressources sont en accès exclusif: deux voitures ne peuvent pas se retrouver sur la même portion de route en même temps.
2. Rétention et attente: chaque voiture détient une portion de route et attend celle qui est devant elle.
3. Non préemption: on a fait l'hypothèse qu’une voiture ne peux pas forcer le passage et pousser la voiture qui est devant elle.
4. Attente cyclique : V1 attend R1 occupée par V2, qui attend R2 occupée
par V3, qui attend R3 occupée par V4, qui attend R4 occupée par V1.
"}],[{"text":"
Considérons un petit système embarqué:
un petit ordinateur relié à trois LED A, B et C.
Une LED peut être éteinte ou allumée et on peut configurer sa couleur.
On dispose de trois programmes qui affichent des signaux lumineux en faisant clignoter les LED.
Chaque programme possède une LED primaire et une LED secondaire.
Le programme P1 affiche ses signaux sur À (primaire) et B (secondaire) en vert.
Le programme P2 affiche ses signaux sur B (primaire) et C (secondaire) en orange.
Le programme P3 affiche ses signaux sur C (primaire) et A (secondaire) en rouge.
Comme les LED ne supportent pas d'être configurées dans deux couleurs en même temps, le système propose deux primitives acquerirLED(nom) et rendreLED(nom) qui permettent respectivement d'acquérir et de relâcher une LED.
Si une LED est déjà acquise, alors acquerirLED() bloque.
On suppose que chacun des trois programmes P1, P2 et P3 effectue les actions suivantes en boucle:
1. acquérir sa LED primaire
2. acquérir sa LED secondaire
3. configurer les couleurs
4. émettre des signaux
5. rendre la LED secondaire
6. rendre la LED primaire
7. recommencer en 1.
Montrer qu'il existe un entrelacement des exécutions qui place P1, P2 et P3 en interblocage.
","title":"Exercice"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":"
Ce problème est une variante du problème du «dîner des philosophes» proposé en 1965 par Edsger Dijkstra.
On considère l’enchaînement suivant:
P1 acquiert A (étape 1) puis est interrompu.
P2 acquiert B (étape 1) puis est interrompu.
P3 acquiert C (étape 1) puis est interrompu.
P1 tente d'acquérir B, sa LED secondaire (étape 2). Il est bloqué car B est tenue par P2.
P2 tente d'acquérir C (étape 2). Il est bloqué car C est tenue par P3.
P3 tente d'acquérir A (étape 2). Il est bloqué car A est tenue par P1.
À ce stade, les trois processus sont bloqués dans une attente circulaire d’une
ressource tenue par un autre processus.
Ils sont donc en interblocage.
"}],[{"text":"
Écrire un programme Python simulant le code de l'exercice précédent. On pourra s'inspirer du programme ci-dessous.
import threading
verrou1 = threading.Lock()
verrou2 = threading.Lock()
def f1():
verrou1.acquire()
print (\"Section critique 1.1\")
verrou2.acquire()
print (\"Section critique 1.2\")
verrou2.release()
verrou1.release()
def f2():
verrou2.acquire()
print (\"Section critique 2.1\")
verrou1.acquire()
print (\"Section critique 2.2\")
verrou1.release()
verrou2.release()
t1 = threading.Thread(target=f1, args=[])
t2 = threading.Thread(target=f2, args=[])
t1.start ()
t2.start ()
Constater qu'en exécutant suffisamment de fois votre prograrame il se bloque.
Indication: afin de laisser plus de chance au système de changer de contexte, on pourra mettre des affichages juste après l'acquisition d'un verrou. En effet, l'écriture dans la console passe
le thread courant en attente, le temps que les écritures soient effectuées, ce
qui laisse une opportunité à l'ordonnanceur de choisir un autre thread où un autre processus.
","title":"Exercice"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":"
On peut utiliser un dictionnaire indicé par les noms des LED pour y stocker les
verrous correspondant.
import threading
VERROU_LED= {}
VERROU_LED[\"A\"] = threading.Lock()
VERROU_LED[\"B\"] = threading.Lock()
VERROU_LED[\"C\"] = threading.Lock()
def acquerirLED(l):
VERROU_LED[l].acquire()
def rendreLED(l):
VERROU_LED[l].release()
def prog(n,lprim, lsec):
whileTrue :
acquerirLED(lprim)
print(\"Acquisition de\", lprim, \"par le programme\", n)
acquerirLED(lsec)
print(\"Acquisition de\", lsec, \"par le programme\", n)
Comment expliqué à la séquence Systèmes de Gestion de Bases de Données, si deux transactions travaillent sur un même objet (par exemple sur une même table), alors la seconde est bloquée jusqu'à ce que la première soit terminée.
Nous pouvons maintenant expliciter ce mécanisme:
lors qu'une transaction accède à une table, elle tente de prendre un verrou sur cette dernière.
Les verrous sont relâchés au moment du COMMIT ou ROLLBACK.
On considère deux tables
CREATE TABLE T (num INTEGER);
CREATE TABLE S (num INTEGER);
INSERT INTO T VALUES(1000);
INSERT INTO S VALUES(1000);
qui peuvent représenter de façon simplifiée des comptes en banque.
Considérons les deux transactions suivantes:
START TRANSACTION;
UPDATE T SET num = num + 100;
UPDATE S SET num = num - 100;
COMMIT;
START TRANSACTION;
UPDATE S SET num = num + 100;
UPDATE T SET num = num - 100;
COMMIT;
La première simule un virement de S vers T et la seconde un virement de T vers S.
Montrer qu'il s'agit d'un interblocage, c'est-à-dire que les quatre conditions de Coffman sont réunies.
","title":"Exercice"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":"
Les deux processus sont les deux transactions Tg et Td (pour transaction de gauche et transaction de droite).
1. Les tables T et S sont des ressources en accès exclusif :d eux transactions ne peuvent pas mettre à jour une même table en même temps.
2. Rétention et attente : chaque transaction a effectué une mise à jour sur une table et attend l’autre.
3. Non préemption : une transaction ne peut pas forcer une autre à relâcher sa table.
4. Attente cyclique : ici, Tg attend S en tenant T et Td attend T en tenant S.
À l'inverse d’un système d'exploitation, un SGBD est un système beaucoup plus «contraint».
La réponse usuelle des SGBD est de terminer en erreur l'une des deux transactions (arbitrairement) lorsqu'un tel interblocage est détecté.
Voici par exemple le message d'erreur affiché par MariaDb après quelques secondes de blocage:
ERROR: deadlock detected
DETAIL: Process 2160 waits for ShareLock on transaction 1000;
blocked by process 2157.
Process 2157 waits for ShareLock on transaction 999;
blocked by process 2161.
HINT: See server log for query details.
CONTEXT: while updating tuple (0,1) in relation \"t\"
Les circuits électroniques d'un ordinateur (ie ménoire, microprocesseur, etc.) manipulent uniquement des chiffres binaires 0 et 1 qui, en interne, sont simplement représentés par des tensions électriques.
","title":"Transistor et portes logiques","tagtitle":"h1"},{"edit":"
une tension basse (proche de 0 volt) et le chiffre 1 par une tension haute (que l'on notera +V volts, car cette tension varie selon les circuits électroniques).
Par exemple, pour le microprocesseur 4004 d'intel (1971) +V=5V et pour le intel i7 (2020) +V=0,6V
"}],[{"text":"
Les opérateurs (logiques ou arithmétiques) sur ces nombres binaires sont construits à partir de circuits électroniques dont les briques élémentaires sont appelés transistors (Tr).
Les transistors que l'on trouvent dans les circuits électroniques des ordinateurs se comportent comme des interrupteurs qui laissent ou non passer un courant électrique, selon le mode du tout ou rien (commutation), comme représenté de la manière suivante:
Dans ce schéma, la commande de l'interrupteur est jouée par la broche B (appelée la Base).
Lorsqu'elle est sous tension haute (niveau logique 1), le courant circule entre la broche C (appelée Collecteur) et la broche E (appelée Emetteur) et la tension entre la borne C et la masse M passe à 0 (niveau logique 0).
Inversement lorsque la base est sous tension basse (niveau logique 0), le courant est bloqué entre la broche C et la broche E et la tension entre la borne C et la masse M passe à +Vcc (niveau logique 1).
Ce simple transistor permet de réaliser une opération élémentaire appelée porte logique NON (appelée NOT en anglais).
Une porte logique est une fonction qui prend un où plusieurs bits en entrée et qui produit un bif en sortie.
La porte NOT implantée par un transistor est la plus simple de toutes les portes.
Elle n'a qu'un seul bit en entrée (P) et sa sortie (Q) vaut 0 quand l'entrée vaut 1, et inversement elle vaut 1 quand son entrée est à 0.
Graphiquement, on représente la porte NOT comme dans le schéma ci-dessous, avec à gauche la notation européenne et à droite la notation américaine.
Symbole européen
Symbole américain
Pour représenter le calcul réalisé par une porte logique, on utilise une table logique (ou table de vérité) qui relie les valeurs des entrées à la valeur du résultat.
La table logique de la porte NOT cst donnée ci-dessous.
Porte NOT
P
R
0
1
1
0
La porte Non a pour équation logique Q=/P.
Vous pouvez vérifier la table de vérité avec l'application ci-dessous:
On peut fabriquer d'autres portes logiques en combinant plusieurs transistors.
Par exemple, en combinant deux transistors en série comme ci-dessous
(schéma de gauche) on peut fabriquer la porte NON ET (appelée NAND en anglais) qui, pour deux entrées P et Q, produit un résultat R dont le calcul est donné par la table de vérité à droite.
Les portes NAND et NOR sont fondamentales dans les circuits électroniques car elles sont complètes, c'est-à-dire que n'importe quel circuit peut être conçu en utilisant uniquement ces deux portes.
Par exemple, la porte NOT peut être fabriquée à partir d'une porte NAND en reliant les deux entrées de cette porte, comme ci-dessous.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Une autre porte logique très inportante est la porte ET (AND en anglais).
Elle peut aussi être construite avec plusieurs portes NOR.
Les grands principes de fonctionnement des ordinateurs tels que nous les connaissons aujourd'hui reposent sur des travaux réalisés au milieu des années 40 par une équipe de chercheurs de l'université de Pennsylvanie.
Ces travaux concernaient la conception d'un ordinateur dans lequel les programmes à exécuter étaient stockés au même endroit que les données qu'ils devaient manipuler, à savoir dans la mémoire de l'ordinateur.
Cette idée d'utiliser une zone de stockage unique pour les programmes et les données est toujours utilisée aujourd'hui.
Cette architecture est appelée modèle de
von Neumann, en l'honneur du mathématicien et physicien John von Neumann qui participa à ces travaux et qui publia en 1945 un rapport sur la conception de l'EDVAC, ce nouvel ordinateur basé sur ce modèle de calcul.
Le schéma de la figure ci-dessous décrit l'organisation des principaux composants d'un ordinateur selon l'architecture de von Neumann.
Ce modèle comporte quatre types de composants:
une unité arithmétique et logique,
une unité de contrôle,
la mémoire de l'ordinateur
et les périphériques d'entrée-sortie.
Figure 1 — Architecture de von Neumann.
Les deux premiers composants sont habituellement rassemblés dans un ensemble de circuit électronique qu'on appelle Unité Centrale de traitement où plus simplement processeur (CPU en anglais, pour Control Processing Unit).
Lorsqu'un processeur rassemble ces deux unités dans un seul et même circuit, on parle alors de microprocesseur.
L'unité arithmétique et logique (Arithinetic Logic Unit en anglais ou ALU) est uu circuit électronique qui effectue à la fois des opérations arithmétiques et des opérations sur les bits de nombres entiers en binaire.
L'unité de contrôle {Control Unit en anglais ou CT) joue le rôle de chef d'orchestre de l'ordinateur. C'est ce composant qui se charge de récupérer en mémoire la prochaine instruction à exécuter et les données sur lesquelles elle doit opérer, puis les envoie à l'unité arithmétique et logique.
La mémoire de l'ordinateur contient à la fois les programmes et les données.
On distingue habituellement deux types de mémoires:
La mémoire vive ou volatile est. celle qui perd son contenu dès que l'ordinateur est éteint. Les données stockées dans la mémoire vive d'un ordinateur peuvent être lues, effacées ou déplacées comme on le souhaite. Le principal avantage de cette mémoire est la rapidité d'accès aux données qu'elle contient, quel que soit l'emplacement mémoire de ces données. On parle souvent de mémoire RAM en anglais, pour Random-Access Memory.
La mémoire non volatile est celle qui conserve ses données quand on coupe l'alimentation électrique de l'ordinateur. Il existe plusieurs types de telles mémoires. Par exemple, la ROM, pour Read-Only Memory en anglais, est une mémoire non modifiable qui contient habituellement des données nécessaires au démarrage d'un ordinateur ou tout autre information dont l'ordinateur à besoin pour fonctionner. La mémoire Flash est un autre exemple de mémoire non volatile. Contrairement à la ROM, cette mémoire est modifiable (un certain nombre de fois) et les informations qu'elle contient sont accessibles de manière uniforme. Contrairement à la RAM, ces mémoires sont souvent beaucoup plus lentes, soit pour lire les données, soit pour les modifier.
Il existe un très grand nombre de périphériques d'entrées/sortie pour un ordinateur.
On peut tenter de les classer par familles. Tout d'abord, les périphériques d'entrée:
les dispositifs de saisie comme les claviers ou les souris,
les manettes de jeu, les lecteurs de code-barres,
les scanners, les appareils photos numériques, les webcams, etc.
Ensuite, les périphériques de sortie comme :
les écrans et vidéo-projecteurs.
les imprimantes,
les haut-parleurs, etc.
Enfin, certains périphériques sont à la fois des dispositifs d'entrée et de sortie, comme par exemple :
les lecteurs de disques (CD, Blue Ray, etc),
les disques durs, les clés USB ou les cartes SD,
les cartes réseaux (modems), etc.
Les flèches du diagramme de la figure ci-dessous décrivent les différentes interactions entre ces composants.
Pour les comprendre, nous allons passer
rapidement en revue le fonctionnement de chaque composant et ses interactions avec les autres composants de l'ordinateur.
Unité de contrôle
Cette unité est essentiellement constituée de trois sous-composants. Tout d'abord, deux registres (mémoires internes très rapides).
Le premier est le registre d'instruction, dénommé IR (car en anglais il se
nomme instruction Register), qui contient l'instruction courante à décoder et exécuter.
Le second registre est le pointe (car en anglais il se nomme Instruction Pointer), qui indique l'emplacement mémoire de la prochaine instruction à exécuter.
Le troisième sous-composant est un programme particulier, appelé micro-programme, qui est exécuté par le CU et qui contrôle presque tous les mouvements de données de la mémoire vers l'ALU (et réciproquement) ou les périphériques d'entrée-sortie.
L'unité de contrôle est donc tout naturellement connectée à tous les autres composants de l'ordinateur.
Unité arithmétique et logique
Cette unité est composée de plusieurs registres, dits registres de données, et d'un registre spécial, appelé accumulateur, dans lequel vont s'effectuer tous les calculs.
À ces registres s'ajoutent tout un tas de circuits électroniques pour réaliser des opérations arithmétiques (addition, soustraction, etc), des opérations logiques (et, ou, complément à un, etc.), des comparaisons (égalité, inférieur, supérieur, etc.), des opérations sur les bits (décalages, rotations) ou des opérations de déplacements mémoire (copie de ou vers la mémoire).
Les entrées d'une ALU sont les données sur lesquelles elle va effectuer une opération (on parle d'opérandes).
Ces registres sont chargés avec des valeurs venant de la mémoire de l'ordinateur et c'est l'unité de contrôle qui indique quelle opération doit être effectuée.
Le résultat d'un calcul (arithmétique ou logique) se trouve dans l'accumulateur.
Cependant, l'ALU peut également envoyer des signaux pour indiquer des erreurs de calcul (division par zéro, dépassement de la mémoire, etc.) où des résultats de comparaison (inférieur, supérieur, etc).
La mémoire
Nous avons vu les mouvements de données entre l'unité centrale de traitement et la mémoire de l'ordinateur.
Ces échanges se font à travers un médium de communication appelé bus.
Mais les périphériques d'entrée-sortie peuvent également lire et écrire directement dans la mémoire à travers ce bus, sans passer par le CPU.
Cet accès direct à la mémoire est réalisé par un circuit électronique spécialisé appelé controleur DMA, pour Direct Memory Access en anglais.
Les dispositifs d'entrée-sortie
Ces composants sont connectés à l'ordinateur par des circuits électroniques appelés ports d'entrée-sortie sur lesquels il est possible d'envoyer ou recevoir des données.
L'accès à ces ports se fait habituellement à travers des emplacements mémoires à des adresses prédéfinies.
Ainsi, l'envoi ou la réception de données revient simplement à lire ou écrire dans ces emplacements réservés.
Pour connaître l'état d'un périphérique, le CPU peut soit périodiquement lire dans ces emplacements mémoires, mais il peut aussi être directement prévenu par un périphérique d'un changement à travers un mécanisme spécial d'interruption prévu à cet effet.
Une fois interrompu, le CPU peut simplement lire le contenu des ports.
","title":"Architecture de von Neumann ","tagtitle":"h1"},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Limitation du modèle de von Neumann
Ce modèle impose un va-et-vient constant entre le CPU et la mémoire, soit pour charger la prochaine instruction à écouter, soit pour récupérer les données sur lesquelles l'instruction courante doit opérer.
Cependant, la différence de vitesse entre les microprocesseurs (nombre d'opérations par seconde) et la mémoire (temps d'accès) est telle qu'aujourd'hui, avec cette architecture, les microprocesseurs modernes passeraient tout leur temps à attendre des données venant de la mémoire, qui, bien qu'ayant aussi gagné en rapidité, reste beaucoup plus lente qu'un CPU.
C'est ce qu'on appelle le goulot d'étranglement du modèle de von Neumann.
Pour tenter de remédier à ce problème, les fabricants d'ordinateurs ont inventé les mémoires caches.
Ce sont des composants très rapides (mais
très chers) qui s'intercalent entre la mémoire principale et le CPU.
L'idée principale pour gagner du temps est qu'une donnée utilisée une fois a de grandes chances d'être utilisée plusieurs fois.
Ainsi, la mémoire cache est chargée avec des données provenant de la RAM quand une instruction en à besoin, ceci afin de diminuer le temps d'accès ultérieurs à ces données.
D'autres pistes ont également été explorées. Il s'agit des architectures
dites parallèles. Le modèle de von Neumann est également appelé SISD (Single Instruction Single Data, une seule instruction et une seule donnéc), le CPU exécutant un seul flot d'instructions sur des données dans une seule mémoire.
Le modèle SIMD (pour Single Instruction Multiple Data). Il s'agit d'une architecture avec un seul CPU où une instruction peut être appliquée en parallèle à plusieurs données, pour produire plusieurs résultats en même temps.
Le modèle MIMD (pour Multiple Instructions Multiple Data). Il s'agit d'une architecture dotée de plusieurs CPU qui exécutent chacun un programme, de manière indépendante, sur des données différentes.
En 1975, Gordon E. Moore (cofondateur de la société Intel) énonça la conjecture suivante sur l'évolution des capacités des circuits intégrés, appelés familièrement puces électroniques:
Dans les microprocesseurs, le nombre de transistors sur une puce va doubler tous les deux ans.
Bien que fondée sur un constat empirique de l'industrie des fabricants de circuits entre les années 1965 et 1975, cette prédiction, qu'on appelle aussi loi de Moore, s'est révélée incroyablement juste.
Intel 4004
2050 Transitores
Gravure 10 micromètres
1971
Apple M1
16 milliards de transistors
Gravure 5 nanomètres
2020
On est ainsi passé de 2250 transistors en 1971 sur un microprocesseur Intel 4004 (un des premiers microprocesseurs) à plusieurs dizaines de milliards aujourd'hui.
Sur les derniers microprocesseurs, où la taille des transistors n'est que de 5 nanomètres (5nm), soit à peine plus que l'épaisseur de quelques dizaines d'atomes de silicium.
Cette loi de Moore, qui est (plus ou moins) généralisable à tous les composants électroniques (mémoire, etc.), à permis non seulement une augmentation de la puissance de calcul des ordinateurs (en augmentant leur fréquence de fonctionnement grâce à une diminution de la distance entre les composants), mais également une baisse des coûts (en rassemblant plusieurs composants en un seul).
La diminution de la taille des transistors a également permis de baisser la tension électrique pour les faire fonctionner, ce qui a engendré une diminution de leur consommation énergétique à puissance de calcul équivalente.
La miniaturisation des circuits électroniques est telle qu'il est possible aujourd'hui de rassembler sur une même puce tous les composants essentiels d'un ordinateur (microprocesseur, mémoire, interfaces d'entrées-sorties, etc.).
Ces systèmes complets, que l'on retrouve dans tous les systèmes embarqués, portent les noms de microcontrôleur, système sur puce (en anglais, System on Chip) ou circuit logique programmable.
","title":"L'évolution des microprocesseurs","tagtitle":"h1"},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Source : https://www.arduino.cc
Les microcontrôleurs sont des circuits intégrés qui regroupent sur une même puce (de quelques centimètres carrés) un microprocesseur, de la mémoire, des ports d'entrée-sortie, des périphériques et des bus de communication.
La puissance de caleul et la capacité en mémoire de ces puces sont bien en dessous des composants que l'on trouve sur les cartes mères des ordinateurs.
Ainsi, la fréquence d'horloge (qui détermine le nombre d'instructions exécutées par seconde) est généralement de quelques dizaines de mégahertz (MHz) et la taille de la mémoire se mesure seulement en kilo octets (Ko).
En revanche, les microcontrôleurs ont une consommation électrique très faible, ce qui leur confère une très grande autonomie lorsqu'ils sont alimentés par des batteries.
Enfin, leur coût de fabrication est aussi très réduit et les microcontrôleurs s'achètent pour seulement quelques euros.
Ces «mini»-ordinateurs sont principalement utilisés dans des systèmes
informatique embarqués, par exemple dans des avions, des voitures, des robots, etc.
Les rôles des microcontrôleurs dans ces systèmes sont très spécifiques mais ils partagent néanmoins certaines caractéristiques:
acquisition de données (grandeurs physiques),
contrôle d'un processus (actions mécaniques ou électroniques),
contraintes de temps.
Prenons l'exemple d'un régulateur de vitesse d'une voiture qui maintient le véhicule à une vitesse définie par le conducteur:
il récupère la vitesse de la voiture via un capteur externe branché à l'un de ses ports d'entrée;
il est en charge de contrôler l'accélération ou le freinage automatique du véhicule pour conserver la vitesse définie par le conducteur et pour cela il envoie des ordres à des actionneurs, externes également, reliés à ses ports de sortie;
il est soumis à des contraintes temporelles (temps de réponse) très fortes pour garantir la sécurité des passagers.
Des périphériques auxiliaires, intégrés dans la puce du microcontrôleur, peuvent être nécessaire pour réaliser cette tâche.
Par exemple, un filtre de conversion analogique/numérique est utilisé pour interpréter les données analogiques en entrée du capteur.
Un timer peut aussi être utilisé pour mesurer le temps afin de s'assurer que les réponses seront envoyées dans un
certain délai.
Étant données les capacités de calcul et de mémoire des microcontrôleurs, il n'est pas envisageable d'y faire tourner un système d'exploitation (OS) qui prendrait beaucoup trop de place et de temps d'exécution.
De plus, un OS complique fortement la mise au point d'applications temps réel car il faut maîtriser par exemple quand celui-ci va interrompre les processus en
activité, combien de temps prend le basculement entre les tâches, etc.
Nous découvrirons ces notion dans la séquence suivante.
","title":"Microcontrôleurs"},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Architecture d'un microcontrôleur
Les microcontrôleurs sont principalement des composants autonomes qui, dès qu'ils sont sous tension, exécutent le programme contenu dans leur mémoire.
Ainsi, la principale différence entre l'architecture d'un microcontrôleur et celle d'un ordinateur basée sur le modèle de von Neumann est que la mémoire qui contient les programmes n'est pas la même que celle qui contient les données.
La mémoire Programme est une mémoire morte, c'est-à-dire une mémoire qui ne
perd pas l'information qu'elle contient quand le microcontrôleur n'est plus alimenté en électricité.
Il y à plusieurs type de mémoire morte:
ROM (Read-Only Memory), désigne une mémoire dont le contenu est enregistré à la construction de la mémoire et ne peut être que lu.
EEPROM (Electrically-Erasable Programmable Read-Onty Memory), désigne une mémoire dont le contenu peut être écrit et effacé plusieurs fois. L'accès en lecture/écriture est assez lent, mais on peut effacer et écrire son contenu octet par octet.
FLASH, désigne un type de mémoire similaire à l'EEPROM mais avec un accès très rapide en lecture et (un peu moins) rapide en écriture.
Contrairement à l'EEPROM, la mémoire FLASH est découpée en secteurs physiques (dont la taille varie de quelques centaines d'octets à plusieurs kilo octets) et ne peut être effacée ou écrite octet par octet comme l'EEPROM, mais uniquement secteur par secteur.
De plus, pour réécrire un octet, on est obligé d'effacer tout le secteur qui le
contient avant d'écrire (un secteur entier).
La mémoire Données (Data) peut être composée d'une partie de mémoire morte et d'une autre de mémoire vive (aussi appelée mémoire RAM, pour Random Access Memory), c'est-à-dire de la mémoire dans laquelle on peut lire et écrire à convenance (avec un temps d'accès identique quelle que soit la partie de la mémoire ), mais qui perd toutes ses données dès qu'elle n'est plus alimentée en électricité.
Certains microcontrôleurs profitent de la séparation des mémoires de programmes et de données pour implémenter une architecture dite de Harvard.
Dans cette architecture, on accède aux mémoires Programme et Données (ou ports d'entrée-sortie) à travers deux bus distincts, ce qui procure plusieurs avantages.
Figure 3 - Architecture de Harvard.
Tout d'abord, cela permet de gagner du temps en transférant simultanément au microprocesseur les instructions et les données sur lesquelles elles agissent.
Ensuite, cela permet d'avoir des mots mémoires différents pour les instructions et les données.
Par exemple, on peut utiliser une mémoire morte avec des mots de 16 bits pour stocker les instructions, et seulement des mots de 8 bits pour la mémoire de données.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Jeu d'instructions
Lorsqu'ils sont basés sur une architecture Harvard, les microcontrôleurs ont un jeu d'instructions réduit (en anglais RISC, Reduced Instruction Set Computer), composé d'instructions simples, avec une longueur et un format d'instruction fixe.
Cela simplifie à la fois le circuit de décodage mais également l'architecture globale.
Par exemple, les instructions pour déplacer directement des données entre deux cellules mémoires sont impossibles (il faut passer par des registres du microprocesseur).
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Périphériques
Les microcontrôleurs sont équipés de nombreux périphériques pour effectuer des tâches bien spécifiques.
Voici ci-dessous quatre familles de périphériques couramment utilisés.
Les Timers sont utilisés pour la gestion du temps. Ils servent à déclencher une action au bout d'un laps de temps t. Ceci est réalisé à l'aide d'un compteur, initialisé à t, qui est décrémenté à chaque cycle d'horloge. L'action est déclenchée quand la valeur du compteur atteint 0.
Les modules de capture de signaux permettent de récupérer la valeur d'un timer au moment où un événement extérieur se déclenche, par exemple quand un signal d'entrée change d'état.
Les convertisseurs Analogique/Numérique permettent de transformer une valeur de tension analogique en un nombre binaire.
Les modules de communication permettent de faire communiquer les microcontroleurs entre eux.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Exemple
Les microcontrôleurs PIC sont fabriqués par la société Microchip.
Il s'agit d'une gamme de microcontrôleurs qui s'appuient sur une architecture de Harvard avec un jeu d'instructions RISC.
Il existe plusieurs familles de puces PIC.
Chaque famille se distingue par le type et la quantité de mémoires utilisées, le nombre de bits par instruction, la fréquence d'horloge du CPU, le nombre de ports d'entrée-sortie ou encore les périphériques présents dans la puce.
Par exemple, la figure ci-dessous représente l'architecture de la famille des microcontrôleurs PIC18F
On y distingue les caractéristiques suivantes:
Un CPU avec jeu d'instructions RISC avec des registres de 8 bits et des instructions codées sur 16 bits.
Une mémoire programme de type FLASH dont la capacité peut aller jusqu'à 128 Ko (même si, en théorie, une capacité de 2Mo est possible).
Un bus de communication vers la mémoire programme sur 21 bits d'adressage (permet donc d'accéder jusqu'à 2 Mo de mémoire) et 16 bits de large pour récupérer en un cycle une instruction sur 16 bits.
Une horloge pouvant être cadencée jusqu'à 64 MHz.
Une mémoire de données de type SRAM (Static RAM) de 4 ko. Il s'agit d'un type de mémoire RAM qui ne nécessite pas de rafraîchissement pour maintenir ses informations.
Un bus de communication sur 12 bits d'adressage et 8 bits de données.
Jusqu'à 5 ports d'entrée-sortie bidirectionnels.
Des périphériques comme des timers, des modules de capture de signaux, des convertisseurs A/N (Analogique Numérique) et des modules de communication.
","title":" ","tagtitle":"h1"},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Un système sur puce, appelé System on Chip (SoC) en anglais, rassemble sur un même circuit intégré tous les composants habituellement présents sur la carte mère d'un ordinateur.
Figure 4 — Architecture d'un System on Chip.
Les caractéristiques d'un SoC sont de fait très proches de celles d'un ordinateur.
Ils ont une puissance de calcul comparable qui repose sur des microprocesseurs de dernière génération avec de nombreux cœurs cadencés à plusieurs giga-hertz (GHz), ainsi que des processeurs dédiés (graphique, sécurité).
Leur capacité mémoire se mesure en gigaoctets et ils incluent des mémoires de type RAM et FLASH.
Enfin, ils contiennent de nombreux périphériques.
Tout cela sur une puce d'environ 100mm.
","title":"Système sur puce"},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Architecture d'un système sur puce
Figure 4 — Architecture d'un System on Chip.
Les SoC reposent habituellement sur une architecture comparable à celle donnée dans le schéma ci-dessus.
Contrairement à l'architecture de Harvard sur laquelle repose les microcontrôleurs, on retrouve un modèle de Von Neumann dans lequel il n'y a pas de différence entre la mémoire des programmes et celle de données.
Les unités de calcul sont ainsi connectées à la mémoire (RAM ou ROM) ou aux périphériques par un unique bus de communication.
Les microprocesseurs des SoC sont presque toujours basés sur un jeu d'instructions RISC, avec des registres de 32 ou 64 bits.
Contrairement aux microcontrôleurs, ces CPU sont cadencés à des fréquences tellement élevées qu'ils sont fortement ralentis dès qu'ils lisent ou écrivent des données dans la mémoire ou qu'ils dialoguent avec des périphériques.
Cette différence de vitesse entre unité de calcul, mémoires et périphériques est telle qu'il est crucial de concevoir des moyens de communication entre ces composants qui soient bien calibrés en fonction des vitesses et débits de données de chacun, afin de ralentir le moins possible le microprocesseur.
Ainsi, parce que les composants mémoire ont généralement une fréquence de fonctionnement et des débits de données bien plus importants que les autres périphériques, certaines architectures de SoC mettent en place un système de transfert d'informations reposant sur deux bus de communication.
Un bus haute performance est dédié à l'échange d'information entre le microprocesseur (CPU), les processeurs dédiés à une tâche particulière comme les processeurs graphique (GPU) et les différents composants mémoire.
Un deuxième bus est utilisé pour la communication avec les périphériques plus lents. Ce bus est relié au bus rapide via un pont de communication qui met en œuvre un protocole d'échange.
Cette architecture permet d'éviter que les bus de communication ne deviennent des goulots d'étranglement pour le CPU qui, lorsqu'il souhaite communiquer avec des composants très rapides comme les mémoires, se retrouve bloqué en attente d'une réponse d'un périphérique beaucoup plus lent.
Afin de limiter encore plus l'impact de la différence de vitesse entre le CPU et les autres composants, les SoC disposent d'un mécanisme d'accès direct à la mémoire (en anglais Direct Memory Access — DMA) similaire aux cartes mères des ordinateurs.
Ce mécanisme permet de transférer directement des données de la mémoire vers où depuis un périphérique, sans aucune intervention du CPU.
Pour cela, le CPU ou les périphériques envoient des informations concernant les adresses et la quantité de données à échanger au contrôleur DMA, qui se charge en retour de réaliser ces transferts et d'avertir quand ils sont effectués.
Même en utilisant un bus à haute performance, le microprocesseur risque
d'être fortement ralenti lorsqu'il accède à la mémoire centrale (RAM ou ROM). Pour limiter l'effet de ce goulot d'étranglement, un mécanisme de mémoire cache, identique à celui des microprocesseurs des ordinateurs, est
intégré dans les systèmes sur puce.
Cela consiste à insérer une mémoire très
rapide entre le CPU et la mémoire centrale afin de servir de tampon en lecture et en écriture.
Ainsi, lorsqu'une instruction doit accéder à la mémoire centrale, elle vérifie d'abord si la mémoire cache contient une copie des cellules qu'elle recherche. Si c'est le cas, alors elle écrit ou lit directement dans le cache.
Sinon, il se produit ce qu'on appelle un défaut de cache et les tâches à réaliser dans ce cas sont différentes selon qu'il s'agisse d'un accès en lecture où en écriture.
Accès en lecture. Un défaut de cache va provoquer un chargement des données de la mémoire centrale vers le cache, puis les données seront ensuite envoyées au CPU. Ce chargement nécessite de libérer de l'espace dans le cache pour accueillir les nouvelles données. Le choix de cet espace n'est pas anodin car le CPU pourrait avoir besoin de son contenu quelques instructions après. La politique de remplacement mise en œuvre dans une mémoire cache est donc un point important de son efficacité.
Accès en écriture. Si le cache ne contient pas la partie de la mémoire dans laquelle le CPU souhaite écrire, il commence par la charger puis elle est modifiée dans le cache. Pour qu'il y ait une cohérence entre la mémoire dans le cache et la mémoire centrale, une politique d'écriture est mise en place. Là encore, le choïx de cette politique a une grande influence sur l'efficacité du SoC.
Bien que l'architecture globale d'un SoC soit celle d'un microprocesseur RISC, le CPU peut également contenir des circuits dédiés à certaines opérations afin d'accélérer certains calculs. Par exemple, on peut trouver
aujourd'hui:
Une unité de calcul pour nombres flottants (FPU) simple où double précision.
Un circuit dédié aux opérations sur les matrices, par exemple pour accélérer les algorithmes d'apprentissage automatique (en anglais Machine Learning — ML) très utilisés par les applications d'intelligence artificielle. Ce circuit peut par exemple exploiter les possibilités de parallélisation des opérations sur ces structures.
Un composant pour la sécurité qui implémente des opérations élémentaires pour accélérer les algorithmes cryptographiques.
Enfin, la liste des périphériques présents dans un système sur puce n'est
limitée que par l'espace physique qu'ils occupent sur la puce.
Voici ci-dessous une liste non exhaustive des composants fréquemment présents sur de telles puces:
des modems (2G/3G/4G),
des circuits radio (WiFi, Bluetooth),
une puce GPS,
des ports d'entrée-sortie (USB, Ethernet, HDMI, Audio), des capteurs (CCD).
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Avantages et inconvénients
Les SoC sont aujourd'hui les composants
incontournables de l'informatique nomade (smartphones, tablettes). Ils sont également de plus en plus utilisés dans le monde plus large des systèmes embarqués (voitures, robots, etc).
Ce succès est dû aux avantages suivants:
Énergie. Il est admis qu'une grande partie de la consommation électrique d'un circuit est liée au câblage (et donc au transport) entre les composants. Comme tous les composants d'un SoC sont connectés entre eux sur des distances très petites, et de manière interne sans câblage énergivore, les gains sont très importants. Cette faible consommation énergétique implique aussi une très faible perte de chaleur qui évite de recourir à un ventilateur pour refroidir la puce. Les SoC sont donc silencieux.
Coût. Le prix d'un système sur puce est très petit si on le compare à celui d'une carte mère rassemblant les mêmes composants. Ceci s'explique d'une part par son coût de production, très bas, résultant de la forte automatisation du processus de fabrication des puces de l'industrie du hardware et, d'autre part, par des volumes de production importants.
Sécurité. Le circuit d'un SoC est conçu de manière globale, tant pour la partie hardware que pour celle du micro-logiciel (en anglais firmware), c'est-à-dire des programmes intégrés dans les différents composants qui leur permettent d'assurer leurs fonctions. Cette vue d'ensemble du système, sans aucune possibilité d'ajouter ou changer les composants, est un avantage important pour garantir la sécurité du système.
Bien qu'ayant de nombreux avantages, les systèmes sur puce ont aussi quelques inconvénients. Contrairement à un ordinateur équipé d'une carte mère, où chaque composant est connecté et peut être remplacé en cas de panne ou pour une nouvelle version, les SoC ne permettent pas de mise à jour, aucune extension n'est possible et, si un seul transistor est endommagé, il ne sera pas possible de réparer l'unité défaillante.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Exemple
Le système sur puce A13 bionic, disponible sur les téléphones et tablettes de la société Apple, est un SoC qui repose sur une architecture similaire à celle présentée dans cette section.
Cette puce électronique contient 8,5 milliards de transistors gravés avec une précision de 7 nanomètres.
La liste ci-dessous résume ses principaux composants:
Un CPU disposant d'un jeu d'instructions RISC et de registres de 64 bits. Il contient six cœurs dont deux rapides (2.65GHz) et quatre plus lents (1.8GH2), ces derniers étant plus économes en énergie. Les cœurs rapides intègrent un module (AMX) pour accélérer les opérations de Machine Learning comme les multiplications de matrices. Ensemble, ces modules sont capables d'effectuer 1000 milliards d'opérations 8 bits par seconde.
Un processeur graphique (GPU) avec quatre cœurs pour toutes les opérations 3D.
Un processeur avec huit cœurs, appelé Neural Engine, qui s'occupe de tous les traitements d'informations et calculs liés à l'intelligence artificielle, pour la reconnaissance faciale et la réalité augmentée notamment.
Un module spécialisé pour les opérations cryptographiques (AES).
Une capacité mémoire de 4Go de RAM, directement sur la puce.
","title":""},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Intel 4004
2050 Transitores
Gravure 10 micromètres
1971
Apple M1
16 milliards de transistors
Gravure 5 nanomètres
2020
La miniaturisation des circuits intégrés aujourd'hui est telle qu'il est possible d'avoir des ordinateurs sur une unique puce d'une centaine de millimètres carrés. Ces mini-ordinateurs. appelés microcontrôleurs ou System on Chip (SoC), intègrent tous les composants habituellement présents sur la carte mère d'un ordinateur (CPU, mémoire et périphériques).
Les microcontrôleurs, peu coûteux et avec des capacités de calculs et de mémoire limités, reposent souvent sur une architecture de Harvard où la mémoire des programmes est dissociée de
celle des données.
Les SoC sont de véritables ordinateurs avec des CPU très puissants, tellement rapides qu'une architecture avec deux bus est nécessaire pour éviter les goulots d'étranglement.
","title":"Conclusion","tagtitle":"h1"},{"edit":"
Mettre le résultat ici (code et figure).
"}],[{"text":"
Quels sont les spécificités et les avantages de Harvard par rapport à celle de Von Neumann?
","title":"Exercice"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":"
Dans une architecture de Harvard, les mémoires contenant les programmes et les données sont séparées.
Cela permet de transférer simultanément au CPU la prochaine instruction à exécuter ainsi que les données sur lesquelles elle doit opérer.
Par ailleurs, les bus de commu-
nication entre ces mémoires étant aussi distincts, cela permet d’avoir des
tailles de mots mémoire différents pour les deux mémoires.
"}],[{"text":"
Que signifie l'acronyme RISC?
","title":"Exercice"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":"
Cet acronyme signifie Reduced Instruction Set Computer et indique qu'un CPU dispose d’un jeu d’instructions plus simples et
plus uniformes qu’un CPU CISC (pour Compler Instruction Set Computer).
"}],[{"text":"
Quels sont les éléments essentiels d'un microcontrôleur?
","title":"Exercice","tagtitle":"h1"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":"
Un microcontrôleur est constitué d’une unité de calcul (CPU), de deux mémoires (programme et données) et de périphériques (comme des capteurs ou des convertisseurs analogique/numérique).
"}],[{"text":"
Donner quelques critères pour choisir d'utiliser un microcontrôleur.
","title":"Exercice"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":"
Les microcontrôleurs sont adaptés pour réaliser une tâche dans un systèmes embarqués qui ne nécessite pas de calculs trop intensifs ni trop complexes.
Leur petite taille fait aussi qu’ils peuvent être installés sans trop de difficultés dans n'importe quel endroit du système à contrôler.
Leur utilisation simplifie également la construction d’un système plus complexe car ils disposent à la fois d’une mémoire qui contient l'unique programme à exécuter, des bus de communication pour transférer des données et des capteurs nécessaires pour interagir avec leur environnement.
"}],[{"text":"
Quelles sont les spécificités et les avantages d'un système sur puce? So
","title":"Exercice","tagtitle":"h1"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":"
Un système sur puce est composé des mêmes éléments qu’un ordinateur.
La principale différence est qu’ils sont rassemblés sur le même circuit intégré.
Outre le gain de place, les distances réduites entre les composants et le fait qu'ils soient directement connecté dans la puce de silicium fait qu’ils consomment beaucoup moins d'énergie électrique.
Ainsi, ils ont une plus grande autonomie.
"}],[{"text":"
On souhaite écrire un programme Python qui affiche toutes les n secondes la température du CPU d'un ordinateur.
Pour cela, on peut utiliser la bibliothèque psutil qui donne accès à de nombreuses informations concernant les composants de la machine.
En particulier, la fonction sensors_temperatures() renvoie des données renvoyées par les sondes de températures sous la forme d'un dictionnaire.
Une entrée 'coretemp' de ce dictionnaire contient les informations de température pour le CPU rassemblées sous la forme d'un tableau qui décrit chaque cœur du CPU.
L'information d'un cœur est donnée sous forme d'un n-uplet nommé qui contient, entre autres, un champ current qui donne sa température actuelle.
Ainsi, si on appelle tempinfos ce dictionnaire, la température courante du premier cœur sera accessible par tempinfos['coretemp'][0].current .
Indication : Pour installer la bibliothèque psutil, il faut utiliser la commande suivante:
pip3 install psutil
","title":"Exercice"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":"
from time import sleep
import psutil
n = int (input (\"fréquence? \"))
whileTrue:
tempinfos = psutil.sensors_temperatures()
t = tempinfos['coretemp'][0].current
print(\"Température du coeur 0 : \", t)
sleep(n)
"}],[{"text":"
Compléter le schéma du SoC ci-dessus
","title":"Exercice"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":""}],[{"text":"
Parmi les images ci-dessus, la(les)quelle(s) représente(nt) un SoC.
","title":"Exercice"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":""}],[{"text":"
A partir de l'article du site elektormagazine.fr 1) Relevez les differentes caractéristiques du SoC du Raspberry Pi 3 modèle B+. 2) Les comparez au SoC du Raspberry Pi 4. 3) Quelles sont les principales évolutions qui contribuent à ce gain? Une copie de l'article est disponible ici.
","title":"Exercice"},{"edit":"
Mettre le résultat ici (code et figure).
"},{"solution":""}],[{"text":"
La photo ci-dessus montre le détail d'un SoC Kirin 990 Identifier les différentes parties de ce SoC
En poursuivant votre navigation sur mon site,
vous acceptez l’utilisation des Cookies et autres traceurs
pour réaliser des statistiques de visites et enregistrer
sur votre machine vos activités pédagogiques.En savoir plus.