中南大学操作系统设备驱动程序设计实验报告.docx
《中南大学操作系统设备驱动程序设计实验报告.docx》由会员分享,可在线阅读,更多相关《中南大学操作系统设备驱动程序设计实验报告.docx(12页珍藏版)》请在冰豆网上搜索。
中南大学操作系统设备驱动程序设计实验报告
中南大学
操作系统课程设计
实验报告
选题:
设备驱动程序设计
一、概述:
设计主要完成的任务和解决的主要问题;
1.任务:
设备驱动程序设计,要求如下:
(1)设计WindowsXP或者Linux操作系统下的设备驱动程序;
(2)设备类型可以是字符设备、块设备或者网络设备;
(3)设备可以是虚拟的也可以是实际设备;
(4)编写测试应用程序,测试对该设备的读写等操作.
2.解决的主要问题:
(1)各个相关函数的重写
(2)虚拟字符设备的挂载
(3)虚拟字符设备的测试
二.设计的基本概念和原理;
1.基本概念
(1)Linux系统设备概述
Linux核心与设备驱动之间有一个以标准方式进行互操作的接口。
每一类设备(字符设备、块设备以及网络设备)都提供了通用接口,以便在需要时为内核提供服务。
这种通用接口使得内核可以以相同的方式来对待不同的设备以及设备驱动。
设备驱动程序只是处理硬件,将如何使用硬件的问题留给应用程序。
可以从不同的角度来看待设备驱动程序:
它是位于应用层和实际设备之间的软件。
设备驱动程序在Linux内核中扮演着特殊的角色,它们是一个个独立的“黑盒子”,使某个特定的硬件响应一个定义良好的内部编程接口,同时完全隐藏了设备的工作细节。
用户操作通过一组标准化的调用完成,而这些调用是和特定的驱动程序无关的。
将这些调用映射到作用于实际硬件的设备特定的操作上,则是设备驱动程序的任务。
针对不同的设备驱动程序分为3类:
字符设备驱动、块设备驱动、网络设备驱动。
(2)字符设备
可以像文件一样访问字符设备,字符设备驱动程序负责实现这些行为。
这样的驱程序通常实现open、close、read和write系统调用。
通过文件系统节点可以访问字符设备,例如/dev/tty1和/dev/lp1。
在字符设备和普通文件系统间的唯一区别是:
普通文件允许在其上来回读写,而大多数字符设备仅仅是数据通道,只能顺序读写。
当然,也存在这样的字符设备,看起来像个数据区,可以来回读取其中的数据。
(3)设备驱动程序
设备驱动程序就是一组由内核中的相关子例程和数据组成的I\O设备软件接口。
每当内核意识到要对某个设备进行特殊的操作时,它就调用相应的驱动例程。
这就使得控制从用户进程转移到了驱动例程,当驱动例程完成后又被返回至用户进程。
(4)模块化
Linux中的可加载模块(module)是Linux内核支持的动态可加载模块,他们是核心的一部分(通常是设备驱动程序),单是并没编译到核心里面去。
Module可以单独编译成为目标代码,module是个目标文件。
它可以根据需要在系统启动后动态地加载到系统核心之中。
当module不再被需要时,可以动态地卸载出系统核心。
在Linux中大多数设备驱动程序或文件系统都是作为module的。
超级用户可以通过insmod和rmmod命令显示地将module载入核心或者卸载。
2.原理
系统调用是操作系统内核、应用程序之间的接口,设备驱动程序是操作系统内核、机器硬件之间的接口。
设备为应用程序屏蔽了硬件的细节,这样从应用程序看来,硬件设备只是一个设备文件,应用程序可以像操作普通文件一样对硬件设备进行操作。
设备驱动程序是内核的一部分,它完成以下的功能:
(1)对设备初始化和释放
(2)把数据从内核传送到硬件和从硬件读取数据
(3)读取应用程序传送给设备文件的数据和回送应用程序请求的数据
(4)检测和处理设备出现的错误
另外,为了让驱动程序能够正常的工作,操作系统内核为驱动程序提供一系列的支持,这些支持包括许多方面。
例如,驱动程序需要向内核申请使用系统内存,驱动程序需要向内核申请使用系统硬件资源,驱动程序需要向内核注册自己。
下面是内核提供的可供驱动程序使用的几个重要的函数。
(1)内存分配函数kmalloc
(2)I/O端口相关函数request_region、release_region、check_region等
(3)内核打印函数printk
此外操作系统将每个外部设备当做文件来处理,内核通过file_operations结构来访问driver的功能。
File_operations的定义在文件中。
每个字符设备都有一个file_operations结构。
这个结构指向一组操作函数(open、read...)。
每个函数的定义由driver提供。
当然,有些标准操作某些设备并不支持,这时,file_operations结构中对应的表项为NULL。
随着Linux内核的不断升级,file_operations结构也不断变大。
在最新的版本中,函数原型也发生了一些变化。
当然,新版本总会向下兼容。
这个结构每一个成员的名字都对应着一个系统调用。
用户进程利用系统调用在对设备文件进行诸如read/write操作,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。
这是Linux的设备驱动程序工作的基本原理。
既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填写file_operations的各个域。
三.总体设计:
实现的方法和主要技术路线;
预先设计好内存大小,利用Linux内核提供的几个重要函数,为自己的虚拟字符设备申请设备号,进行内存分配,进行cdev的注册,重写cdev中的file_operations结构中的write、read、open、close等方法,以实现自定义的对设备的读写操作。
最后,当不用设备时,利用Linux的rmmod命令将该字符设备卸载。
四.详细设计;
字符设备结构
structglobalmem_dev
{
structcdevcdev;/*cdev结构体*/
unsignedintcount;
}
1.模块加载
(1)在globalmem_init(void)中先申请设备号、后分配内存,最后进行cdev的注册。
这三个步骤Linux内核都有提供相应的基本函数以来完成,直接调用即可。
(2)cdev的注册通过globalmem_setup_cdev函数完成,在其中把我们自定义的file_operations结构连接到cdev中的file_operations中
(3)将自定义的模块初始化注册方法放到module_init()中
函数如下:
/*初始化并注册cdev*/
staticvoidglobalmem_setup_cdev(structglobalmem_dev*dev,intindex)
{
printk(KERN_INFO"globalmem_setup_cdev()begin\n");
interr,devno=MKDEV(globalmem_major,index);
cdev_init(&dev->cdev,&globalmem_fops);
dev->cdev.owner=THIS_MODULE;
dev->cdev.ops=&globalmem_fops;err=cdev_add(&dev->cdev,devno,1);
if(err)printk(KERN_NOTICE"Error%daddingLED%d",err,index);
}
/*初始化加载模块*/
intglobalmem_init(void)
{
printk(KERN_INFO"globalmem_init()begin\n");
intresult;
dev_tdevno=MKDEV(globalmem_major,0);
if(globalmem_major){
result=register_chrdev_region(devno,1,"globalmem");
}
else{
result=alloc_chrdev_region(&devno,0,1,"globalmem");
globalmem_major=MAJOR(devno);
}
if(result<0)returnresult;
globalmem_devp=kmalloc(sizeof(structglobalmem_dev),GFP_KERNEL);
if(!
globalmem_devp){
result=ENOMEM;
gotofail_malloc;
}
memset(globalmem_devp,0,sizeof(structglobalmem_dev));
globalmem_setup_cdev(globalmem_devp,0);
return0;
fail_malloc:
unregister_chrdev_region(devno,1);
returnresult;
}
2.设备操作
自己重写write、read、open、close方法,然后就像填表一样放到自己声明的file_operations中,再把该结构体连接到cdev的file_operations结构中。
(1)文件打开
利用Linux调用打开方法时传入的参数,将其中的指向这一设备的文件的私有数据指针与我们自己的设备结构体指针连接在一起,以便于我们之后直接对设备进行操作。
函数如下:
其中inode为设备特殊文件的inode(索引节点)结构的指针,参数file是指向这一设备的文件结构的指针。
open还会增加设备计数,以防文件在关闭前模块被卸载出内核。
intglobalmem_open(structinode*inode,structfile*filp){
filp->private_data=globalmem_devp;
printk(KERN_INFO"globalmem_open()begin\n");
return0;
}
(2)读操作
利用读操作时会自动传入的参数来定义我们自己的对设备的操作方式。
其中filp是指向这一设备的文件结构的指针,buf为缓冲区,size是用户进程要求读取的字节数,ppos是文件当前位移。
先初始化各系列条件,p为当前偏移,count为要读取的字节数。
然后获得设备结构体指针,接着分析和获取有效的写长度。
如果返回ENXIO,则代表某种错误,意思大致是没有这样的设备或地址,就是说文件不能被读取。
接着判断要读取的字节数量与当前文件指针位置的关系,如果要读取的字节数量加上当前文件偏移位置已经超过了设备的内存(4Kb)大小,就是说无法满足用户要读取count个字符的要求,只能读取GLOBALMEM_SIZE-p个文字
当确定可以读取后,用copy_to_user从内核去读取数据到用户区。
数据拷贝成功返回0,否则返回没有拷贝成功的数量。
最后返回已经读取的字符数量。
函数如下:
staticssize_tglobalmem_read(structfile*filp,char__user*buf,size_tsize,loff_t*ppos){
unsignedlongp=*ppos;
unsignedintcount=size;
intret=0;
printk(KERN_INFO"globalmem_read()begin\n");
structglobalmem_dev*dev=filp->private_data;
if(p>=GLOBALMEM_SIZE)
returncount?
ENXIO:
0;
if(count>GLOBALMEM_SIZE-p)
count=GLOBALMEM_SIZE-p;
if(c