Toute la vérité sur RTOS. Article # 10. Scheduler: fonctionnalités avancées et préservation du contexte



Dans un article précédent, nous avons examiné les différents types de planification pris en charge par le RTOS et les capacités associées dans Nucleus SE. Dans cet article, nous examinerons les options de planification supplémentaires dans Nucleus SE et le processus d'enregistrement et de restauration du contexte.

Articles précédents de la série:
Article # 9. Scheduler: implémentation
Article # 8. Nucleus SE: conception interne et déploiement
Article # 7. Nucleus SE: Introduction
Article # 6. Autres services RTOS
Article # 5. Interaction et synchronisation des tâches
Article # 4. Tâches, changement de contexte et interruptions
Article # 3. Tâches et planification
Article # 2. RTOS: Structure et mode temps réel
Article # 1. RTOS: introduction.

Fonctionnalités optionnelles


Pendant le développement de Nucleus SE, j'ai rendu facultatif le nombre maximum de fonctions, ce qui économise de la mémoire et / ou du temps.

Suspendre les tâches


Comme mentionné précédemment dans l'article Scheduler: Implementation , Nucleus SE prend en charge diverses options pour suspendre les tâches, mais cette fonctionnalité est facultative et est incluse par le symbole NUSE_SUSPEND_ENABLE dans nuse_config.h . Si la valeur est TRUE , la structure de données est définie comme NUSE_Task_Status [] . Ce type de suspension s'applique à toutes les tâches. Le tableau est de type U8 , où 2 quartets sont utilisés séparément. Les 4 bits inférieurs contiennent l'état de la tâche:
NUSE_READY, NUSE_PURE_SUSPEND , NUSE_SLEEP_SUSPEND , NUSE_MAILBOX_SUSPEND , etc. Si une tâche est suspendue par un appel d'API (par exemple, NUSE_MAILBOX_SUSPEND ), les 4 bits de poids fort contiennent l'index de l'objet sur lequel la tâche est suspendue. Ces informations sont utilisées lorsque la ressource devient disponible et pour appeler l'API, vous devez savoir laquelle des tâches suspendues doit être reprise.

Pour effectuer la suspension de tâche, une paire de fonctions de planificateur sont utilisées: NUSE_Suspend_Task () et NUSE_Wake_Task () .

Le code NUSE_Suspend_Task () est le suivant:



La fonction enregistre le nouvel état de la tâche (tous les 8 bits), obtenu en tant que paramètre suspend_code. Lorsque vous activez le verrouillage (voir «Verrouiller les appels API» ci-dessous), le code retour NUSE_SUCCESS est enregistré . Ensuite, NUSE_Reschedule () est appelé pour transférer le contrôle à la tâche suivante.

Le code NUSE_Wake_Task () est assez simple:



Le statut de la tâche est défini sur NUSE_READY . Si le planificateur de priorité n'est pas utilisé, la tâche en cours continue d'occuper le processeur jusqu'à ce que le moment soit venu de libérer la ressource. Si le planificateur de priorité est utilisé, NUSE_Reschedule () est appelé avec l'index de tâche comme indication de l'achèvement, car la tâche peut avoir une priorité plus élevée et doit être immédiatement exécutée.

Verrouiller les appels API


Nucleus RTOS prend en charge un certain nombre d'appels d'API avec lesquels un développeur peut suspendre (bloquer) une tâche si les ressources ne sont pas disponibles. La tâche reprendra lorsque les ressources seront à nouveau disponibles. Ce mécanisme est également implémenté dans Nucleus SE et est applicable à un certain nombre d'objets du noyau: une tâche peut être bloquée dans une section mémoire, dans un groupe d'événements, une boîte aux lettres, une file d'attente, un canal ou un sémaphore. Mais, comme la plupart des outils de Nucleus SE, il est facultatif et est défini par le symbole NUSE_BLOCKING_ENABLE dans nuse_config.h . S'il est défini sur TRUE , le tableau NUSE_Task_Blocking_Return [] est défini, qui contient le code retour pour chaque tâche; il peut s'agir de NUSE_SUCCESS ou du code NUSE_MAILBOX_WAS_RESET , indiquant que l'objet a été réinitialisé lorsque la tâche a été verrouillée. Lorsque le verrouillage est activé, le code correspondant est inclus dans les fonctions API à l'aide de la compilation conditionnelle.

Compteur du planificateur


Nucleus RTOS calcule le nombre de fois qu'une tâche a été planifiée depuis sa création et sa dernière réinitialisation. Cette fonctionnalité est également implémentée dans Nucleus SE, mais elle est facultative et est définie par le symbole NUSE_SCHEDULE_COUNT_SUPPORT dans nuse_config.h . S'il est défini sur TRUE , un tableau de NUSE_Task_Schedule_Count [] de type U16 est créé , qui stocke le compteur de chaque tâche dans l'application.

État initial de la tâche


Lorsqu'une tâche est créée dans Nucleus RTOS, vous pouvez sélectionner son état: prêt ou en pause. Dans Nucleus SE, par défaut, toutes les tâches sont prêtes au démarrage. L'option sélectionnée avec le symbole NUSE_INITIAL_TASK_STATE_SUPPORT dans nuse_config.h vous permet de sélectionner l'état de démarrage. Le tableau NUSE_Task_Initial_State [] est défini dans nuse_config.c et nécessite l'initialisation de NUSE_READY ou NUSE_PURE_SUSPEND pour chaque tâche de l'application.

Enregistrement du contexte


L'idée de maintenir le contexte d'une tâche avec n'importe quel type de planificateur, à l'exception de RTC (Run to Completion), a été présentée dans l'article # 3 «Tâches et planification». Comme déjà mentionné, il existe plusieurs façons de maintenir le contexte. Étant donné que Nucleus SE n'est pas conçu pour les processeurs 32 bits, j'ai choisi d'utiliser des tables, pas des piles, pour maintenir le contexte.

Un tableau à deux dimensions du type ADDR NUSE_Task_Context [] [] est utilisé pour enregistrer le contexte de toutes les tâches dans l'application. Les lignes sont NUSE_TASK_NUMBER (le nombre de tâches dans l'application), les colonnes sont NUSE_REGISTERS (le nombre de registres qui doivent être enregistrés; dépend du processeur et est défini dans nuse_types.h) .

Bien sûr, le maintien du contexte et la restauration du code dépendent du processeur. Et c'est le seul code Nucleus SE lié à un appareil spécifique (et à un environnement de développement). Je vais donner un exemple du code de sauvegarde / restauration du processeur ColdFire. Bien que ce choix puisse sembler étrange en raison d'un processeur obsolète, son assembleur est plus facile à lire que les assembleurs de la plupart des processeurs modernes. Le code est assez simple à utiliser comme base pour créer un changement de contexte pour d'autres processeurs:



Lorsque le changement de contexte est requis, ce code est appelé dans NUSE_Context_Swap. Deux variables sont utilisées: NUSE_Task_Active , l'index de la tâche en cours, dont le contexte doit être conservé; NUSE_Task_Next , l'index de la tâche dont vous souhaitez charger le contexte (voir la section Global Data).

Le processus de préservation du contexte fonctionne comme suit:

  • Les registres A0 et D0 sont temporairement stockĂ©s sur la pile;
  • A0 est configurĂ© pour pointer vers un tableau de blocs de contexte NUSE_Task_Context [] [] ;
  • D0 est chargĂ© Ă  l'aide de NUSE_Task_Active et multipliĂ© par 72 (ColdFire a 18 registres, nĂ©cessitant 72 octets pour le stockage);
  • puis D0 est ajoutĂ© Ă  A0 , qui pointe dĂ©sormais vers un bloc de contexte pour la tâche en cours;
  • puis les registres sont stockĂ©s dans le bloc de contexte; d'abord A0 et D0 (Ă  partir de la pile), puis D1-D7 et A1-A6 , puis SR et PC (Ă  partir de la pile, nous examinerons le changement de contexte rapidement initiĂ©), et Ă  la fin, le pointeur de pile est enregistrĂ©.

Le processus de chargement de contexte est la même séquence d'actions dans l'ordre inverse:

  • A0 est configurĂ© pour pointer vers un tableau de blocs de contexte NUSE_Task_Context [] [] ;
  • D0 est chargĂ© Ă  l'aide de NUSE_Task_Active , incrĂ©mentĂ© et multipliĂ© par 72;
  • puis D0 est ajoutĂ© Ă  A0 , qui pointe maintenant vers le bloc de contexte pour la nouvelle tâche (puisque le chargement du contexte doit ĂŞtre effectuĂ© dans le processus inverse de sauvegarde de la sĂ©quence, le pointeur de pile est d'abord requis);
  • puis les registres sont restaurĂ©s Ă  partir du bloc de contexte; d'abord, le pointeur de pile, puis PC et SR sont poussĂ©s sur la pile, puis D1-D7 et A1-A6 sont chargĂ©s, et Ă  la fin de D0 et A0 .

La difficulté de mise en œuvre du changement de contexte est que l'accès au registre d'état est difficile pour de nombreux processeurs (pour ColdFire, il s'agit de SR ). Une solution courante est l'interruption, c'est-à-dire l'interruption de programme ou l'interruption de branchement conditionnelle, qui provoque le chargement du SR sur la pile avec le PC . C'est ainsi que Nucleus SE fonctionne sur ColdFire. La macro NUSE_CONTEXT_SWAP () est définie dans nuse_types.h , qui s'étend à:
asm ("piège # 0");

Voici le code d'initialisation ( NUSE_Init_Task () dans nuse_init.c ) pour les blocs de contexte:



C'est ainsi que se produit l'initialisation du pointeur de pile, PC et SR . Les deux premiers ont des valeurs définies par l'utilisateur dans nuse_config.c . La valeur de SR est définie comme le caractère NUSE_STATUS_REGISTER dans nuse_types.h . Pour ColdFire, cette valeur est 0x40002000 .

Données globales


Le planificateur Nucleus SE nécessite très peu de mémoire pour stocker les données, mais, bien sûr, utilise des structures de données associées aux tâches, qui seront discutées en détail dans les articles suivants.

Données RAM


Le planificateur n'utilise pas les données situées dans la ROM et de 2 à 5 variables globales sont placées dans la RAM (toutes sont définies dans nuse_globals.c ), selon le planificateur utilisé:

  • NUSE_Task_Active - une variable de type U8 contenant l'index de la tâche en cours;
  • NUSE_Task_State - une variable de type U8 contenant une valeur qui indique l'Ă©tat du code en cours d'exĂ©cution, qui peut ĂŞtre une tâche, un gestionnaire d'interruption ou un code de dĂ©marrage; les valeurs possibles sont: NUSE_TASK_CONTEXT , NUSE_STARTUP_CONTEXT , NUSE_NISR_CONTEXT et NUSE_MISR_CONTEXT ;
  • NUSE_Task_Saved_State - une variable de type U8 utilisĂ©e pour protĂ©ger la valeur de NUSE_Task_State dans une interruption gĂ©rĂ©e;
  • NUSE_Task_Next - une variable de type U8 contenant l'index de la tâche suivante, qui doit ĂŞtre planifiĂ©e pour tous les planificateurs sauf RTC;
  • NUSE_Time_Slice_Ticks - une variable de type U16 contenant un compteur de tranches de temps; utilisĂ© uniquement avec le planificateur TS.

Empreinte des données du planificateur


Le planificateur Nucleus SE n'utilise pas de données ROM. La quantité exacte de données RAM varie en fonction du planificateur utilisé:

  • pour RTC - 2 octets ( NUSE_Task_Active et NUSE_Task_State );
  • pour RR et prioritĂ© - 4 octets ( NUSE_Task_Active , NUSE_Task_State , NUSE_Task_Saved_State et NUSE_Task_Next );
  • pour TS - 6 octets ( NUSE_Task_Active , NUSE_Task_State , NUSE_Task_Saved_State , NUSE_Task_Next et NUSE_Time_Slice_Ticks ).

Mise en place d'autres mécanismes de planification


Malgré le fait que Nucleus SE propose un choix de 4 planificateurs, couvrant la plupart des cas, l'architecture ouverte vous permet de mettre en œuvre des opportunités pour d'autres cas.

Tranche de temps avec tâche en arrière-plan


Comme déjà décrit dans l' article # 3, «Tâches et planification», le simple Time Slice Scheduler a des limites car il limite la durée maximale pendant laquelle un processeur peut exécuter une tâche. Une option plus sophistiquée consisterait à ajouter la prise en charge de la tâche d'arrière-plan. Une telle tâche peut être planifiée sur n'importe quel emplacement alloué pour les tâches suspendues et exécutée lorsque l'emplacement est partiellement libéré. Cette approche vous permet de planifier des tâches à intervalles réguliers et avec un pourcentage prévu du temps de cœur du processeur à terminer.

Priorité et Round Robin (RR)


Dans la plupart des cœurs en temps réel, le planificateur de priorités prend en charge plusieurs tâches à chaque niveau de priorité, contrairement à Nucleus SE, où chaque tâche a un niveau unique. J'ai donné la préférence à cette dernière option, car elle simplifie considérablement les structures de données et, par conséquent, le code du planificateur. Pour prendre en charge des architectures plus complexes, de nombreuses tables ROM et RAM seraient nécessaires.

À propos de l'auteur: Colin Walls travaille dans l'industrie électronique depuis plus de trente ans, consacrant la majeure partie de son temps au micrologiciel. Il est maintenant ingénieur firmware chez Mentor Embedded (une division de Mentor Graphics). Colin Walls intervient souvent lors de conférences et séminaires, auteur de nombreux articles techniques et de deux livres sur le firmware. Vit au Royaume-Uni. Blog professionnel de Colin , e-mail: colin_walls@mentor.com.

À propos de la traduction: cette série d'articles semblait intéressante en ce que, malgré les approches décrites obsolètes à certains endroits, l'auteur présente au lecteur mal formé les caractéristiques du système d'exploitation en temps réel. J'appartiens moi-même à l'équipe de créateurs du RTOS russe , que nous avons l' intention de rendre gratuit , et j'espère que le cycle sera utile aux développeurs novices.

Source: https://habr.com/ru/post/fr423967/


All Articles