Laboratoire 3 : Multiplexage vidéo temps réel sur système embarqué

Systèmes embarqués temps réel (GIF-3004)

Objectifs

Ce travail pratique vise les objectifs suivants :

  1. Mettre en place une arborescence de processus temps réel sur un système embarqué;
  2. Comprendre la communication interprocessus par mémoire partagée;
  3. Se familiariser avec les primitives de synchronisation spécifiques au temps réel, en particulier pour éviter le problème de l'inversion de priorité;
  4. Analyser l'impact des différents modes d'ordonnancement offerts par le noyau Linux;
  5. Utiliser une projection en mémoire (memory map) pour la lecture d'un fichier;
  6. Implémenter un système temps réel sur une plateforme limitée en puissance de calcul et comprendre les optimisations à apporter lors de la compilation.

Présentation du projet

Dans ce laboratoire, votre tâche est d'implémenter un système de multiplexage de vidéos qui pourrait, par exemple, être intégré dans un système de surveillance. Plus formellement, vous devez lire plusieurs vidéos en parallèle, appliquer certains effets sur ces derniers (redimensionnement, filtrage, passage en niveaux de gris, etc.), puis les composer dans un affichage qui permet de les voir simultanément. Votre système doit être en mesure de prioriser les divers programmes le composant afin d'obtenir une bonne fluidité (mesurée en images par seconde). En particulier, le fait qu'une source vidéo requiert trop de ressources (et ne soit donc pas en mesure d'offrir un affichage fluide) ne doit pas empêcher les autres d'être fluides. Ce système doit fonctionner sur le même principe que les pipes, au sens où chaque programme doit pouvoir être chaîné avec les autres. Cependant, les processus ne communiqueront pas en utilisant des pipes, qui sont peu adaptés à un système temps réel, mais plutôt via des zones mémoire partagées. Par ailleurs, l'affichage des vidéos est effectué à un très bas niveau afin d'éviter les délais imprédictibles causés par l'utilisation d'un serveur d'affichage classique.

Préparation et outils nécessaires

Ce laboratoire ne nécessite l'installation d'aucune librairie particulière. Toutefois, vous devez récupérer certains fichiers C/C++ qui sont disponibles sur le dépôt Git suivant : https://bitbucket.org/GIF3004/laboratoire3/. Ce dépôt contient des fonctions qui vous sont fournies pour implémenter certaines fonctionnalités du système. De même, plusieurs fichiers d'en-tête (headers) vous sont fournis afin de vous guider dans l'implémentation du système. Votre Raspberry Pi 3 devra être branché sur un écran pour pouvoir observer le résultat. Utilisez le câble HDMI qui vous a été fourni à cet effet.

Configuration d'Eclipse

Aucune librairie externe n'étant utilisée, vous pouvez vous contenter de créer un projet standard. Toutefois, assurez-vous d'utiliser le mode C/C++, car contrairement aux laboratoires précédents, certains fichiers fournis sont en C++ et nécessitent un compilateur adapté. Nous vous conseillons toujours de programmer en C, mais vous aurez donc besoin d'un compilateur C++ pour compiler le projet. L'interface entre les fichiers C et C++ est suffisamment limitée pour ne pas poser de problème.

Dans le cadre de ce laboratoire, vous devrez produire beaucoup d'exécutables différents. Il est bien entendu possible de créer autant de projets que d'exécutables, mais cela peut s'avérer fastidieux. Plus efficacement, il est possible de configurer Eclipse pour générer plusieurs exécutables à partir d'un même projet. Voyez par exemple ce court tutoriel. Notez que cette étape est facultative : vous ne serez pas pénalisés si vous créez un projet par exécutable.

Fichiers de test

Afin de vous permettre de tester votre approche, nous vous fournissons des vidéos adaptées au projet à cette adresse. Ceux-ci sont issus des projets de la Fondation Blender et peuvent être distribués librement. Vous pouvez tester votre projet avec d'autres vidéos, mais assurez-vous que vous possédez les droits pour le faire. Notez que ces vidéos sont encodées de manière spéciale et que vous ne serez pas capables de les ouvrir avec un lecteur classique. Nous reviendrons plus loin sur les spécificités de ce format.

Architecture générale

La figure suivante expose l'architecture générale du système.

Dans ce diagramme, chaque boîte correspond à un processus distinct. C'est donc dire qu'il y a 10 processus actifs dans le système exposé plus haut. Toutes les communications entre les processus se font via des espaces mémoire partagés. La plupart des processus acceptent à la fois un espace partagé en entrée et en sortie. Les processus décodeurs n'utilisent cependant qu'une sortie, puisque leur entrée est constituée d'un fichier vidéo. De même, le compositeur possède plusieurs entrées (une pour chaque flux vidéo), mais n'a pas de sortie, puisqu'il affiche directement le résultat à l'écran.

Tous les processus sont des processus temps réel et la plupart possèdent des dépendances (par exemple, le processus de filtrage de la seconde colonne doit attendre le résultat du processus de redimensionnement, qui nécessite lui-même le résultat du processus de décodage). Il deviendra donc crucial d'assurer un bon ordonnancement de ceux-ci.

Création d'un espace mémoire partagé

Un espace mémoire partagé peut être créé en utilisant la fonction shm_open. Cet espace mémoire possédant initialement une taille de zéro, il faut donc l'agrandir en utilisant la fonction ftruncate. Finalement, cet espace est, par défaut, vu comme un fichier. Pour faciliter son utilisation, nous utilisons un appel mmap qui permet d'utiliser cet espace d'échange comme un pointeur normal. La structure de cette zone mémoire partagée est présentée à la figure suivante.

Les quatre premiers octets permettent de stocker une primitive de synchronisation. Les huit octets suivants contiennent des compteurs permettant de déterminer la trame sur laquelle travaillent les processus écrivain et lecteur (et ainsi éviter de traiter deux fois la même trame). Les 12 octets suivants rapportent des informations à propos des images qui seront transmises. Ils sont suivis d'un entier indiquant le nombre d'images par seconde fournies par la source vidéo. Finalement, les données sont présentées sous la forme d'un tableau de char, dont la longueur dépend des dimensions de l'image.

Synchronisation

La communication se faisant par espace mémoire partagé, il est important que les différents processus soient synchronisés. Par exemple, lorsque le processus écrit dans la mémoire, le lecteur doit attendre la fin de cette écriture pour lire, au risque de se retrouver avec une image composée de deux trames différentes. De même, si le processus écrivain n'est pas capable de fournir un débit suffisant, le processus lecteur ne doit pas recommencer le traitement de la même trame.

Le problème de synchronisation est encore complexifié par le fait qu'il faille gérer l'inversion de priorité. Cela signifie que nous ne pouvons pas utiliser directement un mutex ou un sémaphore sans une configuration particulière pour gérer cette situation. Il faut absolument configurer le mutex avec les attributs lui permettant d'être partagé entre les processus et de tenir compte de potentielles inversions de priorité.

Un mutex peut être référencé par un simple pointeur. Il est donc possible de le partager facilement dans un espace mémoire partagé. Dans notre cas, il nous permet de bloquer le lecteur tant que l'écrivain n'a pas écrit une trame complète et de bloquer l'écrivain tant que le lecteur n'a pas copié l'entièreté de la trame. Vous êtes libres de concevoir votre propre algorithme de synchronisation adapté à ce problème, mais nous vous fournissons une approche fonctionnelle que vous pouvez utiliser. Celle-ci est illustrée à la figure suivante.

Dans ce diagramme, l'axe temporel va de haut en bas. On peut distinguer trois sections distinctes : l'initialisation, l'écriture d'une trame et la lecture d'une trame. Celles-ci sont détaillées dans le texte qui suit.

L'initialisation permet de s'assurer que les processus commencent leur tâche dans un état connu, et ce peu importe l'ordre dans lequel ils ont été lancés. La création de l'espace mémoire partagé est toujours effectuée par le processus écrivain. Le processus lecteur se contente de tenter de l'ouvrir et recommence tant que cette opération échoue. Comme il est également possible que l'écrivain ait créé l'espace partagé mais ne l'ait pas encore agrandi, le lecteur doit également vérifier la taille de l'espace partagé en faisant appel à fstat. Si cette taille est plus grande que zéro, alors il attend que le compteur de l'écrivain ait été incrémenté. Le processus écrivain s'assure de son côté d'avoir créé et acquis le mutex avant d'incrémenter ce compteur. Lorsque le lecteur constate cette incrémentation, il se met en attente sur le mutex. À ce stade, l'écrivain peut commencer son travail et le lecteur sait qu'un écrivain actif est présent, nous passons donc à la boucle principale.

L'écrivain possédant le mutex, il peut modifier la zone mémoire partagée sans risque de problème. Lorsqu'il a terminé, il copie la valeur du compteur du lecteur puis libère le mutex. Le lecteur acquiert ce dernier et incrémente son compteur. Il peut alors lire le contenu de l'espace partagé sans risque de le voir être modifié. Lorsqu'il a terminé sa lecture, il copie la valeur du compteur de l'écrivain dans une variable locale puis libère le mutex. Du côté de l'écrivain, la première chose à faire lorsqu'il libère le mutex est de vérifier si le compteur du lecteur a été incrémenté : si ce n'est pas le cas, cela signifie que le lecteur n'a pas encore pu acquérir le mutex et donc que même si ce dernier est libre, il ne sert à rien de l'acquérir. L'écrivain boucle donc tant que le lecteur n'incrémente pas son compteur. Lorsque c'est le cas, l'écrivain sait que le lecteur possède le mutex et se met en attente sur ce dernier. Lorsque le lecteur a terminé et que l'écrivain récupère le mutex, il incrémente son compteur, ce qui permet au lecteur de savoir qu'il peut se remettre en attente sur le mutex, et le cycle recommence.

Le fichier commMemoirePartagee.h contient des déclarations de structure et de fonctions qui pourront vous être utiles pour implémenter l'algorithme de synchronisation. N'oubliez pas d'être génériques : tous les programmes que vous écrirez ont besoin de ces primitives de synchronisation, il est donc souhaitable d'utiliser toujours les mêmes fonctions!

Considérations importantes : un processus temps réel ne peut être interrompu par le noyau (sauf cas spécifiques, dépendants de l'ordonnanceur utilisé). Par conséquent, lorsque votre processus ne peut s'exécuter, vous devez redonner la main aux autres processus en utilisant soit un appel système qui permet explicitement à l'ordonnanceur de vous interrompre (par exemple, l'attente sur un mutex fait partie de ces opérations), soit en appelant la fonction sched_yield pour indiquer que vous acceptez volontairement de laisser le CPU à d'autres processus. Notez finalement que la plupart des programmes sont à la fois des lecteurs et des écrivains!

Gestion de la mémoire

Dans un programme standard, l'allocation mémoire peut se faire dynamiquement en utilisant par exemple malloc. Toutefois, dans un programme temps réel, cette fonction introduit des délais variables indésirables. Une fois dans la section critique d'un programme temps réel, il faut donc se passer de malloc. Par ailleurs, le noyau peut techniquement toujours déplacer des zones mémoire allouées, que ce soit ailleurs en mémoire ou sur le disque, ce qui n'est encore une fois pas souhaitable dans un cadre temps réel. Ces problèmes peuvent être réglés de la manière suivante :

  1. Lors de l'initialisation du programme, lorsqu'il n'a pas encore commencé sa section critique, allouez dynamiquement (en utilisant malloc) une zone mémoire suffisamment grande pour contenir quatre ou cinq fois la taille des images que votre programme doit traiter. Vous pouvez par la suite remplacer les appels à malloc par une fonction personnalisée, par exemple tempsreel_malloc, qui va utiliser cette zone mémoire comme un bassin de mémoire (memory pool). Lorsqu'une partie de votre programme requiert une allocation mémoire, votre fonction va chercher s'il existe un emplacement libre dans ce bassin et la lui retourner. De même, lorsque la mémoire est libérée (par exemple en utilisant tempsreel_free), la fonction va indiquer que le bloc est maintenant libre.
  2. Afin d'assurer que vos allocations mémoire vont rester en mémoire, utilisez la fonction mlock ou mlockall. Ces fonctions permettent de « fixer » un buffer en mémoire, au sens où le système d'exploitation garantit que leur emplacement ne changera pas tant que vous ne l'accepterez pas explicitement. Par défaut, un programme ne peut « fixer » un nombre limité d'octets. Vous pouvez utiliser la fonction setrlimit avec l'argument RLIMIT_MEMLOCK pour modifier cette valeur. Toutefois, votre programme devra alors être exécuté en tant que root (ou en utilisant sudo).

Des en-têtes implémentant une interface possible pour ce gestionnaire de mémoire simple vous est fourni dans allocateurMemoire.h.

Afin d'éviter que vous dépassiez par inadvertance la capacité mémoire du Raspberry Pi, il vous est conseillé de désactiver le swap sur la carte SD avec la commande sudo swapoff -a.

Arguments des programmes

Tous les programmes possèdent une interface ligne de commandes similaire, soit :

./nomduprogramme [options] flux_entree flux_sortie

flux_entree et flux_sortie constituent respectivement les identifiants des zones mémoire partagées en entrée et en sortie. Les options sont quant à elles optionnelles. Parmi celles devant être disponibles pour tous les programmes, nous retrouvons :

  • "-a", qui détermine l'affinité processeur. Cette option peut recevoir comme valeur soit un nombre entre 0 et 3, soit la lettre "A", soit la lettre "N" suivie d'un chiffre entre 0 et 3. Le premier cas indique que le processus doit s'exécuter uniquement sur le coeur CPU dont l'index est passé en paramètre, le second cas indique que le processus peut s'exécuter partout et le troisième cas permet au processus de s'exécuter partout sauf sur le coeur dont l'index suit la lettre "N". Par défaut, la valeur est "A".

  • "-s" qui détermine le type d'ordonnancement voulu. Il peut prendre la valeur NORT (ordonnancement normal), RR (ordonnancement temps réel avec préemption), FIFO (ordonnancement temps réel sans préemption) ou DEADLINE (ordonnancement de type plus proche échéance). Toute autre valeur est invalide. Par défaut, la valeur est "NORT". Dans le cas de l'ordonnanceur DEADLINE, trois autres paramètres sont optionnels et peuvent être fournis, séparés par des virgules, avec l'option "-d". Dans le cas de NORT, vous n'avez aucune opération à faire, puisque cette option correspond au mode par défaut de l'ordonnanceur. Dans les autres cas, vous devez utiliser sched_setscheduler ou sched_setattr pour choisir le mode d'ordonnancement demandé. Notez que les modes temps réel nécessitent les droits d'administrateur pour pouvoir être utilisés, vous devez donc lancer votre programme avec sudo.

Dans tous les cas, vous pouvez assumer que les arguments donnés à vos programmes seront toujours valides : vous n'avez pas à effectuer de vérification à cet effet. Toutefois, l'ordre des options n'est pas garanti (celui flux_entree / flux_sortie l'est quant à lui). Nous vous recommandons fortement d'utiliser une librairie comme getopt pour vous aider dans l'analyse des arguments.

Modules à implémenter

Vous avez cinq modules (programmes) à implémenter :

  • Décodeur : lit un fichier vidéo, décode les trames qu'il contient et les écrit une par une dans un espace mémoire partagé.
  • Compositeur : reçoit de un à quatre espaces mémoire et affiche les vidéos en simultané sur l'écran.
  • Redimensionneur : redimensionne les images qu'il reçoit en entrée, que ce soit vers une taille plus petite ou plus grande que celle courante.
  • Filtreur : applique un filtre passe-bas ou passe-haut sur l'image.
  • Convertisseur niveaux de gris : convertit une image RGB (3 canaux) en niveaux de gris (1 canal).

Les sous-sections suivantes détaillent chacun de ces programmes.

Décodeur

Le décodeur est responsable de la lecture d'un fichier vidéo. Son premier argument n'est donc pas l'identifiant d'une zone mémoire partagée, mais plutôt un fichier contenant le vidéo. Les formats vidéo (mpeg, h264, hevc, etc.) étant difficiles à gérer en temps réel et sans librairies externes, vous utiliserez un format vidéo spécialement créé pour ce laboratoire, le format ULV (Université Laval Vidéo). Ce format est présenté dans la figure suivante.

Tout fichier ULV commence donc par 4 octets qui correspondent aux lettres S, E, T et R. Cela permet au programme de s'assurer que le fichier qu'il tente de lire est du bon format. Par la suite, 4 octets sont utilisés pour indiquer la largeur des images du vidéo, puis 4 octets pour leur hauteur et 4 octets pour le nombre de canaux. Finalement, 4 octets permettent d'indiquer le nombre d'images par seconde du vidéo (usuellement 30, mais ce nombre peut varier). Les trames du vidéo sont par la suite enregistrées une par une. Pour chaque trame, un entier non signé de 32 bits indique la taille, suivi d'un tableau d'octets. Ce tableau ne contient pas directement l'image, mais son encodage JPEG. Vous devez donc décompresser l'image, nous vous fournissons une fonction le faisant pour vous dans le fichier jpgd.h. Les images sont ainsi encodées à la suite, jusqu'à la dernière qui est suivie d'un entier de 4 octets contenant 0. Cette valeur n'étant pas une taille valide pour une image, vous savez alors que vous avez atteint la fin du fichier. Vous devez alors recommencer la lecture au début du fichier, en boucle.

Afin de vous permettre d'expérimenter avec d'autres vidéos et de voir le résultat attendu, nous vous fournissons deux scripts Python, disponible sur le dépôt Git, capables de convertir (videoConverter.py) et de lire (videoReader.py) les vidéos au format ULV. Ceux-ci requièrent scipy et OpenCV pour fonctionner. Notez que vous n'êtes pas forcés de les utiliser et qu'aucune fonctionnalité du laboratoire ne dépend du bon fonctionnement de ces scripts, qui vous sont fournis à titre d'exemple uniquement.

Note importante : l'affichage du Raspberry Pi 3 gère les images en mode BGR et non RGB (les canaux rouge et bleu sont inversés). Par conséquent, le format ULV enregistre ces images en inversant les canaux rouge et bleu. Les scripts fournis tiennent compte de cette inversion et restituent l'image en réinversant les canaux, mais ne soyez pas surpris par cette inversion de canal si vous essayez d'afficher une trame par vous-mêmes.

Tel que mentionné plus haut, le programme décodeur est lancé en spécifiant un fichier comme premier argument au lieu d'une zone mémoire partagée :

./redimensionneur [options] fichier_entree flux_sortie

Les fonctions d'entrée/sortie ne font pas bon ménage avec le temps réel. En effet, la lecture sur un périphérique de stockage induit des délais aléatoires qui ne sont pas souhaitables. Par conséquent, il faut s'assurer que le fichier soit entièrement copié en mémoire RAM avant de commencer. Pour ce faire, utilisez la fonction mmap sur le descripteur de fichier, accompagnée de l'argument MAP_POPULATE. Vous pourrez ensuite lire le fichier comme s'il s'agissait d'un tableau en mémoire.

Dans le cadre de ce TP, vous pouvez assumer que la somme totale des tailles des fichiers vidéo ne dépassera jamais la taille de la RAM. En d'autres termes, copier les fichiers dans la RAM lors de l'initialisation du décodeur ne devrait jamais causer une erreur par manque de mémoire.

Compositeur

Le compositeur est responsable de l'affichage des flux vidéos. Il doit pouvoir accepter de 1 à 4 flux vidéos distincts, mais possédant les mêmes dimensions, qui seront affichés comme suit (le grand rectangle noir représente l'écran).

La façon de l'appeler sur la ligne de commande diffère quelque peu des autres programmes :

./compositeur [options] flux_entree1 [flux_entree2] [flux_entree3] [flux_entree4]

où flux_entreeN correspond au flux vidéo #N dans les schémas ci-dessous. Le premier flux doit être précisé, les flux vidéo 2 à 4 sont optionnels. Par exemple :

./compositeur -a 3 /flux1_mem /flux2_mem

exécutera le compositeur sur le coeur #3, en affichant deux flux vidéo en simultané.

Considérant l'aspect temps réel de ce laboratoire, l'utilisation d'une interface graphique est à proscrire. Par conséquent, il nous faut donc utiliser des primitives d'affichage de plus bas niveau. Dans ce laboratoire, l'affichage se fait en écrivant directement dans le framebuffer du système. Le framebuffer est un espace mémoire utilisé pour communiquer directement avec la carte graphique. C'est donc dire que nous écrirons directement la valeur de chacun des pixels dans une mémoire lue par la carte vidéo. Cette interface en est une de très bas niveau, aucune abstraction n'est fournie pour, par exemple créer des fenêtres ou des zones dans l'écran. Sa configuration peut également être malaisée. Afin de faciliter votre tâche, nous vous fournissons un fichier nommé compositeur.c qui contient déjà plusieurs fonctions d'affichage. Vous devez le compléter avec le code lui permettant d'aller récupérer les images dans une zone mémoire partagée. Voyez les commentaires inclus dans ce fichier pour plus de détails.

En plus d'afficher les vidéos, le compositeur doit également écrire un fichier nommé stats.txt contenant le nombre d'images par seconde effectif pour chaque source vidéo (autrement dit, le nombre d'images réellement affichées chaque seconde, et non pas le nombre théorique).

Redimensionneur

Le redimensionneur prend en entrée une image d'une taille arbitraire et retourne en sortie la même image, mais redimensionnée aux dimensions demandées. Le redimensionnement peut être fait par une méthode des plus proches voisins (rapide, mais peu précise) ou par interpolation bilinéaire (plus lente, mais produisant de meilleurs résultats). Dans les deux cas, les fonctions opérant ce redimensionnement vous sont fournies dans le fichier utils.h.

En plus des options normales, ce processus requiert "-w" et "-h" (largeur et hauteur des images en sortie) et "-m", qui peut prendre les valeurs 0 ou 1, 0 correspondant à un redimensionnement au plus proche voisin et 1 à une interpolation linéaire.

./redimensionneur [options] flux_entree flux_sortie

Filtreur

Le filtreur filtre l'image qu'il reçoit en entrée avec un filtre passe-bas ou passe-haut. Il écrit l'image résultante dans son espace partagé en sortie. Les fonctions opérant ces opérations de filtrage vous sont fournies dans le fichier utils.h. Sa ligne de commande accepte un paramètre supplémentaire "-t", qui détermine le type du filtre et peut valoir soit 0 pour un filtre passe-bas, soit 1 pour un filtre passe-haut :

./filtreur [options] flux_entree flux_sortie

Convertisseur vers niveaux de gris

Comme son nom l'indique, ce programme convertit son entrée RGB en niveaux de gris. La balance de chaque canal est effectuée selon l'espace de couleur CIE 1931. La fonction opérant cette conversion est déjà codée pour vous et disponible dans le fichier utils.h. Ce programme ne possède pas d'arguments supplémentaires sur sa ligne de commande.

Lancement des programmes

Le compositeur écrivant directement dans le framebuffer du système, le terminal sur lequel les programmes sont lancés n'a pas d'importance. Vous pouvez donc lancer les programmes à partir d'une session à distance (SSH) et néanmoins voir le résultat s'afficher à l'écran. Vous devez toutefois lancer plusieurs programmes simultanément. Bien que cela puisse être fait de manière entièrement manuelle, nous vous conseillons d'utiliser un script bash accélérer le tout. Par exemple, un script lançant les processus correspondant à l'architecture présentée à la figure 1 pourrait ressembler à ceci.

#!/bin/bash
./decodeur -a 0 video1.ulv /mem1 &
./decodeur -a 1 video2.ulv /mem2 &
./redimensionneur -a 2 -w 512 -h 320 -m 0 /mem2 /mem5 &
./filtreur -a 1 -t 1 /mem5 /mem7&
./decodeur -a 2 video3.ulv /mem3 &
./redimensionneur -a 2 -m 0 -w 256 -h 160 /mem3 /mem6 &
./decodeur -a 1 video4.ulv /mem4 &
./convertisseur -a 0 /mem4 /mem8 &
./redimensionneur -a 2 -w 256 -h 160 -m 0 /mem8 /mem9 &
./compositeur -a A /mem1 /mem7 /mem6 /mem9

Notez les & après chaque commande, ils permettent de lancer une commande en arrière-plan, ce qui nous permet de lancer plusieurs programmes en parallèle. Bien évidemment, vos premiers essais n'ont pas à être aussi complexes.

Rappelez-vous que le décodeur lit le fichier source en boucle. Si vous souhaitez arrêter tous les programmes, vous pouvez :

  1. Couper le compositeur (par exemple en utilisant Ctrl+C)
  2. Terminer toutes les tâches d'arrière-plan dans le même terminal où elles ont été lancées (utilisez par exemple kill $(jobs -p))

Nous vous fournissons plusieurs scripts correspondant à des configurations de test possibles dans le répertoire configs du dépôt Git. Le numéro au début du nom des scripts correspond à un classement grossier de leur difficulté pour le Raspberry Pi. Nous vous conseillons de commencer par les plus simples; n'oubliez pas qu'il est possible que certaines configurations ne puissent tout simplement pas être fluides! Référez-vous aux commentaires fournis dans chaque script pour plus de détails. Vous pouvez par ailleurs bien sûr développer vos propres configurations.

Temps réel et optimisation

Essais des différents modes de l'ordonnanceur

Une fois les différents programmes implémentés, vous pouvez passer au test de ceux-ci. Commencez par lancer ces programmes en mode normal (non temps réel) et observez les résultats en utilisant 1, 2, 3 ou 4 vidéos de différentes tailles. Analysez le fichier stats.txt pour déterminer les sources posant le plus problème.

Par la suite, lancez ces programmes en temps réel, avec l'ordonnanceur de base (SCHED_RR); vous devrez pour cela modifier votre code pour y ajouter des appels à l'ordonnanceur. Observez-vous une différence?

Finalement, pour la dernière étape, utilisez l'ordonnanceur SCHED_DEADLINE. Pour cela, vous devrez d'abord mesurer le temps d'exécution estimé pour chaque programme selon ses paramètres d'entrée. Ne cherchez pas à être trop précis et ne passez pas trop de temps à mesurer ces temps, mais assurez-vous d'avoir des estimations fiables. Vous pourrez ensuite passer ces valeurs à l'ordonnanceur et observer la différence par rapport aux autres modes d'ordonnancement. Dans quels cas cela fait-il le plus de différence?

Note importante : certaines combinaisons de vidéos requièrent tout simplement trop de temps pour être traitées en temps réel par le Raspberry Pi (c'est par exemple le cas si vous ouvrez une vidéo en 480p et appliquez un filtre et un redimensionnement avec interpolation). L'ordonnanceur ne peut pas faire de miracle, si le temps CPU demandé par un groupe de programme excède la capacité totale de l'ordinateur, il n'y a rien à faire. Toutefois, remarquez ce qui se passe lorsque, en même temps que ces tâches trop longues, vous lancez une autre tâche qui, elle, pourrait s'exécuter dans les temps. Le choix du mode d'ordonnancement améliore-t-il sa fluidité?

Optimisation de la compilation

Comme vous allez le constater, les tâches demandées au Raspberry Pi dans ce laboratoire sont très exigeantes. Il est donc crucial d'optimiser le code au maximum pour éviter tout délai intempestif. Par défaut, le compilateur GCC/G++ n'inclut pas beaucoup d'optimisations. Il est donc fortement recommandé d'activer les optimisations du compilateur, avec les options suivantes recommandées : -Ofast -funroll-loops -floop-parallelize-all -floop-block -flto. Vous pouvez ajouter ces options de compilation dans Eclipse en allant dans les propriétés du projet, puis, dans les Settings du compilateur, sélectionner l'item Optimization (voir la figure suivante). Notez que ces optimisations retirent beaucoup de choses utiles au débogage : assurez-vous que votre code est fonctionnel avant de les activer. Eclipse possède deux configurations de compilation distinctes (Debug et Release) pour cette raison précise : utilisez-les!

Par la suite, allez encore plus loin et gagnez encore de 5 % à 10 % de performance en profilant le code. Ce traitement s'effectue en deux passes et nécessite une application fonctionnelle. Premièrement, compilez votre code avec l'option -fprofile-generate et exécutez le sur le Raspberry Pi pendant une période de temps représentative. Cela générera des fichiers .gcda. Récupérez ces fichiers depuis votre Raspberry Pi (utilisez scp ou FileZilla, par exemple) et recompilez avec l'option -fprofile-use en prenant soin de fournir les fichiers .gcda dans le répertoire de compilation. GCC utilisera alors les informations de profilage pour optimiser encore plus le code produit.

Modalités d'évaluation

Ce travail doit être réalisé en équipe de deux, la charge de travail étant à répartir équitablement entre les deux membres de l'équipe. Aucun rapport n'est à remettre, mais les deux équipiers doivent être en mesure individuellement d'expliquer leur approche et de démontrer le bon fonctionnement de l'ensemble de la solution de l'équipe du laboratoire. Cette évaluation sera faite lors de la séance de laboratoire du 17 mars 2017, et les deux équipiers doivent y être présents. Si vous ne pouvez pas vous y présenter, contactez l'équipe pédagogique du cours dans les plus brefs délais afin de convenir d'une date d'évaluation alternative. Ce travail compte pour 10 % de la note totale du cours.

Le barème d'évaluation détaillé sera le suivant (laboratoire noté sur 20 points) :

  • (4 pts) Vos programmes sont en mesure d'utiliser des espaces mémoire partagés pour communiquer entre eux.
  • (3 pts) La synchronisation entre vos programmes est adéquate et fonctionnelle (c.-à-d. pas d'images coupées ou d'autres problèmes visuels).
  • (5 pts) Votre système est en mesure d'afficher une ou plusieurs vidéos sur l'écran de manière fluide et de chaîner les traitements effectués sur ces vidéos.
  • (4 pts) Vous utilisez les fonctions de l'ordonnanceur pour sélectionner différents modes d'ordonnancement et êtes en mesure d'expliquer l'impact de chacun de ces modes.
  • (3 pts) Vos programmes respectent les contraintes des programmes temps réel (pas d'allocation mémoire dynamique dans la section critique, pas d'entrée/sortie autrement que pour les fichiers de log, etc.)
  • (1 pts) Les étudiants sont en mesure d'expliquer l'approche utilisée et de répondre aux questions concernant leur code.

Ressources et lectures connexes