Programmer avec Linux en mode utilisateur

Gazette Linux n°90 — Mai 2003

Nick Weber

Article paru dans le n°90 de la Gazette Linux de mai 2003.

Traduction française par Joëlle Cornavin .

Relecture de la traduction française par Encolpe Degoute .

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


Table des matières
1. Introduction
2. Installation
3. Lancement d'UML
3.1. Exemple 1 : gestion réseau
3.2. Exemple 2 : appels système
3.3. Exemple 3 : pilotes de périphériques
3.4. Exemple 4 : IPC SysV
4. Exemples de code
4.1. Exemple 2: code des appels système
4.2. Exemple 3 : code de pilote de périphérique
4.3. Exemple 4 : mémoire partagée
4.4. Exemple 4 (cont) : passage de messages
5. Références

1. Introduction

UML User Mode Linux, Linux en mode utilisateur) est un port de Linux vers Linux. Il crée une machine Linux virtuelle qui tourne sur une installation Linux ordinaire. Cette machine virtuelle peut être lancée en mode utilisateur, ce qui vous permet d'accéder totalement au noyau Linux de la machine virtuelle en tant qu'utilisateur normal. Ce type de puissance et de souplesse vous permet de faire des essais sur le noyau de la machine virtuelle sans avoir à être root ou à redémarrer la machine hôte. La fonction test est une des nombreuses applications d'UML. Dans cet article, nous explorerons certaines opérations courantes que vous effectueriez dans une classe quelconque de systèmes d'exploitation et nous verrons comment les effectuer avec UML. Le projet UML, maintenu par Jeff Dike, se trouve sur http://user-mode-linux.sourceforge.net. Il existe également deux listes de discussion pour ce projet : les listes utilisateur et les listes développement.


2. Installation

Avant de commencer, notez que ces instructions ne fonctionneront pas sur un hôte de 2 Go/2 Go. L'installation d'UML est simple et assez facile à réaliser dans un bref délai. Trois éléments sont indispensables pour réussir à installer UML  :

Vous pouvez obtenir le noyau Linux sur un miroir ou aller sur http://www.kernel.org. La version du noyau doit être identique à celle du correctif UML. Au moment où j'écris cet article, j'emploie le noyau Linux 2.4.19 avec le correctif uml-patch-2.4.19-45.bz2. Le correctif UML et le système de fichiers racine sont disponibles sur user-mode-linux.sourceforge.net/UserModeLinux-HOWTO-2.html.

Astuce

Appliquez le dernier correctif skas du site du projet UML sur le noyau de l'hôte (votre machine). SKAS signifie Separate Kernel Address Space (espace d'adresse séparé du noyau). Sans ce correctif, UML utilisera le mode tt par défaut. Ce mode crée quelques fils d'exécution pour l'UML actif. Le principal avantage du mode skas est que l'instance UML s'exécutera notablement plus vite. À partir de là, il y a quatre étapes pour compléter l'installation :

  1. Décompactez le noyau Linux dans un répertoire. Je vous recommande de configurer un répertoire séparé autre que celui où est stocké le source de votre noyau principal (Russell) [1]. Au fond, toute la question est de faire fonctionner tout cela en mode utilisateur et vous ne devriez pas être en mesure de créer un noyau Linux dans /usr/src/linux à moins que vous ne soyez root.

  2. Appliquez le correctif UML (Russell). Faites un cat uml-patch-2.4.19-45.bz2 | bunzip2 - | patch -p1 (Russell).

  3. Créez un fichier de configuration Linux à partir du source qui a été décompacté dans la première étape. Faites un make xconfig ARCH=um (Russell, section 2). Les valeurs par défaut sont assez correctes pour le premier essai.

  4. Compilez le noyau avec make linux ARCH=um (Russell).


3. Lancement d'UML

Une fois que vous avez compilé le noyau, il ne vous reste plus maintenant qu'à le lancer avec la commande linux (Russell). Cela suppose que vous avez un système de fichiers racine dans le répertoire actuel appelé root_fs. Si vous ne le faites pas, alors utilisez cette commande linux ubd0=nom-du-systeme-de-fichiers-racine (Russell). Vous devriez à présent voir une machine Linux amorcer comme d'habitude, mais dans le terminal avec lequel vous travaillez. Les systèmes de fichiers racine présents sur le site web du projet UML ont tous un nom de connexion (login)/mot de passe de root/root et guest/guest pour le compte root et guest respectivement.


3.1. Exemple 1 : gestion réseau

Maintenant que nous pouvons exécuter des UML multiples, il est temps de les faire communiquer entre eux. Il y a six manières de faire communiquer les UML : un démon switch, ethertap, TUN/TAP, la multidiffusion (multicast), slip, slirp et pcap. Les instructions permettant de configurer chacune des méthodes sont décrites dans le document http://user-mode-linux.sourceforge.net/UserModeLinux-HOWTO.html. La méthode que j'ai trouvée la plus simple à configurer est TUN/TAP.

La première étape pour obtenir TUN/TAP consiste à installer uml_utilities, que vous pouvez télécharger sur le site web du projet UML. Pour installer les utilitaires, désarchivez le fichier avec untar, faites un cd pour passer dans le répertoire créé et saisissez make install. Cette commande installera cinq programmes dans /usr/bin, uml_net étant celui qui nous intéresse. uml_net nous aidera à effectuer la configuration de façon à ce que l'hôte et UML puisse communiquer. Le seul inconvénient de cette méthode est qu'uml_net est un programme setuid et peut être une possible vulnérabilité de sécurité. La configuration qu'effectue le programme uml_net peut également être réalisée sur la machine hôte en tant qu'utilisateur root. Cet aspect sera abordé dans un article ultérieur.

Pour cet exemple, nous configurerons l'hôte avec une adresse IP de 10.0.0.1 et l'UML avec 10.0.0.2. Sur la machine hôte, affectez l'adresse IP à l'interface eth0 avec ifconfig eth0 10.0.0.1. Maintenant, nous amorçons la machine UML avec la commande ./linux eth0=tuntap,,,10.0.0.1. Il y a quatre paramètres que vous pouvez spécifier pour eth0, mais pour l'instant, seuls le premier et le dernier nous intéressent. Le premier indique à UML quel transport employer et le dernier paramètre spécifie l'adresse IP de la machine hôte. Le dernier paramètre est une source de confusion pour de nombreuses personnes. C'est l'adresse IP de la machine hôte et non ce que vous souhaiteriez que soit l'adresse IP de l'UML. Après avoir amorcé, connectez-vous et exécutez ifconfig eth0 10.0.0.2 sur la machine UML. Maintenant, vous devriez être en mesure d'utiliser ping, ssh, ftp, etc. sur la machine hôte depuis la machine UML et vice versa.


3.2. Exemple 2 : appels système

Un noyau a ceci d'amusant que vous pouvez l'ajouter à vos propres appels système. Cette opération exige normalement un accès root à la machine et un redémarrage pour utiliser l'appel système. Depuis UML, il est facile de réamorcer et, une fois que nous avons l'accès root, nous avons tout ce qu'il nous faut pour implémenter notre nouvel appel système sans redémarrer la machine hôte ou avoir besoin d'un accès root à celle-ci.

Vous devrez apporter des changements à trois fichiers dans le répertoire du noyau UML. En commençant par le répertoire UMLkernel, il y a include/asm/arch/unistd.h, arch/um/kernel/sys_call_table.c, arch/um/kernel/Makefile. Le code de l'appel système ira dans le répertoire arch/um/kernel. En vous inspirant du code de la section code comme exemple, procédez comme suit :

  1. Dans unistd.h, ajoutez :

    
#define __NR_mon_nouvel_appel 243
    

    Le nombre placé après mon_nouvel_appel peut être différent en ce qui vous concerne, mais c'est le dernier nombre de la section #define plus 1.

  2. Dans sys_call table.c, les changements suivants sont requis (Karypidis) [2]:

    
extern syscall_handler_t sys_mon_nouvel_appel;
    #define LAST_GENERIC_SYSCALL __NR_mon_nouvel_appel
    [ __NR_mon_nouvel_appel ] = sys_mon_nouvel_appel,
    

  3. Dans le fichier Make, ajoutez :

    
mon_nouvel_appel.o à la liste des cibles de construction
    

  4. À présent, ajoutez le code source de l'appel système au répertoire arch/um/kernel.

  5. Compilez le noyau UML.

Pour utiliser l'appel système sous UML, procédez comme suit :

  1. Amorcez et connectez-vous sous UML.

  2. Créez un programme utilisateur pour pouvoir employer le nouvel appel. Celui-ci contiendra également le conteneur de bibliothèques (library wrapper) pour l'appel système.

  3. Montez l'hôte avec mount none /mnt -t hostfs.

  4. Compilez le programme de test avec gcc -I/mnt/chemin-vers-code-uml/include programme-de-test.c.

  5. Exécutez le programme de test.

La raison pour laquelle nous devons monter la machine hôte dans UML est que le code de l'appel système se trouve en dehors du système de fichiers UML. C'est la seule différence comparable entre utiliser un appel système sous UML et en employer un sur la machine hôte.


3.3. Exemple 3 : pilotes de périphériques

Une autre tâche utile à effectuer sous UML est de tester le code des pilotes de périphériques. Ajouter un pilote au noyau UML relève du même processus que d'en ajouter un au noyau hôte. Il faut d'abord amorcer UML et se connecter. À l'aide du fichier pp.c provenant de la section code, compilez pp.c avec gcc -Wall -c -O2 pp.c. Cette commande produira pp.o, que nous chargerons dans le noyau UML actif. Il est plus que probable que vous recevrez un message d'erreur quand vous essaierez de faire un insmod sur le pilote à propos des différentes versions de noyau, entre le noyau utilisé pour compiler le pilote et celle du noyau ayant servi à créer le système de fichiers pour le root_fs d'UML. Vous pouvez forcer le pilote à être chargé avec le commutateur -f comme insmod -f pp.o. Avant de pouvoir employer le pilote, vous devrez certainement vérifier dans /var/log/messages quelle est la commande permttant d'ajouter un périphérique approrpié dans /dev. Pour tester le pilote, compilez le programme de test avec gcc testprogram.c, puis lancez l'exécutable. Vérifiez dans /var/log/messages pour être sûr que le programme s'est exécuté correctement. Si tel est le cas, vous devriez voir un message vous indiquant le périphérique a été ouvert et ensuite un pour signaler qu'il est en cours de fermeture. Le code et les informations contenus dans cette section sont inspirés de l'ouvrage Linux — Pilotes de périphériques d'Alessandro Rubini.


3.4. Exemple 4 : IPC SysV

Ces exemples sont tirés de l'ouvrageBeginning Linux Programming.

Mémoire partagée

La mémoire partagée vous permet de mapper la mémoire inutilisée à employer par des processus multiples. Quatre fonctions sont employées pour configurer et utiliser un segment de mémoire partagée. Ce sont : shmget(), shmat(), shmdt(), shmctl(). Du fait que ces fonctions sont implémentée via des appels système, nous pouvons espérer trouver l'appel système sous-jacent de sys_shmget(), sys_shmat(), sys_shmdt(), sys_shmctl() pour chacune des fonctions, respectivement. La mémoire partagée sous UML est obtenue de la même manière que pour le noyau hôte. Vérifiez dans shrmem1_sysV.c et shrmem2_sysV.c le code source des deux programmes à l'aide d'un segment de mémoire partagée. Compilez chaque programme avec gcc, start shrmem2_sysV en arrière-plan, puis exécutez shrmem1_sysV.

Passage de messages

Une autre manière de partager des données entre des programmes consiste à faire appel à l'API de passage de messages. Comme la mémoire partagée, l'API de passage de messages est également dotée de quatre fonctions avec des appels système sous-jacents. Les fonctions utilisateur sont msgget(), msgsnd(), msgrcv(), msgctl(), alors que les appels système sont sys_msgget(), sys_msgsnd(), sys_msgrcv(), sys_msgctl. Pour avoir un exemple de passage de messages, compilez les deux fichiers source recvmsg_sysV.c et msendmsg_sysV.c. Démarrez recvmsg_sysV en arrière-plan, puis lancez sendmsg_sysV pour voir le passage de messages en action.


4. Exemples de code

4.1. Exemple 2: code des appels système

4.1.1. mon_nouvel_appel.c


#include <linux/kernel.h>

asmlinkage int sys_mon_nouvel_appel(void) {
printk(KERN_ALERT "sys_mon_nouvel_appel à votre service\n");
return 0;
}


4.1.2. programme-de-test.c


#include <sys/types.h>
#include <linux/unistd.h>


static inline _syscall0(int, mon_nouvel_appel);
int main() {
int result;
result = mon_nouvel_appel();
}


4.2. Exemple 3 : code de pilote de périphérique

pp.c


#define __KERNEL__
#define MODULE
#include <linux/module.h>
#include <linux/version.h>
#include <linux/wrapper.h>
#include <linux/fs.h>
#include <linux/sched.h>
#include <linux/ioport.h>
#include <linux/delay.h>
#include <linux/param.h>
#include <linux/interrupt.h>
#include <linux/time.h>
#include <linux/timer.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define true 1
#define false 0

/* C'est le nom que nous choisissons pour notre périphérique. Nous l'utilisons
également comme préfixe sur des fonctions telles que les points d'entrée
apparaissant dans la structure file_operations. */
#define DEV_NAME "pp"
static int Major;


/* Ce sont des prototypes pour les résidents de la structure file_operations */
static ssize_t pp_read(struct file *, char *, size_t, loff_t *);
static ssize_t pp_write(struct file *, const char *, size_t, loff_t *);
static int pp_open(struct inode *, struct file *);
static int pp_close(struct inode *, struct file *);


/* C'est la structure file_operations. Comme la fonction init_module la
déclarera avec le noyau, le noyau connaîtra tous les points d'entrée
qu'elle contient. */
struct file_operations Fops = {
owner: THIS_MODULE,
read: pp_read,
write: pp_write,
open: pp_open,
release: pp_close,
};


/* La fonction pp_probe n'a aucun rôle ici mais nous rappelle qu'un pilote
'réel' peut avoir besoin de détecter des ressources matérielles. Ces
ressources pourraient plus tard être allouées dans  init_module. */
static int pp_probe(void){
return 0;
}


/* La fonction pp_read est un moignon mais au moins elle effectue un
printk, à des fins de traçage, lorsqu'elle est appelée. */
static ssize_t pp_read(struct file *file, char *buff, size_t ctr, loff_t *woof) {
printk(KERN_ALERT "\npp_read active.\n");
return 0; 
}


/* La fonction pp_write est un moignon mais au moins elle effectue un
printk, à des fins de traçage, lorsqu'elle est appelée. */
static ssize_t pp_write(struct file *file, const char *buff, size_t ctr, loff_t *woof) {
printk(KERN_ALERT "\npp_write active.\n");
return 0; 
}


/* La fonction pp_open effectue un pintk à des fins de traçage. */
static int pp_open(struct inode *inode, struct file *file) {
printk(KERN_ALERT "\nUne instance de %s a été ouverte.\n", DEV_NAME);
return 0;
}


/* La fonction pp_close effectue un printk à des fins de traçage. */
static int pp_close(struct inode *inode, struct file *file) {
printk(KERN_ALERT "\nUne instance de %s a été fermée.\n", DEV_NAME);
return 0;
}


/* Ensuite, nous verrons que comme cet init_module
* déclare la structure file_operations, le noyau connaîtra
les points d'entrée qu'elle contient
* retourne un numéro majeur
* appelle pp_probe pour rechercher des ressources matérielles
Si des ressources matérielles étaient trouvées, il faudrait les
allouer pour que ce pilote puisse les utiliser, probablement
dans la portée d'init_module. */
int init_module(void) {
Major = register_chrdev( 0, DEV_NAME, &amp;Fops);
if (Major lt; 0) {
printk("Déclaration impossible !\n");
return Major; 
}
if (pp_probe() < 0) {
unregister_chrdev(Major, DEV_NAME);
printk(KERN_ALERT "pp_probe() failure!\n");
return -1;
}
printk(KERN_ALERT "\nDéclaré %s, au numéro majeur = %d.\n\n", DEV_NAME, Major);
printk("Pour utiliser %s, vous devez créer un fichier de périphérique.\n", DEV_NAME);
printk("Si cela n'a pas déjà été fait, alors saisissez :\n");
printk(" mknod /dev/%s c %d 0\n\n", DEV_NAME, Major);
printk("Définissez également les permissions appropriées pour /dev/%s.\n\n", DEV_NAME);
return 0;
}


/* La fonction cleanup_module annule la déclaration du pilote et,
dans un pilote 'réel', libérerait toute ressource allouée par by
init_module. */
void cleanup_module(void) { 
int ret;
ret = unregister_chrdev(Major, DEV_NAME);
if (ret < 0)
printk(KERN_ALERT "\nProblème d'annulation de déclaration où ret = %d\n\n", ret);
else 
printk(KERN_ALERT "\nAnnulé la déclaration de %s, au numéro majeur = %d\n\n", DEV_NAME, Major);
}

programme-de-test2.c


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

#define DEVICE "/dev/pp"

int main() {
int ddfd = 0;
int ret = 0;
ddfd = open(DEVICE, O_RDWR);
if (ddfd < 0) {
printf("\nOuverture de %s impossible.\n", DEVICE);
exit(-1);
}
printf("\nOuverture de %s réussie.\n", DEVICE);
ret = close(ddfd);
if (ret < 0) {
printf("\nFermeture de %s impossible.\n", DEVICE);
exit(-1);
}
printf("\nFermeture de %s réussie.\n", DEVICE);
exit(0);
}


4.3. Exemple 4 : mémoire partagée

shrmem1_sysV.c


/* Mémoire partagée sysV IPC - l'écriture dans la mémoire
partagée shrmem1_sysV.c est censée être utilisée avec
shrmem2_sysV :
démarrage de shrmem2_sysV en arrière-plan,
puis démarrage de shrmem1_sysV */

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define MEM_SZ 4096

struct shared_use_st {
int writ_by_you;
char some_text[BUFSIZ];
};

int main() {
int run = 1;
void *shared_mem = (void *) 0;
struct shared_use_st *shared_stuff;
char buffer[BUFSIZ];
int shmid;

shmid = shmget( (key_t)1234, MEM_SZ, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget dans shrmem1_sysV impossible");
exit(EXIT_FAILURE);
}
shared_mem = shmat(shmid, (void *)0, 0);
if (shared_mem == (void *)-1) {
perror("shmat dans shrmem1_sysV impossible");
exit(EXIT_FAILURE);
}
printf("memory attached at %X\n", (int)shared_mem);
shared_stuff = (struct shared_use_st *)shared_mem;
while (run) {
while (shared_stuff->writ_by_you == 1) {
sleep(3);
printf("Attend le client ...\n");
}
printf("Saisissez du texte : ");
fgets(buffer, BUFSIZ, stdin);
strcpy(shared_stuff->some_text, buffer);
shared_stuff->writ_by_you = 1;
if (strncmp(buffer, "end", 3) == 0) {
run = 0;
}
}
if (shmdt(shared_mem) == -1) {
perror("shmdt dans shrmem1_sysV impossible");
 exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}

shrmem2_sysV.c


/* Mémoire partagée sysV IPC - la lecture depuis la mémoire
partagée shrmem2_sysV.c est censée être utilisée avec
shrmem1_sysV :
démarrage de shrmem2_sysV en arrière-plan,
puis démarrage de shrmem1_sysV
*/
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define MEM_SZ 4096
struct shared_use_st {
int writ_by_you;
char some_text[BUFSIZ];
};
int main() {
int run = 1;
void *shared_mem = (void *) 0;
struct shared_use_st *shared_stuff;
int shmid;
shmid = shmget( (key_t)1234, MEM_SZ, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget dans shrmem2_sysV impossible ");
exit(EXIT_FAILURE);
}
shared_mem = shmat(shmid, (void *)0, 0);
if (shared_mem == (void *)-1) {
perror("shmat dans shrmem2_sysV impossible");
exit(EXIT_FAILURE);
}
printf("mémoire attachée à %X\n", (int)shared_mem);
shared_stuff = (struct shared_use_st *)shared_mem;
shared_stuff->writ_by_you == 0;
while (run) {
if (shared_stuff->writ_by_you == 1) {
printf("Vous avez écrit : %s", shared_stuff->some_text);
sleep(rand() % 4);
shared_stuff->writ_by_you = 0;
if (strncmp(shared_stuff->some_text, "end", 3) == 0) {
run = 0;
}
}
}
if (shmdt(shared_mem) == -1) {
perror("shmdt dans shrmem2_sysV impossible");
exit(EXIT_FAILURE);
}
if (shmctl(shmid, IPC_RMID, 0) == -1) {
perror("shmctl dans shrmem2_sysV impossible");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}


4.4. Exemple 4 (cont) : passage de messages

sendmsg_sysV.c


/* Passage de messages sysV IPC  - l'expéditeur sendmsg_sysV.c
est censé fonctionner avec recvmsg_sysV :
démarrage de rcvmesg_sysV en arrière-plan,
puis démarrage de sendmsg_sysV
*/

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define BUF 1024


struct my_msg_st {
long int my_msg_type;
char some_text[BUFSIZ];
};
int main() {
int run = 1;
struct my_msg_st some_data;
int msqid;
char buffer[BUF];
msqid = msgget( (key_t)1234, 0666 | IPC_CREAT);
if (msqid == -1) {
perror("msgget dans sendmsg_sysV impossible");
exit(EXIT_FAILURE);
}
while (run) {
printf("Saisissez du texte :");
fgets(buffer, BUF, stdin);
some_data.my_msg_type = 1;
strcpy(some_data.some_text, buffer);
if (msgsnd(msqid, &amp;some_data, BUF, 0) == -1) {
perror("msgsnd dans sendmsg_sysV impossible");
exit(EXIT_FAILURE);
}
if (strncmp(buffer, "fin", 3) == 0) {
run = 0;
}
}
exit(EXIT_SUCCESS);
}

recmsg_sysV.c


/* Passage de messages sysV IPC  - le receveur recvmsg_sysV.c
est censé fonctionner avec sendmsg_sysV :
démarrage de recvmsg_sysV en arrière-plan,
puis démarrage de sendmsg_sysV */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

struct my_msg_st {
long int my_msg_type;
char some_text[BUFSIZ];
};

int main() {
int run = 1;
struct my_msg_st some_data;
int msqid;
long int msg_to_recv = 1;
msqid = msgget( (key_t)1234, 0666 | IPC_CREAT);
if (msqid == -1) {
perror("msgget dans recvmsg_sysV impossible");
exit(EXIT_FAILURE);
}
while (run) {
if (msgrcv(msqid, &amp;some_data, BUFSIZ, msg_to_recv, 0) == -1) {
perror("msgrcv dans recvmsg_sysV impossible");
exit(EXIT_FAILURE);
}
printf("Vous avez écrit : %s", some_data.some_text);
if (strncmp(some_data.some_text, "fin", 3) == 0) {
run = 0;
} 
}
if (msgctl(msqid, IPC_RMID, 0) == -1) {
perror("msgctl dans recvmsg_sysV impossible");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}


5. Références

Adding a System Call (en ligne), de Alexandros Karypidis — 25 mars 2002 : http://user-mode-linux.sourceforge.net/lksct.

Beginning Linux Programming, de Mathew, Neil, Richard Stones — 2nd ed. Wrox Press, Inc., septembre 1999.

Linux — Pilotes de périphériques, d'Alessandro Rubini et Jonathan Corbet — O'Reilly, novembre 2001.

User Mode Linux HOWTO (en ligne), de Rusty Russell — 18 juin 2002 : user-mode-linux.sourceforge.net/UserModeLinux-HOWTO.html.

Je poursuis actuellement des études d'informatique à l'Eastern Washington University de Cheney, à Washington (États-Unis).

Notes

[1]

Paul (Rusty) Russell est un des auteurs du User Mode Linux HOWTO (en anglais) (N. d. T.).

[2]

Alexandros Karypidis est un des auteurs du User Mode Linux HOWTO (en anglais) (N. d. T.).