Novell Home

Creating SUSE Linux Device Drivers

Novell Cool Solutions: Feature
By Simon Nattrass

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

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


Novell Cool Solutions (corporate web communities) are produced by WebWise Solutions. www.webwiseone.com

© 2014 Novell