Archive for juillet 2011

Éviter les inversions de priorité causées par des mutex

Linux, Temps-réel | Publié par cpb
juil 29 2011

Dans les applications temps-réel il est très important d’éviter les situations dites « d’inversion de priorité ». Il s’agit de cas dans lesquels une tâche de haute priorité est bloquée en attente de la terminaison d’une tâche de plus faible priorité alors qu’elles n’ont rien en commun.

Problème d’inversion de priorité

Examinons un cas classique, reposant sur trois tâches T1, T2 et T3 de priorités strictement croissantes ainsi qu’un mutex M.

Rappelons qu’un mutex est un élément logiciel permettant d’assurer l’exclusion mutuelle de tâches concurrentes. Une fois qu’une tâche a verrouillé le mutex, les autres ne pourront pas l’obtenir avant qu’elle le déverrouille. On utilise ceci pour synchroniser l’accès à des ressources partagées.

 

  1. Initialement, T2 et T3 sont endormies, en attente d’événements externes (par exemple des interruptions matérielles, ou l’expiration de timers). T1 s’exécute et va accéder à une ressource partagée avec T3 (par exemple une zone de mémoire commune). Aussi T1 verrouille-t-elle le mutex M.
  2. L’événement externe qu’attendait T3 se produit. La tâche T3 se réveille, et préempte T1 car elle est plus prioritaire. T3 est donc en exécution, T2 endormie et T1 préemptée.
  3. La tâche T3 va devoir accéder à son tour à la ressource partagée avec T1. Pour cela elle doit d’abord verrouiller le mutex M, toutefois ce dernier est déjà tenu par T1. T3 va donc s’endormir en attendant la libération de M, et T1 va reprendre son exécution. Jusqu’ici tout se déroule parfaitement normalement.
  4. A présent, l’événement attendu par T2 survient. Cette tâche va donc se réveiller. Comme elle est plus prioritaire que T1, cette dernière est préemptée et la situation est la suivante:
      • T1 préemptée tient le mutex M
      • T2 s’exécute
      • T3 endormie attend le mutex M

La tâche T3 est bloquée par l’exécution d’une tâche T2 moins prioritaire, nous sommes dans une situation d’inversion de priorité.

Il est normal que T3 soit bloquée par l’exécution de T1, car ces deux tâches partagent un élément commun (une zone mémoire par exemple) et se synchronisent en utilisant un mutex. Mais il n’est pas normal que T3 soit bloquée alors qu’une tâche T2 avec laquelle elle ne partage rien s’exécute tranquillement.

Le cas survient généralement lorsque T1 et T2 sont approximativement de même priorité et T3 beaucoup plus élevée (par exemple T1 de priorité 10, T2 de priorité 12 et T3 de priorité 90).

Un exemple typique est le problème survenu au robot Pathfinder lors de sa mission sur Mars en 1997. Un descriptif détaillé est présenté dans un article de Glenn E. Reeves. En résumé T1 était une tâche de pilotage d’un outil nommé ASI/MET qui réalisait des analyses météorologiques (très basse priorité), T3 était la tâche principale de transfert des données entre les sous-systèmes et T2 était représenté par un ensemble de tâches de faibles priorités (mais plus élevées que T1). Enfin un sémaphore intégré dans l’implémentation de l’appel système select() – invoqué par T1 et par T3 – jouait le rôle de notre mutex.

Construisons un premier exemple afin de mettre en relief le comportement lors d’une inversion de priorité. Dans le programme suivant, nous allons créer trois threads avec des rôles spécifiques :

  • T1, de faible priorité, démarre en premier et verrouille le mutex. Ensuite T1 va réaliser un nombre arbitraire de boucles actives (à ajuster en fonction de votre processeur afin qu’elles durent une dizaine de secondes) simulant un travail consommant du temps CPU, et libérera le mutex avant de se terminer..
  • T2 de priorité moyenne va dormir deux secondes avant de démarrer sa série de boucles actives d’une dizaine de secondes.
  • T3 de priorité élevée dort une seconde, verrouille le mutex (et devra donc patienter jusqu’à la fin de T1) et attaque une série de boucle active.

Pour que l’inversion se produise, il est nécessaire que les priorités soient fixées de manière précise, aussi utiliserons-nous un ordonnancement temps-réel FIFO. Notez que l’exécution du programme ne pourra réussir qu’avec les droits root. Je déconseille de réaliser une boucle active avec une priorité supérieure ou égale à 50, car c’est la priorité par défaut des gestionnaires d’interruption sur les noyaux Linux récents. Si un handler d’interruption est préempté trop longtemps (20 à 30 secondes), le driver peut cesser de fonctionner, c’est souvent le cas avec les cartes Wifi par exemple.

Le programme ci-dessous n’a d’intérêt que s’il y a une lutte pour le CPU entre T1 et T2, ce qui signifie qu’ils doivent s’exécuter sur le même processeur (même coeur).

test-inversion.c
#define _GNU_SOURCE   // Pour sched_setaffinty
#include
#include
#include
#include
#include
#include 

static void * T1 (void * unused);
static void * T2 (void * unused);
static void * T3 (void * unused);

static pthread_mutex_t  mutex = PTHREAD_MUTEX_INITIALIZER;

int main(void)
{
    cpu_set_t cpu_set;
    pthread_t t1, t2, t3;

    // Fixer l'execution sur un seul coeur
    CPU_ZERO(& cpu_set);
    CPU_SET(0, & cpu_set); // Coeur 0
    if (sched_setaffinity(0, sizeof(cpu_set), & cpu_set) != 0) {
        perror("sched_setaffinity");
        exit(EXIT_FAILURE);
    }

    // Creer les trois threads
    if ((pthread_create(& t1, NULL, T1, NULL) != 0)
     || (pthread_create(& t2, NULL, T2, NULL) != 0)
     || (pthread_create(& t3, NULL, T3, NULL) != 0)) {
        fprintf(stderr, "Erreur de creation des threads");
        exit(EXIT_FAILURE);
    }
    // Terminer le thread main
    pthread_exit(NULL);
}

#define BOUCLE_1  50000
#define BOUCLE_2  50000

static void * T1 (void * unused)
{
    int i;
    int j;
    struct sched_param param;

    fprintf(stderr, "T1 : Creation\n");

    // passer en temps reel faible priorite
    param.sched_priority = 5;
    if (pthread_setschedparam(pthread_self(), SCHED_FIFO, & param) != 0) {
        perror("pthread_setschedparam");
        exit(EXIT_FAILURE);
    }

    // verrouiller le mutex
    pthread_mutex_lock(& mutex);

    // travail actif
    fprintf(stderr, "T1 : Debut boucle\n");
    for (i = 0; i < BOUCLE_1; i ++)
        for (j = 0; j < BOUCLE_2; j ++)
            ;
    fprintf(stderr, "T1 : Fin Boucle\n");

    // liberer le mutex
    pthread_mutex_unlock(& mutex);
    return NULL;
}

static void * T2 (void * unused)
{
    int i;
    int j;
    struct sched_param param;

    fprintf(stderr, "T2 : Creation\n");

    // passer en temps reel moyenne priorite
    param.sched_priority = 15;
    if (pthread_setschedparam(pthread_self(), SCHED_FIFO, & param) != 0) {
        perror("pthread_setschedparam");
        exit(EXIT_FAILURE);
    }
    // attendre deux secondes
    sleep(2);
    // travail actif
    fprintf(stderr, "T2 : Debut boucle\n");
    for (i = 0; i < BOUCLE_1; i ++)
        for (j = 0; j < BOUCLE_2; j ++)
            ;
    fprintf(stderr, "T2 : Fin boucle\n");
    return NULL;
}

static void * T3 (void * unused)
{
    int i;
    int j;
    struct sched_param param;

    fprintf(stderr, "T3 : Creation\n");

    // passer en temps reel haute priorite
    param.sched_priority = 45;
    if (pthread_setschedparam(pthread_self(), SCHED_FIFO, & param) != 0) {
        perror("pthread_setschedparam");
        exit(EXIT_FAILURE);
    }
    // attendre une seconde
    sleep(1);
    // prendre le mutex
    pthread_mutex_lock(& mutex);
    // travail actif
    fprintf(stderr, "T3 : Debut boucle\n");
    for (i = 0; i < BOUCLE_1; i ++)
        for (j = 0; j < BOUCLE_2; j ++)
            ;
    fprintf(stderr, "T3 : Fin boucle\n");
    // liberer le mutex
    pthread_mutex_unlock(&mutex);

    return NULL;
}

Pensez à calibrer les valeurs de BOUCLE_1 et BOUCLE_2 pour que l’exécution du programme dure une dizaine de secondes. Voici un résultat d’exécution :

# cc test-inversion.c -o test-inversion -pthread -lrt -Wall
# ./test-inversion
T3 : Creation
T2 : Creation
T1 : Creation
T1 : Debut boucle
T2 : Debut boucle
T2 : Fin boucle
T1 : Fin Boucle
T3 : Debut boucle
T3 : Fin boucle
#

Nous voyons bien que le thread T2 s’exécute intégralement avant que T3 puisse obtenir le mutex et commencer son travail. C’est un cas d’inversion de priorité

Un remède

Il existe un remède aux problèmes d’inversions de priorités : le système PIP. Toutefois précisons tout de suite qu’il ne fonctionne pas dans tous les cas. Il permit de sauver le robot Pathfinder mais il existe d’autres situations dans lesquelles le remède est pire que le mal et l’on risque de voir les performances du système s’effondrer.

PIP (priority inheritance protocol) est un mécanisme reposant sur deux règles et sur une valeur de priorité appliquée à chaque mutex (et éventuellement à toutes les ressources de synchronisation). Reprenons la situation décrite plus haut, T1 est actif, T2 et T3 dorment. T1 verrouille le mutex. La première règle de PIP s’applique alors :

  • Première règle : un mutex hérite de la priorité la plus élevée parmi celles de tous les threads qui essayent de le verrouiller

Le thread T1 s’exécutant avec la priorité P1, le mutex hérite alors de cette valeur.

Ensuite T3 se réveille et demande à son tour le mutex. La priorité de celui-ci monte alors à la valeur P3. La seconde règle s’applique

  • Seconde règle : un thread s’exécute avec la priorité la plus élevée parmi celles de tous les mutex qu’il a verrouilé.

Le thread T1 est alors propulsé à la priorité P3. Le thread T2 se réveille, mais sa priorité étant à présent inférieure à celle de T1 il ne peut le préempter, et T1 va s’exécuter intégralement, relâcher le mutex, redrescendre alors à la priorité P1. T3 pourra alors prendre le mutex et s’exécuter avant enfin de laisser passer T2.

Dans le programme suivant, j’ai ajouté quelques lignes au début de la fonction main(), qui activent le protocol PIP sur le mutex.

test-pip.c
[...]
int main(void)
{
    cpu_set_t cpu_set;
    pthread_t t1, t2, t3;

    pthread_mutexattr_t attr;
    pthread_mutexattr_init(& attr);
    pthread_mutexattr_setprotocol(& attr, PTHREAD_PRIO_INHERIT);
    pthread_mutex_init(& mutex, & attr);

    // Fixer l'execution sur un seul coeur
    [...]
Voyons l'exécution avec PIP :
# cc test-pip.c -o test-pip -pthread -lrt -Wall
# ./test-pip 
T3 : Creation
T2 : Creation
T1 : Creation
T1 : Debut boucle
T1 : Fin Boucle
T3 : Debut boucle
T3 : Fin boucle
T2 : Debut boucle
T2 : Fin boucle
#

Cette fois, T3 a la possibilité de s’exécuter entièrement avant que T2 démarre. L’inversion de priorité est évitée.

Conclusion

La vraie solution au problème d’inversion de priorité réside dans la conception du système temps-réel. Il ne faut pas laisser de tâches de priorités intermédiaires entre deux tâches partageant des ressources communes. Naturellement ceci est très contraignant et ne peut pas toujours être respecté. PIP représente un correctif plus qu’une véritable solution, mais il a le mérite d’être facilement activable sur les mutex Posix.

[ACTU] 2.6 c’est fini…

Actualité, Linux | Publié par cpb
juil 26 2011

Absent depuis quelques jours (pour cause de Festival d’Avignon) je n’ai pas encore salué le basculement définitif sur la branche 3.0 du noyau Linux survenu vendredi dernier. Un peu plus tôt que je l’imaginais initialement.

Les répertoires du dépôt central Linux Kernel ont été mis à jour, et on trouve

 

J’ajouterais qu’un nouveau patch Preempt-RT (pour améliorer les fonctionnalités temps-réel du noyau standard) a été publié hier pour le noyau 3.0

http://www.kernel.org/pub/linux/kernel/projects/rt/patch-3.0-rt3.patch.bz2

ainsi qu’une archive détaillant tous les petits patches inclus dans le précédent

http://www.kernel.org/pub/linux/kernel/projects/rt/patches-3.0-rt3.tar.bz2

Je testerai ce nouveau patch Preempt-RT rapidement…

Signes de vie d’un système embarqué

Embarqué, Linux, Microprocesseur | Publié par cpb
juil 15 2011

Suite à une question posée par Chriss en commentaire d’un précédent article, j’ai eu envie de revenir rapidement sur un élément essentiel pour la mise au point d’un système embarqué : le signe de vie. Il s’agit simplement de faire effectuer par le système une tâche régulière, avec un effet facilement observable par l’utilisateur, afin de s’assurer du bon fonctionnement global du dispositif. Ce signe de vie peut prendre diverses formes : signal électrique visible à l’oscilloscope sur une broche de test de la carte, trame vide émise régulièrement sur un port réseau, compteur incrémenté périodiquement dans une zone de mémoire partagée, etc. Le signe de vie le plus simple à mettre en oeuvre sur un système embarqué est le clignotement d’une LED.

Premier exemple sur carte Pandaboard

La carte Pandaboard, qui a déjà fait l’objet de plusieurs articles, dispose de trois LEDs. Les deux premières se trouvent à côté du support pour mémoire MicroSD, la troisième est placée à côté du connecteur d’alimentation. Cette dernière n’est pas contrôlable par l’utilisateur, elle indique une surtension d’alimentation. Nous allons donc nous intéresser uniquement aux LEDs 1 et 2.

LEDs sur carte Pandaboard

La première méthode pour accéder à ces LEDs, et en conserver un contrôle complet, va consister à les piloter en programmant les broches GPIO (General Purpose Input Output) du processeur auxquelles les LEDs sont reliées. Nous démarrons la carte en utilisant un kernel Linux 3.0-rc7 compilé (avec ce fichier .config) sans support particulier pour les LEDs. Nous avons simplement activé

  • le support GPIO (dans le menu « Device Drivers »),
  • l’interface « /sys/class/gpio/ » (dans le menu « GPIO Support » activé précédemment),
  • et le driver pour Texas Instruments TWL 4030 et ultérieurs.

Connectons-nous sur la carte Pandaboard, et examinons le contenu du répertoire /sys/class/gpio/.

[~]$ telnet 192.168.3.152
Trying 192.168.3.152...
Connected to 192.168.3.152.
Escape character is '^]'.
(none) login: root
Password:
BusyBox v1.18.4 (2011-06-22 12:27:46 CEST) built-in shell (ash)
Enter 'help' for a list of built-in commands.
(panda)[root]$ cd /sys/class/gpio/
(panda)[gpio]$ ls
export       gpio62       gpiochip128  gpiochip32   gpiochip96
gpio1        gpiochip0    gpiochip160  gpiochip64   unexport
(panda)[gpio]$

Nous voyons que dans ce répertoire le noyau nous propose l’accès à certaines broches GPIO (1 et 62). Toutefois ce ne sont pas celles qui correspondent aux LEDs. En consultant les spécifications de notre carte (pages 48-49), nous voyons que les LEDs 1 et 2 sont connectées aux GPIO 7 et 8. Demandons au noyau d’exporter (c’est-à-dire de nous rendre accessible) la broche GPIO 8. Nous remarquons alors que le contenu du répertoire /sys/class/gpio/ est modifié.

(panda)[gpio]$ echo 8 > export 
(panda)[gpio]$ ls
export       gpio62       gpiochip0    gpiochip160  gpiochip64   unexport
gpio1        gpio8        gpiochip128  gpiochip32   gpiochip96
(panda)[gpio]$

L’entrée gpio8 a fait son apparition, il s’agit d’un sous-répertoire contenant des fichiers virtuels, dont les contenus représentent des paramètres internes du noyau. Voyons donc la direction par défaut de la broche GPIO 8.

(panda)[gpio]$ ls gpio8/
active_low  edge        subsystem   value
direction   power       uevent
(panda)[gpio]$ cat gpio8/direction 
in
(panda)[gpio]$

Le processeur initialise par défaut ses broches GPIO en entrée, pour des raisons de sécurité. Elles présentent alors un état de haute impédance, et les manipulations externes (mises à la masse ou au +5V, court-circuit, etc.) n’ont pas d’influence. Toutefois, pour piloter la LED 2, il faut basculer la broche en sortie.

(panda)[gpio]$ echo out > gpio8/direction 
(panda)[gpio]$

Nous pouvons alors allumer et étendre la LED 2 ainsi :

(panda)[gpio]$ echo 1 > gpio8/value
(panda)[gpio]$ echo 0 > gpio8/value
(panda)[gpio]$

Il devient alors facile de programmer un petit signe de vie en utilisant un script shell simple :

/usr/bin/heartbeat.sh
#! /bin/sh

echo 7 > /sys/class/gpio/export
echo 8 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio7/direction
echo out > /sys/class/gpio/gpio8/direction

while true
do
        echo 1 > /sys/class/gpio/gpio7/value
        usleep 150000
        echo 0 > /sys/class/gpio/gpio7/value
        usleep 100000
        echo 1 > /sys/class/gpio/gpio8/value
        usleep 150000
        echo 0 > /sys/class/gpio/gpio8/value
        usleep 100000
        usleep 500000
done

Lancé en arrière-plan dans le script de démarrage /etc/init.d/rcS ainsi

  /usr/bin/heartbeat.sh &

ce petit script nous permet de contrôler visuellement que le système est opérationnel, et que l’ordonnancement des processus fonctionne normalement (sans qu’une tâche temps-réel ne consomme tout le CPU disponible par exemple).

Deuxième exemple, sur carte IGEP

Sur la carte IGEP v2, que nous avons également abordée précédement, il existe deux LEDs bicolores. La couleur verte de l’une d’entre elles est programmable par l’I2C. Les trois autres couleurs sont accessibles via les GPIO 26, 27, et 28. À noter : lorsque les couleurs rouge et verte de la même LED sont allumées simultanément, la couleur visible est jaune orangée. On peut donc obtenir un signe de vie visuellement plus riche en jouant sur les changements de couleur.

Voici un petit programme C qui assure un scintillement permanent des LEDs et remplit un rôle similaire au premier script ci-dessus.

sdv-igepv2.c
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
	int i;
	int compteur = 0;
	int fd[3];
	char buffer[80];

	fd[0] = open("/sys/class/gpio/gpio26/value", O_WRONLY);
	fd[1] = open("/sys/class/gpio/gpio27/value", O_WRONLY);
	fd[2] = open("/sys/class/gpio/gpio28/value", O_WRONLY);
	if ((fd[0] < 0) || (fd[1] < 0) || (fd[2] < 0)) {
		perror("open");
		exit(EXIT_FAILURE);
	}
	while(1) {
		for (i = 0; i < 3; i ++) { 			sprintf(buffer, "%d", (compteur >> i) & 1);
			write(fd[i], buffer, 2);
		}
		compteur ++;
		compteur %= 8;
		usleep(100000);
	}
}

Troisième exemple, avec les triggers du noyau Linux

Le kernel Linux nous offre une interface pour accéder aux LEDs de notre système, et propose même des triggers, c’est-à-dire des comportements prédéfinis pour leur clignotement. Pour que le noyau puisse accéder aux LEDs et nous offrir un moyen de les contrôler, il est nécessaires de sélectionner les options suivantes lors de sa compilation (cliquez sur l’aperçu ci-dessous). Dans le sous-menu « LED Support » du menu « Device Drivers ».

make ARCH=arm menuconfig

Vous pouvez utiliser directement ce fichier de configuration .config pour Linux 3.0-rc7 sur Pandaboard.

Dans cette configuration, nous ajoutons le support pour les LEDs pilotées par un GPIO, ainsi que la configuration spécifique à la plateforme. Cette configuration se trouve dans le fichier arch/arm/mach-omap2/board-omap4panda.c des sources du kernel.

[...]
static struct gpio_led gpio_leds[] = {
        {
                .name                   = "pandaboard::status1",
                .default_trigger        = "heartbeat",
                .gpio                   = 7,
        },
        {
                .name                   = "pandaboard::status2",
                .default_trigger        = "mmc0",
                .gpio                   = 8,
        },
};

static struct gpio_led_platform_data gpio_led_info = {
        .leds           = gpio_leds,
        .num_leds       = ARRAY_SIZE(gpio_leds),
};
[...]

On y retrouve bien les numéros de GPIO que nous avons utilisés plus tôt. Après compilation du noyau et reboot de la carte, nous trouvons une nouvelle interface dans /sys.

(panda)[root]$ cd /sys/class/
(panda)[class]$ ls
backlight     display       graphics      i2c-dev       leds          misc          net           rtc           scsi_host     ubi           vtconsole
bdi           firmware      hwmon         input         mdio_bus      mmc_host      power_supply  scsi_device   spi_master    usb_device
block         gpio          i2c-adapter   lcd           mem           mtd           regulator     scsi_disk     tty           vc
(panda)[class]$

Allons voir le contenu de cette classe LEDs.

(panda)[class]$ cd leds/
(panda)[leds]$ ls
pandaboard::status1  pandaboard::status2
(panda)[leds]$ ls pandaboard\:\:status1/
brightness      device          max_brightness  power           subsystem       trigger         uevent
(panda)[leds]$

Nous y trouvons deux sous-répertoires, un pour chaque LED, avec les noms observés plus haut dans le code source du noyau. Le pseudo-fichier trigger indique l’évenement qui déclenche le changement d’état de la LED.

(panda)[leds]$ cat pandaboard\:\:status1/trigger
none nand-disk mmc0 mmc1 timer [heartbeat] gpio default-on
(panda)[leds]$

Par défaut il s’agit du trigger heartbeat, qui simule un battement de coeur, dont la fréquence augmente en fonction de la charge du système, mais on peut en choisir d’autres :

  • none nous donne l’accès direct à la LED depuis l’entrée brightness du sous-répertoire,
  • nand-disk fait scintiller la LED lors de l’accès à la mémoire Flash interne (inutilisée sur mon système),
  • mmc0 et mmc1 signalent les accès aux partitions de la carte MicroSD, rappelant les LEDs d’accès aux disques durs,
  • timer allume et éteint la LED périodiquement : elle reste allumée pendant la durée en millisecondes indiquée dans delay_on puis s’éteint pendant delay_off millisecondes,
  • gpio a un comportement proche de celui de none, l’entrée brightness permettant d’allumer ou d’éteindre à volonté la LED.
  • default-on: allumer la LED dès le boot.

Voici par exemple la configuration pour une LED clignotant à 10 Hz, et dont la durée d’allumage est de 10 ms à chaque cycle.

(panda)[leds]$ echo timer > pandaboard\:\:status2/trigger
(panda)[leds]$ echo 10 > pandaboard\:\:status2/delay_on
(panda)[leds]$ echo 90 > pandaboard\:\:status2/delay_off
(panda)[leds]$

Conclusion

Nous voyons que les cartes embarquées sur lesquelles on fait couramment fonctionner Linux nous permettent aisément de disposer d’un petit signe de vie en employant des LEDs. Bien sûr ces LEDs peuvent servir à d’autres utilisations (indication de l’état d’un serveur, de la disponibilité des ressources, etc.). L’avantage d’un signe de vie programmé dans l’espace utilisateur sur celui intégré dans le kernel, est de nous indiquer réellement si le système est utilisable pour les applications, et pas seulement le bon fonctionnement de l’espace noyau. J’en emploie également sur les systèmes embarquant des applications temps-réel, afin de vérifier facilement si CPU est surchargé ou s’il reste du temps processeur pour les tâches temps-partagé.

Compilation de Linux avec une toolchain embarquée native

Embarqué, Linux | Publié par cpb
juil 10 2011

Nous avons compilé dans le précédent article une toolchain embarquée native, c’est-à-dire la chaîne de compilation qui pourra s’exécuter sur une cible embarquée (à processeur Arm dans notre cas) et qui sera susceptible de produire du code pour ce même processeur Arm.

Nous allons valider le fonctionnement de cette chaîne de compilation en lui soumettant un gros morceau de code : le noyau Linux.

Pour cela, commençons par copier les sources du noyau sur la cible. Pour cela, j’ai inséré la carte micro-SD de la Pandaborard directement dans le PC de compilation, et j’y télécharge le noyau :

[~]# cd /media/root/root/
[root]# wget http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.39.tar.gz
--2011-07-10 01:41:36--  http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.39.tar.gz
[...]
2011-07-10 01:43:12 (984 KB/s) - “linux-2.6.39.tar.gz” saved [95993809/95993809]
[root]# cd ../..
[media]# umount /media/*oot

Puis, je démarre la Pandaboard (après y avoir réinséré la mémoire flash) et je m’y connecte.

[media]# telnet 192.168.3.152
Trying 192.168.3.152...
Connected to 192.168.3.152.
Escape character is '^]'.
(none) login: root
Password:
BusyBox v1.18.4 (2011-06-22 12:27:46 CEST) built-in shell (ash)
Enter 'help' for a list of built-in commands.
[PANDABOARD]$ cd /root/
[PANDABOARD]$ ls
hello                hello.c              linux-2.6.39.tar.gz
[PANDABOARD]$

Nous retrouvons l’exemple « hello » de l’article précédent. La version de tar que j’ai compilée dans ma Busybox ne connaît pas les options « z » ou « j » pour décompresser les archives .gz ou .bz2. Aussi je décompresse d’abord manuellement le fichier.

[PANDABOARD]$ gunzip  linux-2.6.39.tar.gz 
[PANDABOARD]$ tar xf linux-2.6.39.tar 
[PANDABOARD]$

Étant donné que je cherche à valider ma chaîne de compilation, et non ma configuration du noyau, je vais utiliser comme fichier de configuration .config celui de mon système en cours de fonctionnement. Pour cela, je vais utiliser l’entrée /proc/config.gz qui permet de récupérer la configuration du noyau en marche. Cette entrée n’est pas toujours disponible – notamment la plupart des distributions ne la proposent pas – et c’est dommage. Pour activer la présence de /proc/config.gz, il faut activer l’option « Kernel .config Support » dans le menu « General Setup » lors de la compilation du noyau.

[PANDABOARD]$ cd linux-2.6.39/
[PANDABOARD]$ zcat /proc/config.gz > .config
[PANDABOARD]$

Avant de lancer la compilation, il est nécessaire de mettre la carte Pandaboard à l’heure. Par défaut elle s’initialise à l’heure Unix zéro (1er janvier 1970 à minuit), et la commande make se basant sur les horodatages des fichiers, la compilation peut poser des problèmes.

[PANDABOARD]$ date -s 2011.07.10-02:00
Sun Jul 10 02:00:00 UTC 2011
[PANDABOARD]$

La commande make oldconfig génère les fichiers de dépendances à partir du .config existant. Il est possible d’éditer le fichier .config à la main pour y faire d’éventuelles modifications (la commande habituelle make menuconfig ne fonctionnera pas à cause de l’absence de la biblipthèque nCurses sur la cible). Entre autre, l’option CONFIG_LOCALVERSION= peut être renseignée pour indiquer un nom spécifique à ajouter en suffixe au numéro de noyau. Ici j’ai utilisé la chaîne pandanatif.

[PANDABOARD]$ make ARCH=arm oldconfig
  HOSTCC  scripts/basic/fixdep
  HOSTCC  scripts/basic/docproc
  HOSTCC  scripts/kconfig/conf.o
  HOSTCC  scripts/kconfig/kxgettext.o
  SHIPPED scripts/kconfig/zconf.tab.c
  SHIPPED scripts/kconfig/lex.zconf.c
  SHIPPED scripts/kconfig/zconf.hash.c
  HOSTCC  scripts/kconfig/zconf.tab.o
  HOSTLD  scripts/kconfig/conf
scripts/kconfig/conf --oldconfig Kconfig
#
# configuration written to .config
#
[PANDABOARD]$

Nous lançons la compilation en tirant parti des deux coeurs de la Pandaboard, en demandant à make de paralléliser son travail sur quatre jobs.

[PANDABOARD]$ make ARCH=arm -j 4 
scripts/kconfig/conf --silentoldconfig Kconfig
  CHK     include/linux/version.h
  UPD     include/linux/version.h
  CHK     include/generated/utsrelease.h
[...] 
  LD [M]  net/bluetooth/bluetooth.ko
  AS      arch/arm/boot/compressed/piggy.gzip.o
  LD      arch/arm/boot/compressed/vmlinux
  OBJCOPY arch/arm/boot/zImage
  Kernel: arch/arm/boot/zImage is ready
[PANDABOARD]$

Nous avons obtenu ici une « zImage », c’est-à-dire une image brute compressée et préfixée par un petit auto-décompresseur. Toutefois, le bootloader de la carte Pandaboard attend une uImage, une image formatée pour U-boot. Pour l’obtenir il suffit de le demander à la chaine de compilation du noyau. Toutefois celle-ci s’appuie sur un petit utilitaire, mkimage, qui n’est pas présent sur le système minimal construit avec Busybox.
Pour compiler mkimage, nous devons télécharger les sources de U-boot et y chercher ce petit outil. Téléchargeons les sources dans le répertoire /root de notre cible (en montant la carte micro-SD sur l’hôte de développement).

[root]# cd /media/root/root/
[root]# wget ftp://ftp.denx.de/pub/u-boot/u-boot-2011.06.tar.bz2
--2011-07-10 08:09:50--  ftp://ftp.denx.de/pub/u-boot/u-boot-2011.06.tar.bz2
[...]
2011-07-10 08:10:00 (996 KB/s) - “u-boot-2011.06.tar.bz2” saved [8469694]
[root]# cd ../..
[media]# umount /media/*oot
[media]#

Après avoir redémarré la Pandaboard (et pensé à la remettre à l’heure) préparons les sources de U-Boot :

[PANDABOARD]$ cd /root/
[PANDABOARD]$ bunzip2 u-boot-2011.06.tar.bz2
[PANDABOARD]$ tar xf u-boot-2011.06.tar
tar: warning: skipping header 'g'
[PANDABOARD]$

La compilation que nous lançons n’a pour unique but que la préparation de l’utilitaire mkimage. Je vous conseille de vous connecter sur un second terminal, et de surveiller le répertoire /root/u-boot-2001-06/tools. Dès que l’exécutable mkimage est présent, on peut arrêter la compilation complète de U-Boot.

[PANDABOARD]$ make OMAP4_PANDA_config
awk '(NF && $1 !~ /^#/) { print $1 ": " $1 "_config; $(MAKE)" }' boards.cfg > .boards.depend
Generating include/autoconf.mk
Generating include/autoconf.mk.dep
Configuring for omap4_panda board...
[PANDABOARD]$ make
Generating include/autoconf.mk
Generating include/autoconf.mk.dep
gcc -DDO_DEPS_ONLY \
                -g  -Os   -fno-common -ffixed-r8 -msoft-float   -D__KERNEL__ -DCONFIG_SYS_TEXT_BASE=0x80e80000 -I/root/u-boot-2011.06/include -fno-builtin -ffreestanding -nostdinc -isystem /usr/lib/gcc/arm-unknown-linux-uclibcgnueabi/4.3.5/include -pipe  -DCONFIG_ARM -D__ARM__ -marm  -mabi=aapcs-linux -mno-thumb-interwork -march=armv5 -Wall -Wstrict-prototypes -fno-stack-protector   \
                -o lib/asm-offsets.s lib/asm-offsets.c -c -S
[...]
drivers/video/libvideo.o drivers/watchdog/libwatchdog.o fs/cramfs/libcramfs.o fs/ext2/libext2fs.o fs/fat/libfat.o fs/fdos/libfdos.o fs/jffs2/libjffs2.o fs/reiserfs/libreiserfs.o fs/ubifs/libubifs.o fs/yaffs2/libyaffs2.o lib/libfdt/libfdt.o lib/libgeneric.o lib/lzma/liblzma.o lib/lzo/liblzo.o lib/zlib/libz.o net/libnet.o post/libpost.o board/ti/panda/libpanda.o --end-group /root/u-boot-2011.06/arch/arm/lib/eabi_compat.o -L /usr/lib/gcc/arm-unknown-linux-uclibcgnueabi/4.3.5 -lgcc -Map u-boot.map -o u-boot
objcopy -O srec u-boot u-boot.srec
objcopy --gap-fill=0xff -O binary u-boot u-boot.bin
[PANDABOARD]$

Notre utilitaire étant prêt, installons-le dans un répertoire système, puis retournons dans les sources de Linux pour obtenir une uImage.

[PANDABOARD]$ cp tools/mkimage /usr/bin/
[PANDABOARD]$ cd /root/linux-2.6.39/
[PANDABOARD]$ make uImage
  CHK     include/linux/version.h
  CHK     include/generated/utsrelease.h
make[1]: `include/generated/mach-types.h' is up to date.
  CALL    scripts/checksyscalls.sh
  CHK     include/generated/compile.h
  Kernel: arch/arm/boot/Image is ready
  SHIPPED arch/arm/boot/compressed/lib1funcs.S
  AS      arch/arm/boot/compressed/lib1funcs.o
  LD      arch/arm/boot/compressed/vmlinux
  OBJCOPY arch/arm/boot/zImage
  Kernel: arch/arm/boot/zImage is ready
  UIMAGE  arch/arm/boot/uImage
Image Name:   Linux-2.6.39-pandanatif
Created:      Sun Jul 10 08:30:58 2011
Image Type:   ARM Linux Kernel Image (uncompressed)
Data Size:    3048144 Bytes = 2976.70 kB = 2.91 MB
Load Address: 80008000
Entry Point:  80008000
  Image arch/arm/boot/uImage is ready
[PANDABOARD]$

Installons le noyau sur la partition de boot en la montant dans l’arborescence et en y copiant la nouvelle uImage.

[PANDABOARD]$ mount /dev/mmcblk0p1 /mnt/
[PANDABOARD]$ cp /mnt/uImage /mnt/uImage-precedent
[PANDABOARD]$ cp arch/arm/boot/uImage /mnt/
[PANDABOARD]$ umount /dev/mmcblk0p1 
[PANDABOARD]$ reboot
Connection closed by foreign host.
[media]#

Après redémarrage de la carte Pandaboard, re-connectons nous et vérifions la version du noyau en fonctionnement…

[media]# telnet 192.168.3.152
Trying 192.168.3.152...
Connected to 192.168.3.152.
Escape character is '^]'.
(none) login: root
Password:
BusyBox v1.18.4 (2011-06-22 12:27:46 CEST) built-in shell (ash)
Enter 'help' for a list of built-in commands.
[PANDABOARD]$ uname -a
Linux (none) 2.6.39-pandanatif #1 SMP Sun Jul 10 02:36:03 UTC 2011 armv7l GNU/Linux
[PANDABOARD]$

Et voilà ! Notre cible embarquée est devenue relativement autonome, puisqu’elle est capable de fonctionner sur un noyau recompilé sur cette même cible. Ceci nous permet de valider le fonctionnement de la chaîne de compilation obtenue dans l’article précédent. Remarquons toutefois que cette toochain pourrait être étendue car nous n’avons intégré aucun outil de débogage (gdb, gdbserver, etc.)

Création et installation d’une toolchain native pour processeur Arm

Embarqué, Linux, Microprocesseur | Publié par cpb
juil 01 2011

Certains projets posent des problèmes difficiles à surmonter lorsqu’il s’agit de les cross-compiler pour une plate-forme différente du poste de compilation. Citons par exemple Apache ou PHP que nous avons laborieusement réussi à cross-compiler dans les quatrième et cinquième articles de notre série « Construire son système personnel sur Pandaboard« . Il peut être utile de disposer sur la cible embarquée d’une chaîne de compilation native c’est-à-dire capable de produire du code exécutable pour le processeur sur lequel la compilation a eu lieu.

Nous allons mettre ceci en oeuvre sur une carte à processeur Arm à l’aide de Buildroot.

Je vous propose de procéder en deux étapes : génération de la chaîne de compilation croisée classique, puis une fois celle-ci prête, construction de la chaîne de compilation native. Comme toujours, il faudra intervenir un peu manuellement pour réussir toute la compilation…

Tout d’abord téléchargeons et décompressons les sources de Buildroot :

[~]$ mkdir Projets/Arm/
[~]$ cd Projets/Arm/
[Arm]$ wget http://buildroot.uclibc.org/downloads/buildroot-2011.05.tar.bz2
--2011-06-30 23:22:45--  http://buildroot.uclibc.org/downloads/buildroot-2011.05.tar.bz2
[...]
2011-06-30 23:22:50 (522 KB/s) - «buildroot-2011.05.tar.bz2» sauvegardé [1834679/1834679]
[Arm]$ tar -xjf buildroot-2011.05.tar.bz2
[Arm]$ cd buildroot-2011.05
[buildroot-2011.05]$

Je vous propose de télécharger ce fichier de configuration qui va générer la chaîne de compilation croisée et installer dans les répertoires destinés à la cible les fichiers d’entête de la bibliothèque C (option Development files in target filesystem du menu Build Options ci-dessous).

[buildroot-2011.05]$ cp ~/config-buildroot-2011-05-pass-1 ./.config
[buildroot-2011.05]$ make menuconfig
[...]

Vérifiez en particulier les chemins mentionnés dans le menu « Build Options », pour qu’il soient en accord avec votre système. Le répertoire « Host Dir » doit être un chemin absolu (il contiendra donc probablement votre nom d’utilisateur plutôt que le mien !). Quittez le menu et lancez la compilation :

[buildroot-2011.05]$ make
[...]

Pendant la compilation, make nous pose une dizaines de questions (probablement de nouvelles options qui ne sont pas enregistrées dans le fichier .config), auxquelles nous pouvons répondre en utilisant le choix par défaut, c’est-à-dire en pressant simplement Entrée.

[...]
		grep -qx $lang /home/cpb/Projets/Arm/buildroot-2011.05/output/build/locales.nopurge || rm -rf $dir/$lang; \
	done; \
done
rm -f /home/cpb/Projets/Arm/buildroot-2011.05/output/build/.fakeroot*
[buildroot-2011.05]$

La première étape est terminée nous avons une chaîne de compilation croisée, contenant GCC, accessible ainsi :

[buildroot-2011.05]$ ~/Projets/Arm/Target/usr/bin/arm-linux-gcc -v
Utilisation des specs internes.
Target: arm-unknown-linux-uclibcgnueabi
Configuré avec: /home/cpb/Projets/Arm/buildroot-2011.05/output/toolchain/gcc-4.3.5/configure --prefix=/home/cpb/Projets/Arm/Target/usr --build=i686-pc-linux-gnu --host=i686-pc-linux-gnu --target=arm-unknown-linux-uclibcgnueabi --enable-languages=c,c++ --with-sysroot=/home/cpb/Projets/Arm/Target/usr/arm-unknown-linux-uclibcgnueabi/sysroot --with-build-time-tools=/home/cpb/Projets/Arm/Target/usr/arm-unknown-linux-uclibcgnueabi/bin --disable-__cxa_atexit --enable-target-optspace --disable-libgomp --with-gnu-ld --disable-libssp --disable-multilib --disable-tls --enable-shared --with-gmp=/home/cpb/Projets/Arm/Target/usr --with-mpfr=/home/cpb/Projets/Arm/Target/usr --enable-threads --disable-decimal-float --with-abi=aapcs-linux --with-pkgversion='Buildroot 2011.05' --with-bugurl=http://bugs.buildroot.net/
Modèle de thread: posix
gcc version 4.3.5 (Buildroot 2011.05)
[buildroot-2011.05]$

Passons à la seconde phase, la compilation d’un environnement de développement natif pour notre système embarqué en utilisant ce fichier de configuration.

[buildroot-2011.05]$ cp ../config-buildroot-2011-05-pass-2 ./.config
[buildroot-2011.05]$ make menuconfig

Vous pourrez remarquer que nous avons inséré plusieurs outils de développement comme Bison, Flex, Make, Gcc, et même l’interpréteur Microperl, qui comme son nom l’indique implémente un sous-ensemble (assez conséquent) du langage Perl.

[buildroot-2011.05]$ make 
[...]
-DDEPENDS_ON_LIBINTL=1 -DEXEEXT=\"\" -Os -pipe -Os -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -c csharpcomp.c -o csharpcomp.o >/dev/null 2>&1
make[5] : on quitte le répertoire « /home/cpb/Projets/Arm/buildroot-2011.05/output/build/gettext-0.16.1/gettext-tools/gnulib-lib »
make[4]: *** [all] Erreur 2
make[4] : on quitte le répertoire « /home/cpb/Projets/Arm/buildroot-2011.05/output/build/gettext-0.16.1/gettext-tools/gnulib-lib »
make[3]: *** [all-recursive] Erreur 1
make[3] : on quitte le répertoire « /home/cpb/Projets/Arm/buildroot-2011.05/output/build/gettext-0.16.1/gettext-tools »
make[2]: *** [all] Erreur 2
make[2] : on quitte le répertoire « /home/cpb/Projets/Arm/buildroot-2011.05/output/build/gettext-0.16.1/gettext-tools »
make[1]: *** [all-recursive] Erreur 1
make[1] : on quitte le répertoire « /home/cpb/Projets/Arm/buildroot-2011.05/output/build/gettext-0.16.1 »
make: *** [/home/cpb/Projets/Arm/buildroot-2011.05/output/build/gettext-0.16.1/gettext-runtime/src/gettext] Erreur 2
[buildroot-2011.05]$

Comment ? Une erreur de compilation ? Quelle surprise !

La cause apparait un peu plus haut dans les traces :

 /home/cpb/Projets/Arm/Target/usr/bin/arm-unknown-linux-uclibcgnueabi-gcc -DHAVE_CONFIG_H -DEXEEXT=\"\" -DEXEEXT=\"\" -I. -I.. -I../intl -I../intl -I../intl -I.. -I.. -DDEPENDS_ON_LIBICONV=1 -DDEPENDS_ON_LIBINTL=1 -DEXEEXT=\"\" -Os -pipe -Os -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -c copy-file.c  -fPIC -DPIC -o .libs/copy-file.o
copy-file.c:35:25: error: sys/utime.h: No such file or directory
make[5]: *** [copy-file.lo] Erreur 1

Un fichier utime.h ne se trouve pas dans le bon répertoire. Classique. Cherchons-le.

[buildroot-2011.05]$ find . -name 'utime.h'
./output/toolchain/uClibc-0.9.31/include/utime.h
./output/toolchain/uClibc_dev/usr/include/linux/utime.h
./output/toolchain/linux/include/linux/utime.h
./output/toolchain/linux-2.6.38.7/include/linux/utime.h
[buildroot-2011.05]$

Nous ne prendrons pas les deux dernières instances, elles sont réservées à la compilation de modules du noyau, mais plutôt la première. A présent nous voyons que le compilateur cherche ce fichier dans un sous-répertoire sys/ or il n’y en a pas dans les répertoires de recherches des fichiers d’entête lors de la compilation de copy-file.c.
Nous allons devoir faire une petite opération manuelle avant de relancer la compilation.

[buildroot-2011.05]$ mkdir -p output/build/gettext-0.16.1/gettext-tools/gnulib-lib/sys
[buildroot-2011.05]$ cp output/toolchain/uClibc-0.9.31/include/utime.h output/build/gettext-0.16.1/gettext-tools/gnulib-lib/sys/
[buildroot-2011.05]$ make

A vrai dire, la commande make s’est à nouveau interrompue en erreur sur mon système. Toutefois, soupçonnant une erreur due à la compilation en parallèle (8 jobs), j’ai relancé simplement make qui a fonctionné.

[...]
do \
		grep -qx $lang /home/cpb/Projets/Arm/buildroot-2011.05/output/build/locales.nopurge || rm -rf $dir/$lang; \
	done; \
done
rm -f /home/cpb/Projets/Arm/buildroot-2011.05/output/build/.fakeroot*
[buildroot-2011.05]$ ls output/target/usr/bin/
addr2line  as     c++  c++filt  elfedit  g++  gccbug  gprof  ld      ldd   microperl  objcopy  perl    readelf  strings  yacc
ar         bison  cc   cpp      flex     gcc  gcov    iconv  ld.bfd  make  nm         objdump  ranlib  size     strip
[buildroot-2011.05]$ file output/target/usr/bin/gcc
output/target/usr/bin/gcc: ELF 32-bit LSB executable, ARM, version 1 (SYSV), dynamically linked (uses shared libs), stripped
[buildroot-2011.05]$

Pour tester notre toolchain native, nous allons l’installer sur un système Pandaboard, contenant simplement un noyau Linux et une Busybox intrégrant une connexion à distante par telnet. Après avoir inséré la carte micro-SD sur mon PC, je vais copier le contenu du répertoire ~/Projets/Arm/buildroot-2011.05/output/target/usr/ sur la cible :

[buildroot-2011.05]$ cp -R output/target/usr/* /media/root/usr/
[buildroot-2011.05]$ umount /media/root/
[buildroot-2011.05]$

Après avoir inséré la micro-SD dans la carte Pandaboard, laissons-la booter, puis connectons-nous dessus via le réseau.

[buildroot-2011.05]$ telnet 192.168.3.152
Trying 192.168.3.152...
Connected to 192.168.3.152.
(none) login: root
Password:
BusyBox v1.18.4 (2011-06-22 12:27:46 CEST) built-in shell (ash)
Enter 'help' for a list of built-in commands.
[PANDABOARD]$  vi /root/hello.c

A l’aide du petit éditeur vi intégré dans Busybox, nous saissons un petit programme de test

#include <stdio.h>

int main(void)
{
        fprintf(stdout, "Hello from the native Panda Compiler !\n");
        return 0;
}

Que nous compilons ainsi :

[PANDABOARD]$ gcc /root/hello.c -o /root/hello
[PANDABOARD]$ /root/hello
Hello from the native Panda Compiler !
[PANDABOARD]$

Voilà ! notre compilateur natif est prêt. Il manque encore quelques utilitaires pour se lancer dans une compilation de plus grande envergure (le noyau Linux par exemple), nous les verrons la semaine prochaine.

PS : Cet article est plus court que d’habitude, mais cette période est très chargée pour moi : plusieurs développements importants arrivant à leurs termes, un déménagement, et quelques nouveaux projets traitant de robotique et de micro-contrôleurs (que je développerai prochainement)…