Simple Character Device Driver

Categories: device driver, Linux, Linux Kernel

It has been nearly 5 years since I did any work on device drivers. Also since then I have been off C or its sibling languages (on Python now-a-days), I thought first lemme get comfortable with linux kernel (& C, of course) before I can even dream of getting back to my core. In that line, I wrote a very simple character device driver which upon read returns a character buffer with every bit turned to one.

In other words, the idea is simple, our (virtual) device is almost like an inverse of (or a not of, if you prefer) of /dev/zero. Reading from this device would result in every bit turning one. For instance, if we read 1 Byte of data from /dev/ones & store it in file ‘out’:

$> dd if=/dev/ones of=out bs=1 count=1
1+0 records in
1+0 records out
1 byte (1 B) copied, 0.000142743 s, 7.0 kB/s

Then dump the content of ‘out’ file in hex mode, we should get:

$> od -t x1 out
0000000 ff
0000001

Download the entire code.

What & Why a character device driver?

Lemme answer the why? Because it is the easiest! πŸ™‚ No complication and a virtual character device can be as limited in its capabilities as you can think of. So, now what is a character device? It is where:

  • Read/write arbitrary bytes at a time
  • Generally seek is _not_ supported. (Generally!)
  • Very importantly, works on streams of bytes (or data, if you like). In other words, it does _NOT_ support buffered I/O. So every R/W is directly sent to (or from) the device.

Examples of character devices are printers, mice, keyboards & likewise.
Examples of pseudo(or virtual) character devices in Linux are /dev/null, /dev/zero, /dev/random & similar devices.

Basics

I am revisiting so lemme just wanna throw some light to the forgotten corners of my knowledge. Writing a device driver (at least, in Linux) is like writing any C – program. The only difference is that the Linux kernel kinda provides a framework and set of libraries that the driver code should adhere to. Very similar to, say, .NET framework or MFC. Linux kernel subsystem expects the driver to:

  1. define the entry & exit points.
  2. implement a set of operations (typically, file operations)
  3. register with the subsystem.
  4. create the device file

Entry & Exit points

These are like constructor & destructor available in many OOP languages like C++ or Java. Unlike C++, these entry points are mandatory. These are called when a kernel module is loaded. (Note: I said kernel module & not a driver!)

A device driver is a kernel module which talks (or at least, can talk to) a hardware device.
A kernel module is just any pluggable piece of code that can be inserted into the Linux subsystem (or framework, if that helps you).

But for this blog let us not worry about the nitty-gritty and the words kernel module and driver are interchangeably used.
So, a simple kernel module, which does nothing, is as follow:

#include <linux/module.h>

static int __init
one_init(void)
{
    return 0;
}

static void __exit
one_exit(void)
{
    return;
}

module_init(one_init);
module_exit(one_exit);

Suppose you name this as ones.c. You can compile the above using a Makefile as defined below:

TARGET_MODULE := ones
BUILDSYSTEM_DIR := '/lib/modules/${shell uname -r}/build'
PWD := $(shell pwd)

obj-m := $(TARGET_MODULE).o

default: test
    ${MAKE} -C ${BUILDSYSTEM_DIR} SUBDIRS=${PWD} modules

clean:
    ${MAKE} -C ${BUILDSYSTEM_DIR} SUBDIRS=${PWD} clean

Don’t worry about the details of the Makefile. Just know & note that the kernel module is compile with Kernel header files (at least). So you gotta download the kernel header files in accordance to your OS flavor. Once you compile, you should get a few additional file of which ‘ones.ko’ is of importance to us & this is the kernel module (or Kernel Object as the extension suggests). The module related Linux commands are:

#insert the kernel module into the kernel subsystem
$> insmod ones.ko

# list to verify
$> lsmod | grep ones
ones 19598 0

# details on the module
$> modinfo ./ones.ko
filename: ./ones.ko
author: Rp <rp@meetrp.com>
description: This device driver turns on every bit to '1'
license: GPL
srcversion: C5FDECEF298D041105E333F
depends:
vermagic: 2.6.32-358.18.1.el6.x86_64 SMP mod_unload modversions

# remove the module from kernel subsystem
$> rmmod ones

Implement of file operations

Let us add more meat to the simple module. Let us develop this simple kernel module into a character device driver. Now, what are our requirements: “Read should result in every bit turned to one. ” This means the use should be able to open, read and close our virtual device. As mentioned above, Linux kernel subsystem provides a framework to define & implement these operations. The are defined under ‘struct file_operations,’ which holds pointers to functions defined by the driver to perform various operations on the device. In other words, Linux kernel subsystem uses the file_operation structure to access driver’s functions. So, continuing with our driver, the structure is populated thus:

static struct file_operations fops = {
    .owner = THIS_MODULE, // pointer to the module struct
    .open = one_open,     // address of one_open
    .release = one_close, // address of one_close
    .read = one_read,     // address of one_read
};

Don’t worry about the “.owner,” part of it. You can safely ignore it as well. Now these function declarations (or prototypes) are available within linux/fs.h. Also the prototypes for the functions of interest are:

int (*open) (struct inode *, struct file *);
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
int (*release) (struct inode *, struct file *);

So now add these to our driver code:

#include <linux/fs.h>

// called when 'open' system call is invoked
static int
one_open(struct inode *in, struct file *fp)
{
    open_count++;
    debug("Count of open dev files #%d", open_count);

    return 0;
}

// called when 'close' is invoked
static int
one_close(struct inode *in, struct file *fp)
{
    if (! open_count) {
        error("Device close attempted while count is #%d", open_count);
        return -EFAULT;
    }

    debug("Device closed. count is #%d", open_count);
    open_count--;
    return 0;
}

Since we are working on virtual device I just maintain the count of open requests and matching those with the number of close. This is _not_ required but then my zodiac sign is Virgo! πŸ˜‰ Let us define the read functionality, which is where our core logic resides. Note the declaration of ‘read’ function pointer:

ssize_t (*read) (struct file *, char *, size_t, loff_t *);
  • The first parameter is a pointer to file structure, which can be ignored in our case as we are _not_ creating (during open) or destroying (during close) any of the file pointers.
  • The second parameter is the buffer that is allocated in the user space for the read data.
  • The third is the number of bytes to be read and finally,
  • the fourth is the offset within the file, which, again, is not required in our case.
  • Also note that the function should return the number of bytes successfully read.

So, what we are interested in is how to copy number of bytes from kernel space to user space pointed by . Simple, yeah?! πŸ™‚

Now the question is how to copy from Kernel space to user space. Without going into details, we have a special set of functions to do such transfers and the one of interest to us is ‘copy_to_user‘ function, which:

  1. as name suggests, copies data from a buffer in kernel to a buffer in user space.
  2. checks the pointer validity
  3. checks the sufficiency of the size of the buffer allocated in user space.

The self-explanatory prototype looks like this:

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

Now, the final piece of the puzzle. The ‘ones’. I used a global buffer array (one_arr) of size of a page (PAGE_SIZE) which has every of its bit of every Byte turned on, i.e., 0xFF. So, our read function is nothing more than just copy data from this global buffer array to the user space, one page at a time. Thus our code is:

// called when the 'read' system call is done on the device file
static ssize_t
one_read(struct file *fp, char *buf, size_t buf_size, loff_t *off)
{
    size_t count = 0;

    if (! buf_size) {
        debug("buf_size is ZERO!!");
        return 0;
    }

    debug("requested size #%ld", buf_size);
    while (buf_size) {
        size_t chunk = buf_size;

        if (chunk > PAGE_SIZE)
            chunk = PAGE_SIZE;

        debug("about to copy #%ld size of data", chunk);
        debug("data: 0x%x", (unsigned char)one_arr[0]);
        if (copy_to_user(buf, one_arr, chunk))
            return -EFAULT;

        buf += chunk;
        count += chunk;
        buf_size -= chunk;
    }

    debug("return #%ld", count);
    return count;
}

Register of character device with the subsystem

Finally, we are now ready to register this module as a character device driver. The registration happens in the constructor (i.e., init function). The registration API has the following prototype:

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

I am not gonna talk about the major number, the first parameter, in this blog. The second parameter is the name we would like to give to our device driver. The final parameter is the pointer to the file_operations structure defined earlier. We can add the following into the init function:

major = register_chrdev(0, "one", &amp;fops);
if (major &lt; 0) {
    error("Device registration failed - %d!!", major);
    return -EFAULT;
}

Thus ends our coding for the character device driver.
The rest of the entire code can be downloaded from here.

Create the device file

When you load our driver we should be able to see these entries in the log (in CentOS/RHEL, it is /var/log/messages):

Sep 13 20:22:50 rp kernel: [ ONE : one_init : 038 ] [DBG] registering the character device
Sep 13 20:22:50 rp kernel: [ ONE : one_init : 040 ] [DBG] return value: 247
Sep 13 20:22:50 rp kernel: [ ONE : one_init : 046 ] [INF] Device registration successful. Major #247
Sep 13 20:22:50 rp kernel: [ ONE : one_init : 047 ] [DBG] about to prepare the array

Note the major number printed. Using this, we can create a device node on the CLI as shown below.

$> mknod /dev/ones c 247 1
$> chmod a+w+r /dev/ones

Remember to be run as a root or in sudo mode.

Tada! The first simple driver ready! πŸ™‚

You can download the entire code including a test program from here.

References

The blog & articles I referenced while rebuilding my knowledge. Thought these might be useful to you guys as well:

Β 

«
»

    Leave a Reply

    Your email address will not be published.