Linux kernel learning 10 -- writing character device drivers

Posted by CBG on Thu, 03 Feb 2022 07:41:40 +0100

I

In the linux kernel, the character device is described by the cdev structure, which is located in / include / linux / cdev H medium

/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_CDEV_H
#define _LINUX_CDEV_H
 
#include <linux/kobject.h>
#include <linux/kdev_t.h>
#include <linux/list.h>
#include <linux/device.h>
 
struct file_operations;
struct inode;
struct module;
 
struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;   //List field to organize all character devices into a linked list. Each equipment is determined by the main equipment number and the secondary equipment number,
	dev_t dev;      //dev is the device number of the character device, including the primary device number and secondary device number
	unsigned int count;   //The count field is the number of secondary device numbers in the same primary device number
} __randomize_layout;
 
void cdev_init(struct cdev *, const struct file_operations *);
 
struct cdev *cdev_alloc(void);
 
void cdev_put(struct cdev *p);
 
int cdev_add(struct cdev *, dev_t, unsigned);
 
void cdev_set_parent(struct cdev *p, struct kobject *kobj);
int cdev_device_add(struct cdev *cdev, struct device *dev);
void cdev_device_del(struct cdev *cdev, struct device *dev);
 
void cdev_del(struct cdev *);
 
void cd_forget(struct inode *);
 
#endif

The device driver of linux can be defined in two forms: one is the global static variable, and the other is to use the API provided by the kernel. Here, the second method is used to realize the driver of a simple virtual device and its read-write function.

First look at the kernel state code

device_drive.c

# include <linux/module.h>
# include <linux/fs.h>
# include <linux/uaccess.h>
# include <linux/init.h>
# include <linux/cdev.h>
 
# define DEMO_NAME "my_demo_dev"
 
static dev_t dev;   //Equipment number
static struct cdev *demo_cdev;
static signed count = 1;
 
 
static int demodrv_open(struct inode *inode, struct file *file)
{   
    //The method of reading primary device number and secondary device number provided by Linux kernel
	int major = MAJOR(inode->i_rdev);   
	int minor = MINOR(inode->i_rdev);
 
	printk("%s: major=%d, minor=%d\n",__func__,major,minor);  //__ func__ Macro gets the current function name
 
	return 0;
}
 
 
static ssize_t demodrv_read(struct file *file, char __user *buf,size_t lbuf,loff_t *ppos)
{
	printk("%s enter\n",__func__);   //Print function name
	
	return 0;
}
 
 
static ssize_t demodrv_write(struct file *file, const char __user *buf,size_t count,loff_t *f_pos)
{
	printk("%s enter\n",__func__);
	
	return 0;
}
 
 //The operation of the device is the same structure as that used in the file system
static const struct file_operations demodrv_fops = {
	.owner = THIS_MODULE,
	.open = demodrv_open,
	.read = demodrv_read,
	.write = demodrv_write
};
 
 
static int __init simple_char_init(void)
{
	int ret;
 
	ret = alloc_chrdev_region(&dev,0,count,DEMO_NAME);
	if(ret)
	{
		printk("failed to allocate char device region\n");
		return ret;
	}
	demo_cdev = cdev_alloc();   //Allocate space
	if(!demo_cdev) 
	{
		printk("cdev_alloc failed\n");
		goto unregister_chrdev;
	}
 
	cdev_init(demo_cdev,&demodrv_fops);
 
	ret = cdev_add(demo_cdev,dev,count);
	if(ret)
	{
		printk("cdev_add failed\n");
		goto cdev_fail;
	}
 
	printk("successed register char device: %s\n",DEMO_NAME);
	printk("Major number = %d,minor number = %d\n",MAJOR(dev),MINOR(dev));
 
	return 0;
 
cdev_fail:
	cdev_del(demo_cdev);
 
unregister_chrdev:
	unregister_chrdev_region(dev,count);
 
	return ret;
} 
 
 
static void __exit simple_char_exit(void)
{
	printk("removing device\n");
 
	if(demo_cdev)
		cdev_del(demo_cdev);
 
	unregister_chrdev_region(dev,count);
}
 
module_init(simple_char_init);
module_exit(simple_char_exit);
 
MODULE_LICENSE("GPL");

The kernel module initialization function executes alloc_chrdev_region function, enter the source code, located in fs/char_dev.c.

/**
 * alloc_chrdev_region() - register a range of char device numbers
 * @dev: output parameter for first assigned number
 * @baseminor: first of the requested range of minor numbers
 * @count: the number of minor numbers required
 * @name: the name of the associated device or driver
 *
 * Allocates a range of char device numbers.  The major number will be
 * chosen dynamically, and returned (along with the first minor number)
 * in @dev.  Returns zero or a negative error code.
 */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)
{
	struct char_device_struct *cd;
	cd = __register_chrdev_region(0, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);
	*dev = MKDEV(cd->major, cd->baseminor);
	return 0;
}

It will be executed later__ register_chrdev_region function. The first parameter is 0, and the main equipment number is automatically assigned.

Then use cdev_alloc function to allocate space. The type defined here is struct cdev *

Next, CDEV will be executed_ Init and perform the assignment operation of fops

Enter the source code and take a look: cedv_alloc allocates space and returns a pointer to the CDEV structure. cdev_init initializes CDEV and assigns fops one more step

If you define a struct cdev structure instead of a pointer type, you only need to execute cdev_init() is OK

/**
 * cdev_alloc() - allocate a cdev structure
 *
 * Allocates and returns a cdev structure, or NULL on failure.
 */
struct cdev *cdev_alloc(void)
{
	struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	if (p) {
		INIT_LIST_HEAD(&p->list);
		kobject_init(&p->kobj, &ktype_cdev_dynamic);
	}
	return p;
}
 
/**
 * cdev_init() - initialize a cdev structure
 * @cdev: the structure to initialize
 * @fops: the file_operations for this device
 *
 * Initializes @cdev, remembering @fops, making it ready to add to the
 * system with cdev_add().
 */
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
	memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops;
}

Next, execute cdev_add to add this device to the system.

In the implementation method, we are in demodrv_ Print the primary and secondary device numbers in the open operation

In demodrv_read and demodrv_ Print only function names in write

Makefile

#Makefile file note: if the previous C file named first c. So here's the makefile file o Wen
#It's going to be called first O only the root user can load and unload modules
obj-m:=device_drive.o                          #Generate device_ Object file of drive module
#The object file should be the same as the module name
CURRENT_PATH:=$(shell pwd)             #Current path of the module
LINUX_KERNEL:=$(shell uname -r)        #Current version of linux kernel code
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
 
all:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules    #Compilation module
#[Tab] the path of the kernel where the current directory is compiled indicates that the kernel module is compiled
 
clean:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean      #Cleaning module

Compile and insert kernel modules

Using dmesg to view kernel messages

Here you can see that the character device has been successfully registered, my_demo_dev is the device name, the primary device number is 243, and the secondary device number is 0

In addition, the generated device needs to generate the corresponding node in the / dev directory, which needs to be generated manually

Using the mknod command

mknod /dev/demo_drv c 243 0

c stands for character equipment, the main equipment number is 243, and the secondary equipment number is 0

Then check the / dev directory

here

Next, use the user space test program to test the character device driver

User space is a program for testing character equipment c

# include <stdio.h>
# include <fcntl.h>
# include <unistd.h>
 
# define DEMO_DEV_NAME "/dev/demo_drv"
 
int main() 
{
	char buffer[64];
	int fd;
 
	fd = open(DEMO_DEV_NAME,O_RDONLY);
	if(fd<0) 
	{
		printf("open device %s failed\n",DEMO_DEV_NAME);
		return -1;
	}
 
	read(fd,buffer,64);
	close(fd);
 
	return 0;
}

Define the path of the device in this test file

Perform an open operation, and the read operation only prints the function name

Compile and execute the user test program

Print kernel messages using dmesg

Print out the methods of open and read

II

Character device drivers can also be registered using misc mechanism, that is, Linux divides some character devices that do not meet the predetermined characters into miscellaneous devices. The main device number of such devices is 10, which is described by miscdevice structure in the kernel

If you use the misc mechanism to create a device, you need to define the miscdevice structure. Let's take a look at the second experiment

Kernel module

drive2.c

# include <linux/module.h>
# include <linux/fs.h>
# include <linux/uaccess.h>
# include <linux/init.h>
# include <linux/cdev.h>
//Adding misc mechanism
# include <linux/miscdevice.h>
# include <linux/kfifo.h>
 
DEFINE_KFIFO(mydemo_fifo,char,64);
 
//Device name
# define DEMO_NAME "my_demo_dev"
 
static struct device *mydemodrv_device;
 
static int demodrv_open(struct inode *inode, struct file *file)
{
	int major = MAJOR(inode->i_rdev);
	int minor = MINOR(inode->i_rdev);
 
	printk("%s: major=%d, minor=%d\n",__func__,major,minor);
 
	return 0;
}
 
 
static ssize_t demodrv_read(struct file *file, char __user *buf,size_t count,loff_t *ppos)
{
	int actual_readed;
	int ret;
 
	ret = kfifo_to_user(&mydemo_fifo,buf, count, &actual_readed);
	if(ret)
		return -EIO;
 
	printk("%s,actual_readed=%d,pos=%lld\n",__func__,actual_readed,*ppos);
 
	return actual_readed;
}
 
 
static ssize_t demodrv_write(struct file *file, const char __user *buf,size_t count,loff_t *ppos)
{
	unsigned int actual_write;
	int ret;
 
	ret = kfifo_from_user(&mydemo_fifo,buf, count, &actual_write);
	if(ret)
		return -EIO;
 
	printk("%s: actual_write=%d,ppos=%lld\n",__func__,actual_write,*ppos);
 
	return actual_write;
}
 
 
static const struct file_operations demodrv_fops = {
	.owner = THIS_MODULE,
	.open = demodrv_open,
	.read = demodrv_read,
	.write = demodrv_write,
};
 
static struct miscdevice mydemodrv_misc_device = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = DEMO_NAME,
	.fops = &demodrv_fops,  //Corresponding operation of equipment
};
 
static int __init simple_char_init(void)
{
	int ret;
 
	ret = misc_register(&mydemodrv_misc_device);
	if(ret)
	{
		printk("failed register misc device\n");
		return ret;
	}
 
	mydemodrv_device = mydemodrv_misc_device.this_device;
 
	printk("successed register char device: %s\n",DEMO_NAME);
 
	return 0;
} 
 
 
static void __exit simple_char_exit(void)
{
	printk("removing device\n");
 
	misc_deregister(&mydemodrv_misc_device);
}
 
module_init(simple_char_init);
module_exit(simple_char_exit);
 
MODULE_LICENSE("GPL");

Let's look at the kernel module initialization function. First, use the kernel API: misc_register() function to register. You can automatically create a device node without mknod to manually create a device node. The passed in parameter is the address of the defined miscdevice structure

Makefile

#Makefile file note: if the previous C file named first c. So here's the makefile file o Wen
#It's going to be called first O only the root user can load and unload modules
obj-m:=drive2.o                          #Generate the target file of drive2 module
#The object file should be the same as the module name
CURRENT_PATH:=$(shell pwd)             #Current path of the module
LINUX_KERNEL:=$(shell uname -r)        #Current version of linux kernel code
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
 
all:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules    #Compilation module
#[Tab] the path of the kernel where the current directory is compiled indicates that the kernel module is compiled
 
clean:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean      #Cleaning module

Compile and insert kernel modules

View the contents under / dev ll /dev

You can see that the main device number of the generated device file is 10misc and the secondary device number is 53

User mode test program test c

# include <stdio.h>
# include <fcntl.h>
# include <unistd.h>
# include <malloc.h>
# include <string.h>
 
# define DEMO_DEV_NAME "/dev/my_demo_dev"
 
int main() 
{
	char buffer[64];
	int fd;
	int ret;
	size_t len;
 
	char message[] = "hello";
	char *read_buffer;
 
	len = sizeof(message);
 
	fd = open(DEMO_DEV_NAME,O_RDWR);
	if(fd<0) 
	{
		printf("open device %s failed\n",DEMO_DEV_NAME);
		return -1;
	}
 
	//Write data to device
	ret = write(fd,message,len);
	if(ret != len)
	{
		printf("cannot write on device %d,ret=%d\n",fd,ret);
		return -1;
	}
 
	read_buffer = malloc(2*len);
	memset(read_buffer,0,2*len);
	//Turn off the device
 
	ret = read(fd,read_buffer,2*len);
	printf("read %d bytes\n",ret);
	printf("read buffer=%s\n",read_buffer);
 
	close(fd);
 
	return 0;
}

Perform an open, write, and read operation

Use gcc to compile and execute

You can see that hello is read, which is 6 bytes

Use dmesg to view the kernel message and print the corresponding information

This is a simple character device driver to share with you.

Topics: Linux Operation & Maintenance