中南大学操作系统设备驱动程序设计实验报告.docx
中南大学操作系统课程设计试验报告选题:设备驱动程序设计1 / 10一、概述:设计主要完成的任务和解决的主要问题;1. 任务:设备驱动程序设计, 要求如下:(1) 设计 Windows XP 或者 Linux 操作系统下的设备驱动程序;(2) 设备类型可以是字符设备、块设备或者网络设备;(3) 设备可以是虚拟的也可以是实际设备;(4) 编写测试应用程序,测试对该设备的读写等操作.2. 解决的主要问题:(1) 各个相关函数的重写(2) 虚拟字符设备的挂载(3) 虚拟字符设备的测试二.设计的根本概念和原理;1. 根本概念(1) Linux 系统设备概述Linux 核心与设备驱动之间有一个以标准方式进展互操作的接口。每一类设备字符设备、块设备以及网络设备都供给了通用接口,以便在需要时为内核供给效劳。这种通用接口使得内核可以以一样的方式来对待不同的设备以及设备驱动。设备驱动程序只是处理硬件,将如何使用硬件的问题留给应用程序。可以从不同的角度来对待设备驱动程序:它是位于应用层和实际设备之间的软件。设备驱动程序在Linux 内核中扮演着特别的角色,它们是一个个独立的“黑盒子”,使某个特定的硬件响应一个定义良好的内部编程接口,同时完全隐蔽了设备的工作细节。用户操作通过一组标准化的调用完成,而这些调用是和特定的驱动程序无关的。将这些调用映射到作用于实际硬件的设备特定的操作上,则是设备驱动程序的任务。针对不同的设备驱动程序分为 3 类:字符设备驱动、块设备驱动、网络设备驱动。(2) 字符设备可以像文件一样访问字符设备,字符设备驱动程序负责实现这些行为。这样的驱程序通常实现 open、close、read 和 write 系统调用。通过文件系统节点可以访问字符设备,例如/dev/tty1 和/dev/lp1。在字符设备和一般文件系统间的唯一区分是:一般文件允许在其上来回读写,而大多数字符设备仅仅是数据通道,只能挨次读写。固然,也存在这样的字符设备,看起来像个数据区,可以来回读取其中的数据。(3) 设备驱动程序设备驱动程序就是一组由内核中的相关子例程和数据组成的IO 设备软件接口。每当内核意识到要对某个设备进展特别的操作时,它就调用相应的驱动例程。这就使得把握从用户进程转移到了驱动例程,当驱动例程完成后又被返回至用户进程。(4) 模块化Linux 中的可加载模块module是 Linux 内核支持的动态可加载模块,他们是核心的一局部通常是设备驱动程序,单是并没编译到核心里面去。Module 可以单独编译成为目标代码,module 是个目标文件。它可以依据需要在系统启动后动态地加载到2 / 10系统核心之中。当 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 的定义在文件<linux/fs.h>中。每个字符设备都有一个 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 命令将该字符设备卸载。四.具体设计;字符设备构造struct globalmem_devstruct cdev cdev;/*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 globalmem_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,&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 %d adding LED %d“,err,index);/*初始化加载模块*/ int globalmem_init(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“);else result=alloc_chrdev_region(&devno,0,1,“globalmem“); globalmem_major=MAJOR(devno);if(result<0) return result;globalmem_devp = kmalloc(sizeof(struct globalmem_dev),GFP_KERNEL); if(!globalmem_devp)result=ENOMEM;goto fail_malloc;memset(globalmem_devp,0,sizeof(struct globalmem_dev); globalmem_setup_cdev(globalmem_devp,0);return 0; fail_malloc:unregister_chrdev_region(devno,1); return result;2. 设备操作自己重写 write 、read、open 、close 方法,然后就像填表一样放到自己声明的file_operations 中,再把该构造体连接到 cdev 的 file_operations 构造中。(1) 文件翻开利用 Linux 调用翻开方法时传入的参数,将其中的指向这一设备的文件的私有数据指针与我们自己的设备构造体指针连接在一起,以便于我们之后直接对设备进展操作。函数如下:其中 inode 为设备特别文件的 inode索引节点构造的指针,参数 file 是指向这一设备的文件构造的指针。open 还会增加设备计数,以防文件在关闭前模块被卸载出内核。int globalmem_open(struct inode *inode,struct file *filp) filp->private_data=globalmem_devp;printk(KERN_INFO “globalmem_open beginn“); return 0;(2) 读操作利用读操作时会自动传入的参数来定义我们自己的对设备的操作方式。其中 filp 是指向这一设备的文件构造的指针,buf 为缓冲区,size 是用户进程要求读取的字节数, ppos 是文件当前位移。先初始化各系列条件,p 为当前偏移,count 为要读取的字节数。然后获得设备构造体指针,接着分析和猎取有效的写长度。假设返回ENXIO,则代表某种错误,意思大致是没有这样的设备或地址,就是说文件不能被读取。接着推断要读取的字节数量与当前文件指针位置的关系,假设要读取的字节数量加上当前文件偏移位置已经超过了设备的内存4Kb大小,就是说无法满足用户要读取count 个字符的要求,只能读取 GLOBALMEM_SIZE-p 个文字当确定可以读取后,用 copy_to_user 从内核去读取数据到用户区。数据拷贝成功返回 0,否则返回没有拷贝成功的数量。最终返回已经读取的字符数量。函数如下:static ssize_t globalmem_read(struct file *filp,charuser *buf,size_t size,loff_t *ppos) unsigned long p=*ppos;unsigned int count=size; int ret=0;printk(KERN_INFO “globalmem_read 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“,count,p);return ret;(3) 写操作利用写操作时会自动传入的参数来定义我们自己的对设备的操作方式。其中 filp 是指向这一设备的文件构造的指针,buf 为缓冲区,size 是用户进程要求读取的字节数, ppos 是文件当前位移。先初始化各系列条件,p 为当前偏移,count 为要读取的字节数。然后获得设备构造体指针,接着分析和猎取有效的写长度。假设返回ENXIO,则代表某种错误,意思大致是没有这样的设备或地址,就是说文件不能被读取。接着推断要写入的字节数量与当前文件指针位置的关系,假设要写入的字节数量加上当前文件偏移位置已经超过了设备的内存4Kb大小,就是说无法满足用户要写入count 个字符的要求,只能写入 GLOBALMEM_SIZE-p 个文字当确定可以写入后,用 copy_from_user 从用户区写数据到内核。数据写入成功返回 0,否则返回没有写入成功的数量。最终返回已经写入的字符数量。函数如下:static ssize_t globalmem_write(struct file *filp,const charuser *buf,size_t size,loff_t*ppos)unsigned long p=*ppos; unsigned int count=size; int ret=0;printk(KERN_INFO “globalmem_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 ret;(4) 重定位操作该方法用于修改一个文件当前的读写位置,并将位置正值作为返回值返回, 出错时返回负值。没设置这个函数的话,会使得相对于文件尾的定位操作失败。使用该 操作时有两种状况,一种是相对文件开头位置偏移,一种是相对文件当前位置。其中 filp 指向我们要操作的设备,offset 为我们要偏移的数值,orig 为我们要操作的状况代号,在函数中依据 orig 数值选择操作。在修改偏移位置前,都要先检查修改后的廉价位置是否越界了上溢出或者下溢出。确认没有越界后,再进展修改。static loff_t globalmem_llseek(struct file *filp,loff_t offset,int orig) loff_t ret=0;printk(KERN_INFO “globalmem_llseek beginn“); switch(orig)case 0:if(offset<0) ret=EINVAL;break;if(unsigned int)offset>GLOBALMEM_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)<0) ret=EINVAL;break;filp->f_pos+=offset; ret=filp->f_pos; 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 globalmem_exit(void)/从系统删除一个 cdev cdev_del(&globalmem_devp->cdev);/*释放设备构造体内存*/ kfree(globalmem_devp);unregister_chrdev_region(MKDEV(globalmem_major,0),1);/*释放设备号*/五.完成的状况;字符设备已经成功挂载写入读出操作成功10 / 10六.简要的使用说明;根本文件为三个:global_mem_driver.cMakefile Test.c将三个文件放在 Linux 内核为 2.6 版本的系统中。呼出终端,进入当前名目,使用make 编译字符设备驱动,gcc 命令编译测试程序。使用 insmod 命令将 global_mem_driver.ko挂载上去,然后用mkmod 命令创立一个文件系统节点连接到设备上,记得修改该文件的权限为0666,然后运行测试程序即可。相关过程截图如下:Make 过程挂载设备创立文件系统节点并修改权限编译测试程序运行测试程序七.总结这次是从零开头自己做一个字符设备驱动程序,为此去图书馆借了一本根底入门的书。通过这本书我了解到了 Linux 设备驱动的相关概念,对于一个 Linux 设备驱动如何在系统上运行起来的或许流程有了很深的了解。感觉假设以后要做一些简洁的设备驱动,万变不离其 宗,大致的开发过程和现在做的这个虚拟字符设备驱动还是差不多的。大致评价一下自己做的设备驱动。嗯,实际上这个虚拟的 globalmem 设备几乎没有任何使用价值,这在入门书中的说法是:为了讲解问题而凭空制造的设备。固然,它也并非一无所用,它可以同时被 2 个或 2 个以上的进程同时访问,其中的全局内存可作为用户空间进程进展通信的蹩脚手段。可以这么说吧,这个字符设备只是为了掩盖大体的开发流程而出来的,因此只有简洁的 操作。但麻雀虽小,五脏俱全。从这个程序的开发中可以引申开来,为我以后开发别的简洁的设备驱动打下根底吧假设有幸从事开发设备驱动的话。再说说这次的缺乏吧,做的只是最根底的字符设备驱动,也只是或许的弄清了其中重要 的根本函数的功能,其它的更简洁的没有什么了解,因此导致有些功能的实现自己感到有些 不满足。例如我的字符设备只能挨次读取,而且每次都是从文件头开头读取或写入。后来通 过内核函数,再加上检查之后自我思考了下,我觉察貌似其中的 llseek 函数得自己在每次读取前调用,以修改文件读写的偏移位置。假设没调用的话,它每次读写都是从文件头开头的。嗯,这次开发最大的收获就是熬炼自己接收没接触过的学问的力气。感觉以后假设走上 软件开发的道路,遇到的不愿定都是自己所熟知的学问,这时候就很考验自己的学习力气了 吧。另外,感觉自己的自学力气还是挺有限的,原来看班里的人大局部都做其次个试验,想 说要做第三个试验,就去网上看了一下,把平台搭建了起来。可是,可能是真的对硬件很不 感冒吧,对于如何在哪个 ucos 上书写代码,或者说如何把写好的代码弄进去执行有点疑心, 所以最终不了了之了。感觉多少自己有点失败吧。八.参考文献商斌编著,飞思科技产品研发中心监制 Linux 设备驱动开发入门与编程实践 电子工业