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 theinit
function. Theexit
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);
first
: It is the beginning minor device number of the range you would like to allocate.count
: The total number of contiguous minor device numbers you are requesting.name
: It is the name that will appear in/proc/devices
and thesysfs
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.