This article is a continuation of the Series on Linux Device Drivers and carries the discussion on character drivers and their implementation. The aim of this series is to provide easy and practical examples that anyone can understand. This is the cdev structure and file operations of the character drivers tutorial – Linux Device Driver Tutorial Part 6.
We have learned the major, minor number, and device files in our previous tutorials. So, as I said earlier, we need to open, read, write, and close the device file. We will focus on those operations in this tutorial.
Table of Contents
Prerequisites
To continue with this tutorial, you must have set up the Ubuntu or Raspberry Pi or Beaglebone. If you aren’t set up anything yet, we suggest you to set up the boards that you have using the below-given tutorials.
You can find a video explanation of this tutorial here. You can also find all the Linux device driver’s video playlists here.
Cdev structure and File Operations of Character drivers
If we want to open, read, write, and close we need to register some structures to the driver.
cdev structure
In Linux kernel struct inode
structure is used to represent files. Therefore, it is different from the file structure that represents an open file descriptor. There can be numerous file structures representing multiple open descriptors on a single file, but they all point to a single inode
structure.
The inode
structure contains a great deal of information about the file. As a general rule, cdev
structure is useful for writing driver code:
struct cdev
is one of the elements of the inode
structure. As you probably may know already, an inode
structure is used by the kernel internally to represent files. The struct cdev
is the kernel’s internal structure that represents char devices. This field contains a pointer to that structure when the inode
refers to a char device file.
struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; };
This is cdev
structure. Here we need to fill the two fields,
file_operation
(This we will see after this cdev structure)owner
(This should beTHIS_MODULE
)
There are two ways of allocating and initializing one of these structures.
- Runtime Allocation
- Own allocation
If you wish to obtain a standalone cdev
structure at runtime, you may do so with code such as:
struct cdev *my_cdev = cdev_alloc( ); my_cdev->ops = &my_fops; |
Or else you can embed the cdev
structure within a device-specific structure of your own by using the below function.
void cdev_init(struct cdev *cdev, struct file_operations *fops); |
Once the cdev
structure is set up with file_operations and owner, the final step is to tell the kernel about it with a call to:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count); |
Where,
dev
is the cdev structure,
num
is the first device number to which this device responds, and
count
is the number of device numbers that should be associated with the device. Often count is one, but there are situations where it makes sense to have more than one device number correspond to a specific device.
If this function returns a negative error code, your device has not been added to the system. So check the return value of this function.
After a call to cdev_add()
, your device is immediately alive. All functions you defined (through the file_operations structure) can be called.
To remove a char device from the system, call:
void cdev_del(struct cdev *dev); |
Clearly, you should not access the cdev
structure after passing it to cdev_del
.
File_Operations
The file_operations
structure is how a char driver sets up this connection. The structure, (defined in <linux/fs.h>
), is a collection of function pointers. Each open file is associated with its own set of functions. The operations are mostly in charge of implementing the system calls and are, therefore, named open, read, and so on.
A file_operations
structure is called fops
. Each field in the structure must point to the function in the driver that implements a specific operation or have to left NULL for unsupported operations. The whole structure is mentioned below snippet.
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 (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); unsigned int (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, u64); ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *, u64); };
This file_operations structure contains many fields. But we will concentrate on very basic functions. Below we will see some fields explanation.
struct module *owner:
The first file_operations
field is not an operation at all; it is a pointer to the module that “owns” the structure. This field is used to prevent the module from being unloaded while its operations are in use. Almost all the time, it is simply initialized to THIS_MODULE
, a macro defined in <linux/module.h>
.
read
ssize_t (*read) (struct file *, char _ _user *, size_t, loff_t *); |
This is used to retrieve data from the device. A null pointer in this position causes the read system call to fail with -EINVAL
(“Invalid argument”). A non-negative return value represents the number of bytes successfully read (the return value is a “signed size” type, usually the native integer type for the target platform).
write
ssize_t (*write) (struct file *, const char _ _user *, size_t, loff_t *); |
It is used to sends the data to the device. If NULL -EINVAL
is returned to the program calling the write system call. The return value, if non-negative, represents the number of bytes successfully written.
ioctl
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); |
The ioctl system call offers a way to issue device-specific commands (such as formatting a track of a floppy disk, which is neither reading nor writing). Additionally, a few ioctl commands are recognized by the kernel without referring to the fops
table. If the device doesn’t provide an ioctl method, the system call returns an error for any request that isn’t predefined (-ENOTTY
, “No such ioctl for device”). You can find the IOCTL tutorial here.
Open
int (*open) (struct inode *, struct file *); |
Though this is always the first operation performed on the device file, the driver is not required to declare a corresponding method. If this entry is NULL, opening the device always succeeds, but your driver isn’t notified.
release (close)
int (*release) (struct inode *, struct file *); |
This operation is invoked when the file structure is being released. Like open, release can be NULL.
Example
static struct file_operations fops = { .owner = THIS_MODULE, .read = etx_read, .write = etx_write, .open = etx_open, .release = etx_release, };
If you want to understand the complete flow, just have a look at our dummy driver.
Cdev structure and File Operations Example
Dummy Driver
Here I have added a dummy driver snippet. In this driver code, we can do all open, read, write, close operations. Just go through the code.
[Get the source code of this example from the GitHub]
/***************************************************************************//** * \file driver.c * * \details Simple Linux device driver (File Operations) * * \author EmbeTronicX * * \Tested with Linux raspberrypi 5.10.27-v7l-embetronicx-custom+ * *******************************************************************************/ #include <linux/kernel.h> #include <linux/init.h> #include <linux/module.h> #include <linux/kdev_t.h> #include <linux/fs.h> #include <linux/err.h> #include <linux/cdev.h> #include <linux/device.h> dev_t dev = 0; static struct class *dev_class; static struct cdev etx_cdev; /* ** Function Prototypes */ static int __init etx_driver_init(void); static void __exit etx_driver_exit(void); static int etx_open(struct inode *inode, struct file *file); static int etx_release(struct inode *inode, struct file *file); static ssize_t etx_read(struct file *filp, char __user *buf, size_t len,loff_t * off); static ssize_t etx_write(struct file *filp, const char *buf, size_t len, loff_t * off); static struct file_operations fops = { .owner = THIS_MODULE, .read = etx_read, .write = etx_write, .open = etx_open, .release = etx_release, }; /* ** This function will be called when we open the Device file */ static int etx_open(struct inode *inode, struct file *file) { pr_info("Driver Open Function Called...!!!\n"); return 0; } /* ** This function will be called when we close the Device file */ static int etx_release(struct inode *inode, struct file *file) { pr_info("Driver Release Function Called...!!!\n"); return 0; } /* ** This function will be called when we read the Device file */ static ssize_t etx_read(struct file *filp, char __user *buf, size_t len, loff_t *off) { pr_info("Driver Read Function Called...!!!\n"); return 0; } /* ** This function will be called when we write the Device file */ static ssize_t etx_write(struct file *filp, const char __user *buf, size_t len, loff_t *off) { pr_info("Driver Write Function Called...!!!\n"); return len; } /* ** Module Init function */ static int __init etx_driver_init(void) { /*Allocating Major number*/ if((alloc_chrdev_region(&dev, 0, 1, "etx_Dev")) <0){ pr_err("Cannot allocate major number\n"); return -1; } pr_info("Major = %d Minor = %d \n",MAJOR(dev), MINOR(dev)); /*Creating cdev structure*/ cdev_init(&etx_cdev,&fops); /*Adding character device to the system*/ if((cdev_add(&etx_cdev,dev,1)) < 0){ pr_err("Cannot add the device to the system\n"); goto r_class; } /*Creating struct class*/ if(IS_ERR(dev_class = class_create(THIS_MODULE,"etx_class"))){ pr_err("Cannot create the struct class\n"); goto r_class; } /*Creating device*/ if(IS_ERR(device_create(dev_class,NULL,dev,NULL,"etx_device"))){ pr_err("Cannot create the Device 1\n"); goto r_device; } pr_info("Device Driver Insert...Done!!!\n"); return 0; r_device: class_destroy(dev_class); r_class: unregister_chrdev_region(dev,1); return -1; } /* ** Module exit function */ static void __exit etx_driver_exit(void) { device_destroy(dev_class,dev); class_destroy(dev_class); cdev_del(&etx_cdev); unregister_chrdev_region(dev, 1); pr_info("Device Driver Remove...Done!!!\n"); } module_init(etx_driver_init); module_exit(etx_driver_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("EmbeTronicX <[email protected]>"); MODULE_DESCRIPTION("Simple Linux device driver (File Operations)"); MODULE_VERSION("1.3");
Makefile
obj-m += driver.o ifdef ARCH #You can update your Beaglebone path here. KDIR = /home/embetronicx/BBG/tmp/lib/modules/5.10.65/build else KDIR = /lib/modules/$(shell uname -r)/build endif all: make -C $(KDIR) M=$(shell pwd) modules clean: make -C $(KDIR) M=$(shell pwd) clean
Testing the Device Driver
- Build the driver by using Makefile (
sudo make
) or if you are using the Beaglebone board, then you can usesudo make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
. - Load the driver using
sudo insmod
- Do
echo 1 > /dev/etx_device
Echo
will open the driver and write 1 into the driver and finally close the driver. So if I do echo
to our driver, it should call the open, write and release functions. Just check.
linux@embetronicx-VirtualBox:/home/driver/driver# echo 1 > /dev/etx_device
- Now Check using
dmesg
linux@embetronicx-VirtualBox:/home/driver/driver$ dmesg [19721.611967] Major = 246 Minor = 0 [19721.618716] Device Driver Insert...Done!!! [19763.176347] Driver Open Function Called...!!! [19763.176363] Driver Write Function Called...!!! [19763.176369] Driver Release Function Called...!!!
- Do
cat > /dev/etx_device
cat
command will open the driver, read the driver, and close the driver. So if I do cat
to our driver, it should call the open, read, and release functions. Just check.
linux@embetronicx-VirtualBox:/home/driver/driver# cat /dev/etx_device
- Now Check using
dmesg
linux@embetronicx-VirtualBox:/home/driver/driver$ dmesg [19763.176347] Driver Open Function Called...!!! [19763.176363] Driver Read Function Called...!!! [19763.176369] Driver Release Function Called...!!!
- Unload the driver using
sudo rmmod
Instead of doing echo
and cat
command in the terminal you can also use open()
, read(), write(), close()
system calls from user-space applications.
Video Explanation
You can check the video explanation of this tutorial below.
I hope you understood this tutorial. This is just a dummy driver tutorial. In our next tutorial, we will see some real-time applications using file operations device drivers.
Please find the other Linux device driver tutorials here.
You can also read the below tutorials.
Embedded Software | Firmware | Linux Devic Deriver | RTOS
Hi, I’m SLR. I am a tech blogger and an Embedded Engineer. I am always eager to learn and explore tech-related concepts. And also, I wanted to share my knowledge with everyone in a more straightforward way with easy practical examples. I strongly believe that learning by doing is more powerful than just learning by reading. I love to do experiments. If you want to help or support me on my journey, consider sharing my articles, or Buy me a Coffee! Thank you for reading my blog! Happy learning!