分享

Linux Driver Tutorial: How to Write a Simple Linux Device Driver

 astrotycoon 2018-01-22

This Linux device driver tutorial will provide you with all the necessary information about how to write a device driver for Linux operating systems. This article includes a practical Linux driver development example that’s easy to follow. We’ll discuss the following:

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

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

Contents:

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

 

1. Overview

Linux has a monolithic kernel. For this reason, writing a device driver for Linux requires performing a combined compilation with the kernel. Another way around is to implement your driver as a kernel module, in which case you won’t need to recompile the kernel to add another driver. We’ll be concerned with this second option: kernel modules.

At its base, a module is a specifically designed object file. When working with modules, Linux links them to its kernel by loading them to its address space. The Linux kernel was developed using the C programming language and Assembler. C implements the main part of the kernel, 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 the Microsoft Windows operating system kernel, because some parts of the Linux kernel source code – header files, to be specific – may include keywords from C++ (for example, delete or new), while in Assembler we may encounter lexemes such as ‘ : : ’.

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

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

2. Loading and Unloading Modules

To create a simple sample module, we don’t need to do much work. Here’s some code that demonstrates this:

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

The only two things this module does is load and unload itself. 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. The my_init and my_exit functions must have identical signatures, which must be exactly as follows:

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

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

3. Registering a character device

The example module above is very simple; now we’re 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 into the kernel and how to interact with device files. These tools may be simple, but they come in handy for any driver, and to some extent, they make the kernel-mode development process richer.

For a start, here’s some useful information about device files. Commonly, you can find device files in the /dev folder. They facilitate interaction between the user and the 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’s read from a device file originates from the module serving this file. We can divide device files into two groups: character files and block files. Character files are non-buffered, whereas block files are buffered. As their names imply, character files allow you to read and write data character by character, while block files allow you to write only whole blocks of data. We’ll leave the discussion of block files out of the scope of this article, and will get straight to character files.

Linux systems have a way of identifying device files via major device numbers, which identify modules serving device files or a group of devices, and minor device numbers, which identify a specific device among a group of devices that a major device number specifies. In the driver code, we can define these numbers as constants or they can be allocated dynamically. In case a number defined as a constant has already been used, the system will return an error. When a number is allocated dynamically, the function reserves that number to prohibit it from being 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 register it, after which the device and the file_operations structure will be linked. If 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 value returned is zero, this signifies success, while a negative number signifies an error. Both device numbers are specified in the 0–255 range.

We pass the device name as a string value of the name parameter (this string can also pass the name of a module if 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 function pointers 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 functions that aren’t required, you can still use the file without implementing them. A pointer to an unimplemented 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 normally. In our case, we'll just implement the read function.

As we're going to ensure the operation of only a single type of device with our Linux driver, our file_operations structure will be global and static. Correspondingly, after it's created, we'll need to fill it 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'll 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 revoke 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 that contains the major device number. When the lifetime of the driver expires, this global variable will revoke the registration of the device file.

We've already listed and mentioned almost all functions, and the last one is the printk function. The declaration of this function is contained in the linux/kernel.h file, and its task is simple: to log kernel messages. You’ve no doubt 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. Levels range from the most insignificant KERN_DEBUG to the critical KERN_EMERG, alerting about kernel instability. This is the only difference between the printk function and the printf library function.

The printk function forms a string, which we write 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 is the overflow of the circular buffer, meaning that the oldest message is not recorded in the log.

The next step is writing a function for reverting the registration of the device file. If a device file is successfully registered, the value of the device_file_major_number will not be zero. This allows us to revoke the registration of the file using the nregister_chrdev function, which we declare in the linux/fs.h file. The major device number is the first parameter of this function, followed by a string containing the device name. The register_chrdev and the unresister_chrdev functions act in analogous ways.

To register 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're 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, the pointer to the file structure. This file structure allows us to get necessary information about the file with which we're working, details on private data related to this current file, and so on. The data that has been 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, and we start reading the bytes from a certain offset defined in the fourth parameter. After executing the function, the number of bytes that have been 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 that the read function must perform is to copy the information to this buffer. The address to which a pointer from that space points and the address in the kernel address space may have different values. That's why we cannot simply dereference the pointer. When working with these pointers, we have a set of specific macros and functions that 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 copies specific data from the kernel buffer to the buffer allocated in the user space. In addition, it also verifies if a pointer is valid and if the buffer size is large enough. Thus, errors in the driver can be processed relatively easily. Here's the code for the copy_to_user prototype:

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

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

This tutorial contains only an example of a Linux device driver without an actual device. If you don't need anything other than strings of text to be returned after reading a device file, then this will be enough.

Here's the code for implementing 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 a Kernel Module

After we've written the code for the driver, it's time to build it and see if it works as we expect. In the earlier kernel versions (such as 2.4), building a module required many more movements from a developer: the environment for compilation needed to be prepared individually and the compilation itself required the GCC compiler. Only after that would a developer receive an *.o file - a module that could be loaded to the kernel. Fortunately, these times are long gone and the process is much simpler now. Today, 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 this file, you need only to initiate the kernel build system:

obj-m := source_file_name.o

As you can see, here we've assigned 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:

make –C KERNEL_MODULE_BUILD_SYSTEM_FOLDER M=`pwd` modules

To clean up the build folder:

make –C KERNEL_MODULES_BUILD_SYSTEM_FOLDER M=`pwd` clean

The module build system is commonly located in /lib/modules/`uname -r`/build. Now it's time to prepare 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

Finally, we combine everything we've learned into one makefile:

TARGET_MODULE:=simple-module
# If we are running by kernel building system
ifneq ($(KERNELRELEASE),)
    $(TARGET_MODULE)-objs := main.o device_file.o
    obj-m := $(TARGET_MODULE).o
# If we running without kernel build system
else
    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
clean:
# run kernel build system to cleanup in current directory
    $(MAKE) -C $(BUILDSYSTEM_DIR) M=$(PWD) clean
load:
    insmod ./$(TARGET_MODULE).ko
unload:
    rmmod ./$(TARGET_MODULE).ko
endif

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

In our tutorial, we've 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 the built module:

#> make load

After executing this command, the name of the driver is added to the /proc/modules file, while the device that the module registers is added to the /proc/devices file. The added records look like this:

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

The first three records contain the name of the added device and the major device number with which it's 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've created the device file, we need to perform the final verification to make sure that what we've done works as expected. To verify, we can use the cat command to display the contents:

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

7. References

Download source code of Simple Linux Driver (zip 2.2 kB)

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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多