Article 2 : Création d'un translateur.

Pierre Chifflier - pollux@wzdftpd.net, David Odin - dindinx@gimp.org

Comme nous l'avons vu lors du précédente article de cette série, la notion de translateur est un des thèmes centraux de GNU/Hurd. Aujourd'hui nous allons décortiquer ce qui se cache derrière ce mot et voir comment un simple utilisateur est capable de créer un tel objet.

Translateurs - Généralités

Dans un système comme le Hurd, les fichiers sont utilisés pour pratiquement tout. Déjà sous GNU/Linux, on trouvait un générateur de nombres aléatoires dans /dev/urandom ou des informations sur les processeurs dans /proc/cpuinfo. Mais sous GNU/Linux, ces fichiers spéciaux ne pouvait être créé que par l'utilisateur root, et avait un comportement forcément assez limité (un fichier comme cpuinfo n'existe que dans un système de fichier particulier, par exemple).

Le Hurd va plus loin dans cette idée grâce à la notion de translateurs. Un translateur est un programme (service) qu'un utilisateur (pas forcément root) va associer à un fichier ou un répertoire pour lui donner un comportement particulier. Ainsi, les fichiers dans /devcachent tous des translateurs, une partition montée est simplement un répertoire (le point de montage) associé à un translateur capable de gérer un système de fichier (ext2 par exemple). Et même l'interface réseau (pfinet) est un translateur particulier.

L'association entre un fichier ou un répertoire et un translateur est effectué à l'aide de la commande settrans et il est possible de connaître l'état d'un translateur passif à l'aide de la commande showtrans, et l'état d'un translateur actif avec la commande fsysopt.

Comme nous le verrons dans la seconde partie de cet article, il est assez facile de créer un translateur donnant un rôle particulier à un fichier. Mais avant de se lancer dans la création d'un tel outil, il faut se pencher sur la notion de ports.

Ports

Dans Mach, les serveurs communiquent en envoyant des messages à travers des "ports", sortes de files de messages. Ces ports disposent de permissions: la réception de messages est associée à une seule tâche, alors que plusieurs tâches peuvent envoyer des messages.

L'utilisation de ces messages est assez semblable à l'utilisation des RPC[1] sous linux. L'envoi de messages ressemble à un appel de fonction, mais en pratique le message transite par Mach qui le redirige vers la tâche associée. L'idée n'est pas limitée à une seule machine, le message pourrait très bien être envoyé sur le réseau à une autre machine de manière transparente.

Pour obtenir un port sur un serveur, il suffit d'explorer le système de fichiers, et d'utiliser la fonction hurd_file_name_lookup qui renvoie un droit d'envoi sur le port du serveur associé au noeud du système de fichiers (si on dispose des droits suffisants sur le fichier). Ce concept fonctionne bien puisque dans Hurd il existe toujours un système de fichiers.

Par exemple, voici comment utiliser le fichier /servers/password pour obtenir un port sur le serveur de mots de passe :

mach_port_t identity;
mach_port_t pwserver;
kern_return_t err;

pwserver = hurd_file_name_lookup
                ("/servers/password");

err = password_check_user (pwserver,
                           0 /* root */, "supass",
                           &identity);
Le premier appel de fonction permet de récupérer un descripteur pour le fichier "/servers/password", qui se trouve être associé au translateur permettant de tester l'identité des utilisateurs. Le second appel vérifie l'identité de l'utilisateur root en s'assurant que son mot de passe est bien "supass", et renvoie éventuellement un port sur son identité (qui pourrait servir par la suite à obtenir un jeton du serveur auth, en utilisant la fonction msg_add_auth). C'est ce que fait par exemple la commande addauth.

Translateurs

Comme indiqué en introduction, un translateur peut être associé à un fichier ou gérer une arborescence complète, ce dernier cas étant certainement le plus puissant. Mais pour aujourd'hui, nous allons nous créer un type de translateur assez simple, dont le point de montage est un fichier. Pour cela, une bibliothèque de fonctions a été mise au point permettant de construire rapidement des translateurs assez simples. Cette bibliothèque se nomme trivfs.

Trivfs

Trivfs est une bibliothèque qui permet de développer rapidement un translateur simple, en général limité à un seul fichier. Dans GNU/Hurd, l'envoi de messages pour les fonctions habituelles sur un fichier sont encapsulées dans les fonctions de la bibliothèque C, qui se charge d'envoyer les messages sur les ports. Cela signifie qu'on n'implémentera pas les fonctions comme read(), mais plutôt la fonction associée au message, par exemple io_read (). Du point de vue du programme appelant, rien ne change, il suffit d'appeler la fonction read() et la libc se charge d'envoyer les messages, ce qui est un point important pour la compatibilité avec les autres systèmes !

Avec trivfs, il faudra implémenter des fonctions dont le prototype est fixé, et qui correspondent à tous les messages que pourra recevoir notre translateur. Trivfs se charge de convertir le port en une structure contenant les données associées à une instance du fichier (credentials). Cela permet de conserver des informations propres à chaque instance du translateur. La fonction read() est donc convertie en un message, que notre translateur recevra par l'intermédiaire de la fonction trivfs_S_io_read, qui décode les arguments de la RPC io_read().

Afin de pouvoir utiliser trivfs, il faut installer les paquets gnumach-dev et hurd-dev, et pour ceux qui n'auraient pas suivi le but de l'article, un compilateur (gcc).

Un translateur en lecture seule

Le translateur que nous allons implémenter aujourd'hui tiendra le rôle de lexique anglais/français. On pourra écrire un mot en anglais dans le fichier attaché, et la lecture de ce fichier fournira le mot traduit en français.

Pour faciliter la compréhension, nous avons choisi de présenter ce programme de manière évolutive : dans un premier temps, nous allons voire comment réaliser un translateur en lecture seule, ce qui posera les bases indispensables, puis nous compliquerons petit à petit pour obtenir le translateur final.

Quelques variables indispensables

Voici donc le début du translateur (vous trouverez sur le CD les différentes versions du code).
#define _GNU_SOURCE 1
Cette constante doit être définie pour tout programme utilisant les bibliothèqued du Hurd. Elle doit être définie avant d'inclure les fichiers d'entête, puisqu'elle modifie légèrement leur comportement. On peut donc maintenant les inclure :
#include <stdio.h>
#include <hurd/trivfs.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <error.h>
Nous voici prêt à définir quelques variables globales indispensables qui indiquent le comportement général de notre translateur :
int trivfs_fstype = FSTYPE_MISC; /* translateur trivfs générique
  * voir /usr/include/hurd/hurd_types.h pour les valeurs possibles */
int trivfs_fsid = 0;  /* doit toujours être à zéro au début */

/* comment notre fichier/translateur peut être ouvert */
int trivfs_allow_open = O_READ;

/* ce que l'on a implémenté */
int trivfs_support_read = 1;
int trivfs_support_write = 0;
int trivfs_support_exec = 0;
Puis vient le moment de déclarer nos propres variables globales :
/* Une structure pour les mots que notre lexique connaît */
struct t_lexique {
  char * value;
};

struct t_lexique lexique[] = {
  { "precis\n" },
  { "biere\n" },
  { "vim\n" },
  { "hurd\n" },
  { "piscine\n" },
  { "vin\n" },
  { NULL }
};

/* le numéro du mot que le translateur renvoie lors d'un read. */
static int index = 0;
Note : on ajoute un retour à la ligne "\n" à la fin de chaque mot uniquement pour que l'affichage soit un peu plus joli lors de nos tests avec cat !

La fonction main()

Les translateurs sont des programmes comme les autres, donc ils ont besoin d'une fonction main(). Cependant, ils ne fonctionneront correctement que s'ils ont été lancés en tant que translateurs (c'est-à-dire en spécifiant à quel noeud ils doivent être attachés avec settrans), donc il nous faut un moyen de vérifier qu'il n'ont pas été lancés comme d'autres programmes. Il suffit de vérifier que le programme dispose d'un port de démarrage avec la fonction task_get_bootstrap_port, et que le retour n'est pas MACH_PORT_NULL.

int
main (int argc, char **argv)
{
  error_t err;
  mach_port_t bootstrap;
  struct trivfs_control *fsys;

  task_get_bootstrap_port (mach_task_self (), &bootstrap);
  if (bootstrap == MACH_PORT_NULL)
    error (1,0, "Doit être lancé comme un translateur");

  /* Réponse au parent */
  err = trivfs_startup (bootstrap, 0, 0, 0, 0, 0, &fsys);
  mach_port_deallocate (mach_task_self (), bootstrap);
  if (err)
    error (3, err, "trivfs_startup");

  /* C'est parti ! */
  ports_manage_port_operations_one_thread (fsys->pi.bucket, trivfs_demuxer, 0);

  return 0;
}
error_t
trivfs_goaway (struct trivfs_control *cntl, int flags)
{
  exit (0);
}

Les fonctions indispensables à un trivfs

Lors de la définition des variables globales, nous avons indiqué à la bibliothèque trivfs que nous avions l'intention d'implémenter un certain nombre de fonctions, notamment celle de lecture (read()). Mais il va falloir également définir d'autres fonctions, servant à l'utilisation générale du translateur.

Les fonctions de rappel (Hooks)

trivfs offre la possibilité d'appeler une fonction après certains événements, par exemple lorsque le translateur est ouvert, détruit, etc. Nous allons utiliser ces fonctions pour connaître le nombre d'octets que nous avons déjà envoyé dans les précédents appels. C'est indispensable, car la fonction read() sera toujours appelée au moins deux fois : une fois pour lire les données, et un autre appel qui indiquera la fin du fichier. Nous allons donc garder dans une structure un variable qui nous donnera combien d'octets ont été lus depuis l'ouverture du fichier attaché à notre translateur. Il faudra donc s'assurer que cette variable soit remise à zéro lors de chaque ouverture. Nous allons donc demander à trivfs d'appeler automatiquement des fonctions lors de l'ouverture et lors de la fermeture du fichier.

On déclare deux fonctions de rappel (hooks) à partir des prototypes définis dans le fichier /usr/include/hurd/trivfs.h dont voici un extrait :

/* If this variable is set, it is called every time a new peropen
   structure is created and initialized. */
error_t (*trivfs_peropen_create_hook)(struct trivfs_peropen *) = open_hook;

/* If this variable is set, it is called every time a peropen structure
   is about to be destroyed. */
void (*trivfs_peropen_destroy_hook) (struct trivfs_peropen *) = close_hook;

A l'ouverture du translateur, un hook est appelé avec une structure trivfs_peropen en paramètre. Cette structure contient un champ que nous pouvons utiliser pour stocker les valeurs à conserver tant que le translateur reste ouvert par un processus. Nous allons y stocker une structure struct open définie au début du programme, qui contiendra l'offset des données lues :

/* On gardera dans cette structure ce qui nous permet de
   décrire l'état d'un descripteur de fichier : l'offset
   depuis le début du fichier. */
struct open
{
  off_t offs;
};

static error_t
open_hook (struct trivfs_peropen *peropen)
{
  struct open *op = malloc (sizeof (struct open));
  if (op == NULL)
    return ENOMEM;

  /* Initialisation de l'offset. */
  op->offs = 0;
  peropen->hook = op;
  return 0;
}
Dans le hook pour la fonction close(), nous n'avons pas grand chose à faire si ce n'est libérer la mémoire que l'on a alloué dans celui pour la fonction open() :
static void
close_hook (struct trivfs_peropen *peropen)
{
  free (peropen->hook);
}

Les fonctions de base : seek(), read() et stat()

Maintenant que nous avons réglé le problème des hook, nous pouvons passer à la réalisation des fonctions vraiment utiles.

Notre première version de la fonction stat() se contentera du strict minimum : indiquer que notre translateur est un fichier, et lui donner les permissions suffisantes pour être lu :

void trivfs_modify_stat (struct trivfs_protid *cred, io_statbuf_t *st)
{
  /* On a un fichier en lecture seule de 42 octets. */
  st->st_mode &= ~(S_IFMT | ALLPERMS);
  st->st_mode |= (S_IFREG | S_IRUSR | S_IRGRP | S_IROTH);
  st->st_size = 42;
}

Sous linux, nous aurions du implémenter la fonction read(), qui prend en paramètre un descripteur de fichiers. Sous Hurd, la fonction est io_read(), et son argument principal est un port, qui agit de manière semblable. La bibliothèqueTrivfs encapsule cette fonction pour aider à décoder ses arguments, en particulier les autorisations (credentials).

Nous nous contenterons donc d'implémenter la fonction trivfs_S_io_read(). Les arguments sont les suivants:

TypeNomDescription
trivfs_protid_tcredinformations de sécurité (credentials)
mach_port_treplyLe port où sera envoyée la réponse
mach_msg_type_name_treplyPolyles permissions sur le port pour répondre
data_t *dataun pointeur sur l'endroit où écrire les données
mach_msg_type_number_t *data_lenla taille des données que nous renvoyons. Au départ, cette variable contient la taille initiale allouée pour data
loff_toffsLa position actuelle. Si la valeur est -1, nous utilisons la valeur que nous avons stocké
vm_size_tamountla quantité de données demandée

Le principe est le suivant: après toutes les vérifications nécessaires pour s'assurer que la quantité de données à lire est correcte, on calcule la quantité effective de données que la fonction va renvoyer. Si le tableau passé en argument data n'est pas assez grand, il faut le réallouer. Il est important ne noter qu'il n'est pas possible d'utiliser la fonction malloc(), puisque le pointeur renvoyé serait spécifique à notre processus, alors que nous voulons le donner au processus qui effectue la lecture.

On termine la fonction en recopiant les données, en spécifiant la quantité effective lue, et en indiquant par un code de retour nul que la fonction s'est bien déroulée.

error_t
trivfs_S_io_read (
	trivfs_protid_t cred,
	mach_port_t reply,
	mach_msg_type_name_t replyPoly,
	data_t *data,
	mach_msg_type_number_t *data_len,
	loff_t offs,
	vm_size_t amount
)
{
  struct open *op;
  char * value;

  /* On refuse l'accès s'il n'y a pas les bons crédits ou si
   * le fichier n'est pas ouvert en lecture */
  if (! cred)
    return EOPNOTSUPP;
  else if (! (cred->po->openmodes & O_READ))
    return EBADF;

  /* On récupère l'offset. */
  op = cred->po->hook;
  if (offs == -1)
    offs = op->offs;

  if (index > sizeof(lexique)/sizeof(*lexique)-1)
    return EINVAL;
  value = lexique[index].value;

  /* On récupère la quantité de données à lire, et on la corrige
   * si nécessaire. */
  if (offs > strlen (value))
    offs = strlen (value);
  if (offs + amount > strlen (value))
    amount = strlen (value) - offs;

  if (amount > 0)
    {
      /* Reallocation si besoin */
      if (*data_len < amount)
        *data = (data_t) mmap (0, amount, PROT_READ|PROT_WRITE,
                                     MAP_ANON, 0, 0);

      /* c'est ici que la lecture effective se passe */
      memcpy ((char *) *data, value + offs, amount);

      op->offs += amount;
    }

  *data_len = amount;
  return 0;
}

Pour pouvoir écrire un "Hello, world !", on a déjà vu plus court !

La lecture des données n'est pas le seul cas ou la position peut être modifiée, le processus peut également appeler la fonction seek(). Dans notre cas, il n'est pas obligatoire de l'implémenter (il ne serait juste plus possible de modifier la position), mais il est plus cohérent de pouvoir modifier explicitement l'offset que nous stockons dans nos données privées.

/* Déplacement du pointeur de lecture/écriture */
error_t
trivfs_S_io_seek (
	trivfs_protid_t cred,
	mach_port_t reply,
	mach_msg_type_name_t replyPoly,
	loff_t offs,
	int whence,
	loff_t *new_offs
)
{
  char * value;
  struct open *op;
  error_t err = 0;

  /* on teste si on a les droits suffisants */
  if (! cred)
    return EOPNOTSUPP;

  /* Ceci ne peut jamais arriver, mais bon. */
  if (index > sizeof(lexique)/sizeof(*lexique)-1)
    return EINVAL;
  value = lexique[index].value;

  /* On récupère notre donnée privée */
  op = cred->po->hook;

  /* voir la page man de lseek() */
  switch (whence)
    {
    case SEEK_SET:
      op->offs = offs; break;
    case SEEK_CUR:
      op->offs += offs; break;
    case SEEK_END:
      op->offs = strlen (value) - offs; break;
    default:
      err = EINVAL;
    }
  /* on devrait tester la validité de op->offs maintenant,
   * mais c'est peu important
   */

  if (! err)
    *new_offs = op->offs;

  return err;
}

Makefile

Pour pouvoir tester rapidement le translateur, on utilise un Makefile assez simple (en réalité, seules quelques lignes sont utiles .. mais le compactage de Makefile est une activité qui ne sera pas abordée ici).
CC=gcc
CFLAGS= -O0 -Wall -ggdb
LDFLAGS= -ltrivfs -lfshelp


all: lexique

lexique: lexique.o
	$(CC) -o $@  $< $(LDFLAGS)

test_begin: lexique
	settrans -ac foo lexique

test_end: foo
	settrans -fg foo; rm foo

lexique.o: lexique.c

clean:
	rm -f *.o *~

distclean: clean
	rm -f lexique

L'apparition de l'écriture

Bon, avoir un translateur pour afficher un mot, c'est peut-être rigolo (note de bas de page : et encore, ce serait plus sympa s'il s'agissait d'une fortune comme le fait le fortunefs [ref 12342213124]), mais pour notre lexique ce serait bien aussi que le mot affiché dépende d'un autre mot que l'on écrirait dans ce même translateur. C'est ce que nous allons réaliser maintenant.

Écriture simple

Pour ajouter le support en écriture dans notre translateur, il va bien évidemment falloir ajouter une fonction d'écriture. Là encore, nous simplifions un peu les choses en supposant que l'écriture d'un mot se fait toujours en une fois. Mais avant, nous devons indiquer que notre translateur pourra être ouvert aussi bien en lecture qu'en écriture. Pour cela, on ajoute la valeur O_WRITE à la variable globale trivfs_allow_open :
int trivfs_allow_open = O_READ | O_WRITE;
Et comme on ajoute le support pour l'écriture, on l'indique en changeant la variable trivfs correspondante :
int trivfs_support_write = 1;
Le but de notre translateur est de traduire des mots de l'anglais vers le français (c'est un translateur traducteur). Pour cela, nous allons changer un peu la structure t_lexique et la variable lexique associée :
struct t_lexique {
  char * key;
  char * value;
};

struct t_lexique lexique[] = {
  { "accurate", "precis\n" },
  { "beer", "biere\n" },
  { "editor", "vim\n" },
  { "hurd", "hurd\n" },
  { "swimming-pool", "piscine\n" },
  { "wine", "vin\n" },
  { NULL, NULL }
};
Libre à vous ensuite de compléter cette liste. Il nous reste maintenant à ajouter la fonction d'écriture qui se nomme (si vous avez bien suivi) trivfs_S_io_write :
error_t trivfs_S_io_write (
  trivfs_protid_t cred,
  mach_port_t reply,
  mach_msg_type_name_t replyPoly,
  data_t data,
  mach_msg_type_number_t datalen,
  loff_t offset,
  vm_size_t *amount
) 
{ 
  int i; 

  if (!cred) 
    return EOPNOTSUPP;
  else if (!(cred->po->openmodes & O_WRITE))
    return EBADF;

  for (i=0; lexique[i].key; i++) {
    if (strlen(lexique[i].key)==(datalen-1) &&
        !strncmp(lexique[i].key,data,datalen-1))
      index = i;
  }

  *amount = datalen;

  return 0;
}
Comme vous le voyez, rien de bien compliqué dans cette fonction. On vérifie simplement que les "credential" sont là et que le fichier correspondant à notre translateur a bien été ouvert en écriture. Si c'est le cas, on parcourt notre lexique pour voir si ce qui a été écrit dans le translateur correspond à l'un des mots que l'on connait. Le cas échéant, on positionne la variable globale index à une nouvelle valeur. Quelques remarques :

Le coup de la troncature

Comme nous venons de le voir, notre translateur n'est pas encore au point (le message "Computer bought the farm!" est un voisin du fameux SegFault). En effet, tant que l'on se contente d'ajouter des données (l'opérateur >> ajoute en fichier de ficher), la séquence d'appels système correspondante est simplement une ouverture en mode append, une écriture et la fermeture du fichier. Mais lorsque l'on remplace les données (ce que réalise l'opérateur >), l'ouverture du fichier se fait avec le mode O_TRUNC, qui donne au fichier une taille de 0. Dans notre cas, cela n'a pas un grand sens de changer la taille d'un fichier (puisque nous n'avons pas de fichier dans le sens traditionnel du terme) mais nous devons tout de même créer la fonction qui est appelée lors d'une ouverture avec O_TRUNC. Cette fonction se nomme trivfs_S_file_set_size. Et dans notre cas, nous nous contenterons de ne rien faire, c'est-à-dire que notre fonction renverra simplement 0 pour indiquer qu'elle s'est déroulée correctement et que l'on a bien pris note que le fichier avait changé de taille (en gros, on s'en fout complètement, mais comme cette fonction est appelée, il faut bien l'implémenter).

Voici cette fonction :
/* Truncate file. */
error_t
trivfs_S_file_set_size (
    trivfs_protid_t cred,
    mach_port_t reply,
    mach_msg_type_name_t replyPoly,
    loff_t new_size
    )

{
  if (!cred)
    return EOPNOTSUPP;
  else
    return 0;
}
On peut vérifier le meilleur fonctionnement de notre lexique sur le screenshot [12355346]

Un peu plus de contrôle : un paramètre de lancement

Lors de l'appel à settrans, il est possible de passer des paramètres au translateur. Par exemple le translateur pfinet récupère les paramètres réseau (adresse ip, masque, etc.) sur la ligne de commande.

Nous allons rester un peu plus modeste, en n'utilisant qu'un seul paramètre. Si ce paramètre est présent, nous l'interpréterons comme un mot de départ pour le lexique. Pour cela, il suffit d'ajouter les lignes suivantes au début de notre fonction main() :

  if (argc > 1) {
    int i;
    for (i=0; lexique[i].key; i++)
      if (!strcmp(argv[1],lexique[i].key))
        index = i;
  }

Une "vraie" fonction stat()

Maintenant que notre translateur prend forme, on va pouvoir le peaufiner un peu. Jusqu'à maintenant, la fonction stat se contentait de renseigner les champs :

Pour la taille, nous allons maintenant pouvoir retourner la vraie taille des données qu'il sera possible de lire, c'est-à-dire la taille du mot traduit (facile à trouver à partir de l'index courant. Et pour le mode nous utiliserons une nouvelle variable globale qui contiendra le mode courant  current_mode.

Par défaut cette variable aura la même valeur que précédemment :

static int current_mode = (S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);

La fonction trivfs_modify_stat devient alors :

void
trivfs_modify_stat (struct trivfs_protid *cred,
                    io_statbuf_t         *st)
{
  /* Mark the node as a read-only plain file. */
  st->st_mode &= ~(S_IFMT | ALLPERMS);
  st->st_mode |= current_mode;
  st->st_size = strlen(lexique[index].value);
}
Et on peut vérifier que cette fonction tient bien son rôle sur le screenshot [123312342]

Et un support pour le chmod

Maintenant que l'on a une variable qui contient le mode courant, il devient trivial (!) d'implémenter l'appel système chmod() au travers de la fonction trivfs_S_file_chmod. Il faut simplement s'assurer que le fichier garde son type (la partie S_IFMT).

Voici donc cette fonction :
error_t
trivfs_S_file_chmod(
    trivfs_protid_t cred,
    mach_port_t reply,
    mach_msg_type_name_t replyPoly,
    mode_t new_mode
)
{
  if (!cred)
    return EOPNOTSUPP;

  current_mode = (current_mode & S_IFMT) | new_mode;

  return 0;
}

Encore une fois, on peut vérifier son bon fonctionnement sur le screenshot [66815686]

La prochaine fois

La prochaine fois, Manuel Menal introduira un nouveau type de translateurs et une nouvelle bibliothèque, qui permettra d'implémenter des translateurs plus évolués.

help

Références et remerciements

.

help

Glossaire

Références

Dernières nouvelles du Hurd

Toujours en pleine évolution, le petit monde du Hurd bouge à grande vitesse.