PROCEDE D'ORDONNANCEMENT AVEC CONTRAINTES D'ECHEANCE, EN PARTICULIER SOUS LINUX, REALISE EN ESPACE UTILISATEUR
L'invention porte sur un procédé permettant la réalisation d'un ordonnancement monoprocesseur, multiprocesseur ou muiticœurs de tâches avec contraintes d'échéances. Ordonnanceur de tâches avec contraintes d'échéances signifie que chaque tâche possède un temps de terminaison qu'elles ne doit pas dépasser. L'ordonnancement réalisé supporte l'exécution sur un seul ou plusieurs processeurs. Il est réalisé sous Linux, ou tout autre système d'exploitation supportant la norme POSIX, et plus particulièrement son extension POSIX.1c, mais n'est pas intégré au noyau : il fonctionne en effet en espace utilisateur.
L'espace utilisateur est un mode de fonctionnement pour les applications utilisateur, par opposition à l'espace noyau qui est un mode de fonctionnement superviseur qui dispose de fonctionnalités avancées ayant tous les droits, en particulier celui d'accéder à toutes les ressources d'un microprocesseur. La manipulation des ressources est néanmoins rendue possible en espace utilisateur par des fonctionnalités dites API (« Application Programming Interface », c'est-à-dire Interfaces de Programmation d'Applications), qui elles-mêmes s'appuient sur l'utilisation de pilotes périphériques. Le développement d'une solution en espace utilisateur rend possible et facilite le développement d'ordonnanceurs à contraintes temporelles par rapport à une intégration directe dans le noyau du système d'exploitation qui est rendue très difficile par sa complexité. Un autre avantage est d'apporter une stabilité accrue car une erreur d'exécution en espace utilisateur n'entraîne pas de conséquence grave pour l'intégrité du reste du système.
Pour réaliser un ordonnancement en espace utilisateur, le procédé de l'invention s'appuie en particulier sur les mécanismes définis par la norme POSIX {« Portable Operating System Interface - X », c'est-à-dire « interface portable pour les systèmes d'exploitation », le « X » exprimant l'héritage Unix) et son extension POSIX.1 c, et notamment sur la structure de tâche POSIX, les threads POSIX (« pthreads ») et les API de gestion de tâche POSIX. Il convient de rappeler qu'un processus est une instance de programme en cours d'exécution, une « tâche » est un découpage d'un processus et un
« thread » est la réalisation d'une tâche sous Linux aux moyen des API POSIX ; un thread ou tâche ne peut pas exister sans processus, mais il peut y avoir plusieurs threads ou tâches par processus. On pourra se rapporter à ce propos à l'ouvrage de Robert Love « LINUX System programming », O'ReiNy, 2007.
Linux est un système où la gestion du temps est dite à « temps partagé », par opposition à une gestion « temps réel ». Dans ce type de gestion, plusieurs facteurs liés au fonctionnement du système comme le partage des ressources, la gestion des entrées/sorties, les interruptions, l'influence de la mémoire virtuelle, etc. entraînent des incertitudes temporelles sur les temps d'exécution des tâches. Dès lors, il n'est pas possible de garantir une solution temps réel « dur » (en anglais « hard real time »), c'est-à-dire où l'exécution de chaque tâche est remplie dans des limites temporelles strictes et inviolables. La réalisation de l'ordonnanceur proposé convient néanmoins pour des systèmes temps réel dit « souple » (en anglais « soft real time »), ou « avec contraintes d'échéance », qui acceptent des variations dans le traitement des données de l'ordre de la seconde au maximum. C'est le cas d'un grand nombre d'applications temps réel, par exemple multimédia.
N'étant pas nativement un système temps réel, plusieurs solutions techniques sont cependant possibles pour rendre ie noyau Linux compatible avec les contraintes temps réel. La solution la plus courante consiste à lui associer un noyau temps réel auxiliaire disposant d'un véritable ordonnanceur temps réel (RT Linux, RTAI, XENOMAI). Ce noyau temps réel auxiliaire est prioritaire et traite directement les tâches temps réel. Les autres tâches (non temps réel) sont déléguées au noyau standard Linux qui est considéré comme une tâche (ou un travail) de fond moins prioritaire. Cependant, le développement d'un ordonnanceur spécifique intégré au noyau est très difficile puisqu'il nécessite une connaissance approfondie du noyau et implique des développements noyau lourds, avec toute la complexité et instabilité que cela entraîne. Par ailleurs, les extensions temps réel Linux sont supportées par un nombre plus réduit de plateformes. La solution proposée permet une réalisation en espace utilisateur de ce fait plus simple, plus stable et applicable à n'importe quelle plateforme supportant un noyau Linux standard. Cette approche ne permet pas de garantir les contraintes temps
réel dures, mais simplifie considérablement le développement d'un ordonnanceur tout en convenant pour les applications temps réei souple (la grande majorité des applications).
Un procédé d'ordonnancement selon l'invention convient en particulier à ia mise en œuvre de politiques d'ordonnancement orientées faible consommation, par exemple une politique EDF (« earliest deadline first », c'est- à-dire de priorité à l'échéance la plus proche) - qui est un type particulier d'ordonnancement avec contraintes d'échéances - avec changement dynamique de tension-fréquence (DVFS) afin de minimiser la consommation, ce qui est important dans les applications embarquées. Voir à ce propos les articles suivants :
R. Chéour et al. « EDF scheduier technique for wireless sensors networks: case study » Fourth International Conférence on Sensing Technology, 3 - 5 juin 2010, Lecce, Italie;
- R. Chéour et al. « Exploitation of the EDF scheduling in the wireless sensors networks », Measurement Science Technology Journal (IOP), Spécial Issue on Sensing Technology: Devices, Signais and Materials, 2011.
Il existe de nombreux travaux sur l'ordonnancement faible consommation, mais qui restent pour l'essentiel théoriques et ne sont pratiquement jamais intégrés à un système d'exploitation à cause de la complexité de ce travail. La solution proposée, comme elle est entièrement réalisée en espace utilisateur, facilite grandement cette intégration.
Un objet de l'invention est donc un procédé d'ordonnancement de tâches avec contraintes d'échéances, basé sur un modèle de tâches périodiques indépendantes et réalisé en espace utilisateur, dans lequel :
• chaque tâche à ordonnancer est associée à une structure de données, définie en espace utilisateur et contenant au moins une information temporelle (échéance et/ou période) et une information indicative d'un état d'activité de ia tâche, ledit état d'activité étant choisi dans une liste comprenant au moins :
- un état de tâche en exécution ;
- un état de tâche en attente de la fin de sa période d'exécution ; et
- un état de tâche prête à être exécutée, en attente d'une condition de reprise;
• au cours de son exécution, chaque tâche modifie ladite information indicative de son état d'activité et ie cas échéant, en fonction d'une politique d'ordonnancement prédéfinie, appelle un ordonnanceur qui est exécuté en espace utilisateur ;
• à chaque appel, ledit ordonnanceur :
- établit une file d'attente des tâches prêtes à être exécutées, en attente d'une condition de reprise;
- trie ladite file d'attente en fonction d'un critère de priorité prédéfini ;
- si nécessaire, préempte une tâche en exécution en lui envoyant un signal la forçant à passer dans ledit état de tâche prête à être exécutée, en attente d'une condition de reprise ; et
- envoie ladite condition de reprise au moins à la tâche se trouvant en tête de ladite file d'attente
Selon différents modes de réalisation du procédé de l'invention :
Ladite politique d'ordonnancement peut être une politique préemptive, par exemple choisie parmi une politique de type EDF et ses dérivés tels que DSF {« Deterministic Stretch to Fit », c'est-à-dire « extension et ajustement déterministe de la fréquence ») et AsDPM (« Assertive Dynamic Power Management », ou « politique assertive de gestion dynamique des modes repos »), RM (« Rate Monotonie », ou « ordonnancement à taux monotone »), DM {« Deadiine Monotonie » ou « ordonnancement en fonction de l'échéance »), LLF (« Least Laxity First » ou ordonnancement en fonction de la laxité la plus faible).
Le procédé peut être mis en œuvre dans une plateforme multiprocesseur, ladite structure de données comprenant également une information relative à un processeur auquel la tâche correspondante est affectée, et dans lequel ledit ordonnanceur affecte à un processeur du système chaque tâche prête à être exécutée.
Ledit ordonnanceur peut modifier la fréquence d'horloge et la tension d'alimentation du ou d'au moins un processeur en fonction d'une politique DVFS.
Le procédé peut comporter une étape d'initialisation, au cours de laquelle :
- les tâches à ordonnancer sont créées, affectées à un même processeur et placées dans un état d'attente d'une condition de reprise, une variable globale dite de rendez-vous étant incrémentée ou décrémentée lors de la création de chaque dite tâche ;
- lorsque ladite variable de rendez-vous prend une valeur prédéfinie indiquant que toutes les tâches ont été créées, ledit ordonnanceur est exécuté pour la première fois.
Ladite structure de données peut contenir également des informations indicatives d'un pthread associé à ladite tâche et à son temps d'exécution dans le pire cas.
Le procédé peut être exécuté sous un système d'exploitation compatible avec une norme POSIX.Ic, qui peut en particulier être un système Linux. Dans ce cas :
A chaque appel de l'ordonnanceur, un «pthread» est crée pour assurer son exécution.
Le procédé peut comporter l'utilisation d'un MUTEX pour assurer l'exécution d'une seule instance à la fois de l'ordonnanceur.
L'affectation d'une tâche à un processeur peut être effectuée au moyen de l'API CPU Affinity.
Un autre objet de l'invention est un produit programme d'ordinateur pour la mise en œuvre d'un procédé d'ordonnancement selon l'une des revendications précédentes.
D'autres caractéristiques, détails et avantages de l'invention ressortiront à fa lecture de la description faite en référence aux dessins annexés donnés à titre d'exemple et qui représentent, respectivement :
La figure 1 , le principe de réalisation d'un procédé d'ordonnancement selon l'invention en espace utilisateur Linux ;
La figure 2, un exemple d'un ensemble de tâches périodiques indépendantes ; et
La figure 3, un diagramme des états et des transitions des tâches applicatives dans un procédé d'ordonnancement selon l'invention.
Le principe de réalisation d'un ordonnanceur en espace utilisateur selon un mode de réalisation de l'invention, basé sur un système d'exploitation Linux, est illustré à la figure 1 . Une application est vue comme un ensemble de tâches à ordonnancer 2. L'ordonnanceur 1 contrôle l'exécution de ces tâches au moyen de trois fonctions spécifiques (référence 3) : « prempt(Task) », « resume(Task) » et « run_on(Task, CPU) ». Ces fonctions - dont les noms sont arbitraires et donnés uniquement à titre d'exemple non limitatif - permettent la préemption d'une tâche, la reprise d'une tâche et l'allocation d'une tâche à un processeur, respectivement. Elles s'appuient sur l'utilisation de fonctionnalités fournies par les API Linux (référence 5) permettant le contrôle des tâches et des processeurs depuis l'espace utilisateur.
Pour son fonctionnement, l'ordonnanceur doit disposer d'informations spécifiques qui découlent du modèle de tâches utilisé. Pour un modèle de tâches périodiques et indépendantes, il s'agit au moins d'informations relatives à l'échéance de chaque tâche, sa période et son état d'activité ; d'autres informations temporelles peuvent également être fournies : pire cas de temps d'exécution (WCET, de l'anglais « Worst Case Execution Time), échéance ultérieure (c'est-à-dire : temps courant plus échéance - pour éviter une confusion avec Γ « échéance ultérieure », « échéance » est parfois appelée « échéance absolue »), etc. Dans le cas d'une plateforme multiprocesseur, l'ordonnanceur nécessite également une information indicative du processeur auquel la tâche est affectée. En outre, chaque tâche est associée à un thread POSIX {«pthread»), qui doit également être connu de l'ordonnanceur.
D'autres informations susceptibles d'être nécessaires à l'ordonnanceur sont : un UTEX associé à la tâche, une condition associée à la tâche, un identifiant Linux de la tâche.
Le meilleur cas du temps d'exécution (BCET, de l'anglais « Best Case Execution Time ») et loe temps d'exécution effectif (AET, de
l'anglais « Actual Execution Time ») sont des informations qui, sans être indispensables pour réaliser l'ordonnancement, sont très utiles pour la mise au point car ils permettent (en particulier ΓΑΕΤ) de fixer le temps d'exécution d'une tâche (le temps d'exécution d'une tâche varie normalement d'une exécution à l'autre). Cela facilite la vérification de l'ordonnancement en permettant de le comparer à des résultats de simulations (avec des tâches ayant les mêmes paramètres et surtout exactement ies mêmes temps d'exécution).
Dans ia structure de tâche standard POSIX Linux (pthread) ces informations ne sont pas accessibles en espace utilisateur. Pour cette raison, la mise en œuvre de l'invention nécessite une extension de cette structure de tâche, par la création d'un type personnalisé à l'aide d'une structure de données.
Le modèle applicatif est constitué de tâches périodiques et indépendantes. Un modèle de tâches périodiques se réfère à la spécification d'ensembies homogènes de travaux (« jobs ») qui se répètent à des intervalles périodiques ; un « travail » est une forme particulière de tâche, et plus précisément une tâche indépendante dont son exécution est strictement indépendante des résultats des autres « travaux ». Un modèle de tâches indépendantes se réfère à un ensemble de tâches pour lequel l'exécution d'une tâche n'est pas subordonnée à la situation d'une autre. Ce modèle supporte l'exécution synchrone et asynchrone de tâches, il est applicable dans u grand nombre d'applications réelles qui doivent respecter des contraintes temporelles. La connaissance des caractéristiques et contraintes temporelles des tâches (échéance, période, pire temps d'exécution, etc.) est donc nécessaire pour appliquer ce type d'ordonnancement. Comme expliqué plus haut, ces informations sont rajoutées dans une structure spécifique qui étend le type de tâche standard sous Linux.
La figure 2 illustre un modèle applicatif de tâches périodiques et indépendantes comprenant quatre tâches T1 - T4. Les tâches T1 et T2 doivent s'exécuter avant leur échéance (« deadiine » DDLN) de 21 ms (millisescondes),
T3 avant son échéance de 31 ms et T4 avant son échéance de 40 ms. Chaque tâche s'exécute en un temps effectif AET (« Actuai Execution Time »} f, qui est compris entre une valeur minimum BCET (« Best Case Execution Time ») et
une valeur maximum WCET (« Worst Case Execution Time »). Au terme de son exécution, une tâche se met en attente jusqu'à atteindre sa période où elle devient à nouveau prête pour réexécution. Ainsi, une tâche prend à chaque instant un état parmi ies suivants :
- un état de tâche en exécution ;
un état de tâche en attente de la fin de sa période d'exécution ; et
un état de tâche prête à être exécutée, en attente d'une condition de reprise;
auxquels on peut ajouter un état de tâche inexistante, avant son actïvation.
L'exemple de la figure 2 se réfère en particulier à une application vidéo où chacune des quatre tâches correspond à un traitement particulier d'une image. Ces quatre tâches sont donc répétées toutes ies 40 ms (période) afin de pouvoir traiter 25 images par seconde.
Une ou plusieurs files d'attente sont utilisées pour mémoriser les tâches. Au moins une file d'attente est nécessaire pour la réalisation d'un ordonnancement à contraintes d'échéances, pour mémoriser la liste des tâches prêtes. Une priorité est également associée à chaque tâche pour définir leur ordre d'exécution. Le critère de priorité dépend de la politique d'ordonnancement. Pour la politique EDF (« Earliest Deadline First », c'est-à- dire priorité à l'échéance la plus proche) par exemple, le critère de priorité est la proximité de l'échéance : la tâche ayant l'échéance la plus proche est la plus prioritaire. La file des tâches prêtes est généralement triée par ordre de priorité décroissante. Le critère de priorité considéré ici est différent des critères de priorité utilisés dans les ordonnanceurs Linux natifs (temps partagé). L'ordonnanceur est appelé à des instants précis, appelés instants d'ordonnancement, qui sont déclenchés par les tâches elles-mêmes à des moments caractéristiques de leur exécution. Ces moments sont appelés des événements de tâche (référence 2 sur la figure 1 ), ils correspondent par exemple à leur activation (« onActivate »), leur fin d'exécution (« onBlock »), leur réactivation (« onUnBlock ») ou leur terminaison (« onTerminate ») - ces noms étant arbitraires et donnés uniquement à titre d'exemple non limitatif.
Les événements sont déclenchés, aux moments adéquats de l'exécution d'une tâche applicative, par des appels à des fonctions « onActivate() », « onBlock() », « onUnBlock() », « onTerminate() », insérés dans son code. Chaque événement de tâche met à jour les champs de ia structure de tâche pour la tâche concernée (état, échéance ultérieure, période, etc.) et appelle l'ordonnanceur si nécessaire. L'appel pu non de l'ordonnanceur par un événement de tâche dépend de la politique d'ordonnancement. Pour une politique EDF par exemple, l'ordonnanceur est appelé sur les événements « onActivate », « onBlock » et « onUnBlock ».
L'événement « onActivate » correspond à la création d'une tâche. Il marque le passage de l'état « inexistante » à « prête ». Pour des raisons de synchronisation à la création des tâches (expliquées en 15.), l'événement onActivate incrémente, à la fin de son exécution, une variable « rendez-vous », puis met la tâche en attente d'une condition d'activation de l'ordonnanceur, par exemple en appelant la fonction « pthread_cond_wait » de ΓΑΡΙ « Pthread Condition Variable » de la norme POSiX.lc.
L'événement « onBlock est » déclenché lorsqu'une tâche termine son exécution effective (AET). Elle passe alors à l'état « en attente » où elie se met en attente jusqu'à atteindre sa période. Lorsqu'elle atteint sa période, la tâche déclenche l'événement « onUnBlock », qui la fait passer à l'état « prêt », puis la met en attente d'une condition de reprise, par exemple en appelant la fonction POSIX pthread_cond_wait. Cette condition sera signalée par l'ordonnanceur au moyen de la foncion « pthread_cond_broadcast » qui appartient elle aussi à ΓΑΡΙ « Pthread Condition Variable » de ia norme POSiX.lc.
A tout moment de son exécution, une tâche peut être préemptée par une autre tâche devenue plus prioritaire (dans le cas d'un algorithme EDF, parce que son échéance est devenue plus proche de celle de la tâche en cours d'exécution). La préemption d'une tâche la fait passer de l'état « en exécution » à « prête ». Réciproquement, la tâche qui reprend à la suite d'une préemption passe de l'état « prête » à l'état « en exécution ».
Les transitions entre états déclenchées par les événements de tâches sont illustrées par la figure 3.
Pour éviter les phénomènes d'inter biocage, l'ordonnanceur n'est pas appelé directement par les événements de tâches mais par l'intermédiaire d'un «pthread». En d'autres termes, la fonction qui réalise un événement de tâche (onActivate, onBlock, onUnBlock, onTerminate) appelle une autre fonction « cail_scheduler » (nom donné à titre d'exemple non limitatif), qui crée un «pthread» d'ordonnancement pour exécuter l'ordonnanceur.
A chaque appel, l'ordonnanceur effectue, au moyen d'une fonction principale « select_ » (nom donné à titre d'exemple non limitatif) les actions suivantes : établissement d'une file d'attente des tâches prêtes, tri de la file d'attente par ordre de priorité décroissante (tâche la plus prioritaire en début de liste), préemption des tâches non prioritaires en cours d'exécution, allocation des tâches éligibles aux processeurs libres et démarrage des tâches éligibles par envoi d'une condition de reprise. La détermination des tâches éligible nécessite la connaissance par l'ordonnanceur de l'état de toutes les tâches existantes. La liste d'actions donnée ici correspond à un ordonnancement EDF, mais peut être modifiée - et en particulier enrichie - pour la réalisation d'autres politiques d'ordonnancement.
Du fait de l'exécution multitâche et multiprocesseur, plusieurs instances de l'ordonnanceur pourraient être appelées à s'exécuter en même temps . Pour empêcher une telle éventualité, un Mutex (de l'anglais « MUTual Exclusion device », c'est-à-dire dispositif d'exclusion mutuelle) est utilisé pour protéger certaines variables partagées comme la file d'attente des tâches prêtes. Le Mutex est verrouillé au début de l'exécution de l'ordonnanceur, puis libéré à la fin de l'appel à l'ordonnanceur. Ce procédé garantit qu'une seule instance de l'ordonnanceur ne s'exécute à la fois, ce qui permet la modification sans risque des variables partagées.
La première exécution de l'ordonnanceur intervient juste après la création des tâches. Afin de garantir le contrôle des tâches par l'ordonnanceur, une synchronisation doit être réalisée pour être sûr que toutes les tâches ont bien fini d'être créées avant d'exécuter l'ordonnanceur et que les tâches ne démarrent pas leur exécution tant que l'ordonnanceur ne leur en a pas donné l'ordre. Pour cela, tout d'abord, une variable globale de type rendez-
vous est initialisée à 0 avant la création des tâches. Ensuite, toutes les tâches sont créées et placées sur le premier processeur du système. Au tout début de leur création, en d'autres termes à l'exécution l'événement onActivate(), chaque tâche incrémente la variable « rendez-vous » puis suspend immédiatement son exécution en se mettant en attente d'une condition de reprise (utilisation de la fonction POSIX « pthread_cond_wait »). Lorsque la valeur de la variable « rendez-vous » est égale au nombre de tâches, l'ordonnanceur peut être exécuté. Au cours de cette exécution, l'ordonnanceur reprend l'exécution des tâches éligibles en leur signalant leur condition de reprise (fonction « pthread_cond_broadcast »).
Pour contrôler l'exécution des tâches applicatives, l'ordonnanceur nécessite l'utilisation de deux fonctions spécifiques pour la suspension et la reprise d'une tâche, appelées respectivement « preempt(Task) » et « resume(Task) » sur la figure 1 (référence 3), ces noms étant donnés uniquement à titre d'exemple non limitatif. La fonction preempt(Task) se base sur l'utilisation de ΑΡΙ « Signa! » de la norme POSIX. Pour préempter une tâche, le signal S1GUSR1 est envoyé au pthread correspondant (fonction POSIX « pthread_kill »). Le gestionnaire de signal associé (« sigusrl ») met la tâche en attente d'une condition de reprise (« pthread„cond_wait ») dès la réception du signal. îl existe une condition de reprise pour chaque tâche de l'application. La fonction resume(Task) signale la condition de reprise adéquate à la tâche concernée pour reprendre son exécution. Elle est donc strictement équivalente à appeler la fonction Linux pthread_cond_broadcast ; en fait, la création d'une fonction spécifique se justifie essentiellement pour des raisons de lisibilité du code. Ce mécanisme exploite le fait qu'en pratique, le gestionnaire de signal SIGUSR1 est exécuté par le «pthread» Linux qui reçoit le signal. Ainsi, le gestionnaire de signai peut identifier le «pthread» à suspendre (nécessaire pour envoyer la condition d'attente au «pthread» concerné par la fonction pthread_cond_wait) en comparant par exemple son propre identifiant de processus (tid) avec celui de tous les « pthreads » de l'application. Il importe de noter que l'ordonnanceur n'utilise pas les mécanismes de préemption et reprise de tâche fournis par le noyau, car ces derniers ne sont pas accessibles en espace utilisateur.
Pour contrôler l'exécution des tâches applicatives dans une plateforme multiprocesseur, l'ordonnanceur nécessite une fonction explicite permettant de fixer l'exécution d'une tâche sur un processeur donné. Bien qu'il existe une API pour ie contrôle des «pthread» Linux par les processeurs d'une plateforme multiprocesseur (« CPU affinity »), il n'existe pas de fonction spécifique pour l'allocation d'une tâche sur un processeur. La réalisation de cette fonction (indiquée par « run_on » sur la figure 1 , référence 3, ce nom étant donné à titre d'exempte non limitatif) affecte le processeur « CPU » à l'exécution de la tâche « Task » en s'appuyant sur ΓΑΡΙ Linux « CPU affinity ». Ceci se base sur la spécification du processeur CPU (uniquement) dans le masque d'affinité de la tâche Task, ce masque d'affinité est ensuite affecté à la tâche (par la fonction pthread_setaffinity_np de l'APi « CPU affinity »). Ensuite, pour garantir que l'ordonnanceur n'exécute jamais plus d'une tâche par processeur, on empêche toutes les autres tâches applicatives d'utiliser le processeur CPU, On vérifie par ailleurs qu'une tâche n'est jamais affectée à plus d'un seul processeur.
Une fonction spécifique « change_freq » (nom donné uniquement à titre d'exemple non limitatif) est utilisée dans le cas d'un ordonnanceur utilisant les techniques DVFS pour contrôler le changement dynamique de tension/fréquence des processeurs (figure 1 , référence 3). Cette fonction utilise une APi Linux appelée « CPUFreq » qui permet de changer la fréquence de chaque processeur par l'intermédiaire du système de fichier virtuel « sysfs ». il suffit par exemple d'écrire la fréquence désirée dans un fichier - le fichier « /sys/devices/system/cpu/cpuO/cpufreq/scaling_setspeed » sous Linux - pour modifier la fréquence du processeur 0. L'ordonnanceur peut utiliser la fonction change_freq pour permettre la modification de la fréquence en récrivant dans le système de fichier. Il nécessite par ailleurs d'utiliser préalablement le gouverneur « Userspace » qui est ie seul mode DVFS sous Linux qui permette à un utilisateur ou à une application de changer la fréquence processeur à volonté. « Userspace » est un des cinq gouverneurs DVFS Unix et son nom indique que la fréquence (et donc la tension) peuvent être modifiées à volonté par l'utilisateur.
Une technique possible d'ordonnancement à faible consommation consiste à exploiter le « siack » dynamique (temps fourni par une tâche après son exécution) pour ajuster la fréquence et la tension de fonctionnement du ou des processeurs (DVFS) afin d'économiser l'énergie tout en offrant des garanties de délai. Par exemple, l'ordonnanceur de l'invention a été appliqué à la réalisation d'un ordonnancement de type « DSF » (« Deterministic Stretch~to-Fit », dans lequel la fréquence des processeurs (et donc la tension, qui en dépend) est recalculée à chaque événement d'ordonnancement, en utilisant le temps d'exécution réel (AET - « Actual Execution Time ») pour les tâches accomplies et le pire délai d'exécution (WCET - « Worst Execution Time ») pour les autres, pour allouer le temps de battement de la tâche précédente à la tâche suivante, ce qui permet de diminuer la fréquence de fonctionnement du processeur concerné.
L'annexe contient le code source, écrit en langage C, d'un programme d'ordinateur permettant la mise en oeuvre d'un procédé d'ordonnancement selon l'invention, utilisant la technique DSF-DVFS décrite ci- dessus. Ce code est donné uniquement à titre d'exemple non limitatif.
Le code se compose d'une pluralité de fichiers contenus dans deux répertoires : DSF_Scheduler et pm_drivers.
Le répertoire DSF_Scheduler contient le c ur du code de l'ordonnanceur DSF. Il comprend notamment le fichier prototype N_scheduler,h qui contient ia définition des types nécessaires (structure de tâche, structure processeur, etc.), les variables globales (liste de tâches, liste de processeurs, paramètres de l'ordonnanceur, etc.), et tes prototypes des fonctions exportées.
Le fichier DSF_Scheduler.c décrit ensuite les fonctions principales de l'ordonnanceur. La plus importante est la fonction select_, c'est elle qui décrit tout le processus d'ordonnancement réalisé à chaque instant d'ordonnancement. Les événements de tâches onActivate, onBlock, onUnBlock et onTerminate qui déclenchent les instants d'ordonnancement sont également décrits dans ce fichier. La fonction d'ordonnancement s'appuie sur une fonction slow_down qui calcule et applique le changement de fréquence. Enfin, les particularités de l'ordonnanceur au démarrage de l'application ont nécessité le développement d'une fonction spécifique start_sched. Celie-ci ne s'exécute
qu'une seule fois au tout début du lancement de l'application et se base en grande partie sur le code de l'ordonnanceur principal (select_). Pour tous les autres instants d'ordonnancement, c'est la fonction select_ qui est appelée, via la fonction call_scheduter. Le fichier Makefile est utilisé pour la compilation,
L'application à ordonnancer est décrite dans le fichier
N_application.c. C'est aussi le fichier contenant le « main » du programme. Celui-ci réalise les diverses initialisations nécessaires, créé les tâches POSIX, lance l'ordonnanceur et synchronise le tout. Il convient de préciser ici qu'une application est vue comme un ensemble de tâches, chacune comportant des caractéristiques temporelles telles que le pire cas du temps d'exécution (WCET), la période, les échéances (deadlines), etc. Les tâches utilisées effectuent un traitement simple (multiplication des éléments d'un tableau d'entiers) jusqu'à un certain temps d'exécution (AET) - ce qui est décrit par la fonction usertask_actualexec - puis se mettent en mode d'attente jusqu'à leur réactivation (usertask_sieepexec) lorsqu'elles atteignent leur période.
Enfin, pour garder une structure du code la plus claire possible en gardant uniquement les fonctions essentielles de l'ordonnanceur dans le fichier N_Schedu!er.c, les fonctions secondaires nécessaires ont été regroupées elles dans un autre dans un fichier : N_utiis.c. Celui-ci contient par exemple des fonctions d'initialisation, de préemption de tâches, d'allocation de tâches aux processeurs, de tri, d'affichage, etc.
Le répertoire pmjdrivers contient les fonctions plus bas niveau sur lequel s'appuie l'ordonnanceur. II s'agit plus précisément des fichiers prototypes intei_ core 5_m520.h, pm_typedef.h et du fichier pm_cpufreq.c.
Le fichier prototype intel_core_i5_m520.h contient la description de la plateforme cible dont il porte le nom. Il s'agit des informations sur le nombre de processeurs de la platforme et sur les fréquences possibles pour chacun des cœurs, représentés sous forme d'états. Les types utilisés (e.g. les états processeur) sont définis dans le fichier pm_typedef.h,
Le fichier pm_cpufreq.c contient en particulier les fonctions permettant le changement effectif dynamique de fréquence sur la plateforme d'exécution, en se basant sur ΓΑΡΙ Linux CPUfreq, Le changement dynamique de tension fréquence est géré par des politiques appelées governors
(gouverneurs) sous Linux, connues en elles-mêmes. Plus précisément, pour pouvoir changer la (les) fréquence(s) processeur(s), il faut d'abord utiliser un gouverneur dit userspace. Une première fonction set_governor est présente pour cela. Ensuite, l'interaction avec le pilote Linux de changement de fréquence se fait via des fichiers d'échanges localisés dans /sys/devices/system/cpu/cpuX/cpufreq/ où X représente le numéro du processeur concerné. Les fonctions open_cpufreq et close_cpufreq permettent d'ouvrir et de fermer le fichier d'échange scaling_set$peed. Le changement de fréquence se fait par la fonction CPU_state_manager_apply_state.
Les différentes fonctions utilisent notamment les API POSIX suivantes :
■ API Pthreads :
• pthread_creaie, pour la création d'un Pthread,
• pthread Join, pour synchroniser îa terminaison des Pthreads,
• pthread_exit, à la fin de l'exécution d'un Pthread,
• pthread_cancel, pour forcer îa terminaison d'un Pthread.
- API Signal :
• pthread_kill
' API Variables conditions
• pthread_cond_wait
• pthread cond broadcast
" API Mutex
• pthread mutexjock
• pthread_mutex_unlock
Les différentes fonctions utilisent également les APis non POSIX suivantes :
« API CPU affinity » :
• CPU ZERO pour vider la liste des processeurs alloués à une tâche,
• CPU_SET pour ajouter un processeur à la liste des processeurs alloués,
• CPU_CLR pour enlever un processeur à la liste des processeurs alloués,
* CPU_ISSET pour tester si une tâche est affectée à un processeur donné et
· pthread_setaffinity_np pour fixer le masque d'affinité à une tâche.
■ API CPUfreq pour le changement dynamique de tension- fréquence.
L'invention a été décrite en détail en référence à un mode de réalisation particulier : système multiprocesseur sous Linux, politique d'ordonnancement EDF avec changement dynamique de tension-fréquence (DVFS). Il ne s'agit pas, cependant, de limitations essentielles.
Ainsi, le procédé d'ordonnancement de l'invention peut être appliqué à une plateforme monoprocesseur et/ou ne pas mettre en œuvre de mécanismes de réduction de la consommation.
La mise en œuvre de l'invention est particulièrement aisée sous LINUX, car les API décrites précédemment sont disponibles. Cependant, un ordonnanceur selon l'invention peut être réalisé sous un autre système d'exploitation compatible avec la norme POSIX et plus précisément avec la norme IEEE POSIX 1 .c (ou, ce qui est équivalent, POSIX 1003.1c), sous réserve qu'il supporte un équivalent des APis CPU Affinity (pour les applications multiprocesseurs) et CPUfreq (pour les applications exploitant le changement dynamique de tension fréquence).
D'autres politiques d'ordonnancement avec contraintes d'échéance peuvent être mises en œuvre, comme par exemple des politiques « Rate Monotonie » (ordonnancement à taux monotone, RM), « Deadline Monotonie » (ordonnancement en fonction de l'échéance des processus, DM) ou « Latest Laxity First » (ordonnancement en fonction de la laxité plus faible, LLF).
De même, diverses techniques de réduction de la consommation peuvent être mises en œuvre. On peut citer à titre d'exemples non limitatifs la technique DSF (« Deterministic Stretch~to~Fit », c'est-à-dire « extension et ajustement déterministe de la fréquence »), exploitant le
changement dynamique de tension fréquence (il s'agit de la technique utilisée dans l'exemple qui vient d'être décrit) et AsDPM (« Assertive Dynamic Power Mangement » c'est à dire « politique assertive de gestion dynamique des modes repos ») exploitant les modes repos processeur.
il est à remarquer que la reproduction d'un ordonnanceur de tâches sous contraintes d'échéances en espace utilisateur telle que décrite ci- dessus n'est réalisable qu'à certaines conditions qui dépendent du système d'exploitation (dénommé en anglais OS pour Operating System). Ces conditions sont par exemple les suivantes. Le procédé selon l'invention doit supporter la notion d'ordonnancement préemptif en espace utilisateur. La possibilité de préemption explicite d'une tâche depuis l'espace utilisateur est une caractéristique qui n'est pas réalisable dans tous les OS. Le modèle de tâche doit intégrer des attributs de contrainte d'échéance et d'état. La possibilité d'extension du modèle de tâche avec ces attributs, accessibles en mode utilisateur dépend de l'OS.
Il est à remarquer également que la réalisation de politiques d'ordonnancement orientées faible consommation en espace utilisateur n'est réalisable qu'à certaines conditions qui dépendent de l'OS. La possibilité de mise en sommeil ou de changement dynamique de fréquence d'un processeur en espace utilisateur n'est pas réalisable dans tous les systèmes d'exploitation, notamment sous Windows.
Il est à noter également que le procédé selon l'invention ne nécessite aucune intervention explicite du mode superviseur et se base exclusivement sur des mécanismes en mode utilisateur sous forme d'APis.
La structure de tâche standard POS!X Linux (pthread), décrite dans le mode de réalisation particulier de la Figure 1 , peut être généralisée à une structure de tâche standard POSIX (thread) sous un OS qui permet de la réaliser. La mise en œuvre de l'invention pour cette structure de tâche généralisée nécessite à l'instar de la structure de tâche standard POSIX Linux (pthread) de la Figure 1 leur extension par des paramètres accessibles en espace utilisateur. De même, à l'instar du modèle applicatif décrit pour la Figure 1 , les informations concernant les caractéristiques et contraintes temporelles des tâches sont rajoutées dans une structure spécifique qui étend le type de
tâche standard POSIX par des paramètres rendus accessibles en espace utilisateur.
Il est à remarquer que le procédé de préemption de tâches POSIX en espace utilisateur mis en oeuvre dans l'invention n'existe pas dans les OS dits « grand public », c'est-à-dire dépourvus de contraintes temps réel. Selon l'invention et de manière générale, pour contrôler l'exécution des tâches applicatives, l'ordonnanceur nécessite de réaliser une fonctionnalité de préemption explicite de tâches POSIX en espace utilisateur, qui se base sur l'utilisation de deux fonctions spécifiques pour la suspension et la reprise d'une tâche.
ANNEXE : CODE SOURCE
1. DSF_Scheduler
N scheduler.h
#ifndef N_SCHEDULER
#define N^SCHEDULER
#include <stdbool.h>
#include <unistd.h>
#snclude <sys/types.h>
#include <sys/time.h>
#inciude <pthread.h>
#include <semaphore.h> // PRIORITE MAXIMALE POUR LA POLITIQUE SCHED_FIFO
// UTILISEE POUR DONNER UNE PRIORITE MAXIMALE A L'ORDONNANCEUR
#define MAX_PR!0 99
// NOMBRE DE TACHES DE L'APPLICATION
#define NU _THREADS 4
// NOMBRE DE PROCESSEURS UTILISES PAR L'ORDONNANCEUR
// (PEUT ETRE INFERIEUR OU EGAL AU NOMBRE DE PROCESSEUR TOTAL DISPONIBLE) #define CPU size 2
// DEFINITION DU TYPE ENUMERE STATE_T: ETAT D'UNE TACHE
enum state_t { unexisting, running, waiting, ready };
// unexisting = inexistante; running = en exécution; waiting = en attente; ready = prête // DEFINITION DU TYPE TASK (EXTENSION DU TYPE PTHREAD)
typedef struct task
{
short id; // identifiant de tâche utilisée seulement pour le débogage ftoat wcet; // temps d'exécution au pire cas
fioat bcet; // temps d'exécution au meilleur cas
fioat aet; // temps d'exécution effectif (wcet < aet < bcet) f ioat ret; // RET :temps d'exécuton, restant
fioat deadline; // échéance ("deadline") absolue
float next_deadiine; // échéance ("deadiine") relative
unsigned int cpu; // processeur exécutant cette tâche
float period; // période de la tâche = échéance
float nexLperiod; // période relative
double begintime; // temps de début du travail
double endtime; // temps de fin du travail
int préemption; // 1 : préemptée; 0: non préemptée.
double preempidelay; // durée de la dernière préemption
double last_preemp _time; // temps auquel s'est produite la dernière préemption double total_preempt_duration; // durée de préemption totale si le travail est préempté
// plusieurs fois - peut être calcutée à partir de preemptdelay // et last_preempt„time
enum statej state; // état de la tâche: waiting, ready, running, unexisting pthread_mutex_t mut_wait; // Mutex utilisé pour suspendre et reprendre une tâche pthread_cond_t cond_resume; / Variable condition utilisée pour suspendre et reprendre
// une tâche
pthread__t *pthread; // thread posix associé à une tâche
pid J thread_pid; // identifiant linux {process ID, ou pid) du thread posix associé
// à la tâche
}task;
// DEFINITION DU TYPE ENUMERE STATE_P: ETAT D'UN PROCESSEUR
enum state_p { RUNNING,STAND_BY,IDLE,SLEEP, DEEP_SLEEP };
// DEFINITION DU TYPE CPU (état courant, numéro du CPU)
typedef struct cpu
{
enum state__p state;
int cpu Jd;
}cpu;
// DECLARATION DES VARIABLES GLOBALES DE l'ORDONNANCEUR
// LISTE DES PROCESSEURS UTILISES POUR LORDONNANCMENT
cpu CPUs[CPU_size];
// LISTE DES TACHES APPLICATIVES A ORDONNANCER
task *T[NUM_THREADS];
// LISTE DES TACHES PRETES
task *list_ready[NUM_THREADS];
int list_ready_size;
// LISTE DES TACHES NON PRETES
task *lisi_noready[NU _THREADS];
int list_noready_size;
struct timevai start_time, current_time;
int rdv; //Pour synchroniser les threads à leur création
bool OTE;
// PARAMETRES POUR LE CALCUL DU FACTEUR DE RALENTISSEMENT DANS LA FONCTION //SLOW DOWN
float wcei_Fn_1 [NUM_THREADS]; // WCET avant le calcul d'une nouvelle fréquence float aet_Fn_1 [NUM_THREADS]; // AET avant le calcul d'une nouvelle fréquence float abs_eet[NUM_THREADSJ; // temps d'exécution (absolu) de la tâche depuis
// l'instant d'activation
float total_eet [N U M__TH READS] ; // temps total écoulé d'exécution de la tâche
// (considéré à Fmax) depuis l'activation de la tâche {requis // en cas de préemption)
float total_abs_eet[NU _THREADSÎ; J; // temps total (absolu) écoulé d'exécution
// de la tâche depuis son activation
double task_durationJNU _THREADS]; // durée (absolue) de la tâche depuis l'instant
// d'activation
float last__resume_time[NUM_THREADS]; // Dernier temps auquel la tâche a été reprise (à la
// suite d'une préemption). Ce temps est remis à zéro // à chaque période de la tâche
float siackJocal[CPU„size]; // "slack": temps restant dû au fait que la tâche
// précédente est terminée avant son WCET float t_available_Fn_1 [CPU_size]; // slack avant le calcul d'une nouvelle fréquence float estimated_WCET_Fn_1 [CPU_size];// WCET estimé considérant le slack précédent et
// avant le calcul d'une nouvelle fréquence float estimated_AET_Fn_1 [CPU_sizej; // AET estimé considérant le slack précédent et
// avant le calcul d'une nouvelle fréquence float SRJocal[CPU_size]; // Facteur de ralentissement local (par rapport à la
// fréquence précédente)
float SRJotal[CPU_size]; // Facteur de ralentissement total (par rapport à la
// fréquence maximale)
double Fn_1 ; // fréquence précédente
double Fn; // fréquence actuelle
float offset_delaylNUM_THREADSI; // Temps additionnel à prendre en compte pour
// l'exécution de la tâche, dû à la possibilité d'offset // (étant donnée une tâche qui normalement // commence à T0, l'offset permet de démarrer la // tâche à T0 + offset).
exec|NUM_THREADSJ;
// PARAMETRES POUR LA GESTION DU TEMPS
double schedjsme;
double begin__sched_time;
double end_sched_time;
double sched_duration[150];
short nb_sched_call;
double simulationjîme;
// UTEX POUR EMPECHER L'EXECUTION PARALELLE DE 2 INSTANCES L'ORDONNACEUR
pthread_mutex_t sched_mut_waît;
pthread_co nd_t sched_con d_res urne;
// VARIABLES POUR CALCULER LE NOMBRE DE TRANSITIONS D'ETATS PROCESSEURS int STAND_BY_to_RUNNING;
int DEEP_SLEEP_to_RUNNING;
int SLEEP_to_RUNNiNG;
int !DLE_to_RUNNING;
int Preemption_counter;
// PROTOTYPES DES FONCTIONS EXPORTEES
void start_sched{);
void slow_down(task *T, cpu *P); void o n Activa te (tas k *T);
void onUnBiock(task *T);
void onBlock{task *T);
void onTerminateftask *T);
#endif
DSF__Scheduler, c
#define _GNU_SOURCE ffinclude <stdlib.h>
#inc1ude <sidio.h>
#include <signal.h>
#include <sîring.h>
#tnclude <math.h>
#include <sched.h>
#include <semaphore.h>
#include <sys/syscall.h> #include "N_utils.h"
#include "../p m_dn vers/i n te l_co re j 5_m 520, h "
//#define DEBUG
// FONCTION PRINCIPALE DE L'ORDONNANCEUR, APPELEE DEPUIS "CALL^SCHEDULER" // LE COEUR DU TRAITEMENT DE L'ORDONNANCEUR SE TROUVE DANS CETTE FONCTION void *select_() {
int i,j,rc;
double r__nxt;
schedjime = get_time();
OTE == false;
// DEFINITION DE LA LISTE DES TACHES PRETES (LIST_READY)
// DEFINITION DE LA LISTE DES TACHES NON-PRETES (LiST_NO_READY) iist_ready_size = 0;
Hst_noready_size - 0;
for(i=0; i<NUM_THREADS; {
if((T[i]->state == ready) H {T[i]->state ~ running)) {
list__ready[list_ready_size] - T[i];
list_ready_size++;
}
else {
list„noready[iist_noready__sizeJ = Tji];
list_noready_size++;
}
>
// TRI DES LISTES DE TACHES PAR ORDRE D'ECHEANCE CROISSANTE (LA PLUS PROCHE EN PREMIER)
so rt( I i st_re ad y , I i sf_ready_s ize) ;
sort(list_noready, list_noready_size);
printf("\n*************Sched ροίη¾:%.4Γ**************\η",8θΙΐΘά_ίίΓηβ);
// MESSAGES DE DEBUG QUI AFFICHE LA LISTE DES TACHES PRETES
prin f("Sor ed eady List:\ ");
Display_list(list_ready, list_ready_size);
// MESSAGES DE DEBUG QUI AFFICHE LA LISTE DES TACHES NON PRETES
#ifdef DEBUG
printf("Sorted No_Ready ListAt");
DisplayJist(list_noready, list_noready_size);
#endif
// PREEMPTION DES TACHES NON PRIORITAIRES DE LA LISTE DES TACHES PRETES ET EN //COURS D'EXECUTION
for (i=CPU_size; i<list_ready_size; {
task *job = list_readyEi];
if (job->state == running) {
abs_eet[job->id-1j = schedjime - iast_resume_time[job->id-1]; total_abs_eet|job->id-1] = total_abs_eetrjob->id-1 ] + abs_eet[job->(d-1];
total_eet[job->id-1] = total_eetjjob->id-1 ] + (abs_eet[job->id-1 ] / SRjotalfjob-
>cpu]);
if {!{sched_time < job->begintime + job->offset))
T_preempt(job);
}
}
// MISE A JOUR DU "RET" DE CHAQUE TACHE (REMAINING EXECUTION TIME)
for (i=0; i<NUM_THREADS; i++) {
task *job = T[i];
if Gob->preemption == 1 ) {
job->preemptdelay = sched_time - job->last__preempt_time;
job->totai_j)reempt_duration ~ job->total_preempt_duraiion + job- >preem tdelay;
}
if (job->state == running)
job->rei = job->aet - total__eet[job->id-1 ];
else if{job->state == ready) {
if (job->preemption == 1 )
job->ret = G°b->aet - totai_eet[job->id-1]);
else
job->ret = job->aet;
}
else if(job->state == waiting)
job->ret = 0.0;
}
// Le mutex suivant permet d'éviter d'exécuter plusieurs instances de l'ordonnanceur
// simultannément, ce qui conduirait à des accès concurrents et conflictuels sur certaines variables
// partagées
pthread_mutex_lock{&sched jnut_wait);
// ALLOCATION DES TACHES PRETES AUX PROCESSEURS LIBRES
for (i=0; (i<CPU_size) && (i<list_ready_size); i++) {
task *]ob = list_ready[i];
if (job->state != running) {
cpu *P = NULL;
for (j = 0; j<CPU_size; j++) {
P = &CPUs[jJ;
if (PjsRunning(P->cpuJd) == 0) {
if(P->state == DEEP_SLEEP) DEEP^SLEEPJo_„RUNNING++;
if(P->state == IDLE) !DLE_to_RUNNING++;
if(P->staie == STAND_BY) STAND_BY to RUNNING++;
if(P->state == SLEEP) SLEEPJoJ¾UNNING++;
// CALCUL DU RALENTISSEMENT (FREQUENCE MINIMUM) DU PROCESSEUR ALLOUE
if(nb_exec[P->cpujd] != 0) {
siow_down(job, P);
OTE = faîse;
>
etse
{
SR_local[P->cpu_id] = 1 .0;
SR_total[P->cpu_id3 - 1 .0;
}
pthread_mutex_lock(&job->mut_wait); // sans ce, mutex,
//parfois un travail serait recommence avant d'avoir été assigné à un processeur.
T_runningOn(job, P->cpu_id);
pthread_mutex_unlock(&job->mut_wait);
#ifdef DËBUG
printf{'T%d is assigned to CPU%d\n",job~>id,P->cpu_id);
#endif
if(job->preemption == 1 ) {
job->preemption = 0;
iast__resume_time[job->id-1 ] - sched_time;
}
eise if{job->preemption == 0) {
job->begintime = sched_time;
job->last_preempt_time = job->begintime;
last_resume_time[job->id-1 ] = job->begintime;
}
// DEMARRAGE DE LA TACHE PRETE SUR LE PROCESSEUR ALLOUE pthread_mutexJock(¾ob->mut_wait);
pthread_cond_broadcast(&job->cond_resume);
pthread_mutex_unlock(&job->mut_wait);
break;
}
}
}
}
pthread_m utex_u n lock(&sched_m u t_wa it); int nb = 0;
// MESSAGES DE DEBUG QU! AFFICHENT L'ETAT POUR CHAQUE PROCESSEUR (ACTIVITE // ET/OU TACHE ALLOUEE)
#ifdef DEBUG
printf("Processors StaieAn");
#endif
for 0=0; i<CPU_size; i++) {
cpu *P = &CPUs[i];
#ifdef DEBUG
printf("CPU%d -> " ,P->cpujd);
if (P->state == RUNNING) {
printf{"RUNN!NG ");
for (j=0; j<NU _THREADS; j++)
if ( (T[j]->state == running) && (T ]->cpu == P->cpujd) ) printf("T%d ", T[j]->id); printf("\n");
}
eise if (P->state STAND„BY) phntffSTAND BY\n");
eise if (P->state IDLE) printf("IDLE\n");
eise if (P->state SLEEP) prinif("SLEEP\n");
else if (P->sta†e DEEPJ3LEEP) printf("DEEP SLEEPVn");
else prinîf("UNDEFÎNED\n");
#endif
if (PjsRunning(P->cpu_id) == 1 ) {
nb++;
}
else {
slackJocai[P->cpu_id] = 0;
#ifdef DEBUG
prinif("-> slack_local[CPU%d] = 0\n", P->cpu_id);
#endif
}
}
#ifdef DEBUG
printf("Total Préemptions = %d\n" reemption_counter);
#endif
>
// FONCTION D'APPEL DE L'ORDONNANCEUR
// CREE UN THREAD AVEC LA FONCTION SELECT_ PRECEDENTE
void call_scheduler{) {
int i, rc;
pid__t pid;
pthread__t schedjhread;
begin__sched_time = get_tïme();
/* Création du pthread de l'ordonnanceur */
if(pthread_create{&schedjhread, NULL, seiect„ , "4") < 0) {
printf("pihread_create: error creating sched thread\n");
exit{1 );
}
end__sched_time = get_time();
// LE TEMPS D'EXECUTION DE L'ORDONNANCEUR EST MESURE POUR ANALYSE DES TEMPS
// DE REPONSE
sched_duration[nb_sched_call++J = end_sched_time - begin__sched_time;
ffifdef DEBUG
printffSCHEDULER DURATION #%d %f\n", nb_sched_call, sched___duration[nb_sched__ca!i - 1 ]);
#endif
}
// LES FONCTIONS SUIVANTES CORRESPONDENT AUX EVENEMENTS DE TACHES:
// - ONACTIVATE
// - ONBLOCK
// - ONUNBLOCK
// - ONTERMINATE
// ONACTIVATE
// CET EVENEMENT INTERVIENT A LA CREATION D'UNE TACHE
void onActivate(task *task) {
double randomvalue;
schedjime = 0.;
task->next__period = schedjime + task->period;
task->next_deadline = task->deadline;
// LE TEMPS D'EXECUTION (AET) EST TiRE AU HASARD ENTRE BCET ET WCET
randomvalue = {doub!e)rand()/RAND_MAX;
task->aet = ( randomvalue * (task->wcei-task->bcet)+task->bcet );
// INITIALISATION DE TOUTES LES VARIABLES NECESSAIRE A L'ORDONNANCEUR
// (DECLAREES/DETAILLEES DANS N_SCHEDULER,H)
aet_Fn_1 [task->id-1 ] ~ task->aet;
abs_eet[task->id-1 ] = 0.;
task->ret ~ task->aet;
task->preemptde!ay = 0.;
task->state = ready;
printf("T%d\t %.4ftt\t %.4f\I %.4f\t %.4f\n",task->id,task->wcei,iask->deadline,task-
>period,task->offset);
// SYNCHRONISATION PAR RDV: UNE FOIS CREEE, LA TACHE INCREMENTE RDV PUIS // ATTEND LE SiGNAL D'ACTIVATION task->cond_resume
// CE SIGNAL EST ENVOYE LORSQUE TOUTES LES TACHES SONT CREEES (LE. RDV = // NB_THREADS)
rdv++;
pthread_∞nd_wait(&task->cond_resume, &task->mut_wait); task->begintime = sched_time;
task->last_preempt_time = task->begintime;
last_resume_time[task->id-1] = task->begintime;
}
// ONBLOCK
// CET EVENEMENT INTERVIENT A LA FIN DE L'EXECUTION D'UNE TACHE
void onBlock(task *task) { sched_time = get_time();
task->endtime = sched_time; task->state = waiting;
slack_local[task->cpu] - task->wcet - task->aeî;
#ifdef DEBUG
printf("T%d is terminated at iime = %.4f and generated slackJoca![CPU%d] = %.4f\n", task->id,task->endiime,task->cpu, slack_locai[iask->cpu]);
#endif
CPUs[iask->cpu].state = IDLE;
task->cpu ~ -1 ;
#ifdef DEBUG
printf("T%d FINISHES EXECUTION AT %.2ftn^iask->id,schedjime);
#endif
// SCHEDULSNG POINT: APPEL DE L'ORDONNANCEUR
ca!l_scheduier{); }
// ONUNBLOC
// CET EVENEMENT INTERVIENT A LA FIN DE LA PERIODE D'UNE TACHE (REACTIVATION) void onUnBlock(task *task) {
double randomvalue; sched_time = get_time();
// LE TEMPS D'EXECUTION (AET) EST TIRE AU HASARD ENTRE BCET ET WCET randomvalue = (double)rand()/RAND_MAX;
task->aet = ( randomvalue * {task->wcet-task->bcet)+task->bcet );
// REINITIALISATION DE TOUTES LES VARIABLES NECESSAIRE A L'ORDONNANCEUR // (DECLAREES/DETAILLEES DANS N_SCHEDULER.H)
aet_FnJ [task->id-1 ] = task->aet;
wcet_Fn_1 [task->id-1 ] = task->wcet; abs__eet[task->id-1 ] = 0,;
total__eet[task->id-1] = 0.;
total_abs_eet[task->id-1 ] = 0.;
task->preemptdelay = 0.;
task->state = ready;
task->next_deadline = task->next__persod + task->deadline;
task->next_period = task->nexi_period + task->period; task->preemptdelay - 0.;
task->total_preempt_duration = 0.;
#ifdef DEBUG
printf("T%d REACHES PERIOD AT %.2f \n", task->id, sched_time);
#endif
// SCHEDULING POINT: APPEL DE L'ORDONNANCEUR
call_scheduler();
// LA TACHE EST ARRETEE, ELLE SERA REDEMARREE PAR L'ORDONNANCEUR AU MOMENT // OPPORTUN
pthreadjtill{*{task->pthread), SIGUSR1);
// ONTERMINATE
// CET EVENEMENT INTERVIENT A LA TERMINAISON D'UNE TACHE (FIN DE LA DERNiERE // EXECUTION)
void onTerminate(task *task) {
int i;
sched_time = get_time();
tas k->state = u n ex ist in g ;
#ifdef DEBUG
printf("T%d IS TERMINATED AT %.4f and generated a slack_local[CPU%d]= %.4f n", task->id,sched_time,task->cpu, slackJocal[task->cpu]);
#endif
// A CE STADE, LA SIMULATION EST TERMINEE. ON FORCE L'ARRET DE TOUTES LES // TACHES
for (i=0; i<NU _THREADS; i++)
pthread_cancel(*(T[i]->pthread));
}
// LE PREMIER APPEL A L'ORDONNANCEUR EST PARTICULIER DU FAIT DE LA CREATION ET DE // L'INITIALISATION DES TACHES
// CE TRAITEMENT SPECIFIQUE EST CONFIE A LA FONCTION START_SCHED, CORRESPONDANT // AU PREMIER APPEL DE L'ORDONNANCEUR
void start_sched() {
int i, j, err, rc;
pid_t pid; printff\n*************Sched point:%.4f***************\n",sched_iime); gettimeofday(&start_time,NULL); // TOUTES LES TACHES SONT AFFECTEES AU CPUO
cpu_set_t mask;
for (j=0; j<NUM_THREADS; j++) {
CPU_ZERO(&mask);
CPU_SET(0, &mask);
/* PREVENT TASK TO USE OTHER CPUs 7
// EMPECHER LA TACHE COURANTE DE S'EXECUTER SUR LES PROCESSEURS
AUTRES
// QUE CPUO
for (i-1 ; i<CPU_size;
CPU _CLR(i, &mask);
err = pthread_setaffinity_np(*(T[j]->pthread), sizeof{cpu__set_t), &mask);
}
// DEFINITION DE LA LSSTE DES TACHES PRETES {LSST_READY)
iist_ready_size - 0;
for {i=0; i<NUM_THREADS; {
lisi_ready[i] = T[i];
lisi_ready_size++;
}
// TRI DE LA READY_ LIST PAR ORDRE DE D'ECHEANCE LA PLUS PROCHE sort(lisi_ready, list_ready_size);
#ifdef DEBUG
printf("Sorted Ready ListAt");
Displayjist(lisi__ready, lisi_ready_size);
#endsf
// ALLOCATION DES TACHES PRETES AUX PROCESSEURS LIBRES
for (i=0; {i<CPU_size) && (i<listjready_size); i++) {
task *job = list_ready{i];
T_runningOn(job, i);
job->begintime = sched_time;
]ob->last_preempt__time = job->begintime;
lastj*esumeJirne[job->id-1] = job->begintime;
// DEMARRAGE DES TACHES PRETES SUR LES PROCESSEURS ALLOUES
for (i=0; (i<list_ready_size) && (i<CPU_size) ; i++) {
pthread_mutex_!ock(&list_ready[i]->mut_wait);
pthread_cond_broadcast(&!ist_ready[i]->cond_resume);
pihread_mutex_unlock{&list_ready[i]->mut_waii);
}
}
// CETTE FONCTION EST APPELEE PAR L'ORDONNANCEUR POUR CALCULER LE
// CHANGEMENT DE FREQUENCE A PARTIR DU FACTEUR DE RALENTISSEMENT
// POUR UNE TACHE ALLOUEE SUR UN PROCESSEUR, ON CALCULE LA FREQUENCE A LAQUELLE
// LA TACHE PEUT ETRE EXECUTEE, ET ON CHANGE LA FREQUENCE PROCESSEUR A CETTE VALEUR
void slow_down{task *job, cpu *P) {
int i;
int procjd = P->cpu_id;
char *setspeed__cpu_filename;
FILE *setspeed_cpu;
// RECUPERER LA FREQUENCE COURANTE DU CPU CONCERNE
Fn = {double)CPU_state_manager_query_current_freq{procjd);
#ifdef DEBUG
printff'Frequency Adjusfment on CPU%d before executing T%d:\n",proc_id,job->id);
#endif
// CALCUL DU FACTEUR DE RALENTISSEMENT (SRjotal et SRJocal)
estimated_WCET_FnJ [procjd] = wcet_Fn_1 [job->id-1 ] * SR_totalIproc_id];
estimated_AET_Fn_1 [procjd] = aet_Fn_1 [job->id-1] * S _total[proc_id];
#ifdef DEBUG
printf(''slack_local[CPU%d]\t***\t\t\t\t\t\i\t*i*\t\t\t= %.4f\n", proc jd, siack_local[proc_id]); prinif("SRJocal[CPU%d]\t\t***\t\t\t\t\t\t\t***\t\t\t= % .4f n", proc_id, SR_local[proc_id]);
printf("SRJotal[CPU%d]\t\i***\t\t\t\i\t\t\i***\t\t\t= %.4f\n", proc jd, SRJotal[proc_id]);
printf("esiimated_WCET_Fn_1 [CPU%d]= WCET[T%d]*SR_total[CPU%d]\t\t\t\t=
%.4f"%.4f\t\t= %.4f\n",
proc_id, job->id, proc_id, wcet_Fn_1 rjob->id-1], SRJotaifprocjd], estimated__WCET_Fn_1 [procjd]);
printf("estimated_AET_Fn_1 [CPU%dj= AET[T%d]*SRJoial[CPU%d]\t\t\t\t= %.4f*%.4f\t\t= %.4f\n",
procjd, job->id, procjd, aet_Fn_1 [job->id-1], SR_totai[proc_id], estimated_AET_Fn_1 [proc_id]);
#endif if{OTE ~ false) {
t_available_Fn_1 [proc_id] = wcet_Fn_1 [job->id-1] + slack_local[proc_id];
#ifdef DEBUG
printf("t_available_Fn_1 [CPU%d]\t= WCET[T%d]+stackJocal[CPU%d]\t\t\t\t= %.4f+%.4f \t= %.4f\n",
proc_id, job->id, procjd, wcetJFn_1 [job->id-1], slackjocalfprocjd], t_avaHable_FnJ [procjd]);
#endif
if (job->preemption ~~ false) {
SR_loca![proc_id] = (float) {t_availabie_Fn_1 [procjd] / estimated_WCET_Fn_1 [procjd]);
#ifdef DEBUG
printf("NEW SR_local[CPU%d]\t= t_available_Fn_1[CPU%dJ/estimated_WCET_Fn_1[CPU%d]\i= %.4f/%.4ftttt= %.4f \n",
procjd, proc_id, procjd, t_available_Fn_1 [proc_id], estimated_WCET_Fn J [procjd], SRJocal[proc_idj);
#endif
>
else if(job->preemption == true) {
/* mise à jour de slack, SR, ret. CPU mis à la fréquence maximale */ siackJocal[proc_id] = 0.0;
SRJocal[proc_id] = 1.0;
SR_totai[proc_id] = 1.0;
Fn = (double)_CPU_STATE[proc_id][0].freq;
#ifdef DEBUG
printfi" PREEMPTION BY T%d OCCURED AT TIME=%.4f, slackjoca!=0.0, SR_local=1.0, SR_total=1.0, Fn=maxfreq\n", job->id, sched_iirne);
#endif
}
}
if <SRJocal[procjd] >= 1.00) {
#ifdef DEBUG
printffNEW WCET(T%d]\t\t= WCET_Fn_1 [T%d]*SRJocal[CPU%d]\t\t\t\t= %.4f*%.4ftt\t= ",
job->id, job->id, procjd, wceî_Fn_1 [job->id-13, SRJocal[proc_id]); #endif
wceLFnJ [job->!d-1] = (float)(wcel_Fn^1 [job->id-1 ] * SR_local(proc_id]);
#ifdef DEBUG
printf("%.4f\n", wcet_Fn_1 jjob->id-1 ]);
#endif
#ifdef DEBUG
printf("NEW AET[T%d]\t\t= AET_Fn^1 [T%d]*SRJocal[CPU%d]\t\i\t\t= %.4f*%.4Mt= job->id, job->td, procjd, aet_Fn_1Dob->id-1], SRJocal[procjd]);
#endif
aet_Fn_1 [job->id-1] = (doubie)(aet_Fn_1 [job->id-1] * SRJocal[proc_id]);
#ifdef DEBUG
printf("%.4f n", aet_Fn_1 [job->id-1 ]);
#endif
}
else if (S RJocal [procjd] < 1 .00) {
#ifdef DEBUG
printffNEW WCET[T%d]\t\t= esi_WCET_Fn_1 [CPU%dfSRJocal[CPU%d]\t\t\t= %.4f*%.4f\t\t= ",
job->id, procjd, procjd, estimated_WCET_Fn_1 [procjd], SR_iocai[procjd]); #endif
wcet_Fn_1[job->id-1] - (estimated_WCET_Fn_1 [procjd] * SRJocal[procjd]);
#ifdef DEBUG
printf{"%.4f\n", wcet_Fn_1 [job->id-1]);
#endif
#ifdef DEBUG
printf("NEW AET[T%d]\t\i= est_AET_Fn_1 [CPU%d]*SR_local[CPU%d]\t\t\t= %.4f*%.4f\t\t= ",
job->id, procjd, procjd, estimated_AET_Fn_1 [procjd], SRJocal[proc_id]); #endif
aet_Fn_1 [job->id-1J = (estimated__AET_Fn_1 [procjd] * SRJocalJprocJdj);
#ifdef DEBUG
printf("%.4f\n", aet_Fn_1 |j"ob->id-1 ]):
#endif
}
/* CALCUL DE LA NOUVELLE FREQUENCE "THEORIQUE"
double Fn_1 = (double)(Fn / SRJocal[proc_idJ);
// RECHERCHE DE LA NOUVELLE FREQUENCE EFFECTIVE {LES FREQUENCE PROCESSEUR // SONT PREDEFINIES, DONC DIFFERENTES DE LA FREQUENCE THEORIQUE)
Elementary_state State;
State = __CPU_STATE[proc„id]fO];
for (i-0; i<NB_STATES_ CPU-1 ; i++)
if ( ((int)Fn_K=_CPU„STATE[proc_id][i].freq) && ((int)Fnm1 <=_CPU_STATE[proc_id][i+1].freq) )
State = _CPU_STATE[proc_id][i+1 ï;
#ifdef DEBUG
printf("FREQUENCY TO SWiTCH (%.0f): %d\t", Fn_1 , (int)State.freq);
#endif
// APPEL DE LA FONCTION DE CHANGEMENT DE FREQUENCE
CPU_state_manager apply_state{ proc_id, &State );
// LE FACTEUR DE RALENTISSEMENT DOIT ETRE RECALCULE EN TENANT COMPTE DU // CHANGEMENT EFFECTIF DE LA FREQUENCE PROCESSEUR
S RJocal [procjd] = Fn / State.freq;
SRJotallproc jd] = SR_total[proc_id] * SR_local[procjd];
#ifdef DEBUG
printf("NEW SRJocal[CPU%d]\t= Fn / State.freq = %.0f / %d = %.4f n", proc jd, Fn, (int)State.freq, SRJocal[proc_idJ);
printf("NEW SR_total[CPU%d]\t= SR_total[CPU%d] * SRJocal[CPU%d] = %.4f\n", procjd, procjd, procjd, SR_totai[proc_id]);
#endif
// REMISE DU SLACK A ZERO CAR ON VIENT DE CONSOMMER LE SLACK PRECEDENT POUR // DIMINUER LA FREQUENCE
slackjocalfprocj'd] = 0.0;
}
N_utifs.c
#define _GNU_SOURCE
#inciude <stdio.h>
#include <unistd.h>
#inciude <sys/time.h>
#include <signal.h>
#include <sched.h>
#include <time.h>
#include <errno.h>
#include <pthread.h>
#include "N_utils.h"
#define TARGET_CPU_DEFINED
#i ncl ude " .. /p m_d ri vers/i ntel_coreJ5_m 520. h "
#define DEBUG
// FONCTION D'INITIALISATION DES PROCESSEURS DE LA PLATEFORME
void init_CPUs()
{
printf("\nlntializing CPUs... \n");
int i;
// FAIRE LA LISTE DES PROCESSEURS DISPONIBLES ET METTRE A JOUR LES ETATS CPUs for (i=0;i<CPU_size;ï++) {
CPUs[i].cpu_id = i;
CPUs[i].state = IDLE;
}
printffSystem has %d processor(s)\n",NB_CPU_MAX);
printf{"System uses %d processor(s)\n",CPU_size);
#ifdef DEBUG
for (i=0; i<CPU_size; {
printf("CPU%d is ", CPUs[i].cpu_id);
if (CPUs[i].state == RUNNING) printf("RUNNING\n");
else if(CPUs[i].staie == STAND_BY) printf("STAND BY\n");
else if (CPUsfsJ.state == IDLE) printf("IDLE\n");
efse if (CPUs[i].state == SLEEP) printf("SLEEP\n");
else if (CPUs[i].state == DEEP^SLEEP) printf("DEEP_SLEEP\n");
else printf{"UNDEF!NED\n");
#endif
// OUVERTURE DES FICHIERS POUR LE CHANGEMENT DE FREQUENCE (API CPUfreq) printffSetting cpufreq\n"); for (i=0; i<CPU__size; {
setjgovernor(CPUs[i].cpu_id,"userspace");
open_cpufreq(i);
}
// ON DEMARRE A LA PFREQUENCE MAX (SPECIFQUE A L'ALGORITHME DSF) printff'Setting CPUs to maximum frequency\n");
for (i=0; i<CPU_size; i++)
CPU_state_manager_apply__state{ i, &_CPU_STATE[i][0] ); printf("End CPUs initializationAn");
}
// FONCTION DE LIBERATION DES PROCESSEURS DE LA PLATEFORME
voîd exit_CPUs()
{
int i;
// FERMETURE DES FICHIERS POUR LE CHANGEMENT DE FREQUENCE (API CPUfreq) for {i=0; i<CPU_size; i++)
close cpufreq(i);
}
// FONCTION D'INITIALISATION DES TACHES APPLICATIVES
void init_TASKs(task task[NUM_THREADS], pthreadj thread[NUM_THREADS])
{
int i, rc;
struct sched__param my_sched_params; printi("\ninitiaHzing tasks.,.\n");
printff'Application has %d task(s)\n", NUM_THREADS);
// SAUVEGARDE DU WCET POUR LA FREQUENCE COURANTE
for (i=0; i<NUMJ"HREADS; i++)
wcet_Fn_1 [i] = task[i].wcet;
// INITIALISATION DES MUTEX, UTILISES COMME MECANISME DE PREEMPTION pi h read_m utexj nit( &s ched_m ut_wait, ULL);
for (F0; i<NUM„THREADS; {
pthread__mutex_inii(&task[i],mut_wait, NULL);
pt read_cond_init(&task[r].cond_resume, NULL);
}
// INITIALISATION DES PARAMETRES DE TACHES
for (i=0; i<NUM_THREADS; i++)
{
T[i] = &task|i];
T[i]->pthread = &thread[i];
nb_exec[i] = 0;
tota[_eet[i] = 0.0;
total_abs_eet[i] = 0.0;
}
// INITIALISATION DES PARAMETRES DE L'ORDONNANCEUR
for (i=0; i<CPU_size; i++)
{
s[ack_local[i] ~ 0.0;
SRJocalfi] = 1.0;
SR_total[i] = 1.0;
Lavailable_Fn_1 [i] = 0.0;
estimaied_WCET_Fn_1 [i] = 0.0;
estimated_AET_Fn_1 [i] = 0.0;
}
STAND_BY_to_RUNNING=0;
SLEEPjo_RUNNiNG=0;
DEEP_SLEEPJo_RUNNING=0;
IDLE_to_RUNN!NG=0;
Preemption_counter=0; nb_sched_call = 0;
rdv = 0; printf("End tasks irtitializationAn");
// FONCTION PERMETTANT DE PREEMPTER UNE TACHE
void T_preempt(task *task)
{
int i, rc;
struct sched__param mymsched_params; printf("T%d[CPU%d] preempted at %.4f with ABS_EET=%.4f \n", iask->id, task->cpu, sched time, abs__eet[task->id-1 ]);
Preemption_counter++;
task->lastj?reempt_time = sched_time;
task->preemption = 1 ;
task->preemptdelay = 0.;
task->state = ready;
CPUs[task->cpu].state = IDLE;
task->cpu = -1 ;
// LE MECANISME DE PREMPTiON CONSISTE A ENVOYER UN SIGNAL A DESTINATION DE // LA TACHE
pthread_kil!(*(task->pthread)J SIGUSR1 );
// LORSQUE CELLE-CI LE REÇOIT, LE SIGNAL HANDLER sigusri EXECUTE
// PTHREAD_COND_WAIT QUI A POUR EFFET DE SUSPENDRE LA TACHE
// VOIR sigusri dans APPLiCATiON.C
}
// ALLOUE UNE TACHE A UN PROCESSUR
void T_runningOn{task *task, int cpujd)
{ int i, j, err;
cpu_set_t mask;
CPU _ZERO(&mask);
Γ SET TASK TO CPUJD */
// Alloue la tâche 'task' au processeur numéro 'cpujd'
CPU_SET(cpujd, &mask);
/* PREVENT TASK TO USE OTHER CPUs 7
// Faire en sorte que la tâche 'task' ne puisse s'exécuter sur aucun autre processeur for (i=0; i<CPU_size; i++)
if (i!=cpujd) CPU jOLRii, &mask);
err = pthread_setaffinity__np(*(iask->pthread), sizeof(cpu_set__t), &mask);
tf (err != 0) { printf (1 ) PTH RE AD_S ET AFFI ITY RETURNED; %d \n", err); exit(0); >
task->cpu = cpujd;
task->state = running;
CPUs[cpujd].state = RUNNING;
nbj3xec|cpujd]++;
/* PREVE T ALL OTHER TASK TO USE CPUJD*/
// Faire en sorte que toute tâche autre que 'task' ne puissent pas s'exécuter
// sur le processeur numéro cpu_id
for {i=0; i<NUM_THREADS; i++)
if (T[i]->id != task->id) {
err = pthread_getaffinity_np{*{T[!]->pthread), sizeof{cpu_set_i), &mask);
if (err != 0) { printf ("**** (2) PTHREAD_SETAFFINITY RETURNED: %d \n", err); exit(0); }
if ( (T[i]->id != task->id) )
CPlLCLR(cpu_td, &mask);
/* CHECK THAÏ EACH THREAD CAN NOT USE
MORE THAN ONE CPU (default: CPUO) */
// Vérifier que chaque tache ne peut pas utiliser plus d'un seul CPU {par défaut, CPUO) short cpu_aiiocated = 0;
for (j=0; j<CPU„size; j++)
if ( CPUJSSET(j, &mask) } cpu_allocated++;
if (cpu_allocaied == 0) CPU_SET(0, &mask);
else if {cpu_allocated > 1 ) printffWARNiNG: T%d IS ALLOCATED MORE THAN ONE CPU\n", T[i]->id);
err = pthread_setaffinity_np(*(T[i]->pthread), sizeof(cpu_setJ), &mask);
if (err != 0) { printf ("**** (3) PTH RE AD_S ET AF F ! ITY RETURNED: %d \n", err); exit(0); }
}
}
// FONCTION PERMETTANT DE TESTER SI UN PROCESSEUR EST UTILISE {1 ) OU NON (0) int P_isRunning(int cpujd)
{
int i; int cpu_isRunning = 0;
if (CPUs[cpu_id].state == RUNNiNG) cpujsRunning = 1 ;
return cpujsRunning;
}
// TRI D'UNE LISTE DE TACHES PAR ORDRE DE D'ECHEANCE CROISSANTE (LA PLUS PROCHE EN // PREMIER)
void sort{iask *list[J, int list_size)
{
task *temp;
int i, jt min;
for (i = 0; i < list__size- 1 ;
{
min = i;
for (j=i+1 ; j < list_size; j++)
{
if (Hst[j]->next_deadline < !ist[min]->next_deadline)
{
m.in=j;
}
>
if (i != min)
{
temp=list[i];
iist[i]=iist[min];
iist[minj=temp;
}
}
// FONCTION QUI RETOURNE LE TEMPS ECOULE DEPUIS LE DEBUT DE LA SIMULATION double get_time{)
{
double îime;
struct ismeval currenijime; ïf {gettimeofday(¤tJirne,NULL) == -1 )
{
putsfERROR : getJime_ ofday");
return -1 ;
}
time = current_time.tv_.sec - start_time.iv_sec \
+0.000001 *(current_time.tv_usec - start_tirr¾e.tv_usec); return time;
}
// TACHES APPLICATIVES:
// LES TACHES APPLICATIVES SE DIVISENT EN DEUX PHASES:
// - UNE PHASE D'EXECUTION EFFECTIVE (CORRESPONDANT A usertask_actualexec)
// - UNE PHASE D'ATTENTE (CORRESPONDANT A usertask_sleepexec). LA TACHE DOIT ATTENDRE // LE TEMPS DE SA PERIODE AVANT DE POUVOIR ETRE A NOUVEAU PRETE POUR REEXECUTION
int usertask_actualexec(task *task)
{
double duration;
double data[262144J;
int i = 0;
nbTicks++; task->begintime - sched_time;
#ifdef DEBUG
printf("T%d Starts exécution on CPU%d with AET = %.4f (*%.4f=%.4f) at %.4An",
task->id, task->cpu, task->ret, SR_total[task->cpu], task->ret*SRJotal[task->cpu], get_time()/*task->begintime*/);
#endif
// LES TACHES EFFECTUENT UN SIMPLE ACCES TABLEAU / MULTIPLICATION ENTIERE // JUSQU'A ATTEINDRE ΙΆΕΤ
// (EN TE ANT COMPTE DES PREEMPTiONS ET CHANGEMENT DE FREQUENCE POSSIBLES) do
{
duration - get_îime() - task->begintime;
daia[i%262144j = duration*i++;
} while (duration < iotal_abs„eet[task->id-1] + task->total_preempt_duraiion + (task->ret * SR_totai[task->cpu3) + offset_delay[task->id-1 ]);
task_duration[task->id-1J = duration; return 0;
} int usertask_s!eepexec(task *îask)
double sleep - task->next_period - schedjime;
if(sleep < 0)
sleep = 0.0;
usleep({unsigned iong)(1000000*sleep));
task->preemptdelay = 0; return 0;
}
// FONCTIONS D'AFFICHAGE UTILISEE POUR LE DEBUG
void DispiayQ
{
int i; sched_time=getjime();
printf("Scheduling point: %.4f, sched_time);
printf("\nTask \t cpu \t ihread_pid \t nxt ddlne \t prio \t starts at \t stops at \t idle until \t state\n");
for (i=0; i<NUM_THREADS; i++)
printf("T%d \t %d \t %d \t %.4f \t \t %d \t %,4f \t \t %.4f \t \t %.4f \t\t %d\n",
T[i]->id, T[i]->cpu, T[i]->thread_pid, T[t]->next_dead!ine,
T[i]->prio, T[i3->begintime, T[i]->endtime, sched_time, T[i]->state);
printf("
**\n");
} void Dispiayjist{task *listfl. int list_size)
{
int i;
printf("\t Task \t Next_deadline\n");
for (i = 0; i < list_size; i++)
printf("\i \t \t \t T%d \t (%.4f) \n", list[i]->id, list[i]->next_deadline);
printf("\n");
}
N applica tion. c
#define _GNU_SOURCE
#include <sched.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#inciude <sys/time.h>
#include <signal.h>
#include <string.h>
#include <sched.h>
#include <sys/types.h>
#include <sys/syscaSi.h> #include "N utils. h"
// L'EXEMPLE APLICATIF UTILISE ICI EST TIRE D'UNE APPLICATION VIDEO H264 ENCODEUR r
4 TASKS / 2 CPUS
- T1 -> Motion Estimation #1 Estimation de mouvement sur la demi-image numéro 1
- T2 -> Motion Estimation #2 Estimation de mouvement sur la demi-image numéro 2
- T3 -> Inv. Prédiction + Texture Encoding + Syntax Writing Prédiction inverse + Encodage Texture + Ecriture syntaxique
- T4 -> Loop Filter Filtre de déblocage
TH!S VERSION REQUIRES 2 CPUs Cet exemple nécessite 2 CPUS
*/ statîc task Task[NUM_THREADS]= {
/* Task[0] = */{
1 , // id
20.63e-1 , // wcet
5.65e-1 , // bcet
10.40e-1 , // aet
0, //ret
21 .0e-1 , // deadline
0, // nex_deadline
-1 , // cpu
40.0e-1 , // period
0, // next_period
-1 , // begintime
-1, //endtime
0, // préemption
0, // preemptdeiay
0.0, // last_preemp_time
0.0, // totaLpreempt_duration unexisting, // staie
PTHREAD_ UTEXJNITIALIZER,
PT H READ_CONDJ NIT!ALIZER,
NULL, // adresse du pthread associé
0 // t read_pid
}.
/*Task[1] = 7{
2, // id
20.63e-1, //wcet
5.65e-1 , // bcet
10.50e-1, //aet
0, /ret
21.0Θ-1, //deadline
0, // next dead!ine
-1 , // cpu
40.0e-1, //period
0, // next_period
-1, // beginttme
-1, //endtime
0, // préemption
0, // preemptdelay
0.0, // tast_preempt__time
0.0, // total_preempt_du ration unexisting, // state
PTHREAD__MUTEX_!NITIALÎZER,
PTHREAD_CONDJN!TIALIZER,
NULL, // adresse du pthread associé
0 // threadjaid
}.
Γ Task[2] = {
3, // id
8.25e-1, //wcet
3.38e-1, //bcet
5.96e-1, //aet
0, //ret
31.0e-1, //deadline
0, // next deadline
-1 , // cpu
40.0e-1, //period
0, // next_period
-1 , // begintime
-1, // endtime
0, // préemption
0, // preemptdelay
0.0, // !ast_preempt_time
0.0, // total_preempt_duration unexisting, // state
PTHREAD_MUTEX_iN!T)ALIZER,
PTHREAD_CONDJNITIALIZER,
NULL, // adresse du pthread associé
0 // ihread_pid
},
/* Task[3] = */{
4, // id
5.78e-1, //wcet
1.81Θ-1, /bcet
3.27Θ-1, //aet
0, //ret
40.0e-1, //deadiine
0, // next deadline
-1 , // cpu
40.0e-1, //period
0, // rsext_period
-1, // beg intime
-1 , // endtime
0, // préemption
0, // preemptdelay
0.0, // last„preempt_time
0.0, // total_preempt_duration
unexisting, // state
PTHREADJvlUTEXJNlTIALIZER,
PTHREAD_COND_INITiAUZER,
NULL, // adresse du pthread associé
0 // thread_pid
} };
// MECANISME UTILISE POUR PREEMPTER UNE TACHE
void sigusrl (int dummy)
{
int i;
pidj pid;
// Récupérer le pid du thread courant
pid = (long)syscal!(SYS_gettid);
// Comparer le pid du thread courant avec celui de tous les threads de l'application
// afin d'identifier le thread à suspendre. On exploite ici le fait que le signal handier (sigusrl ) // s'exécute avec le pid du thread qui l'a reçu,
for {F0; i<NUM_THREADS; i++) {
if (Task[iJ.thread_pid == pid) {
Task[i].state = ready;
pthread_cond_wait{&Task[i].cond_resume, &Task{iî.mut_waiî);
// POUR REDEMMARRER LA TACHE: pthread_cond_broadcast(&Task[i].cond__resume)
TaskfiJ.staie = running;
}
}
// DECLARATION DES TACHES APPLICATIVES, AVEC LEURS EVENEMENTS DE TACHE
// - OIMACTIVATE AU DEBUT DE L4EXECUTION
// - ONOFFSET: PAS UTILISE
// - ONBLOCK: A LA FIN DE L'EXECUTION EFFECTIVE D'UNE TACHE
// - ONUNBLOCK: LORSQU'UNE TACHE ATTEINT SA PERIOD
// - ONTERMINATE: LORSQU'UNE TACHE EST COMPLETEMENT TERMINEE
// CHAQUE TACHE CORRESPOND A UNE FONCTION, QUI SERA ENCAPSULEE DANS UN THREAD
// POSIX PAR LA SUITE:
void *task0{void * arg)
{
int i; identifiant de tâche requis par l'ordonnanceur */ TaskjO].thread_pid = (long)syscall(SYS_gettid); onActivate(&Task[0]);
do
{ if (usertask_actualexec(&Task[0]) == -1 ) { puts("ERROR : usertask_actualexec"); pthread_exit(0);
}
onBlock(&Task[0]);
if (usertask_sleepexec(&Task[0]) == -1 ) { puts("ERROR : usertask_sleepexec"); pthread_exit{0);
}
onUnBlock(&Task{0]);
} whiie (sched_time<simulation_time);
onTerminate(&Task{0]);
pthread_exit{0);
}
void *iask1 (void * arg)
{
int i;
Γ Identifiant de tâche requis par l'ordonnanceur */ Task[1 ].threadj)id = (tong)syscall(SYS_gettid); onActivate(&Task[1 ]);
do
{ if (usertask_actualexec{&Task[1]) == -1 ) { putsfERROR : usertask_actualexec"); pthread_exit(0);
}
onBlock(&Task[1]);
if {usertask_sleepexec{&Task[1]) == -1) { puts{"ERROR : usertask_sleepexec"); pthread_exit(0);
}
onUnBlock(&Task[13);
} while (sched_time<simulation_time);
onTerminate(&Task[1 ]);
pthread_exit(0);
} void *task2(void * arg)
{
int i;
/* Identifiant de tâche requis par l'ordonnanceur */ Task[2J.thread„pid = (tong)syscall(SYS_gettid); onActivate(&Task[2J);
do
{ if (usertask_actua!exec(&Task[2]) == -1) { puts{"ERROR : usertask_aciualexec"); pthread_exit(0);
}
onBlock(&Task[2]);
if (usertask_sleepexec{&Task[2]) == -1 ) { putsfERROR : usertask_sieepexec"); pthread_exit(0);
}
onUnBlock(&Task[2]);
} while (sched__time<simu!aÎion_time);
onTerminate(&Task[2]);
pthreadmexii(0);
} void *task3(void * arg)
{
int i;
/* identifiant de tâche requis par l'ordonnanceur */
Task[3].thread_pid = (long)syscall{SYS_gettid);
onActivate(&Task[3]);
do
{ if (usertask_actuaiexec(&Task[3]) == -1 ) {
putsfERROR : usertask_actualexec");
pthread_exit(0);
}
onBlock{&Task[3]);
if (usertask_sleepexec(&Task[3]) «- -1 ) puts("ERROR : usertask_sleepexec");
pt read_exit(0);
}
onUnBlock(&Task[3]);
} whiie (sched_time<simulation_time);
onTerminate(&Task[3]);
pthread_exit(0);
rnt main {)
{
int i, n;
void *ret;
pthreadj Thread[NUM_THRt=ADS]; simuiaiionjime = 80e~1 ;
// INITIALISATIONS RELATIVES AUX PROCESSEURS init_CPUs();
// INITIALISATIONS RELATIVES AUX TACHES APPLICATIVES init_TASKs(Task, Thread);
// Initiaiisaiion du gestionnaire de signal sigusrl
if (signaUSIGUSRI , sigusrl ) == SIG_ERR) {
perror("signaln);
exit(1 );
}
printf("\nTask \t WCET \t\t Deadline \t Period \t Offset\n");
// CREATION DES THREADS POSIX
if(pthread_create(&Thread[OJ, NULL, taskO, "0") < 0)
{
printf{"pthread_create: error creating thread 1\n");
exit(1);
}
if(pthread„create(&Thread[1 ], NULL, taskl , Ί ") < 0)
{
printf{"pthread_create: error creating thread 2\n");
exit(1 );
}
if(pthread_create(&Thread[2], NULL, task2, "2") < 0)
{
printf{"pthread_create: error creating thread 3\n");
exit(1 );
}
if(pthread_create(&Thread[3], NULL, task3, "3") < 0)
{
printf("pthread_create: error creating thread 4\n");
exit(1 );
}
Γ SYNCHRONISATION: ATTENDRE LA FIN EFFECTIVE DE CREATION
DE TOUS LES THREADS AVANT DE LANCER START_SCHED
while (rdv != NUM„THREADS) { } start_sched();
(void)pthreadJoin(Thread[0],&rei);
(void)pthreadJoin(Thread[1 ],&ret);
(void)pthread_join(Thread[2],&ret);
(void)pthreadJoin(Thread[3],&rei);
// FERMETURE DES FICHIERS DE CHANGEMENT DE FREQUENCE
exst_CPUs();
// MESURE DU TEMPS DE TRAITEMENT DE L'ORDONNANCEUR (MOYEN PAR APPEL, TOTAL) double average__sched_duration;
double total_sched_duration = 0;
prinîf("NUMBER OF SCHEDULER CALLS: %d\n", nb_sched_call);
for (i=0; i<nbmsched_cal!; {totai_sched_duration += sched_du ration [ij; printf("%f ", sched_duration[i]); }
printf("\nTOTAL SCHEDULER CALL DURATION: %f n", total_sched_duration);
average_sched__duration = total_sched_duration / nb_sched_call;
prinîfC'AVERAGE SCHEDULER CALL DURATION: %f n", average_sched_duration); return 0;
2. pm_drivers
in tel__core_i5_m 520. h
* intel_core_i5_m520.h
7
#ifndef INTEL_CORE_I5_M520
#define !NTEL_CORE_I5_M520
#include "pm_typedef.h"
#define NB_CPU_MAX 4
Γ fréquences disponibles
/* IMPORTANT: liste des frequencies disponibles en ordre décroissant 2400000 2399000 2266000 2133000 1999000 1866000 1733000 1599000 1466000 1333000 1199000 */
/*definition des états possible de CPU07
#define NB_STATES_CPU 1 #ifdef TARGET_ CPU_DEFINED
Elementary_state CPU_STATE[NB_CPU_MAX][NB__STATES_CPU];
#else
Bementary_state _CPU_STATE[NB_CPU_MAX]INB_STATES_CPU]
freq/elementary_state_id/core_id
{ 2400000, 0, 0 }, // CPU0[0]
{ 2399000, 1,0}, // CPU0[1]
{ 2266000, 2, 0 }, // CPU0[2]
{2133000, 3,0}, // CPU0[3]
{1999000, 4,0}, // CPU0[4]
{1866000, 5, 0}, // CPU0[5]
{ 1733000, 6, 0 }, // CPU0[6]
{ 1599000, 7, 0 }, // CPU0[7]
{1466000, 8, 0}, // CPU0[8]
{ 1333000, 9, 0 }, // CPU0[9]
{1199000, 10,0}}, // CPU0[10]
{ // fréquence / numéro de l'état élémentaire (fréquence) concerné
// /numéro du CPU concerné— Ces champs sont définis dans
// pm_typedf.h
{ 2400000, 0, 1 }, // CPU1[0]
{2399000, 1, 1 }, //CPU1[1]
{ 2266000, 2, 1 }, //CPU1[2]
{2133000, 3, 1 }, //CPU1[3]
{ 1999000, 4, 1 }, //CPU1[4J
{ 1866000, 5, 1 }, //CPU1[5]
{ 1733000, 6, 1 }, //CPU1[6]
{1599000, 7, 1 }, // CPU1(7]
{1466000, 8, 1 }, //CPU1[8]
{ 1333000, 9, 1 }, //CPU1{9]
{ 1199000, 10, 1 }}, //CPU1[10]
// fréquence / numéro de l'état élémentaire (fréquence) concerné
// /numéro du CPU concerné
{ 2400000, 0, 2 }, // CPU2(0]
{2399000, 1,2}, // CPU2I1]
{ 2266000, 2, 2 }, // CPU2[2]
{2133000, 3,2}, // CPU2Î3]
{1999000, 4,2}, // CPU2[4]
{1866000, 5,2}, // CPU2[5]
{1733000, 6,2}, // CPU2[6]
{1599000, 7,2}, // CPU2[7]
{ 1466000,8,2}, // CPU2[8]
{ 1333000, 9, 2 }, // CPU2[9]
{ 1199000, 10, 2 }}, // CPU2[10]
// fréquence / numéro de l'état élémentaire (fréquence) concerné
/numéro du CPU concerné
{ 2400000, 0, 3 }, // CPU3[0]
{2399000, 1, 3}, //CPU3[1]
{ 2266000, 2, 3 }, // CPU3[2]
{2133000, 3,3}, // CPU3[3]
{1999000, 4,3}, // CPU3[4]
{ 1866000, 5, 3}, // CPU3[5]
{1733000, 6,3}, // CPU3[6]
{ 599000, 7, 3}, il CPU3[7]
{ 1466000, 8, 3 }, // CPU3[8]
{ 1333000, 9, 3}, // CPU3[9]
{1199000, 10,3}} //CPU3[10]
#endif
#endif
pm typedef.h
#ifndef PM TYPEDEF
#define PM_TYPEDEF
/*— Structures de données— */ typedef unsigned long long Core_freq; typedef struct Elemeniary_state {
Core_freq freq;
unsigned long elementary statejd;
unsigned long corejd;
} Elementary__state; typedef struct Global_state {
Elementary_state* state;
unsigned int nb values;
f!oat curr;
float voit;
} Globai_state; typedef struct
Global_state_and_transition {
float delay;
float energy;
Global_state target;
} Globai_state_and_transition;
typedef struct Global_states_and_Jransitiorts {
Global_state_and_transition* values;
unsigned long nb values;
} Global_states_andJransitions; typedef struct Corejds {
unsigned long* values;
unsigned long nb_yalues;
} Corejds;
/*— Opérations d'interface— */
/*
Co re jds* CP U_state_m a nager_q u ery_core_i ds() ;
Global_siates_and_transitions* CPU_statejïianager jueryjiext__.possible_states(); void CPU_state_manager_apply_staie{ Globai_state* s );
#endif
pm_cpufreq.c
!*
* pm_cpufreq.c
* fonctions pour changer la fréquence des processeurs utilisant cpufreq
7
#include <stdio.h>
#inc!ude <string.h>
#inciude <fcntt.h>
#include "pm_typedef.h"
#define DEBUG
#define CPUFREQPATH "/sys/devices/system/cpu/cpu"
#define GOVPATH "/cpufreq/scaling^governor"
#define FREQPATH "/cpufreq/scaling_setspeed"
FILE *setspeed_cpu[4]; void itoa (int n, char *u) {
int i=0J;
char s[17]; do {
s[i++]=(char)( n%10 + 48 );
n-=n%10;
} while ((n/=10)>0); for {p0;j<i;j++)
u[i-1 -j]=s[j]; u[j]='\0';
} void set_governor(int cpu.char * ¾ι °v) {
F ( LE *cpu_governor;
char cpu_gov filename
// initialisation du nom de fichier complet (incluant le chemin) pour le gouverneur du coeur numéro
'cpu' char cpu_gov_sÎr[17J;
tioa{cpu, cpu_gov_str);
strcpy(cpu_gov_filename, CPUFREQPATH);
sircat(cpu_gov_ftlename, cpu_gov__str);
strcat(cpu_gov_f i lena me, GO VPAT H);
char strbufOI [20];
char strbuf02[20]; // AFFICHE LE GOUVERNEUR CPU COURANT
cpu_governor = fopen{cpu_gov_filename, "r");
fscanf(cpu_governor, "%s", strbufOI);
#ifdef DEBUG
printf("For CPU%d\t",cpu);
prinif{"CPU%d current governor:%s\t", cpu, strbufOI };
#endif
fclose(cpu__governor);
// MODIFIE LE GOUVERNEUR CPU COURANT
cpu_governor = fopen(cpu_gov_filename, "w");
fprintf(cpu_governor, "%s", gov);
fclose(cpu_governor);
// lAFFICHE LE NOUVEAU GOUVERNEUR CPU COURANT {POUR VERIFICATION) cpu_governor = fopen{cpu_gov_filename, "r");
fscanf(cpu_govemor, "%s", strbuf02);
#ifdef DEBUG
printf("->\iCPU%d new governor:%s\n", cpu, strbuf02);
#endif
fclose(cpu_governor);
void open_cpufreq(int cpu) {
char cpu_freq_filename[60];
#ifdef DEBUG
printf('Opening\t\t\t\t\t/sys/devices/system/cpu/cpu%d/cpufreq/scaling_setspeed\n'', cpu); #endif
// initialisation du nom de fichier complet {incluant le chemin)
// pour le fichier de spécification de fréquence (scaling_setspeed) du coeur numéro 'cpu' char cpu_freq_str[17J;
itoa(cpu, cpu_jfreq_str);
strcpy(cpujreqjilename, CPUFREQPATH);
strcat(cpu_freq_filename, cpu_freq_str);
strcat(cpu_freq_filename, FREQPATH);
setspeed_cpu[cpu] = fopen(cpu_freq_filenameI "r+w");
> void CPU_state__rnanager_apply_state{ int cpu, E[ementary_state *s) { // FILE *setspeed_cpu;
int freqO, freql ;
// AFFICHE LA FREQUENCE COURANTE DE CPU
fscanf(setspeed_cpu[cpu], "%d", &freqO);
fseek(setspeed_cpu{cpu], 0, SEEK_SET);
#ifdef DEBUG
printf{"CPU%d CURRENT FREQUENCY:%d\t",cpu,freqO);
#endif fprintf(setspeed_cpu[cpu], "%llu", s->freq);
fseek(setspeed__cpu[cpu], 0, SEEKJ3ET);
freql =-1 ;
fscanf(setspeed_cpu[cpuî, "%d", &freq1 );
fseek(setspeed_cpu[cpu], 0, SEEKJ3ET);
#ifdef DEBUG
printf("->\tCPU%d NEW FREQUENCY:%d\n",cpu, freql);
#endif
} int CPU_statejîianager_query_current_freq{int cpu) {
int freqO; fscanf(setspeed_cpu[cpu], "%d", &freqO);
fseek{setspeed_cpu[cpu], 0, SEEK_SET);
return freqO;
} void close_cpufreq(int cpu) {
#ifdef DEBUG
printf('Olossng\t\i\t\t/sys/devices/system/cpu/cpu%d/cpufreq/scaling_setspeed\n", cpu); #endif
f close(s etspeed__cpu [cpu ] ) ;
}