Linux Kernel

Basic Character Driver in Linux

We will go through the Linux way of implementing the character driver. We will first try to understand what the character driver is and how the Linux framework enables us to add the character driver. After that, we will do the sample test user space application. This test application uses the device node exposed by the driver for writing and reading the data from the kernel memory.

Description

Let us start the discussion with the character driver in Linux. Kernel categorizes the drivers into three categories:

Character Drivers – These are the drivers which doesn’t have too much of data to deal with. Few examples of character drivers are touch screen driver, uart driver, etc. These all are the character drivers since the data transfer is done through character by character.

Block Drivers – These are the drivers which deal with too much data. Data transfer is done block by block since too much of the data needs to be transferred. Example of block drivers are SATA, NVMe, etc.

Network Drivers – These are the drivers which functions in the network group of drivers. Here, data transfer is done in the form of data packets. Wireless drivers like Atheros comes under this category.

In this discussion, we will focus only on character driver.

As an example, we will take the simple read/write operations to understand the basic character driver. Generally, any device driver have these two minimum operations. Additional operation could be open, close, ioctl, etc. In our example, our driver has the memory in kernel space. This memory is allocated by the device driver and can be considered as the device memory since there is no hardware component involved. Driver creates the device interface in the /dev directory which can be used by the user space programs to access the driver and perform the operations supported by the driver. For the userspace program, these operations are just like any other file operations. The user space program has to open the device file to get the instance of the device. If the user wants to perform the read operation, the read system call can be used to do so. Similarly, if the user wants to perform the write operation, the write system call can be used to achieve the write operation.

Character Driver

Let us consider to implement the character driver with the read/write data operations.

We start with taking the instance of the device data. In our case, it is “struct cdrv_device_data”.

If we see the fields of this structure, we have cdev, device buffer, size of buffer, class instance, and device object. These are the minimum fields where we should implement the character driver. It depends on the implementer on which additional fields does he want to add to enhance the functioning of the driver. Here, we try to achieve the minimum functioning.

Next, we should create the object of the device data structure. We use the instruction to allocate the memory in static manner.

struct cdrv_device_data char_device[CDRV_MAX_MINORS];

This memory can also be allocated dynamically with “kmalloc”. Let us keep the implementation as simple as possible.

We should take the read and write functions implementation. The prototype of these two functions is defined by the device driver framework of Linux. The implementation of these functions needs to be user defined. In our case, we considered the following:

Read: The operation to get the data from the driver memory to the userspace.

static ssize_t cdrv_read(struct file *file, char __user *user_buffer, size_t size, loff_t *offset);

Write: The operation to store the data to the driver memory from the userspace.

static ssize_t cdrv_write(struct file *file, const char __user *user_buffer, size_t size, loff_t * offset);

Both of the operations, read and write, need to be registered as a part of struct file_operations cdrv_fops. These are registered to the Linux device driver framework in the init_cdrv() of the driver. Inside the init_cdrv() function, all the setup tasks are performed. Few tasks are as follows:

  • Create class
  • Create device instance
  • Allocate major and minor number for the device node

The complete example code for the basic character device driver is as follows:

#include <linux/fs.h>

#include <linux/cdev.h>

#include <linux/init.h>

#include <linux/device.h>

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/uaccess.h>

#define CDRV_MAJOR       42
#define CDRV_MAX_MINORS  1
#define BUF_LEN 256
#define CDRV_DEVICE_NAME "cdrv_dev"
#define CDRV_CLASS_NAME "cdrv_class"

struct cdrv_device_data {
    struct cdev cdev;
    char buffer[BUF_LEN];
    size_t size;
    struct class*  cdrv_class;
    struct device* cdrv_dev;
};

struct cdrv_device_data char_device[CDRV_MAX_MINORS];
static ssize_t cdrv_write(struct file *file, const char __user *user_buffer,
                    size_t size, loff_t * offset)
{
    struct cdrv_device_data *cdrv_data = &char_device[0];
    ssize_t len = min(cdrv_data->size - *offset, size);
    printk("writing:bytes=%d\n",size);
    if (len buffer + *offset, user_buffer, len))
        return -EFAULT;

    *offset += len;
    return len;
}

static ssize_t cdrv_read(struct file *file, char __user *user_buffer,
                   size_t size, loff_t *offset)
{
    struct cdrv_device_data *cdrv_data = &char_device[0];
    ssize_t len = min(cdrv_data->size - *offset, size);

    if (len buffer + *offset, len))
        return -EFAULT;

    *offset += len;
    printk("read:bytes=%d\n",size);
    return len;
}
static int cdrv_open(struct inode *inode, struct file *file){
   printk(KERN_INFO "cdrv: Device open\n");
   return 0;
}

static int cdrv_release(struct inode *inode, struct file *file){
   printk(KERN_INFO "cdrv: Device closed\n");
   return 0;
}

const struct file_operations cdrv_fops = {
    .owner = THIS_MODULE,
    .open = cdrv_open,
    .read = cdrv_read,
    .write = cdrv_write,
    .release = cdrv_release,
};
int init_cdrv(void)
{
    int count, ret_val;
    printk("Init the basic character driver...start\n");
    ret_val = register_chrdev_region(MKDEV(CDRV_MAJOR, 0), CDRV_MAX_MINORS,
                                 "cdrv_device_driver");
    if (ret_val != 0) {
        printk("register_chrdev_region():failed with error code:%d\n",ret_val);
        return ret_val;
    }

    for(count = 0; count < CDRV_MAX_MINORS; count++) {
        cdev_init(&char_device[count].cdev, &cdrv_fops);
        cdev_add(&char_device[count].cdev, MKDEV(CDRV_MAJOR, count), 1);
        char_device[count].cdrv_class = class_create(THIS_MODULE, CDRV_CLASS_NAME);
        if (IS_ERR(char_device[count].cdrv_class)){
             printk(KERN_ALERT "cdrv : register device class failed\n");
             return PTR_ERR(char_device[count].cdrv_class);
        }
        char_device[count].size = BUF_LEN;
        printk(KERN_INFO "cdrv device class registered successfully\n");
        char_device[count].cdrv_dev = device_create(char_device[count].cdrv_class, NULL, MKDEV(CDRV_MAJOR, count), NULL, CDRV_DEVICE_NAME);

    }

    return 0;
}

void cleanup_cdrv(void)
{
    int count;

    for(count = 0; count < CDRV_MAX_MINORS; count++) {
        device_destroy(char_device[count].cdrv_class, &char_device[count].cdrv_dev);
        class_destroy(char_device[count].cdrv_class);
        cdev_del(&char_device[count].cdev);
    }
    unregister_chrdev_region(MKDEV(CDRV_MAJOR, 0), CDRV_MAX_MINORS);
    printk("Exiting the basic character driver...\n");
}
module_init(init_cdrv);
module_exit(cleanup_cdrv);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Sushil Rathore");
MODULE_DESCRIPTION("Sample Character Driver");
MODULE_VERSION("1.0");

We create a sample makefile to compile the basic character driver and test app. Our driver code is present in crdv.c and the test app code is present in cdrv_app.c.

obj-m+=cdrv.o
all:
        make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules
        $(CC) cdrv_app.c -o cdrv_app
clean:
        make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean
        rm cdrv_app
~

After the issuing is made to the makefile, we should get the following logs. We also get the cdrv.ko and executable(cdrv_app) for our test app:

root@haxv-srathore-2:/home/cienauser/kernel_articles# make
make -C /lib/modules/4.15.0-197-generic/build/ M=/home/cienauser/kernel_articles modules
make[1]: Entering directory '/usr/src/linux-headers-4.15.0-197-generic'
  CC [M]  /home/cienauser/kernel_articles/cdrv.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/cienauser/kernel_articles/cdrv.mod.o
  LD [M]  /home/cienauser/kernel_articles/cdrv.ko
make[1]: Leaving directory '/usr/src/linux-headers-4.15.0-197-generic'
cc cdrv_app.c -o cdrv_app

Here is the sample code for the test app. This code implements the test app which opens the device file created by the cdrv driver and writes the “test data” to it. Then, it reads the data from the driver and prints it after reading the data to be printed as “test data”.

#include<stdio.h>

#include <fcntl.h>

#define DEVICE_FILE "/dev/cdrv_dev"

char *data = "test data";

char read_buff[256];

int main()

{

        int fd;
        int rc;
        fd = open(DEVICE_FILE, O_WRONLY , 0644);
        if(fd<0)
        {
                perror("opening file:\n");
                return -1;
        }
        rc = write(fd,data,strlen(data)+1);
        if(rc<0)
        {
                perror("writing file:\n");
                return -1;
        }
        printf("written bytes=%d,data=%s\n",rc,data);
        close(fd);
        fd = open(DEVICE_FILE, O_RDONLY);
        if(fd<0)
        {
                perror("opening file:\n");
                return -1;
        }
        rc = read(fd,read_buff,strlen(data)+1);
        if(rc<0)
        {
                perror("reading file:\n");
                return -1;
        }
        printf("read bytes=%d,data=%s\n",rc,read_buff);
        close(fd);
        return 0;

}

Once we have all the stuff in place, we can use the following command to insert the basic character driver to the Linux kernel:

root@haxv-srathore-2:/home/cienauser/kernel_articles# insmod cdrv.ko

root@haxv-srathore-2:/home/cienauser/kernel_articles#

After inserting the module, we get the following messages with dmesg and get the device file created in /dev as /dev/cdrv_dev:

root@haxv-srathore-2:/home/cienauser/kernel_articles# dmesg

[ 160.015595] cdrv: loading out-of-tree module taints kernel.

[ 160.015688] cdrv: module verification failed: signature and/or required key missing - tainting kernel

[ 160.016173] Init the basic character driver...start

[ 160.016225] cdrv device class registered successfully

root@haxv-srathore-2:/home/cienauser/kernel_articles#

Now, execute the test app with the following command in the Linux shell. The final message prints the read data from the driver which is exactly the same as what we wrote in the write operation:

root@haxv-srathore-2:/home/cienauser/kernel_articles# ./cdrv_app

written bytes=10,data=test data

read bytes=10,data=test data

root@haxv-srathore-2:/home/cienauser/kernel_articles#

We have few additional prints in the write and read path which can be seen with the help of the dmesg command. When we issue the dmesg command, we get the following output:

root@haxv-srathore-2:/home/cienauser/kernel_articles# dmesg

[ 160.015595] cdrv: loading out-of-tree module taints kernel.

[ 160.015688] cdrv: module verification failed: signature and/or required key missing - tainting kernel

[ 160.016173] Init the basic character driver...start

[ 160.016225] cdrv device class registered successfully

[ 228.533614] cdrv: Device open

[ 228.533620] writing:bytes=10

[ 228.533771] cdrv: Device closed

[ 228.533776] cdrv: Device open

[ 228.533779] read:bytes=10

[ 228.533792] cdrv: Device closed

root@haxv-srathore-2:/home/cienauser/kernel_articles#

Conclusion

We have gone through the basic character driver which implements the basic write and read operations. We also discussed the sample makefile to compile the module along with the test app. The test app was written and discussed to perform the write and read operations from the user space. We also demonstrated the compilation and execution of the module and test app with logs. The test app writes few bytes of test data and then reads it back. The user can compare both the data to confirm the correct functioning of the driver and test app.

About the author

Sushil Rathore

Sushil Rathore is having hands-on experience in Linux Platform SW. He's an expert of Linux on ARM/X86 Boards. He has very good understanding on Bootloaders and other platform softwares. He has good industrial experience and have worked in reputed Organizations. Currently he is associated with a reputed firm in the networking domain.