Programming with User Mode Linux LG #90
<< Prev |
TOC |
Front Page |
Talkback |
FAQ
...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:
- Linux kernel
- UML patch that is the same version as the kernel
- A root file system
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:
- 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.
- Apply the UML patch (Russell). cat uml-patch-2.4.19-45.bz2 | bunzip2
- | patch -p1 (Russell)
- 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.
- 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:
- 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.
- 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,
- In the Make file add:
my_new_call.o to the list of build targets.
- Now add the source code for the system call to the arch/um/kernel directory
- Compile the UML kernel
To use the system call within UML do the following:
- Boot and login to UML
- Create a user program to make use of the new call. This will also contain the library wrapper for the system call.
- Mount the host by 'mount none /mnt -t hostfs'
- Compile the test program with 'gcc -I/mnt/path-to-uml-code/include testprogram.c'
- 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.
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