Introduction to Device Drivers...in Linux...

A brief overview of the procedure to write your own character device driver in Linux.

I recommend everyone to read Linux Device Drivers for a more comprehensive picture.

The Linux Kernel has the ability to load modules on the fly without needing to be rebooted. Features can be added or removed while the system is running in the form of modules. Each module is made up of object code (not linked into a complete executable) that can be dynamically linked to the running kernel by the insmod program and can be unlinked by the rmmod program. In this issue, we shall cover the basics of a character device driver, which is written in C.

Device drivers are mostly modules in the Linux environment. Common types of devices are,

  • Character Device
  • Block Device
  • Network Device, etc

Character Devices

A character (char) device is one that can be accessed as a stream of bytes (like a file); a char driver is in charge of implementing this behavior. The minimum functions to be implemented by a char driver are:

  • Open
  • Close
  • Read
  • Write

Char Devices show up in the /dev/ directory, like all devices in Linux, these files are called the device nodes. But little do people know that the driver has to explicitly create nodes inside this directory during the driver registration stage.

Block Devices

Like char devices, block devices are accessed by filesystem nodes in the /dev directory. A block device is a device that can host a filesystem. In most Unix systems, a block device can only handle I/O operations that transfer one or more whole blocks. A single block is usually 512 bytes (or a larger power of two) bytes in length.

Block drivers have a completely different interface to the kernel than char drivers.

Network Devices

Any network transaction is made through an interface, that is, a device that is able to exchange data with other hosts. Usually, an interface is a hardware device, but it might also be a pure software device, like the loopback ( localhost/ 127.0.0.1) interface.


The Simplest Device Driver

A device driver that simply prints to the kernel log upon initialization.

#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
	printk(KERN_ALERT "Hello from Neoned71\n");
	// KERN_ALERT is the priority
    
	return 0;
}

static void hello_exit(void)
{
	printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(hello_init);//Special kind of Macro
module_exit(hello_exit);//Special kind of Macro
Every kernel module just registers itself in order to serve future requests in the init function. The exit function of a module must carefully undo everything the init function built up. Because no library is linked to modules, source files should never include the usual header files. A module runs in kernel space, whereas applications run in user space.

To compile, just save the file and run :

gcc -DMODULE -D__KERNEL__ -isystem /lib/modules/$(uname -r)/build/include -c hello.c -o hello.ko

Once compiled, the kernel can be patched up with the newly created module by executing insmod hello.ko. The printk function logs to the kernel developer log, which can be displayed using the dmesg command. rmmod hello.ko removes the loaded module from the kernel tree.

Parameters can also be passed to the kernel module like this, insmod hello.ko howmany=10 whom="Mom". The module code should register these parameters beforehand as in the following code:

//declaration of variables
static char *whom = "world";
static int howmany = 1;

//registering variables as module params
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);

Upon this, we build a more complete Character Device.


Theory behind the device drivers

Major and Minor Numbers

Char devices are accessed through names in the filesystem. Those names are called special files or device files or simply nodes of the filesystem tree; they are conventionally located in the /dev directory. Special files for char drivers are identified by a c in the first column of the output of ls –l. If you issue the ls –l command, you’ll see two numbers, a Major Number that selects a driver and a Minor number that is used by the kernel to determine exactly which device is being referred to.
So, the drivers have to register Major and Minor numbers in order to be able to work on a Linux system. Within the kernel, the dev_t type (defined in <linux/types.h>) is used to hold device numbers
To obtain the major or minor parts of a dev_t, special macros have been defined in the Linux kernel,

//Macros used to get Major and Minor # from dev_t struct
	MAJOR(dev_t dev);
	MINOR(dev_t dev);

//If, instead, you have the major and minor numbers and need a dev_t 
	MKDEV(int major, int minor);

Registering the device Major and Minor numbers

Because the driver always work with these numbers, the next step is to register these numbers before creating the nodes in the /dev/ directory.

The necessary function for this task is register_chrdev_region, which is declared in <linux/fs.h>:

int register_chrdev_region(dev_t first, unsigned int count,char *name);

  1. first: It is the beginning minor device number of the range you would like to allocate.
  2. count: The total number of contiguous minor device numbers you are requesting.
  3. name: It is the name that will appear in /proc/devices and the sysfs file systems.

Instead, to allocate a major number for you on the fly, which is recommended these days,

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);

Regardless of how you allocate your device numbers, you should free them when they are no longer in use. Device numbers are freed with(usually in the cleanup function):

void unregister_chrdev_region(dev_t first, unsigned int count);

To get the major number after registration you have to parse /proc/devices file & look for the name of driver.

Use this command in the terminal for fetching it up automatically, replace driver_name with the name of your driver:

major=$(awk '$2=="driver_name" {print $1}' /proc/devices)

What about the operations we can perform over this device node?

Well, for that we need to implement the struct file_operations, this is a collection of functions to be executed when we open, read or write to the /dev/my_device file we have just created.

The struct file_operations has the following definition,

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 *);
    };

We implement the above struct using the C99 convention, provided these functions on the right-hand side of the assignments are already declared.

struct file_operations driver_fops = {
		.owner = THIS_MODULE,
		.llseek = driver_llseek,
		.read = driver_read,
		.write = driver_write,
		.ioctl = driver_ioctl,
		.open = driver_open,
		.release = driver_release

		//and so on...
		
		};

Struct file

The file (defined in <linux/fs.h>) struct represents an open file in the kernel sources. A pointer to the struct file is usually called either file or filep. It is created by the kernel on open and is passed to any function that operates on the file, until the last close.

The file struct also contains a reference to its file_operations struct.

Struct inode

The inode struct is used by the kernel internally to represent files. Each file has an inode, open or not. All the open files will have different file struct but if they are open versions of the same file then the inode number will be the same among all. The inode structure contains a great deal of information about the file.

struct inode {

	dev_t i_rdev; //For inodes that represent device files, this field contains the actual device number
	struct cdev *i_cdev; //struct cdev is the kernel’s internal structure that represents char devices

	//....more fields are there but not really interesting...

}

A major and minor number can be acquired from an inode struct using

unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);


A More complete Character driver example

After implementing all the basics we discussed above with file operations defined, other than write operation which can be implemented by you in the process of learning.

Code credit goes to Peter Jay Salzman for his blog post and the code below.

#define MODULE
#define LINUX
#define __KERNEL__

#if defined(CONFIG_MODVERSIONS) && ! defined(MODVERSIONS)
   #include <linux/modversions.h>
   #define MODVERSIONS
#endif
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h>  /* for put_user */
#include <asm/errno.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 the device */
static char msg[BUF_LEN];    /* The msg the device will give when asked    */
static char *msg_Ptr;

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


/* Functions */

int init_module(void)
{
   Major = register_chrdev(0, DEVICE_NAME, &fops);

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

   printk("<1>I was assigned major number %d.  To talk to\n", Major);
   printk("<1>the driver, create a dev file with\n");
   printk("'mknod /dev/hello c %d 0'.\n", Major);
   printk("<1>Try various minor numbers.  Try to cat and echo to\n");
   printk("the device file.\n");
   printk("<1>Remove the device file and module when done.\n");

   return 0;
}


void cleanup_module(void)
{
   /* Unregister the device */
   int ret = unregister_chrdev(Major, DEVICE_NAME);
   if (ret < 0) printk("Error in unregister_chrdev: %d\n", ret);
}


/* 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;
   MOD_INC_USE_COUNT;

   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. */
   MOD_DEC_USE_COUNT;

   return 0;
}


/* Called when a process, which already opened the dev file, attempts to
   read from it.
*/
static ssize_t device_read(struct file *filp,
   char *buffer,    /* The buffer to fill with data */
   size_t length,   /* The length of the buffer     */
   loff_t *offset)  /* Our offset in the file       */
{
   /* 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;
         * 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)
{
   printk ("<1>Sorry, this operation isn't supported.\n");
   return -EINVAL;
}

MODULE_LICENSE("GPL");

For a full code sample, you should have a look at Peter Jay Salzman's simple character device driver in C. Play around with the code to code up your own character device.

This driver just prints the message in the variable. Real devices also deal with hardware. One essential concept is to be understood first before dealing with real-world hardware, Interrupts. In the next post, we shall discuss the mechanism of interrupting the CPU in a productive way.

Sign up for the blog-newsletter! I promise that we do not spam...