<< Prev  |  TOC  |  Front Page  |  Talkback  |  FAQ
LINUX GAZETTE
...making Linux just a little more fun!
Programming with User Mode Linux
By Nick Weber

Introduction
Installation
Running
Example 1: Networking
Example 2: System Calls
Example 3: Device Drivers
Example 4: SysV IPC
Code
References

Introduction

User Mode Linux (UML) is a port of Linux to Linux. It creates a virtual Linux machine that runs on top of a current Linux installation. This virtual machine can be run in usermode, which allows you to complete access the linux kernel of the virtual machine as a normal user. This type of power and flexibility allows you to try things out on the kernel of the virtual machine without having to be root on the host machine or rebooting the host machine. Testing is one of many applications of UML. In this article we will explore some common things you would do in an operating systems class and see how these things can be done with UML. The UML project can be found at user-mode-linux.sourceforge.net and is maintained by Jeff Dike. There is also two mailing lists for the UML project: the user and development lists.

Installation

Before beginning note that these instructions will not work on a 2G/2G host. UML installation is straight forward and easy enough to accomplish in a short period of time. Three things are required to successfully install UML: You can obtain the Linux kernel from a mirror or go to www.kernel.org to obtain a Linux kernel. The kernel version must be the same as the UML patch version. At the time of this writing I used the Linux 2.4.19 kernel with the uml-patch-2.4.19-45.bz2 patch. The UML patch and root file system can be acquired from the UML sourceforge site at user-mode-linux.sourceforge.net/UserModeLinux-HOWTO-2.html. Recommendation: apply the latest skas patch from the UML website to the host (your machine) kernel. SKAS stands for separate kernel address space. Without this patch, UML will use tt mode by default. This mode creates quite a few threads for the UML that is running. The main advantage of skas mode is that the UML instance will run noticeably faster. From here there are 4 steps to complete the installation:
  1. Unpack the Linux kernel into a directory. It is recommended that you set up a separate directory other than the one where the source for your main kernel is stored (Russell). After all, the whole point is to get this running in user mode and you shouldn't be able to create a Linux kernel in /usr/src/linux unless you are root.
  2. Apply the UML patch (Russell). cat uml-patch-2.4.19-45.bz2 | bunzip2 - | patch -p1 (Russell)
  3. Create a Linux config file from the source that was unpacked in step one. make xconfig ARCH=um (Russell, p.2). The defaults are good enough for the first try.
  4. Compile the kernel with 'make linux ARCH=um' (Russell).

Running UML

Once you have a compiled kernel all you have to do now is run it with the command 'linux' (Russell). This assumes that you have a root filesystem in the current directory called root_fs. If you don't then use this command 'linux ubd0=name-of-root-filesystem' (Russell). You should now see a Linux machine booting up like normal, but in you terminal that you are working with. The root filesystems that are on the UML website all have a login/password of root/root and guest/guest for the root and guest account respectively.

Example 1: Networking

Now that we can run multiple UMLs it's time to make them talk to each other. There are six ways to get the UMLs to communicate: a switch daemon, ethertap, TUN/TAP, multicast, slip, slirp, and pcap. The instructions to set up each of the methods is described at user-mode-linux.sourceforge.net/networking.html. The method that I found the easiest to set up was TUN/TAP.

The first step to get TUN/TAP is to install uml_utilities. This can be obtained from the UML website. To install the utilities untar the file, cd into the created directory and type 'make install'. This will install five programs into /usr/bin with uml_net being the one that we are interested in. uml_net will help do the setup so that the host and UML can communicate. The only drawback of this method is that uml_net is a setuid program and can be a possible security vulnerability. The setup that the uml_net program does can also be done on the host machine as the root user. This will be covered in a later addition.

For this example we will setup the host with an ip of 10.0.0.1 and the UML with 10.0.0.2. On the host machine assign the ip address to the eth0 interface with 'ifconfig eth0 10.0.0.1'. Now we boot the the UML machine with the following command './linux eth0=tuntap,,,10.0.0.1'. There are four paramaters that can be specified for eth0, but we are interested in the first and last one for now. The first one tells UML which transport to use and the last paramter specifies the ip of the host machine. A point of confusion for many is the last paramter. This is the ip of the host machine and not what you want the ip of the UML to be. After booting login and run 'ifconfig eth0 10.0.0.2' on the UML machine. Now you should be able to ping, ssh, ftp, etc to the host machine from UML and vice versa.

Example 2: System Calls

A fun thing to do with a kernel is to add to it with our own system calls. This normaly requires root access to the machine and a reboot to use the system call. Since UML is easy to reboot and we have root access to it we have everything we need to implement our new system call without rebooting the host machine or needing root access to it.

You will need to make changes to three files in the UML kernel directory. Starting from the UMLkernel directory they are include/asm/arch/unistd.h, arch/um/kernel/sys_call_table.c, arch/um/kernel/Makefile. The code for the system call will go in the arch/um/kernel directory. Using the code from code section as an example do the following:

  1. To unistd.h add:
    #define __NR_my_new_call 243
    The number after __NR_my_new_call may be different in your case but is the last number of the #define section plus 1.
  2. In sys_call table.c the following changes are required (Karypidis):
    extern syscall_handler_t sys_my_new_call;
    #define LAST_GENERIC_SYSCALL __NR_my_new_call
    [ __NR_my_new_call ] = sys_my_new_call,
  3. In the Make file add:
    my_new_call.o to the list of build targets.
  4. Now add the source code for the system call to the arch/um/kernel directory
  5. Compile the UML kernel

To use the system call within UML do the following:
  1. Boot and login to UML
  2. Create a user program to make use of the new call. This will also contain the library wrapper for the system call.
  3. Mount the host by 'mount none /mnt -t hostfs'
  4. Compile the test program with 'gcc -I/mnt/path-to-uml-code/include testprogram.c'
  5. Run the test program
The reason we have to mount the host machine into UML is that the code for the system call is located outside of the UML filesystem. This is the only comparable difference between using a system call in UML and one on the host machine.

Example 3: Device Drivers

Another Useful thing to do with UML is to test code for device drivers. Adding a driver to the UML kerenel is the same process as adding one the host kernel. First thing is to boot up UML and login. Using the file pp.c from the code section compile pp.c with 'gcc -Wall -c -O2 pp.c'. This will produce pp.o, which we will load into the running UML kerenel. More than likely you will get an error message when trying to insmod the driver about different kerenel versions between the kernel used to compile the driver and that of the one used to create the filesystem for the UML root_fs. You can force the driver to be loaded with the -f switch like so 'insmod -f pp.o'. Before you can use the driver you will probably need to check /var/log/messages for the command to add proper device in /dev. To test the driver compile the test program with 'gcc testprogram.c' and then run the executable. Check /var/log/messages to be sure the program ran correctly. If it did you should see a message telling you that the device was opened and then one for the device being closed. The code and information in this section relies on Alessandro Rubini's book Linux Device Drivers.

Example 4: SysV IPC

These examples are taken from the book Beginning Linux Programming.

Shared Memory
Shared memory allows you to map unused memory to be used by multiple processes. There are four functions that are used to set up and use a shared memory segment. They are: shmget(), shmat(), shmdt(), shmctl(). Since these functions are implemented through system calls we can expect to find the underlying system call of: sys_shmget(), sys_shmat(), sys_shmdt(), sys_shmctl() for each of the functions respectively. Shared memory in UML is done the same as you would for the host kernel. Check out shrmem1_sysV.c and shrmem2_sysV.c for the source code of two programs using a segment of shared memory. Compile each program with gcc, start shrmem2_sysV in the background then run shrmem1_sysV.

Message Passing
Another way to share data between programs is throught the use of the message passing API. Like shared memory, the message passing API also has four functions with underlying system calls. The user functions are msgget(), msgsnd(), msgrcv(), and msgctl(), while the system calls are sys_msgget(), sys_msgsnd(), sys_msgrcv(), and sys_msgctl. For an example of message passing compile the two source files recvmsg_sysV.c and sendmsg_sysV.c. Start recvmsg_sysV in the background then run sendmsg_sysV to see message passing in action.

Example of Code

Example 2: System Call Code

my_new_call.c

#include <linux/kernel.h>

asmlinkage int sys_my_new_call(void) {
printk(KERN_ALERT "sys_my_new_call at your service\n");
return 0;
}

testprogram.c

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

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

Example 3: Device Driver Code

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

/* This will be the name we choose for our device. We will also use this as a prefix on functions such as the entry points appearing in the file_operations struct. */
#define DEV_NAME "pp"
static int Major;

/* These are prototypes for residents of the file_operations struct */
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 *);

/* This is the file_operations struct. The init_module function will register this with the kernel so the kernel will know all the entry points it contains. */
struct file_operations Fops = {
owner: THIS_MODULE,
read: pp_read,
write: pp_write,
open: pp_open,
release: pp_close,
};

/* The pp_probe function does nothing here, but reminds us that a 'real' driver may need to probe for hardware resources. These resources might later be allocated in init_module. */
static int pp_probe(void){
return 0;
}

/* The pp_read function is a stub, but at least does a printk, for tracing purposes, when it is called. */
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;
}

/* The pp_write function is a stub, but at least does a printk, for tracing purposes, when it is called. */
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;
}

/* The pp_open function does a printk for tracing purposes. */
static int pp_open(struct inode *inode, struct file *file) {
printk(KERN_ALERT "\nAn instance of %s has been opened.\n", DEV_NAME);
return 0;
}

/* The pp_close function does a printk for tracing purposes. */
static int pp_close(struct inode *inode, struct file *file) {
printk(KERN_ALERT "\nOne instance of %s has been closed.\n", DEV_NAME);
return 0;
}

/* Next we'll see that that init_module
* registers the file_operations struct so the kernel will know about the entry points therein
* gets back a major number
* calls pp_probe, to look for hardware resources
Had hardware resources been found, they would need to be allocated for use by this driver, probably within the scope of init_module. */
int init_module(void) {
Major = register_chrdev( 0, DEV_NAME, &Fops);
if (Major < 0) {
printk("Registration Failure!\n");
return Major;
}
if (pp_probe() < 0) {
unregister_chrdev(Major, DEV_NAME);
printk(KERN_ALERT "pp_probe() failure!\n");
return -1;
}
printk(KERN_ALERT "\nRegistered %s, at major number = %d.\n\n", DEV_NAME, Major);
printk("To use %s, you must create a device file.\n", DEV_NAME);
printk("If this has not already been done, then enter:\n");
printk(" mknod /dev/%s c %d 0\n\n", DEV_NAME, Major);
printk("Also set appropriate permissions for /dev/%s.\n\n", DEV_NAME);
return 0;
}

/* The cleanup_module function unregisters the driver and, in a 'real' driver would free up any resources allocated by init_module. */
void cleanup_module(void) {
int ret;
ret = unregister_chrdev(Major, DEV_NAME);
if (ret < 0)
printk(KERN_ALERT "\nUnregistration problem where ret = %d\n\n", ret);
else
printk(KERN_ALERT "\nUnregistered %s, at major number = %d\n\n", DEV_NAME, Major);
}

testprogram.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("\nOpen of %s failed.\n", DEVICE);
exit(-1);
}
printf("\nOpen of %s succeeded.\n", DEVICE);
ret = close(ddfd);
if (ret < 0) {
printf("\nClosing %s failed.\n", DEVICE);
exit(-1);
}
printf("\n Close of %s succeeded.\n", DEVICE);
exit(0);
}

Example 4: Shared Memory

shrmem1_sysV.c
/* sysV IPC shared memory - write to shared memory
shrmem1_sysV.c
meant to be used with shrmem2_sysV:
start shrmem2_sysV in background,
then start 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 in shrmem1_sysV failed");
exit(EXIT_FAILURE);
}
shared_mem = shmat(shmid, (void *)0, 0);
if (shared_mem == (void *)-1) {
perror("shmat in shrmem1_sysV failed");
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("Waiting for client ...\n");
}
printf("Enter some text: ");
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 in shrmem1_sysV failed");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}

shrmem2_sysV.c
/* sysV IPC shared memory - read from shared memory
shrmem2_sysV.c
meant to be used with shrmem1_sysV:
start shrmem2_sysV in background,
then start 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 in shrmem2_sysV failed");
exit(EXIT_FAILURE);
}
shared_mem = shmat(shmid, (void *)0, 0);
if (shared_mem == (void *)-1) {
perror("shmat in shrmem2_sysV failed");
exit(EXIT_FAILURE);
}
printf("memory attached at %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("You_wrote: %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 in shrmem2_sysV failed");
exit(EXIT_FAILURE);
}
if (shmctl(shmid, IPC_RMID, 0) == -1) {
perror("shmctl in shrmem2_sysV failed");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}

Example 4(cont): Message Passing

sendmsg_sysV.c
/* sysV IPC message passing - sender
sendmsg_sysV.c
meant to work with recvmsg_sysV:
start rcvmesg_sysV in background,
then start 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 in sendmsg_sysV failed");
exit(EXIT_FAILURE);
}
while (run) {
printf("Enter some text:");
fgets(buffer, BUF, stdin);
some_data.my_msg_type = 1;
strcpy(some_data.some_text, buffer);
if (msgsnd(msqid, &some_data, BUF, 0) == -1) {
perror("msgsnd in sendmsg_sysV failed");
exit(EXIT_FAILURE);
}
if (strncmp(buffer, "end", 3) == 0) {
run = 0;
}
}
exit(EXIT_SUCCESS);
}

recmsg_sysV.c
/* sysV IPC message passing - receiver recvmsg_sysV.c meant to work with sendmsg_sysV: start recvmsg_sysV in background, then start 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 in recvmsg_sysV failed");
exit(EXIT_FAILURE);
}
while (run) {
if (msgrcv(msqid, &some_data, BUFSIZ, msg_to_recv, 0) == -1) {
perror("msgrcv in recvmsg_sysV failed");
exit(EXIT_FAILURE);
}
printf("You wrote: %s", some_data.some_text);
if (strncmp(some_data.some_text, "end", 3) == 0) {
run = 0;
}
}
if (msgctl(msqid, IPC_RMID, 0) == -1) {
perror("msgctl in recvmsg_sysV failed");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}

References

Karypidis, Alexandros. "Adding a System Call" Online. Internet. 25, March 2002 Available: user-mode-linux.sourceforge.net/lksct.

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

Rubini, Alessandro, Jonathan Corbert. Linux Device Drivers. 2nd ed. O'Reilly Associates, Incorporated, July 2001.

Russell, Rusty. "User Mode Linux HOWTO". Online. Internet. 18, June 2002. Available: user-mode-linux.sourceforge.net/UserModeLinux-HOWTO.html.

 

[BIO] I am currently pursuing a graduate degree in Computer Science from Eastern Washington University in Cheney, Washington, USA.


Copyright © 2003, Nick Weber. Copying license http://www.linuxgazette.net/copying.html
Published in Issue 90 of Linux Gazette, May 2003

<< Prev  |  TOC  |  Front Page  |  Talkback  |  FAQ