Subscribe to receive all latest blog updates

If you are searching for a good Linux driver tutorial, this article will provide the necessary information about how to write a device driver for this OS. This article includes a practical Linux driver development example, which is easy to follow. We will discuss the following:

  • Kernel logging system
  • How to work with character devices
  • Working with user level memory from the kernel

We’ll use Linux kernel version 2.6.32. We could also use other versions, but their API can be modified and thus can be different from the API used in our examples and build system. After studying this tutorial, you will be acquainted with the process of writing a device driver for Linux operating system – a kernel module.


1. Overview
2. Loading and unloading modules
3. Registering character device
4. Using memory allocated in user mode
5. Build system of kernel module
6. Loading and using module
7. References


1. Overview

Linux represents a monolithic kernel. For this reason, writing device driver in Linux requires to perform a combined compilation with the kernel. As another way around, we need to implement it as a kernel module: thus there will be no need to recompile the kernel when there is a necessity to add another driver. We will be concerned exactly with that: kernel modules.

At its base, a module represents a specifically designed object file. When working with modules, Linux links them to itself by loading them to its address space. The Linux kernel was developed using the C programming language and Assembler. C implements its main part and Assembler implements parts that depend on the architecture. Unfortunately, these are the only two languages we can use for device driver programming in Linux. We cannot use C++, which is used for Microsoft Windows operating system kernel, because some parts of the kernel source code – header files, to be specific – may include specific key words from C++ (for example, delete or new), while in Assembler you may encounter lexemes like ‘ : : ’.

We run the module code in the kernel context. This requires a developer to be much more attentive as extra responsibilities arise: if a developer makes a mistake during the implementation of a user-level application, this will not cause problems outside the user application in most cases, but if a developer makes such a mistake during the implementation of a kernel module, the consequences will be problems on a system level. Luckily for us, the Linux kernel has a specifics of being resistant to errors in the code of modules. When the kernel encounters non-critical errors (for example, null pointer dereferencing), you will see the oops message (insignificant malfunctions during Linux operation are called oops), after which the malfunctioning module is unloaded, allowing the kernel and other modules to work as usual. In addition, you will also be able to find a record in the kernel log, which precisely describes the error. But be aware that continuing work after the oops message is not recommended as this may lead to instability and kernel panic.

Basically, the kernel and its modules represent a single program module. So keep in mind that a single program module uses a single global name space. In order to minimize it, you must watch what is being exported by the module: the exported global characters must be named uniquely (as a commonly used workaround, you can simply use the name of the module exporting the characters as a prefix) and must be cut to the bare minimum.

2. Loading and unloading modules

To create the simplest sample module, we don’t need to do much work. Here is the code sample that demonstrates that:

  #include <linux/init.h>
  #include <linux/module.h>
  static int my_init(void)
                         return  0;
  static void my_exit(void)

The only two things this module does are module loading and its unloading. To load a Linux driver, we call the my_init function, and to unload it, we call the my_exit function. The module_init and module_exit macros notify the kernel about driver loading and unloading. Both the my_init and my_exit function must have identical signatures, which must be exactly as follows:

int init(void);
void exit(void);

If the module requires certain kernel version and must include the information on the version, we need to link the linux/module.h header file. Loading a module built for another version of the kernel will lead to the Linux OS prohibiting its loading. There is a reason for such behavior: the updates to the kernel API are released quite often and when you call a module function, whose signature was changed, you will cause damages to the whole stack. The module_init and module_exit macros are declared in the linux/init.h header file.

3. Registering character device

The module cited above is very simple, and we are going to work with something more complex. Nevertheless one of the purposes of this short Linux kernel driver tutorial is to show how to work with logging in to the kernel and how to interact with device files. However simple these tools may be, they may come in handy for any driver, and to some extent, they make such kernel-mode development process richer.

For the start, here is some useful information about the device files. Commonly, you can find these files in the /dev/ folder hierarchy. They facilitate the interaction between the user and kernel code. If the kernel must receive anything, you can just write it to a device file to pass it to the module serving this file; anything that is read from a device file originates from a module serving this file. We can divide device files into two groups: the character files and the block files. The character files are non-buffered, the block files are buffered. As their names imply, the former allow reading and writing data to them character-by-character, while the latter allow it for whole blocks of data. We will leave the discussion of the block files out of the scope of topics for this article and will get straight to the character files.

Linux OS has a way of identifying device files via major device numbers, which identify modules serving device files or a group of devices it servers, and minor device numbers, which identify a specific device from a group of devices, which the major device number specifies. In the driver code, we may define these numbers as constants when writing Linux device drivers, or they can be allocated dynamically. In case a number defined as a constant is already used, an error will be returned by the system. In case a number is allocated dynamically, the function reserves the corresponding number to prohibit it to be used by anything else.

The function cited below is used for registering character devices:

int register_chrdev (unsigned int   major,
                     const char *   name,
                     const struct   fops);
                     file_operations * 

Here we specify the name and major number of a device to perform its registration, after which the device and the file_operations structure become linked. In case we assign zero to the major parameter, the function will allocate a major device number (i.e. the value it returns) on its own. If the returned value is zero, it signifies that completion is successful, and a negative number signifies an error. Both device numbers are specified in the 0–255 range.

We pass the device name in the string value of the name parameter (this string can also pass the name of a module in case it registers a single device). We then use this string to identify a device in the /sys/devices file. Device file operations such as read, write, and save are processed by the functions pointers to which are stored within the file_operations structure. These functions are implemented by the module and the pointers to the module structure identifying this module are also stored within the file_operations structure. Here you can see the 2.6.32 kernel version structure:

struct file_operations {
       struct module *owner;
       loff_t (*llseek) (struct file *, loff_t, int);
       ssize_t (*read) (struct file *, char *, size_t, loff_t *);
       ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
       int (*readdir) (struct file *, void *, filldir_t);
       unsigned int (*poll) (struct file *, struct poll_table_struct *);
       int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
       int (*mmap) (struct file *, struct vm_area_struct *);
       int (*open) (struct inode *, struct file *);
       int (*flush) (struct file *);
       int (*release) (struct inode *, struct file *);
       int (*fsync) (struct file *, struct dentry *, int datasync);
       int (*fasync) (int, struct file *, int);
       int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*readv) (struct file *, const struct iovec *, unsigned long,
          loff_t *);
    ssize_t (*writev) (struct file *, const struct iovec *, unsigned long,
          loff_t *);

If the file_operations structure contains some not required functions, you can still use the file without their implementation. A pointer to a not implemented function can simply be set to be zero. After that, the system will take care of the implementation of the function and make it behave in some standard way. In our case, we will just implement the read function.

As we are going to ensure the operation of only a single type of devices with our Linux driver, our file_operations structure will be global and static. Correspondingly, after it is created, we need to perform its filling statically. Here you can see how this is done:

static struct file_operations simple_driver_fops = 
    .owner   = THIS_MODULE,
    .read    = device_file_read,

The declaration of the THIS_MODULE macro is contained in the linux/module.h header file. We transform the macro into the pointer to the module structure of the required module. A bit later we will get to writing the body of the function with a prototype, but right now we have only the pointer to it, which is device_file_read.

 ssize_t device_file_read (struct file *, char *, size_t, loff_t *);

The file_operations structure allows us to write several functions that will perform and revert the registration of the device file.

    static int device_file_major_number = 0;
static const char device_name[] = "Simple-driver";
static int register_device(void)
        int result = 0;
        printk( KERN_NOTICE "Simple-driver: register_device() is called." );
        result = register_chrdev( 0, device_name, &simple_driver_fops );
        if( result < 0 )
            printk( KERN_WARNING "Simple-driver:  can\'t register character device with errorcode = %i", result );
            return result;
        device_file_major_number = result;
        printk( KERN_NOTICE "Simple-driver: registered character device with major number = %i and minor numbers 0...255"
             , device_file_major_number );
        return 0;

The device_file_major_number is a global variable, which – as its name states – contains the major device number. When the life time of the driver expires, this global variable will revert the registration of the device file.

We have already listed and mentioned almost all functions, the last one is the printk() function. Its declaration is contained in the linux/kernel.h file and its task is simple: it just logs the kernel messages. You must have paid attention to the KERN_NOTICE and KERN_WARNING prefixes, which are present in all listed format strings of printk. As you might have guessed, NOTICE and WARNING signify the priority level of a message. The levels range from the most insignificant KERN_DEBUG to the critical KERN_EMERG alerting about the kernel instability. This is the only difference between the printk() function and the printf library function.

The printk function forms a string. After that we write it to the circular buffer, where the klog daemon reads it and sends it to the system log. The implementation of the printk function allows it to be called from anywhere in the kernel. The worst case scenario here is the overflow of the circular buffer: it means that the oldest message is not recorded in the log.

Next step is writing a function for reverting the registration of the device file. According to its logic, in case of a successful registration of the device file, the value of the device_file_major_number will not be zero. This allows us to revert the registration of the file using the nregister_chrdev function, which we declare in the linux/fs.h file. The major device number is its first parameter followed by the string containing device name. The register_chrdev and the unresister_chrdev functions act in analogous ways.

To perform the registration of a device, we use the following code:

void unregister_device(void)
    printk( KERN_NOTICE "Simple-driver: unregister_device() is called" );
    if(device_file_major_number != 0)
        unregister_chrdev(device_file_major_number, device_name);

4. Using memory allocated in user mode

The function we are going to write will read characters from a device. The signature of this function must be appropriate for that from the file_operations structure:

 ssize_t (*read) (struct file *, char *, size_t, loff_t *);

Let’s have a look at the first parameter: it is the pointer to the file structure. This file structure allows us to get the necessary information: the file with which we work, details on private data related to the current file, and so on. The data that was read is allocated to the user space using the second parameter, which is a buffer. The number of bytes for reading is defined in the third parameter; we start reading the bytes from a certain offset defined in the fourth parameter. After an execution of the function, the number of bytes that were successfully read must be returned, after which the offset must be refreshed.

The user allocates a special buffer in the user-mode address space. And the other action, which the read function must perform, is to copy the information to this buffer. The address to which a pointer from that space refers and the address in the kernel address space may have different values. That is why we cannot simply dereference the pointer. When working with these pointers, we have a set of specific macros and functions, which we declare in the asm/uaccess.h file. The most suitable function in our case is copy_to_user(). Its name speaks for itself: it simply transfers specific data from the kernel buffer to the buffer allocated in the user space by copying it. In addition, it also verifies if a pointer is valid and if the buffer size is big enough. Thus the errors in the driver can be processed relatively easy. Here is the code of the copy_to_user prototype:

 long copy_to_user( void __user *to, const void * from, unsigned long n );

First of all, the function must receive three pointers as parameters: the first one to the buffer, the second one to the data source, and the third one to the number of bytes for copying. As it was mentioned, an error returns a value other than zero, and in the case of a successful execution, it will be zero. The function contains the _user macro, whose task is to perform documenting process. It has another useful application, which allows us to analyze if the code uses the pointers from the address space correctly; this is done using the sparse analyzer, which performs the analysis of static code. Make sure to always mark the user address space pointers as _user.

As it was mentioned, this tutorial contains only an example of Linux device driver without an actual device. In case you do not need something other than strings of text to be returned after device file reading, this will be enough.

Here is the code for the implementation of the read function:

static const char    g_s_Hello_World_string[] = "Hello world from kernel mode!\n\0";
static const ssize_t g_s_Hello_World_size = sizeof(g_s_Hello_World_string);
static ssize_t device_file_read(
                        struct file *file_ptr
                       , char __user *user_buffer
                       , size_t count
                       , loff_t *position)
    printk( KERN_NOTICE "Simple-driver: Device file is read at offset = %i, read bytes count = %u"
                , (int)*position
                , (unsigned int)count );
    /* If position is behind the end of a file we have nothing to read */
    if( *position >= g_s_Hello_World_size )
        return 0;
    /* If a user tries to read more than we have, read only as many bytes as we have */
    if( *position + count > g_s_Hello_World_size )
        count = g_s_Hello_World_size - *position;
    if( copy_to_user(user_buffer, g_s_Hello_World_string + *position, count) != 0 )
        return -EFAULT;    
    /* Move reading position */
    *position += count;
    return count;

5. Build system of kernel module

So after we have written the code for the driver, it is time to build it and see if it works as we expect. In the earlier kernel versions (like 2.4), the building of a module required much more body movements from a developer: the environment for compilation must have been prepared personally and the compilation itself required the GCC compiler. Only after that a developer would receive an *.o file, which was a module that could be loaded to the kernel. Fortunately, these times are long gone and the process is much simpler now. Now much of the work is done by the makefile: it starts the kernel build system and provides the kernel with the information about the components required to build the module. A module built from a single source file requires a single string in the makefile. After creating the file you need only to initiate the kernel build system:

 obj-m := source_file_name.o

As you can see, here we assign the source file name to the module, which will be a *.ko file.

Correspondingly, if there are several source files, only two strings are required:

obj-m := module_name.o 
module_name-objs := source_1.o source_2.o … source_n.o

The make command initializes the kernel build system:

To build the module:


To clean up the build folder:


The module build system is commonly located in /lib/modules/`uname -r`/build. Now for the preparations of the module build system. To build the first module, execute the following command from the folder where the build system is located:

#> make modules_prepare

And finally we combine everything we learned into one makefile:

# If we are running by kernel building system
    $(TARGET_MODULE)-objs := main.o device_file.o
    obj-m := $(TARGET_MODULE).o
# If we running without kernel build system
    BUILDSYSTEM_DIR:=/lib/modules/$(shell uname -r)/build
    PWD:=$(shell pwd)
all : 
# run kernel build system to make module
    $(MAKE) -C $(BUILDSYSTEM_DIR) M=$(PWD) modules
# run kernel build system to cleanup in current directory
    $(MAKE) -C $(BUILDSYSTEM_DIR) M=$(PWD) clean
    insmod ./$(TARGET_MODULE).ko
    rmmod ./$(TARGET_MODULE).ko

The load target loads the build module and the unload target deletes it from the kernel.

In our tutorial, we used code from main.c and device_file.c to compile the driver. The resulting driver is named simple-module.ko.

6. Loading and using module

The following command executed from the source file folder allows us to load and the built module:

#> make load

After the execution of this command, the name of the driver is added to the /proc/modules file, while the device, which the module registers, is added to the /proc/devices file. The added records look as follows:

Character devices:
1 mem
4 tty
4 ttyS
250 Simple-driver

The first tree records contain the name of added device and the major device number, with which it is associated. The minor number range (0–255) allows the device files to be created in the /dev virtual file system.

#> mknod /dev/simple-driver c  250 0

After we have created the device file, we need to perform the final verification to make sure that what we have done works as expected. Use the cat command to display the content:

  $> cat /dev/simple-driver
  Hello world from kernel mode!

7. References

Download source of Simple Linux Driver (zip, 2,2 KB)

We hope this tutorial will come in handy. You can learn more about Apriorit driver development.