Linux驱动入门(五)——简单的字符设备驱动程序


前言

在Linux设备驱动程序的家族中,字符设备驱动程序是较为简单的驱动程序,同时也是应用非常广泛的驱动程序。所以学习字符设备驱动程序,对构建Linux设备驱动程序的知识结构非常重要。本篇博客将带领读者编写一个完整的字符设备驱动程序。

字符设备驱动程序框架

本节对字符设备驱动程序框架进行了简要的分析。字符设备驱动程序中有许多非常重要的概念,下面将从最简单的概念讲起:字符设备和块设备。

字符设备和块设备

Linux系统将设备分为3类:字符设备、块设备和网络接口设备。其中字符设备可块设备难以区分,下面对其进行重要讲解。
1、字符设备
字符设备是指那些只能一个字节一个字节读写数据的设备,不能随机读取设备内存中的某一数据。其读取数据需要按照先后顺序,从这点来看,字符设备是面向数据流的设备。常见的字符有鼠标、键盘、串口、控制台和LED等设备。
2、块设备
块设备是指那些可以从设备的任意位置读取一定长度数据的设备。其读取数据不必按照先后顺序,可以定位到设备的某一具体位置,读取数据。常见的块设备有硬盘、磁盘、U盘、SD卡等。
3、字符设备和块设备的区分
每个字符设备或块设备都在/dev目录下对应一个设备文件。读者可以通过查看/dev目录下的文件的属性,来区分设备是字符设备还是块设备。使用cd命令进入/dev目录,并执行ls -l命令就可以查看设备的属性。
在这里插入图片描述
在这里插入图片描述
ls -l命令的第一字段中的第一字符c表示设备是字符设备,b表示设备是块设备。第234字段对驱动程序开发来说没有关系。第5,6字段分别表示设备的主设备号和次设备号,将在后面讲解。第7字段表示文件的最后修改时间。第8字段表示设备的名字。
由第1和8字段可知,adsp是字符设备,dm-0是块设备。其中adsp设备的主设备号是14,次设备号是12。

主设备号和次设备号

一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。
1、主设备号和次设备的表示
在Linux内核中,dev_t类型用来表示设备号。在Linux2.6.29.4中,dev_t定义为一个无符号长整型变量,如下,:

typedef u_long dev_t

u_long在32位机中是4字节,在64位机中是8字节。以32位机为例,其中高12位表示主设备号,低20位表示次设备号,如下如所示:
在这里插入图片描述
2、主设备号和次设备号的获取
为了写出可移植的驱动程序,不能假定主设备号和次设备号的位数。不同机型中,主设备号和次设备号的位数可能是不同的。应该使用MAJOR宏得到主设备号,使用MINOR宏来得到次设备号。下面是两个宏的定义:

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

MAJOR宏将dev_t向右移动20位,得到主设备号;MAJOR宏将dev_t的高12位清零,得到次设备号。相反,可以将主设备号和次设备转换成设备类型(dev_t),使用宏MKDEV可以完成这个功能。

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

MKDEV宏将主设备号(ma)左移20位,然后与次设备号(mi)相与,得到设备号。
3、静态分配设备号
静态分配设备号,就是驱动程序开发者,静态地指定一个设备号。对于一部分常用的设备,内核开发者已经为其分配了设备号。这些设备号可以在内核源码documentation/devices.txt文件中找到。如果只有开发者自己使用这些设备驱动程序,那么其可以选择一个尚未使用过的设备号。在不添加新硬件的时候,这种方式不会产生设备号冲突。但是当添加新硬件时,则很可能造成设备冲突,影响设备的使用。
4、动态分配设备号
由于静态分配设备号存在冲突的问题,所以内核社区建议开发者使用动态分配设备号的方法。动态分配设备号的函数是alloc_chrdev_region(),该函数将在"申请和释放设备号"一节讲述。
5、查看设备号
当静态分配设备号时,需要查看系统中已经存在的设备号,从而决定使用那个新设备号。可以读取/proc/devices文件获得设备的设备号。/proc/devices文件包含字符设备和块设备的设备号。如下所示。
在这里插入图片描述

申请和释放设备号

内核维护着一个特殊的数据结构,用来存放设备号与设备的关系。在安装设备时,应该给设备申请一个设备号,使系统可以明确设备对应的设备号。设备驱动程序中的很多功能,是通过设备号来操作设备的。下面,首先对申请设备号进行简述。
1、申请设备号
在构建字符设备之前,首先要向系统申请一个或者多个设备号。完成该工作的函数是register_chrdev_region(),该函数在<fs/char_dev.c>中定义:

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

其中,from是要分配的设备号范围的起始值。一般只提供from主设备号,from的次设备号通常被设置成0。count是需要申请的连续设备号的个数。最后的name是和该范围编号相关联的设备名称,该名称不能超过64字节
和大多数内核函数一样,register_chrdev_region()函数成功时返回0。错误时,返回一个负的错误码,并且不能为字符设备分配设备号。下面是一个例子代码,其申请了CS5535_GPIO_COUNT个设备号。

retval = register_chrdev_region(dev_id, CS5535_GPIO_COUNT, NAME);

在Linux中有非常多的字符设备,在人为的为字符设备分配设备号时,很可能发生冲突。Linux内核开发者一直在努力将设备号变成动态的。可以使用alloc_chrdev_region()函数达到这个目的。

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

上面的函数中,dev作为输出参数,在函数返回成功后将保存已经分配的设备号。函数有可能申请一段连续的设备号,这是dev返回的第一个设备号。baseminor表示要申请的第一个次设备号,其通常设为0。countnameregister_chrdev_region()函数的对应参数一样。count表示要申请的连续设备号个数,name表示设备的名字。下面是一个例子代码,其申请了CS5535_CPIO_COUNT个设备号。

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

2、释放设备号
使用上面两种方式申请的设备号,都应该在不使用设备时,释放设备号。设备号的释放统一使用下面的函数:

void unregister_chrdev_region(dev_t from, unsigned count);

在上面这个函数中,from表示要释放的设备号,count表示从from开始要释放的设备号个数。通常,在模块的卸载函数中调用unregister_chrdev_region()函数。

初识cdev结构

当申请字符设备的设备号后,这时,需要将字符设备注册到系统中,才能使用字符设备。为了理解这个实现过程,首先解释一下cdev结构体。

cdev结构体

在Linux内核中使用cdev结构体描述字符设备。该结构体是所有字符设备的抽象,其包含了大量字符设备所共有的特征。cdev结构体定义如下:

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

cdev结构中的kobj结构用于内核管理字符设备,驱动开发人员一般不使用该成员。ops是指向file_operations结构的指针,该结构定义了操作字符设备的函数。由于此结构体较为复杂,所以将在后面一节进行讲解。
dev就是用来存储字符设备所申请的设备号。count表示目前有多少个字符设备在使用该驱动程序。当使用rmmod卸载模块时,如果count成员不为0,那么系统不允许卸模块。
list结构是一个双向链表,用于将其他结构体连接成一个双向链表。该结构在Linux内核中广泛使用1,需要读者掌握。

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

在这里插入图片描述
如上图所示,cdev结构体的list成员连接到了inode结构体i_devices成员。其中i_devices也是一个list_head结构。这样,使cdev结构与inode结点组成了一个双向链表。inode结构体表示/dev目录下的设备文件,该结构体较为复杂,所辖将在下面讲述。
每一个字符设备在/dev目录下都有一个设备文件,打开设备文件就相当于打开相应的字符设备。例如应用程序打开设备文件A,那么系统会产生一个inode结点。这样可以通过inode结点的i_cdev字段找到c_dev字符结构体。通过cdevops指针,就能找到设备A的操作函数。对操作函数的讲解,将放在后面的内容中。

file_operations结构体

file_operations是一个对设备进行操作的抽象结构体。Linux内核的设计非常巧妙。内核允许为设备建立一个设备文件,对设备文件的所有操作,就相当于对设备的操作。这样的好处是,用户程序可以使用访问普通文件的方法访问设备文件,进而访问设备。这样的方法,极大地减轻了程序员的编程负担,程序员不必去熟悉新的驱动接口,就能访问设备。
对普通文件的访问常常使用open()、read()、write()、close()、ioctl()等方法。同样对设备文件的访问,也可以使用这些方法。这些调用最终会引起对file_operations结构体中对应函数的调用。对于程序员来说,只要为不同的设备编写不同的操作函数就可以了。
为了增加file_operations的功能,所以将很多函数集中在了该结构中。该结构的定义目前已经比较庞大了,其定义如下:

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_operations结构体的重要成员进行讲解。

  • owner成员根本不是一个函数;它是一个指向拥有这个结构模块的指针。这个成员用来维持模块的引用计数,当模块还在使用时,不能用rmmod卸载模块。几乎所有时刻,它被简单初始化为THIS_MODULE,一个在<linux/module.h>中定义的宏。
  • llseek()函数用来改变文件中的当前读/写位置,并将新位置返回。loff_t参数是一个"long long"类型,“long long”类型即使在32位机上也是64位宽。这是为了与64位机兼容而定的,因为64位机的文件大小完全可以突破4G。
  • read()函数用来从设备获取数据,成功时函数返回读取的字节数,失败时返回一个错误码。
  • write()函数用来写数据到设备中。成功时该函数返回写入的字节数,失败时返回一个负的错误码。
  • ioctl函数提供了一个执行设备特定命令的方法。例如使设备复位,这既不是读操作也不是写操作,不适合用read()write()方法来实现。如果在应用程序中给ioctl传入没有定义的命令,那么将返回-ENOTTY的错误,表示该设备不支持这个命令。
  • open()函数用来打开一个设备,在该函数中可以对设备进行初始化。如果这个函数被复制NULL,那么设备打开永远成功,并不会对设备产生影响。
  • release()函数用来释放open()函数中申请的资源,将在文件引用计数为0时,被系统调用。其对应应用程序的close()方法,但并不是每次调用close()方法,都会触发release()函数,在对设备文件的所有都释放后,才会被调用。

cdev和file_operation结构体的关系

一般来说,驱动开发人员会将特定设备的特定数据放到cdev结构体后,组成一个新的结构体。如下图,“自定义字符设备”中就包含特定设备的数据。该“自定义设备”中有一个cdev结构体。cdev结构体中有一个指向file_operations的指针。这里,file_operations中的函数就可以用来操作硬件,或者“自定义字符设备”中的其他数据,从而起到控制设备的作用。
在这里插入图片描述

inode结构体

内核使用inode结构在内部表示文件。inode一般作为file_operation结构中函数的参数传递过来。例如,open()函数将传递一个inode指针进来,表示目前打开的文件节点。需要注意的是,inode成员已经被系统赋予了合适的值,驱动程序只需要使用该结点中的信息,而不用更改。open()函数为:

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

inode结构中包含大量的有关文件的信息。这里,只对编写驱动程序有用的字段进行介绍,对于该结构更多的信息,可以参考内核源码。

  • dev_t i_rdev,表示设备文件对应的设备号。
  • struct list_head i_devices,该成员使设备文件连接到对应的cdev结构,从而对应到自己的驱动程序。
  • struct cdev *i_cdev该成员也指向cdev设备。
    除了从dev_t得到主设备号和次设备号外,这里还可以使用imajor()iminor()函数从i_rdev中得到主设备号和次设备号。
    imajor()函数在内部调用MAJOR宏,如下代码所示。
static inline unsigned imajor(const struct inode *inode)
{
	return MAJOR(inode->i_rdev); /*从inode->i_rdev中提取主设备号*/
}

同样,iminor()函数在内部调用MINOR宏,如下代码所示。

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

字符设备驱动的组成

了解字符设备驱动程序的组成,对编写驱动程序非常有用。因为字符设备在结构上都有很多相似的地方,所以只要会编写一个字符设备驱动程序,那么相似的字符设备驱动程序的编写,就不难了。在Linux系统中,字符设备驱动程序由以下几个部分组成。

字符设备加载和卸载函数

在字符设备的加载函数中,应该实现字符设备号的申请和cdev的注册。相反,在字符设备的卸载函数中应该实现字符设备号的释放和cdev的注销。
cdev是内核开发者对字符设备的一个抽象。除了cdev中的信息外,特定的字符设备还需要特定的信息,常常将特定的信息放在cdev之后,形成一个设备结构体,如代码中的xxx_dev
常见的设备结构体、加载函数和卸载函数如下面的代码:

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结构体和其他成员函数

file_operations结构体中的成员函数都对应着驱动程序的接口,用户程序可以通过内核来调用这些接口,从而控制设备。大多数字符设备驱动都会实现read()、write()和ioctl()函数,这三个函数的常见写法如下面代码所示。

/*文件操作结构体*/
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_fops中保存了操作函数的指针。对于没有实现的函数,被赋值为NULLxxx_fops结构体在字符设备加载函数中,作为cdev_init()的参数,与cdev建立了关联。
设备驱动程序的read()和write()函数有同样的参数。filp是文件结构体的指针,指向打开的文件。buf是来自用户空间的数据地址,该地址不能再驱动程序中直接读取。size是要读的字节。ppos是读写的位置,其相当于文件的开头。
xxx_ioctl控制函数的cmd参数是事先定义的I/O控制命令,arg对应该命令的参数。

驱动程序与应用程序的数据交换

驱动程序和应用程序的数据交换是非常重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。通过数据交换,驱动程序和应用程序可以彼此了解对方的情况。但驱动程序和应用程序属于不同的地址空间。驱动程序不能直接访问应用程序的地址空间;同样应用程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从而造成系统崩溃,或者数据破坏。
安全的方法是使用内核提供的专用函数,完成数据在应用程序和驱动程序空间的交换。这些函数对用户程序传过来的指针进行了严格的检查和必要的转换,从而保证用户程序与驱动程序交换数据的安全性。这些函数有:

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);

字符设备驱动程序组成小结

字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operation结构体中的read()、write()、ioctl()等重要函数。如下图所示cdev结构体、file_operations和用户空间调用驱动的关系。
在这里插入图片描述

VirtualDisk字符设备驱动

后面部分都将以一个VirtualDisk设备为蓝本进行讲解。VirtualDisk是一个虚拟磁盘设备,在这个虚拟磁盘设备中分配了8K的连续内存空间,并定义了两个端口数据(port1和port2)。驱动程序可以对设备进行读写、控制和定位操作,用户空间的程序可以通过Linux系统调用访问VirtualDisk设备中的数据。

VirtualDisk的头文件、宏和设备结构体

VirtualDisk驱动程序应该包含必要的头文件和宏信息,并定义一个与实际设备相对应的设备结构体,相关的定义如下面的代码所示。

#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;      /*记录设备目前被多少设备打开*/
};
  • 1~11行列出了必要的头文件,这些头文件中包含了驱动程序可能使用的函数。
  • 19~26行代码,定义了VirtualDisk设备结构体。其中包含了cdev字符设备结构体,和一块连续的8K的设备内存。另外定义了两个端口port1port2,用来模拟实际设备的端口。count表示设备被打开的次数。在驱动程序中,可以不将这些成员放在一个结构中,但放在一起的好处是借助了面向对象的封装思想,将设备相关的成员封装成了一个整体。
  • 22行定义了一个8K的内存块,驱动程序中一般不静态的分配内存,因为静态分配的内存的生命周期非常长,随着驱动程序生和死。而驱动程序一般运行咋系统的整个开机状态中,所以驱动程序分配的内存,一直不会得到释放。所以,编写驱动程序,应避免申请大块内存和静态分配内存。这里,只是为了方便演示,所以分配了静态内存。

加载和卸载驱动程序

前面已经对字符设备驱动程序的加载和卸载模板进行了介绍。VirtualDisk的加载和卸载函数也和前面介绍的相似,其实现如下:

/*设备驱动模型加载函数*/
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); /*释放设备号*/
}
  • 7~13行,使用两种方式申请设备号。VirtualDisk_major变量被静态定义为200。当加载模块时不使VirtualDisk_major等于0。那么就执行register_chrdev_region()函数静态分配一个设备号;如果VirtualDisk_major等于0,那么就使用alloc_chrdev_region()函数动态分配一个设备号,并由参数devno返回。12行,使用MAJOR宏返回得到的主设备号。
  • 17~22行,分配一个VirtualDisk设备结构体。
  • 23行,将分配的VirtualDisk设备结构体清零。
  • 25行,调用自定义的VirtualDisk_setup_cdev()函数初始化cdev结构体,并加入内核中。该函数将在下面讲到。
  • 32~37行是卸载函数,该函数中注销了cdev结构体,释放了VirtualDisk设备所占的内存,并且释放了设备占用的设备号。

cdev的初始化和注册

前面代码中调用的VirtualDisk_setup_cdev()函数完成了cdev的初始化和注册,其代码如下:

/*初始化并注册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");
}

下面对该函数进行简要的解释:

  • 5行,使用MKDEV宏构造一个主设备号为VirtualDisk_major,次设备号为minor的设备号
  • 6行,调用cdev_init()函数,将设备结构体cdevfile_operations指针相关联。这个文件操作指针定义如下代码所示。
/*文件操作结构体*/
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, /*释放设备函数*/
}
  • 8行,指定VirtualDisk_fops为字符设备的文件操作函数指针。
  • 9行,调用cdev_add函数将字符设备加入到内核中。
  • 10、11行,如果注册字符设备失败,则返回。

打开和释放函数

当用户程序调用open()函数打开设备文件时,内核会最终调用VirtualDisk_open()函数。该函数的代码如下:

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

下面对该函数进行简要解释:

  • 5、6行,将Virtualdisk_devp赋给私有数据指针,在后面将用到这个指针。
  • 7行,将设备打开计数增加1。
    当用户程序调用close()函数关闭设备文件时,内核会最终调用VirtualDisk_release()函数。这个函数主要是讲计数器减1。该函数代码如下。
/*文件释放函数*/
int VirtualDisk_release(struct inode *inode, struct file *filp)
{
	struct VirtualDisk *devp = filp->private_data; /*获得设备结构体指针*/
	devp->count--;   /*减少设备打开次数*/
	return 0;
}

读写函数

当用户程序调用read()函数读设备文件中的数据时,内核会最终调用VirtualDisk_read()函数。该函数的代码如下:

/*读函数*/
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;
}

下面对函数进行简要的分析

  • 5~7行,定义了一些局部变量
  • 8行,从文件指针中获得设备结构体指针。
  • 10行,如果要读取的位置大于设备的大小,则出错。
  • 12行,如果要读的数据的位置大于设备的大小,则只读到设备的末尾。
  • 15~24行,从用户空间复制数据到设备中。如果复制数据成功,就将文件的偏移位置加上读出的数据个数。
    当用户程序调用write()函数向设备文件写入数据时,内核会最终调用VirtualDisk_write()函数。该函数的代码如下:
/*写函数*/
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;
}

下面对该函数进行简要的介绍

  • 5~7行,定义了一些局部变量
  • 8行,从文件指针中获得设备结构体指针。
  • 10行,如果要读取的位置大于设备的大小,则错误。
  • 12行,如果要读的数据的位置大于设备的大小,则只读到设备的末尾。
  • 15~24行,从设备中复制数据到用户空间中。如果复制数据成功,就将文件的偏移位置加上写入的数据个数。

seek函数

当用户程序调用fssek()函数在设备文件中移动文件指针时,内核会最终调用VirtualDisk_llseek()函数。该函数的代码如下:

/*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;
}

下面对该函数进行简要介绍:

  • 4行,定义了一个返回值,用来表示文件指针现在的偏移量。
  • 5行,用来选择文件指针移动方向。
  • 7~20行,表示文件指针移动的类型是SEEK_SET,表示相对于文件的开始移动指针offset个位置。
  • 8~12行,如果偏移小于0,则返回错误。
  • 13~17行,如果偏移大于文件的长度,则返回错误。
  • 18行,设置文件的偏移值到filp->f_pos,这个指针表示文件的当前位置。
  • 21~34行,表示文件指针移动的类型是SEEK_CUR,表示相对于文件的当前位置移动指针offset个位置。
  • 22~26行,如果偏移值大于文件的长度,则返回错误。
  • 27~31行,表示指针小于0的情况,这种情况指针是不合法的。
  • 32行,将文件的偏移值filp->f_pos加上offset个偏移。
  • 35、36行,表示命令不是SEEK_SET或者SEEK_CUR,这种情况下表示传入了非法命令,直接返回。

ioctl()函数

当用户程序调用ioctl()函数改变设备的功能时,内核会最终调用VirtualDisk_ioctl()函数。该函数的代码如下:

/*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;
}

下面对该函数进行简要介绍:

  • 5行,得到文件的私有数据,私有数据中存放的是VirtualDisk设备的指针。
  • 6~20行,根据ioctl()函数传进来的参数判断将要执行的操作。这里的字符设备支持3个操作,第一个操作是将字符设备的内存全部清零,第二个是将端口1设置为0,第三个是将端口2设置成0。

小结

讲解了字符设备驱动程序。字符设备是Linux中三大设备之一,很多设备都可以看成是字符设备,所以学习字符设备驱动程序的编程是很有用的。本章先从整体上介绍了字符设备的框架结构,然后介绍了字符设备结构体struct cdev。接着介绍了字符设备的组成,最后详细讲解了一个VirtualDisk字符设备驱动程序。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jacky~~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值