Avant d'aller plus loin, consultez le petit tableau disponible au lien ci-après pour voir les principales différences (en ce qui concerne le type de programmation que nous faisons en 420 231 ) entre la norme pré-ISO (supportée par Borland C 3.1) et celle, plus récente, à laquelle vous êtes habituée...
La norme antérieure de C++: ce que vous devez savoir...De même, vous trouverez du support important pour votre TP2 à travers le lien ci-dessous...
Utiliser l'interruption "21h": ce qu'il faut savoirOn vous a enseigné les structures de contrôle fondamentales de la séquence, de l'alternative et de la répétitive dans votre cours d'introduction à la programmation. Toutefois, ces structures ne tiennent que lorsque votre programme est en contrôle de son flot d'exécution.
Il se produit pourtant des tas de choses qui, dans un ordinateur, sont indépendantes de votre programme:
On nomme généralement ces bris de séquence événements lorsqu'ils ont trait à la multiprogrammation, et interruptions lorsqu'elles ont trait au matériel ou aux services de très bas niveau du système d'exploitation.
Une interruption au sens informatique n'est pas un événement externe et fortuit comme un appel téléphonique dérangeant l'usager pendant son travail (en anglais, un tel événement serait une "interruption", alors que les interruptions dont nous discutons ici seraient des "Interrupt"). La nuance tient du fait que les interruptions auxquelles nous sommes intéressés sont des composantes essentielles au bon fonctionnement d'un ordinateur.
Il existe des interruption matérielles et des interruptions logicielles (certains parleront d'interruptions externes et internes, respectivement).
Les interruptions matérielles (ou externes, comme dans externes au processeur) sont des signaux émis par des composantes physiques qui brisent le flot d'exécution du système d'exploitation de telle sorte qu'il doive s'interroger à savoir quoi faire pour bien leur répondre. Les tics d'horloge, les données arrivant de la souris ou du clavier, un signal du disque rigide signifiant que celui-ci est prêt à transférer des données peuvent tous être source d'interruptions. On dira "Interrupt Request" (IRQ) lorsqu'on voudra parler de l'interruption identifiant une composante donnée. |
Les interruptions logicielles (ou internes, comme dans internes au processeur) sont générées volontairement par un processus en exécution (un de vos programmes, par exemple), donc à même le processeur. On compte parmi celles-ci toute la gamme des erreurs (instructions invalides, division par zéro, erreurs d'accès à la mémoire, ...). Ce qu'on appelle les services du système d'exploitation sont des fonctions de bas niveau auxquelles correspondent certaines interruptions, et qui permettent d'accomplir certaines tâches (afficher un pixel à l'écran, redémarrer l'ordinateur, ce genre de truc). L'endroit en mémoire où se trouvent les bouts de code spécifiquement dédiés au traitement des interruptions se nomme vecteur d'interruptions. |
Choisir des IRQ qui n'entrent pas en conflit fait partie intégrante d'une installation matérielle qui fonctionne (des IRQ en conflit empêchent simplement l'ordinateur de fonctionner).
Lorsqu'une composante matérielle a besoin des services du processeur, elle le signalera en produisant une interruption. Le processeur traitera celles-ci en fonction de leur importance relative à l'ensemble du système.
Un exemple: lorsqu'une imprimante termine une tâche d'impression, elle envoie un signal d'interruption. Ce signal interrompt momentanément le traitement en cours dans le processeur de telle sorte que celui-ci peut prendre une décision à savoir quoi faire ensuite.
Du fait que de multiples signaux vers l'ordinateur sur une même "ligne[1]" d'interruption pourraient ne pas être bien distingués les uns des autres à la réception, un IRQ unique est spécifié pour chaque composante et pour le chemin entre celle-ci et l'ordinateur en soi.
Pour voir la configuration des IRQ et des composantes matérielles sur un PC Windows95+, passez d'abord par Démarrer --> Paramètres --> Système. Allez à l'onglet "Gestionnaire des périphériques" et vous obtiendrez un dialogue similaire à celui ci-après: |
|
||
|
Sur le dialogue tel que présenté, on aperçoit à peu près ce qui se trouvait sur le poste de travail familial chez votre aimable professeur au moment de la saisie d'écran. Vous y voyez plusieurs rubriques, soit une pour chaque composante installée et reconnue par Windows95+. Celle en relief est le port de communication COM2, dont nous allons examiner ensembles les propriétés telles que présentées par Windows. |
||
|
Lorsque les propriétés du COM2 nous sont présentées, on aperçoit plusieurs onglets. Portez votre attention sur l'onglet "Resources". La rubrique "Interrupt Request" indique l'IRQ associé au COM2 sur l'ordinateur en question. On trouve ainsi l'IRQ associée à chaque composante matérielle installée sur un PC. Ces composantes fonctionnant toutes par interruption, il est important que les IRQ attribuées n'entrent pas en conflit d'une composante à l'autre. |
Autre détail: les contrôleurs d'unités de disque. Certaines technologies de contrôleurs (les contrôleurs IDE, par exemple) peuvent servir de porte d'accès à plus d'une unité de disque à la fois, économisant (entre autres choses) des connexions IRQ.
La technologie "Plug and play"MD est en fait un standard offrant aux usagers la capacité de brancher une composante dans un ordinateur et de lui laisser la reconnaître par lui-même, sans intervention de l'usager en tant que tel.
L'avènement de la technologie "Plug and play"MD, que l'on retrouve par exemple intégrée à Windows95+ (et depuis longtemps sur les ordinateurs Macintosh), cherche à éviter à un usager d'avoir à prendre conscience des IRQ et à lui éviter de devoir les déterminer "manuellement", lui-même, lors de l'ajout d'une nouvelle composante matérielle à un ordinateur.
L'ajout d'une composante n'étant pas "Plug and play"MD est plus délicate, demandant une certaine prudence pour éviter les conflits IRQ (un conflit naît de deux composantes se voyant attribuer une même "ligne"). Normalement, le manufacturier offrira des directives pour la bonne allocation d'un IRQ plausible pour la composante. Si ces directives sont absentes, ou si un doute subsiste, appeler le support technique de la compagnie manufacturant la composante est souvent sage. Sinon, il faut y aller à tâtons, et chercher à éviter les conflits en notant l'attribution des IRQ au fur et à mesure.
Pour plus d'informations sur cette technologie (pour Windows95+), consultez:
http://zenon.airtime.co.uk/users/hal9000/pnp.htm |
|
Les PICSur les vieux PC, on trouvait une seule puce (Programmable Interrupt Controller, ou PIC) dédiée à la gestion des interruptions matérielles. Ce contrôleur pouvait arbitrer jusqu'à huit (8) requêtes (IRQ) distinctes, qui étaient gérées en fonction de leur numérotation (de 0 à 7). L'horloge du système se vit accorder sa ligne dédiée (IRQ 7). On fit de même pour le contrôleur du clavier (IRQ 1). |
Lorsqu'on réalisa que huit "lignes" d'interruptions (huit IRQ) était insuffisant, on ajouta un deuxième contrôleur qui, lui, relaye ses propres interruptions au premier à travers... une ligne d'interruption (IRQ 2).
Les IRQ sont traités en ordre de priorité, et les priorités sont fonction de la proximité physique des lignes IRQ avec le processeur (les plus près physiquement ont priorité sur les autres, tout simplement).
Le traitement des interruptions demandant un peu de temps (cela va de soi) mais étant aussi extrêmement important, il est requis de ne faire, pendant le traitement d'une interruption, que le strict minimum.
Exemple: Une interruption est générée par un tic d'horloge. Si on met au point le code à exécuter lors d'un tic d'horloge, il faut que celui-ci puisse s'exécuter en entier durant le laps de temps entre deux tics, et avec un petit coussin (au cas où d'autres interruptions interféreraient en chemin), sinon la prochaine interruption d'horloge aura lieu avant la complétion de la précédente (et là, les erreurs vont se mettre à débouler!)
La plupart des cartes qu'on peut acheter et installer sur une carte maîtresse ne demandent qu'une seule ligne IRQ. Parfois, même une seule ligne s'avère être beaucoup demander. Sur un ordinateur à 16 lignes IRQ, seulement 5 sont toujours disponibles, et seulement 3 sont souvent disponibles.
Les autres sont déjà réservées, en pratique, par des circuits sur la carte maîtresse ou par des ajouts communs comme les ports série, le clavier et les unités de disquette.
L'allocation habituelle des IRQ est comme suit:
IRQ |
Disponibilité |
Utilisation normale |
---|---|---|
0 |
Jamais |
Minuterie du système (à 18,2 Hertz[2]) |
1 |
Jamais |
Clavier (signaux du clavier) |
2 |
Jamais |
Vidéo; lié en cascade par IRQ 9. |
3 |
Rarement |
COM2 et COM4 (si le COM2 est déjà utilisé, localiser le COM4 peut demander de l'imagination) |
4 |
Rarement |
COM1 et COM3 (voir la note sur COM4, plus haut). |
5 |
Souvent |
LPT2 (si un ordinateur n'utilise pas LPT2, cet IRQ peut fort bien servir pour un COM3 par exemple...) |
6 |
Jamais |
Contrôleur de l'unité de disquette |
7 |
Souvent |
LPT1. Plusieurs pilotes d'imprimante n'utilisent même pas LPT1, en fait; lorsque les ressources se font rares, certaines imprimantes fonctionneront malgré un IRQ de LPT1 désactivé. |
8 |
Jamais |
Horloge temps réel (à 1 Kilohertz) |
9 |
Jamais |
Redirigé à IRQ 2 |
10 |
Toujours |
Aucun |
11 |
Toujours |
Aucun |
12 |
Souvent |
Si cet IRQ est utilisé, c'est pour une souris connectée au bus ("bus mouse") |
13 |
Souvent |
Si cet IRQ est utilisé, ce sera normalement pour un co-processeur mathématique. |
14 |
Souvent |
Disque rigide (IDE 1), certains contrôleurs (surtout SCSI) |
15 |
Habituellement |
Si cet IRQ est utilisé, ce sera généralement par un lecteur de CD-ROM IDE(IDE 2) |
La plupart des ordinateurs sont (bien que ce nous soit à peu près transparent) dirigés par interruptions. Le flot d'exécution d'un programme suit la liste de ses instructions jusqu'à ce que le programme se termine ou jusqu'à ce qu'il se produise une interruption. Une fois l'interruption produite, l'ordinateur retournera au flot d'exécution du programme en cours, ou encore démarrera l'exécution d'un autre programme.
Un processeur, grossièrement, ne peut traiter qu'une seule instruction à la fois[3]. Toutefois, le flot d'exécution d'un programme pouvant être interrompu, il est possible pour un processeur d'exécuter "plusieurs programmes à la fois", à toutes fins pratiques. Un seul processeur peut donc supporter un système d'exploitation multitâche, et il est possible pour un programme d'accomplir une série de calculs pendant qu'un usager entre des commandes au clavier.
Note: | le truc est que les processeurs calculent tellement rapidement que le fait qu'un système d'exploitation alloue à chaque processus en cours d'exécution une petite tranche de temps donne l'apparence qu'ils s'exécutent tous de façon purement concurrente. |
Une interruption est un événement qui demande un certain traitement. Puisqu'une interruption demande un traitement, il faut consacrer une petite tranche de temps à sa gestion. Puisqu'il faut lui consacrer du temps, il devient possible qu'une interruption se produise pendant qu'une autre est en cours de traitement.
Les interruptions étant généralement des événements importants, il ne faut pas en manquer une (il pourrait y avoir des conséquences fâcheuses).
Un système d'exploitation offre normalement un système de gestion et d'ordonnancement des interruptions. Ainsi, il peut garder en note (dans une file d'attente, par exemple) les interruptions encore à gérer, et prendre des décisions à savoir laquelle de celles encore à gérer doit être considérée comme étant la plus prioritaire.
Il va de soi qu'une fois le stade de l'installation et de la configuration matérielle passée, les tâches d'un(e) informaticien(ne) relatives aux interruptions tendent à devenir des tâches de programmation. Ainsi, regardons un peu de quoi ce monde un peu particulier a l'air.
Cette section offre quelques exemples propres à la manipulation et à la gestion des interruptions sur un PC DOS[4].
La bonne nouvelle (oui, oui!) est qu'il est possible pour vous de programmer en utilisant quelque chose d'aussi près de la machine que les interruptions.
La mauvaise nouvelle (oui, oui!) est que pour ce faire, vous devrez quitter l'environnement Visual C++ pour quelques temps.
Pour être en mesure de saisir et traiter les interruptions, il faut que votre programme soit en contrôle du processeur. Visual C++ ne vous donnera pas un niveau de contrôle suffisant pour ce faire.
Nous allons donc retraiter vers Borland C++ 3.1 (BC3.1) pour une courte période.
BC3.1 supporte une norme antérieure du langage C++. Ce qui suit détaille les principales différences entre le C++ que vous connaissez et son prédécesseur (il y a d'autres différences, mais elles touchent des points que vous n'avez pas encore rencontré dans votre formation).
Norme courante |
Norme antérieure |
---|---|
#include <iostream> using namespace std; |
#include <iostream.h> |
struct NomStruct { typeMembres nomMembres; }; |
typedef struct { typeMembres nomMembres; } NomStruct; |
enum Nom { elem1, elem2, elem3 }; |
typedef enum { elem1, elem2, elem3 } NomEnum; |
Le type "bool" existe à même le langage |
Le type "bool" doit être défini si on veut s'en servir: typedef enum { false, true } bool; |
Pour utiliser les brins de code qui suivent, quelques éléments de savoir doivent se greffer à vos compétences courantes. Ça peut paraître subtil, mais il faut porter attention et bien saisir le tout.
La plupart des concepts et mots clé suivants ne sont pas aussi critiques à votre formation que le reste de ce qui est présenté dans ce document (ou dans le cours en général). Nous avons quand même choisi de vous les présenter dans le but de répondre "simplement" aux questions que vous pourriez vous poser à leurs sujets.
En C++, comme en C d'ailleurs, il est possible de gérer des fonctions au nombre de paramètres variable. La manière de noter une telle fonction est d'utiliser comme dernier paramètre à l'appel une ellipse ("..."). Le programme est responsable de gérer les types et détails associés aux paramètres ainsi passés.
En langage C, les fonctions de base d'entrée/ sortie ("printf" et "scanf") faisaient un usage systématique des ellipses. En C++, avec "cin" et "cout", l'utilisation d'ellipses a fortement diminué. Toutefois, les fonctions gérant des interruptions doivent avoir un nombre de paramètres variables, et c'est pourquoi la notation elliptique apparaît dans le code en exemple ci-après.
En pratique, tout ce que vous avez besoin de savoir ici est que les ellipses dans le code en exemple sont nécessaires; vous n'avez pas besoin de gérer les paramètres variables vous-mêmes (c'est une simple question de respect de prototypes).
Il est possible, en C comme en C++, d'utiliser des pointeurs de fonction comme on utilise des pointeurs d'entiers ou des pointeurs de "CEleve" (la différence est seulement sur le plan de la syntaxe).
Exemple: |
---|
#include <iostream.h> int f (int); // f est une fonction prenant en paramètre un "int" int (*g) (int); // g un pointeur à une fonction prenant en // paramètre un "int" // ptrFct est un type dont chaque variable est un pointeurs sur // une fonction prenant en paramètre un "int" typedef int (*ptrFct) (int); int main () { ptrFct p= &f; g = &f; cout << (*g) (4)<< ','<< (*p) (8) << endl; // imprime 9,13 } int f (int a) { return (a+ 5); } // f est une fonction prenant en paramètre un "int" |
La déclaration d'un pointeur de fonction diffère légèrement de celle d'une fonction (notez les parenthèses autour de "*g" dans l'exemple: elles sont nécessaires pour s'assurer que le compilateur voit "int(*g)()" plutôt que "(int*)g()").
L'appel via un pointeur de fonction est aussi fort similaire: plutôt que "f(4)", le code en exemple appelle "(*g)(4)", donc le contenu pointé par "g" est une fonction prenant en paramètre un "int".
On peut définir un type "pointeur à une fonction prenant en paramètre un "int"" comme le montre la définition du type "ptrFct" dans l'exemple. Notez la déclaration de la variable "p" de type "ptrFct", et son utilisation dans le "cout"...
On aura parfois besoin de pointeurs de fonction pour gérer les interruptions, du fait qu'on voudra remplacer la fonction gérant habituellement l'interruption par une de notre cru.
Toutefois, il n'est pas requis ici de bien maîtriser la syntaxe détaillée et la manipulation des pointeurs de fonction. Se fier aux exemples plus bas sera suffisant pour le moment.
Les fonctions remplaçant un gestionnaire d'interruption doivent être déclarées avec le mot clé "interrupt" (voir les exemples plus bas), sinon ça ne fonctionnera pas.
Un programme bien écrit devrait s'assurer que l'ordinateur retombe dans un état opérationnel une fois son exécution terminée.
Les programmes prenant le contrôle des interruptions ont le devoir d'être particulièrement rigoureux en ce sens: mal écrits, ils rendent l'ordinateur inutilisable à toutes fins pratiques.
C'est pourquoi chaque bout de code en exemple remplaçant une fonction de traitement d'interruption par une autre, plus bas, s'assurera de respecter le pseudo-code suivant:
soit "I" le numéro de l'interruption à capturer soit "f" la fonction gérant normalement "I" soit "f+" votre fonction pour gérer "I" ALGO remplacementDeLInterruption ancien <-- adresse de "f" mettre "f+" à la place de "f" // votre programme s'exécute remettre "f" à la place de "f+" FIN Algo |
Durant l'exécution de votre programme, c'est donc votre gestionnaire d'interruption qui aura le contrôle. Une fois l'exécution de votre programme terminée, le gestionnaire qui était en contrôle précédemment sera remis en place, et tout se poursuivra normalement.
Parfois, lorsqu'on sait qu'une variable va changer de valeur très fréquemment, et pas nécessairement du à la séquence d'exécution normale du programme (par exemple, quand une variable va être modifiée dans une fonction appelée lors d'une interruption, comme vous en verrez dans les exemples plus bas), il est de mise de souligner au compilateur de ne pas chercher à effectuer sur elles des optimisations trop agressives.
Le mot clé "volatile" (comme dans "volatile int unCompteur;" par exemple) existe pour cette raison. Son seul rôle est de dire au compilateur que la variable est de nature à changements fréquents de valeur.
La combinaison de touches "MAJ + Impr écran" ("Shift + Print Screen") génère une interruption (la 5h, ou 0x05, en fait, selon la documentation du PC). Si un programme voulait se saisir de ce moment bien précis, il devrait donc réagir à la place du code gérant normalement cette interruption lorsque cette combinaison bien précise de touches est pressée.
Le code suivant (annoté: voir plus bas pour les détails) fait précisément ce travail.
#include <iostream.h> // du vieux code! #include <dos.h> // le symbole __cplusplus est défini à la précompilation lorsque // le compilateur est un compilateur "C++", pas "C" #ifdef __cplusplus // pour du code C++ #define __CPPARGS ... #else // pour du code C #define __CPPARGS #endif const int INTERRUPTION= 0x05; // (0) int boucle= 1; // (1) void interrupt Nouveau (__CPPARGS) // (2) { boucle= 0; } // Nouveau () int main(void) { void interrupt (*Precedent) (__CPPARGS); // (3) cout << "Appuyez <Shift><PrtSc> pour terminer"; Precedent= _dos_getvect (INTERRUPTION); // (4) _dos_setvect(INTERRUPTION, Nouveau); // (5) while (boucle); // (6) _dos_setvect(INTERRUPTION, Precedent); // (7) cout << "Voila!"; } // main () |
Les notes dans le code exemple signifient ce qui suit:
Pendant la durée de vie de notre programme, lorsque les touches provoquant l'interruption seront pressées, la fonction appelée sera celle que nous avons imposé à la ligne (5). Ainsi, la variable "boucle" changera de valeur et permettra au programme de se conclure normalement.
Note: |
dans cet exemple, la fonction gérant normalement ces touches (celle qui est remplacée) ne sera pas du tout appelée. Il serait possible de modifier légèrement ce code pour que notre programme et la fonction originale soient tous deux appelés, en séquence, si cela s'avérait préférable. L'exemple suivant vous montre comment, avec une autre interruption. |
Compter avec une relative précision la durée de vie d'un programme peut se faire via les interruptions. On peut "remplacer" le compteur normal de temps lors d'un tic d'horloge par une fonction de notre cru, tout en s'assurant que le processus normal de gestion d'un tic d'horloge soit maintenu.
Note: | l'horloge du système (pas l'horloge en temps réel) tique environs 18,2 fois par seconde. |
Le code en exemple est:
#include <iostream.h> #include <dos.h> const int INTERRUPTION_HORLOGE = 0x1c; const long TICS_PAR_SECONDE = 18; // approximation... volatile long secondes= 0; // (0) volatile long totalTics= 0; // (1) void interrupt (*ancien) (...); // (2) void interrupt compterLesTics(...) { totalTics++; // (3) if ((totalTics% TICS_PAR_SECONDE) == 0) { secondes++; // (4) } (*ancien)(); // (5) } const int TEMPS_A_COMPTER= 30; // nombre de secondes de vie int main () { long temps= 0; // variable locale ancien= _dos_getvect (INTERRUPTION_HORLOGE); _dos_setvect (INTERRUPTION_HORLOGE, compterLesTics); while (temps < TEMPS_A_COMPTER) { if (secondes != temps) // afficher le temps qui passe { cout << secondes << endl; temps = secondes; } } _dos_setvect (INTERRUPTION_HORLOGE, ancien); } // main () |
Que fait ce programme? Il compte le nombre approximatif de secondes passées depuis son propre démarrage, et affiche le nombre de secondes passées à chaque seconde qui passe.
Notez l'utilisation d'une variable locale ("temps") qui se "souvient" de la dernière fois que le programme a affiché le nombre de secondes passées. C'est grâce à lui que le temps s'affiche correctement (si vous n'êtes pas convaincu(e)s, essayez de programmer le tout sans cette variable, vous serez alors en mesure de comparer).
Remarquez que la séquence de remplacement et de restauration du gestionnaire d'interruption est à peu près la même que dans l'exemple précédant. Pour la plupart des interruptions, ce modèle est la norme.
Les notes (0) et (1) vous présentent des variables "volatiles". Remarquez que la fonction utilisée comme gestionnaire d'interruption est celle qui les modifie. L'interruption de l'horloge est un événement fréquent et qui doit être géré rapidement; l'utilisation de cet indicatif de volatilité y est donc approprié.
La note (2) indique que dans ce cas, nous avons opté d'utiliser une variable globale comme pointeur de fonction pour garder trace du gestionnaire d'interruption original. C'est qu'à la note (5), une fois que notre gestion d'interruption est terminée, nous laissons le gestionnaire original faire son propre travail (au cas où ce serait important). De cette façon, dans la mesure où notre gestion est courte et efficace, le reste du système ne verra que du feu à notre manoeuvre.
À la note (3), remarquez qu'un compteur ("totalTics") est incrémenté à chaque tic d'horloge, tandis qu'à la note (4) un autre compteur ("secondes") n'est augmenté qu'à chaque "TICS_PAR_SECONDE" tics d'horloge.
Question piège: |
---|
Y a-t-il moyen d'améliorer la précision de ce compteur de temps qui passe? Si oui, comment? Si non, pourquoi? |
Une interruption ressort du lot, sur un PC DOS, et c'est l'interruption "21h" (ou encore "0x21"), soit celle propre aux services du système d'exploitation.
À l'appel de l'interruption "21h", la valeur contenue dans le registre "AH" est examinée, et représente un numéro de fonction. Le travail accompli par l'interruption dépendra donc de la valeur de ce registre au moment où l'interruption aura été provoquée.
Puisque l'interruption "21h" est provoquée par logiciel, notre programme en devient responsable, et elle se gère plus comme un appel de fonction que comme un événement perturbateur. À un tel point que BC3.1 offre une fonction qui en fait clairement l'appel: la fonction "intdos()".
Pour faire usage de l'interruption "21h" avec BC3.1, il convient de faire attention aux détails suivants:
La fonction "intdos()" est le moyen privilégié, avec BC3.1, d'utiliser l'interruption "21h" ("0x21"). Elle prend en paramètre deux "union REGS*" (voir plus loin), soit un en entrée, et l'autre en sortie.
Par exemple:
union REGS registre1, registre2; int valeurDeRetour; // ... préparation pour l'appel ... // ici, on utilise le même "union REGS" en entrée et en sortie valeurDeRetour = intdos (®istre1, ®istre1); // ici, on utilise deux "union REGS" différents ... valeurDeRetour = intdos (®istre1, ®istre2); |
Je vous invite à consulter l'aide de BC3.1 si vous voulez en savoir plus sur les valeurs de retour possibles pour cette fonction.
Le type "union REGS" (il faut l'utiliser comme tel; il n'est malheureusement pas déclaré gentiment avec un "typedef") représente un ensemble de registres utilisés dans le cadre d'un appel à l'interruption "21h".
Vous pouvez examiner le type "union REGS" à l'aide de l'aide en ligne de BC3.1 (en fait, nous vous y invitons). Vous verrez par exemple qu'on peut faire comme suit pour accéder à ce qui y représente le registre "AX":
union REGS unRegistreREGS; unRegistreREGS.x.ax = 12; // met la valeur 12 dans "AX" unRegistreREGS.h.ah = 6; // met la valeur 6 dans "AH" |
Pour faire un appel à l'interruption "21h", il est de mise de préparer correctement le "union REGS" utilisé en entrée.
Par exemple:
const int LA_FONCTION = /* numéro de la fonction désirée */; int fonction uneFonction () { union REGS registreEnEntree; union REGS registreEnSortie; // si nécessaire registreEnEntree.h.ah = LA_FONCTION; // (0) intdos(®istreEnEntree, ®istreEnSortie); // (1) // traiter les valeurs de "registreEnSortie" // (2) } // uneFonction() |
À la ligne (0), l'action de déposer le numéro de la fonction désirée dans ce qui représente le registre "AH" de "registreEnEntree" fait en sorte que l'appel à l'interruption "21h" fera le travail qu'on s'attend d'elle.
L'appel en tant que tel se trouve à la ligne (1). Ceci est équivalent à faire, en assembleur, un appel à l'interruption "21h" après avoir mis la valeur "LA_FONCTION" dans le registre "AH".
La ligne (2) est laissée délibérément vague. Chaque fonction (chaque service) de l'interruption "21h" dépose des valeurs ayant un sens spécifique au service lui-même dans différents registres; il faut s'en remettre à la documentation appropriée pour savoir quels registres lire (quels membres de "registreEnSortie", ici) et quel sens attribuer à leurs valeurs respectives.
L'interruption "21h" regroupe un ensemble de services mis à la disposition des programmes par le système d'exploitation DOS.
La liste est du domaine public (on la trouve sûrement sur Internet, entre autres), mais regroupe des fonctions pour allumer un pixel d'une certaine couleur à l'écran, lire un code produit par le clavier, et ainsi de suite.
La programmation par interruption produit, lorsque bien réalisée, du code très performant. On ne fait pas de très grandes applications entièrement en programmation par interruptions, comme on ne fait pas de larges systèmes entièrement en assembleur.
On n'écrit pas non plus d'applications se voulant portables en misant largement sur la programmation par interruptions: si on prend par exemples les services de l'interruption "21h", ou si on regarde les autres interruptions qui sont très dépendantes du matériel ou de la plate-forme, ce code se prête mal aux migrations d'une plate-forme à l'autre.
On utilise par contre des techniques de programmation par interruptions lorsqu'on doit gérer efficacement un périphérique sur une plate-forme donnée; ou si on rédige une application exigeante sur le plan de la performance (comme un jeu vidéo ou une application médicale, par exemple).
Il faut par contre être prudent, et s'assurer que le code gérant une interruption soit clair, simple et surtout court.
Prenez par exemple le code exécuté à chaque tic d'horloge: il doit prendre une tranche de temps significativement plus courte qu'un dix-huitième de seconde à s'exécuter, sinon:
Dans un cas comme dans l'autre, on parle de problèmes majeurs!
Note finale: | sauvegardez souvent votre travail lorsque vous vous trouvez en situation de devoir programmer par interruptions. Les bugs produits tendent à rendre votre poste de travail inutilisable, et il est triste de perdre de larges efforts à cause d'une simple imprudence... |
[1] ... par analogie avec une ligne de téléphone, par exemple.
[2] Un Hertz (1Hz) signifie "une fois par seconde". Un Kilohertz (1 KHz) signife "mille fois par seconde". La notation hertzienne est donc fort utile pour marquer des fréquences. Par exemple: un système devant rafraîchir son image vidéo 16 fois par seconde (pour que l'oeil humain voit une image continue) devra avoir une fréquence de rafraîchissement de 16 Hertz (16Hz)
[3] ... quoiqu'en pratique, ce ne soit pas tout à fait vrai, mais nous pouvons nous permettre de simplifier ainsi le propos pour l'instant.
[4] Pour des raisons pratiques, nous allons nous débarrasser de Windows: ce système d'exploitation cherche à garder le contrôle sur certains éléments dont nous voudrons nous-mêmes prendre le contrôle; chercher à travailler dans Windows malgré tout nous mènerait à des conflits dont nous n'avons vraiment pas besoin.