Introduction
The previous post (part 4) discussed device files, how they can be created and managed in Linux kernel as an essential step for establishing communication between user application and kernel module.
This article is a continuation of Linux kernel device driver series and will explain some concepts about device files operations.
File operations are actions to be performed on a device via their appropriate files. They are defined in a specific way in the module code following some steps.
Below a high level summary of steps to be followed to implement file operations in a kernel module:
- Defining Cdev structure
- Initializing the structure
- Adding the structure to the kernel
- Defining File_Operations
Defining Cdev structure
In linux kernel, files and directories are represented by the inode structure defined in <linux/fs.h> and they’re manipulated by the kernel thanks to information described in this structure.
cdev in its role is a field of inode that points to a structure that represents char devices when the inode concerns a char device file. Cdev is defined in <linux/cdev.h>:
struct cdev {
13 struct kobject kobj;
14 struct module *owner;
15 const struct file_operations *ops;
16 struct list_head list;
17 dev_t dev;
18 unsigned int count;
19 };
The two fields to be specified are:
- struct module *owner
Refers to the current module, to be filled with THIS_MODULE (a macro defined in linux/init.h - const struct file_operations *ops
a structure defining file operations (Discussed in section 5)
Initializing the structure
There are two ways to initialize cdev structure:
- cdev_alloc()
- cdev_init()
cdev_alloc()
cdev_alloc() allocates and initializes the cdev structure. It returns a cdev structure on success or NULL on failure.
It’s defined in <linux/cdev.h> and its source code can be found in linux/fs/char_dev.c :
struct cdev *cdev_alloc(void);
cdev_alloc() can be used as shown in snippet below:
struct cdev *my_dev = cdev_alloc();
my_dev->ops = &my_fops; /* The file_operations structure */
my_dev->owner = THIS_MODULE; /* The module owner */
cdev_init()
cdev_init() initializes a cdev that has already been set in memory. It’s also defined in <linux/cdev.h> and its source code can be found in linux/fs/char_dev.c
void cdev_init(struct cdev *, const struct file_operations *);
Adding the structure to the kernel
The next step after initializing the “cdev” structure is to add it to the system and specify the corresponding device number. This can be done using the function “cdev_add()” described below:
int cdev_add(struct cdev *cdev, dev_t first, unsigned int count);
- *cdev
Refers to the “cdev” structure for the device. - first
The first device number for which this device is responsible. - count
The number of consecutive minor numbers corresponding to this device.
To remove a char device from the system the function “cdev_del()” is to be used:
void cdev_del(struct cdev *cdev);
Defining File_Operations
The file_operations structure is defined in linux/fs.h. It contains a set of pointers to functions that execute several operations on the device. The functions pointers are defined in the driver code and each element of the structure is represented by its corresponding function address.
The struct is defined in the snippet below:
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 *);
};
This article will be using only the few basic operations (open, read, write and release):
- open – int (*open) (struct inode *, struct file *);
It’s the first operation that will be performed on the device file when it’s opened. - read – ssize_t (*read) (struct file *, char *, size_t, loff_t *);
On success, the program returns the number of bytes read (zero indicates end of file) and returns “-1” and the error number (errno) on failure. - write – ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
On success, the program returns the number of bytes written and returns “-1” and the error number (errno) on failure. - release – int (*release) (struct inode *, struct file *);
It is called when the file is closed.
Note: These operations are called when their corresponding functions from user space are called. E.g open does not open the file, it is called when the file is opened from user space using open() and same applies on the other operations.
Open function example
Below a snippet for an “open” function printing kernel log information. When opening a file it’s essential to allocate a dedicated memory in advance using for e.g. “kmaloc()” and this could be implemented in the module “_init” function such as the example in last section of this article.
static int device_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Device File is opened...\n");
return 0;
}
Read function example
The “read” function content is based on the function call “copy_to_user()” that puts a value from kernel space to user space. It takes three arguments:
- Destination variable (buf ⇔ user space variable)
- Source variable (kernel_buf ⇔ kernel space variable)
- Variable size (mem_size)
The “read” function is called when the file is being read from user space. The “*off” refers to the offset to the file position. It is reset to “0” after the file is read which may cause an infinite loop when trying to read the file with tools such as “cat”.
Depending on tools to be used and use cases the implementation of “read” function can be adjusted to avoid unexpected behaviors. Example below is updating the offset to the last position when it is reached and returning “0” (end of file):
static ssize_t data_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
/* Managing position in file */
ssize_t remaining_bytes = (size_t)(mem_size - *off); // Remaining bytes to read from file
if( copy_to_user(buf, kernel_buf, mem_size) )
{
pr_err("Err : Cannot copy data to user !\n");
}
/* Update the current file position */
*off += remaining_bytes;
printk(KERN_INFO "Data Read : Successfull!\n");
return remaining_bytes;
}
Write function example
The “write” function content is based on the function call “copy_from_user()” that puts a value from user space to kernel space. It takes three arguments:
- Destination variable (kernel_buf ⇔ kernel space variable)
- Source variable (buf ⇔ user space variable)
- Variable size (mem_size)
static ssize_t data_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
if( copy_from_user(kernel_buf, buf, len) )
{
pr_err("Err : Cannot write data from user !\n");
}
printk(KERN_INFO "Data Write : Successfull!\n");
return len;
}
Release function example
static int device_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Device File is closed...!!!\n");
return 0;
}
Module example with file operations
/*******************************************************************************
* \file driver-example.c
*
* \details Simple driver example
*
* \author AJ Embedded Systems Consultant https://aj-ese.com
*
* *******************************************************************************/
#include<linux/kernel.h>
#include<linux/init.h>
#include<linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/err.h>
#include <linux/device.h>
#include<linux/slab.h>
#include<linux/uaccess.h>
#define file_size 1024 // memory size to be reserved for the file
/* Associating Major and Minor number to a device*/
dev_t dev = 0;
const char * device_name = "AJ_Dev";
static struct class *device_class;
static struct cdev AJ_cdev;
uint8_t *kernel_buf;
static int __init AJ_driver_init(void);
static void __exit AJ_driver_exit(void);
static int device_open(struct inode *inode, struct file *file);
static ssize_t data_read(struct file *filp, char __user *buf, size_t len,loff_t * off);
static ssize_t data_write(struct file *filp, const char *buf, size_t len, loff_t * off);
static int device_release(struct inode *inode, struct file *file);
static struct file_operations fops =
{
.owner = THIS_MODULE,
.open = device_open,
.read = data_read,
.write = data_write,
.release = device_release,
};
/* The function that will be called when the device file is open */
static int device_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Device File is opened...\n");
return 0;
}
/* The function that will be called when data is read from device file */
static ssize_t data_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
/* Managing position in file */
ssize_t remaining_bytes = (size_t)(file_size - *off); // Remaining bytes to read from file
if( copy_to_user(buf, kernel_buf, file_size) )
{
pr_err("Err : Cannot copy data to user !\n");
}
/* Update the current file position */
*off += remaining_bytes;
printk(KERN_INFO "Data Read : Successfull!\n");
return remaining_bytes;
}
/* The function that will be called when data is written to device file */
static ssize_t data_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
if( copy_from_user(kernel_buf, buf, len) )
{
pr_err("Err : Cannot write data from user !\n");
}
printk(KERN_INFO "Data Write : Successfull!\n");
return len;
}
/* The function that will be called when device file is closed */
static int device_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Device File is closed...!!!\n");
return 0;
}
/*
** Module Init function
*/
static int AJ_driver_init(void)
{
/* Creating a device with the reserved Major and Minor numbers */
if ( ( alloc_chrdev_region(&dev, 0, 1, device_name)) < 0 )
{
printk(KERN_INFO "Cannot allocate major number for device %s \n", device_name);
return -1;
}
/*Initializing cdev structure*/
cdev_init(&AJ_cdev,&fops);
/*Adding character device to the system*/
if((cdev_add(&AJ_cdev, dev, 1)) < 0)
{
printk(KERN_INFO "Cannot add the device to the system\n");
goto destroy_class;
}
/*Creating device class*/
device_class = class_create(THIS_MODULE,"AJ_class");
if(IS_ERR(device_class))
{
printk(KERN_ERR "Cannot create the struct class for device\n");
goto destroy_class;
}
/*Creating device*/
if(IS_ERR(device_create(device_class,NULL,dev,NULL,"AJ_device")))
{
printk(KERN_ERR "Cannot create the Device\n");
goto destroy_device;
}
/*Creating Physical memory */
if((kernel_buf = kmalloc(file_size , GFP_KERNEL)) == 0)
{
pr_info("Cannot allocate memory in kernel\n");
goto destroy_device;
}
printk(KERN_INFO "The device %s is created with Major = %d Minor = %d \n",device_name, MAJOR(dev), MINOR(dev));
printk(KERN_INFO "The AJ-driver is inserted successfully...\n");
return 0;
destroy_class:
class_destroy(device_class);
destroy_device:
device_destroy(device_class, dev);
return -1;
}
/*
** Module Exit function
*/
static void AJ_driver_exit(void)
{
kfree(kernel_buf);
device_destroy(device_class,dev);
class_destroy(device_class);
cdev_del(&AJ_cdev);
unregister_chrdev_region(dev, 1);
printk(KERN_INFO "The AJ-driver is removed successfully...\n");
}
module_init(AJ_driver_init);
module_exit(AJ_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("AJ Embedded Systems Consultant https://aj-ese.com");
MODULE_DESCRIPTION("Simple driver example");
MODULE_VERSION("1:1.3");

Leave a Reply