Cours - Logiciel embarqué

Slide 35 VISUEL

Slide 35 - OS minimalistes vs OS généralistes et primitives RTOS
Support du cours : transition entre scheduling multi-tâches et besoins d'OS.

Clarifications à la slide 35 FOCUS

À la slide 35, le prof veut surtout faire le pivot entre “nous avons plusieurs tâches” (slide 34 sur le scheduling) et “dès que nous avons plusieurs tâches, il nous faut des services OS pour que ça reste maîtrisable et fiable”.

Cette slide sert à faire passer 3 idées d'un coup Deux familles d'OS, la coordination multi-tâches, et la préparation des notions suivantes.

Deux familles d'OS, deux philosophies

  • OS minimaliste (RTOS) : le cœur, c'est le scheduler + quelques objets de synchro/communication. Objectif principal : temps réel, délais déterministes (prévisibles). Exemples : FreeRTOS, microC/OS, MbedOS, VxWorks.
  • OS généraliste : en plus du scheduling, beaucoup plus de couches “système” (drivers, abstractions périphériques larges, services...), donc c'est moins déterministe. Exemples : Linux, Windows.

Dès qu'il y a plusieurs threads, le problème devient la coordination

Les petits schémas ne sont pas décoratifs : ils illustrent les 4 outils typiques d'un RTOS pour éviter le chaos en multi-tâches.

  • Queue : communication thread A → thread B en envoyant des messages (producteur/consommateur).
  • Flag : signalisation d'évènement (A “set”, B “get”), utile pour dire “c'est prêt”, “c'est arrivé”.
  • Mutex : protection d'une variable partagée (éviter les race conditions).
  • Semaphore : protection d'une ressource partagée (souvent compteur, ex : N buffers, N accès possibles).

Préparer la suite du cours

  • Slide 36 (task states : Ready, Running, Blocked, Suspended) : les objets (queue, sémaphore...) font souvent passer une tâche en Blocked en attendant un évènement.
  • Slide 37 sur la réentrance : sans mutex/synchro, une fonction non réentrante devient vite un bug “aléatoire” à l'exécution.
Ce qu'il faut retenir RTOS = déterminisme + primitives (queue, flags, mutex, semaphores) pour faire du multi-tâches propre ; OS généraliste = confort + abstraction large, mais moins de garanties temps réel.

C'est quoi les race conditions DEFINITION

Une race condition (condition de compétition) arrive quand au moins deux threads ou interruptions accèdent à la même donnée, et que le résultat dépend de l'ordre exact d'exécution, ordre qui peut changer d'une exécution à l'autre.

Mini exemple classique

Nous avons une variable partagée counter = 0. Deux threads font chacun counter++ en même temps. Or counter++ n'est pas une opération atomique.

  1. Lire counter
  2. Ajouter 1
  3. Réécrire counter

Scénario possible : Thread A lit 0, Thread B lit 0, Thread A écrit 1, Thread B écrit 1. Résultat final : counter == 1 au lieu de 2.

Pourquoi c'est dangereux en embarqué / RTOS

  • Produit des bugs difficiles à reproduire.
  • Peut casser un état interne (ex : structure de file, buffer circulaire).
  • Déclenche des comportements incohérents (valeurs impossibles, pertes d'évènements...).

Comment on évite ça

  • Mutex autour de la section critique (protection d'une variable/structure partagée).
  • Désactiver les interruptions pour une section ultra courte, quand c'est approprié.
  • Opérations atomiques (si disponibles).
  • Utiliser des queues ou messages plutôt que partager directement des variables.

Exemple en C CODE

Version non patchée (race condition)

// gcc -O2 -pthread race.c -o race
#include <pthread.h>
#include <stdio.h>

#define LOOPS 1000000

static int counter = 0;

void* worker(void* arg) {
    (void)arg;
    for (int i = 0; i < LOOPS; i++) {
        counter++; // RACE: lecture + addition + écriture non atomiques
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;

    pthread_create(&t1, NULL, worker, NULL);
    pthread_create(&t2, NULL, worker, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("counter = %d (attendu: %d)\n", counter, 2 * LOOPS);
    return 0;
}

Résultat typique : counter est souvent plus petit que 2 * LOOPS.

Version patchée (mutex)

// gcc -O2 -pthread race_mutex.c -o race_mutex
#include <pthread.h>
#include <stdio.h>

#define LOOPS 1000000

static int counter = 0;
static pthread_mutex_t counter_lock = PTHREAD_MUTEX_INITIALIZER;

void* worker(void* arg) {
    (void)arg;
    for (int i = 0; i < LOOPS; i++) {
        pthread_mutex_lock(&counter_lock);
        counter++;
        pthread_mutex_unlock(&counter_lock);
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;

    pthread_create(&t1, NULL, worker, NULL);
    pthread_create(&t2, NULL, worker, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("counter = %d (attendu: %d)\n", counter, 2 * LOOPS);
    return 0;
}

Variante patchée (atomique C11)

// gcc -O2 -pthread -std=c11 race_atomic.c -o race_atomic
#include <pthread.h>
#include <stdio.h>
#include <stdatomic.h>

#define LOOPS 1000000

static atomic_int counter;

void* worker(void* arg) {
    (void)arg;
    for (int i = 0; i < LOOPS; i++) {
        atomic_fetch_add(&counter, 1);
    }
    return NULL;
}

int main(void) {
    atomic_init(&counter, 0);

    pthread_t t1, t2;
    pthread_create(&t1, NULL, worker, NULL);
    pthread_create(&t2, NULL, worker, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("counter = %d (attendu: %d)\n", atomic_load(&counter), 2 * LOOPS);
    return 0;
}

Exemple FreeRTOS RTOS

Version non patchée (race condition)

#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>

#define LOOPS 1000000

static volatile int counter = 0;

static void TaskA(void *pvParameters) {
    (void)pvParameters;
    for (int i = 0; i < LOOPS; i++) {
        counter++;  // RACE: pas atomique
        taskYIELD();
    }
    vTaskDelete(NULL);
}

static void TaskB(void *pvParameters) {
    (void)pvParameters;
    for (int i = 0; i < LOOPS; i++) {
        counter++;  // RACE: pas atomique
        taskYIELD();
    }
    vTaskDelete(NULL);
}

int main(void) {
    // init hardware/uart si besoin...

    xTaskCreate(TaskA, "A", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);
    xTaskCreate(TaskB, "B", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);

    vTaskStartScheduler();

    for (;;) {}
}

Version patchée (mutex FreeRTOS)

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>

#define LOOPS 1000000

static int counter = 0;
static SemaphoreHandle_t counterMutex = NULL;

static void TaskA(void *pvParameters) {
    (void)pvParameters;
    for (int i = 0; i < LOOPS; i++) {
        xSemaphoreTake(counterMutex, portMAX_DELAY);
        counter++;
        xSemaphoreGive(counterMutex);
        taskYIELD();
    }
    vTaskDelete(NULL);
}

static void TaskB(void *pvParameters) {
    (void)pvParameters;
    for (int i = 0; i < LOOPS; i++) {
        xSemaphoreTake(counterMutex, portMAX_DELAY);
        counter++;
        xSemaphoreGive(counterMutex);
        taskYIELD();
    }
    vTaskDelete(NULL);
}

int main(void) {
    // init hardware/uart si besoin...

    counterMutex = xSemaphoreCreateMutex();
    if (counterMutex == NULL) {
        for (;;) {}
    }

    xTaskCreate(TaskA, "A", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);
    xTaskCreate(TaskB, "B", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);

    vTaskStartScheduler();

    for (;;) {}
}
Remarques importantes (RTOS)
  • On utilise un mutex (pas un sémaphore “counting”) pour protéger une donnée partagée.
  • portMAX_DELAY veut dire “j'attends jusqu'à obtenir le verrou”.
  • Pour afficher le résultat, on fait souvent une 3e tâche qui attend la fin, ou on déclenche un flag/notification.
Retour à la matière