Archives de la catégorie ‘Microprocesseur’

Parallélisation de compilations

Linux, Microprocesseur | Publié par cpb
jan 14 2012

(English translation here)

Il m’arrive très fréquemment de compiler des noyaux Linux, souvent durant des sessions de formation ou des prestations d’ingénierie (principalement dans le domaine de l’embarqué ou le développement de drivers), et parfois à titre expérimental ou par simple curiosité pour rédiger des articles ou mon prochain livre.

La durée de compilation varie beaucoup en fonction de la quantité de code (de drivers, systèmes de fichiers, protocoles, etc.) et de la puissance de la machine hôte. Sur un PC de milieu de gamme, la compilation d’un kernel ajusté pour un système embarqué dure environ trois minutes. Sur une machine d’entrée de gamme (ou un peu ancienne), la compilation d’un noyau générique pour PC (disposant donc de centaines de drivers sous forme de modules) peut durer une heure.

Pour tirer parti du parallélisme proposé par les processeurs actuels (systèmes multiprocesseurs, multicoeurs ou avec hyper-threading), la commande make nous permet de lancer simultanément plusieurs jobs. Ainsi

$ make -j 4

s’arrangera pour qu’il y ait toujours quatre jobs de compilation actifs.

J’ai longtemps répété que « si vous avez N processeurs (ou coeurs, ou CPU virtuels) disponibles, vous gagnerez du temps de compilation en lançant 2N jobs en parallèle« . Ceci repose sur l’idée que pour chaque processeur nous avons un job qui effectue de la compilation (en consommant du temps CPU) et tandis qu’un autre job peut terminer de sauvegarder les résultats de la compilation précédente ou charger le fichier source du traitement suivant. Mais… est-ce vrai ?

Script de test

Pour en avoir le coeur net, j’ai écrit le petit script suivant, qui télécharge au besoin les sources d’un noyau et les décompresse, puis réalise plusieurs compilations en démarrant un nombre variable de jobs.

Par exemple si on lance

$ ./test-make-j.sh 3 5 8

Il effectue trois compilations complètes : l’une avec trois tâches en parallèle, la suivante avec cinq jobs et la dernière avec huit, les résultats étant cumulés dans un fichier de texte. Le script est le suivant.

test-make-j.sh 
#! /bin/sh

KERNEL_VERSION="linux-3.2"
KERNEL_URL_PATH="www.kernel.org/pub/linux/kernel/v3.0/"
RESULT_FILE="compilation-timing.txt"

if [ "$#" -eq 0 ]
then
  echo "usage: $@ jobs_number..." >& 2
  exit 0
fi

if [ ! -d "${KERNEL_VERSION}" ]
then
  if [ ! -f "${KERNEL_VERSION}.tar.bz2" ]
  then
    wget "${KERNEL_URL_PATH}/${KERNEL_VERSION}.tar.bz2"
    if [ $? -ne 0 ] || [ ! -f "${KERNEL_VERSION}.tar.bz2" ]
    then
      echo "unable to obtain ${KERNEL_VERSION} archive" >&2
      exit 1
    fi
  fi
  tar xjf "${KERNEL_VERSION}.tar.bz2"
  if [ $? -ne 0 ]
  then
    echo "Error while uncompressing kernel archive" >&2
    exit 1
  fi
fi

cd "${KERNEL_VERSION}"

echo "# Timings of ${KERNEL_VERSION} compilations" >> "${RESULT_FILE}"
nb_cpu=$(grep "^processor" /proc/cpuinfo | wc -l)

echo "# Processors: ${nb_cpu}" >> "${RESULT_FILE}"
affinity=$(taskset -p $$ | sed -e 's/^.*://') >> "${RESULT_FILE}"

echo "# Affinity mask: ${affinity}" >> "${RESULT_FILE}"
for nb in "$@"
do
  echo "# Compiling with $nb simultaneous jobs" >> "${RESULT_FILE}"
  make mrproper
  make i386_defconfig
  sync
  sleep 10 # Let's all calm down
  start=$(date "+%s")
  make -j $nb
  sync
  end=$(date "+%s")
  # This script will fail during february 2038 ;-)
  echo "$nb     $((end - start))" >> "${RESULT_FILE}"
done

Résultats

Voici les résultats d’une exécution sur un processeur Intel Q6600 Quad-Core (fichier Intel-Q6600-1.txt)

# Timings of linux-3.2 compilations
# Processors: 4
# Affinity mask:  f
# Compiling with 1 simultaneous jobs
1     675
# Compiling with 2 simultaneous jobs
2     346
# Compiling with 3 simultaneous jobs
3     241
# Compiling with 4 simultaneous jobs
4     197
# Compiling with 5 simultaneous jobs
5     198
# Compiling with 6 simultaneous jobs
6     194
# Compiling with 7 simultaneous jobs
7     195
# Compiling with 8 simultaneous jobs
8     196
# Compiling with 9 simultaneous jobs
9     197
# Compiling with 10 simultaneous jobs
10     198
# Compiling with 11 simultaneous jobs
11     198
# Compiling with 12 simultaneous jobs
12     198
# Compiling with 13 simultaneous jobs
13     200
# Compiling with 14 simultaneous jobs
14     201
# Compiling with 15 simultaneous jobs
15     201
# Compiling with 16 simultaneous jobs
16     200

Observons-les graphiquement avec cette petite ligne de commande pour Gnuplot. Horizontalement, nous voyons le nombre de jobs simultanés et verticalement le temps de compilation en secondes.

$ echo "set terminal png size 640,480 ; set output './Intel-Q6600-1.png'; plot 'Intel-Q6600-1.txt' with linespoints" | gnuplot

 

Compilations parallèles sur quatre CPU

Compilations parallèles sur quatre CPU

Visiblement, les meilleurs résultats sont atteints (à quelques fluctuations près) dès make -j 4. Essayons de confirmer ceci. Avant de lancer le script, nous le limitons sur deux processeurs avec la commande suivante qui fixe les jobs lancés à partir du shell courant sur les processeurs 2 et 3.

$ taskset -pc 2-3 $$

Voici le résultat (fichier Intel-QL6600-2.txt).

# Timings of linux-3.2 compilations
# Processors: 4
# Affinity mask:  c
# Compiling with 1 simultaneous jobs
1     684
# Compiling with 2 simultaneous jobs
2     360
# Compiling with 3 simultaneous jobs
3     362
# Compiling with 4 simultaneous jobs
4     366
# Compiling with 8 simultaneous jobs
8     370
# Compiling with 16 simultaneous jobs
16     376
# Compiling with 32 simultaneous jobs
32     377
# Compiling with 64 simultaneous jobs
64     378
Compilations parallèles sur deux CPU

Compilations parallèles sur deux CPU

Cette fois, il est visible que le minimum de temps est obtenu avec make -j 2. Si nous répétons l’expérience sur un seul CPU, on obtient les valeurs suivantes (fichier Intel-Q6600-3.txt).

# Timings of linux-3.2 compilations
# Processors: 4
# Affinity mask:  8
# Compiling with 1 simultaneous jobs
1     683
# Compiling with 2 simultaneous jobs
2     698
# Compiling with 3 simultaneous jobs
3     708
# Compiling with 4 simultaneous jobs
4     709
# Compiling with 5 simultaneous jobs
5     719
# Compiling with 6 simultaneous jobs
6     719
# Compiling with 7 simultaneous jobs
7     720
# Compiling with 8 simultaneous jobs
8     724

Ce qui se représente sur le graphique suivant.

Compilations parallèles sur un seul CPU

Compilations parallèles sur un seul CPU

Nous pouvons regrouper ces trois courbes sur un même graphique pour mieux visualiser leurs échelles (je n’ai pas prolongé la courbe de la compilation sur un seul CPU, mais on peut imaginer qu’elle se poursuit avec une légère croissance).

Compilations parallèles sur processeur Q6600

Compilations parallèles sur processeur Q6600

Pour en avoir le coeur net, nous pouvons recommencer l’expérience sur un autre processeur avec deux coeurs (AMD QL66). Les résultats sont les suivants (fichier AMD-QL66-1.txt).

# Timings of linux-3.2 compilations
# Processors: 2
# Affinity mask:  3
# Compiling with 1 simultaneous jobs
1     1113
# Compiling with 2 simultaneous jobs
2     844
# Compiling with 3 simultaneous jobs
3     875
# Compiling with 4 simultaneous jobs
4     863
# Compiling with 5 simultaneous jobs
5     840
# Compiling with 6 simultaneous jobs
6     844
# Compiling with 7 simultaneous jobs
7     844
# Compiling with 8 simultaneous jobs
8     851
Compilations parallèles sur deux CPU

Compilations parallèles sur deux CPU

Essayons une dernière expérience, sur la même machine (deux CPU), en désactivant deux éléments :

  • la lecture anticipée des blocs suivants du disque (qui permet d’améliorer les lectures localisées) avec echo 0 > /sys/block/sda/read_ahead_kb
  • l’écriture différée (de 30 secondes environ) des blocs (qui évite les accès répétitifs au disque en cas de modifications successives) avec mount / -o sync,remount.

 

Cette fois les résultats sont très différents (fichier AMD-QL66-2.txt). Les temps sont beaucoup plus longs que précédemment car à chaque écriture sur le disque, le processus attend que les données soient transmises au périphérique pour continuer son travail.

 Timings of linux-3.2 compilations
# Processors: 2
# Affinity mask:  3
# Compiling with 1 simultaneous jobs
1     3487
# Compiling with 2 simultaneous jobs
2     2562
# Compiling with 3 simultaneous jobs
3     2198
# Compiling with 4 simultaneous jobs
4     1963
# Compiling with 5 simultaneous jobs
5     1779
# Compiling with 6 simultaneous jobs
6     1646
# Compiling with 7 simultaneous jobs
7     1636
# Compiling with 8 simultaneous jobs
8     1602
# Compiling with 9 simultaneous jobs
9     1738
# Compiling with 10 simultaneous jobs
10     1577
Compilations parallèles sans optimisation disque

Compilations parallèles (2 CPU) sans optimisation disque

Ici, la courbe est plus proche de celle que j’imaginais à l’origine. Le fait de placer plusieurs jobs par CPU permet de tirer parti des temps d’attente liés au disque pour avancer dans une autre compilation. Regroupons les deux courbes pour bien voir les durées respectives.

Compilation parallèle sur QL66

Compilations parallèles sur QL66

Conclusion

Nous voyons qu’avec la qualité de l’ordonnanceur d’entrées-sorties (IO Scheduler) de Linux, et la gestion optimisée des périphériques blocs, les meilleurs temps de compilation sont obtenus dès que l’on lance un job par processeur.

Je modifierai donc à l’avenir ma recommandation en « Si vous avez N processeurs disponibles, compilez votre noyau avec  make -j N  pour avoir le meilleur temps d’exécution« .

PS : si vous avez l’occasion de faire fonctionner ce script sur des architectures différentes (8 processeurs, 16 processeurs, etc.) je serai très intéressé par vos résultats.

Parallelizing Compilations

Linux, Microprocesseur | Publié par cpb
jan 14 2012

(Version originale en français)

I very frequently compile Linux kernels, often during training sessions or engineering services (mainly in the field of embedded systems or drivers development), sometimes while writing articles or books.

Compilation time varies greatly depending on the amount of code (drivers, filesystems, protocols, etc.) and on the CPU power of the host machine. On a mid-range PC, compiling a kernel adjusted for an embedded system (with very few drivers) lasts about three minutes. On an entry level machine (or a little old one), compiling a generic kernel for PC (with hundreds of drivers as modules) can last an hour.

To take advantage of the parallelism offered by the current processors (multiprocessor, multicore or hyper-threading), the make command allows us to run multiple jobs. So

$ make -j 4

guarantees there is always four compilation jobs active.

I have long reiterated that « if you have N processors (or cores, or virtual CPUs) available, you will save time by starting 2N compilation jobs in parallel« . The idea is that for every proccessor we have a job that performs the compilation (consuming CPU time) while another job is saving the results of the previous compilation and loading the source file of the next task. But wait… is this true?

Test script

I wrote the following script which downloads and unpack the required kernel sources, then makes several compilations using a variable number of jobs. For example, if we start

$ ./test-make-j.sh 3 5 8

It performs three complete compilations: one with three tasks in parallel, one with the five jobs and the last with eight jobs. The results are recorded in a text file. The script follows.

test-make-j.sh 
#! /bin/sh

KERNEL_VERSION="linux-3.2"
KERNEL_URL_PATH="www.kernel.org/pub/linux/kernel/v3.0/"
RESULT_FILE="compilation-timing.txt"

if [ "$#" -eq 0 ]
then
  echo "usage: $@ jobs_number..." >& 2
  exit 0
fi

if [ ! -d "${KERNEL_VERSION}" ]
then
  if [ ! -f "${KERNEL_VERSION}.tar.bz2" ]
  then
    wget "${KERNEL_URL_PATH}/${KERNEL_VERSION}.tar.bz2"
    if [ $? -ne 0 ] || [ ! -f "${KERNEL_VERSION}.tar.bz2" ]
    then
      echo "unable to obtain ${KERNEL_VERSION} archive" >&2
      exit 1
    fi
  fi
  tar xjf "${KERNEL_VERSION}.tar.bz2"
  if [ $? -ne 0 ]
  then
    echo "Error while uncompressing kernel archive" >&2
    exit 1
  fi
fi

cd "${KERNEL_VERSION}"

echo "# Timings of ${KERNEL_VERSION} compilations" >> "${RESULT_FILE}"
nb_cpu=$(grep "^processor" /proc/cpuinfo | wc -l)

echo "# Processors: ${nb_cpu}" >> "${RESULT_FILE}"
affinity=$(taskset -p $$ | sed -e 's/^.*://') >> "${RESULT_FILE}"

echo "# Affinity mask: ${affinity}" >> "${RESULT_FILE}"
for nb in "$@"
do
  echo "# Compiling with $nb simultaneous jobs" >> "${RESULT_FILE}"
  make mrproper
  make i386_defconfig
  sync
  sleep 10 # Let's all calm down
  start=$(date "+%s")
  make -j $nb
  sync
  end=$(date "+%s")
  # This script will fail during february 2038 ;-)
  echo "$nb     $((end - start))" >> "${RESULT_FILE}"
done

Results

Here are the results of a run on an Intel Q6600 Quad-Core (file: Intel-Q6600-1.txt)

# Timings of linux-3.2 compilations
# Processors: 4
# Affinity mask:  f
# Compiling with 1 simultaneous jobs
1     675
# Compiling with 2 simultaneous jobs
2     346
# Compiling with 3 simultaneous jobs
3     241
# Compiling with 4 simultaneous jobs
4     197
# Compiling with 5 simultaneous jobs
5     198
# Compiling with 6 simultaneous jobs
6     194
# Compiling with 7 simultaneous jobs
7     195
# Compiling with 8 simultaneous jobs
8     196
# Compiling with 9 simultaneous jobs
9     197
# Compiling with 10 simultaneous jobs
10     198
# Compiling with 11 simultaneous jobs
11     198
# Compiling with 12 simultaneous jobs
12     198
# Compiling with 13 simultaneous jobs
13     200
# Compiling with 14 simultaneous jobs
14     201
# Compiling with 15 simultaneous jobs
15     201
# Compiling with 16 simultaneous jobs
16     200

Let’s see them graphically with this little Gnuplot command line. On the horizontal axis, lies the number of concurrent jobs and on the vertical axis is the compilation time (in seconds).

$ echo "set terminal png size 640,480 ; set output './Intel-Q6600-1.png'; plot 'Intel-Q6600-1.txt' with linespoints" | gnuplot

 

Parallel Compilations on 4 CPU

Parallel Compilations on 4 CPU

Apparently, the best results are achieved (with some fluctuations) with make-j 4. Let’s try to confirm this. Before running again the script, we limit it to two processors with the following command which binds on processors 2 and 3 all the jobs launched from the running shell.

$ taskset -pc 2-3 $$

Here are the results (file: Intel-QL6600-2.txt).

# Timings of linux-3.2 compilations
# Processors: 4
# Affinity mask:  c
# Compiling with 1 simultaneous jobs
1     684
# Compiling with 2 simultaneous jobs
2     360
# Compiling with 3 simultaneous jobs
3     362
# Compiling with 4 simultaneous jobs
4     366
# Compiling with 8 simultaneous jobs
8     370
# Compiling with 16 simultaneous jobs
16     376
# Compiling with 32 simultaneous jobs
32     377
# Compiling with 64 simultaneous jobs
64     378
Parallel Compilations on 2 CPU

Parallel Compilations on 2 CPU

This time, it is clear that the minimum time is achieved with make-j 2. If we repeat the experiment on a single CPU, we obtain the following values (file: Intel-Q6600-3.txt).

# Timings of linux-3.2 compilations
# Processors: 4
# Affinity mask:  8
# Compiling with 1 simultaneous jobs
1     683
# Compiling with 2 simultaneous jobs
2     698
# Compiling with 3 simultaneous jobs
3     708
# Compiling with 4 simultaneous jobs
4     709
# Compiling with 5 simultaneous jobs
5     719
# Compiling with 6 simultaneous jobs
6     719
# Compiling with 7 simultaneous jobs
7     720
# Compiling with 8 simultaneous jobs
8     724

Represented on the graph below:

Parallel Compilations on a single CPU

Parallel Compilations on a single CPU

We can group these three curves on a single graph to better see their scales (I have not extended the curve of the compilation on a single CPU, but we can imagine that it continues with a slight increase).

Parallel Compilations on Q6600

Parallel Compilations on Q6600

To be sure, we can repeat the experiment on a different processor with two cores (AMD QL66). The results are as follows (file: AMD-QL66-1.txt).

# Timings of linux-3.2 compilations
# Processors: 2
# Affinity mask:  3
# Compiling with 1 simultaneous jobs
1     1113
# Compiling with 2 simultaneous jobs
2     844
# Compiling with 3 simultaneous jobs
3     875
# Compiling with 4 simultaneous jobs
4     863
# Compiling with 5 simultaneous jobs
5     840
# Compiling with 6 simultaneous jobs
6     844
# Compiling with 7 simultaneous jobs
7     844
# Compiling with 8 simultaneous jobs
8     851
Parallel Compilations on two CPU

Parallel Compilations on two CPU

Let’s try one last experiment on the same machine (two CPU), by disabling two elements:

  • prefetching of the next blocks of the disk (which can improve localized readings) with echo 0 > /sys/block/sda/read_ahead_kb
  • delayed (about 30 seconds) block writes (avoiding repetitive access to the disk in case of subsequent modification of the same block) with mount / -o sync,remount.

 

This time the results are very different (file: AMD-QL66-2.txt). The times are much longer than before because for each write to disk, the process waits for data to be transmitted to the device to continue his work.

 Timings of linux-3.2 compilations
# Processors: 2
# Affinity mask:  3
# Compiling with 1 simultaneous jobs
1     3487
# Compiling with 2 simultaneous jobs
2     2562
# Compiling with 3 simultaneous jobs
3     2198
# Compiling with 4 simultaneous jobs
4     1963
# Compiling with 5 simultaneous jobs
5     1779
# Compiling with 6 simultaneous jobs
6     1646
# Compiling with 7 simultaneous jobs
7     1636
# Compiling with 8 simultaneous jobs
8     1602
# Compiling with 9 simultaneous jobs
9     1738
# Compiling with 10 simultaneous jobs
10     1577
Parallel Compilations on 2 CPU without disk optimizations

Parallel Compilations on 2 CPU without disk optimizations

Here, the curve is closer to that than I imagined at first. Placing more jobs per CPU can take advantage of the wait times due to the disk access to progress in another compilation. Group the two curves in order to see the respective durations.

Parallel Compilations on QL66

Parallel Compilations on QL66

Conclusion

We see that with the quality of the I/O scheduler of Linux, and the optimized management of block devices, the best compilation time are obtained as soon as we launch one job per processor.

So I will modify my recommendation in the future as « If you have N processors available, compile your kernel with make -j N to get the best execution time. »

PS: If you have the opportunity to run this script on different architectures (8 processors, 16 processors, etc.). I am very interested in your results.

Expérimentation sur la préemptibilité du noyau Linux

Embarqué, Linux, Microprocesseur, Temps-réel | Publié par cpb
nov 04 2011

Cet article est extrait de la version préparatoire de mon livre « Solutions temps-réel sous Linux » (parution envisagée au début 2012).

J’ai eu envie de mettre en évidence la différence de comportement entre un noyau préemptible (avec l’option CONFIG_PREEMPT activée durant sa compilation) et un noyau non-préemptible classique. Toutefois, cette mise en évidence n’est pas très simple, car elle concerne précisément des cas rares et difficiles à  reproduire.

Nous allons nous intéresser à une interruption déclenchée par le port de communication série RS-232. Nous allons envoyer un caractère sur une liaison série à destination d’un système Linux sur lequel fonctionnera un processus temps-réel qui renverra le même caractère dans l’autre sens sur la même liaison série.

Mise en œuvre

L’expérience met en œuvre une carte à processeur Arm (Pandaboard, à gauche sur la photo ci-dessous) fonctionnant avec un noyau Linux 3.0. Ce choix s’explique par le fait que la Pandaboard dispose d’un véritable port série RS-232 possédant sa propre interruption – et non d’une émulation basée sur un contrôleur USB comme la plupart des PC actuels. Nous essaierons de mesurer précisément le temps de réponse de notre processus temps-réel et voir s’il varie sous l’influence d’un processus de moindre priorité.

Pour effectuer cette mesure, j’utiliserai une carte de développement (STK500, à droite sur la photo) pour micro-contrôleurs Atmel, plus particulièrement ici un ATmega32 (inséré dans le support de programmation rouge sur la partie gauche de la carte). Ce dernier fonctionne sans système d’exploitation et nous permet un contrôle direct et sans perturbation de la liaison série. Le micro-contrôleur enverra le caractère et mesurera le temps écoulé jusqu’à la réception du caractère renvoyé.

 

Pandaboard et STK500

Voici le programme qui est compilé puis chargé dans le micro-contrôleur (à l’aide des utilitaires avr-gcc et avr-dude qui sont librement disponible sous Linux). Mentionnons pour le lecteur non habitué à la programmation de micro-contrôleur que les constantes UDR, UCSRA, UCSRB, UCSRC, UBRRH et UBRRL représentent des accès directs aux registres du micro-contrôleur.

exemple-mesure-atmega.c :
// Declarer la frequence du micro-controleur (1MHz)
#define F_CPU 1000000
// Inclusion d'entetes specifiques au micro-controleur
#include <avr/io.h>
#include <util/delay.h>

void envoyer_octet (unsigned char octet)
{
    // Attendre buffer emission libre
    while ((UCSRA & (1<<UDRE)) == 0)
        ;
    // Ecriture sur le port Usart Data Register
    UDR = octet;
}

int main(void)
{
    unsigned char uc;
    unsigned long int nb_boucles;

    // Parametrage du port de communication
    // 12 -> 9600 bits/sec
    UBRRH = (unsigned char) (0);
    UBRRL = (unsigned char) (12);
    // Clock x 2
    UCSRA = (1 << U2X);
    // TX et RX actives
    UCSRB = (1 << RXEN) | (1 << TXEN);
    // 8 bits, 1 stop, pas de parite
    UCSRC = (1 << URSEL) | (1 << UCSZ1) | (1 << UCSZ0);

    while (1) {
        nb_boucles = 0;

        envoyer_octet(0xFF);
        // Attendre 1 octet recu
        while ((UCSRA & (1 << RXC)) == 0)
            nb_boucles ++;
        // Lire l'octet
        uc = UDR;

        envoyer_octet((nb_boucles >> 24) & 0xFF);
        _delay_ms(2);
        envoyer_octet((nb_boucles >> 16) & 0xFF);
        _delay_ms(2);
        envoyer_octet((nb_boucles >> 8 ) & 0xFF);
        _delay_ms(2);
        envoyer_octet((nb_boucles >> 0 ) & 0xFF);
        _delay_ms(500);
    }
    return 0;
}

On peut voir que ce programme envoie un octet (0xFF) sur le port série et boucle – en incrémentant une variable – tant qu’il n’a pas reçu de réponse. La durée de la boucle importe peu. Chaque itération dure environ 10 micro-secondes, et on pourrait la calibrer précisément, mais ceci ne présente pas de véritable intérêt. Ce n’est pas le nombre absolu de boucles effectuées qui nous concerne, mais plutôt les variations de ce nombre au cours des essais successifs. Une fois la réponse obtenue, nous envoyons sur le même port série le nombre de boucles effectuées. Le petit sommeil de 2 ms entre les octets sert à garantir la réception par le correspondant car il n’y a pas de contrôle de flux (CTS/RTS, DTS/DSR, etc.) sur la carte à micro-contrôleur employée ici.

Le programme qui fonctionne sur la Pandaboard sous Linux est le suivant :

exemple-reponse-irq-serie.c  :
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>

volatile int quitter = 0;

void handler_sigint(int unused)
{
    quitter = 1;
}

int main(int argc, char * argv[])
{
    int fd;
    unsigned char  octet;
    int nb_cycles;
    struct termios parametres;
    struct termios original;

    if (argc != 2) {
        fprintf(stderr, "usage: %s port_serie\n",argv[0]);
        exit(EXIT_FAILURE);
    }

    signal(SIGINT, handler_sigint);

    if ((fd = open(argv[1], O_RDWR | | O_NONBLOCK))<0){
        perror(argv[1]);
        exit(EXIT_FAILURE);
    }

    tcgetattr(fd, & original);
    tcgetattr(fd, & parametres);
    cfmakeraw(& parametres);
    cfsetispeed(& parametres, B9600);
    cfsetospeed(& parametres, B9600);
    parametres.c_iflag |= IGNPAR;
    parametres.c_cflag ^= (CSIZE | PARENB | CSTOPB);
    parametres.c_cflag |= CS8 | CLOCAL;
    if (tcsetattr(fd, TCSANOW, & parametres) != 0) {
        perror("tcsetattr");
        exit(EXIT_FAILURE);
    }
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) ^ O_NONBLOCK);

    while (! quitter) {
        if (read(fd, & octet, 1) < 0) {
            perror(argv[1]);
            exit(EXIT_FAILURE);
        }
        if (write(fd, & octet, 1) < 0) {
            perror(argv[1]);
            exit(EXIT_FAILURE);
        }
        nb_cycles = 0;
        read(fd, & octet, 1);
        nb_cycles |= octet;
        nb_cycles <<= 8;
        read(fd, & octet, 1);
        nb_cycles |= octet;
        nb_cycles <<= 8;
        read(fd, & octet, 1);
        nb_cycles |= octet;
        nb_cycles <<= 8;
        read(fd, & octet, 1);
        nb_cycles |= octet;
        fprintf(stdout, "%d\n", nb_cycles);
    }
    tcsetattr(fd, TCSANOW, & original);
    close(fd);
    fprintf(stderr, "Bye !\n");
    return EXIT_SUCCESS;
}

Le programme attend donc un caractère, le renvoie en écho, récupère les quatre octets représentant son temps de réponse et les affiche sur sa sortie standard. Voici un exemple d’exécution sur un noyau non-préemptible.

[Panda]# ./exemple-reponse-irq-serie /dev/ttyO2
290
287
289
287
[...]
287
287
286
286
(Contrôle-C)
Bye !
[Panda]#

Appel-système long

Pour perturber le fonctionnement du programme, j’ai réalisé un petit script shell qui charge (avec insmod) et décharge (avec rmmod) toutes les cinq secondes ce petit module du kernel :

module-delay.c  :
#include <linux/module.h>

static int __init module_delay_init (void)
{
    unsigned long fin = jiffies + HZ/2; // 500 ms
    while(time_before(jiffies, fin))
        ;
    return 0;
}

static void __exit module_delay_exit (void)
{}

module_init(module_delay_init);
module_exit(module_delay_exit);
MODULE_LICENSE("GPL");

Ce module effectue une boucle active de 500 millisecondes dès son chargement dans le kernel. Ceci nous permet de disposer d’un appel-système suffisamment long pour perturber le temps de réponse à une interruption sur un système non-préemptible. Une remarque : j’ai également monté le thread kernel kworker qui est utilisé pour gérer une partie de l’interruption série à la priorité Fifo 50.

Résultats sur système non-préemptible

[NB: Pour étudier les résultats, après les avoir redirigés dans un fichier, j'utilise un ensemble de petits outils développés précédement dans le livre]

[Panda]# uname -a
Linux (Pandaboard) 3.0.0-cpb #4 SMP Mon Oct 31 19:52:43 CET 2011 armv7l GNU/Linux
[Panda]# taskset -p 1  $$
pid 547's current affinity mask: 3
pid 547's new affinity mask: 1
[Panda]#  chrt -f 40 . ./exemple-reponse-irq-serie /dev/ttyO2 > resultats-non-preempt.txt
(Contrôle-C après quelques minutes)
Bye!
[Panda]#

Pendant le déroulement du programme, mon script qui charge et décharge le module perturbateur toutes les cinq secondes s’exécute sur le même processeur, avec une priorité temps-réel 10.

L’analyse du résultat donne :

$ ../chapitre-04/calculer-statistiques < resultats-non-preempt.txt
Nb mesures = 1156
Minimum = 283
Maximum = 46181
Moyenne = 4737
Ecart-type = 13397
$ ../chapitre-04/calculer-histogramme 100 0 50000  < resultat-nonpreempt.txt > histo-nonpreempt.txt

[NB: L'histogramme calculé ici est ensuite injecté dans un petit script Gnuplot, présenté dans un chapitre précédent, pour produire les figures ci-dessous]

Les résultats statistiques paraissent assez catastrophiques : une variabilité entre 283 et 46181 itérations de boucles, et un écart-type largement plus grand que la moyenne des valeurs ! Graphiquement les résultats sont plus compréhensibles, comme nous le voyons sur la figure suivante.

Réponse aux interruptions sur noyau non-préemptible

Notre système fait un grand écart entre un temps de réponse faible (290 itérations par boucles, environ 3 millisecondes) et une réponse très retardée par l’appel-système perturbateur (45000 itérations par boucle, à peu près 500 ms).

Résultat sur système préemptible

Après avoir redémarré la carte Pandaboard sur un noyau Linux compilé avec l’option CONFIG_PREEMPT, nous obtenons les résultats suivants.

[Panda] # uname -a 
Linux Pandaboard 3.0.0-rc7-cpb #1 SMP PREEMPT Thu Sep 29 14:49:25 CEST 2011 armv7l GNU/Linux
[Panda]# taskset -p 1 $$
pid 579's current affinity mask: 3
pid 579's new affinity mask: 1
[Panda]#  chrt -f 40 . ./exemple-reponse-irq-serie /dev/ttyO2 > resultats-preempt.txt
(Contrôle-C après quelques minutes)
Bye!
[Panda]#

Après avoir rappatrié le fichier sur le PC de développement, nous analysons les résultats.

$ ../chapitre-04/calculer-statistiques < resultats-preempt.txt
Nb mesures = 1307
Minimum = 284
Maximum = 309
Moyenne = 287
Ecart-type = 2
$

Voilà qui est beaucoup mieux ! Le résultat est visible sur la figure suivante avec la même échelle que précédemment.
Réponse aux interruptions sur noyau préemptible (1)

Un zoom au début de l’axe des abscisses est représenté sur la figure ci-dessous. Cette fois aucune préemption du processus de haute priorité, la réponse à l’interruption est toujours comprise entre 284 et 309 boucles de la carte à micro-contrôleur. La réponse n’est pas réellement plus rapide, mais elle est plus fiable et prévisible.
Réponse aux interruptions sur noyau préemptible (2)

Conclusion

La préemptibilité optionnelle du noyau, disponible depuis sa version 2.6, est un élément important pour améliorer la fiabilité des systèmes temps-réel dans leur réponse aux événements externes. Elle permet de garantir qu’un processus de haute priorité en attente d’un événement externe (matérialisé par cette interruption) sera réveillé avec rapidité et surtout fiabilité, même si une autre tâche – de priorité moindre – est en train d’exécuter un appel-système. Ceci évite le problème classique de l’inversion de priorité.

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é.

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)…