Remplir un système de fichier avec des données aléatoires

Gazette Linux n°153 d'Août 2008

René Pfeiffer

Adaptation française: Tbowan

Relecture de la version française: Deny

Article paru dans le n°153 de la Gazette Linux d'août 2008.

Article publié sous Open Publication License. La Linux Gazette n'est ni produite, ni sponsorisée, ni avalisée par notre hébergeur principal, SSC, Inc.


Table des matières

Pourquoi des données aléatoires ?
Utiliser un script bash
Employer C++
Options du programme
Noms de fichier et chemins
Trouver une source aléatoire
Remplir récursivement
Notes
Ressources utiles

De temps en temps, je teste les systèmes de fichier. Depuis que EXT4 a été ajouté dans le noyau Linux, j'ai installé quelques partitions et je les utilise pour y stocker des données. Parfois, vous ne voulez pas copier des données existantes sur un nouveau système de fichier quand vous le testez. Pourquoi n'utiliserions-nous pas des données aléatoires ? J'ai posé la question à « The Answer Gang », c'est comme ça que j'ai eu une excuse pour coder un outil.

Pourquoi des données aléatoires ?

L'une des manières les plus facile de remplir un système de fichier est d'utiliser des outils de téléchargement, extraire le contenu d'une distribution GNU/Linux (faire une installation standard et utiliser le système de fichier racine), ou simplement copier des parties de /dev/zero en utilisant la commande dd. Vous pouvez même mettre des sauvegardes sur le système de fichier en le remplissant sensiblement. Toutes ces méthodes fonctionnent, mais copier signifie que vous devez aller chercher les données quelque part. Il y a toujours une source et une destination. Je ne veux pas de source, je veux créer des données à partir de rien et utiliser uniquement une simple destination. Ensuite, quand on fait des tests d'un système de fichier et que quelque chose tourne mal, on a pas nécessairement envie de publier des parties du système de fichier dans le rapport de bogue, contenant des parties de vos sauvegardes. En plus, je veux juste donner les limites en terme de profondeur de l'arborescence, nombre de fichiers/répertoires, et nombre maximum d'octets par fichier et que ça suffise.

Utiliser un script bash

Et donc, après avoir envoyé ma question à la mailing list, Ben m'a répondu avec un script bash.

#!/bin/bash
# Created by Ben Okopnik on Wed Jul 16 18:04:33 EDT 2008

########    Paramétrage utilisateur     ############
MAXDIRS=5
MAXDEPTH=2
MAXFILES=10
MAXSIZE=1000
######## Fin du Paramétrage utilisateur ############

# À quelle profondeur dans le système
# de fichier sommes-nous ?
TOP=`pwd|tr -cd '/'|wc -c`

populate() {
	cd $1
	curdir=$PWD

	files=$(($RANDOM*$MAXFILES/32767))
	for n in `seq $files`
	do
		f=`mktemp XXXXXX`
		size=$(($RANDOM*$MAXSIZE/32767))
		head -c $size /dev/urandom > $f
	done

	depth=`pwd|tr -cd '/'|wc -c`
	if [ $(($depth-$TOP)) -ge $MAXDEPTH ]
	then
		return
	fi

	unset dirlist
	dirs=$(($RANDOM*$MAXDIRS/32767))
	for n in `seq $dirs`
	do
		d=`mktemp -d XXXXXX`
		dirlist="$dirlist${dirlist:+ }$PWD/$d"
	done

	for dir in $dirlist
	do
		populate "$dir"
	done
}

populate $PWD

Et cela effectue tout le travail. Il utilise le générateur pseudo-aléatoire (PRNG - Pseudo-RaNdom Generator) inclus dans bash, et crée et descend récursivement dans les répertoires jusqu'à atteindre la profondeur maximale. Le shell est parfaitement adapté pour ça. Vous pouvez aussi le faire en Perl, Python ou n'importe quel autre langage de script.

Employer C++

Les raisons pour écrire une version C++ du script sont purement pédagogiques. Ça ne fait pas de mal d'écrire du code. Étapes par étapes, vous pouvez apprendre des choses et vous améliorer, ce qui n'est pas plus mal. Ma principale intention a été d'essayer la librairie d'analyse des options du programme du projet Boost. L'algorithme se trouve déjà dans le script shell, tout ce qu'on a donc à faire c'est de récupérer les options de l'utilisateur et de créer les fichiers et les répertoires.

Options du programme

Les options en ligne de commande consistent habituellement en quelques choix. Certains prennent un paramètre, d'autres non. Très souvent, il y a une option d'aide présentant un petit texte d'aide, décrivant toutes les options possibles et leurs valeurs par défaut. La librairie d'analyse des options de programme de Boost vous permet de le faire très facilement. Vous créez un objet options_description, et décrivez simplement ce que vous voulez avoir.

popt::options_description desc("Options for fspopulate");
desc.add_options()
    ("debug,d", popt::value<unsigned int>(&opt_debug)->default_value(0),
     "Définit le niveau de débogage du code.")
    ("dirlevels,D", popt::value<unsigned int>(&opt_dirlevels)->default_value(16),
     "Définit le nombre maximum de niveaux de répertoires.")
    ("help,h", "Affiche un bref message d'aide avec des explications.")
    ("logfile,l", popt::value(&opt_logfile), "Log file for recording information.")
    ("maxfilesize", popt::value<unsigned int>(&opt_max_filesize)->default_value(INT_MAX/2),
     "Taille maximum de fichiers crées.")
    ("maxnamelen", popt::value<unsigned short>(&opt_max_namelength)->default_value(100),
     "Longueur maximum des noms de fichier et de répertoire.")
    ("numdirs", popt::value<unsigned int>(&opt_number_directories)->default_value(512),
     "Nombre maximum de répertoires crées.")
    ("numfiles", popt::value<unsigned int>(&opt_number_files)->default_value(1024),
     "Nombre maximum de fichiers crées.")
    ("path,p", popt::value(&opt_path), "Chemin du répertoire qui doit être rempli.")
    ("quiet,q", popt::bool_switch(&opt_quiet), "Soyez tranquille au sujet de l'ensemble de l'operation.")
;


Ceci définit toutes les options, les valeurs par défaut, les types de données, et fournit un petit texte d'aide. Comme vous pouvez le voir, il est possible de fournir différents choix comme par exemple --debug et -d pour donner le niveau de déboguage. Tout ce qu'on a donc à faire maintenant, c'est d'analyser le tableau des options de la ligne de commande. Vous le faites en créant un objet variables_map et en utilisant les méthodes store/notify.

popt::store(popt::parse_command_line(argc, argv, desc), vm);
popt::notify(vm);

C'est tout. Maintenant, vous pouvez accéder aux options de la ligne de commande via l'objet vm. Vérifier la présence d'une option et extraire sa valeur peut se faire de cette façon :

// Vérifie l'option d'aide
if ( vm.count("help") > 0 ) {
    cout << desc << endl;
    return(rc);
}
// Extrait le niveau de déboguage
if ( (vm.count("debug") > 0) or (vm.count("d") > 0) ) {
    opt_debug = vm["debug"].as<unsigned int>();
}


En plus, la librairie des options de programme Boost vous permet d'analyser les options à partir d'un fichier de configuration.

Noms de fichier et chemins

Les noms de fichiers et les chemins sont en gros des chaînes de caractères avec des propriétés supplémentaires. Il n'y a aucun problème, tant que vous écrivez le code pour un seul système. Si vous voulez écrire du code portable, vous allez devoir gérer les séparateurs de répertoires ("/" ou "\"), les lettres de lecteurs, et autres bizarreries. La librairie de système de fichier de Boost nous aide à gérer les noms de fichiers et les chemins de manière facilement compréhensible. Vous n'avez qu'à utiliser un objet path, et la librairie gère la traduction du chemin en local.

L'objet path représente un chemin. L'objet connaît les séparateurs de répertoires valides utilisés par votre système. Il connaît les concepts comme le répertoire racine, et vous pouvez facilement extraire le nom de base d'un fichier en appelant la méthode leaf(). (Vous pouvez en faire plus, voici une liste affichant la table de décomposition des chemins qui montre ce qu'on peut extraire et comment le faire.) Vous pouvez aussi utiliser les itérateurs pour naviguer récursivement dans le système de fichier. En plus, la librairie surcharge les opérateurs, de telle manière que la construction d'objets path soit aussi facile que d'écrire newpath = arg_path / "foo" / "bar". À ce stade, les gens qui utilisent des langages de script vont surement bailler. Et bien, vous pouvez faire la même chose en C++.

Trouver une source aléatoire

Maintenant, on a besoin d'une source aléatoire pour notre code. Utiliser /dev/random est hors de question; cette ressource est trop précieuse pour créer des nombres aléatoires, puisque la taille de la réserve d'entropie est limitée. /dev/urandom a l'air mieux, mais y lire va aussi vider la réserve d'entropie du noyau, bien que beaucoup plus lentement. Imaginez si nous peuplions un système de fichier avec des gigas octets de données. Ça nous laisserait presque aucune entropie dans la réserve, bien que /dev/urandom ne s'arrête pas et continue de nous fournir des nombres aléatoires. Si une application lit /dev/random en même temps, elle devra attendre que la réserve d'entropie se remplisse à nouveau. Il doit y avoir une autre manière. Nous n'avons pas vraiment besoin d'entropie pour notre travail, en effet, le pseudo-alétoire est plus qu'assez dans notre cas. Nous pourrions utiliser la fonction C lrand48(), mais il y a une autre solution. Le Mersenne Twister PRNG est assez rapide, et a une implémentation en C. Il y a même une version qui exploite les optimisations de certains CPU. Essayons-le.

Nous allons lier notre code avec celui de l'implémentation SIMD du Fast Mersenne Twister. C'est très facile à faire. Le code fournit les fonctions habituelles pour initialiser l'état du PRNG et extraire les nombres aléatoires au format entiers 32/64 bits ou à virgule flotante.

  • init_gen_rand() initialise le générateur avec une graine;

  • gen_rand32() et gen_rand64() retournent des entiers aléatoires;

  • genrand_res53() fournit un nombre aléatoire à virgule flotante (53 bits). Le nombre est généré en interne à partir de nombres entiers aléatoires;

  • fill_array32() et fill_array64() remplissent des tableaux d'entiers 32/64 bits avec des données aléatoires. Ces fonctions sont très adaptées à la production de masse de nombre aléatoires

Le fichier inclus SFMT/SFMT.h dans le code d'exemple liste toutes les fonctions. Quand on utilise les fonctions fill_array32() et fill_array64(), on doit être prudent. Si on utilise l'une des fonctions gen_randXX() avant les fonctions fill_arrayXX(), vous devez réinitialisez le PRNG, ou le code va planter avec une exception. C'est à cause de l'algorithme, mais ça m'a pris des heures pour voir l'indice dans les commentaires du code source.

Le code peut exploiter l'ensemble d'instructions Streaming SIMD Extensions 2 (SSE2) des CPU modernes. SIMD signifie Single Instruction, Multiple Data, et ses instructions peuvent manipuler les registres 128 bits qui permette des opérations sur les tableaux plus rapides. Les instructions SSE2 sont aussi très utiles si vous utilisez de l'arithmétique en virgule flottante, puisqu'elles gèrent les données en virgule flottante différemment que ne le fait le FPU du CPU (NDT : FPU—Floating Processor Unit). Le Mersenne Twister utilise les vecteurs, le SSE2 peut donc accélérer les calculs internes. Vous n'avez qu'à compiler le code avec -DSSE2 si votre CPU gère le SSE2. (Vous pouvez le vérifier en regardant dans /proc/cpuinfo.) Si vous utilisez du code SIMD, faites attention à aligner vos structures de données correctement. En mode SSE2, tous les pointeurs vers les zones mémoires doivent être alignés sur 16-octets pour éviter de planter. C'est pour ça que j'utilise memalign() pour allouer la mémoire.

L'algorithme Mersenne Twister peut utiliser différentes périodes pour ses nombres pseudo-aléatoires. La période est donnée par l'exposant de Mersenne (MEXP dans le code source), basé sur un nombre premier de Mersenne. Le nombre premier est représenté par 2MEXP-1. Si vous regardez dans le code C, vous allez voir une liste de nombres premier de Mersenne possibles. Ils peuvent être choisi au moment de la compilation, en changeant la valeur de >MEXP. (SFMT-params.h lit les symboles et inclus le fichier d'en-tête correspondant.)

Oui, on sort carrément l'artillerie lourde, mais, encore une fois, c'est une bonne opportunité de créer des codes d'exemples, au cas où vous auriez vraiment besoin de ces fonctionnalités dans d'autres projets.

Remplir récursivement

Le coeur du code C++ est une simple fonction qui fait, en gros, la même chose que le script shell de Ben. Les seuls paramètres sont le répertoire initial et la profondeur. La fonction s'appelle elle-même récursivement, quand elle entre dans un sous-répertoire. Elle garde trace de la profondeur dans l'arborescence, et termine quand la limite est atteinte.

namespace fs = boost::filesystem;

void populate( fs::path path, unsigned int level ) {
    unsigned int i;
    unsigned int depth_level;
    string dirname;
    string fullpath;
    unsigned int nr_directories;
    unsigned int nr_files;
    unsigned int size;

    fullpath = path.root_directory() + path.relative_path().string();
    if ( opt_debug > 0 ) {
        cout << "DEBUG: Entering <" << fullpath.c_str() << ">" << endl;
    }
    if ( chdir( fullpath.c_str() ) != 0 ) {
        cerr << "ERROR: Cannot chdir into directory <" << fullpath.c_str()
             << "> (" << strerror(errno) << ")" << endl
             << "ERROR: Level " << level << endl;
        return;
    }

    // Garde trace de la profondeur dans l'arborescence
    depth_level = level+1;

    // Crée les fichiers dans le répertoire courant
    nr_files = (gen_rand32() % opt_number_files)+1;
    for ( i=1; i<=nr_files; i++ ) {
        size = gen_rand32() % opt_max_filesize;
        if ( ! create_random_file( size, opt_max_namelength ) ) {
            cerr << "ERROR: Cannot create file (" << strerror(errno)
                 << "). Aborting." << endl;
            return;
        }
    }

    // Vérifie la profondeur, on ne crée des sous-répertoire que quand on a pas atteint la limite de profondeur.
    if ( depth_level < opt_dirlevels ) {
        // Crée un nombre aléatoire de répertoires
        nr_directories = (gen_rand32() % opt_number_directories)+1;
        for ( i=1; i<=nr_directories; i++ ) {
            // Crée le nom et le répertoires
            dirname = create_random_string(opt_max_namelength);
            if ( mkdir( dirname.c_str(), S_IRWXU|S_IRWXG|S_IROTH|S_IXOTH ) != -1 ) {
                // Peuple le répertoire
                fs::path newpath = path / dirname;
                if ( opt_debug > 0 ) {
                    cout << "DEBUG: New path <"
                         << newpath.root_directory() + newpath.relative_path().string()
                         << ">" << endl;
                }
                populate( newpath, depth_level );
                // Retourne dans le répertoire parent. C'est important parce que populate()
                // change de répertoire vers un répertoire plus en profondeur et nous ne pouvons
                // pas le refaire si nous ne faisons pas un deuxième chdir() après que la fonction
                // aie terminé.
                if ( chdir( fullpath.c_str() ) != 0 ) {
                    cerr << "ERROR: Cannot chdir into directory <"
                         << fullpath.c_str() << "> (" 
                         << strerror(errno) << ")" << endl;
                }
            }
            else {
                cerr << "ERROR: Cannot create directory (" << strerror(errno) 
                     << "). Aborting." << endl;
                return;
            }
        }
    }

    return;
}


vous pouvez trouver le code source complet avec un Makefile et le code SFMT dans une archive téléchargable. Veuillez lire le code source, car je n'ai donné ici que les idées principales du code. Le Makefile pourrait nécessiter quelques affinements, car j'ai utilisé la dernière librairie Boost (1.35 au moment de la rédaction de cet article). Je n'ai utilisé que les fonctionnalités de base, vous devriez donc vous en sortir avec les plus anciennes versions de la librairie. J'ai ajouté quelques variantes pour CFLAGS et LDFLAGS au Makefile.

Faites attention aux limites quand vous utilisez le code. Être trop généreux va faire écrire une quantité vraiment énorme de données dans le système de fichier. Prenez la limite des répertoires; EXT3 a une limite du nombre de sous-répertoire de 32768, ce n'est donc pas une très bonne idée de tester toute la plage de la limite de répertoires, à moins d'avoir quelques teraoctets de disponible.

Notes

Bien sûr, le shell script est très bien, et vous n'avez pas vraiment besoin de SSE2 pour ce genre de job. Mais la devise TMTOWTDIn'est pas réservée au perl.

Et s'il vous plaît, testez Ext4. C'est un super système de fichier, et il a besoin de retours : Sans retours et sans utilisateurs, le code ne peut pas s'améliorer. Peut-être que fspopulate peut vous aider à le tester.

Ressources utiles

René Pfeiffer est né l'année de la fondation d'Atari® et de la sortie du jeu Pong. Depuis tout petit, il a toujours cherché à démonter les objets pour savoir comment ils marchaient. A tel point qu'il ne pouvait pas passer devant un chantier sans regarder si les câbles électriques pouvaient être intéressants. Son intérêt pour l'informatique remonte à l'époque où son grand-père lui offrit un micro-ordinateur 4 bits avec 256 octets de mémoire vive et un système d'exploitation occupant 4 096 octets, l'obligeant ainsi à apprendre l'assembleur avant tout autre langage.

Après ses études secondaires, il entra à l'Université pour y étudier la physique. Il put ainsi mener des expériences avec un C64 et un C128, deux Amiga®, DEC® Ultrix, OpenVMS et finalement GNU/Linux sur un PC en 1997. Il utilise Linux depuis ce jour et aime toujours autant démonter et remonter les choses. Son amour de la liberté de pensée le rapprocha du mouvement pour le Logiciel Libre où il s'investit afin de promouvoir le droit à comprendre comment les logiciels fonctionnent. Il est également actif au sein de groupements pour la liberté civile, oeuvrant notamment dans le domaine des droits numériques.

Depuis 1999, il offre ses compétences en freelance. Ses activités principales sont l'administration système et réseaux, la programmation et le conseil. En 2001, il commença une série de conférences sur la sécurité au Technikum de Vienne. En dehors du travail sur écran, de la maintenance du matériel et de la configuration des équipements réseaux, il est passionné de plongée sous-marine, de littérature et de photographie numérique. Il espère également pouvoir de nouveau conter des histoires ou jouer à des jeux de rôles dés qu'il aura un peu plus de loisirs.

Adaptation française de la Gazette Linux

L'adaptation française de ce document a été réalisée dans le cadre du Projet de traduction de la Gazette Linux.

Vous pourrez lire d'autres articles traduits et en apprendre plus sur ce projet en visitant notre site : http://wiki.traduc.org/Gazette_Linux.

Si vous souhaitez apporter votre contribution, n'hésitez pas à nous rejoindre, nous serons heureux de vous accueillir.