Linux character device driver (cdev registration, use and cancellation)

The previous article described how to create the device number of the character device driver. Now let’s talk about how the character driver is implemented in the kernel.

In the Linux kernel, cdev is a structure used to describe character devices.

It was explained before that the user space uses the device number to find the character driver, and the character device driver must be identified by the same device number as the user space in the kernel space.

As shown in the figure, cdev is mainly concerned with dev_t and file_operations. dev_t is the device number, and file_operations is a set of operation functions, which is equivalent to the interface function provided by the character device.

Character device driver model

Below we will explain one by one according to the character-driven model.

Driver initialization

For static allocation and dynamic allocation of device numbers, refer to the previous article (application for device numbers).

To use a character device driver, you need to add a cdev structure representation to the kernel to declare a character device driver.

<include/linux/cdev.h>
 
struct cdev {
struct kobject kobj; //Embedded kernel object.
struct module *owner; //The object pointer of the kernel module where the character device is located.
const struct file_operations *ops; //This structure describes the methods that character devices can implement, and is a very critical structure.
struct list_head list; //Used to form a linked list of all character devices that have been registered with the kernel.
dev_t dev; //The device number of the character device, which is composed of the major device number and the minor device number.
unsigned int count; //The number of minor device numbers belonging to the same major device number.
};

We use the cdev_init function to initialize cdev.

//Initialize cdev, provide cdev with a set of operation functions

void cdev_init(struct cdev *cdev, const struct file_operations *fops);

Then add cdev to the kernel using the cdev_add function.

//Add cdev to the kernel, and also bind the device number to cdev

int cdev_add(struct cdev *p, dev_t dev, unsigned count);

parameter:

p – the cdev structure to add

dev – the starting device number

count – the number of device numbers

Return 0 for success, non-zero for failure

Implement the operation function

The structure file_operations is defined in the header file linux/fs.h, and is used to store pointers to functions provided by the driver kernel module to perform various operations on the device. Each field of this structure corresponds to the address of a function used by the driver kernel module to handle a requested transaction.

To put it simply, if you want the character device driver to implement any function, you must write the corresponding function, and assign the function address to the corresponding member in file_operations. The members without assignment are initialized to NULL by gcc.

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(*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
  ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *);
  ssize_t(*aio_write) (struct kiocb *, const char __user *, 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 (*aio_fsync) (struct kiocb *, 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 *);
  ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *);
  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);

};

When we implement the operation function, the function structure should be consistent with that in file_operations. Select a few commonly used operation functions as examples.

int cdd_open(struct inode *inode, struct file *filp)
{
printk("enter cdd_open!\
");
return 0;
}

ssize_t cdd_read(struct file *filp, char __user *buf, size_t size, loff_t *offset)
{
printk("enter cdd_read!\
");
return 0;
}

ssize_t cdd_write(struct file *filp, const char __user *buf, size_t size, loff_t *offset)
{
printk("enter cdd_write!\
");
return 0;
}

long cdd_ioctl(struct file *filp, unsigned int cmd, unsigned long data)
{
printk("enter cdd_ioctl!\
");
return 0;
}

int cdd_release(struct inode *inode, struct file *filp)
{
printk("enter cdd_release!\
");
return 0;
}

// Declare a set of operation functions
struct file_operations cdd_fops = {
.owner = THIS_MODULE,//The count of modules that own this structure, generally THIS_MODULE
.open = cdd_open,
.read = cdd_read,
.write = cdd_write,
.unlocked_ioctl = cdd_ioctl, //ioctl interface
.release = cdd_release, //corresponding user close interface
};

There are multiple function parameters above, including the file structure and the inode structure. Next, we will introduce these two structures.

file:

The struct file structure is defined in the linux/fs.h header file. The file structure is used to describe the relationship and operation between processes and files, and represents an open file. Every open file in the system has an associated file in the kernel space. It is created by the kernel when a file is opened and passed to any function that operates on the file. After all instances of the file are closed, the kernel frees this data structure. The pointer of struct file is usually named file or filp, and the file structure has the following important members:

struct file{
    mode_t fmode; /*file mode, such as FMODE_READ, FMODE_WRITE*/
    loff_t f_pos; /*loff_t is a 64-bit number, it must be converted to 32-bit when necessary*/
    unsigned int f_flags; /* file flags, such as: O_NONBLOCK*/
    struct file_operations *f_op;
    void *private_data; /*very important, used to store the converted device description structure pointer*/
};

inode:

Files are stored on the hard disk, and the smallest storage unit of the hard disk is called a “sector”. Each sector stores 512 bytes. When the operating system reads the hard disk, it does not read sectors one by one, which is too inefficient. Instead, it reads multiple sectors continuously at one time, that is, reads one at a time. block” (block). This “block” composed of multiple sectors is the smallest unit of file access. The size of the “block”, the most common is 4KB, that is, eight consecutive sectors form a block. File data is stored in “blocks”, so obviously, we must also find a place to store the source information of the file, such as the creator of the file, the date of creation of the file, the size of the file, and so on. This area that stores file source information is called an inode.

Driver logout

We have a function to add cdev to the kernel, and naturally there is a function cdev_del to remove cdev from the kernel.

void cdev_del(struct cdev *p);

Then, for the deregistration of the device number, please refer to the previous article (application for device number).

A complete character device driver is completed. Let’s write an example and practice it.

Driver cdd.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>

//Major device number
#define CDD_MAJOR 220
//Start secondary device number
#define CDD_MINOR 0
//Number of device numbers
#define CDD_COUNT 1

//Device No
dev_t dev;
//declare cdev
struct cdev cdd_cdev;
/*
Inode is the node structure of the file, which is used to store the static information of the file
When the file is created, there will be an inode structure in the kernel
The file structure records the information about the file opening
When the file is opened, the kernel will create a file structure
*/
int cdd_open(struct inode *inode, struct file *filp)
{
printk("enter cdd_open!\
");
return 0;
}

ssize_t cdd_read(struct file *filp, char __user *buf, size_t size, loff_t *offset)
{
printk("enter cdd_read!\
");
return 0;
}

ssize_t cdd_write(struct file *filp, const char __user *buf, size_t size, loff_t *offset)
{
printk("enter cdd_write!\
");
return 0;
}

long cdd_ioctl(struct file *filp, unsigned int cmd, unsigned long data)
{
printk("enter cdd_ioctl!\
");
return 0;
}

int cdd_release(struct inode *inode, struct file *filp)
{
printk("enter cdd_release!\
");

return 0;
}

// Declare a set of operation functions
struct file_operations cdd_fops = {
.owner = THIS_MODULE,
.open = cdd_open,
.read = cdd_read,
.write = cdd_write,
.unlocked_ioctl = cdd_ioctl, //ioctl interface
.release = cdd_release, //corresponding user close interface
};

//load function
int cdd_init(void)
{
int ret;
#if 1
// Construct device number
dev = MKDEV(CDD_MAJOR, CDD_MINOR);

// 1. Statically apply for device number
ret = register_chrdev_region(dev, CDD_COUNT, "cdd_demo");
if(ret<0){
printk("register_chrdev_region failed!\
");
return ret;
}

#else
// 2. Dynamically apply for device number
ret = alloc_chrdev_region( & dev, CDD_MINOR, CDD_COUNT, "cdd_demo");
if(ret<0){
printk("alloc_chrdev_region failed!\
");
return ret;
}

#endif
// 2. Register cdev
//initialization
cdev_init( &cdd_cdev, &cdd_fops);
//Add cdev to the kernel
ret = cdev_add( & cdd_cdev, dev, CDD_COUNT);
if(ret<0){
//Register the device number
unregister_chrdev_region(dev, CDD_COUNT);
printk("cdev_add failed!\
");
return ret;
}

printk("cdev_add success!\
");

return 0;
}


//uninstall function
void cdd_exit(void)
{
cdev_del( &cdd_cdev);
//Register the device number
unregister_chrdev_region(dev, CDD_COUNT);
}

//Declared as the entry and exit of the module
module_init(cdd_init);
module_exit(cdd_exit);


MODULE_LICENSE("GPL");//GPL module license
MODULE_AUTHOR("xin");//Author
MODULE_VERSION("1.0");//Version
MODULE_DESCRIPTION("character driver!");//Description information

Application test program test.c

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <linux/rtc.h>

int main()
{
    int fd;
char ch = 0;
char buf[10] = {0};

fd = open("/dev/cdd",O_RDWR);
if(fd==-1){
perror("open");
exit(-1);
}

printf("open successful!fd = %d\
",fd);

while (1) {
ch = getchar();
getchar();

if(ch=='q')
break;

switch(ch){
case 'r':
read(fd,buf,0);
break;
case 'w':
write(fd,buf,0);
break;
default:
printf("error input!\
");
break;
}

sleep(1);
}

close(fd);
return 0;
}

Makefile

ifeq ($(KERNELRELEASE),)

# Kernel source code path
KERNELDIR ?= /home/xin/6818GEC/kernel
#cross compiler path
CROSS_PATH := /home/xin/6818GEC/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi-
# module source code path
PWD := $(shell pwd)

default:
         $(MAKE) CROSS_COMPILE=$(CROSS_PATH) -C $(KERNELDIR) M=$(PWD) modules
clean:
        rm -rf *.o *.ko *.mod .*.cmd *.mod.* modules.order Module.symvers .tmp_versions

else
#obj-m means to compile and generate a loadable module, and obj-y means to compile the module directly into the kernel.
obj -m := cdd.o

endif

The above is the introduction and use of the character-driven device cdev. If you have any questions, please discuss them in the comment area.