Creating SUSE Linux Device Drivers
Novell Cool Solutions: Feature
By Simon Nattrass
Reader Rating
from 13 ratings
|
Digg This -
Slashdot This
Posted: 16 Nov 2004 |
With the open nature of Linux and the ever-growing abundance of new devices, driver development is an excellent string to add to your bow. If you're ready to make that step from user application development to ?kernel hacking? this paper will point you in the right direction.
The content and examples contained within are targeted at a 2.4 kernel. Changes and updates required for the recent 2.6 are the subject of a future paper.
- Kernel Module Introduction
- Compilation
- Configuring the Kernel Source
- helloworld.c
- Device Drivers
- File Operations
- Driver Registration and Removal
- sample_driver.c
- Try it Out! - sample_driver.c
Kernel Module Introduction
Modules are programs which may be loaded and unloaded into and from the kernel dynamically. This is an evolution of the traditional Unix kernel, which required all functionality to be statically bound to the kernel, so support for that newfangled audio card required, you guessed it, a new kernel (well a recompilation).
It is important to note that although the fundamentals of writing kernel modules are not that dissimilar to application development, there are some important distinctions. When developing applications in user space, the kernel provides a buffer of safety ensuring that your code does not mistakenly hog the processor preventing other processes from executing or trample on the memory of another process, etc. Unfortunately this buffer does not extend to kernel space, and thus responsible and thoughtful development is a must.
In user space, libraries and associated functions such as libc for printf(), etc., are abundant. Once in kernel space, these libraries are unavailable and modules may only link against the kernel itself. As such system calls (as opposed to library) calls will be utilized heavily, which in user space are hidden behind common libraries such as libc.
Kernel development puts the developer closer to the metal without some of the graces taken for granted in user space.
| Compilation | |||
The compilation of kernel modules is similar to that of conventional applications with a few caveats.
There are some new symbols which need to be passed to the compiler:
| __KERNEL__ | The kernel header files included are to be part of a kernel module and not a user process. |
| MODULE | We're building a kernel module. |
| MODVERSIONS | Ensure that the module and kernel versions are compatible. Modules loaded into an incompatible kernel could cause problems. |
Flags to pass to the compiler:
| -O2 | Modules are expected to be optimized for use with inline functions within the kernel, thus the -O2 flag is used. |
| -Wall | It is advisable to turn on all warnings with the -Wall option. |
| -c | A module is not an independent executable but instead will be linked against the kernel at insertion time. |
| -I | The include path for the header files from the kernel tree, usually /usr/src/linux |
Combining all of the above gives:
gcc -D__KERNEL__ -DMODULE -DMODVERSIONS -O2 -Wall -I/usr/src/linux/include -o module.o -c module.c
| Configuring the Kernel Source | |||
Before we can start building against the kernel source, and since the source shipped with SUSE represents a multitude of architectures, we need to ensure that the kernel source at /usr/src/linux/ is the same as the running kernel. This can be done in several ways, the most painless being to run make cloneconfig, which expands /prog/config.gz to /usr/src/linux/.config and then runs make oldconfig.
After that, make dep is run to gather the dependencies.
# make cloneconfig # make dep
| helloworld.c | |||
Let's dive in and write our first kernel module, the ubiquitous HelloWorld! Which we will then dissect.
/*
* helloworld.c
*/
#include <linux/module.h>
#include <linux/kernel.h>
int init_module(void)
{
printk("<1>Hello World!\n");
return 0;
}
void cleanup_module(void)
{
printk(KERN_ALERT "Goodbye World...\n");
}
- #include <linux/module.h>
Every module requires this included. - #include <linux/kernel.h>
This is required for the macro expansion of printk(). - int init_module(void)
Every module requires an entry point. Upon loading, this is the first portion of code to be called. init_module() is expected to return a non zero value if this function fails. - printk("<1>Hello world 1.\n");
This is the kernel logging mechanism to report information on warnings, errors, etc. Each printk() statement has an associated priority from <0> to <7>, with lower numbers indicating the severity of the message. To aid readability each level has an associated name, defined in kernel.h<0>
KERN_EMERG
<4>
KERN_WARNING
<1>
KERN_ALERT
<5>
KERN_NOTICE
<2>
KERN_CRIT
<6>
KERN_INFO
<3>
KERN_ERR
<7>
KERN_DEBUG - void cleanup_module(void)
As every module requires an entry point, so does it require an exit point. This function is responsible for any housekeeping code prior to the module unloading.
Having examined the code, let's compile and insert the resulting module into the kernel.
$ gcc -D__KERNEL__ -DMODULE -DMODVERSIONS -O2 -Wall -I /usr/src/linux/include -o helloworld.o -c helloworld.c
With the module successfully compiled, it can now be inserted into the running kernel with insmod, ignoring any messages relating to tainting the kernel.
# insmod helloworld.o # Warning: loading helloworld.o will taint the kernel: no license
The module insert can be confirmed via checking /proc/modules which describes all the currently loaded modules.
# less /proc/modules | grep helloworld helloworld 320 0 (unused)
As a further confirmation /var/log/messages can be examined for the Hello World!from the printk() function.
# tail /var/log/messages ... July 27 14:38:07 exelon kernel: Hello World! #
Lastly the module is removed from the kernel via the rmmod command, which again may be verified by examining /var/log/messages for the output from the printk() in the cleanup_module() function.
# rmmod helloworld # tail /var/log/messages ... July 27 14:38:07 exelon kernel: Hello World! July 27 14:41:53 exelon kernel: Goodbye World... #
So there we have it, our first (although admittedly limited) kernel module.
| Device Drivers | |||
One particular type of kernel module is the device driver. These typically provide the interface between a hardware device and the kernel. Although it is not necessary that the driver must control a piece of physical hardware, the majority do. Implementing drivers as dynamically loadable modules removes the need for the kernel to be aware of all possible devices, resulting in a leaner kernel with less bloat. As new devices come along, new modules are written which may then be loaded into the existing kernel.
Device drivers fall largely into two classes, block devices and character devices.
- Block devices allow random access, reading data in multiples of a specified block size. Access is buffered.
- Character devices stream data sequentially, with access being unbuffered.
Each driver has an associated major and minor number associated with it. The major number describes a specific device while the minor number describes an instance within this device. The remainder of this document will examine those drivers representing character devices.
| File Operations | |||
Character and block devices are accessed via the file system and as such present a set of related operations as defined by file_operations structure (linux/fs.h). This structure holds a set of pointers to callback functions implemented by each driver, which define the operations supported. The file_operations structure defines every possible operation, although not all need to be implemented by the corresponding entries in the structure set to NULL.
| llseek | Change read/write position within a file |
| read | Retrieve data from the device. Failure returns -EINVAL |
| write | Write data to the device. Failure returns -EINVAL |
| readdir | Used by filesystems to examine directories |
| poll | Inquire if a device is readable or writable or in some special state |
| ioctl | Issue device-specific commands or -ENOTTY if command is not supported |
| mmap | Memory mapping from device address space to user space |
| open | Open the device |
| flush | Flush buffered data |
| release | Release data (device close) |
| fsync | Synchronize memory data state with device, write out dirty data in the buffer. |
| fasync | Notify the device of a change in its FASYNC flag |
| lock | Lock a file (filesystems) |
| readv | gather read operation |
| writev | Scatter write operations |
When assigning elements to the structure there are two possible syntaxes available, the C99 variant and the GNU C variant, the former being more favorable in terms of portability:
C99 syntax
struct file_operations fops = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release
};
GNU C syntax
struct file_operations fops = {
read: device_read,
write: device_write,
open: device_open,
release: device_release
};
| Device Registration and Removal | |||
Character devices are required to register with the kernel, informing the kernel of the supported file operations.
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
Registration returns a non negative return value on success. Providing zero as the major number provides dynamic major number assignment and the kernel returns the next available value in the range as the return value. The name is only used for something readable to be registered in /proc/devices, and fops describes the file_operations structure the driver implements.
Having registered the device with the kernel, the occasion will arise when the device will require to be removed. To avoid removing a device which may still be in use, with open connections etc... a reference counter to the device is used to monitor the device usage. This counter is controlled via three macros from linux/modules.h:
- MOD_INC_USE_COUNT: Increment the use count.
- MOD_DEC_USE_COUNT: Decrement the use count.
- MOD_IN_USE: Display the use count.
Only when a module has a usage count of zero may it be removed from the kernel.
| sample_driver.c | |||
With the theory under our belt, it's time to advance helloworld.c to a more functional device driver. sample_driver.c represents a fictional device which can read and write data, to and from an internal data store.
/*
* sample_driver.c
*/
#include <linux/module.h>
#if defined(CONFIG_MODVERSIONS)
#define MODVERSIONS
#include <linux/modversions.h>
#endif
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/wrapper.h>
#include <asm/io.h>
#include <asm/uaccess.h>
/*
* Function forward definitions for the file_operations structure
*/
static int sampledriver_open(struct inode *, struct file *);
static int sampledriver_release(struct inode *, struct file *);
static ssize_t sampledriver_read(struct file *, char *, size_t, loff_t *);
static ssize_t sampledriver_write(struct file *, const char *, size_t, loff_t *);
/*
* Assign the elements to file_operations
*/
static struct file_operations fops = {
.read = sampledriver_read,
.write = sampledriver_write,
.open = sampledriver_open,
.release = sampledriver_release
};
/*
* Fake data pool for our device
*/
static char* data_pool;
static int data_pool_size = 0;
/*
* Initialization
*/
int init_module(void)
{
struct page *page;
// get one free page for the data pool
data_pool = (char *) __get_free_page(GFP_KERNEL);
if ( data_pool == NULL )
{
return -ENOMEM;
}
page = virt_to_page(data_pool);
// reserve the page and fill it with garbage
mem_map_reserve(page);
memset(data_pool, 0x00, PAGE_SIZE);
// register the module with the kernel
int major = register_chrdev(0, "sample_driver", &fops);
if (major < 0)
{
printk (KERN_DEBUG "registering driver failed\n");
return major;
}
printk(KERN_ALERT "kenel assigned major number: %d to sample_driver\n",
major);
return 0;
} // init_module()
/*
* Cleanup
*/
void cleanup_module(void)
{
struct page *page;
// unreserve our page used for the data pool and free it
page = virt_to_page(data_pool);
mem_map_unreserve(page);
free_page((unsigned long) data_pool);
printk(KERN_ALERT "goodbye World...\n");
} // cleanup_module()
/*
* Open
*/
static int sampledriver_open(struct inode *inode, struct file *file)
{
printk(KERN_ALERT "opening sample_driver\n");
MOD_INC_USE_COUNT; // increment the usage count
return 0;
} // sampledriver_open()
/*
* Release
*/
static int sampledriver_release(struct inode *inode, struct file *file)
{
printk(KERN_ALERT "releasing sample_driver\n");
MOD_DEC_USE_COUNT; // decrement the usage count
return 0;
} // sampledriver_release()
/*
* Read
*
* Read data from internal pool
*/
static ssize_t sampledriver_read(struct file *file, char *buffer, size_t length, loff_t *offset)
{
int bytesRead = 0;
if (data_pool_size > length) {
bytesRead = length;
}
else {
bytesRead = data_pool_size;
}
// copy the requested data from kernel to user space
if ( copy_to_user(buffer, data_pool, bytesRead) ) {
// copy failed
printk(KERN_ALERT "failed to write to device\n");
return -EFAULT;
}
data_pool_size -= bytesRead;
file->f_pos += bytesRead;
printk(KERN_ALERT "read %u bytes from device\n", bytesRead);
printk(KERN_ALERT "data pool is now: %u bytes\n", data_pool_size);
return bytesRead;
} // sampledriver_read()
/*
* Write
*
* Write data to internal pool
*/
static ssize_t sampledriver_write(struct file *file, const char *buffer, size_t length, loff_t *offset)
{
// copy the requested data from user to kernel space
if ( copy_from_user(data_pool, buffer, length) ) {
// copy failed
printk(KERN_ALERT "failed to write to device\n");
return -EFAULT;
}
data_pool_size += length;
file->f_pos +=length;
printk(KERN_ALERT "written %u bytes to device\n", length);
printk(KERN_ALERT "data pool is now: %u bytes\n", data_pool_size);
return length;
} // sampledriver_write()
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Simon Nattrass");
MODULE_DESCRIPTION("A sample driver");
That's the code. Now to examine the interesting sections.
init_module()
During initialization, the code allocates a single page as the data_pool (4K on Intel architectures), and registers the device with the kernel, requesting a dynamic major number assignment. Memory management at the kernel level is beyond the scope of this document, see this page for further information.
cleanup_module()
The memory grabbed by init_module() is returned when the driver is removed.
read
The device reads data from kernel to user space via the copy_to_user() function. After having read the data the size of the data_pool it reduced accordingly and the pointer to the next byte to read is moved.
write
In addition to reading, the device can write data from user to kernel space with the copy_from_user() function. Again the pool size is adjusted, as is the file pointer ready for the next byte to write.
Module Licensing etc.
A device driver is expected to repost selected meta information about itself to the kernel, such as supported license for the module. This prevents the waring relating to ?tainting the kernel? previously observed when loading helloworld.o
| Try it Out! - sample_driver.c | |||
Having compiled the code, the module is loaded into the kernel and verified by examining /var/log/messages.
# insmod sample_driver.o # tail /var/log/messages Aug 25 14:41:03 exelon kernel: kernel assigned major number: 254 to sample_driver #
With the module loaded, we now create /dev/sample_driver with the major numbers.
# mknod sample_driver c 254 0 #
Write to the device and verify.
# cp foo.txt /dev/sample_driver # tail /var/log/messages opening sample_driver written 3897 bytes to device data pool is now: 3897 releasing sample_driver #
Read from the device and verify.
# cp /dev/sample_driver bar.txt # tail /var/log/messages opening sample_driver read 1024 bytes from device data pool is now: 2873 ... data pool is now: 0 releasing sample_driver #
Summary
Hopefully this whirlwind tour of device driver development has whetted the appetite for further research and provided a road map from which to begin. Within the bounds of this paper we have only begun to skim the surface of the subject, which is as interesting as it is involved.
References
- The Linux Kernel Module Programming Guide
- Linux Device Drivers, 2nd Edition, O'Reilly
- Write a Linux Hardware Device Driver
- Beginning Linux Programming, 3rd Edition, Wrox Press
- Linux Kernel Development, Developer's Library Press
Reader Comments
- Wonderful and its very useful for me t thanks a ton
Novell Cool Solutions (corporate web communities) are produced by WebWise Solutions. www.webwiseone.com
