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 <embetronicx@gmail.com>");
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 Driver | RTOS
Hi, 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!
Discover more from EmbeTronicX
Subscribe to get the latest posts sent to your email.

Hello there,
after running your code, I was encountered with an error which is auto.conf or auotconf.h file is not present after that I tried make prepare and make oldconfig but still it’s not working, what should I do as I am stuck on this error from past week.Please help
Kernel v6.6.y
accepts only one parameter, the device name. THIS_MODULE is no more required and therefore must not be passed.
I noticed something.
When the driver is removed, the device /dev/etx_device remains.
I think the following error was caused by not rebooting between tutorials.
After I rebooted, it worked fine. So, I was wondering why the device does not go away and is there a way to remove it after the driver is removed.
$ sudo echo 1 > /dev/etx_device
ksh: /dev/etx_device: cannot create [No such device]
$ ls /dev/e*
/dev/etx_device
$ ls -al /dev/etx_device
crw-rw-rw- 1 root root 246, 0 Apr 9 21:01 /dev/etx_device
$ lsmod | grep driver
driver 16384 0
Thank you
Could you able to reload the driver without rebooting and after removing?
Why dont we write “cdev_del(&etx_cdev);” in __init function in case of error?
I meet a problem, when echo 1 to /dev/ext_device, write function is implement in loop
What is the return value write function?
Can I change the order of calling function in init_module().That is creating cdev structure and adding it after creating the struct class and device.
Hi Shouldn’t we write cdev_del(&ext_cdev); in case of an error in init function?
Hello,
Yes, we have to do that. We will update the code. Thanks.
Hi,
Perfect attempt!
Could you please explain what are the use cases and advantages of using runtime allocation and own allocation?
Hi EmbetronicX,
why did you use static keyword for most of the functions and variables except for etx_driver_exit() and dev?
Thanks
Hi AtlaskD,
Its your wish to use static keyword before any variable and functions. But u should know how static keyword works.
Static keyword limit the visibility to that file. So you cannot access that function or variable from outside of the file (i.e) you cannot access it from other files.
Here my intention to write a driver with multiple files. So all the files will have same init and exit function. Other function’s scope is limited to only this file. I have to remove that static keyword from init function.
Hope this would answered your question.
For everyone that has trouble with permission to write, please read the following post on stackoverflow:
https://stackoverflow.com/questions/34522426/unable-to-write-on-dev-files
TLDR:
sudo echo 1 > /dev/etx_device does not do what you think it would.
rather, get the root prompty by
sudo su
and echo to file
echo 1 > /dev/etx_device
Allt the best
Martin
Hi EmbetroniX,
Your tutorials are very precise and easy to understand. I tried the dummy driver. When I write to the device,it is writing to driver but when check logs they are not printed. please help me on this
Thanks
how to create a vitual device drive to copy the files from one vm to other vm
After giving the command dmesg. I could see the message “module inserted” but I am not getting the message when driver got open, read and released. I had given the command echo 1 /dev/etx_device before dmesg.
Hi Srishti,
Have you changed the source code?
Thanks.
This tutorial is perfect for people who want to start learning kernel programming
Articles are simple and precise to refresh the concepts