Getting started with Linux drivers (5) - simple character device drivers


foreword

In the family of Linux device drivers, the character device driver is a relatively simple driver, but also a very widely used driver. Therefore, learning character device drivers is very important for building the knowledge structure of Linux device drivers. This blog will lead readers to write a complete character device driver.

Character Device Driver Framework

This section provides a brief analysis of the character device driver framework. There are many very important concepts in the character device driver, the following will start with the simplest concept: character device and block device.

character and block devices

The Linux system divides devices into three categories: character devices, block devices, and network interface devices. Among them, it is difficult to distinguish between character devices and block devices, and an important explanation will be given below.
1、字符设备
字符设备是指那些只能一个字节一个字节读写数据的设备, cannot randomly read a certain data in the device memory. It needs to read data in sequence. From this point of view, character devices are data stream-oriented devices. Common characters include devices such as mouse, keyboard, serial port, console and LED.
2、块设备
块设备是指那些可以从设备的任意位置读取一定长度数据的设备. It does not need to read data in sequence, and can locate a specific location of the device to read data. Common block devices include hard disks, magnetic disks, U disks, SD cards, etc.
3、字符设备和块设备的区分
Each character device or block device /devcorresponds to a device file in the directory. Readers can /devdistinguish whether the device is a character device or a block device by viewing the attributes of the files in the directory. Use cdthe command to enter /devthe directory and execute ls -lthe command to view the properties of the device.
insert image description here
insert image description here
ls -lThe first character c in the first field of the command indicates that the device is a character device, and b indicates that the device is a block device. Field 234 is not relevant for driver development. Fields 5 and 6 represent the major device number and minor device number of the device respectively, which will be explained later. The seventh field indicates the last modification time of the file. The eighth field represents the name of the device.
It can be seen from the 1st and 8th fields that adspit is a character device and dm-0a block device. The adspmajor device number of the device is 14, and the minor device number is 12.

major and minor numbers

A character or block device has a major and minor number. The major device number and the minor device number are collectively referred to as 设备号. The major number is used to identify a specific driver. The minor number is used to denote each device using the driver. For example, an embedded system has two LED indicators, and the LED lights need to be turned on or off independently. Then, you can write a character device driver for LED lights, and you can register its major device number as device No. 5, and its minor device numbers are 1 and 2 respectively. Here, the minor device numbers represent two LED lights respectively.
1、主设备号和次设备的表示
In the Linux kernel, dev_ttypes are used to represent device numbers. In Linux2.6.29.4, dev_tit is defined as an unsigned long integer variable, as follows:

typedef u_long dev_t

u_longIt is 4 bytes on a 32-bit machine and 8 bytes on a 64-bit machine. Take a 32-bit machine as an example, where 高12位表示主设备号,低20位表示次设备号, as follows:
insert image description here
2、主设备号和次设备号的获取
In order to write a portable driver, the number of bits in the major and minor device numbers cannot be assumed. In different models, the number of digits of the major device number and the minor device number may be different. A macro should be used MAJORto get the major number and MINORa macro to get the minor number. Here are the definitions of the two macros:

#define MINORBITS 20 /*次设备号位数*/
#define MINORMASK ((1U << MINORBITES) - 1) /*次设备号掩码*/
#define MAJOR(dev) ((unsigned int)((dev) >> MINORBITS)) /*dev右移20位得到主设备号*/
#define mINOR(dev) ((unsigned int)((dev) & MINORMASK))  /*与次设备掩码与,得到次设备号*/

MAJORThe macro will dev_tmove 20 bits to the right to get the major device number; MAJORthe macro will dev_tclear the upper 12 bits to get the minor device number. Instead, the major and minor numbers can be converted to device types ( dev_t), MKDEVwhich can be done using macros.

#define MKDEV(ma, mi)  (((ma) << MINORBITS) | (mi))

MKDEVThe macro shifts the major device number (ma) to the left by 20 bits, and then ANDs it with the minor device number (mi) to obtain the device number.
3、静态分配设备号
Statically assigning a device number means that the driver developer statically assigns a device number. For some commonly used devices, kernel developers have assigned device numbers to them. These device numbers can documentation/devices.txtbe found in the kernel source files. If only the developer himself uses these device drivers, he can choose an unused device number. When no new hardware is added, this method will not cause device number conflicts. However, when new hardware is added, it is likely to cause device conflicts and affect the use of the device.
4、动态分配设备号
Due to the problem of conflicts in statically allocating device numbers, the kernel community recommends that developers use the method of dynamically allocating device numbers. The function of dynamically allocating device numbers is alloc_chrdev_region()described in the section "Applying and releasing device numbers".
5、查看设备号
When statically assigning device numbers, it is necessary to check the existing device numbers in the system to determine which new device number to use. You can read /proc/devicesthe file to get the device number of the device. /proc/devicesThe file contains device numbers for character devices and block devices. As follows.
insert image description here

Apply for and release device number

The kernel maintains a special data structure used to store the relationship between device numbers and devices. When installing a device, you should apply for a device number for the device so that the system can specify the device number corresponding to the device. Many functions in the device driver operate the device through the device number. In the following, firstly, the device number application is briefly described.
1、申请设备号
Before building a character device, one or more device numbers must be applied to the system. The function that does the job is defined register_chrdev_region()in <fs/char_dev.c>:

int register_chrdev_region(dev_t from, unsigned count, const char *name)

where fromis the starting value of the range of device numbers to be allocated. Generally only fromthe major device number is provided, fromand the minor device number is usually set to 0. countIt is the number of consecutive device numbers that need to be applied for. The last nameis the device name associated with that range number, which cannot be exceeded 64字节.
Like most kernel functions, register_chrdev_region()the function returns 0 on success. On error, a negative error code is returned, and no device number can be assigned to a character device. The following is an example code, which applies for CS5535_GPIO_COUNTa device number.

retval = register_chrdev_region(dev_id, CS5535_GPIO_COUNT, NAME);

There are a lot of character devices in Linux, and conflicts are likely to occur when artificially assigning device numbers to character devices. Linux kernel developers have been working hard to make device numbers dynamic. alloc_chrdev_region()Functions can be used for this purpose.

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

In the above function, devas an output parameter, the allocated device number will be saved after the function returns successfully. The function may apply for a continuous device number, which is devthe first device number returned. baseminorIndicates the first minor device number to apply for, which is usually set to 0. countSame as the corresponding parameter nameto register_chrdev_region()the function. countIndicates the number of consecutive device numbers to apply for, and nameindicates the name of the device. The following is an example code that applies for CS5535_CPIO_COUNT device numbers.

retval = alloc_chrdev_region(&dev_id, 0, CS5535_GPIO_COUNT, NAME);

2、释放设备号
The device number applied for by the above two methods should be released when the device is not in use. The release of the device number uses the following functions uniformly:

void unregister_chrdev_region(dev_t from, unsigned count);

In the above function, fromit indicates the device number to be released, and countindicates fromthe number of device numbers to be released from the beginning. Typically, the function is called within the module's unload function unregister_chrdev_region().

First understanding of cdev structure

After applying for the device number of the character device, at this time, the character device needs to be registered in the system before the character device can be used. In order to understand this implementation process, first explain cdevthe structure.

cdev structure

cdevCharacter devices are described using structures in the Linux kernel . This structure is an abstraction of all character devices, which contains a large number of common features of character devices. cdevThe structure is defined as follows:

struct cdev{
    
    
	struct kobject kobj; /*内嵌的kobject结构,用于内核设备驱动模型的管理*/
	struct module *owner; /*指向包含该结构的模块的指针,用于引用计数*/
	const struct file_operations *ops; /*指向字符设备操作函数集的指针*/
	struct list_head list; /*该结构将使用该驱动的字符设备连接成一个链表*/
	dev_t dev;  /*该字符设备的起始设备号,一个设备可能有多个设备号*/
	unsigned int count; /*使用该字符设备驱动的设备数量*/
};

cdevThe structure in the structure kobjis used by the kernel to manage character devices, and driver developers generally do not use this member. opsis file_operationsa pointer to a structure that defines functions for manipulating character devices. Due to the complexity of this structure, it will be explained in the next section.
devIt is used to store the device number applied for by the character device. countIndicates how many character devices are currently using the driver. When using rmmodthe unload module, if countthe member is not 0, then the system does not allow the unload module.
listThe structure is a doubly linked list, which is used to connect other structures into a doubly linked list. This structure is widely used in the Linux kernel1 and needs to be mastered by the reader.

struct list_head{
    
    
	struct list_head *next, *prev;	
};

insert image description here
As shown above, the members cdevof the structure listare connected to the members inodeof the structure i_devices. Which i_devicesis also a list_headstructure. In this way, cdevthe structure and inodenodes form a doubly linked list. inodeThe structure represents /devthe device files under the directory. This structure is relatively complicated, and its jurisdiction will be described below.
Each character device /devhas a device file in the directory, and opening the device file is equivalent to opening the corresponding character device. For example, if the application program opens device file A, then the system will generate a inodenode. In this way, the character structure can be found through the field inodeof the node . Through the pointer, you can find the operation function of device A. The explanation of the operation function will be placed in the following content.i_cdevc_devcdevops

file_operations structure

file_operationsIt is an abstract structure that operates on the device. The design of the Linux kernel is very clever. The kernel allows a device file to be created for the device, and all operations on the device file are equivalent to operations on the device. The advantage of this is that the user program can use the method of accessing ordinary files to access device files, and then access the device. This method greatly reduces the programmer's programming burden, and the programmer can access the device without getting familiar with the new driver interface.
Access to ordinary files often uses open()、read()、write()、close()、ioctl()methods such as . Similarly, access to device files can also use these methods. These calls eventually lead to file_operationscalls to the corresponding functions in the structure. For programmers, it is enough to write different operation functions for different devices.
In order to increase file_operationsthe functionality, many functions are concentrated in this structure. The definition of this structure is already relatively large, and its definition is as follows:

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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const  struct iovec *, unsigned long, 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);
	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 *, 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 (*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 **);
};

file_operationsThe important members of the structure are explained below .

  • ownermember isn't a function at all; it's a pointer to the module that owns the struct. This member is used to maintain the reference count of the module. When the module is still in use, it cannot be used to rmmodunload the module. Almost all the time, it is simply initialized to THIS_MODULEa <linux/module.h>macro defined in .
  • llseek()The function is used to change the current read/write position in the file and return the new position. loff_tThe argument is a "long long" type, which is 64 bits wide even on 32-bit machines. This is for compatibility with 64-bit machines, because the file size of 64-bit machines can completely exceed 4G.
  • read()The function is used to get data from the device, the function returns the number of bytes read when successful, and returns an error code when it fails.
  • write()The function is used to write data to the device. The function returns the number of bytes written on success, or a negative error code on failure.
  • ioctlFunctions provide a way to execute device-specific commands. For example, to reset the device, which is neither a read operation nor a write operation, is not suitable for implementation read()with and write()methods. ioctlIf an undefined command is passed in to the application , -ENOTTYan error will be returned, indicating that the device does not support this command.
  • open()The function is used to open a device, in which the device can be initialized. If this function is copied NULL, then the device will always be successfully opened and will not affect the device.
  • release()The function is used to release open()the resources requested in the function, and will be called by the system when the file reference count is 0. It corresponds to the method of the application close(), but not every time close()the method is called, the function will be triggered release(), and it will be called only after all the device files are released.

The relationship between cdev and file_operation structure

Generally speaking, the driver developer will put the specific data of a specific device into cdevthe structure to form a new structure. As shown in the figure below, the "custom character device" contains the data of a specific device. cdevThere is a structure in this "custom device" . cdevThe structure has a file_operationspointer to it. Here, file_operationsthe function in can be used to operate the hardware, or other data in the "custom character device", so as to play the role of controlling the device.
insert image description here

inode structure

The kernel uses inodestructures to represent files internally. It is generally passed inodeas a parameter of a function in a structure. file_operationFor example, open()the function will pass a inodepointer in, indicating the currently open file node. It should be noted that inodethe members have been given appropriate values ​​by the system, and the driver only needs to use the information in the node without changing it. open()The function is:

int (*open) (struct inode *, struct file *);

inodeThe structure contains a lot of information about the file. Here, only the fields that are useful for writing drivers are introduced. For more information about this structure, you can refer to the kernel source code.

  • dev_t i_rdev, indicating the device number corresponding to the device file.
  • struct list_head i_devices, this member connects the device file to the corresponding cdevstructure, thus corresponding to its own driver.
  • struct cdev *i_cdevThis member also points to cdevthe device.
    In addition to dev_tgetting the major device number and minor device number from it, you can also use the imajor()and iminor()function to get the major device number and minor device number i_rdevfrom it.
    imajor()The function calls the macro internally MAJOR, as shown in the following code.
static inline unsigned imajor(const struct inode *inode)
{
    
    
	return MAJOR(inode->i_rdev); /*从inode->i_rdev中提取主设备号*/
}

Likewise, iminor()the function calls the macro internally MINOR, as shown in the following code.

static inline unsigned iminor(const struct inode *inode)
{
    
    
	return MINOR(inode->i_rdev); /*从inode->i_rdev中提取次设备号*/
}

Composition of character device drivers

Understanding the composition of character device drivers is very useful for writing drivers. Because character devices have many similarities in structure, as long as you can write a character device driver, it is not difficult to write a similar character device driver. In the Linux system, the character device driver consists of the following parts.

Character device load and unload functions

In the loading function of the character device, the application and cdevregistration of the character device number should be realized. cdevOn the contrary, the release and logout of the character device number should be implemented in the unload function of the character device .
cdevIt is an abstraction of character devices by kernel developers. In addition to cdevthe information in, a specific character device also needs specific information, and the specific information is often placed cdevafter it to form a device structure, such as in the code xxx_dev.
The common device structure, loading function and unloading function are as follows:

struct xxx_dev    /*自定义设备结构体*/
{
    
    
	struct cdev dev;   /*cdev结构体*/
	...         /*特定设备的特定数据*/
};
static int __init xxx_init(void)
{
    
    
	...
	/*申请设备号,当xxx_major不为0时,表示静态指定;当为0时,表示动态申请*/
	if(xxx_major)
		result = register_chrdev_region(xxx_devno, 1, "DEV_NAME"); /*静态申请*/
	else
		result = alloc_chrdev_region(&xxx_devno, 0, 1, "DEV_NAME"); /*动态申请*/
		xxx_major = MAJOR(xxx_devno); /*获取申请的主设备号*/
	/*初始化cdev结构,并传递file_operations结构指针*/
	cdev_init(&xxx_dev.cdev, &xxx_fops);
	dev->cdev.owner = THIS_MODULE;         /*指定所属模块*/ 
	err = cdev_add(&xxx_dev.cdev, xxx_devno, 1); /*注册设备*/
}
static void __exit xxx_exit(void)   /*模块卸载函数*/
{
    
    
	cdev_del(&xxx_dev.cdev);  /*注销cdev*/
	unregister_chrdev_region(xxx_devno, 1); /*释放设备号*/
}

file_operations structure and other member functions

file_operationsThe member functions in the structure correspond to the interface of the driver program, and the user program can call these interfaces through the kernel to control the device. Most character device drivers will implement read()、write()和ioctl()functions, and the common writing methods of these three functions are shown in the following code.

/*文件操作结构体*/
static const struct file_operations xxx_fops =
{
    
    
	.owner = THIS_MODULE,  /*模块引用,任何时候都赋值THIS_MODULE*/
	.read = xxx_read,      /*指定设备的读函数*/
	.write = xxx_write,    /*指定设备的写函数*/
	.ioctl = xxx_ioctl     /*指定设备的控制函数*/
};
/*读函数*/
static ssize_t xxx_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
    
    
	...
	if(size>8)
		copy_to_user(buf,...,...); /*当数据较大时,使用copy_to_user(),效率较高*/
	else
		put_user(....,buf);    /*当数据较小时,使用put_user(),效率较高*/
	....
}
/*写函数*/
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
    
    
	...
	if(size>8)
		copy_from_user(...,buf,...); /*当数据较大时,使用copy_to_user(),效率较高*/
	else
		get_user(...,buf);      /*当数据较小时,使用put_user(),效率较高*/
	....
}
/*ioctl设备控制函数*/
static long xxx_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    
    
	...
	switch(cmd)
	{
    
    
		case xxx_cmd1:
			...  /*命令1执行的操作*/
			break;
		case xxx_cmd2:
			... /*命令2执行的操作*/
			break;
		default:
			return -EINVAL; /*内核和驱动程序都不支持该命令时,返回无效的命令*/
	}
	return 0;
}

xxx_fopsThe pointer to the operation function is saved in the file operation structure . For functions that are not implemented, it is assigned a value of NULL. xxx_fopsThe structure is associated cdev_init()with the character device loading function as a parameter . Device driver functions have the same parameters. It is a pointer to the file structure, pointing to the opened file. is the data address from user space, which cannot be read directly in the driver. is the bytes to read. Is the position for reading and writing, which is equivalent to the beginning of the file. The parameter of the control function is a pre-defined I/O control command, and arg corresponds to the parameter of the command.cdev
read()和write()filpbufsizeppos
xxx_ioctlcmd

Data exchange between driver and application

Data exchange between drivers and applications is very important. file_operationsThe functions in read()和write()are used to exchange data between the driver and the application. Through data exchange, drivers and applications can learn about each other. But drivers and applications belong to different address spaces. The driver cannot directly access the address space of the application program; similarly, the application program cannot directly access the address space of the driver program, otherwise the data in each other's space will be destroyed, resulting in system crash or data destruction.
The safe method is to use the dedicated function provided by the kernel to complete the exchange of data between the application program and the driver program space. These functions strictly check and convert the pointer passed by the user program, so as to ensure the security of data exchange between the user program and the driver program. These functions are:

unsigned long copy_to_user(void __user *to, const void *from, unsigned long);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long);
put_user(local, user);
get_user(local, user);

Character device driver composition summary

A character device is a relatively simple type of device among the three major types of devices (character device, block device, network device). The main work done in its driver is to initialize, add and delete structures, apply for and release device numbers, and cdevfill file_operationOperate functions in the structure, and implement other important functions file_operationin the structure . read()、write()、ioctl()The relationship between the cdev structure file_operationsand the user space call driver is shown in the figure below.
insert image description here

VirtualDisk character device driver

The following parts will be explained based on a VirtualDisk device. VirtualDisk is a virtual disk device, in which 8K continuous memory space is allocated, and two port data (port1 and port2) are defined. The driver program can read, write, control and locate the device, and the program in the user space can access the data in the VirtualDisk device through the Linux system call.

VirtualDisk header files, macros and device structures

The VirtualDisk driver should include necessary header files and macro information, and define a device structure corresponding to the actual device. The related definitions are shown in the following code.

#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#define VIRTUALDISK_SIZE     0x2000  /*全局内存最大8K字节*/
#define MEM_CLEAR    0x1             /*全局内存清零*/
#define PORT1_SET    0x2             /*将port1端口清零*/
#define PORT2_SET    0x3             /*将port2端口清零*/
#define VIRTUALDISK_MAJOR  200        /*预设的VitrualDisk的主设备号为200*/

static int VirtualDisk_major = VIRTUALDISK_MAJOR;
/*VirtualDisk设备结构体*/
struct VirtualDisk
{
    
    
	struct cdev cdev;     /*cdev结构体*/
	unsigned char mem[VIRTUALDISK_SIZE]; /*全局内存8K*/
	int port1;      /*两个不同类型的端口*/
	long port2;
	long count;      /*记录设备目前被多少设备打开*/
};
  • Lines 1 to 11 list the necessary header files, which contain functions that may be used by the driver.
  • 19~26 lines of code define the VirtualDisk device structure. It contains cdevthe character device structure and a continuous 8K device memory. port1In addition, two ports and are defined port2, which are used to simulate the ports of actual devices. countIndicates the number of times the device was opened. In the driver program, it is not necessary to put these members in a structure, but the benefit of putting them together is to use the object-oriented encapsulation idea to encapsulate the device-related members into a whole.
  • Line 22 defines an 8K memory block. The driver generally does not allocate memory statically, because the life cycle of statically allocated memory is very long, and it lives and dies with the driver. The driver generally runs in the entire boot state of the system, so the memory allocated by the driver will never be released. Therefore, when writing a driver, you should avoid applying for large blocks of memory and statically allocating memory. Here, just for the convenience of demonstration, static memory is allocated.

Load and unload drivers

The loading and unloading templates of character device drivers have been introduced above. The loading and unloading functions of VirtualDisk are also similar to those described above, and their implementation is as follows:

/*设备驱动模型加载函数*/
int VirtualDisk_init(void)
{
    
    
	int result;
	dev_t devno = MKDEV(VirtualDisk_major, 0); /*构建设备号*/
	/*申请设备号*/
	if(VirtualDisk_major)
		result = register_chardev_region(devno, 1, "VirtualDisk");
	else
		result = alloc_chrdev_region(&devno, 0, 1, "VirtualDisk");
		VirtualDisk_major = MAJOR(devno); /*从申请设备号中得到主设备号*/
	if(result < 0)
		return result;
	/*动态申请设备结构体的内存*/
	struct VirtualDisk *Virtualdisk_devp = kmalloc(sizeof(struct VitualDisk), GFP_KERNEL);
	if(!Vitualdisk_devp) /*申请失败*/
	{
    
    
		result = -ENOMEM;
		goto fail_kmalloc;
	} 
	memset(Virtualdisk_devp, 0, sizeof(struct VirtualDisk));/*清零*/
	/*初始化并且添加cdev结构体*/
	VirtualDisk_setup_cdev(Virtualdisk_devp, 0);
	return 0;
fail_kmalloc:
	unregister_chrdev_region(devno, 1);
	return result;
}
/*模块卸载函数*/
void VirtualDisk_exit(void)
{
    
    
	cdev_del(&Vitualdisk_devp->cdev);  /*注销cdev*/
	kfree(Virtualdisk_devp);   /*释放设备结构体内存*/
	unregister_chrdev_region(MKDEV(VirtualDisk_major, 0), 1); /*释放设备号*/
}
  • Lines 7~13, use two methods to apply for the device number. VirtualDisk_majorThe variable is statically defined as 200. Do not set equal to 0 when loading modules VirtualDisk_major. Then execute register_chrdev_region()the function to statically allocate a device number; if it VirtualDisk_majoris equal to 0, then use alloc_chrdev_region()the function to dynamically allocate a device number and devnoreturn it by the parameter. Line 12, use MAJORthe macro to return the obtained major device number.
  • Lines 17~22 allocate a VirtualDisk device structure.
  • Line 23, clear the allocated VirtualDisk device structure.
  • Line 25, call a custom VirtualDisk_setup_cdev()function to initialize cdevthe structure and add it to the kernel. This function will be described below.
  • cdevLines 32~37 are the unloading function. In this function, the structure is canceled , VirtualDiskthe memory occupied by the device is released, and the device number occupied by the device is released.

Initialization and registration of cdev

The function called in the previous code VirtualDisk_setup_cdev()completes the initialization and registration of cdev, and its code is as follows:

/*初始化并注册cdev*/
static void VirtualDisk_setup_cdev(struct VirutalDisk *dev, int minor)
{
    
    
	int err;
	devno = MKDEV(VirtualDisk_major, minor);  /*构造设备号*/
	cdev_init(&dev->cdev, &VirtualDisk_fops); /*初始化cdev设备*/
	dev->cdev.owner = THIS_MODULE;  /*使驱动程序属于该模块*/
	dev->cdev.ops = &VirtualDisk_fops; /*cdev连接file_operations指针*/
	err = cdev_add(&dev->cdev, devno, 1); /*将cdev注册到系统中*/
	if(err)
		printk(KERNEL_NOTICE "Error in cdev_add()\n");
}

Here is a brief explanation of the function:

  • Line 5, use MKDEVa macro to construct a device number with a major device number of VirtualDisk_major, and a minor device minornumber of
  • Line 6 calls cdev_init()the function and associates the device structure cdevwith file_operationsthe pointer. This file operation pointer is defined as shown in the following code.
/*文件操作结构体*/
static const struct file_operation VirtualDisk_fops =
{
    
    
	.owner = THIS_MODULE,
	.llseek = VirtualDisk_llseek,  /*定位偏移量函数*/
	.read = VirtualDisk_read,     /*读设备函数*/
	.write = VirtualDisk_write,   /*写设备函数*/
	.ioctl = VirtualDisk_ioctl,   /*控制函数*/
	.open = VirtualDisk_open,     /*打开设备函数*/
	.release = VirtualDisk_release, /*释放设备函数*/
}
  • Line 8, designated VirtualDisk_fopsas the file operation function pointer of the character device.
  • Line 9, call cdev_addthe function to add the character device to the kernel.
  • Lines 10 and 11, if the registration of the character device fails, return.

open and release functions

When the user program calls open()the function to open the device file, the kernel will finally call VirtualDisk_open()the function. The code for this function is as follows:

/*文件打开函数*/
int VirutalDisk_open(struct inode *inode, struct file *filp)
{
    
    
	/*将设备结构体指针赋值给文件私有数据指针*/
	filp->private_data = Virtualdisk_devp;
	struct VirtualDisk *devp = filp->private_data; /*获得设备结构体指针*/
	devp->count++;     /*增加设备打开次数*/
	return 0; 
}

Here is a brief explanation of the function:

  • Lines 5 and 6 will Virtualdisk_devpbe assigned to the private data pointer, which will be used later.
  • Line 7, increment the device open count by 1.
    When the user program calls close()the function to close the device file, the kernel will finally call VirtualDisk_release()the function. This function is mainly about decrementing the counter by 1. The function code is as follows.
/*文件释放函数*/
int VirtualDisk_release(struct inode *inode, struct file *filp)
{
    
    
	struct VirtualDisk *devp = filp->private_data; /*获得设备结构体指针*/
	devp->count--;   /*减少设备打开次数*/
	return 0;
}

read and write functions

When the user program calls read()the function to read the data in the device file, the kernel will finally call VirtualDisk_read()the function. The code for this function is as follows:

/*读函数*/
static ssize_t VirtualDisk_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
    
    
	unsigned long p = *ppos;  /*记录文件指针偏移位置*/
	unsigned int count = size; /*记录需要读取的字节数*/
	int ret = 0; /*返回值*/
	struct VirtualDisk *devp = filp->private_data; /*获得设备结构体指针*/
	/*分析和获得有效的读长度*/
	if(p >= VIRTUALDISK_SIZE)  /*需要读取的偏移大于设备内存空间*/
		return count? -ENXIO: 0;  /*读取地址错误*/
	if(count > VIRTUALSIZE - p) /*要读取的字节大于设备的内存空间*/
		count = VIRTUALSIZE - p;   /*将要读取的字节数设为剩余的字节数*/
	/*内核空间->用户空间交换数据*/
	if(copy_to_user(buf, (void *)(devp->mem + p), count))
		ret = -EFAULT;
	else
		*ppos += count;
		ret = count;
		printk(KERNEL_INFO "read %d bytes(s) from %d\n",count, p);
	return ret;
}

The following is a brief analysis of the function

  • Lines 5~7 define some local variables
  • Line 8, get the device structure pointer from the file pointer.
  • Line 10, it is an error if the location to read is larger than the size of the device.
  • Line 12, if the position of the data to be read is greater than the size of the device, only read to the end of the device.
  • Lines 15 to 24, copy data from user space to the device. If the copy data is successful, the offset position of the file is added to the number of read data.
    When the user program calls write()the function to write data to the device file, the kernel will finally call VirtualDisk_write()the function. The code for this function is as follows:
/*写函数*/
static ssize_t VirtualDisk_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
    
    
	unsigned long p = *ppos;  /*记录文件指针偏移位置*/
	int ret = 0;    /*返回值*/
	unsigned int count = size;  /*记录需要写入的字节数*/
	struct VirtualDisk *devp = file ->private_data; /*获得设备结构体指针*/
	/*分析和获取有效的写长度*/
	if(p >= VIRTUALDISK_SIZE)   /*要写入的偏移大于设备的内存空间*/
		return count ? -ENXIO: 0;  /*写入地址错误*/
	if(count > VIRTUALDISK - p) /*要写入的字节大于设备的内存空间*/
		count = VIRTUALDISK_SIZE - p;  /*将要写入的字节数设为剩余的字节数*/
	/*用户空间->内核空间*/
	if(copy_from_user(devp->mem + p , buf, count))
		ret = -EFAULT;
	else
		*ppos += count;   /*增加偏移位置*/
		ret = count;   /*返回实际的写入字节数*/
		printk(KERNEL_INFO "write %d bytes(s) from %d\n",count, p);
	return ret;
}

The following is a brief introduction to the function

  • Lines 5~7 define some local variables
  • Line 8, get the device structure pointer from the file pointer.
  • Line 10, error if the location to read is larger than the size of the device.
  • Line 12, if the position of the data to be read is greater than the size of the device, only read to the end of the device.
  • Lines 15-24, copy data from the device to user space. If the copy data is successful, the offset position of the file is added to the number of written data.

seek function

When the user program calls fssek()the function to move the file pointer in the device file, the kernel will eventually call VirtualDisk_llseek()the function. The code for this function is as follows:

/*seek文件定位函数*/
static loff_t VirtualDisk_llseek(struct file *filp, loff_t offset, int orig)
{
    
    
	loff_t ret = 0;   /*返回的位置偏移*/
	switch(orig)
	{
    
    
		case SEEK_SET:
			if(offset <  0)
			{
    
    
				ret = - EINVAL
				break;
			}
			if((unsigned int )offset > VIRTUALDISK_SIZE)
			{
    
    
				ret = - EINVAL;
				break;
			}
			filp->f_pos = (unsigned int) offset;
			ret = filp->f_pos;
			break;
		case SEEK_CUR:
			if((filp->f_pos + offset) > VIRTUALDISK_SIZE)
			{
    
    
				ret = - EINVAL;
				break;
			}
			if((filp->f_pos + offset) < 0)
			{
    
    
				ret = - EINVAL
				break;
			}
			filp->f_pos += offset;
			ret = filp->f_pos;
			break;
		default:
			ret = - EINVAL;
			break;
	}
	return ret;
}

Here is a brief introduction to this function:

  • Line 4 defines a return value to indicate the current offset of the file pointer.
  • 5 lines, used to select the moving direction of the file pointer.
  • Lines 7~20 indicate that the type of file pointer movement is SEEK_SET, indicating that the pointer is moved relative to the beginning of the file offset.
  • Lines 8~12, if the offset is less than 0, an error will be returned.
  • Lines 13~17, if the offset is greater than the length of the file, an error will be returned.
  • Line 18, set the offset value of the file to filp->f_pos, this pointer indicates the current location of the file.
  • Lines 21 to 34 indicate that the type of file pointer movement is yes SEEK_CUR, indicating that the pointer is moved relative to the current position of the file offset.
  • Lines 22~26, if the offset value is greater than the length of the file, an error will be returned.
  • Lines 27~31 indicate that the pointer is less than 0, and the pointer is illegal in this case.
  • Line 32, add an offset filp->f_posto the offset value of the file.offset
  • Lines 35 and 36 indicate that the command is not SEEK_SETor SEEK_CUR. In this case, it means that an illegal command has been passed in, and it will return directly.

ioctl() function

When a user program calls ioctl()a function to change the functionality of the device, the kernel will eventually call VirtualDisk_ioctl()the function. The code for this function is as follows:

/*ioctl设备控制函数*/
static int VirtualDisk_ioctl(struct inode *inodep, struct file *filp, unsigned int cmd, unsigned long arg)
{
    
    
	struct VirtualDisk *devp = filp->private_date;
	switch(cmd)
	{
    
    
		case MEM_CLEAR:  /*设备内存清零*/
			memset(devp->mem, 0, VIRTUALDISK_SIZE);
			printk(KERNEL_INFO "VirtualDisk is set to zero\n");
			break;
		case PORT1_SET:  /*将端口1置为0*/
			devp->port1 = 0;
			break;
		case PORT2_SET: /*将端口2置0*/
			devp->port2 = 0;
			break;
		default:
			return -EINVAL;
	}
	return 0;
}

Here is a brief introduction to this function:

  • Line 5, the private data of the file is obtained, and the pointer of the device is stored in the private data VirtualDisk.
  • Lines 6~20, ioctl()judge the operation to be performed according to the parameters passed in by the function. The character device here supports 3 operations, the first operation is to clear all the memory of the character device, the second is to set port 1 to 0, and the third is to set port 2 to 0.

summary

Describes character device drivers. Character device is one of the three major devices in Linux. Many devices can be regarded as character devices, so it is very useful to learn the programming of character device drivers. This chapter first introduces the frame structure of the character device as a whole, and then introduces the structure of the character device struct cdev. Then it introduces the composition of the character device, and finally explains a VirtualDiskcharacter device driver in detail.

Guess you like

Origin blog.csdn.net/m0_56145255/article/details/131411393