一步步理解linux字符设备驱动框架(转)
/*
*本文版权归于凌阳教育。如转载请注明
*原作者和原文链接 http://blog.csdn.net/edudriver/article/details/18354313*
*特此说明并保留对其追究法律责任的权利*
*/
现实社会中存在着大量设备,各种设备有自己的工作方式以及硬件特性。但是如果每类设备(比如串口这类设备)都有自己不同的驱动框架的话,这无疑对驱动程序员来说是一个非常大的挑战。所以为了简化驱动程序员的工作,linux系统从千千万万设备中提取它们的共性,将这些设备分成3大类:字符设备,块设备,网络设备。具体字符设备与块设备有什么区别,大家可以看一下这篇文章(http://www.cnblogs.com/qlee/archive/2011/07/27/2118406.html),今天我们主要介绍字符设备的驱动框架。
字符设备是前面提到的这三类设备中最常见的设备,比如生活中大家常见的键盘、鼠标、触摸屏等都属于字符设备。所以掌握字符设备驱动框架是每个驱动程序员所必须的。下面我们通过先通过一个图把字符驱动的整体概念建立起来:
1、首选上层应用程序调用open函数打开/dev目录下的一个文件(这个文件就是“设备文件”)
2、我们知道open是库里面的函数,所以库里面的一部分代码会被调用
3、这时候库里面会执行一些异常指令,执行这些异常指令最后导致的结果就是产生“系统调用”进入内核,这个时候已经从用户空间进入了内核空间
4、进入内核后,内核会接受到这个异常并开始处理,处理的结果是找到对应的驱动程序
5、找到对应的驱动后,驱动程序中的open()函数就会被调用。因为驱动程序是在内核里面执行的,它的运行权限就比较大,所以在驱动的open()函数就可以对硬件设备进行操作了。
这就是上层open函数到驱动中的open函数的整个过程。当然,如果你open一个文件后,下面紧跟着就会调用read、write和close函数,他们的调用过程跟open函数的调用过程是如出一辙的,这里面我们就不再具体介绍了。
在整个的流程中,大家可能会感觉到看似整个过程非常完整,但是还是有几点过不去。比如当在库中执行一段异常指令就会通过系统调用进入内核,内核是怎么找到对应的驱动程序的呢?如果考虑到这个问题,我们就不得不介绍与字符设备驱动密切相关的两个概念了:设备文件(就是open打开的/dev目录下的文件)和主从设备号。
设备文件:从文字上看就是设备的文件,这样解释也是可以的,这类文件就是专门给硬件设备服务的文件,而且这类文件都存在/dev目录下,每当我们向内核中加一个设备驱动程序,这时候就会在相应的/dev目录下生成相应的设备文件。总结起来:设备文件就是跟驱动程序对应起来的,是上层应用访问驱动的接口。下面我们就来看一下这个设备文件的属性:
从这个图中我们可以看到这个设备文件中非常重要的3个属性:
—设备类型:c和b,c代表这个设备文件对应的设备驱动程序是一个字符设备驱动程序,b代表这个设备文件对应的设备驱动程序是一个块设备驱动程序。
—主设备号(范围:1~254):标识一类驱动程序。通过前面这个图我们可以看出主设备号204是标识串口这一类驱动程序;主设备号31标识mtd这一类驱动程序。那这里就有一个问题了,我们知道一台电脑上不止一个串口,这多个串口的驱动程序都是用主设备号204来标识,那我们怎么从中找到某一个具体的串口驱动程序呢,这个时候就需要我们的从设备号了。
—从设备号(范围:0~255):对应一类驱动程序中某一个具体的驱动程序。当我们通过204这个主设备号找到串口这一类驱动程序后,再通过从设备号找到具体的某一个串口的驱动程序。
所以通过上面的分析我们就知道了我们上层的open函数一调用是怎么找到具体的驱动程序并且调用该驱动程序中的open函数的,最后要归功于设备文件中的主从设备号。所以我们从这里也得出了一个非常重要的结论:主从设备在跟设备文件绑定的同时也跟具体的驱动程序绑定在一起。
好了以上就是我们对写具体的字符设备驱动程序所做的热身,下面我们就来看一下怎么写具体的字符设备驱动程序。
首先,我们先建立一个.c文件把模块搭建起来。
然后,我们就要在模块初始化函数里面注册字符设备并且生成相应的设备文件。我们只需要3个函数就可以完成我们这里的任务。
int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
这个函数完成了对字符设备的注册,在这个函数中有两个非常值得我们注意的信息:主设备号和struct file_operartions结构体指针变量。
register_chrdev()的第一个参数就是主设备号,我们如果给第一个参数赋值为0的话,这时候内核会动态的给我们分配一个主设备号,分配的主设备号会通过这个函数的返回值返回。这里大家可能会思考从设备号我们需要在那里指定呢?在这里从设备号是由内核给我们指定的,指定的从设备号就是0~255。也就说这个主设备号对应的所有从设备号我们都占用了,我们可以通过主设备号和0~255中任何一个从设备号都可以找到我们注册的这个字符设备驱动程序。 这样我们通过第一个参数我们的主从设备号就可以跟我们的字符设备驱动程序绑定在一起了,下面我们就来看一下另一个非常重要的参数struct file_operatiosn *变量。
我们在调用register_chrdev()这个函数之前要定义这样的一个结构体变量,在这个结构体里面有什么呢?
在这里只是标出我们这个结构体里面我们看起来比较熟悉的成员:open、read、write、ioctl和release(就是我们驱动里面的close函数),这些成员都是函数指针,这些函数指针都指向我们驱动里面的一些具体函数。只要是被我们open这个函数指针指向的函数就是我们驱动里面的open函数,还有read函数、write函数也是如此。所以总起来说我们驱动程序中的所有open、read、write、ioctl和release函数都被封装到这个结构体里面。我们通过把这个结构体变量的地址传给register_chrdev()函数,就可以把它跟字符驱动绑定在一起。这是我们register_chrdev的第三个参数,第二个参数就是我们给这个字符驱动起的名字,大家不要以为这个名字是设备文件的名字,它只是我们字符驱动的名字,我们可以通过cat /proc/devices命令查看内核里面主设备号使用情况的时候可以看到这个名字。通过上面的函数我们已经把字符设备驱动程序注册进了内核,下面我们的任务就是给这个字符设备驱动生成设备文件。
生成设备文件我们有两种方法:一个是手动生成,一个是自动生成。这里我们只介绍自动生成设备节点的方法。
自动生成设备节点的方法无非就是在register_chrdev()函数后面再调用两个函数:class_create()和device_create()函数,下面我们就主要看一下我们怎么调用这两个函数。
struct class * class_key = class_create(THIS_MODULE, “key_class”);
这个函数主要是内核用来给设备文件进行分类用的,具体怎么实现这里我们就不深入说了,如果大家想了解可以学习有关《设备模型》的知识。
struct device *device_create(struct class *cls, struct device *parent,
dev_t devt, void *drvdata,
const char *fmt, ...)
这个函数就完成了创建设备节点的任务,第一个参数就是我们前面class_create()函数的返回值,第二个参数我们填上NULL就可以了,第三个参数就是主从设备号(MKDEV(主设备号,从设备号)),第四个参数为NULL,最后一个参数为设备文件的名字。通过调用这个函数我们的设备文件就已经生成成功了。
最后,当我们不想用这个驱动程序的时候,我们必须从内核中把这个驱动程序删除掉。我们需要注销字符设备及删除设备节点。我们同样涉及到了3个函数。
void unregister_chrdev(unsigned int major, const char *name)
注销字符设备驱动的函数。第一个参数主设备号,第二个参数设备的名字。
device_destroy(struct class *class, dev_t devt)
删除设备节点函数。第一个参数class_create()函数的返回值,第二个参数创建设备文件时用到的主从设备号。
void class_destroy(struct class *cls)
删除前面class_create()函数创建的变量。
以上,就是我们写字符设备驱动程序涉及到的主要函数,下面我们就看一下led灯驱动程序。