Table of Content

Acknowledgement

This tutorial is an exerpt from the “The Linux Kernel Module Programming Guide” maintained by Bob Mottram.

Experiment and Programming Environment

Debian Linux System

We tested the code shown here in a Debian Linux system with kernerl version 4.19. To build and install a Debian Linux system, please refer to the bootstrap tutorial.

Linux Packages for Kernel Module Development

Become the root and run the following to install necessary packages,

apt-get install -y build-essential kmod linux-headers-$(uname -r)

Writing a Simple Character Device Driver

Device Driver Source Code (chardev.c)

In Linux, we typically write a device driver as a loadable kernel module. Below is the source code a simple character device driver. Create chardev.c file and copy the following to the file.

/*
 *  chardev.c: Creates a read-only char device that says how many times
 *  you've read from the dev file
 */

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/irq.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <linux/poll.h>
#include <linux/cdev.h>

/*
 *  Prototypes - this would normally go in a .h file
 */
int init_module(void);
void cleanup_module(void);
static int device_open(struct inode *, struct file *);
static int device_release(struct inode *, struct file *);
static ssize_t device_read(struct file *, char *, size_t, loff_t *);
static ssize_t device_write(struct file *, const char *, size_t, loff_t *);

#define SUCCESS 0
#define DEVICE_NAME "chardev"   /* Dev name as it appears in /proc/devices   */
#define BUF_LEN 80              /* Max length of the message from the device */

/*
 * Global variables are declared as static, so are global within the file.
 */

static int Major;               /* Major number assigned to our device driver */
static int Device_Open = 0;     /* Is device open?
                                 * Used to prevent multiple access to device */
static char msg[BUF_LEN];       /* The msg the device will give when asked */
static char *msg_Ptr;

static struct class *cls;

static struct file_operations chardev_fops = {
    .read = device_read,
    .write = device_write,
    .open = device_open,
    .release = device_release
};

/*
 * This function is called when the module is loaded
 */
int init_module(void)
{
    Major = register_chrdev(0, DEVICE_NAME, &chardev_fops);

    if (Major < 0) {
        pr_alert("Registering char device failed with %d\n", Major);
        return Major;
    }

    pr_info("I was assigned major number %d.\n", Major);

    cls = class_create(THIS_MODULE, DEVICE_NAME);
    device_create(cls, NULL, MKDEV(Major, 0), NULL, DEVICE_NAME);

    pr_info("Device created on /dev/%s\n", DEVICE_NAME);

    return SUCCESS;
}

/*
 * This function is called when the module is unloaded
 */
void cleanup_module(void)
{
    device_destroy(cls, MKDEV(Major, 0));
    class_destroy(cls);

    /*
     * Unregister the device
     */
    unregister_chrdev(Major, DEVICE_NAME);
}

/*
 * Methods
 */

/*
 * Called when a process tries to open the device file, like
 * "cat /dev/mycharfile"
 */
static int device_open(struct inode *inode, struct file *file)
{
    static int counter = 0;

    if (Device_Open)
        return -EBUSY;

    Device_Open++;
    sprintf(msg, "I already told you %d times Hello world!\n", counter++);
    msg_Ptr = msg;
    try_module_get(THIS_MODULE);

    return SUCCESS;
}

/*
 * Called when a process closes the device file.
 */
static int device_release(struct inode *inode, struct file *file)
{
    Device_Open--;          /* We're now ready for our next caller */

    /*
     * Decrement the usage count, or else once you opened the file, you'll
     * never get get rid of the module.
     */
    module_put(THIS_MODULE);

    return SUCCESS;
}

/*
 * Called when a process, which already opened the dev file, attempts to
 * read from it.
 */
static ssize_t device_read(struct file *filp,   /* see include/linux/fs.h   */
                           char *buffer,        /* buffer to fill with data */
                           size_t length,       /* length of the buffer     */
                           loff_t * offset)
{
    /*
     * Number of bytes actually written to the buffer
     */
    int bytes_read = 0;

    /*
     * If we're at the end of the message,
     * return 0 signifying end of file
     */
    if (*msg_Ptr == 0)
        return 0;

    /*
     * Actually put the data into the buffer
     */
    while (length && *msg_Ptr) {

        /*
         * The buffer is in the user data segment, not the kernel
         * segment so "*" assignment won't work.  We have to use
         * put_user which copies data from the kernel data segment to
         * the user data segment.
         */
        put_user(*(msg_Ptr++), buffer++);

        length--;
        bytes_read++;
    }

    /*
     * Most read functions return the number of bytes put into the buffer
     */
    return bytes_read;
}

/*
 * Called when a process writes to dev file: echo "hi" > /dev/hello
 */
static ssize_t device_write(struct file *filp,
                            const char *buff,
                            size_t len,
                            loff_t * off)
{
    pr_alert("Sorry, this operation isn't supported.\n");
    return -EINVAL;
}

MODULE_LICENSE("GPL");

Makefile

To compile the device driver kernel module, we create a file called Makefile.

obj-m += chardev.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Note that the “spaces” before the lines led by make must be tabs.

Compiling the Device Driver

To compile the device driver kernel module, type

make

Testing the Kernel Module

Listing Loaded Kernel Modules

Before we load the kernel module, we run lsmod and observe kernel modules that have been loaded,

lsmod

Loading the Kernel Module

Next, we load the chardev kernel module using insmod. You must do this as root

insmod chardev.ko

Examining Whether the Kernle Module is Loaded

We then examine kernel modules that have been loaded again,

lsmod 

The result can be lenthy and cumbersome to locate the line that contains “chardev”. We can use grep to make this task earier,

lsmod | grep chardev

The following is an example output,

$ lsmod | grep chardev
chardev                16384  0

In addition, we can also view system logs. In Debian Linux, we view relevant system logs in the /var/log/syslog file. You can only view this file as root. Switch to root and then run or use sudo to run the following

tail -f /var/log/syslog

Note that tail only show you the “tail” of the file. You can also use an editor to view the /var/log/syslog file. The following is an example of the above command,

# tail -f /var/log/syslog
Feb 10 13:33:54 debian10r32 kernel: [ 4525.232319] I was assigned major number 246.
Feb 10 13:33:54 debian10r32 kernel: [ 4525.232836] Device created on /dev/chardev

Unloading the Kernel Module

Sometimes you want to remove (or unload) the kernel module, e.g., you revise the kernel module source code, recompiled it, and wants to reload it. To remove the kernel module, run the following as root (or using sudo),

rmmod chardev

Testing the Device Driver

We write a simple C program called chardevclient.c to access the “character device” via the device driver,

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
        int fd, c, rtn;
        fd = open("/dev/chardev", O_RDWR);
        if (fd == -1) {
                perror("open /dev/chardev");
                exit(EXIT_FAILURE);
        }

        printf("Reading from /dev/chardev: \n");
        while ((rtn = read(fd, &c, 1)) > 0) {
                printf("%c", c);
        }
        if (rtn == -1) {
                perror("reading /dev/chardev");
        } else {
                printf("\n");
        }

        printf("Writing to /dev/chardev: \n");
        c = 'h';
        while ((rtn = write(fd, &c, 1)) > 0) {
                printf("wrote %c\n", c);
        }
        if (rtn == -1) {
                perror("writing /dev/chardev");
        }

        exit(EXIT_SUCCESS);
}

Add the following lines to the Makefile,

chardevclient:
	cc -Wall chardevclient.c -o chardevclient

To compile, run

make

To run the chardevclient program, switch to root and type (or run it using sudo)

./chardevclient

What do you observe? Can you explain what you observe?

Further Studying

It is worth studying the “The Linux Kernel Module Programming Guide” completely.

To read the guide, first figure out the Linux kernel version by using the command below,

uname -r

Next, go to the guide and find the directory that best matches your kernel version. For instance, I observe the uname -r command outputs the following,

4.19.0-6-686

The directory in the guide that best matches the kernel version number is

4.17.2

under

older_versions