Catégorie : Programmation     Tags :      0 Commentaire

    Retrouvez cet article dans : Linux Magazine 85

    Cette série d´articles à pour but de présenter la programmation système à l´aide d´un langage de script de haut niveau : Ruby. Pour suivre les exemples, il vous suffit simplement d´installer le paquetage Ruby correspondant à votre distribution (vous pouvez même l´installer sous Windows et faire tourner la plupart de ces scripts !). Nous donnons dans la section " références ", à la fin de cet article, les URL permettant d´obtenir la dernière version de Ruby et les différentes documentations (la plupart sont en anglais).

    1. Introduction

    Le but de cette série d’articles consiste à vous familiariser avec ce que l´on a coutume d´appeler la " programmation système ", c´est-à-dire la programmation d´applications faisant directement appel à l´API de votre système, sans passer par les bibliothèques intermédiaires. Ce faisant, notre but est également de vous montrer les coulisses de votre système, en nous plaçant à son niveau le plus bas, sans pour autant perturber votre compréhension par des détails techniques qui ne sont, finalement, pas liés à la programmation système.
    Un bon nombre des appels système d´Unix a été normalisé par POSIX, afin d´assurer une portabilité optimale entre les différents systèmes : un programme ne faisant appel qu´à l´API POSIX fonctionnera donc sur tout système POSIX, de Linux à FreeBSD en passant par Mac OS X. Une autre méthode, pour assurer cette portabilité, consiste à utiliser les bibliothèques standards des différents langages : un programme C n´utilisant que la bibliothèque standard de C, par exemple, devrait être portable sur tous les systèmes. Il en va de même pour Ada, C++, etc.
    Le problème avec les langages sus-mentionnés est que ce sont des langages nécessitant une compilation sur la plate-forme d´arrivée : ils sont portables au niveau source, pas au niveau binaire. Et encore... les différences de représentation des entiers, par exemple, exigent que le développeur ait pris en compte ces différences, ce qui n´est malheureusement pas toujours le cas et ce qui alourdit d´autant le code. Par ailleurs, ces bibliothèques standards sont souvent incomplètes, voire réduites à leur plus simple expression, ce qui oblige le programmeur à utiliser des bibliothèques d´extension et donc à quitter la voie de la portabilité (certaines extensions peuvent ne pas être disponibles sur toutes les plateformes). En outre, elles constituent une couche d´abstraction supplémentaire qui n´est pas toujours nécessaire. Enfin, étant " standard ", elles ne peuvent pas proposer des fonctionnalités spécifiques à certains systèmes (création de sous-processus, par exemple).
    On notera que la voie qu´a choisi Sun avec Java a pour but de pallier ces deux problèmes en proposant, d´une part, une portabilité au niveau binaire (un programme Java compilé sur une plate-forme s´exécutera tel quel sur toute autre plate-forme disposant d´une machine virtuelle Java) et une bibliothèque standard très riche censée couvrir tous les besoins. Mais, pour assurer cette portabilité binaire, Java est obligé de faire abstraction du système sous-jacent et de tout réduire à un dénominateur commun... Or, en termes de programmation système, il y a peu de points communs entre Windows et Unix : là encore, les fonctionnalités spécifiques aux systèmes sont absentes.
    L´autre approche, celle que nous avons choisie, consiste à utiliser un langage de haut niveau, comme Perl, Python ou Ruby. Ces trois langages sont disponibles sur toutes les plateformes (au contraire de Java, leur licence est libre et ils ne dépendent pas d’une société commerciale) et disposent d´une bibliothèque standard impressionnante, couvrant la plupart des besoins (et, si cela ne suffit pas, le nombre de modules d´extension est tout aussi impressionnant).
    Le fait que le code produit par ces langages soit plus lent que le code équivalent en C n´est pas un véritable problème, car il ne s´agit pas ici de faire du calcul scientifique gros consommateur de puissance CPU, mais d´accéder aux fonctionnalités du système sous-jacent, notamment au système de fichiers : les performances pures des scripts écrits dans ces langages sont donc généralement bien suffisantes (si elles ne l´étaient pas, il reste toujours possible d´écrire des modules en C et de les appeler à partir des scripts). L´avantage, par contre, est un développement bien plus rapide et sûr, produisant un code plus lisible et bien plus facile à maintenir : finis les problèmes d´allocation mémoire et les fuites qui vont avec. Il ne reste plus qu´à se concentrer sur l´essentiel, l´apprentissage des différents appels système POSIX.
    Bien entendu, le problème de la portabilité se pose avec ces langages comme avec les autres : un script Ruby créant des processus fils ne fonctionnera pas sous Windows, mais la démarche choisie par les concepteurs de ces langages est fondamentalement différente de celle choisie par Java : ici, on n’interdit pas en ne proposant pas la fonctionnalité, on propose la fonctionnalité en précisant qu’elle ne pourra fonctionner que sur telle ou telle plate-forme. On considère que les développeurs sont des adultes consentants.
    Qu’il soit bien clair que nous ne soutenons pas que les langages de scripts soient la solution ultime : des langages comme C seront toujours nécessaires pour écrire les parties critiques d’un système, mais, honnêtement, quel est l’intérêt d’utiliser C pour écrire un script d’administration qui serait développé bien plus simplement en Perl, en Python ou en Ruby ? Quel est l’intérêt de peiner sur les malloc() ou les free() pour comprendre ce que fait l’appel système read() ? Ne vaut-il pas mieux d’abord comprendre ce que fait cet appel, le faire fonctionner et, ensuite, si besoin est, utiliser un autre langage comme C si l’on a besoin de coder dans ce langage ?
    D’ailleurs, ces articles présenteront tour à tour les différents appels système, avec la syntaxe utilisée dans les pages de man, qui restent les documents de référence et auxquels nous vous renvoyons pour tous les détails que nous ne pouvons évidemment pas présenter ici. Cette syntaxe est une syntaxe C : tous ces appels sont des fonctions C prévues pour être appelées à partir de programmes C et il est bon de la connaître. Cependant, grâce à l’abstraction que nous procure Ruby, les détails " embêtants " de leurs appels sont masqués : nous pouvons alors nous concentrer sur leur fonctionnement.
    Pour illustrer notre propos, examinons la syntaxe de l’appel signal(), tel qu’il nous est fourni par la page de manuel signal(3)[1]. Cet appel sera étudié plus tard plus en détail, mais il illustre bien ce que nous voulons dire :

    % man signal
    (...)
    SYNOPSIS
         #include <signal.h>
          void (*signal(int sig, void (*func)(int)))(int);
          or in the equivalent but easier to read typedef’d version:
          typedef void (*sig_t) (int);
          sig_t signal(int sig, sig_t func);

    Joli, non ? Il suffit pourtant de savoir que cet appel attend en paramètre un entier (le numéro du signal) et une fonction renvoyant void et prenant un int en paramètre (le reste de la page man se charge de l’expliquer). L’appel lui-même renvoie un pointeur vers une fonction renvoyant un void et prenant un int en paramètre. Tout le côté ésotérique de cette syntaxe provient du fait que pour passer une fonction en paramètre ou pour renvoyer une fonction en C, on doit passer par des pointeurs de fonction...
    Notre postulat est qu’il n’est pas nécessaire de comprendre les pointeurs de fonctions pour savoir comment gérer les signaux. L’important est de savoir comment tout cela fonctionne. Le jour où vous devrez coder ça en C, nous vous faisons confiance, vous saurez utiliser la syntaxe de C...
    C’est bon ? Vous avez compris la démarche ? Alors on y va...
    De quoi avons-nous besoin pour commencer à travailler ?

    • De la commande man d’Unix (exemple : man 2 umask). Quand on fera référence à un appel système, on le notera umask(2), le 2 faisant référence à la section du manuel.
    • De la commande ri de Ruby (exemple : ri File.umask).
    • De l’interpréteur interactif de Ruby : irb (on en sort par la commande quit).
    • De l’interpréteur non interactif de Ruby : ruby auquel on indiquera le nom du fichier contenant le script à exécuter (par convention, un script Ruby porte l’extension .rb.
    • De votre éditeur de texte préféré (il existe des modes Ruby pour Vim et Emacs).
    • D’une connaissance minimale du langage Ruby : la section référence de cet article vous donnera toutes les bonnes adresses...

    Commençons d’abord par quelques rappels pour nous mettre en jambe...

    2. Paramètres d’un programme principal

    Le lancement d’un programme C provoque l’appel de sa fonction principale main(), qui peut avoir l’un des deux prototypes suivants :

     int main(void);          /* int main() en C++ */
    int main(int argc, char *argv[]);

    Note :
    Le prototype void main(...) ne fait pas partie des normes C ou C++ (contrairement à ce que prétendent certains ouvrages). Cette syntaxe n’est reconnue que par les compilateurs C/C++ sous Windows. Ne l’utilisez pas !

    Dans les deux cas, main() doit renvoyer (par return ou exit(3)[2]) la valeur 0 ou EXIT_SUCCESS si elle s’est bien passée, une valeur différente sinon (il existe EXIT_FAILURE).
    Si main() a des paramètres, le premier (traditionnellement appelé argc) contiendra le nombre de paramètres de la ligne de commande (le nom du programme en fait partie).
    Le second paramètre (traditionnellement appelé argv) est un tableau de chaînes contenant tous les paramètres passés à la ligne de commande du shell (argv[0] et donc le chemin complet du programme).

    [1] Nous utilisons ici un système FreeBSD... Sur Linux, le contenu peut varier, mais il ne sera pas bien différent...
    [2] On rappelle que cette notation signifie que exit() est documenté dans la section 3 du manuel, pas qu’il faut lui passer 3 en paramètre !

    2.1 En Ruby...

    Avec Ruby, la situation est un peu différente : les paramètres passés au script sont placés dans un tableau global nommé ARGV, le nom du script lui-même est stocké dans la  variable globale $0 (en shell, $0 désigne le shell courant). On notera qu’en Ruby la casse est importante : tout ce qui commence par une majuscule est une constante ou un nom de classe. Puisque nous y sommes, disons également que tout ce qui commence par un $ est une variable globale. Toutes les autres variables, qu’elles soient scalaires ou composées, commencent par une minuscule (il n’y a pas de différenciation entre scalaires, tableaux et hachage comme en Perl).
    Par conséquent, ARGV[0] est le premier paramètre passé au script. ARGV étant un tableau, donc une instance de la classe Array (voir ri Array), nous pouvons utiliser les méthodes de cette classe et notamment sa méthode length pour connaître le nombre de paramètres qui ont été passés au script :

    nb_params = ARGV.length
    puts(“Je suis #{$0}, vous m’avez passé #{nb_params} paramètre(s)")
    if nb_params > 0
       puts(“Les voici : “)
       ARGV.each { |param| puts(param) }
    end

    Si vous sauvegardez ces 3 lignes dans un fichier nommé params.rb, vous pouvez tester tout cela :

     % ruby params.rb
    Je suis params.rb, vous m’avez passé 0 paramètre(s)
    % ruby params.rb titi toto tata
    Je suis params.rb, vous m’avez passé 3 paramètre(s)
    Les voici :
    titi
    toto
    tata

    Les paramètres transmis par le shell sont des chaînes (des char * en C, des String en Ruby), ce qui veut dire qu’il faudra peut-être les convertir en entiers, par exemple, ou en flottants.
    En C, on doit utiliser les appels atoi(3) et atof(3). En Ruby, la classe String dispose des méthodes to_i et to_f (voir ri String). Soit le script suivant :

     nbre = ARGV[0].to_i
    puts(“Résultat : #{nbre * 2}")

    Voici un exemple d’utilisation :

    % ruby params.rb 42
    Résultat : 84

    Question subsidiaire : si l’on n’avait pas converti le paramètre en entier, le résultat aurait été 4242. Pourquoi ? (indice : ri "String.*" – les guillemets sont nécessaires ici pour éviter que le shell n’interprète le * comme un caractère spécial.)

    Un script Ruby qui se termine normalement fait toujours un exit(0) implicite. Pour éviter l’exécution des finalisateurs et autres fonctions de rappel à la fin des processus fils, on utilisera la forme exit!(n) (c’est inutile pour le processus père).

    3. Analyse des options de la ligne de commande

    La gestion des paramètres de la ligne de commande que nous venons de voir convient aux cas simples, mais elle est insuffisante pour gérer les appels complexes pouvant contenir des options au format court (-c) ou au format long (--option). Pour vous donner une idée du problème, on rappelle que, par exemple, la commande ls -a -l est équivalente aux commandes ls -al, ls -la et aux commandes ls --all --long, ls -a --long, etc. sur les systèmes reconnaissant les options longues...
    En C, la fonction getopt(3) permet de traiter les options courtes (de la forme -c) passées à un programme. Cet appel utilise ensuite des variables globales (optarg, optind, etc.). La page de manuel contient un exemple d’utilisation permettant de comprendre assez rapidement son fonctionnement.
    La bibliothèque GNU dispose également d’un appel permettant de gérer les options longues (de la forme --mot), mais elle n’est pas présente partout.

    3.1 En Ruby

    En Ruby, on dispose de la classe GetoptLong du module getoptlong, qui permet de gérer à la fois les options courtes et les options longues (de la forme --mot). Le principe de fonctionnement est relativement simple et un exemple vaut mieux qu’un long discours :

    require ‘getoptlong’
    require ‘ostruct’ 
    
    def print_usage
        STDERR.puts <<-FIN
           Usage:
           #{$0} [--bla | -b ] [--nbre=val | -n val | -c val]
                 [--longueur[=val] | -l [val] ] fichier1 [fichier2]
        FIN
        exit!(1)
    end
    
    # Création d’une instance pour analyser les options
    parser = GetoptLong.new(
           ["--bla",      "-b",       GetoptLong::NO_ARGUMENT],
           ["--nbre",     "-n", "-c", GetoptLong::REQUIRED_ARGUMENT],
           [“--longueur”, “-l”,       GetoptLong::OPTIONAL_ARGUMENT])
    
    # On crée une structure d’options pour le programme et on initialise
    # ses champs avec les valeurs par défaut
    options = OpenStruct.new
    options.compteur = 0
    options.longueur = 0
    options.bla = false
    # Analyse de chaque option
    parser.quiet = true  # on veut gérer nous-mêmes les messages d’erreur
    begin
       parser.each do |opt, arg|
           case opt
                when "--bla"
                   options.bla = true
                when "--nbre"
                   options.cpteur = arg
                when “--longueur”
                   options.longueur = (arg ? arg : 10)
           end
       end
       rescue Exception
          print_usage
    end
    
    # Analyse de ARGV
    ...

    On crée une instance de la classe GetoptLong en lui indiquant les options que l’on accepte, avec leurs caractéristiques. Puis, on parcourt les options capturées par cette instance sur la ligne de commande et on positionne les champs d’une structure en fonction de ces options (on aurait pu utiliser ici 3 variables séparées, mais leur regroupement en structure est plus propre). Si une option non autorisée est rencontrée, le parcours provoquera une exception que nous capturons afin d’afficher un écran d’aide et sortir du programme en signalant l’erreur par un code de retour différent de 0. Si tout s’est bien passé, il reste dans ARGV les paramètres passés au programme.
    Il existe une classe bien plus évoluée, OptionParser, mais elle n’est malheureusement pas documentée dans ri. Il faut lire le fichier optparse.rb qui contient un exemple complet.
    Voici l’équivalent du code précédent :

     require ‘optparse’
    require ‘ostruct’
    
    # On crée une structure d’options pour le programme et on initialise
    # ses champs avec les valeurs par défaut
    options = OpenStruct.new
    options.compteur = 0
    options.longueur = 0
    options.bla = false
    
    # Analyse des paramètres
    ARGV.options do |opts|
       begin
          opts.banner << " params..."
          opts.on_tail(“-h”, “--help”, “Affiche cet écran d’aide") do
             puts opts
             exit 0
          end
          opts.on("-b" , "--bla", "Option super-utile") do
             options.bla = true
          end
          opts.on(“-n”, “-c”, “--nbre=VAL”, Integer,
                  “Fixe la valeur du compteur”) do |val|
              options.compteur = val
          end
          opts.on(“-l” , “--longueur=[VAL]”, Integer,
                 “Fixe la longueur du truc (10 par défaut)") do |v|
              options.longueur = (v ? v : 10)
          end
          opts.parse!
       rescue RuntimeError => err
            STDERR.puts(err)
            exit 1
      end
    end
    
    # On teste...
    puts(“longueur = #{options.longueur}”)
    puts(“compteur = #{options.compteur}”)
    puts(“il reste les paramètres #{ARGV}")

    Par rapport à Getoptlong, on voit qu’ici il est possible de spécifier le type des valeurs attendues. On peut également indiquer le message qui s’affichera lorsque l’écran d’aide sera produit. Faire la même chose en C demanderait certainement plus d’efforts !

    4. Gestion des erreurs

    Quasiment tous les appels aux fonctions des bibliothèques Unix et C fournissent un moyen de savoir si l’appel s’est bien passé. Si l’appel s’est mal passé, le programmeur peut avoir envie de savoir pourquoi... Par conséquent, toutes les fonctions système (ou presque) renvoient un code d’erreur et initialisent une variable globale avec un code représentant la raison de cette erreur.
    Pour cela, il faut inclure le fichier errno.h (l’ancienne méthode consistant à déclarer manuellement la variable errno doit être évitée). Le fichier errno.h définit correctement la variable errno pour votre système, ainsi que toutes les constantes qui lui sont associées, qui symbolisent la raison de l’erreur (voir errno(2)).

    Note :

    errno n’est positionnée qu’en cas d’erreur dans un appel de fonction. Elle n’est jamais remise à zéro : par conséquent, pour savoir si la valeur de errno est significative, il faut d’abord tester la valeur de retour de la fonction. Si la fonction renvoie un code d’erreur, alors errno contient la raison de cette erreur (sinon, elle contient la raison d’une erreur précédente).

    La non-vérification des valeurs de retour des appels système est une des sources de bug les plus fréquentes... Comme c’est une opération fastidieuse (le code de vérification est souvent plus long que le code " utile "), certains croient bon de ne pas le faire et produisent ainsi des programmes fragiles.

    4.1 Manipulation de errno

    errno est un entier et, pour produire des messages utiles, la bibliothèque standard de C fournit plusieurs opérations pour afficher le texte de l’erreur correspondant à la valeur de errno :

    • La fonction void perror(const char *message) affiche message sur stderr, suivi d’un :, suivi du texte correspondant à la valeur de errno.
    • La fonction char *strerror(int num) renvoie le message correspondant à la valeur d’erreur num (donc, strerror(errno) produit le message correspondant à la valeur courante de errno...).

    L’utilisation de strerror() donne plus de souplesse que perror(), car on peut formater le message selon ses goûts.
    On rappelle que les messages d’erreurs d’un programme doivent être envoyés sur stderr et non sur stdout. On utilisera donc plutôt des appels à fprintf() :

    #include <stdio.h>      /* pour fprintf() */
    #include <string.h>     /* Pour strerror() */
    #include <errno.h>      /* Pour les constantes de errno */
          ...
    fprintf(stderr, “L’erreur ‘%s’ s’est produite\n”, strerror(errno));

     4.2 Erreurs en Ruby

    Ruby dispose du mécanisme des exceptions grâce à la construction begin.... rescue... ensure...end (on l’a déjà utilisée dans les exemples précédents).
    S’il y a une clause ensure, elle sera toujours exécutée, quelle que soit la façon dont on sort du bloc (c’est là qu’on met le " code de nettoyage ", comme la fermeture d’un fichier, par exemple).
    La classe de base des exceptions est Exception (c’est la plus générale). Une de ses classes filles s’appelle SystemCallError qui a elle-même pour classe fille Errno (voir ri Errno). Sous Errno se trouvent, par exemple, les classes Errno::ENOENT et Errno::EWOULDBLOCK. Il existe également RuntimeError, qu’on lève par exemple lorsque le programme n’a pas été appelé avec les bons paramètres.
    La méthode constants de Errno produit un tableau contenant toutes les constantes errno reconnues par le système et Errno::EIntr::XXX renvoie la valeur entière de l’erreur XXX :

    % irb --simple-prompt
    >> Errno.constants
    => ["EHOSTDOWN", "EINTR", "ENFILE", "ENOMSG", ....
        „ENOTEMPTY“, „ENOTDIR“, „EMSGSIZE“, „ETOOMANYREFS“, ....
         „ENOSTR“, „ENOTTY“, „EALREADY“, „ENOTSOCK“, „ENOLCK“ ...]
    
    >> Errno::EIntr::Errno
    => 4

    En Ruby, il n‘y a pas de méthode strerror : le message associé à une exception Errno est produit en appelant sa méthode to_s (ce que fait implicitement puts).
    On peut connaître la classe précise de l’exception qui a été levée en appelant la méthode class de l’exception. Exemple :

    begin
      fd = File.open("inexistant.txt", File::RDONLY)
      # si on est là, c’est que c’est ok
      # on travaille sur le fichier
    rescue SystemCallError => e
      STDERR.puts("#{e.class} : #{e}")
    ensure
      fd.close if fd and not fd.closed?
    end

    En consultant les pages de man des appels système sous-jacents (open(2), ici), on peut connaître les différentes exceptions Errno::xxx susceptibles d’être levées. Mais, le plus souvent, il suffit de se reposer sur le mécanisme des exceptions classique, sauf quand l’on souhaite traiter une erreur bien spécifique.

    5. Entrées/Sorties sur disque

    L’utilisation d’un fichier consiste en :

    • son ouverture dans un certain mode ;
    • des accès ;
    • sa fermeture.

    Note :

    La fermeture d’un fichier, notamment lorsqu’il y a eu des écritures depuis l’ouverture est une opération essentielle : son oubli peut provoquer la corruption du fichier. Ne jamais supposer que le programme le fera pour vous !

    Unix gère les types de fihiers suivants (les constantes sont définies dans sys/stat.h, voir stat(2)) :

    • les fichiers ordinaires (S_IFREG) ;
    • les répertoires (S_IFDIR) ;
    • les périphériques en mode caractère (S_IFCHR) ;
    • les périphériques en mode bloc (S_IFBLK) ;
    • les tubes nommés (S_IFIFO) ;
    • les sockets (S_IFSOCK) ;
    • les liens symboliques (S_IFLNK).

    Tout fichier est décrit au niveau du système par un descripteur de fichier (un entier positif ou nul). Un processus Unix peut avoir 20 fichiers ouverts simultanément (en fait, au moins 20 : la fonction getdtablesize(2) renvoie le nombre correspondant à votre système).
    Pour chaque processus, 3 fichiers sont automatiquement ouverts : STDIN, STDOUT et STDERR qui correspondent, respectivement, aux descripteurs 0, 1 et 2.
    Dans la mesure du possible, on remplacera un appel à getdtablesize(2), qui n’est pas POSIX, par un appel à sysconf(3) qui est défini par POSIX et qui est plus général, puisqu’il permet d’interroger de nombreux paramètres du système (celui qui nous intéresse ici est : _SC_OPEN_MAX) :

    #include <stdio.h<
    #include <unistd.h<
    
    int main(void) {
       printf(“Nombre max de descripteurs/process : %ld\n”,
            sysconf(_SC_OPEN_MAX));
       printf(“Idem, mais avec getdtablesize() : %d\n”,
            getdtablesize());
       return 0;
    }

    Sur ma machine, le résultat des deux appels est 256, ce qui signifie que chaque programme peut ouvrir 253 fichiers (les 3 autres étant les E/S standards).

    5.1 En Ruby

    La classe File encapsule toutes les fonctionnalités de bas niveau et de haut niveau des fichiers en Ruby.
    Elle hérite de la classe IO qui, elle, encapsule toutes les opérations liées aux entrées/sorties quelles qu’elles soient : c’est donc la classe mère des fichiers, mais également des sockets. C’est elle qui permet de créer des pipes, etc.
    Ruby définit les trois constantes globales STDIN, STDOUT et STDERR, ainsi que les trois variables globales $sdtin, $stdout et $stderr, valant initialement les constantes correspondantes. Tous ces objets sont des instances de la classe IO.
    La page de manuel sysconf(3) préconise l’emploi de la commande shell getconf(1) (également POSIX) pour obtenir le même résultat à partir d’un script. C’est ce que nous ferons en Ruby, puisque sa bibliothèque ne dispose pas de l’appel sysconf. Pour lancer une commande shell en Ruby et récupérer sous forme de chaîne ce que cette commande affiche, il suffit d’utiliser la construction %x(commande) – ceux qui connaissent la programmation en shell reconnaîtront le fonctionnement de `commande` ou $(commande). On notera également l’utilisation de to_i pour convertir la chaîne obtenue par %x(...) en entier :

    >> %x(getconf OPEN_MAX).to_i
    256

    5.2 Droits d’un fichier et masque de création

    Lors de sa création, un fichier possède les droits d’accès qui sont spécifiés par un masque de création de 9 bits. Les bits à 1 interdisent l’accès.
    Le masque 027 (000010111), par exemple, correspondra aux droits rwxr-x---. Attention à ne pas confondre avec ce qui est donné à la commande chmod (c’est exactement l’inverse, puisque c’est un masque...).
    En C, la fonction umask(2) permet de fixer la valeur du masque. De plus, elle renvoie la valeur du masque précédent, ce qui permet de le restaurer.
    Si une application crée un fichier avec les droits 0666 et que le masque est de 0077, les permissions réelles de ce fichier seront donc :

    0666 & (~ 0077) = 0666 & 0700 = 0600

    5.2.1 En Ruby

    Le masque de création est fourni par la méthode de classe File.umask qui peut être appelée avec ou sans paramètre (comme la commande umask(1) du shell) :

    File.umask       => valeur de l’umask courant
    File.umask(0666) => fixe la valeur de l’umask et renvoie sa valeur précédente

    5.3 Création, ouverture, fermeture, suppression

    L’ouverture/création d’un fichier se fait par l’appel système open(2) :

    #include <fcntl.h>
    int open(const char *chemin, int mode [, mode_t droits])

    Ouvre un fichier dans le mode et avec les droits indiqués (les droits ne sont nécessaires que lorsque l’on crée un fichier avec le mode O_CREAT).
    Cette fonction renvoie une valeur positive ou nulle en cas de succès (le descripteur du fichier ouvert) ou -1 en cas d’erreur (avec positionnement de errno).
    Le mode est un OU logique de constantes appartenant à 2 catégories :

    • les types d’accès (O_RDONLY, O_WRONLY, O_RDWR) ;
    • les contraintes de création O_APPEND, O_CREAT, O_EXCL, O_TRUNC, O_SHLOCK, O_EXLOCK, O_NONBLOCK).

    Les droits s’expriment en octal ou par un OU logique de constantes définies dans sys/stat.h (méthode préférable, mais plus laborieuse). Ainsi, les droits S_IRWXU | S_RGRP sont équivalents à la valeur octale 740 et correspondent à rwxr-----. Ces constantes sont documentées dans stat(2).
    Lorsqu’on ouvre un fichier qui est censé exister, que O_CREAT n’a pas été utilisé et que le fichier n’existe pas, l’appel échoue et errno vaut ENOENT. On peut utiliser cette caractéristique pour tester l’existence d’un fichier. Voir open(2) pour plus d’informations.

    #include <fcntl.h>
    #include <errno.h>
    #include <string.h>
    #include <stdio.h>
    
    int main(void) {
      int fd;
    
      if ((fd = open("truc", O_RDONLY)) == -1) { /* Pb d’ouverture */
           if (errno == ENOENT) {  /* Le fichier n’existe pas...   */
               fprintf(stderr, “fichier inexistant : %s\n”, strerror(errno));
           } else {              /* Autre erreur ... */
                fprintf(stderr, “Pb d’accès : %s\n", strerror(errno));
           }
      }
    
      /* Le fichier est ouvert : on travaille avec puis on le ferme */
    
      return 0;
    }

    Note :

    Quand un fichier est créé, son groupe est celui du répertoire dans lequel il est créé (peut varier selon les Unix).

    La fonction close(2) ferme le fichier et renvoie 0 en cas de succès, -1 en cas d’erreur (avec positionnement de errno).
    La fonction unlink(2) supprime le lien physique vers le nom qui lui est passé en paramètre. Le nombre de liens associés à ce fichier est alors décrémenté : s’il atteint 0, le fichier est supprimé du disque s’il n’est pas en cours d’utilisation par un autre processus (sinon, il le sera dès qu’il n’est plus utilisé).
    Cette fonction ne peut agir que sur un nom de fichier. Pour les répertoires, on utilisera rmdir(2).

    5.3.1 En Ruby

    Pour ouvrir ou créer un objet fichier, on utilise la méthode de classe File.new ou File.open, qui prend les mêmes paramètres que open(2) :

    fic = File.new(nomfic, mode = "r" [, droits])

    mode peut valoir "r" (lecture seule), "w" (écriture seule/troncature/création), "a" (écriture en fin de fichier/création), "r+" (lecture/écriture en début de fichier), "w+" (lecture/écriture en début de fichier/troncature/création), "a+" (lecture/écriture à la fin du fichier). On a donc typiquement ici une syntaxe à la fopen(3).
    Avec Windows uniquement, on dispose en plus du mode "b" (qui se place avant les r, w, a) pour indiquer une ouverture en mode binaire.
    Ce mode peut également être donné comme avec open(2), par des OU logiques de constantes (sans le préfixe O_). Par exemple :

    fic = File.new(nomfic, File::CREAT | File::RDWR | File::TRUNC, 0666)

    Note :

     Pour rester cohérent avec la syntaxe des appels système, on n’utilisera que cette dernière forme.

    Comme pour open(2), on peut indiquer les droits du fichier lors de sa création en donnant une valeur octale. Si cette appel échoue, une exception SystemCallError est levée (Errno::ENOENT, par exemple).
    La classe File disposant d’une méthode de classe exists, le code C précédent qui testait l’existence d’un fichier pourrait être écrit de la façon suivante :

    if File.exists(nomfic) then
       fic = File.open(nomfic, File::RDONLY)
       ... travail avec fic puis fermeture
    else
       ... traiter problème

    Mais on peut également faire confiance au mécanisme des exceptions :

    begin   # bloc dans lequel une exception peut survenir
      fic = File.new(nomfic, File::RDONLY)
      ... travail avec nomfic
    rescue Errno::ENOENT
       STDERR.puts(Fichier inexistant)
       exit!(1)
    rescue Exception => e   # toutes les autres
       STDERR.puts(e)
       exit!(2)
    ensure
       fic.close if fic and not fic.closed?
    end

    Comme on vient de le voir, un objet fichier est fermé par l’appel à sa méthode close.
    Si l’on utilise open au lieu de new et qu’on lui associe un bloc, le fichier sera automatiquement fermé à la fin du bloc :

     begin
       File.open(nomfic, File::RDONLY) do |fic|
          travail avec fic
       end # fermeture automatique de fic
    rescue Errno::ENOENT
       STDERR.puts(Fichier inexistant)
       exit!(1)
    rescue Exception => e   # toutes les autres
       STDERR.puts(e)
       exit!(2)
    end

    Évidemment, si une exception survient dans le bloc associé à open, celui-ci fermera le fichier. Une sécurité de plus...
    La méthode File.unlink("truc") fonctionne comme unlink(2) et lève Errno::ENOENT si le fichier n’existe pas, par exemple.

    5.4 Lecture/écriture dans un fichier

    Les fonctions :

    #include <sys/types.h>
    #include <unistd.h>
    ssize_t read(int fdesc, void *adr, unsigned taille)
    ssize_t write(int fdesc, void *adr, unsigned taille)

    permettent de lire et écrire taille octets dans le fichier décrit par fdesc. adr est une zone mémoire, qui doit avoir été créée (soit par une déclaration de tableau, soit par une allocation de pointeur), dans laquelle ou à partir de laquelle ces octets seront écrits ou lus. Ces fonctions renvoient le nombre d’octets effectivement lus ou écrits.
    read(2) renvoie 0 lorsqu’il a atteint la fin de fichier, -1 en cas d’erreur (avec positionnement de errno). Normalement, read doit renvoyer le même nombre que celui qu’on lui a demandé de lire, sauf s’il en reste moins dans le fichier.
    write(2) renvoie -1 en cas d’erreur (avec positionnement de errno).
    Généralement, l’utilisation de read est de la forme :

    int Nb_Lus;
    T_Element tampon[10];
    /* Lecture élément par élément */
    while (Nb_Lus = read(fdesc, tampon, sizeof(T_Element))) {
        /* Nb_Lus contient le nombre d’éléments lus :
           Soit 1, soit 0 : si c’est 0 => fin de fichier, on sort
           sinon, on traite */
    }
    /* fin de fichier atteinte */

    ou

    /* Lecture de 10 éléments par 10 éléments */
    while (Nb_Lus = read(fdesc, tampon, 10 * sizeof(T_Element))) {
        /* Nb_Lus contient le nombre d’éléments lus :
           Soit 10, soit moins : si c’est 0 => fin de fichier, on sort
           sinon, on traite */
    }
    /* fin de fichier atteinte */

    Une solution couramment employée consiste, dans certains cas, à manipuler le fichier déjà ouvert via les fonctions de la bibliothèque standard du C (fprintf, fscanf, fgets, etc.). Pour cela, il faut donc associer un FILE * au descripteur de fichier obtenu par open (voir fdopen(2)) :

     /* fd est un descripteur de fichier obtenu par open */
    FILE *fdesc = fdopen(fd, "r");  /* si fd a été ouvert en lecture... */
    /* on peut utiliser fscanf, fgets, fread, etc. */

    Cette manipulation n’est évidemment pas nécessaire en Ruby, puisque la classe IO s’occupe de tout...

    5.4.1 En Ruby

    On utilise les méthodes de la classe IO (donc valable aussi pour les pipes, les sockets, etc.)
    Les deux méthodes qui encapsulent les appels système read(2) et write(2) sont, respectivement, fd.sysread(n) et fd.sywrite(chaine). sysread tente de lire n octets et lève SystemcallError ou EOFError :

     begin
    fd = File.open(ARGV[0], File::RDONLY)
      # Le fichier est ouvert, on le lit 10 par 10 caractères
      while tampon = fd.sysread(10)
         STDOUT.syswrite(tampon)
      end
    rescue EOFError  # on ne fait rien => on passe au ensure
    rescue SystemcallError => e
      STDERR.puts(e)
      exit(1)
    ensure
      fd.close
    end

    ou, en utilisant un bloc avec open :

    File.open(ARGV[0], File::RDONLY) do |fd|
      begin
         while tampon = fd.sysread(10)
           STDOUT.syswrite(tampon)
         end
      rescue EOFError# on ne fait rien => quitte le bloc et ferme le fichier
      end
    end

    On peut aussi utiliser les méthodes suivantes, qui sont bien plus élaborées (sans passer par une conversion, mais il ne faut surtout pas les utiliser en même temps que les précédentes sur le même objet).

    5.4.1.1 Lecture

    Toutes ces méthodes renvoient nil s’il y a eu tentative de lecture après la fin du fichier :

    str = fd.read(taille)  => lit au plus taille octets dans fd
    str = fd.read          => lit tout fd
    str = fd.gets          => lit la ligne suivante de fd
    car = fd.getc          => lit le caractère suivant
    tab = fd.readlines     => lit tout et le met dans un tableau de chaines

    Certaines méthodes, dans la même situation, lèvent une exception :

    str = fd.readline      => comme fd.gets, mais lève une exception EOFError
    car = fd.readchar      => lit un seul caractère, lève une exception EOFError

    Dans tous les autres cas, toutes ces méthodes lèveront une exception (mode incompatible, droits insuffisants, etc.).

    5.4.1.2 Écriture

    On dispose des méthodes suivantes :

    fd.write(chaine)     => écrit chaine dans fd, renvoie le nbre d’octets écrits
    fd << chaine         => écrit chaine dans fd
    fd.print(objet)
    fd.puts(objet)       => appel de la méthode objet.to_s
    fd.printf(format, objet)
    fd.putc(car)         => écrit un seul caractère

    Note:

    Dans le cadre de cette présentation, nous nous en tiendrons volontairement à sysread et syswrite (ce qui ne veut certainement pas dire que vous ne devez pas utiliser les autres méthodes dans d’autres occasions !).

    5.4.1.3 Itération sur un fichier

    Les objets IO sont " énumérables " : ils disposent donc d’itérateurs...
    Au niveau de la classe, la méthode foreach ouvre un fichier en lecture et renvoie successivement chacune de ses lignes, puis le ferme :

    IO.foreach(ARGV[0]) do |ligne|
       print ligne
    end

    Au niveau des instances, la méthode each (ou each_line) s’applique à un objet fichier ouvert en lecture et renvoie toutes ses lignes :

    File.open(ARGV[0]) do |fic|
       fd.each_line do |ligne|        # ou each, qui est synonyme...
          puts ligne
       end
    end

    Mais, une méthode encore plus simple (si le fichier ne fait pas plusieurs mégaoctets) consiste à lire tout le fichier d’un seul coup avec la méthode de classe IO.read qui stocke tout le fichier dans une seule grosse chaîne :

     puts(IO.read(ARGV[0]))

    Note :

    Un énumérable dispose aussi des méthodes find, collect, etc. (voir ri IO). 

    5.5 Gestion de la position courante

    Note :

    Maintenant, vous avez compris le principe : on ne donnera plus que les références des appels de l’API système et on décrira leur fonctionnement via leurs équivalents Ruby.

    L’appel lseek(2) permet de se positionner dans le fichier relativement à un point de référence (SEEK_SET, SEEK_END et SEEK_CUR).
    Avec Ruby, on utilisera fd.seek sur un fichier ouvert, avec les constantes IO::SEEK_SET, IO::SEEK_CUR et IO::SEEK_END. L’appel fd.pos = n est un synonyme de fd.seek(n, IO::SEEK_SET).
    Ces fonctions/méthodes permettent de se déplacer à une position d’octet par rapport à un point de référence. La position du 1er octet est 0. La valeur du déplacement peut être négative avec IO::SEEK_CUR et IO::SEEK_END. Si l’on arrive avant le début du fichier, l’exception correspondant à EINVAL (invalid argument) est levée.
    Par contre, on peut se déplacer plus loin que la fin d’un fichier (pour créer un fichier creux pour garantir qu’il y aura assez de place sur le disque, par exemple) :

     begin
       fic = File.open("fichier_creux", File::WRONLY | File::CREAT)
    rescue Exception => e
       STDERR.puts(e)
       exit!(1)
    end
    begin
       fic.seek(10000, IO::SEEK_END)
       fic.syswrite(“bla\n”)
       puts(“le fichier fait #{File.size(‘fichier_creux’)} octets”)
       retour = 0
    rescue Exception => e
       STDERR.puts(e)
       retour = 2
    ensure
       fic.close
       File.unlink(„fichier_creux“)
       exit(retour)
    end

    affiche : le fichier fait 10004 octets

    5.6 Duplication de descripteurs

    Voir dup(2) et dup2(2). La technique pour, par exemple, rediriger STDOUT vers le fichier toto consiste à :

    • Ouvrir toto en écriture (le créer selon le cas). Soit fd son descripteur.
    • Fermer le descripteur 1 (celui de STDOUT).
    • Dupliquer le descripteur de toto avec dup(fd). Comme celui-ci prend la première entrée libre dans la table des descripteurs, il prendra l’entrée 1...
    • Fermer toto pour libérer son descripteur.

    Note :

    dup2(fd, 1) fait tout cela en une seule instruction...

    En Ruby, c’est un peu différent, car l’appel à dup(2) est encapsulé dans un méthode portant un nom différent (dup existe en Ruby, mais ne fait pas du tout la même chose).
    Cela dit, le principe reste le même : pour rediriger la sortie standard vers le fichier toto, il faut :

    • ouvrir le fichier toto en écriture (ou le créer) ;
    • activer la synchro des tampons pour ce fichier ;
    • réinitialiser STDOUT avec une copie de toto (c’est ce qui correspond à l’appel dup(2)) ;
    • fermer toto.

    Par exemple :

    File.open("toto", "w") do |fic|
       fic.sync = true
       STDOUT.reopen(fic)
    end
    # La sortie standard se fait maintenant dans toto

    Toutes les classes de Ruby héritent de la classe Object, qui définit une méthode dup (qu’il ne faut pas confondre avec l’appel dup(2)) : cette méthode crée une copie de surface de l’objet. On peut donc sauvegarder l’ancienne valeur de STDOUT en faisant save = STDOUT.dup avant le reopen. Pour revenir à l’ancien STDOUT, il suffira alors de faire STDOUT.reopen(save).
    Le principe est le même pour STDERR et pour STDIN (dans ce dernier cas, le fichier doit, bien sûr, avoir été ouvert en lecture).

    Application : on écrit la commande moncat, qui peut être appelée moncat fic1 (affichage à l’écran) ou moncat fic1 fic2 (copie de fic1 dans fic2).

    begin
       raise "Usage: #{$0} fic1 [fic2]" if not ARGV[0] or ARGV.length > 2
    
       File.open(ARGV[0], File::RDONLY) do |src|
          if ARGV[1]    # il y a donc deux paramètres => redirection de STDOUT
             File.open(ARGV[1], File::WRONLY | File::CREAT) do |dest|
                dest.sync = true
                STDOUT.reopen(dest)   # Redirection de STDOUT
             end                      # Fermeture de dest
          end
    
          # Quoi qu’il en soit, on affiche sur STDOUT
          loop do
             ligne = src.sysread(100)
             STDOUT.syswrite(ligne)
          end
       end
    rescue EOFError   # on ne fait rien, c’est OK
    rescue Exception => e
       STDERR.puts(e)
       exit(1)
    end

    Méthode plus simple (si le fichier n’est pas énorme) :

     begin
       raise "Usage: #{$0} fic1 [fic2]" if not ARGV[0] or ARGV.length > 2
    
       if ARGV[1]   # il y a donc deux paramètres => redirection de STDOUT
          File.open(ARGV[1], File::WRONLY | File::CREAT, 0600) do |fd|
             fd.sync = true
             STDOUT.reopen(fd)
          end
       end
    
       puts(IO.read(ARGV[0]))
    
    rescue Exception => e
       STDERR.puts(e)
       exit(1)
    end

    5.7 Informations sur les fichiers et les répertoires

    Les appels stat(2), lstat(2) et fstat(2) permettent d’obtenir des informations sur les fichiers et les répertoires. Ces trois fonctions remplissent une structure struct stat dont les champs sont documentés dans la page de manuel. Les deux premiers appels permettent d’interroger un fichier ou un répertoire en indiquant son nom, le troisième permet d’interroger un fichier ou un répertoire déjà ouvert en indiquant son descripteur. Les appels stat(2) et lstat(2) ne diffèrent que dans leur gestion des liens symboliques : la première suit les liens (donc ne renseigne pas sur le lien lui-même, mais sur sa cible), tandis que la seconde ne le suit pas (elle renseigne donc sur le lien lui-même).

    5.7.1 En Ruby

    La classe File fournit les méthodes stat et lstat, qui peuvent être appelées comme des méthodes de classes sur un nom de fichier ou comme des méthodes d’instances sur un objet fichier déjà ouvert (la sémantique de fstat est celle de stat appelée comme méthode d’instance d’un objet File existant).
    Ces méthodes renvoient une instance de la classe File::Stat, qui contient une kyrielle de méthodes d’instances permettant d’extraire l’information voulue (s.uid, s.size, par exemple), ainsi que de nombreuses méthodes d’interrogation permettant d’interpréter les informations précédentes (s.symlink?, s.directory?, s.readable?, par exemple). Voir ri File::Stat pour la liste exhaustive.
    Pour simplifier l’accès à certaines informations, les classes IO et File définissent certaines méthodes de classe ou d’instance permettant d’éviter de passer explicitement par un objet File::Stat. Ainsi, io.pid est l’équivalent de io.stat.pid et File.ctime("toto") est l’équivalent de File.stat.ctime("toto") :

    >> File.stat("Notes.xls")
    => #<File::Stat dev=0xe000009, ino=89359454, mode=0100644, nlink=1, \
                    uid=501, gid=80, rdev=0x0, size=19968, blksize=4096, \
                    blocks=40, atime=Tue Feb 14 19:10:35 CET 2006, \
                    mtime=Tue Dec 13 21:36:53 CET 2005, \
                    ctime=Tue Dec 13 21:39:30 CET 2005>
    >> File.stat("Notes.xls").size
    => 19968
    >> File.stat(“Notes.xls”).mtime
    => Tue Dec 13 21:36:53 CET 2005
    >> File.stat("Notes.xls").directory?
    => false
    >> File.stat("Notes.xls").file?
    => true

    Ruby dispose également de la classe Pathname (qu’il faut inclure, car elle ne fait pas partie du core), qui permet de manipuler directement un fichier ou un répertoire en passant par son nom, sans devoir créer un objet File. Cette classe dispose de toutes les méthodes de la classe File (voir ri Pathname) :

    >> require ‘pathname’
    fic = Pathname.new("Notes.xls")
    => #<Pathname:Notes.xls>
    >> fic.size
    => 19968
    >> fic.mtime
    => Tue Dec 13 21:36:53 CET 2005
    >> fic.directory?
    => false
    >> fic.file?
    => true

    6. Autres outils (pour Unix)

    6.1 Interrogation de /etc/passwd et /etc/group

    Les appels getpw... (voir getpwent(3)) permettent d’interroger le fichier /etc/passwd. Ils remplissent une structure struct passwd décrite dans ces pages de manuel.

    Les appels getgr... (voir getgrent(3) permettent d’interroger le fichier /etc/group. Ils remplissent une structure struct group décrite dans ces pages de manuel.

    6.1.1 En Ruby

    Le module Etc (ce n’est pas une classe) permet de gérer facilement à la fois les entrées des fichiers /etc/passwd et /etc/group.
    Ce module fournit les fonctions Etc.getlogin, Etc.getwpnam(nom), Etc.getpwuid([uid]), Etc.getgrgid(gid), Etc.getgrnam(nom), Etc.group et Etc.passwd.
    getpwpnam(nom), getpwuid([uid]), getgrnam(nom) et getgrgid(gid) renvoient, respectivement, une structure passwd et group correspondant au nom ou à l’identifiant qui leur est passé en paramètre (voir les appels système correspondants pour connaître les noms des différents champs de ces structures). Si getpwuid est appelé sans paramètre, c’est celui de l’utilisateur courant qui est utilisé.
    group et passwd sont, respectivement, des itérateurs sur les fichiers /etc/group et /etc/passwd.
    Exemple :

    require "etc"
    puts("Bonjour #{Etc.getpwuid.name}")
    puts("Tu utilises le shell #{File.basename(Etc.getpwuid.shell)}")
    puts("Celui de root est #{Etc.getpwnam(‘root’).shell}")
    infos_passwd = File.stat(‘/etc/passwd’)
    nom_user  = Etc.getpwuid(infos_passwd.uid).name
    nom_group = Etc.getgrgid(infos_passwd.gid).name
    puts("Le fichier /etc/passwd appartient à #{nom_user}/#{nom_group}")
    puts("Les groupes de ce système sont :")
    Etc.group do |entree|
       puts(entree.name)
    end
    puts(“Les utilisateurs qui utilisent zsh sont :”)
    Etc.passwd do |entree|
       puts(entree.name) if entree.shell =~ %r(/zsh$)
    end

    6.2 Conversion d’une structure C

    Le problème est le suivant : on veut relire en Ruby des enregistrements correspondant à une certaine struct C ou l’inverse : on veut produire en Ruby des enregistrements correspondant à une certaine struct du C. Ce genre d’opération est très fréquent pour, par exemple, utiliser ou relire des données selon un format défini par un certain protocole.
    Que ce soit en Perl, en Python ou en Ruby, ces deux opérations s’appellent, respectivement, unpack et pack. Le principe consiste à leur indiquer le format des données pour qu’elles puissent les extraire ou les formater.
    Pour les exemples, on supposera le programme C suivant, qui écrit un enregistrement dans un certain format :

    #include <string.h>
    #include <stdio.h>
    
    int main(void) {
    
      typedef struct {
          char nom[30];
          double taille;
          int age;
          char plop;
      } enreg_t;
    
      enreg_t enreg;
      FILE *fic;
    
      strcpy(enreg.nom, "Dupont");
      enreg.taille = 1.8;
      enreg.age = 30;
      enreg.plop = ‘c’;
      printf(“taille d’un enregistrement = %d\n”, sizeof(enreg_t)); /* 48 */
    
      fic = fopen("test_enreg.dat", "w");
      fwrite(&enreg, sizeof(enreg), 1, fic);
    
      /* test */
      fic = freopen("test_enreg.dat", "r", fic);
      fread(&enreg, sizeof(enreg_t), 1, fic);
      fclose(fic);
      printf("%s %f %d %c\n", enreg.nom, enreg.taille, enreg.age, enreg.plop);
    
      return 0;
    }

    On notera qu’il y a eu alignement en mémoire (on a une taille totale de 48 au lieu de 80 + 8 + 4 + 1 = 43).

    6.2.1 En Ruby

    La méthode Array.pack permet de construire une chaîne d’octets à partir d’un tableau selon une chaîne de format.
    Chaque élément du tableau sera codé en fonction du spécificateur de format qui lui correspond dans la chaîne de format (les spécificateurs peuvent être suivis d’un nombre dont la signification dépend du spécificateur, voir ri pack).
    La méthode String.unpack permet de décoder une chaîne d’octets pour obtenir un tableau contenant les différents éléments extraits selon la chaîne de format indiquée.
    Le programme Ruby suivant permet de relire le fichier créé par ce programme. On notera que la taille de la chaîne a été mise à 32 pour tenir compte de l’alignement réalisé par le compilateur C.

    >> fd = File.open("test_enreg.dat", File::RDONLY)
    => #<File:test_enreg.dat>
    >> ligne = fd.sysread(48).unpack("Z32dic")
    => [“Dupont”, 1.8, 30, 99]
    >> fd.close

    unpack peut également servir à découper une chaîne :

     >> chaine = "12345678"
    => "12345678"
    >> tab = chaine.unpack("a2" * 4)
    => ["12", "34", "56", "78"]
    tab = "1650571120163".unpack("a" + ("a2" * 3) + ("a3" * 2))
    => [“1”, “65”, “05”, “71”, “120”, “163”]

    7. Manipulation des répertoires

    Les répertoires sont traités par les appels système opendir(3) et associés (la page de manuel de opendir les recense tous).
    Globalement, le fonctionnement est le même qu’avec les fichiers : on ouvre un répertoire avec opendir(3), qui renvoie un DIR * qu’on utilise ensuite pour les autres opérations. Lorsqu’on a fini de travailler sur ce répertoire, on le ferme avec closedir(3).
    Pour parcourir les entrées de ce répertoire, on dispose de l’appel readdir(3) qui renvoie les informations dans un struct dirent. On peut accéder à des endroits particuliers grâce à l’appel seekdir(3), notamment.

    7.1 En Ruby

    La classe Dir encapsule les différents appels système sur les répertoires.
    Dir.new(chemin) et Dir.open(chemin) fonctionnent comme leurs homologues de File (notamment, open ferme automatiquement le répertoire à la fin du bloc).
    Dir.rmdir(chemin) supprime le répertoire indiqué s’il est vide (sinon, elle lève l’exception SystemCallError[3]).
    Sur un objet Dir ouvert, on peut appliquer les méthodes suivantes :

    • rep.each {|entrée| bloc} applique le bloc à chaque entrée de rep ;
    • rep.pos ou rep.tell renvoie la position courante dans rep ;
    • rep.pos= ou rep.seek positionne à l’entrée indiquée dans rep ;
    • rep.rewind positionne sur la première entrée de rep ;
    • rep.close ferme rep.

    Exemple : un ls allégé (et sans effort de présentation...).

    require ‘getoptlong’
    require "etc"
    
    opts = GetoptLong.new(
        ["--long", "-l", GetoptLong::NO_ARGUMENT],
        ["--all",  "-a", GetoptLong::NO_ARGUMENT])
    
    format_tous = format_long = false
    opts.each do |opt, arg|
       if opt == "--long" then
          format_long = true
       elsif opt == "--all" then
          format_tous = true
       end
    end
    
    begin
       raise "Usage: #{$0} [-a|-l|--all|--long] [rep]" if ARGV and ARGV.length > 1
       ARGV[0] ||= ‘.’  # Si ARGV[0] n’existe pas, on lui affecte ‘.’
       Dir.open(ARGV[0]) do |rep|
          if not format_tous then   # on ne garde que ceux qui ne commencent pas par ‘.’
             noms = rep.find_all {|nom| nom !~ /^\./}
          else
             noms = rep.to_a
          end
    
          if format_long then
             noms.each do |nom|
                stats = File.lstat("#{ARGV[0]}/#{nom}")
                print("#{stats.mode} #{stats.nlink} #{Etc.getpwuid(stats.uid).name}")
                print(" #{Etc.getgrgid(stats.gid).name} #{stats.size}");
                puts("\t#{stats.ctime.strftime(‘%d %b %H:%M’)} #{nom}")
             end
          else
             puts(noms)
          end
       end
    
    rescue Exception => e
       STDERR.puts(e)
       exit!(1)
    end

    [3] Pour supprimer un répertoire non vide, utilisez FileUtils.rm_r, mais à vos risques et périls !

    8. Fichiers mappés en mémoire

    Un fichier mappé en mémoire apparaît au programme comme une zone mémoire pouvant être ou non partagée par plusieurs processus : on peut donc le manipuler comme un tableau, ce qui est parfois plus simple que d’utiliser des primitives d’entrées sorties.
    Le fichier n’est pas copié en mémoire (ce qui serait coûteux en temps et en espace) : c’est le système de mémoire virtuelle qui s’occupe de lire et écrire physiquement les pages du fichier au moment où l’on y accède et qui gère tous les tampons.
    On peut ne mapper qu’une partie du fichier (en indiquant l’offset par rapport au début du fichier et la longueur de la zone concernée).
    L’appel système mmap(3) permet de créer un fichier mappé dans une zone mémoire qui sera ensuite manipulable comme un tableau d’octets. munmap(3) supprime l’association entre le fichier et la zone de mémoire.
    Les fichiers mappés en mémoire pouvant être partagés par plusieurs processus, ils constituent donc un mécanisme d’IPC possible (voir la section sur les processus).

    8.1 En Ruby

    La bibliothèque standard de Ruby ne dispose pas de classe permettant d’effectuer cette association. Toutefois, la classe Mmap, écrite par Guy Decoux, est disponible sur http://moulon.inra.fr/ruby/mmap.html et se compile sur tous les Unix que nous avons testés.
    On crée une association en créant un objet Mmap avec sa méthode new. On la supprime en appelant la méthode d’instance munmap. La zone ainsi créée peut être manipulée comme un objet String (enfin, presque : voir la documentation pour connaître les méthodes disponibles).
    Les paramètres attendus par new sont, dans l’ordre :

    • Le nom du fichier à mapper.
    • Le mode, qui est "r" par défaut, mais qui peut valoir "w", "rw" ou "a".
    • La protection, qui vaut par défaut Mmap::MAP_SHARED (partage entre tous les processus mappant la même zone du fichier). La valeur Mmap::MAP_PRIVATE crée une association privée au processus.
    • Un hachage comportant les options de l’association : length (longueur de la zone), offset (début de la zone) et advice (type de l’accès). Par défaut, ce hachage est vide (c’est donc tout le fichier qui est mappé).

    Exemple : on veut afficher un fichier à l’envers (sans le modifier) :

    require ‘mmap’
    def affiche_envers(mmap)
      (mmap.size - 1).downto(0) do |i|
         putc(mmap[i])
      end
    end
    
    begin
      raise "Usage: #{$0} fichier" if not ARGV[0] or ARGV.length > 2
      tampon = Mmap.new(ARGV[0], "r")
      affiche_envers(tampon)
      puts
    rescue Exception => e
      STDERR.puts(e)
      exit!(1)
    ensure
      tampon.munmap if tampon
    end

    Pour que le fichier soit modifié sur place, il suffit de changer le mode "r" en "rw" et de remplacer l’appel à affiche_envers par la ligne tampon.reverse!.

    9. Les verrous

    Les fichiers sont, par essence, des données partagées et sont donc des ressources critiques du système. La synchronisation de leurs accès passe par l’utilisation de verrous.

    9.1 Verrous externes

    Le verrouillage externe d’une ressource consiste à utiliser un fichier " verrou " externe à la ressource.
    Le principe est simple : un processus désirant écrire dans un fichier crée, au préalable, un fichier verrou. Si cette création échoue (parce que ce fichier verrou existe déjà), le processus se met en sommeil, puis refait une tentative.
    Au départ, c’était le seul mécanisme disponible pour verrouiller des ressources. Il repose sur l’atomicité de la primitive open(3) et a été renforcé ensuite par des options (O_EXCL fait échouer open(3) si elle est utilisée avec O_CREAT et que le fichier existe déjà).
    Pour poser un verrou externe, il suffit donc d’utiliser un code comme celui-ci :

    void verrouiller(void) {
       int fdesc = -1;
    
       do {
          if ((fdesc = open("verrou", O_CREAT|O_WRONLY|O_EXCL, 0600)) == -1)
          if (errno == EEXIST)  /* Le verrou existe déjà */
             sleep(1);
          else { /* Trop de descripteurs ouverts ? (par exemple) */
             fprintf(stderr, "%s : création du verrou impossible\n",
                        strerror(errno));
             abort();
          }
       } while (fdesc == -1);
    
       close(fdesc);
    }

    Avec ce code, le processus tente de créer le verrou verrou : il ne pourra poursuivre son exécution que si le verrou n’existe pas déjà (O_EXCL) et a pu être créé.
    Pour supprimer un verrou, on utilisera l’appel unlink(3) :

     void deverrouiller(void) {
      unlink("verrou");
    }

    Quelques remarques :

    • Ce code utilise un appel à sleep(3), ce qui impose un temps de latence : c’est le principe de l’attente active, car le processus reste actif.
    • Si le programme s’arrête avant d’avoir supprimé le verrou, sa prochaine exécution se bloquera, car le verrou existe toujours.
    • Ce système n’est pas applicable avec des systèmes de fichiers NFS, car rien ne garantit que O_CREAT|O_EXCL sera respecté (opération non atomique).
    • Cette technique est grossière, car elle verrouille la totalité d’un fichier : même si d’autres processus souhaitaient mettre à jour un autre enregistrement de ce fichier, ils devraient quand même attendre.

    Pour faire la même chose en Ruby, on peut bien évidemment créer un fichier traditionnel, comme on l’a expliqué plus haut, ainsi que la fonction sleep (ri sleep) pour mettre le processus en attente. La traduction est laissée en exercice.
    Pour Ruby, il existe également une classe Lockfile, disponible sur http://www.codeforpeople.com/lib/ruby/lockfile/ permettant d’automatiser ces opérations. En outre, les verrous créés par cette classe peuvent fonctionner avec NFS.

    9.2 Verrous internes

    Ici, le verrou est intégré à la ressource elle-même. Le verrouillage est un service offert par le noyau qui s’occupe de l’attribution des verrous aux processus et de leur libération. Il existe différents types de verrous internes : les verrous consultatifs (advisory) et impératifs (mandatory). Dans chaque cas, le verrou peut être partagé ou exclusif.

    9.2.1 Situations d’interblocage (deadlock)

    Supposons que deux processus P1 et P2 essaient de poser des verrous exclusifs sur deux fichiers F1 et F2 :

    • P1 a réussi à poser un verrou exclusif sur F1 et P2 a fait de même avec F2.
    • Si P1 demande à poser un verrou sur F2, il est donc bloqué. Si P2 demande à poser un verrou sur F1, il est également bloqué : on a alors une situation typique de deadlock, car P1 est bloqué par P2, lui-même bloqué par P1...

    9.2.2 Verrouillage consultatif sur tout un fichier

    L’appel flock(2) permet de poser un verrou sur tout un fichier déjà ouvert. Le verrou peut être partagé (LCK_SH), exclusif (LCK_EX), bloquant (par défaut) ou non bloquant (LCK_NB, auquel cas l’appel échoue si le verrou ne peut être posé : errno vaut alors EWOULDBLOCK, mais l’exécution se poursuit). On relâche un verrou en utilisant le même appel, mais avec la constante LOCK_UN.

    Note :

    Les verrous sont posés sur les fichiers, pas sur les descripteurs. Il n’y a donc pas de duplication de verrous en cas d’appel à dup ou dup2. Par ailleurs, si un processus fils relâche un verrou posé par son père, ce dernier perd également le verrou.

    La pose de ces verrous dépend de la présence éventuelle d’autres verrous qui seraient incompatibles :

    • Si un fichier est verrouillé en lecture, d’autres processus peuvent le lire en même temps et sans problème : c’est un verrou partagé.
    • Si un fichier est verrouillé en lecture, les demandes d’écriture sont bloquées pour assurer la cohérence de la lecture en cours.
    • On ne peut donc poser un verrou d’écriture que lorsqu’il n’y a aucun verrou de lecture.
    • Un verrou d’écriture empêche la pose de tout autre verrou. Les verrous d’écriture sont donc des verrous exclusifs.

    Par rapport au système des fichiers verrous, ce système offre donc :

    • De meilleures performances : plus de sleep, le noyau peut reprendre l’exécution du processus bloqué dès que le verrou peut être posé.
    • Les verrous peuvent désormais être partagés par NFS.
    • La possibilité de distinguer les opérations de lecture de celles d’écriture.
    • Il n’y a plus de risque d’avoir des " verrous orphelins ", puisque le noyau se charge de les supprimer à la fin du processus qui les a posés.

    Par contre, là encore, il n’y a pas moyen de ne verrouiller que la partie concernée d’un fichier. Les verrous POSIX, que nous verrons plus loin, permettent de résoudre ce problème.

    9.2.2.1 En Ruby

    La méthode flock des objets File prend une constante (File::LCK_SH, par exemple) en paramètre et permet de verrouiller l’objet fichier en lecture, en écriture ou de le déverrouiller. Le verrouillage peut être bloquant ou non bloquant (voir ri File.flock). Le fonctionnement est identique à flock(2).

    9.2.2.2 Exemple

    V1 pose un verrou exclusif sur le fichier F1 et un verrou partagé sur deux fichiers F2 et F3. V2 pose un verrou partagé sur F1 et F2 et exclusif sur F3 alors que F1 n’a pas encore levé les siens.
    Solution en Ruby :

     # Code de V1
    desc1 = File.new("F1", File::RDWR|File::CREAT, 0600)
    desc2 = File.new("F2", File::RDWR|File::CREAT, 0600)
    desc3 = File.new("F3", File::RDWR|File::CREAT, 0600)     
    
    if desc1.flock(File::LOCK_EX)
       puts “V1 (#{Process.pid}) : Verrou exclusif pose sur F1”
    end
    if desc2.flock(File::LOCK_SH)
       puts “V1 (#{Process.pid}) : Verrou partagé pose sur F2"
    end
    if desc3.flock(File::LOCK_SH)
       puts “V1 (#{Process.pid}) : Verrou partagé pose sur F3"
    end
    sleep(10)
    if desc1.flock(File::LOCK_UN)
       puts “V1 (#{Process.pid}) : Verrou ôté de F1"
    end
    sleep(10)
    puts("Fin de V1")
    
    # Code de V2
    desc1 = File.new("F1", File::RDWR|File::CREAT, 0600)
    desc2 = File.new("F2", File::RDWR|File::CREAT, 0600)
    desc3 = File.new("F3", File::RDWR|File::CREAT, 0600)
    
    if desc1.flock(File::LOCK_SH)
       puts “V2 (#{Process.pid}) : Verrou partage pose sur F1”
    end
    if desc2.flock(File::LOCK_SH)
       puts “V2 (#{Process.pid}) : Verrou partage pose sur F2”
    end
    if desc3.flock(File::LOCK_EX)
       puts “V2 (#{Process.pid}) : Verrou exclusif pose sur F3”
    end
    puts(“Fin de V2 (#{Process.pid})”)

    9.3 Le verrouillage d’enregistrement avec les verrous POSIX

    La principale faiblesse de flock(2) par rapport aux verrous POSIX (cf. fcntl(2)) est qu’il porte sur tout un fichier alors que les verrous POSIX permettent de ne verrouiller qu’une partie d’un fichier.
    Comme souvent avec les appels POSIX (voir l’API des signaux, plus loin), le principe consiste à remplir une structure avec les paramètres adéquats, puis à passer cette structure à la fonction. Un verrou POSIX est défini par la structure struct flock (décrite dans fcntl(2)). Outre cette structure, l’appel attend une constante indiquant la commande que l’on veut exécuter : pose d’un verrou non bloquant (F_SETLK), pose d’un verrou bloquant (F_SETLKW), obtention d’informations sur un verrou (F_GETLK). Un verrou est supprimé en " posant " un verrou de type F_UNLCK. Les autres types disponibles sont F_RDLCK (partagé) et F_WRLCK.
    Si la pose d’un verrou provoque un deadlock, l’appel échoue et errno vaut EDEADLK. Si un verrou non bloquant ne peut pas être posé, l’appel échoue et errno vaut EACCES.
    En configurant la structure pour qu’elle recouvre l’ensemble du fichier, on retrouve la sémantique des verrous de type flock(2).

    9.3.1 En Ruby

    Les verrous POSIX ne sont pas directement implémentés en Ruby (il existe une implémentation, http://raa.ruby-lang.org/project/posixlock/, mais elle ne permet pas un verrouillage partiel, bien que cet ajout ne semble pas très compliqué à écrire).
    Nous sommes typiquement dans une situation ou Ruby/DL (http://raa.ruby-lang.org/project/ruby-dl/) peut permettre de résoudre le problème (une nouvelle version, ruby-dl2,  est en cours de développement). DL signifie Dynamic Loader : avec ce module, un script Ruby peut directement importer une fonction C lors de son exécution en accédant à une bibliothèque partagée (.so Unix ou .dll de Windows), mais cela nous emmènerait trop loin.

    9.4 Verrous impératifs

    Les verrous précédents étaient consultatifs : cela suppose donc que tous les processus concernés coopèrent, car si l’un deux ne teste pas la présence de verrous, rien ne l’empêchera de modifier un fichier pourtant déjà verrouillé en écriture.
    Un verrouillage impératif a pour but d’empêcher cette situation. Ce ne sont plus les programmes qui posent ou lèvent les verrous, c’est le noyau qui s’en charge automatiquement :

    • Toute demande d’écriture sera bloquée si un autre processus a une région verrouillée en lecture ou en écriture qui entrerait en conflit avec cette écriture.
    • Toute tentative de lecture sera bloquée si un autre processus a une région verrouillée en écriture qui entrerait en conflit avec cette lecture.

    9.4.1 Inconvénients et avantages du verrouillage impératif

    • Cette forme de verrouillage a un coût en termes de performances, car chaque opération de lecture ou d’écriture dans un fichier implique des tests de la part du noyau.
    • Un processus abusif peut conserver indéfiniment un verrou exclusif/partagé, empêchant ainsi toute opération sur ce fichier.
    • En outre, le verrouillage impératif ne fait pas partie du standard POSIX.1 et certaines implémentations ne le proposent pas : il est pris en charge par SGI IRIX, HPUX, UnixWare 7, Solaris 8, AIX 4.3 et les versions récentes de Linux. Par contre, il n’existe pas dans les Unix BSD (pas dans ceux que nous connaissons, en tout cas).
    • Avec le verrouillage impératif, tous les processus voient leurs accès aux fichiers synchronisés, qu’ils aient posé ou non un verrou.
    • En outre, pour les simples mises à jour, il n’y a plus besoin de poser/libérer de verrous.

    9.4.2 Mise en place d’un verrou impératif

    Pour mettre en place un verrouillage impératif, on utilise la commande chmod(1), qui doit alors permettre de positionner le bit setgid tout en désactivant le bit x du groupe (chmod g+s g-x). Pour savoir si le système dispose des verrous impératifs, consultez les pages man de chmod et stat (recherchez la macro S_ENFMT).

    10. Les signaux

    Les signaux sont des mécanismes permettant de prendre en compte les événements pouvant se produire pendant l’exécution d’un processus. Ce sont des interruptions logicielles asynchrones.
    Ces événements peuvent être :

    • des événements inattendus ([Ctrl]-[C], par exemple) ;
    • des erreurs de programmation (erreurs d’adressage, par exemple) ;
    • des événements programmés.

    Lorsque de tels événements surviennent, le processus reçoit un signal et exécute alors un traitement associé à ce signal (un gestionnaire de signal) puis, généralement, se termine (mais un processus peut se protéger contre certains signaux, afin de les ignorer s’ils surviennent).
    Le traitement d’un signal par un processus consiste à annuler le traitement par défaut pour ce signal en fournissant un traitement, défini par le programmeur. Ce traitement peut consister :

    • à exécuter du code défini par une fonction ;
    • à se masquer contre un signal (sauf SIGKILL) ;
    • à revenir au traitement par défaut.

    Sous Unix, les signaux sont définis dans le fichier sys/signal.h, qui associe à chaque signal (un entier positif) une constante symbolique commençant par SIG. Par exemple :

    • SIGINT (2) est le signal d’interruption : il est envoyé à tous les processus créés à partir d’un terminal lorsque l’on fait [Ctrl]-[C] dans celui-ci.
    • SIGQUIT (3) a le même effet que SIGINT, mais crée en plus un fichier core que l’on pourra analyser avec un débogueur. Il est envoyé lorsque l’on fait break ou [Ctrl]-[X].
    • SIGKILL (9) est l’arme fatale pour détruire un processus (on ne peut pas se protéger contre ce signal). Le processus cesse immédiatement son exécution en laissant tout en l’état...
    • SIGBUS (10) provient lorsque des pointeurs ont été mal initialisés.
    • SIGALARM (14) est un signal expédié à l’expiration d’un délai.
    • SIGCHLD (20) est envoyé à un processus père quand l’un de ses fils se termine.
    • SIGUSR1 (30) et SIGUSR2 (31) n’ont pas de rôles prédéfinis. Ils sont mis à la disposition du programmeur pour synchroniser les processus.

    Note :

    Ne jamais employer directement les numéros de signaux, car ceux-ci peuvent varier d’un système à l’autre !

    La fonction strsignal(2) permet d’obtenir la désignation d’un signal, lorsque l’on connaît son numéro. La constante symbolique NSIG définie dans signal.h, permet de connaître le nombre de signaux disponibles sur le système. Voici comment afficher tous les numéros de signaux et leur désignation symbolique sur votre système :

    #include <stdio.h>
    #include <signal.h>
    #include <unistd.h>
    
    int main(void) {
       int i;
    
       /* affiche les n° de signaux et leurs noms */
       for (i = 0; i < NSIG; i++)
          printf("%d : %s\n", i, strsignal(i));
    
       return 0;
    }

    10.1 En Ruby

    Ruby dispose de la classe Signal qui fournit la méthode de classe list. Celle-ci renvoie un hachage dont les clés sont les noms des signaux (des chaînes) et les valeurs leurs numéros.
    Un hachage n’étant pas trié, une petite manipulation est nécessaire pour produire la même liste que le programme précédent (ce n’est que de la décoration...).
    On inverse le hachage pour que les valeurs (les noms des signaux) deviennent les clés et que les numéros des signaux deviennent les valeurs, puis on trie le hachage obtenu :

    # Affichage des couples triés
    Signal.list.invert.sort.each do |paire|
       puts(paire.join(" : "))
    end

    10.2 Émission d’un signal

    Un processus envoie le signal SIG à un autre processus identifié par PID en utilisant l’appel kill(PID, SIG) (voir kill(2)) :

    • Si PID vaut 0, le signal est envoyé à tous les processus appartenant au même groupe que le processus émetteur.
    • Si PID vaut -1, le signal est envoyé à tous les processus, sauf les processus système (ceux ayant l’indicateur P_SYSTEM positionné), le processus de PID 1 (init) et celui qui a envoyé le signal. Seul le super-utilisateur peut émettre un tel appel.
    • Si PID vaut -N, le signal est envoyé à tous les processus appartenant au groupe N.
    • Dans tous les autres cas, le signal est envoyé au processus ayant le PID indiqué.
    • Si SIG vaut 0, aucun signal n’est envoyé : cela permet de tester l’existence de PID (l’appel échouera si le processus visé n’existe pas et errno vaudra ESRCH. Il ne fera rien si le processus existe...).

    À la réception d’un signal SIG, le processus PID est dérouté pour exécuter le traitement associé à ce signal. S’il n’est pas détruit, il poursuit ensuite son exécution.
    En Ruby, un processus est une instance de la classe Process (voir ri Process). Celle-ci contient la méthode kill (voir ri Process.kill) prenant en paramètre un signal (numérique ou symbolique) et un pid. Toutes les deux se comportent comme la fonction kill que nous venons de décrire :

    begin
       Process.kill(0, 12345)
       puts “Le processus 12345 existe”
    rescue Errno::ESRCH
       puts “Le processus 12345 n’existe pas”
    end

    10.3 Traitement des signaux

    L’API POSIX pour le traitement des signaux repose sur l’utilisation de jeux de signaux.
    L’ancienne API, non fiable, qui utilisait l’appel signal ne doit plus être utilisée (l’appel signal(3) actuel est désormais une interface simplifiée de l’API POSIX). Voir signal(3), sigaction(2), sigemptyset(3), sigfillset(3), sigaddset(3), sigdelset(3) et sigismember(3).
    Exemple de détournement de SIGINT :

    #include <signal.h>
    
    void traite_sig( int sig ) {
       static char cpt = 1;
       if (cpt++ == 5) exit(0);
    }
    
    int main(void) {
       struct sigaction sa_new;
    
       sa_new.sa_handler = traite_sig;
       sigemptyset(&sa_new.sa_mask);  /* raz du masque */
       sa_new.sa_flags = 0;           /* pas de flags */
    
       if (sigaction(SIGINT, &sa_new, NULL)) {
          perror(“Erreur avec sigaction() “);
          exit(1);
       }
    
       for ( ;; );
    
       exit(0);
     }

    10.3.1 En Ruby

    La classe Signal de Ruby dispose de la méthode de classe trap, qui peut prendre deux formes :

    [NDLR] Signalons que cet article n’est pas rédigé par n’importe qui. Tout le monde n’est pas cité dans le " Guide du Linuxien Pervers " ;)

    Signal.trap(sig, proc)
    signal.trap(sig) { bloc }

    La première syntaxe est surtout utilisée pour ignorer un signal (proc vaut "IGNORE" ou "SIG_IGN") ou pour revenir au traitement par défaut (proc vaut "DEFAULT" ou "SIG_DFL").
    La deuxième syntaxe permet de mettre en place un gestionnaire de traitement associé au signal :

    nb_sigint = 0
    Signal.trap("SIGINT") do
       nb_sigint +=1
       exit(0) if nb_sigint == 5
    end
    
    loop do; end

    11. La prochaine fois

    Après cette mise en jambe, nous sommes prêts à aborder la création de processus et le mécanisme fork/exec, la liaison de processus par des pipes et l’utilisation des tubes nommés. Ce sera l’objet du prochain article.

    Liens :

    • Si Ruby n’est pas déjà installé sur votre système, vous pouvez utiliser votre gestionnaire de paquetage ou télécharger les sources et les compiler vous-même (ce qui n’est pas bien difficile, puisque cela consiste essentiellement à exécuter le trio infernal configure/make/make install). Le fichier archive des sources de la dernière version publique, la 1.8.4, est disponible à l’URL suivante : ftp://ftp.ruby-lang.org/pub/ruby/ruby-1.8.4.tar.gz. Après l’installation de Ruby, vous devriez disposer de tous les outils nécessaires utilisés dans cet article : ri, irb et ruby.
    • Le site www.ruby-lang.org/ contient une foule de pointeurs liés au langage.
    • Le portail http://www.ruby-doc.org/ vous permettra d’accéder à quasiment toutes les ressources en ligne concernant Ruby. Vous y trouverez notamment la documentation de l’API, ainsi que des liens vers des ouvrages et des articles librement téléchargeables ou disponibles en librairie.
    • Le site de Why, the lucky stiff, http://qa.poignantguide.net/, comporte des liens intéressants et, surtout, son ouvrage, A poignant guide to Ruby (il en existe une traduction française, en cours d’élaboration : http://fr-draft.poignantguide.net/).
    • Le même Why, sur le site http://tryruby.hobix.com/, vous permet d’apprendre Ruby interactivement, grâce à un programme écrit en... Ruby.
    • Le forum Usenet consacré à Ruby est comp.lang.ruby (c’est une réplication de la liste de diffusion ruby-talk à laquelle vous pouvez vous abonner à partir du site ruby-lang (Attention ! Cette liste est très prolixe...).
    • Les éditions O’Reilly ont également une page consacrée à Ruby : http://www.oreillynet.com/ruby/

    Retrouvez cet article dans : Linux Magazine 85

    Posté par (La rédaction) | Signature : Éric Jacoboni | Article paru dans

    Laissez une réponse

    Vous devez avoir ouvert une session pour écrire un commentaire.