文章目录
- 前言
- 字符设备驱动程序框架
- 字符设备和块设备
- 主设备号和次设备号
- 申请和释放设备号
- 初识cdev结构
- cdev结构体
- file_operations结构体
- cdev和file_operation结构体的关系
- inode结构体
- 字符设备驱动的组成
- 字符设备加载和卸载函数
- file_operations结构体和其他成员函数
- 驱动程序与应用程序的数据交换
- 字符设备驱动程序组成小结
- VirtualDisk字符设备驱动
- VirtualDisk的头文件、宏和设备结构体
- 加载和卸载驱动程序
- cdev的初始化和注册
- 打开和释放函数
- 读写函数
- seek函数
- ioctl()函数
- 小结
前言
在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。count
和name
与register_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
字符结构体。通过cdev
的ops
指针,就能找到设备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
中保存了操作函数的指针。对于没有实现的函数,被赋值为NULL
。xxx_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的设备内存。另外定义了两个端口port1
和port2
,用来模拟实际设备的端口。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()
函数,将设备结构体cdev
与file_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
字符设备驱动程序。