Retrouvez cet article dans : Linux Magazine 85
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% 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 - De la commande
mand’Unix (exemple :man 2 umask). Quand on fera référence à un appel système, on le noteraumask(2), le2faisant référence à la section du manuel. - De la commande
ride Ruby (exemple :ri File.umask). - De l’interpréteur interactif de Ruby :
irb(on en sort par la commandequit). - De l’interpréteur non interactif de Ruby :
rubyauquel 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...
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
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é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é % 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 tataLes paramètres transmis par le shell sont des chaînes (des
nbre = ARGV[0].to_i
puts(“Résultat : #{nbre * 2}")
Voici un exemple d’utilisation :
% ruby params.rb 42 Résultat : 84Question subsidiaire : si l’on n’avait pas converti le paramètre en entier, le résultat aurait été
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 (3.1 En Ruby
En Ruby, on dispose de la classerequire ‘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 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 à 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 fichier4.1 Manipulation de errno
- La fonction
void perror(const char *message)affichemessagesurstderr, suivi d’un :, suivi du texte correspondant à la valeur de errno. - La fonction
char *strerror(int num)renvoie le message correspondant à la valeur d’erreurnum(donc,strerror(errno)produit le message correspondant à la valeur courante deerrno...).
#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% 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 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 (5. Entrées/Sorties sur disque
L’utilisation d’un fichier consiste en :- son ouverture dans un certain mode ;
- des accès ;
- sa fermeture.
- 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).
#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>> %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 droits0666 & (~ 0077) = 0666 & 0700 = 0600
5.2.1 En Ruby
Le masque de création est fourni par la méthode de classeFile.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#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
- 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).
#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 5.3.1 En Ruby
Pour ouvrir ou créer un objet fichier, on utilise la méthode de classefic = File.new(nomfic, mode = "r" [, droits])
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
if File.exists(nomfic) then fic = File.open(nomfic, File::RDONLY) ... travail avec fic puis fermeture else ... traiter problèmeMais 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? endComme on vient de le voir, un objet fichier est fermé par l’appel à sa méthode
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é à 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
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 (/* 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
5.4.1 En Ruby
On utilise les méthodes de la classe 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 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 renvoientstr = fd.read(taille) => lit au plus taille octets dans fd str = fd.read => litCertaines méthodes, dans la même situation, lèvent une exception :toutfd 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
str = fd.readline => comme fd.gets, mais lève une exception EOFError car = fd.readchar => lit un seul caractère, lève une exception EOFErrorDans 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èreNote: Dans le cadre de cette présentation, nous nous en tiendrons volontairement à
5.4.1.3 Itération sur un fichier
Les objetsIO.foreach(ARGV[0]) do |ligne| print ligne endAu niveau des instances, la méthode
File.open(ARGV[0]) do |fic| fd.each_line do |ligne| # ou each, qui est synonyme... puts ligne end endMais, 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
puts(IO.read(ARGV[0]))Note : Un énumérable dispose aussi des méthodes
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 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- Ouvrir
totoen écriture (le créer selon le cas). Soit fd son descripteur. - Fermer le descripteur 1 (celui de
STDOUT). - Dupliquer le descripteur de
totoavecdup(fd). Comme celui-ci prend la première entrée libre dans la table des descripteurs, il prendra l’entrée 1... - Fermer
totopour libérer son descripteur.
- ouvrir le fichier
totoen écriture (ou le créer) ; - activer la synchro des tampons pour ce fichier ;
- réinitialiser
STDOUTavec une copie detoto(c’est ce qui correspond à l’appeldup(2)) ; - fermer
toto.
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 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 appels5.7.1 En Ruby
La classe>> 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 >> 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 appels6.1.1 En Ruby
Le modulerequire "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#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>> 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
>> 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ème7.1 En Ruby
La classerep.each {|entrée| bloc}applique le bloc à chaque entrée derep;rep.posourep.tellrenvoie la position courante dansrep;rep.pos=ourep.seekpositionne à l’entrée indiquée dansrep;rep.rewindpositionne sur la première entrée derep;rep.closefermerep.
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ème8.1 En Ruby
La bibliothèque standard de Ruby ne dispose pas de classe permettant d’effectuer cette association. Toutefois, la classe- 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 valeurMmap::MAP_PRIVATEcré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) etadvice(type de l’accès). Par défaut, ce hachage est vide (c’est donc tout le fichier qui est mappé).
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 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 primitivevoid 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 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_EXCLsera 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.
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 :P1a réussi à poser un verrou exclusif sur F1 et P2 a fait de même avec F2.- Si
P1demande à 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- 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.
- 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.
9.2.2.1 En Ruby
La méthode9.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 de9.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 (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 commande10. 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.
- à exécuter du code défini par une fonction ;
- à se masquer contre un signal (sauf
SIGKILL) ; - à revenir au traitement par défaut.
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 queSIGINT, mais crée en plus un fichier core que l’on pourra analyser avec un débogueur. Il est envoyé lorsque l’on faitbreakou [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) etSIGUSR2(31) n’ont pas de rôles prédéfinis. Ils sont mis à la disposition du programmeur pour synchroniser les processus.
#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# 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- 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_SYSTEMpositionné), 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
errnovaudraESRCH. Il ne fera rien si le processus existe...).
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#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 classeSignal.trap(sig, proc)
signal.trap(sig) { bloc }
La première syntaxe est surtout utilisée pour ignorer un signal (proc vaut 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- 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,irbetruby. - 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





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