✔ La Fabrique du MCU

Partager les ressources critiques avec les Sémaphores

Le module semaphore permet de limiter le nombre de tâches qui accèdent simultanément à une ressource. Il permet également de synchroniser tâche et interruption. Cette page détaille le fonctionnement et l'utilisation des fonctions de ce module.

1. Synthèse des fonctions

Les fonctions de ce module sont déclarées dans le fichier mk_semaphore.h :

Fonction Description
mk_semaphore_create Crée un sémaphore.
mk_semaphore_take Prend un jeton d'un sémaphore.
mk_semaphore_release Libère un jeton d'un sémaphore.
mk_semaphore_delete Supprime un sémaphore.

2. Fonctionnement

Un sémaphore est un compteur à jetons. Le nombre de jetons est défini à la création. Il correspond au nombre de tâches pouvant accéder simultanément à la ressource protégée par le sémaphore. Un jeton est soit disponible, soit non disponible. Quand il est non disponible, le jeton est détenu par une tâche.


Les sémaphores ne possédant qu'un jeton sont appelés sémaphores binaires alors que les sémaphores en possédant plusieurs sont appelés sémaphores à compteur.


La figure ci-dessous montre l'état initial d'un sémaphore constitué de 3 jetons dont seulement un est utilisé :

État initial d'un sémaphore à trois jetons lors de sa création
Figure 1 : État initial d'un sémaphore à trois jetons lors de sa création.

Les sémaphores sont alloués par le noyau dans l'espace privilégié. Ils sont constitués d'une variable stockant le nombre total de jetons (TokenNumber), d'une variable stockant le nombre de jetons non disponibles (UsedToken) et d'une liste stockant les tâches en attente d'un jeton (BlockedList).


Le nombre de sémaphores pouvant être alloués par le noyau est limité. Il est défini à la compilation par la constante K_MK_SCHEDULER_MAX_NUMBER_OF_SEMAPHORES.


Les tâches interagissent avec un sémaphore de plusieurs manières. Lors d'un take, la tâche demande au noyau de lui attribuer un jeton. Si un jeton est disponible, le noyau le fournit et la tâche poursuit alors son exécution :

Évolution du sémaphore après un take
Figure 2 : Évolution du sémaphore après un take.

Lorsqu'aucun jeton n'est disponible, la tâche peut décider de se bloquer en attendant la libération d'un jeton.


Les tâches bloquées sont insérées dans la liste d'attente du sémaphore (BlockedList). Elles sont triées par ordre de priorité. Si plusieurs tâches attendent la libération d'un jeton alors la tâche la plus prioritaire sera réveillée en premier.


La figure ci-dessous montre la situation où trois tâches sont en attente d'un sémaphore qui n'a plus de jeton :

Sémaphore sans jeton disponible avec trois tâches en attente
                  d'un jeton
Figure 3 : Sémaphore sans jeton disponible avec trois tâches en attente d'un jeton.

Les tâches 2 et 7 sont les tâches les plus prioritaires mais possèdent une priorité identique. Dans cette situation, la tâche réveillée en premier sera la première à avoir été insérée dans la liste. Dans l'exemple ci-dessus, le noyau réveillera d'abord la tâche 7 puis les tâches 2 et 3.


Lors d'un release, un jeton pris par une tâche est restitué au sémaphore. Si au moins une tâche est en attente de celui-ci, le noyau attribue immédiatement le jeton à la tâche la plus prioritaire :

État d'un sémaphore après un release avec une tâche en attente
Figure 4 : Un release réveille la tâche la plus prioritaire.

La tâche 7 s'est vue attribuer le jeton et a donc été retirée de la liste d'attente. La figure ci-dessous montre l'évolution du sémaphore après 3 release successifs :

Évolution du sémaphore après plusieurs release
Figure 5 : Évolution du sémaphore après plusieurs release.

On remarque que toutes les tâches en attente d'un jeton ont été servies et qu'un jeton supplémentaire s'est libéré.


Le nombre de jetons disponibles dans un sémaphore ne peut pas dépasser le nombre total de jetons. Un release sur un sémaphore ne possédant que des jetons disponibles aboutit à une erreur.

3. Avantages et inconvénients

Un sémaphore possède les avantages suivants :

  • ✔Il permet à plusieurs tâches d'utiliser simultanément une ressource.
  • ✔Il permet de synchroniser tâche et vecteur d'interruption.
  • ✔Il n'a pas besoin de buffer applicatif.

Néanmoins, un sémaphore n'a pas de propriétaire. Le noyau ne sait pas quelle tâche détient les jetons et ne peut donc pas réaliser d'héritage de priorité. Une tâche peu prioritaire détenant un jeton peut alors retarder une tâche très prioritaire (inversion de priorité).


Pour protéger l'accès exclusif à une ressource partagée entre plusieurs tâches, il est préférable d'utiliser un mutex. Le sémaphore est plus adapté au comptage de ressources et à la synchronisation.

4. Utilisation

La création d'un sémaphore s'effectue avec la fonction mk_semaphore_create. L'exemple ci-dessous crée un sémaphore binaire ne possédant qu'un seul jeton non utilisé :


/* Pointeur du sémaphore */
K_MK_PRIVILEGED_BSS_MEMORY T_mkSemaphore* g_semaphore;

/* Une tâche privilégiée */
void mk_task_privileged ( T_mkAddr p_param )
{
   /* Déclaration de la variable de retour */
   T_mkCode l_result;

   /* Création d'un sémaphore binaire (1 jeton au total) */
   /* Aucun jeton n'est utilisé à la création */
   /* Il est protégé et ne peut pas être manipulé par des tâches non privilégiées */
   l_result = mk_semaphore_create ( &g_semaphore, K_MK_AREA_PROTECTED,
      1, /* Nombre total de jetons */
      0 ); /* Nombre de jetons utilisés */

   /* ... */
   for ( ;; ) ;
}

Après sa création, un jeton est pris avec mk_semaphore_take et restitué avec mk_semaphore_release. Le paramètre p_mkTick définit le comportement de la tâche lorsque le sémaphore n'a plus de jeton disponible. L'exemple ci-dessous montre comment protéger l'accès à une ressource unique avec un sémaphore binaire :


/* Une tâche privilégiée */
void mk_task_privileged ( T_mkAddr p_param )
{
   /* Déclaration de la variable de retour */
   T_mkCode l_result;

   while ( 1 )
   {
      /* La tâche attend au maximum 10 ticks qu'un jeton se libère */
      l_result = mk_semaphore_take ( g_semaphore, 10 );

      if ( l_result == K_MK_OK ) {
         /* Le jeton a été pris, la ressource peut être utilisée ... */

         /* Libération du jeton */
         l_result = mk_semaphore_release ( g_semaphore );
      }
      else if ( l_result == K_MK_ERROR_TIMEOUT ) {
         /* Aucun jeton ne s'est libéré pendant 10 ticks */
      }
      else {
         /* Gestion des erreurs ...  */
         /* K_MK_ERROR_PARAM, K_MK_ERROR_ISR, K_MK_ERROR_RIGHT */
      }
   }
}

Si le paramètre p_mkTick est nul et si le sémaphore n'a plus aucun jeton disponible, la fonction mk_semaphore_take retourne l'erreur K_MK_ERROR_TIMEOUT.


Un sémaphore à compteur permet de limiter l'accès à des ressources identiques. L'exemple ci-dessous autorise trois tâches à utiliser simultanément un des trois canaux de communication d'un périphérique :


/* Création d'un sémaphore à compteur possédant 3 jetons disponibles */
l_result = mk_semaphore_create ( &g_semaphore, K_MK_AREA_PROTECTED, 3, 0 );

/* ... */

/* Dans chacune des tâches utilisatrices */
/* Les trois premières tâches obtiennent un jeton et les autres seront */
/* bloquées tant qu'aucun jeton ne s'est libéré */
l_result = mk_semaphore_take ( g_semaphore, K_MK_TASK_WAIT_FOREVER );

if ( l_result == K_MK_OK ) {
   /* Utilisation d'un canal ... */

   /* Le jeton est libéré. Si des tâches sont en attente, la tâche */
   /* la plus prioritaire sera réveillée */
   l_result = mk_semaphore_release ( g_semaphore );
}

Un sémaphore peut également synchroniser une tâche et une interruption en utilisant la fonction mk_semaphore_release. Pour ce faire, un sémaphore binaire doit être créé avec son jeton utilisé :


/* Tâche de traitement */
void mk_handler_task ( T_mkAddr p_param )
{
   /* Création d'un sémaphore binaire dont le jeton est non disponible */
   T_mkCode l_result = mk_semaphore_create ( &g_semaphore, 
      K_MK_AREA_PROTECTED,
      1,   /* Nombre total de jetons */
      1 ); /* Nombre de jetons utilisés */

   while ( 1 )
   {
      /* Attente infinie de l'interruption */
      l_result = mk_semaphore_take ( g_semaphore, K_MK_TASK_WAIT_FOREVER );

      if ( l_result == K_MK_OK ) {
         /* Traitement de l'événement ... */
      }
   }
}

/* Vecteur d'interruption */
void IRQHandler ( void )
{
   /* Réveil de la tâche de traitement */
   T_mkCode l_result = mk_semaphore_release ( g_semaphore );

   /* Si K_MK_ERROR_TIMEOUT, tous les jetons sont déjà disponibles */
   ( void ) l_result;
}

L'interruption doit être moins prioritaire que le vecteur SVC pour pouvoir utiliser les appels système (priorité numérique supérieure ou égale à K_MK_SCHEDULER_MASK_PRIORITY).


Un schéma identique à celui ci-dessus peut être utilisé pour synchroniser deux tâches plutôt qu'une tâche et un vecteur d'interruption.


La dernière fonction de l'API est la fonction mk_semaphore_delete. Elle permet de détruire le sémaphore en demandant au noyau de le désallouer :


/* Suppression du sémaphore */
T_mkCode l_result = mk_semaphore_delete ( g_semaphore );

/* Si une erreur s'est produite */
if ( l_result != K_MK_OK )
{
   /* Gestion des erreurs ...  */
   /* K_MK_ERROR_PARAM, K_MK_ERROR_ISR, K_MK_ERROR_RIGHT */
}

5. Protéger ses sémaphores

Les sémaphores peuvent être créés avec les attributs K_MK_AREA_PROTECTED ou K_MK_AREA_UNPROTECTED.


Lorsque l'attribut K_MK_AREA_PROTECTED est utilisé, il est impossible pour une tâche non privilégiée de créer ou de manipuler un sémaphore. Un sémaphore non protégé doit donc être utilisé pour synchroniser des tâches privilégiées avec des tâches non privilégiées.


Du fait de leur niveau d'exécution, l'ensemble des règles de protection ne s'applique pas aux vecteurs d'interruption. La fonction mk_semaphore_release peut donc libérer les jetons d'un sémaphore protégé lorsqu'elle est exécutée dans un vecteur d'interruption.


Les sémaphores étant alloués par le noyau dans l'espace privilégié, aucune règle de placement ne s'applique. Seules les variables stockant leurs adresses doivent être accessibles aux tâches qui les utilisent. Les règles de placement de ces variables sont détaillées dans l'article Protéger son programme avec Mk.

6. API

6.1. La fonction mk_semaphore_create()

Cette fonction crée un sémaphore.


Prototype :


T_mkCode mk_semaphore_create ( T_mkSemaphore** p_mkSemaphore, 
   uint32_t p_mkAreaType, uint32_t p_mkTokenNumber, 
   uint32_t p_mkInitialTokenNumber );
                  

Paramètres :

  • p_mkSemaphore [out]

    Adresse d'un pointeur de type T_mkSemaphore*.


  • p_mkAreaType [in]

    Type du sémaphore. Les valeurs possibles sont les suivantes :

    • K_MK_AREA_PROTECTED : le sémaphore est protégé, il ne peut pas être manipulé par des tâches non privilégiées.
    • K_MK_AREA_UNPROTECTED : le sémaphore n'est pas protégé, il peut être manipulé par des tâches privilégiées ou non privilégiées.
  • p_mkTokenNumber [in]

    Nombre total de jetons du sémaphore. Ce paramètre ne peut pas être nul.


  • p_mkInitialTokenNumber [in]

    Nombre de jetons non disponibles à la création du sémaphore. Ce paramètre doit être inférieur ou égal à p_mkTokenNumber.

Retour :

  • K_MK_OK : le sémaphore a été créé.
  • K_MK_ERROR_PARAM : le sémaphore n'a pas été créé car au moins un paramètre est invalide.
  • K_MK_ERROR_MALLOC : le sémaphore n'a pas été créé car le nombre maximal de sémaphores pouvant être alloués a été atteint.
  • K_MK_ERROR_ISR : le sémaphore n'a pas été créé car cette fonction a été exécutée dans un vecteur d'interruption.
  • K_MK_ERROR_RIGHT : le sémaphore n'a pas été créé car une tâche non privilégiée ne peut pas créer un sémaphore protégé.

Note :

  • Cette fonction ne peut pas être exécutée dans un vecteur d'interruption.
  • La constante K_MK_SCHEDULER_MAX_NUMBER_OF_SEMAPHORES définit le nombre maximal de sémaphores pouvant être créés.
6.2. La fonction mk_semaphore_take()

Cette fonction prend un jeton dans un sémaphore. Si aucun jeton n'est disponible, la tâche peut être bloquée en attendant qu'un jeton soit libéré.


Prototype :


T_mkCode mk_semaphore_take ( T_mkSemaphore* p_mkSemaphore, 
   uint32_t p_mkTick );
                  

Paramètres :

  • p_mkSemaphore [in, out]

    Adresse d'une structure de type T_mkSemaphore.


  • p_mkTick [in]

    Durée maximale de blocage de la tâche. Les valeurs possibles sont les suivantes :

    • 0x0 : la fonction retourne immédiatement si aucun jeton n'est disponible.
    • 0x1 à 0xFFFFFFFE : la tâche est bloquée pendant la durée spécifiée (en multiple de tick) tant qu'aucun jeton n'est disponible.
    • 0xFFFFFFFF (K_MK_TASK_WAIT_FOREVER) : la tâche est bloquée tant qu'aucun jeton n'est disponible.

Retour :

  • K_MK_OK : un jeton a été pris par la tâche.
  • K_MK_ERROR_PARAM : aucun jeton n'a été pris car le paramètre est invalide.
  • K_MK_ERROR_TIMEOUT : aucun jeton n'a été pris car aucun jeton ne s'est libéré dans le délai imparti.
  • K_MK_ERROR_ISR : aucun jeton n'a été pris car la fonction a été exécutée dans un vecteur d'interruption.
  • K_MK_ERROR_RIGHT : aucun jeton n'a été pris car une tâche non privilégiée ne peut pas manipuler un sémaphore protégé.

Note :

  • Cette fonction ne peut pas être exécutée dans un vecteur d'interruption.
  • Lorsque plusieurs tâches sont bloquées sur le même sémaphore, le jeton libéré est attribué à la tâche la plus prioritaire.
6.3. La fonction mk_semaphore_release()

Cette fonction libère un jeton d'un sémaphore. Si une tâche est en attente du sémaphore, elle est réveillée et le jeton lui est attribué.


Prototype :


T_mkCode mk_semaphore_release ( T_mkSemaphore* p_mkSemaphore );
                  

Paramètres :

  • p_mkSemaphore [in, out]

    Adresse d'une structure de type T_mkSemaphore.

Retour :

  • K_MK_OK : un jeton a été libéré.
  • K_MK_ERROR_PARAM : aucun jeton n'a été libéré car le paramètre est invalide.
  • K_MK_ERROR_TIMEOUT : aucun jeton n'a été libéré car le sémaphore a déjà libéré tous ses jetons.
  • K_MK_ERROR_RIGHT : aucun jeton n'a été libéré car une tâche non privilégiée ne peut pas manipuler un sémaphore protégé.

Note :

  • Cette fonction peut être exécutée dans un vecteur d'interruption.
  • Cette fonction peut libérer un jeton qui n'a pas été pris par la tâche appelante.
6.4. La fonction mk_semaphore_delete()

Cette fonction détruit un sémaphore.


Prototype :


T_mkCode mk_semaphore_delete ( T_mkSemaphore* p_mkSemaphore );
                  

Paramètres :

  • p_mkSemaphore [in, out]

    Adresse d'une structure de type T_mkSemaphore.

Retour :

  • K_MK_OK : le sémaphore a été détruit.
  • K_MK_ERROR_PARAM : le sémaphore n'a pas été détruit car le paramètre est invalide.
  • K_MK_ERROR_ISR : le sémaphore n'a pas été détruit car la fonction a été exécutée dans un vecteur d'interruption.
  • K_MK_ERROR_RIGHT : le sémaphore n'a pas été détruit car une tâche non privilégiée ne peut pas manipuler un sémaphore protégé.

Note :

  • Cette fonction ne peut pas être exécutée dans un vecteur d'interruption.

Attention :

  • Les tâches bloquées par la fonction mk_semaphore_take ne sont pas réveillées lorsque le sémaphore est détruit.
6.5. Les définitions

Le module semaphore peut être configuré à l'aide des définitions suivantes :

Définition Description Valeur
K_MK_SCHEDULER_MAX_NUMBER_OF_SEMAPHORES Cette constante définit le nombre de sémaphores pouvant être créés. Elle ne peut pas être nulle. 32

<

Communiquer avec les boîtes de messages

Prévenir les accès concurrents avec les Mutex

>