Archive for janvier 2012

Mise au point de bibliothèque dynamique (1/3)

Linux | Publié par cpb
jan 28 2012

(English translation here)

Lors d’une récente session de formation, une conversation avec un participant m’a poussé à vérifier les options nécessaires pour effectuer du débogage et des tests en couverture sur une bibliothèque partagée.

Une bibliothèque dynamique (fichier libXXXX.soso pour Shared Object) est chargée dans la mémoire du processus au moment de son démarrage. Le fichier exécutable et la bibliothèque sont indépendants avant le lancement de l’application, et peuvent être maintenus séparément.

J’ai réalisé que certains points étaient loin d’être évidents, par exemple la gestion des numéros de version ou l’activation des tests en couverture. Voici un petit récapitulatif des étapes de mise au point d’une bibliothèque dynamique. Le premier article est consacré à la compilation, la gestion des versions et la création des liens symboliques nécessaires. Le second s’intéressera au débogage et au suivi pas-à-pas du code de la bibliothèque depuis une application. Le troisième décrira comment effectuer des tests en couverture sur le contenu de la bibliothèque.

Compilation et installation de la bibliothèque

Compilation du code de la bibliothèque

Commençons par créer une petite bibliothèque dynamique, avec une fonction relativement simple : l’implémentation de la fonction mathématique « factorielle ».

Je crée un répertoire de travail factorielle regroupant tous les fichiers concernant cette bibliothèque. Nous y créons trois sous-répertoires :

  • src/ qui contiendra le code source de la bibliothèque,
  • lib/ où seront regoupés les fichiers binaires et les liens symboliques décrits plus bas,
  • include/ dans lequel les fichiers d’en-tête de la bibliothèque seront stockés.
[~]$ mkdir factorielle
[~]$ mkdir factorielle/src
[~]$ mkdir factorielle/include
[~]$ mkdir factorielle/lib
[~]$ cd factorielle
[factorielle]$

Créons un fichier src/fact.c implémentant notre fonction.

Et si vous pensez avoir trouvé un bug dans le code ci-dessous, ayez la gentillesse de lire l’article en entier avant de m’envoyer un mail de moquerie ;-)

#include <fact.h>

long long int factorielle(long int n)
{
        long long int f = 1;
        do {
                f = f * n;
                n = n - 1;
        } while (n > 1);
        return f;
}

Ce fichier commence par inclure son propre fichier d’en-tête, ce qui permet de s’assurer à la compilation de la concordance du prototype et de l’implémentation.

Le fichier include/fact.h contient les lignes suivantes.

#ifndef LIB_FACT_H
#define LIB_FACT_H
    long long int factorielle(long int n);
#endif

Lors de la compilation de ce fichier nous fournirons sur la ligne de commande de gcc les options:

  • -c pour arrêter gcc après la phase de compilation et obtenir ainsi un fichier objet (pas d’édition des liens).
  • -I include/ qui indique à gcc de rechercher les fichiers d’en-tête .h dans le répertoire include/ en supplément des répertoires usuels (/usr/include…).
  • -fPIC pour demander la génération d’un code relogeable (Position Independant Code) comme c’est nécessaire pour la création de bibliothèques partagées même si cette option n’est pas  indispensable sur certaines architectures (x86 32 bits par exemple).

Voici un exemple de compilation.

[factorielle]$ ls src/
fact.c
[factorielle]$ ls include/
fact.h
[factorielle]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact.c
[factorielle]$ ls src/
fact.c fact.o
[factorielle]$

Cette compilation a généré un fichier objet fact.o que nous utiliserons ci-après.

On notera que durant la phase de test et de mise au point, il ne faut utiliser aucune option d’optimisation, sinon le compilateur risque de modifier le code exécutable créé (en regroupant des blocs de code par exemple) et il n’y aura plus de correspondance exacte avec le fichier source.

Génération de la bibliothèque

La bibliothèque proprement dite est obtenue en invoquant gcc avec l’option -shared. Nous allons lui demander d’enregistrer la bibliothèque dans le fichier libfact.so.1.0. Les numéros 1 et 0 correspondent respectivement aux numéros majeur et mineur de version de la bibliothèque.

Il est d’usage de considérer qu’un changement de numéro majeur représente une rupture de la compatibilité binaire de la bibliothèque et nécessite une recompilation des applications, alors qu’une variation du numéro mineur signifie des corrections ou des améliorations internes n’influant pas sur l’interface de programmation.

Nous allons indiquer à gcc d’enregistrer dans l’en-tête de la bibliothèque son nom officiel incluant le numéro majeur de version avec l’option -Wl. Celle-ci transmet au linker la chaîne de caractères qui la suit après avoir remplacé les virgules par des espaces. C’est donc l’option -soname libfact.so.1 qui est passée.

Il est conseillé de répêter les options passées lors de la compilation précédente, comme -fPIC.

[factorielle]$ gcc -fPIC -shared -Wl,-soname,libfact.so.1 -o lib/libfact.so.1.0 src/fact.o
[factorielle]$ ls lib/
libfact.so.1.0
[factorielle]$

Nous disposons donc d’un fichier libfact.so.1.0 dont l’en-tête contient le nom libfact.so.1

Création des liens symboliques

Lorsque nous compilerons une application, nous préciserons à gcc de la lier avec la bibliothèque fact. Celui-ci recherchera un fichier libfact.so. Et non pas libfact.so.1.0. Aussi va-t-il falloir créer un lien symbolique pour indiquer le chemin vers le fichier. Ce lien est créé manuellement avec la commande ln.

[factorielle]$ cd lib/
[lib]$ ln -sf libfact.so.1.0 libfact.so
[lib]$ ls -l lib*
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:04 libfact.so -> libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
[lib]$

Lors de la compilation, gcc enregistrera dans le fichier exécutable généré le nom de la bibliothèque qu’il a utilisé. Il s’agit du nom « officiel » qu’il trouve dans la section SONAME que nous avons renseignée avec l’argument -Wl,-soname précédement.

A l’exécution, le chargeur recherche donc la bibliothèque dont le numéro majeur correspond à celui utilisé lors de la compilation. Il va donc falloir qu’il trouve un fichier libfact.so.1, ou plutôt un lien libfact.so.1 qui pointe vers libfact.so.1.0.

La création du premier lien symbolique était nécessaire pour pouvoir compiler une application avec la bibliothèque, le second lien est indispensable pour pouvoir exécuter un programme lié avec elle. Ce lien est donc utilisé beaucoup plus fréquemment que le précédent. Pour simplifier la vie de l’administrateur, une commande nommée ldconfig va l’aider à créer automatiquement les liens dont son système a besoin pour que les utilisateurs puissent exécuter les applications. Elle parcourt les répertoires-système contenant des bibliothèques (/lib, /usr/lib, /usr/local/lib, etc. plus tous ceux indiqués dans /etc/ld.so.conf) et crée sur chaque fichier de bibliothèque un lien avec le nom contenu dans sa section SONAME. Nous allons en voir un exemple en forçant ldconfig à explorer uniquement notre répertoire grâce à son option -n.

[lib]$ ldconfig -n .
[lib]$ ls -l lib*
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:04 libfact.so -> libfact.so.1.0
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:05 libfact.so.1 -> libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
[lib]$

Les liens présents permettront donc de compiler une application nécessitant notre bibliothèque (par le biais de libfact.so) puis de l’exécuter en s’assurant que la version majeure soit la bonne (grâce à libfact.so.1).

Utilisation de la bibliothèque

Compilation d’une application

Écrivons un petit programme qui utilise notre bibliothèque. Le fichier factorielle.c va invoquer notre fonction factorielle() sur tous les nombres fournis sur sa ligne de commande.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fact.h>

int main (int argc, char * argv[])
{
        long int n;
        int i;
        if (argc < 2) {
                fprintf(stderr, "usage: %s valeurs...\n", argv[0]);
                exit(EXIT_FAILURE);
        }
        for (i = 1; i < argc; i ++)
                if (sscanf(argv[i], "%ld", & n) == 1)
                        fprintf(stdout, "%ld! = %lld\n", n, factorielle(n));
        return EXIT_SUCCESS;
}

Ce programme se trouve dans le répertoire factorielle/test/ que nous créons pour l’occasion. Il inclut le fichier d’en-tête <fact.h>. Il faudra donc que le compilateur puisse le trouver. Pour cela deux solutions :

  • placer le fichier d’en-tête dans /usr/include, /usr/local/include ou tout autre répertoire que gcc consulte – ceci doit être réservé aux fichiers cruciaux pour plusieurs applications utiles pour l’ensemble du système,
  • laisser le fichier dans un répertoire spécifique à notre bibliothèque et indiquer à gcc où le trouver.

C’est naturellement la seconde option que je vais utiliser.

En outre, nous ajouterons en fin de ligne l’argument -lfact qui demande au linker de réaliser l’édition des liens avec la bibliothèque libfact.so. Comme pour le fichier d’en-tête il faudra préciser à gcc où il pourra trouver le fichier libfact.so que nous avons créé plus haut sous forme de lien symbolique. C’est le rôle de l’option -L.

[factorielle]$ gcc -I ./include/ -L ./lib/ -o ./test/factorielle ./test/factorielle.c -lfact
[factorielle]$ 
[factorielle]$ ls -l test/
total 12
-rwxrwxr-x 1 cpb cpb 7359 2012-01-27 10:41 factorielle
-rw-r--r-- 1 cpb cpb  382 2012-01-25 18:29 factorielle.c
[factorielle]$

Exécution de l’application

Si nous testons directement notre programme, son exécution échoue.

$ ./test/factorielle 4 5 6
./test/factorielle: error while loading shared libraries: libfact.so.1: cannot open shared object file: No such file or directory
$

En effet, l’éditeur de liens dynamique qui doit démarrer le processus ne sait pas où trouver la bibliothèque. On remarque au passage qu’il recherche bien le fichier libfact.so.1 (avec le numéro majeur comme extension). Si notre application est suffisamment importante pour être employée régulièrement par différents utilisateurs, il est légitime de placer les fichiers de bibliothèque dans /usr/local/lib où le linker les trouvera. Toutefois si l’application est en phase de mise au point ou réservée à un emploi rare, on préférera laisser les bibliothèques dans un répertoire personnel. Dans ce cas, il faudra remplir (éventuellement dans un script de lancement) la variable d’environnement LD_LIBRARY_PATH pour ajouter le chemin d’accès à ces fichiers.

[factorielle]$ export LD_LIBRARY_PATH=./lib/ 
[factorielle]$ ./test/factorielle 4 5 6
4! = 24
5! = 120
6! = 720
[factorielle]$

Bien sûr, le contenu de la variable LD_LIBRARY_PATH peut être renseigné avec un chemin absolu plutôt que relatif si on souhaite lancer le programme exécutable depuis un emplacement quelconque.

Bibliothèque dynamique version 1.0

Bibliothèque dynamique version 1.0

Maintenance de la bibliothèque

Modification de version mineure

Notre bibliothèque semble fonctionner, nous pouvons commençer à nous livrer à des tests intensifs :

[factorielle]$ ./test/factorielle 3
3! = 6
[factorielle]$

Très bien !

[factorielle]$ ./test/factorielle 2
2! = 2
[factorielle]$

Parfait !

[factorielle]$ ./test/factorielle 1
1! = 1
[factorielle]$

Aucun souci.

[factorielle]$ ./test/factorielle 0
0! = 0
[factorielle]$

Aïe !

Et oui, par convention, il est posé que 0! = 1 (vous pouvez vérifier sur Wikipédia si vous le souhaitez). Notre programme est donc défectueux. La correction est relativement simple, il suffit de remplacer la boucle

        do {
                f = f * n;
                n = n - 1;
        } while (n > 1);

par

        while (n > 1) {
                f = f * n;
                n = n - 1;
        }

C’est ce que j’ai fait dans le fichier fact-2.c. En principe je devrais garder le même nom de fichier source et le remplacer simplement pour la nouvelle version de la bibliothèque. Je veux ici conserver la version précédente simplement à titre pédagogique.

Je vais le compiler, puis générer une nouvelle version de bibliothèque en incrémentant le numéro mineur. L’interface de la fonction n’étant pas modifiée, les fichiers exécutables qui en dépendent doivent continuer à fonctionner normalement.

[factorielle]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact-2.c
[factorielle]$ gcc -fPIC -shared -Wl,-soname,libfact.so.1 -o lib/libfact.so.1.1 src/fact.o

Notre bibliothèque a été re-générée dans un nouveau nom de fichier, aussi faut-il relancer la commande ldconfig.

[factorielle]$ ldconfig -n lib/
[factorielle]$ ls -l lib/
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:04 libfact.so -> libfact.so.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
[factorielle]$
[factorielle]$ ./test/factorielle 0 1 2
0! = 1
1! = 1
2! = 2
[factorielle]$

Notre programme fonctionne correctement pour 0!, et l’ancienne version de la bibliothèque n’étant plus utilisée, il est possible de l’effacer.

[factorielle]$ rm -f lib/libfact.so.1.0 
[factorielle]$
Bibliothèque dynamique version 1.1

Bibliothèque dynamique version 1.1

Modification de version majeure

Après quelques essais, nous arrivons face à un nouveau problème avec notre bibliothèque.

[factorielle]$ ./test/factorielle -3
-3! = 1
[factorielle]$

Notre fonction renvoie une valeur lorsqu’on lui passe un nombre négatif. La véritable factorielle mathématique n’est définie que sur l’ensemble des entiers naturels, pas pour les entiers relatifs négatifs. Notre fonction devrait donc signaler l’erreur d’argument et non pas renvoyer une valeur, cohérente il est vrai mais trompeuse.

Nous choisissons de modifier l’interface de notre routine, qui va prendre en argument un pointeur sur un entier long long dans lequel elle stockera le résultat, et renverra une valeur de réussite (zéro) ou d’échec (-1). Cette modification d’interface va impliquer une adaptation et une recompilation des applications utilisant la bibliothèque, aussi devrons-nous changer de version majeure.

La nouvelle fonction fact-3.c est définie comme suit.

int factorielle(long int n, long long int * result)
{
        * result = 1;
        if (n < 0)
                return -1;
         do {
                 (*result) = (*result) * n;
                 n = n - 1;
         } while (n > 1);
        return 0;
}

Bien sur, on modifie le fichier d’en-tête en (fact-3.h):

#ifndef LIB_FACT_H
#define LIB_FACT_H
        int factorielle(long int n, long long int * result);
#endif

Compilons notre bibliothèque comme précédemment.

[factorielle]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact-3.c
[factorielle]$ gcc -fPIC -shared -Wl,-soname,libfact.so.2 -o lib/libfact.so.2.0 src/fact.o
[factorielle]$ ldconfig -n lib/
[factorielle]$ ls -l lib/
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:04 libfact.so -> libfact.so.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:26 libfact.so.2 -> libfact.so.2.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:26 libfact.so.2.0
[factorielle]$ cd lib/
[lib]$ ln -sf libfact.so.2 libfact.so
[lib]$ ls -l
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:27 libfact.so -> libfact.so.2
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:26 libfact.so.2 -> libfact.so.2.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:26 libfact.so.2.0
[lib]$ cd ..
[factorielle]$

Les liens sont en place pour compiler une nouvelle version du programme de test (factorielle-2.c).

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fact.h>

int main (int argc, char * argv[])
{
        long int n;
        long long int f;
        int i;
        if (argc < 2) {
                fprintf(stderr, "usage: %s valeurs...\n", argv[0]);
                exit(EXIT_FAILURE);
        }
        for (i = 1; i < argc; i ++)
                if (sscanf(argv[i], "%ld", & n) == 1) {
                        if (factorielle(n, & f) == 0)
                                fprintf(stdout, "%ld! = %lld\n", n, f);
                        else
                                fprintf(stdout, "%ld! n'existe pas\n", n);
                }
        return EXIT_SUCCESS;
}

Compilons et essayons-le :

[factorielle]$ gcc -I ./include/ -L ./lib/ -o ./test/factorielle-2 ./test/factorielle-2.c -lfact
[factorielle]$ ./test/factorielle-2 3 0 -3
3! = 6
0! = 0
-3! n'existe pas
[factorielle]$

Cette fois notre programme se comporte correctement. On peut noter que la présence de l’ancienne version majeure permet à notre précédent exécutable de continuer à fonctionner.

[factorielle]$ ./test/factorielle 3 0 -3
3! = 6
0! = 1
-3! = 1
[factorielle]$
Bibliothèque dynamique version 2.0

Bibliothèque dynamique version 2.0

Conclusion

La gestion des numéros majeurs et mineurs de version pour les bibliothèques dynamiques offre les avantages suivants :

  • Les modifications internes uniquement, représentées par des évolutions du numéro mineur, permettent aux exécutables déjà compilés de fonctionner directement avec la nouvelle version de la bibliothèque et de bénéficier – sans recompilation – des améliorations.
  • Les transformations de l’interface externe de la bibliothèque impliquent une recompilation (éventuellement après adaptation) pour pouvoir fonctionner.

Plusieurs versions majeures de la bibliothèque peuvent cohabiter simultanément permettant un fonctionnement correct de différentes générations d’une application. Toutefois les nouvelles compilations utiliseront la version majeure pointée par le lien symbolique contenant uniquement le nom de la bibliothèque (libfact.so)

Nous verrons dans le prochain article comment déboguer le code de la bibliothèque dynamique en effectuant un suivi pas-à-pas de l’exécution et en examinant le contenu de ses variables.

 

Development of a dynamic library (1/3)

Linux | Publié par cpb
jan 28 2012

(Version originale en français)

During a recent training session, a conversation with a participant gave me the idea to check the options needed to perform debugging and coverage tests on a shared library.

A dynamic library (libXXXX.so file – « so » standing for Shared Object) is loaded into memory when the process starts. The executable file and the library file are independent before launching the application, and can be maintained separately.

I realized that some points were far from obvious, such as managing version numbers or activating coverage tests. Here is a list of the steps needed for the development of a dynamic library. The first article is devoted to compilation, version control and symbolic links. The second one will focus on debugging and step-by-step tracing of the library code. The third will describe how to perform coverage tests on the library.

Compiling and installing the library

Compiling library code

Let’s start by creating a small dynamic library, with a simple function: the implementation of the mathematical « factorial ».

I create a working directory named factorial including all the files. Then we make three sub-directories:

  • src/ containing the source code of the library,
  • lib/ where stand binary files and symbolic links described below,
  • include/ storing the header files of the library.
[~]$ mkdir factorial
[~]$ mkdir factorial/src
[~]$ mkdir factorial/include
[~]$ mkdir factorial/lib
[~]$ cd factorial
[factorial]$

Let’s create a file named src/fact.c containing our function.

And if you think you have found a bug in the code below, be kind and read the entire article before sending me a mocking mail ;-)

#include <fact.h>

long long int factorial(long int n)
{
        long long int f = 1;
        do {
                f = f * n;
                n = n - 1;
        } while (n > 1);
        return f;
}

This file includes its own header, which ensures consistency of the prototype and implementation.

The file include/fact.h contains the following lines.

#ifndef LIB_FACT_H
#define LIB_FACT_H
    long long int factorial(long int n);
#endif

When compiling this file we will provide the following options on the gcc command line:

  • -c to tell gcc to stop his job after the compilation phase and thus providing an object file (not linking).
  • -I include/ telling gcc to look for .h header files in the include/ directory in addition to the usual directories (/usr/include…).
  • -fPIC to request the generation of a relocatable code (PIC stands for Position Independent Code). It is necessary for the creation of shared libraries even if this option has no effect on some architectures (x86 32-bit for example).

Here is an example of compilation:

[factorial]$ ls src/
fact.c
[factorial]$ ls include/
fact.h
[factorial]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact.c
[factorial]$ ls src/
fact.c fact.o
[factorial]$

The generated fact.o object file will be used below.

Note that during the development and testing phase, we do not use any optimization option, otherwise the compiler may change the executable code created and there won’t be an exact matching with the source file.

Generation of the library

The library itself is created by invoking gcc with the -shared option. We ask him to save the library in the libfact.so.1.0 file. The numbers 1 and 0 correspond respectively to the major and minor numbers of the library version.

It is customary to consider that a major number change represents a break in binary compatibility of the library and requires recompilation of applications, while a variation of the minor number represents only internal corrections or improvements that do not interfere with the programming interface.

We will tell gcc with the -Wl option to record in the heade rof the library its official name of the library including the major version number. It passes to the linker the string that follows the -Wl option after replacing commas by spaces. Thus the linker gets the -soname libfact.so.1 string.

It is recommanded to repeat the options passed in the previous compilation, such as -fPIC.

[factorial]$ gcc -fPIC -shared -Wl,-soname,libfact.so.1 -o lib/libfact.so.1.0 src/fact.o
[factorial]$ ls lib/
libfact.so.1.0
[factorial]$

So we get the libfact.so.1.0 file, whose header contains the libfact.so.1 name.

Creating symbolic links

When we compile an application, we tell gcc to link it with the fact library. He looks for a file named libfact.so, not libfact.so.1.0. So we have to create a symbolic link named libfact.so pointing to the real library file. This link is created manually using the ln command.

[factorial]$ cd lib/
[lib]$ ln -sf libfact.so.1.0 libfact.so
[lib]$ ls -l lib*
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:04 libfact.so -> libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
[lib]$

During compilation gcc records the name of the library he used into the executable. This is the « official » name found in the SONAME section we filled previously with the -Wl,-soname option.

At runtime, the loader searches the library which major number matches those used during compilation. So he has to find a file named libfact.so.1, or rather a symbolic link named libfact.so.1 pointing to libfact.so.1.0.

The creation of the first symbolic link was needed to compile an application with the library, the second link is essential to run a program associated with it. This link is used much more frequently than the first one. To make the life of the administrator easier, a command named ldconfig will help to automatically create the links needed to allow users to run applications. It searches the directories containing system libraries (/lib, /usr/lib, /usr/local/lib, etc… and all given in /etc/ld.so.conf) and creates links on each library file with the name contained in the section. Let’s see an example where I force ldconfig through its -n option to explore only our lib/ directory.

[lib]$ ldconfig -n .
[lib]$ ls -l lib*
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:04 libfact.so -> libfact.so.1.0
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:05 libfact.so.1 -> libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
[lib]$

The links will allow us to compile an application that requires our library (through libfact.so) and then run it by making sure the major version is the right one (with libfact.so.1).

Using the library

Compiling an application

Let’s write a small program that uses our library. The factorial.c file will call our factorial() function for all the numbers found on the command line.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fact.h>

int main (int argc, char * argv[])
{
        long int n;
        int i;
        if (argc < 2) {
                fprintf(stderr, "usage: %s value...\n", argv[0]);
                exit(EXIT_FAILURE);
        }
        for (i = 1; i < argc; i ++)
                if (sscanf(argv[i], "%ld", & n) == 1)
                        fprintf(stdout, "%ld! = %lld\n", n, factorial(n));
        return EXIT_SUCCESS;
}

This file is located in the factorial/test/ directory that we create now. It includes the <fact.h> header file. So the compiler has to find this header file. Two solutions:

  • Put the header file in /usr/include, /usr/local/include or any other directory where gcc searches. This should be reserved for critical files, needed by several applications and useful for the entire system.
  • Keep the file in an application specific directory and tell gcc where to find it.

I will obviously choose the second one.

In addition, we will put the -lfact option at the end of the command line, asking the linker to perform the linking with the libfact.so library. As for the header file, gcc must be told where he can find the libfact.so file we created previously as symbolic link. It is the role of the -L option.

[factorial]$ gcc -I ./include/ -L ./lib/ -o ./test/factorial ./test/factorial.c -lfact
[factorial]$ 
[factorial]$ ls -l test/
total 12
-rwxrwxr-x 1 cpb cpb 7359 2012-01-27 10:41 factorial
-rw-r--r-- 1 cpb cpb  382 2012-01-25 18:29 factorial.c
[factorielle]$

Running the application

If we run our program directly, the execution fails.

$ ./test/factorial 4 5 6
./test/factorial: error while loading shared libraries: libfact.so.1: cannot open shared object file: No such file or directory
$

Indeed, the dynamic linker which should start the process does not know where to find the library. We can see that it searches for the libfact.so.1 file (with the major number as extension). If our application is important enough to be used regularly by different users, it is legitimate to place library files in /usr/local/lib where the loader will find them. However if the application is currently under development or reserved for personnal use, it is preferable to leave the library in a sub-directory of our home directory. In this case, we must be fill (possibly in a startup script) the environment variable LD_LIBRARY_PATH to add the path to the library file.

[factorial]$ export LD_LIBRARY_PATH=./lib/ 
[factorial]$ ./test/factorial 4 5 6
4! = 24
5! = 120
6! = 720
[factorial]$

Of course, the LD_LIBRARY_PATH variable can be given an absolute path rather than a relative one if you want to lauch the application from any location of the filesystem tree.

Dynamic library v. 1.0

Dynamic library v. 1.0

Maintaining the library

Minor version update

Our library seems to works, let’s engage intensive testings:

[factorial]$ ./test/factorial 3
3! = 6
[factorial]$

Very good!

[factorial]$ ./test/factorial 2
2! = 2
[factorial]$

Perfect!

[factorial]$ ./test/factorial 1
1! = 1
[factorial]$

No problem.

[factorial]$ ./test/factorial 0
0! = 0
[factorial]$

Ouch!

By convention, it is hypothesized that 0! = 1 (you can check on Wikipedia if you wish). Our program is defective. The correction is fairly simple, just replace the loop

        do {
                f = f * n;
                n = n - 1;
        } while (n > 1);

by

        while (n > 1) {
                f = f * n;
                n = n - 1;
        }

That’s what I did in fact-2.c file. Theoretically I should keep the same source file and replace it in the new version of the library. I wanted to keep the previous version here for demonstration purposes so did I rename the file.

I will compile and generate a new library version incrementing the minor number. The interface of the factorial() function is not changed, executable files that depend on the library will continue to operate normally.

[factorial]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact-2.c
[factorial]$ gcc -fPIC -shared -Wl,-soname,libfact.so.1 -o lib/libfact.so.1.1 src/fact.o

Our library has been re-created with a new file name, so it’s necessary to rerun the ldconfig command.

[factorial]$ ldconfig -n lib/
[factorial]$ ls -l lib/
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:04 libfact.so -> libfact.so.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
[factorial]$
[factorial]$ ./test/factorial 0 1 2
0! = 1
1! = 1
2! = 2
[factorial]$

Our code works correctly for 0!. The previous version of the library is no more used, so we can delete it

[factorial]$ rm -f lib/libfact.so.1.0 
[factorial]$
Dynamic library v. 1.1

Dynamic library v. 1.1

Major version update

After a few tests, we are facing a new problem with our library.

[factorial]$ ./test/factorial -3
-3! = 1
[factorial]$

Our function returns a value when given a negative number. in mathematics, the factorial is only defined for natural numbers, not for negative integers. The function should report the argument error and not return a value (coherent but misleading).

We choose to modify the interface of our routine, which will take as argument a pointer to a long long integer where it will store the result and return a success (zero) or failure (-1) value. This change will involve an interface adaptation and recompilation of the applications using the library. So must we change the major version.

The new function in fact-3.c is implemented as follow:

int factorial(long int n, long long int * result)
{
        * result = 1;
        if (n < 0)
                return -1;
         do {
                 (*result) = (*result) * n;
                 n = n - 1;
         } while (n > 1);
        return 0;
}

Of course, we change the header file (fact-3.h):

#ifndef LIB_FACT_H
#define LIB_FACT_H
        int factorial(long int n, long long int * result);
#endif

We compile our library as we did before:

[factorial]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact-3.c
[factorial]$ gcc -fPIC -shared -Wl,-soname,libfact.so.2 -o lib/libfact.so.2.0 src/fact.o
[factorial]$ ldconfig -n lib/
[factorial]$ ls -l lib/
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:04 libfact.so -> libfact.so.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:26 libfact.so.2 -> libfact.so.2.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:26 libfact.so.2.0
[factorial]$ cd lib/
[lib]$ ln -sf libfact.so.2 libfact.so
[lib]$ ls -l
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:27 libfact.so -> libfact.so.2
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:26 libfact.so.2 -> libfact.so.2.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:26 libfact.so.2.0
[lib]$ cd ..
[factorial]$

The links are in place to compile a new version of the test program (factorial-2.c).

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fact.h>

int main (int argc, char * argv[])
{
        long int n;
        long long int f;
        int i;
        if (argc < 2) {
                fprintf(stderr, "usage: %s value...\n", argv[0]);
                exit(EXIT_FAILURE);
        }
        for (i = 1; i < argc; i ++)
                if (sscanf(argv[i], "%ld", & n) == 1) {
                        if (factoriel(n, & f) == 0)
                                fprintf(stdout, "%ld! = %lld\n", n, f);
                        else
                                fprintf(stdout, "%ld! doesn't exist\n", n);
                }
        return EXIT_SUCCESS;
}

Let us compile and try it:

[factorial]$ gcc -I ./include/ -L ./lib/ -o ./test/factorial-2 ./test/factorial-2.c -lfact
[factorial]$ ./test/factorial-2 3 0 -3
3! = 6
0! = 0
-3! doesn't exist
[factorial]$

This time our application behaves correctly. We can remark that the presence of the former major release enables our previous executable to continue operating.

[factorial]$ ./test/factorial 3 0 -3
3! = 6
0! = 1
-3! = 1
[factorial]$
Dynamic library v. 2.0

Dynamic library v. 2.0

Conclusion

The management of major and minor numbers of dynamic library versions offers the following advantages:

  • The internal only modifications, represented by changes in the minor number, allow already compiled executables to run directly with the new version of the library and enjoy – without recompilation – the latest improvements.
  • Changes in the external interface of the library require a recompilation (possibly after adaptation) of the application.

Several major versions of the same library can coexist simultaneously for proper operation of different generations of an application. However, the compiler uses the new major version pointed to by the main symbolic link of the library (libfact.so)

We will see in the next article how to debug the code in the dynamic library, tracing it with step-by-step execution and examining the contents of its variables.

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.

Linux 3.2 – CFS CPU Bandwidth

Linux, Temps-réel | Publié par cpb
jan 07 2012

(English translation here)

Linus Torvalds a publié le noyau Linux 3.2 il y a deux jours. Ce dernier contient comme d’habitude de nombreux ajouts. L’un d’eux a attiré mon attention et j’ai souhaité observer son fonctionnement, il s’agit du contrôleur de consommation CPU pour l’ordonnanceur CFS.

Il existait déjà de nombreux moyens de régler le pourcentage de CPU dont pouvait disposer une tâche par rapport aux autres pour l’ordonnancement temps-partagé (entre autres avec l’appel-système setpriority(), la commande en ligne nice, ou les paramètres du systèmes de fichiers /sys/fs/cgroup/cpu). On pouvait ainsi attribuer facilement 25% du temps processeur à une tâche et 75% à une autre par exemple. Mais jusqu’alors, si la seconde se terminait, la première disposait alors de 100% du temps CPU.

Autrement dit, la « bande passante CPU » d’une tâche seule active était toujours de 100%. Il est à présent possible de modifier ceci afin de lui attribuer une portion plus réduite du temps processeur, même lorsqu’elle est seule.

Quel peut-être l’intérêt de réduire la consommation CPU instantanée d’un processus ? Pour les systèmes courants, il faut bien l’avouer, cet intérêt est réduit. Mais il en va autrement pour certains gros serveurs où l’on facture le temps processeur consommé mais également la puissance CPU mise à disposition instantanément. Dans ces environnements il est très intéressant de limiter la puissance CPU dont disposera un processus quelle que soit la charge globale du système.

Un autre intérêt de cette limitation est de réguler la disponibilité du CPU en évitant les pointes de puissance et les brusques ralentissements. On peut imaginer encore un environnement hôte sur lequel on est amené à faire fonctionner simultanément plusieurs – disons 4 – émulateurs (de type Qemu par exemple). En limitant à 25% la puissance CPU accordée à chacun d’entre-eux, le comportement d’un système virtuel ne dépendra pas de la présence ou non d’un autre émulateur sur l’hôte.

Mise en oeuvre

Il existe une nouvelle option de compilation que l’on trouve dans le menu de configuration du kernel dans l’arborescence suivante : « General Setup » – « Control Group support » – « Group CPU scheduler » – « CPU bandwith provisioning for FAIR_GROUP_SCHED« . Il est nécessaire d’activer toutes les options de ce chemin.

Voici un fichier de configuration générique Linux 3.2 pour PC (provenant d’une distribution Ubuntu 11.10) avec l’option « CPU bandwidth provisioning » activée.

Après compilation et redémarrage sur le nouveau kernel, nous observons la présence d’un nouveau fichier  /proc/sys/kernel/sched_cfs_bandwidth_slice_us qui indique la durée des tranches de temps employées lorsqu’une tâche bénéficie d’une puissance CPU reposant sur plusieurs processeurs. Par défaut cette valeur est de 5 millisecondes.

# cat /proc/sys/kernel/sched_cfs_bandwidth_slice_us
5000
#

Pour accéder au paramétrage de la puissance CPU accordée à un groupe ou à une tâche nous devons passer par le système de fichiers cgroup :

# mount none /sys/fs/cgroup -t tmpfs
# mkdir /sys/fs/cgroup/cpu
# mount none /sys/fs/cgroup/cpu/ -t cgroup -o cpu
# ls /sys/fs/cgroup/cpu/
cgroup.clone_children  cgroup.procs       cpu.cfs_quota_us  cpu.rt_runtime_us  cpu.stat           release_agent
cgroup.event_control   cpu.cfs_period_us  cpu.rt_period_us  cpu.shares         notify_on_release  tasks
#

Essais

Tâche unique

Pour verifier la puissance CPU accordée à une tâche nous allons créer un petit programme qui boucle indéfiniment, et affiche toutes les secondes sur sa sortie standard le nombre de boucles qu’il a pu réaliser durant la seconde écoulée.

consomme-cpu.c:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

int main(void)
{
    long long int compteur;
    time_t debut = time(NULL);
    // Attendre un debut de seconde
    while (time(NULL) == debut)
        ;
    while (1) {
        compteur = 0;
        time (& debut);
        while (time(NULL) == debut)
            compteur ++;
        fprintf(stdout, "[%u]%lld\n", getpid(), compteur);
    }
    return 0;
}

Lançons ce processus dans un terminal et laissons-le tourner.

$ ./consomme-cpu 
          [3040]8046202
          [3040]8051003
          [3040]8038645
          [3040]8049329
          [3040]8378210
          [3040]8419106
          [3040]8416285
          [3040]8418075
          [3040]8415878
          [3040]8419727
          [3040]8416073
          [3040]8417343
          [3040]8414809
          ...

Il réalise environ huit millions de boucles par seconde. Dans une autre console, créons un groupe de contrôle spécifique et inscrivons notre processus.

# cd /sys/fs/cgroup/cpu/
# mkdir groupe-1
# echo 3040 > groupe-1/tasks
#

Les paramètres de contrôle de la bande passante sont par défaut les suivants.

# cat groupe-1/cpu.cfs_period_us
100000
# cat groupe-1/cpu.cfs_quota_us
-1
#

La période de régulation de la bande passante est donc de 100 millisecondes, et le quota CPU accordé à la tâche vaut -1, une valeur négative indiquant qu’aucune contrainte n’est appliquée au processus.

Nous allons modifier cette valeur pour lui accorder un quota de 25 millisecondes par périodes de 100 millisecondes.

# echo 25000 > groupe-1/cpu.cfs_quota_us
#

Le comportement du processus varie alors :

          [3040]8416132
          [3040]8417509
          [3040]4302933
          [3040]1766917
          [3040]1763016
          [3040]1798414
          [3040]1740740
          ...

Le nombre de boucles descend alors à 18 millions environ. Ré-augmentons le quota à 50%.

# echo 50000 > groupe-1/cpu.cfs_quota_us
#

Nous voyons alors le nombre de boucles par secondes augmenter à nouveau.

          [3040]1672509
          [3040]2776452
          [3040]3662061
          [3040]3777860
          [3040]3694039
          [3040]3768352
          [3040]3745732
          [3040]3822385
          ...

Restaurons-la valeur 100000 microsecondes et notre processus reprend son comportement initial.

# echo 100000 > groupe-1/cpu.cfs_quota_us
#
          [3040]3708664
          [3040]3779417
          [3040]5944852
          [3040]8051275
          [3040]8049984
          [3040]8050405
          ...

Groupe de tâches

Observons à présent le comportement lorsque nous lançons plusieurs tâches en parallèle que nous inscrivons dans le même groupe de contrôle.

          $ ./consomme-cpu & ./consomme-cpu
          [6105]8051374
          [6106]8050150
          [6106]8047698
          [6105]8046993
          [6106]8046275
          [6105]8046629
          [6106]8050943
          [6105]8044749
          [6106]8049421
          [6105]8047196
          ...

Nos tâches ont été placées sur deux processeurs (ou cœurs) distincts et arrivent ainsi à réaliser chacune 8 millions de boucles par seconde. Toutefois nous pouvons les limiter et leur attribuer un quota de temps-processeur plus faible. Par exemple 100% CPU en tout (pour les deux).

# echo 6105 > groupe-1/tasks
# echo 6106 > groupe-1/tasks
# echo 100000 > groupe-1/cpu.cfs_quota_us
#

Chacune arrive à réaliser environ 4 millions de boucles par seconde.

          [6105]8055582
          [6106]8023743
          [6105]7876233
          [6106]7598323
          [6106]3678658
          [6105]3910164
          [6105]3800836
          [6106]3753870
          [6105]3776468
          [6106]3659704
          ...

Ou encore une valeur de 150% CPU (en s’appuyant sur deux processeurs bien sûr).

# echo 150000 > groupe-1/cpu.cfs_quota_us
#
          [6105]3667414
          [6106]3831150
          [6106]3839798
          [6105]3714979
          [6105]5004119
          [6106]5851544
          [6106]5801272
          [6105]6028022
          [6105]5810675
          [6106]5749970
          [6106]5784018
          [6105]5769202
          ...

Conclusion

Les valeurs observées ci-dessus ne sont pas parfaitement stables et exactes, eu égard à l’ordonnancement temps-partagé qui prend en compte l’ensemble des tâches prêtes à s’exécuter et le comportement de chacune d’entre-elles vis-à-vis de la consommation de temps processeur.

Le contrôle de la puissance CPU disponible pour chaque groupe de tâches est à mon avis principalement intéressant pour les serveurs d’applications et les containers de machines virtuelles. Dans mon cas particulier, je pense l’employer lorsque je fais tourner des programmes de tests sur plusieurs instances de Qemu afin d’isoler chacune de la puissance CPU globalement disponible sur le serveur hôte.

<p style= »text-align: justify; »>