LESSON 18: The simplest possible character device driver you can write

Printer-friendly versionPrinter-friendly version

A quick recap, and where we're going from here

Recall that, back in Lesson 17, we wrote, compiled and loaded an astonishingly trivial character device driver that did, well, nothing of any value as far as character drivers are concerned. All it did was request a major device number at which to live in kernel space, and register one or more minor device numbers, the result of which you could see in user space by listing the contents of the /proc/devices file. And then you unloaded it. And that was it.

In this lesson, we're going to add just enough to our character driver so that we can "read" from it. And by "read," I mean that we'll be able to run a command like:

$ cat /dev/mychardrv

and have that command actually generate whatever you decide represents the "output" of the driver from kernel space.

And as an alternative, this "reading" functionality should also extend to doing that from within a simple C program, where you could open that same device file for reading and, well, read from it. Both of these operations will be implemented simply by adding read functionality to our character driver, so let's get to it.

NOTE: A lot of this is covered in considerable detail in the online book "Linux Device Drivers (3rd ed)", specifically in Chapter 3, but I'm going to take a slightly different approach and start by designing and implementing the simplest character driver imaginable.

Getting access to your character driver from user space

Recall that, in order to have a working (and accessible) character driver, you need to do two things:

  • Write, build and load a module that registers itself at a major device and one or more minor device numbers in kernel space, and
  • For now, manually create the corresponding special device file(s) in user space through which you'll talk to your driver.

In other words, once you load your driver module and check /proc/devices to see where it ended up in terms of its major device number, it will be your responsibility to run the appropriate mknod command to create the corresponding device file, such as:

$ sudo mknod /dev/victoryismine c 250 0  [if 250 is major number]

Without a special device file that matches your loaded driver, there's nothing you can do in terms of accessing it, and note well that the name of your device file doesn't have to match any other name you've used until now -- all that matters is that it's created as a "c"haracter file, with the matching major and minor number. You're free to use whatever name you want.

Not surprisingly, if you choose to register a driver that allocates several minor device numbers, you'll need to create a separate device file for each minor number if you want them to be treated separately, but let's keep it simple for now -- one minor number. We'll be covering how to support multiple minor numbers in a later lesson.

Defining your driver's file operations

It's entirely up to you to decide how much file functionality you want to build into your character driver, and you do that by declaring a struct file_operations in your module, and filling in the code for whatever operations you want your driver to support. From the kernel header file include/linux/fs.h, we have the declaration (where you can see how many different file operations you can define for your driver):

struct file_operations {
  struct module *owner;
  loff_t (*llseek) (struct file *, loff_t, int);
  ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
  ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
  ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
  ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, 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);
  ... snip ...
};

This should remind you of when you implemented file operations for a proc file -- for each operation you want to support in your character driver, you'll need to write and associate the corresponding, kernel space file operation routine. But, again, we'll keep it short and do as little as possible -- we'll define simply what it means to "read."

Let's talk about struct cdev

Here's where you actually register your loadable module as a character driver in the kernel, and you do that by defining a struct cdev structure, filling it in appropriately, and calling a routine that registers your driver with the kernel based on the information you filled in.

From the kernel header file include/linux/cdev.h, here's the structure:

struct cdev {
        struct kobject kobj;
        struct module *owner;
        const struct file_operations *ops;
        struct list_head list;
        dev_t dev;
        unsigned int count;
};

Most of the above should actually be self-explanatory -- before you register your character driver with the kernel, you need to declare an instance of the above structure, and fill it in with the corresponding values:

  • owner should be assigned the standard macro "THIS_MODULE",
  • ops should be set to refer to your file operations structure, where you've defined which file operations you're supporting,
  • dev should be set to the device structure defining the major and minor number to be used, and
  • count should be set to the number of minor device numbers to associate with your driver (again, in our simple case, one).

You'll see all this happening shortly when we get into the sample driver, where you'll see that you register your character driver by calling cdev_init(), and deregister it by calling cdev_del(). And, finally, the most important bit of code you'll have to write -- what it means to "read" from your driver.

Defining the read() routine

The easiest way to do this is just show an example of a read routine and walk through the code. Here's the one we're going to use, which is an admittedly trivial and contrived example but demonstrates all of the basic principles:

static char output[] = "Victory is mine!\n";

ssize_t
my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    printk(KERN_INFO "In chardrv read routine.\n");
    printk(KERN_INFO "Count field is %lu.\n", count);
    printk(KERN_INFO "Offset is %lu.\n", *f_pos);

    if (output[*f_pos] == '\0') {
        printk(KERN_INFO "End of string, returning zero.\n");
        return 0;
    }

    copy_to_user(buf, &output[*f_pos], 1);
    *f_pos += 1;
    return 1;  // returned a single character
}

Quite simply, when you try to "read" from your driver from user space, this corresponds to invoking (in kernel space) whatever routine you defined as your driver's read routine, which then passes/copies back to user space the result of the "read." Now let's figure out what all that code up there is doing.

As sample output from our driver, the static character string output represents what I want to return when someone tries to read from the device file for this driver ("Victory is mine!\n"), so all you'll be getting back is a short character string, time and time again. But let's see how that read routine actually works by explaining each parameter one at a time:

  • filp: Ignore that argument to the read routine -- we don't need it in our example but we'll be coming back to it in a later lesson. For now, just leave it alone.
  • buf: A character pointer back to user space, and it's where you'll copy the data you want "read" by the user. In short, it's where you copy the "output" of the driver. More on this shortly.
  • count: The size of the user space buffer that's available to you, so if you're trying to pass back more than that number of bytes, well, you can't. You have to limit yourself to that as a maximum copy size, but don't worry as you'll be called enough times to pass back that maximum number of bytes until you're done.
  • f_pos: An "offset" value that is available to you to keep track of how much data you've passed back and which you can check on each subsequent call to the read routine to determine where to pick up where you left off.

But let's talk about that last argument a bit more.

Given that you might be trying to return far more data during a "read" operation than can fit in the buffer you've been handed by user space, you'll have to return all that data a chunk at a time, and you'll be called continuously until you specify that that's all the data you have and you return an EOF marker.

But in order to keep track of how much data you've already returned, you're supplied with the address of that offset variable and, right after you copy another chunk of data into the buffer, you should increment that offset by whatever is an appropriate value. So what's an appropriate value? That depends.

It's entirely up to you to decide how to interpret whatever value you put in there. In a simple case, maybe it's the byte offset, that's pretty typical. On the other hand, if you're passing back strict 1024-byte blocks, you might simply set the offset value to the block number -- 1, 2, 3, 4 and so on. There's no restriction on how you define that offset value -- all that's important is that you know what it means and can use its value to calculate where to pick up where you left off during the next read call. And on that note, let's dissect our read routine a bit at a time.

static char output[] = "Victory is mine!\n";

That's the string I'll be passing back. Next, the opening of the routine:

ssize_t
my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    printk(KERN_INFO "In chardrv read routine.\n");
    printk(KERN_INFO "Count field is %lu.\n", count);
    printk(KERN_INFO "Offset is %lu.\n", *f_pos);

Just some debugging so I can watch what's happening in /var/log/messages.

Moving on, how do I know when I have no more data left to pass back? In my case, when I hit the end-of-string null byte:

    if (output[*f_pos] == '\0') {
        printk(KERN_INFO "End of string, returning zero.\n");
        return 0;  // EOF indicator
    }

And, finally, I'm going to make this read routine as inefficient as possible by passing back a single character at a time during each read operation:

    copy_to_user(buf, &output[*f_pos], 1);
    *f_pos += 1;
    return 1;  // returned a single character

This is grossly resource-inefficient, of course, but it demonstrates the principle of being called repeatedly until you are out of data. And now, let's pull all this together.

Our character driver, from beginning to end

The final form of this driver file is attached, and we can skim over the parts we already know and touch on just the new content. First, there's this:

static dev_t mydev

That is, of course, the kernel typedef that represents the (major,minor) combination identifying a device.

Next, there's the read routine that we've already covered in detail, so we can jump over that.

And then there's what we need to register our character device:

struct cdev my_cdev;

struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .read = my_read,
};

static int __init chardrv_in(void)
{
    printk(KERN_INFO "module chardrv being loaded.\n");

    alloc_chrdev_region(&mydev, 0, 1, "rday");
    printk(KERN_INFO "%s\n", format_dev_t(buffer, mydev));

    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;
    cdev_add(&my_cdev, mydev, 1);

    return 0;
}

As you can see, I'll need a struct cdev, I'll need an instance of file operations where I can define that my device supports only a read operation and, finally, in the module entry routine, you can see how I register for a major and minor device number and, given what I get back, I create and register as a character driver with a single minor number. And given all that, the exit routine shouldn't be a surprise:

static void __exit chardrv_out(void)
{
    printk(KERN_INFO "module chardrv being unloaded.\n");

    cdev_del(&my_cdev);
    unregister_chrdev_region(mydev, 1);
}

And now, the moment of truth.

Run that baby!

At this point, you're ready to test your new driver. First, build and load your new driver, at which point you can verify it's been loaded by checking /var/log/messages:

... module chardrv being loaded.
... 250:0

and make sure it's visible in /proc/devices:

$ cat /proc/devices
...
250 rday
...

So far, so good -- my character driver appears to be loaded and running.

The next step is to create the device file corresponding to your driver (and, again, you can use whatever name you want for the special device file -- all that matters is that you define it as a character device file with the matching major and minor number):

$ sudo mknod /dev/victory c 250 0  [or whatever number works for you]

And, finally, try to "read" from that device file:

$ cat /dev/victory
Victory is mine!
$

And, while you're at it, you can see what's scrolling by in /var/log/messages:

Jul 17 11:50:04 lynx kernel: [83719.785314] module chardrv being loaded.
Jul 17 11:50:04 lynx kernel: [83719.785321] 250:0
Jul 17 11:51:44 lynx kernel: [83819.954592] In chardrv read routine.
Jul 17 11:51:44 lynx kernel: [83819.954598] Count field is 32768.
Jul 17 11:51:44 lynx kernel: [83819.954601] Offset is 0.
Jul 17 11:51:44 lynx kernel: [83819.954615] In chardrv read routine.
Jul 17 11:51:44 lynx kernel: [83819.954618] Count field is 32768.
Jul 17 11:51:44 lynx kernel: [83819.954621] Offset is 1.
Jul 17 11:51:44 lynx kernel: [83819.954626] In chardrv read routine.
Jul 17 11:51:44 lynx kernel: [83819.954629] Count field is 32768.
Jul 17 11:51:44 lynx kernel: [83819.954632] Offset is 2.
... snip ...
Jul 17 11:51:44 lynx kernel: [83819.954778] In chardrv read routine.
Jul 17 11:51:44 lynx kernel: [83819.954781] Count field is 32768.
Jul 17 11:51:44 lynx kernel: [83819.954783] Offset is 17.
Jul 17 11:51:44 lynx kernel: [83819.954786] End of string, returning zero.

And that's it. Feel free to list that device file as many times as you want, and you'll see you get the same results each time.

NOTE: If you examine the read routine, you'll notice that the test for when we have no more data to pass back is checking for the null byte that represents the end of a standard C character string. And note well that that null byte is not part of the data being passed back:

 if (output[*f_pos] == '\0') {
    printk(KERN_INFO "End of string, returning zero.\n");
    return 0;  // EOF indicator
}

This is simply emphasizing that it's up to you to decide precisely what represents the data being "read" from kernel space, and whether that data contains terminating null bytes of end-of-line characters is your decision.

Exercises for the student

Given how primitive our first character driver is, we can try a number of enhancements; some simple, some a bit trickier. I suggest you make a copy of your working driver, and do all of the following exercises on a throw-away copy.

Exercise 1: Your first driver explicitly allocated and registered a single minor device number (in our case, zero). Extend your driver to allocate a number of minor numbers (say, 64), then rebuild and reload your driver, then test by creating some new device files and verify that you can still access your driver through any of those minor numbers.

For the time being, we're not going to distinguish between between minor numbers when our driver's read() routine is called. We'll do that in a later lesson. For now, just verify that using any valid minor number gets you to the same place -- your driver's read routine -- and that the output is identical for all of those minor numbers.

Exercise 2: Note how we were incredibly inefficient in our original driver in that we passed back only a single character during every read operation. Since the string we're "returning" is fairly short, modify your driver to pass back the entire string in a single copy operation to user space. How will you need to modify the rest of your driver?

Assuming you'll want to calculate the length of the output string, you can do that by including the kernel header file include/linux/string.h, and using the kernel library routine strlen().

Exercise 3: If you're feeling ambitious, and you've verified that you can "read" from your character driver at the command line, feel free to write a C program that opens that device file for reading and check that you can read from it identically from a C program.

So what can possibly go wrong?

Note that we're doing very little error checking in our sample driver above, which is fine for experimentation but probably not so smart for production-level code. We should at least check the return codes on a couple of our kernel routine calls to make sure everything is proceeding properly.

First, it's a good idea to verify that we allocated our major and minor device number(s):

int res;
...
res = alloc_chrdev_region(&mydev, 0, 1, "rday");
if (res < 0) {
    printk(KERN_WARNING
        "Uh oh ... couldn't allocate major and minor number(s).\n");
    return res;
}

Until now, we've simply assumed our call to alloc_chrdev_region() always worked and, for the most part, that's a safe assumption, but it never hurts to check.

Next, you might check the return code from registering your cdev struct with the kernel, as in:

int cdev_res;
...
cdev_res = cdev_add(&my_cdev, mydev, 1);
if (cdev_res < 0) {
    ... uh oh again ...

Finally, we can error check our call to copy_to_user to make sure our kernel space data was copied back to the user space buffer properly:

if (copy_to_user(...) > 0) {
    return -EFAULT;
}

What's happening up there is that, when you invoke copy_to_user(), all of the data you're trying to copy back to user space should be copied, and what is returned by that call is the amount of data still to be copied from that operation. Ideally, that copy routine should have worked and each of those calls should return zero. If the returned value is non-zero, something went wrong.

Exercise for the student: Feel free to add as much of the error checking above to your driver as you wish.

In addition, if you're feeling ambitious, you can see how much of this is implemented in the kernel source file fs/char_dev.c.

Coming next lesson: extending your character driver in all sorts of interesting ways. You don't want to miss it.

AttachmentSize
chardrv.c1.6 KB

Comments

alloc_chardrv_region() not working for me

When I call alloc_chardrv_region() with a third arg > 1, the call succeeds; however, device files with minor numbers > 0 don't work. Attempts to cat them produce the error message "No such device or address."

Exercise 3, pass 1

Contrasting the "count" fields reported from cat(1), getchar(3), and read(2) gives the results I expect, and is fun. (I'm doing single-character-at-a-time I/O for the latter two.)

Combining this with Exercise 2 is still more fun. With a whole-string-copy implementation of the driver, cat(1) and getchar(3) still read the file with no problem; however, read(2) quickly segfaults, leading right in to "What can possibly go wrong?" :-)

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <p> <br> <pre> <h1> <h2> <h3> <h4>
  • Lines and paragraphs break automatically.

More information about formatting options

By submitting this form, you accept the Mollom privacy policy.