Chinaunix首页 | 论坛 | 博客
  • 博客访问: 706873
  • 博文数量: 240
  • 博客积分: 3616
  • 博客等级: 大校
  • 技术积分: 2663
  • 用 户 组: 普通用户
  • 注册时间: 2010-04-21 23:59
文章分类

全部博文(240)

文章存档

2013年(6)

2012年(80)

2011年(119)

2010年(35)

分类: LINUX

2011-07-28 16:26:10


一.驱动程序的概念


     所谓设备驱动程序就是控制与管理硬件设备数据收发的软件,它是应用程序与硬件设备沟通的桥梁。从本质上讲驱动程序主要负责硬件设备的数据读写,参数配置与中断处理。设备驱动程序是操作系统的一部分,通常运行在内核层。应用层通过系统调用进入内核层,内核层根据系统调用号来调用驱动程序对应的接口函数。在linux中驱动程序运行的原理如下图1.1所示:

                   图 1.1   linux系统中硬件.驱动与运用层的分层关系


二.linux中设备类型


    Linux系统将设备分成三种基本类型,每个模型通常实现为其中某一类:字符模块,块模块或网络模块。这三种类型如下:

2.1.字符设备


   字符(char)设备是个能够像字节流(类似文件)一样被访问的设备,由字符设备驱动程序来实现这种特性。字符设备驱动程序通常至少要实现open,close,read和write系统调用。大多数字符设备是一个个只
能顺序访问的数据通道。


2.2.块设备


     和字符设备类似,块设备也是通过/dev目录下的文件系统节点来访问。块设备能容纳文件系统,在大多数unix系统中,进行I/O操作时块设备每次只能传输一个或多个完整的块。而每个块包含512个字节(或2的更高次幂字节的数据)。Linux可以让应用程序像字符设备一样地读写块设备,允许一次传递任意多字节的数据。因此,块设备和字符设备的区别仅仅在于内核内部管理数据的方式,也就是内核及驱动程序之间的软件接口,而这些不同对用户来讲是透明的。在内核中,和字符设备驱动程序相比,块驱动程序具有完全不同的接口。

2.3.网络接口


    任何网络事务都经过一个网络接口形成,即一个能够和其他主机交换数据的设备。通常,接口是个硬件设备,但也可能是个纯软件设备。Linux的网络子系统主要是基于BSD UNIX的socket机制,在网络子系统和驱动程序之间定义专门的数据结构(sk buff)进行数据的传递。Linux操作系统支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多种网络协议的支持.

三.字符设备驱动原理


3.1.file_operations

    linux中字符设备驱动程序的主要功能是实现设备的读写和控制接口。对于字符设备驱动程序,最核心的就是file_operations结构,这个结构实际上是VFS(虚拟文件系统)的文件接口,它的每个成员函数一般都对应一个系统调用。用户进程利用系统调用对设备文件进行诸如读和写等操作时,系统调用通过设备文件的猪设备号找到对应的设备驱动程序,并且调用相应的驱动程序函数。
file_operations结构定义如下:
struct file_operations
{
struct module *owner;//应用该结构的模块的指针,一般为THIS_MODULES
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 *);//向设备发送数据
int (*open) (struct inode *, struct file *);//打开
int (*release) (struct inode *, struct file *);//关闭
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);//执行设备I/O命令
…………………………………
};

其中struct file代表一个打开的文件,在执行file_operations中的open操作时被创建。


3.2.应用层与驱动层的调用关系


图3.1.清晰地描述了应用层与驱动层之间的调用关系,假设驱动程序中定义的file_operation是fops.

         图3.1.应用层与驱动层之间的调用关系


3.3.一般字符设备驱动需要提供的接口


    在linux中一切设备的驱动都是以文件形式存在(以字符设备说明操作布置),操作一个文件肯定得需要先打开,然后根据实际硬件的特征,看是否需要进行有关的参数设置,实质就是操作硬件设备的寄存器(不包括CPU本身),然后实现CPU与具体的硬件进行通信,这种通信只有两种方式,一是向硬件中写数据,二是向硬件中读数据。图3.1已经很清楚了描述了一般的字符设备驱动需要向应用层提供的API函数。


四.linux字符设备驱动中的游戏规则


4.1.主次设备号


    在linux应用程序中打开一个文件后会返回一个文件描述符fd(网络连接成功后也会返回一个文件描述符,有的地方称为“句柄”),用于区别各个文件。那么在linux中怎么管理设备文件呢?设备驱动文件一般位于/dev目录下,如果执行ls –l命令,则可在设备文件项的最后修改日期前看到两个数(用逗号分隔),这个两个数就是相应设备的主设备号和次设备号。
crw-rw----. 1 root root     10,  60  5月 23 15:14 network_latency
crw-rw----. 1 root root     10,  59  5月 23 15:14 network_throughput
crw-rw-rw-. 1 root root      1,   3  5月 23 15:14 null
crw-r-----. 1 root kmem     10, 144  5月 23 15:14 nvram
crw-rw----. 1 root root      1,  12  5月 23 15:14 oldmem
通常而言,主设备号标识设备对应的驱动程序,现代的linux内核允许多个驱动程序共享主设备号,次设备号由内核使用用于正确确定设备文件所指的设备。

4.2.设备编号的内部表达


    在内核中,dev_t类型(在)用来保存设备编号-----包括主设备号和次设备号。dev_t是一个32位的数,其中的12位用来表示主设备号,而其余20位用来表示次设备号。如果要获得dev_t的主设备号或次设备号,应使用:
    MAJOR(dev_t dev);
    MINOR(dev_t dev);
相反,如果需要将主设备号和次设备号转换为dev_t类型,则使用:
MKDEV(int major,int minor);

4.3.cdev结构体


在linux2.6内核中,使用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;
};
   Linux2.6内核提供了一组函数用于操作cdev结构体;
void cdev_init(struct cdev *,struct file_operations *);
strcut cdev *cdev_alloc(void);
void cdev_put(strcut cdev *p);
int cdev_add(struct cdev *,dev_t ,unsigned );
void cdev_del(struct cdev *);
各个函数的作用如下:
cdev_init()函数用于初始化cdev的成员,并建立cdev和file_operations之间的联系。
cdev_alloc()函数用于动态申请一个cdev内存。
cdev_add()函数和cdev_del函数分别向系统添加和删除一个cdev,完成字符设备的注册和注销。对cdev_add()的调用通常发生在字符设备驱动模块加载函数中,而对cdev_del()函数的调用则通常发生在字符设备驱动模块卸载函数中。
注意:在老2.6的版本和2.4的内核中注册一个字符设备驱动程序的经典方式为:
int register_chrdev(unsigned int major,const char *name,struct file_operations *fops);
其中,major是设备的主设备号,name是驱动程序的名字,而fops是默认的file_operations结构。
如果使用register_chrdev函数,将自己的设备从系统中移除的正确函数是:
int unregister_chrdev(unsigned int major,const char *name);


4.4. 分配和释放设备编号


在调用cdev_add()函数向系统注册字符是设备之前,应首先调用register_chrdev_region()或者
alloc_chrdev_region()函数向系统申请设备号,这两个函数的原型为:
int register_chrdev_region(dev_t from,unsigned count,const char *name);
int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned const char *name);
 register_chrdev_region()函数用于已经知道起始设备的设备号的情况,而alloc_chrdev_region()
用于设备号未知,向系统动态申请未被使用的设备号的情况,函数调用成功之后,会得到的设备号放入
第一个dev中。alloc_chrdev_region()与register_chrdev_region()对比的优点在于它会自动避开设备
号重复的冲突。
相反地,在调用cdev_del()函数从系统注销字符设备之后,unregister_chrdev_region()应该被调用以
释放原先申请的设备号,这个函数的原型为:
void unregister_chrdev_region(dev_t from,unsigned count); 

五.字符设备驱动程序设计流程与方法


    字符设备的驱动可以总结为两点:一是对cdev结构体进行初始化,实现对字符设备的注册,二是实现file_operations这个结构体中需要实现的API函数接口。


5.1.整体的驱动设计模块


字符设备驱动程序的整体框架如下图5.1所示:

                       

                     图5.1 字符设备驱动程序的整体框架

5.2.字符设备驱动模块加载与卸载函数


5.2.1字符设备驱动模块加载函数设计流程

采用2.6版本的核的注册字符设备方法实现。字符设备驱动模块加载函数的设计流程如下图5.2所示:

                     图5.2  字符设备驱动模块加载函数的设计流程

    在申请设备号的时候可以采用动态申请或者采用固定主设备的方式去申请(这种方式务必不要占用现有内核已经使用到的设备号).在给结构体分配内存时使用kmalloc函数,分配完后务必需要将里面的数据清0。
在初始化cdev这个结构体都是按照标准的流程走的。在这里有个出错处理方法务必要注意,这是linux编写程序的一大特点,无论是在驱动程序还是在应用程序中都会设计到出错处理的。


5.2.2.字符设备驱动模块卸载函数


模块的卸载函数比较简单,软件设计流程如下图5.3所示:

           

           图5.3  模块的卸载软件设计流程


注销cdev采用函数cdev_del(),释放设备结构体内存采用函数kfree(),释放设备号采用unregister_chrdev_region().

5.3. read()和write()函数设计


5.3.1.read()和write()的使用介绍


read和write方法完成的任务是相似的,亦即,拷贝数据到应用程序空间,或反过来从应用程序拷贝数据。它们的函数原型如下:
ssize_t read(struct file *filp,char _ _user *buff,size_t count,loff_t *offp);
ssize_t write(struct file *filp,const char _ _user *buff,size_t count,loff_t *offp);
参数filp是文件指针,参数count是请求传输的数据长度。参数buff是指向用户空间的缓冲区,这个缓冲区或者保存要写入的数据,或者是一个存放新读入的空缓冲区。最后的oftp是一个指向”long offset type(长偏移量类型)”对象的指针,这个对象指明用户在文件中存取操作的位置。返回值是”signed size type(有符号的尺寸类型)”。read和write方法的buff参数是用户空间的指针。因此,内核代码不能直接引用其中的内容。出现这种限制的原因如下:
1).随着驱动程序所运行的构架的不同或者内核配置的不同,在内核模式中远行时,用户空间的指针是无效的。该地址可能根本无法被映射到内核空间,或者可能指向某些随机数据。
2).即使该指针在内核空间中代表相同的东西,但用户空间的内存是分页的,而在系统调用被调用时,涉及到的内存可能根本不在RAM中。对于用户空间内存的直接引用将导致页错误,而这对内核代码来说是不允许发生的事件。其结果可能是一个”opps”,它将导致调用该系统调用的进程死亡。
3).我们讨论的指针可能是由用户程序提供,而该程序可能存在缺陷或者是个恶意程序。如果我们的驱动程序盲目引用用户提供的指针,将导致系统出现打开的后门,从而允许用户空间程序随意访问或者覆盖系统中的内存。如果我们不打算因为自己的驱动程序而危及用户系统的安全性,则永远不应直接引用用户空间的指针。


read和write代码要做的工作就是在用户地址空间和内核地址空间之间进行整段数据的拷贝。这种能力是由下面的内核函数提供的,它们用于拷贝任意的一段字节序列,这也是大多数read和write方法实现的核心部分。
unsigned long copy_to_user(void _ _user *to,const void *from,unsigned long count);
unsigned long copy_from_user(void *to,const void _ _user *from,unsigned long count);
虽然这些函数的行为很像通常的memcpy函数,但当内核空间内运行的代码访问用户空间时需要注意。
这两个函数的作用不限于在内核空间和用户空间之间的拷贝数据,它们还检查用户空间的指针是否有效,如果指针无效,就不会进行拷贝;另外一个方面,如果在拷贝过程中遇到无效地址,则仅仅会复制部分数据。在这两种情况下,返回值是还需要拷贝的内存数量值。至于实际的访问方法,read方法的任务是从设备拷贝数据到用户空间(copy_to_user),而write方法则是从用户空间拷贝数据到设备上(使用copy_from_user).每次read或write系统调用都会请求一定数目的字节传输,不过驱动程序也并不限制小数据量的传输。

5.3.2.write()/read()程序设计流程


write/read程序设计流程如图5.4所示

    

                        图 5.4write/read程序设计流程


5.4.ioctl()函数设计


5.4.1.ioctl()函数介绍

除了读取和写入设备之外,大部分驱动程序还需要另外一种能力,即通过设备驱动程序执行各种类型的硬件控制。简单数据传输之外,大部分设备可以执行其他一些操作,比如,用户空间经常会改变波特率的大小,设置设备地址等。这些操作通常通过ioctl方法支持,该方法实现了同名的系统调用。在用户空间,ioctl系统调用具有如下原型:
int ioctl(int fd,unsigned long cmd,…);
由于使用了一连串的”.”缘故,这个原型在UNIX系统调用中显得比较特别,通常这个点代表可变数目的参数表。
驱动程序的ioctl方法原型和用户空间的版本存在一些不同:
int (*ioctl) (struct inode *inode,strcut file *filp,unsigned int cmd,unsigned long arg);
inode和filp两个指针的值对应于应用程序传递的文件描述符fd,这和传给open方法的参数一样。参数cmd由用户空间不经修改地传递给驱动程序,可选的arg参数则无论用户程序使用的是指针还是整数值,它都以unsigned long 的形式传递给驱动程序。

5.4.2.ioctl()命令


在编写ioctl代码之前,需要对应不同的命令编号。多数程序员的第一本能是从0或者1开始选择一组小的编号,然而,有许多种理由要求不能这样选择命令的编号。为了防止对错误的设备使用正确的命令,命令号应该在系统范围内唯一。这种错误匹配并不是不会发生,程序可能发现自己正在试图对FIFO和audio等这类非串行设备输入流修改波特率。如果每一个ioctl命令都是唯一的,应用才程序进行这种操作时就会得到一个EINVAL错误,而不是无意间成功地完成了意想不到的操作。要按linux内核的约定方法为驱动程序选择ioctl编号,应该首先看看include/asm/ioctl.h和Documentation/ioctl-neumber.txt这两个文件。
头文件定义了要使用的位字段:类型(幻数),序数,传送方向以及参数的大小等等。
type:幻数。选择一个号码,并在整个驱动程序中使用这个号码。这个字段有8位宽(_IOC_TYPEBITS)。
number:序数(顺序编号)。它也是8位宽(_IOC_NRBITS)。
direction:如果相关命令涉及到数据传输,则该位字段定义数据传输的方向。可以使用的值包括_IOC_NONE(没有数据传输),_IOC_READ,_IOC_WRITE以及_IOC_READ|_IOC_WRITE(双向传输数据)。数据传输是从应用程序的角度看的,也就是说,IOC_READ意味着从设备中读取数据,所以驱动程序必须向用户空间写入数据。
Size:所涉及的用户数据大小。这个字段的宽度与体系结构有关,通常是13位或14位,具体可通过_IOC_SIZEBITS找到针对体系结构的具体数据。
I/O控制命令的组成如下表5.1所示:
设备类型 序列号 方向 数据尺寸
8bit 8bit 2bit 13/14bit
表5.1 I/O控制命令的组成


5.5.open()函数设计


open()函数提供给驱动程序以初始化的能力,从而为以后的操作完成初始化做准备。在大部分驱动程序中,open应该完成如下工作:检查设备特定的错误(诸如设备未就绪或类似的硬件问题)。如果设备是首次打开,则对其进行初始化。如有必要,更新f_op指针。分配并填写置于filp->private_data里的数据结构。
open()函数的原型如下:
int (*open) (strcut inode *inode,struct file *filp);

5.6.release()函数


release方法的作用正好与open相反。有时候也会发现这个方法的实现被称为device_close,而不是device_release。无论是那种形式,这个设备方法都应该完成下面的任务:释放由open分配的,保存在filp->private_data中的所有内容。在最后一次关闭操作时关闭设备。

六.内核提供的API函数


int MAJOR(dev_t dev);
int MINOR(dev_t dev);
这两个宏从设备编号中抽取出主/次设备号
dev_t MKDEV(unsigned int major,unsigned int minor);
这个宏由主/次设备号构造一个dev_t数据项
int register_chrdev_region(dev_t from,unsigned count,const char *name);
int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned const char *name);
void unregister_chrdev_region(dev_t from,unsigned count); 
提供给驱动程序用来分配和释放设备编号范围的函数。在期望的主设备号预先知道的情况下,应调用register_chrdev_region();
而对动态分配,使用int alloc_chrdev_region().
int register_chrdev(unsigned int major,const char *name,struct file_operations *fops);
老的(2.6版本之前)字符设备注册例程。
int unregister_chrdev(unsigned int major,const char *name);
用于注销由register_chrdev函数注册的驱动程序。major和name字符串必须包含与注册驱动程序时使用相同的值。
void cdev_init(struct cdev *,struct file_operations *);
strcut cdev *cdev_alloc(void);
void cdev_put(strcut cdev *p);
int cdev_add(struct cdev *,dev_t ,unsigned );
void cdev_del(struct cdev *);
用来管理cdev结构的函数,内核中使用该结构表示字符设备。
unsigned long copy_to_user(void _ _user *to,const void *from,unsigned long count);
unsigned long copy_from_user(void *to,const void _ _user *from,unsigned long count);
在用户和内核空间之间拷贝数据。
int (*ioctl) (struct inode *inode,strcut file *filp,unsigned int cmd,unsigned long arg);
用于对硬件的控制。

 

 

 

阅读(2990) | 评论(2) | 转发(0) |
给主人留下些什么吧!~~

andyluo3243242011-07-29 16:24:27

copy不上去,在想办法,visio画的。

小雅贝贝2011-07-29 11:02:19

看不到图啊?