1、中南大学操作系统课程设计实验报告选题: 设备驱动程序设计 一、概述:设计主要完成的任务和解决的主要问题;1.任务:设备驱动程序设计, 要求如下:(1) 设计 Windows XP 或者 Linux 操作系统下的设备驱动程序;(2) 设备类型可以是字符设备、块设备或者网络设备;(3) 设备可以是虚拟的也可以是实际设备;(4) 编写测试应用程序,测试对该设备的读写等操作.2.解决的主要问题:(1)各个相关函数的重写(2)虚拟字符设备的挂载(3)虚拟字符设备的测试2.设计的基本概念和原理;1.基本概念(1 ) Linux 系统设备概述Linux 核心与设备驱动之间有一个以标准方式进行互操作的接口。每
2、一类设备(字符设备、块设备以及网络设备)都提供了通用接口,以便在需要时为内核提供服务。这种通用接口使得内核可以以相同的方式来对待不同的设备以及设备驱动。设备驱动程序只是处理硬件,将如何使用硬件的问题留给应用程序。可以从不同的角度来看待设备驱动程序:它是位于应用层和实际设备之间的软件。设备驱动程序在 Linux 内核中扮演着特殊的角色,它们是一个个独立的“黑盒子” ,使某个特定的硬件响应一个定义良好的内部编程接口,同时完全隐藏了设备的工作细节。用户操作通过一组标准化的调用完成,而这些调用是和特定的驱动程序无关的。将这些调用映射到作用于实际硬件的设备特定的操作上,则是设备驱动程序的任务。针对不同的
3、设备驱动程序分为 3 类:字符设备驱动、块设备驱动、网络设备驱动。(2 ) 字符设备可以像文件一样访问字符设备,字符设备驱动程序负责实现这些行为。这样的驱程序通常实现 open、close、read 和 write 系统调用。通过文件系统节点可以访问字符设备,例如/dev/tty1 和/dev/lp1。在字符设备和普通文件系统间的唯一区别是:普通文件允许在其上来回读写,而大多数字符设备仅仅是数据通道,只能顺序读写。当然,也存在这样的字符设备,看起来像个数据区,可以来回读取其中的数据。(3 ) 设备驱动程序设备驱动程序就是一组由内核中的相关子例程和数据组成的 IO 设备软件接口。每当内核意识到要
4、对某个设备进行特殊的操作时,它就调用相应的驱动例程。这就使得控制从用户进程转移到了驱动例程,当驱动例程完成后又被返回至用户进程。(4 ) 模块化Linux 中的可加载模块(module)是 Linux 内核支持的动态可加载模块,他们是核心的一部分(通常是设备驱动程序) ,单是并没编译到核心里面去。Module 可以单独编译成为目标代码,module 是个目标文件。它可以根据需要在系统启动后动态地加载到系统核心之中。当 module 不再被需要时,可以动态地卸载出系统核心。在 Linux 中大多数设备驱动程序或文件系统都是作为 module 的。超级用户可以通过 insmod 和rmmod 命令
5、显示地将 module 载入核心或者卸载。2.原理系统调用是操作系统内核、应用程序之间的接口,设备驱动程序是操作系统内核、机器硬件之间的接口。设备为应用程序屏蔽了硬件的细节,这样从应用程序看来,硬件设备只是一个设备文件,应用程序可以像操作普通文件一样对硬件设备进行操作。设备驱动程序是内核的一部分,它完成以下的功能:(1 ) 对设备初始化和释放(2 ) 把数据从内核传送到硬件和从硬件读取数据(3 ) 读取应用程序传送给设备文件的数据和回送应用程序请求的数据(4 ) 检测和处理设备出现的错误另外,为了让驱动程序能够正常的工作,操作系统内核为驱动程序提供一系列的支持,这些支持包括许多方面。例如,驱动
6、程序需要向内核申请使用系统内存,驱动程序需要向内核申请使用系统硬件资源,驱动程序需要向内核注册自己。下面是内核提供的可供驱动程序使用的几个重要的函数。(1 ) 内存分配函数 kmalloc(2 ) I/O 端口相关函数 request_region、release_region 、check_region 等(3 ) 内核打印函数 printk此外操作系统将每个外部设备当做文件来处理,内核通过 file_operations 结构来访问 driver 的功能。 File_operations 的定义在文件中。每个字符设备都有一个 file_operations 结构。这个结构指向一组操作函数(
7、open 、read. ) 。每个函数的定义由 driver 提供。当然,有些标准操作某些设备并不支持,这时,file_operations 结构中对应的表项为 NULL。随着 Linux 内核的不断升级,file_operations 结构也不断变大。在最新的版本中,函数原型也发生了一些变化。当然,新版本总会向下兼容。这个结构每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如 read/write 操作,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是 Linux 的设备驱动程序工作的基本原理。
8、既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填写 file_operations 的各个域。3.总体设计:实现的方法和主要技术路线;预先设计好内存大小,利用 Linux 内核提供的几个重要函数,为自己的虚拟字符设备申请设备号,进行内存分配,进行 cdev 的注册,重写 cdev 中的 file_operations 结构中的write、read、open、close 等方法,以实现自定义的对设备的读写操作。最后,当不用设备时,利用 Linux 的 rmmod 命令将该字符设备卸载。四. 详细设计;字符设备结构struct globalmem_devstruct cdev cdev
9、;/*cdev 结构体*/unsigned int count;1.模块加载(1 )在 globalmem_init(void)中先申请设备号、后分配内存,最后进行 cdev 的注册。这三个步骤 Linux 内核都有提供相应的基本函数以来完成,直接调用即可。(2 ) cdev 的注册通过 globalmem_setup_cdev 函数完成,在其中把我们自定义的file_operations 结构连接到 cdev 中的 file_operations 中(3 )将自定义的模块初始化注册方法放到 module_init()中函数如下:/*初始化并注册 cdev*/static void globa
10、lmem_setup_cdev(struct globalmem_dev *dev,int index)printk(KERN_INFO “globalmem_setup_cdev() beginn“);int err,devno=MKDEV(globalmem_major,index);cdev_init(dev-cdev.owner=THIS_MODULE;dev-cdev.ops= err=cdev_add(if(err) printk(KERN_NOTICE “Error %d adding LED %d“,err,index);/*初始化加载模块*/int globalmem_ini
11、t(void)printk(KERN_INFO “globalmem_init() beginn“);int result;dev_t devno= MKDEV(globalmem_major,0);if(globalmem_major)result=register_chrdev_region(devno,1,“globalmem“);elseresult=alloc_chrdev_region(globalmem_major=MAJOR(devno);if(resultprivate_data=globalmem_devp;printk(KERN_INFO “globalmem_open(
12、) beginn“);return 0;(2 ) 读操作利用读操作时会自动传入的参数来定义我们自己的对设备的操作方式。其中 filp是指向这一设备的文件结构的指针,buf 为缓冲区,size 是用户进程要求读取的字节数,ppos 是文件当前位移。先初始化各系列条件,p 为当前偏移,count 为要读取的字节数。然后获得设备结构体指针,接着分析和获取有效的写长度。如果返回 ENXIO,则代表某种错误,意思大致是没有这样的设备或地址,就是说文件不能被读取。接着判断要读取的字节数量与当前文件指针位置的关系,如果要读取的字节数量加上当前文件偏移位置已经超过了设备的内存(4Kb)大小,就是说无法满足用户
13、要读取 count 个字符的要求,只能读取 GLOBALMEM_SIZE-p 个文字当确定可以读取后,用 copy_to_user 从内核去读取数据到用户区。数据拷贝成功返回 0,否则返回没有拷贝成功的数量。最后返回已经读取的字符数量。函数如下:static ssize_t globalmem_read(struct file *filp,char _user *buf,size_t size,loff_t *ppos)unsigned long p=*ppos;unsigned int count=size;int ret=0;printk(KERN_INFO “globalmem_read
14、() beginn“);struct globalmem_dev *dev=filp-private_data;if(p = GLOBALMEM_SIZE)return count?ENXIO:0;if(count GLOBALMEM_SIZE-p)count =GLOBALMEM_SIZE-p;if(copy_to_user(buf,(void *)(dev-mem+p),count)/*返回不能复制的字节数*/ret=EFAULT;/ret=-1else*ppos+=count;ret=count;printk(KERN_INFO “read %d bytes(s) from %dn“,c
15、ount,p);return ret;(3 ) 写操作利用写操作时会自动传入的参数来定义我们自己的对设备的操作方式。其中 filp是指向这一设备的文件结构的指针,buf 为缓冲区,size 是用户进程要求读取的字节数,ppos 是文件当前位移。先初始化各系列条件,p 为当前偏移,count 为要读取的字节数。然后获得设备结构体指针,接着分析和获取有效的写长度。如果返回 ENXIO,则代表某种错误,意思大致是没有这样的设备或地址,就是说文件不能被读取。接着判断要写入的字节数量与当前文件指针位置的关系,如果要写入的字节数量加上当前文件偏移位置已经超过了设备的内存(4Kb)大小,就是说无法满足用户要
16、写入 count 个字符的要求,只能写入 GLOBALMEM_SIZE-p 个文字当确定可以写入后,用 copy_from_user 从用户区写数据到内核。数据写入成功返回 0,否则返回没有写入成功的数量。最后返回已经写入的字符数量。函数如下:static ssize_t globalmem_write(struct file *filp,const char _user *buf,size_t size,loff_t *ppos)unsigned long p=*ppos;unsigned int count=size;int ret=0;printk(KERN_INFO “globalme
17、m_write() beginn“);struct globalmem_dev *dev=filp-private_data;if(p = GLOBALMEM_SIZE)return count?ENXIO:0;if(count GLOBALMEM_SIZE-p)count =GLOBALMEM_SIZE-p;if(copy_from_user(dev-mem+p,buf,count) ret=EFAULT;else*ppos+=count;ret = count;printk(KERN_INFO “written %d bytes(s) from %dn“,count,p);return r
18、et;(4 ) 重定位操作该方法用于修改一个文件当前的读写位置,并将新位置(正值)作为返回值返回,出错时返回负值。没设置这个函数的话,会使得相对于文件尾的定位操作失败。使用该操作时有两种情况,一种是相对文件开始位置偏移,一种是相对文件当前位置。其中 filp 指向我们要操作的设备, offset 为我们要偏移的数值, orig 为我们要操作的情况代号,在函数中根据 orig 数值选择操作。在修改偏移位置前,都要先检查修改后的便宜位置是否越界了(上溢出或者下溢出) 。确认没有越界后,再进行修改。static loff_t globalmem_llseek(struct file *filp,lo
19、ff_t offset,int orig)loff_t ret=0;printk(KERN_INFO “globalmem_llseek() beginn“);switch(orig)case 0:if(offsetGLOBALMEM_SIZE)ret=EINVAL;break;filp-f_pos=(unsigned int)offset;ret=filp-f_pos;break;case 1:if(filp-f_pos+offset)GLOBALMEM_SIZE)ret=EINVAL;break;if(filp-f_pos+offset)f_pos+=offset;ret=filp-f_p
20、os;break;default:ret=EINVAL;break;return ret;(5 ) 文件关闭调用这一方法,操作便结束了,Linux 会自己执行文件的断开。函数如下:int globalmem_release(struct inode *inode,struct file *filp)printk(KERN_INFO “globalmem_release() beginn“);return 0;3.模块卸载利用 Linux 内核提供的基本函数,对 cdev 进行注销,回收内存,释放设备号。然后将这个模块卸载方法函数与 module_exit()相连接。函数如下:void glob
21、almem_exit(void)/从系统删除一个 cdevcdev_del(/*释放设备结构体内存*/kfree(globalmem_devp);unregister_chrdev_region(MKDEV(globalmem_major,0),1);/*释放设备号*/五. 完成的情况;字符设备已经成功挂载写入读出操作成功六. 简要的使用说明;基本文件为三个:global_mem_driver.c Makefile Test.c将三个文件放在 Linux 内核为 2.6 版本的系统中。呼出终端,进入当前目录,使用make 编译字符设备驱动,gcc 命令编译测试程序。使用 insmod 命令将
22、global_mem_driver.ko 挂载上去,然后用 mkmod 命令创建一个文件系统节点连接到设备上,记得修改该文件的权限为 0666,然后运行测试程序即可。相关过程截图如下:Make 过程挂载设备创建文件系统节点并修改权限编译测试程序运行测试程序七. 总结这次是从零开始自己做一个字符设备驱动程序,为此去图书馆借了一本基础入门的书。通过这本书我了解到了 Linux 设备驱动的相关概念,对于一个 Linux 设备驱动如何在系统上运行起来的大概流程有了很深的了解。感觉如果以后要做一些复杂的设备驱动,万变不离其宗,大致的开发过程和现在做的这个虚拟字符设备驱动还是差不多的。大致评价一下自己做的
23、设备驱动。嗯,实际上这个虚拟的 globalmem 设备几乎没有任何使用价值,这在入门书中的说法是:为了讲解问题而凭空制造的设备。当然,它也并非一无所用,它可以同时被 2 个或 2 个以上的进程同时访问,其中的全局内存可作为用户空间进程进行通信的蹩脚手段。可以这么说吧,这个字符设备只是为了覆盖大体的开发流程而出来的,因此只有简单的操作。但麻雀虽小,五脏俱全。从这个程序的开发中可以引申开来,为我以后开发别的复杂的设备驱动打下基础吧(如果有幸从事开发设备驱动的话) 。再说说这次的不足吧,做的只是最基础的字符设备驱动,也只是大概的弄清了其中重要的基本函数的功能,其它的更复杂的没有什么了解,因此导致有
24、些功能的实现自己感到有些不满意。例如我的字符设备只能顺序读取,而且每次都是从文件头开始读取或写入。后来通过内核函数,再加上检查之后自我思考了下,我发现貌似其中的 llseek 函数得自己在每次读取前调用,以修改文件读写的偏移位置。如果没调用的话,它每次读写都是从文件头开始的。嗯,这次开发最大的收获就是锻炼自己接收没接触过的知识的能力。感觉以后如果走上软件开发的道路,遇到的不一定都是自己所熟知的知识,这时候就很考验自己的学习能力了吧。另外,感觉自己的自学能力还是挺有限的,本来看班里的人大部分都做第二个实验,想说要做第三个实验,就去网上看了一下,把平台搭建了起来。可是,可能是真的对硬件很不感冒吧,对于如何在哪个 ucos 上书写代码,或者说如何把写好的代码弄进去执行有点疑惑,所以最后不了了之了。感觉多少自己有点失败吧。八. 参考文献商斌编著,飞思科技产品研发中心监制 Linux 设备驱动开发入门与编程实践 电子工业出版社