Slide 35 VISUEL
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”.
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.
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.
- Lire counter
- Ajouter 1
- 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 (;;) {}
}
- 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.