《内存设备驱动程序设计分析教学教材.doc》由会员分享,可在线阅读,更多相关《内存设备驱动程序设计分析教学教材.doc(31页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、Good is good, but better carries it.精益求精,善益求善。内存设备驱动程序设计分析-完整的内存设备驱动程序目录一、设备驱动中的并发控制21、并发22、自旋锁22.1、自旋锁的使用23、信号量23.1、信号量的相关操作33.2、信号量用于同步3二、设备驱动中的阻塞与非阻塞31、阻塞操作32、非阻塞操作33、等待队列33.1、等待队列的相关操作34、轮询操作4三、设备驱动中的异步通知41、异步通知42、信号的接收43、信号的释放43.1、异步通知编程用到一项数据结构和两个函数4四、设备I/O端口和I/O内存的访问51、I/O端口与I/O内存52、可以使用以下函数访
2、问定位于I/O空间端口521、I/O内存52.2、对设备内存映射的虚拟地址的读写53、申请与释放设备I/O端口和I/O内存54、设备I/O端口和I/O内存访问流程54.1、设备I/O端口访问流程54.2、I/O内存访问流程5五、globalfifo驱动涉及的结构体、操作及代码51、globalfifo设备结构体51.1、cdev结构体61.2、设备号的分配和释放61.3、structfile_operations结构体62、使globalfifo驱动实现异步通知73、文件打开函数于释放函数74、读写函数74.1读函数74.2、写函数85、ioctl设备控制函数106、轮询操作107、初始化并注
3、册cdev118、文件操作结构体119、设备驱动模块加载函数129.1自动创建设备文件1310、模块卸载函数1311、其他代码1311.1、必要的头文件1311.2模块的相关信息14六、Makefile14七、模块加载141、直接编译内核142、使用模块法15设备驱动最通俗的理解是“驱使硬件设备行动”。驱动与底层硬件直接打交道,按照硬件设备的具体工作方式,读写设备的寄存器,完成设备的轮询、中断处理、DMA通信,进行物理内存向虚拟内存的映射等,最终让通信设备能收发数据,让显示设备能显示文字和画面,让存储设备能记录文件和数据。现以globalfifo设备驱动为例介绍完整的内存设备驱动程序。首先描述
4、一下该驱动中所涉及到的并发控制、自旋锁、信号量、阻塞与非阻塞I/O、轮询操作、异步通知与异步I/O和I/O访问等。一、设备驱动中的并发控制1、并发指的是多个执行单元同时、并行被执行。而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问很容易导致竞态。处理并发的常用技术是加锁或者互斥,即确保在任何时候只有一个执行单元可以操作共享资源。在Linux内核中主要通过信号量机制和自旋锁机制实现。2、自旋锁可以从它的工作方式理解,即,在某CPU上运行的代码需要先执行一个原子操作,该操作测试并设置某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。如
5、果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋锁”。自旋锁最多只能被一个可执行单元持有。2.1、自旋锁的使用a定义自旋锁spinlock_tlock;b.初始化自旋锁spin_lock_init(lock)该宏用于动态初始化自旋锁lock。c获得自旋锁spin_lock(lock)该宏用于获得自旋锁lock,如果能立即获得锁,马上返回,否则,它将自旋在那里,直到该自旋锁的保持者释放。d释放自旋锁spin_unlock(lock)3、信号量它主要提供对进程间共享资源访问控制机制。与自旋锁类
6、似,只有得到信号量的进程才能执行临界区代码。与自旋锁不同的是,当获得不到信号量时,进程不会原地打转而是进入休眠等待状态。3.1、信号量的相关操作a定义信号量structsemaphoresem;定义名为sem的信号量。b初始化信号量voidsema_init(structsemaphore*sem,intval);该函数初始化信号量,并设置信号量sem的值为val。c获得信号量voiddown(structsemaphore*sem);该函数用于获得信号量sem,它可能会导致睡眠。d释放信号量voidup(structsemaphore*sem);该函数释放信号量sem,唤醒等待者。3.2、信
7、号量用于同步如果信号量被初始化为0,则它可以用于同步,同步意味着一个执行单元的继续执行需等待另一个执行单元完成某事,保证执行的先后顺序。二、设备驱动中的阻塞与非阻塞1、阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足操作的条件后在进行操作。被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。2、非阻塞操作是指进程在不能进行设备操作时,并不挂起,它或者放弃,或者不停地查询,直至可以进行操作为止。3、等待队列用来实现进程的阻塞。等待队列可看作保存进程的容器,在阻塞进程时,将进程放入等待队列,当唤醒进程时,从等待队列中取出进程。3.1、等待队列的相关操作a定义“
8、等待队列头”wait_queue_head_tmu_queue;b初始化“等待队列头”init_waitqueue_head(&my_queue);c定义等待队列DECLARE_WAITQUEUE(name,tsk)该宏用于定义并初始化一个名为name的等待队列。d添加/移除等待队列voidfastcalladd_wait_queue(wait_queue_head_t*q,wait_queue_t*wait);voidfastcallremove_wait_queue(wait_queue_head_t*q,wait_queue_t*wait);e等待事件wait_event(queue,c
9、ondition)wait_event_interruptible(queue,condition)f唤醒队列voidwake_up(wait_queue_head_t*queue);voidwake_up_interruptible(wait_queue_head_t*queue);上述操作会唤醒queue作为等待队列头的所有等待队列中所有属于该等待队列头的等待队列对应的进程。g在等待队列上睡眠sleep_on(wait_queue_head_t*q);interruptible_sleep_on(wait_queue_head_t*q);sleep_on()函数的作用就是将目前进程的状态置
10、成TASK_UNINTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q,直到资源可获得,q引导的等待队列被唤醒。4、轮询操作在用户程序中,select()和poll()也是与设备阻塞与非阻塞访问息息相关的论题。select()系统调用用于多路监控,当没有一个文件满足要求时,select将阻塞调用进程。应用程序常常调用select系统调用,它可能会阻塞进程。这个调用用驱动的poll()方法实现,其原型为:unsignedint(*poll)(structfile*filp,poll_table*wait)。poll()方法负责完成使用poll_wait将等待队列添加到poll
11、_table中;返回描述设备是否可读或可写的掩码。三、设备驱动中的异步通知1、异步通知是指一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态。异步通知使用信号来实现。2、信号的接收在用户程序中,为了捕获信号,可以使用signal()函数来设置信号的处理函数:void(*signal(intsignum,void(*handler)(int)(int);如果signal()调用成功,它返回最后一次为信号signum绑定的处理函数handler值,失败则返回SIG_ERR。为了在用户空间中能处理一个设备释放的信号,它必须完成3项工作: 通过F_SETOWNIIO控制命令设置设备
12、文件的拥有者为本进程,这样从设备驱动法出的信号才能被本进程接收到。 通过F_SETFLIO控制命令设置设备文件支持FASYNC,即异步通知模式。 通过signal()函数连接信号和信号处理函数。3、信号的释放在设备驱动和应用程序的异步通知交互中,仅仅在应用程序端捕获信号是不够的,因为信号的源头在设备驱动端。因此,应该在合适的时机让设备驱动释放信号。为了使设备支持异步通知机制,驱动程序中应涉及3项工作: 支持F_SETOWN命令,能在这个控制命令处理中设置filp-f_owner为对应进程ID。 支持F_SETFL命令处理,每当FASYNC标志改变时,驱动程序中的fasync()函数将得以执行。
13、 在设备资源获得时,调用kill_fasync()函数激发相应的信号。3.1、异步通知编程用到一项数据结构和两个函数数据结构是fasync_struct结构体。两个函数分别是:a.处理FASYNC标识变更的:intfasync_helper(intfd,structfile*filp,intmode,structfasync_struct*fa);b.释放信号用的函数:voidkill_fasync(structfasync_struct*fa,intsig,intband);四、设备I/O端口和I/O内存的访问1、I/O端口与I/O内存设备通常会提供一组寄存器来用于控制设备、读写设备和获取设
14、备状态,即控制寄存器、数据寄存器和状态寄存器。这些寄存器可能位于I/O空间,也可能位于内存空间。当位于I/O空间时,被称为I/O端口,位于内存空间时,被称为I/O内存。2、可以使用以下函数访问定位于I/O空间端口a读写字节端口(8位宽)unsignedinb(unsignedport);voidoutb(unsignedcharbyte,unsignedport);类似的还有读写字端口(16位宽)和读写长字端口(32位宽),b读写一串字节:voidinsb(unsignedport,void*addr,unsignedlongcount);voidoutsw(unsignedport,void
15、*addr,unsignedlongcount);类似的还有读写一串字和读写一串字长。21、I/O内存使用ioremap()函数将设备所处的物理地址映射到虚拟地址,其原型为:void*ioremap(unsignedlongoffset,unsignedlongsize);通过ioremap()获得的虚拟地址应该被iounmap()函数释放,其原型为:voidiounmap(void*addr);2.2、对设备内存映射的虚拟地址的读写可通过读I/O内存、写I/O内存、读一串I/O内存、写一串I/O内存、复制I/O内存和设置I/O内存来完成。3、申请与释放设备I/O端口和I/O内存n I/O端口
16、申请u structresource*request_region(unsignedlongfirst,unsignedlongn,constchar*name);n I/O端口释放u voidrelease_region(unsignedlongstart,unsignedlongn);n I/O内存申请u structresource*request_mem_region(unsignedlongstart,unsignedlonglen,char*name);n I/O内存释放u voidrelease_mem_region(unsignedlongstart,unsignedlongl
17、en);4、设备I/O端口和I/O内存访问流程4.1、设备I/O端口访问流程在设备打开或驱动模块被加载时申请I/O端口区域,之后使用inb()、outb()等进行端口访问,最后,在设备关闭或驱动被卸载时释放I/O端口范围。4.2、I/O内存访问流程首先,调用request_mem_region()申请资源,接着将寄存器地址通过ioremap()映射到内核空间虚拟地址,之后通过Linux设备访问编程接口访问这些设备的寄存器。访问完成后,应对ioremap()申请的虚拟地址进行释放,并释放release_mem_region()申请的I/O内存资源。五、globalfifo驱动涉及的结构体、操作及
18、代码1、globalfifo设备结构体structglobalfifo_devstructcdevcdev;/cdev结构体unsignedintcurrent_len;/fifo有效数据长度unsignedcharmemGLOBALFIFO_SIZE;/全局内存structsemaphoresem;/并发控制用的信号量wait_queue_head_tr_wait;/阻塞读用的等待队列头wait_queue_head_tw_wait;/阻塞写用的等待队列头structfasync_struct*async_queue;/异步结构体指针,用于读;structglobalfifo_dev*glo
19、balfifo_devp;/设备结构体指针1.1、cdev结构体其中,cdev结构体描述一个字符设备,cdev结构体的定义如下所示:structcdevstructkobjectkobj;/内嵌的kobject对象structmodule*owner;/所属模块structfile_operations*ops;/文件操作结构体structlist_headlist;/内核链表结构体dev_tdev;/设备号unsignedintcount;cdev结构体的dev_t成员定义了设备号,为32位,其中主设备号12位,次设备号20位。主设备号用来标识与设备文件相连的驱动程序。次设备号被驱动程序用来
20、辨别操作的是哪个设备。1.2、设备号的分配和释放使用register_chrdev_trgion()或alloc_chrdev_region()函数向系统申请设备号其原型分别为:intregister_chrdev_tegion(dev_tfrom,unsignedcount,constchar*name);intalloc_chrdev_tegion(dev_t*dev,unsignedbaseminor,unsignedcount,constchar*name);register_chrdev_tegion()函数用于已知起始设备的设备号的情况,而alloc_chrdev_region()
21、用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功后,会把得到的设备号放入第一个参数dev中。使用unregister_chrdev_region()函数释放申请的设备号,其原型为:voidunregister_chrdev_tegion(dev_tfrom,unsignedcount);1.3、structfile_operations结构体structfile_operations结构体是一个函数指针的集合,定义能在设备上进行的操作。结构中的成员指向驱动中的函数,这些函数实现一个特别的操作,对于不支持的操作保留为NULL。file_operations结构体主要成员有:l
22、llseek()函数:用来修改一个文件的当前读写位置,并将新位置返回,出错时返回一个负值。l read()函数:用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。l write()函数:向设备发送数据,成功时函数返回写入的字节数。如果此函数未被实现,当用户进行write()系统调用时,将得到-EINVAL返回值。l ioctl()提供设备相关控制命令的实现,当调用成功时,返回给调用程序一个非负值。l mmap()函数:将设备内存映射到进程内存中,如果设备驱动为实现此函数,用户进行mmap()系统调用时将获得-ENODEV返回值。l poll()函数:用于询问设备是否可被非阻
23、塞地立即读写。当询问的条件未触发事,用户空间进行select()和poll()系统调用将引起进程阻塞。2、使globalfifo驱动实现异步通知staticintglobalfifo_fasync(intfd,structfile*filp,intmode)structglobalfifo_dev*dev=filp-private_data;returnfasync_helper(fd,filp,mode,&dev-async_queue);3、文件打开函数于释放函数intglobalfifo_open(structinode*inode,structfile*filp)/将设备结构体指针赋值
24、给文件私有数据指针filp-private_data=globalfifo_devp;return0;该函数用于打开文件,并将文件的私有数据private_data指向结构体,在read()、write()、ioctl()、llseek()等函数通过private_date访问设备结构体。intglobalfifo_release(structinode*inode,structfile*filp)globalfifo_fasync(-1,filp,0);return0;该函数用于将文件从异步通知列表中删除,并实现关闭文件的功能。4、读写函数4.1读函数staticssize_tglobalf
25、ifo_read(structfile*filp,char_user*buf,size_tcount,loff_t*ppos)intret;structglobalfifo_dev*dev=filp-private_data;DECLARE_WAITQUEUE(wait,current);down(&dev-sem);/*获得信号量*/add_wait_queue(&dev-r_wait,&wait);/*进入读等待队列头*/*等待FIFO非空*/if(dev-current_len=0)if(filp-f_flags&O_NONBLOCK)ret=-EAGAIN;gotoout;_set_c
26、urrent_state(TASK_INTERRUPTIBLE);/*改变进程状态为睡眠*/up(&dev-sem);schedule();/*调度其他进程执行*/if(signal_pending(current)/*如果是因为信号唤醒*/ret=-ERESTARTSYS;gotoout2;down(&dev-sem);/*拷贝到用户空间*/if(countdev-current_len)count=dev-current_len;if(copy_to_user(buf,dev-mem,count)ret=-EFAULT;gotoout;elsememcpy(dev-mem,dev-mem+
27、count,dev-current_len-count);/*fifo数据前移*/dev-current_len-=count;/*有效数据长度减少*/printk(KERN_INFOread%dbytes(s),current_len:%dn,count,dev-current_len);wake_up_interruptible(&dev-w_wait);/*唤醒写等待队列*/ret=count;out:up(&dev-sem);/*释放信号量*/out2:remove_wait_queue(&dev-w_wait,&wait);/*从附属的等待队列头移除*/set_current_sta
28、te(TASK_RUNNING);returnret;4.2、写函数staticssize_tglobalfifo_write(structfile*filp,constchar_user*buf,size_tcount,loff_t*ppos)structglobalfifo_dev*dev=filp-private_data;intret;DECLARE_WAITQUEUE(wait,current);down(&dev-sem);add_wait_queue(&dev-w_wait,&wait);/*等待FIFO非满*/if(dev-current_len=GLOBALFIFO_SIZE
29、)if(filp-f_flags&O_NONBLOCK)ret=-EAGAIN;gotoout;_set_current_state(TASK_INTERRUPTIBLE);/*改变进程状态为睡眠*/up(&dev-sem);schedule();/*调度其他进程执行*/if(signal_pending(current)/*如果是因为信号唤醒*/ret=-ERESTARTSYS;gotoout2;down(&dev-sem);/*获得信号量*/*从用户空间拷贝到内核空间*/if(countGLOBALFIFO_SIZE-dev-current_len)count=GLOBALFIFO_SIZ
30、E-dev-current_len;if(copy_from_user(dev-mem+dev-current_len,buf,count)ret=-EFAULT;gotoout;elsedev-current_len+=count;printk(KERN_INFOwritten%dbytes(s),current_len:%dn,count,dev-current_len);wake_up_interruptible(&dev-r_wait);/*唤醒读等待队列*/*产生异步读信号*/if(dev-async_queue)kill_fasync(&dev-async_queue,SIGIO,
31、POLL_IN);printk(KERN_DEBUG%skillSIGIOn,_func_);ret=count;out:up(&dev-sem);/*释放信号量*/out2:remove_wait_queue(&dev-w_wait,&wait);/*从附属的等待队列头移除*/set_current_state(TASK_RUNNING);returnret;读写方法分别用于从设备中读取数据到用户空间;将数据传递给驱动程序。它们的原型为:ssize_txxx_read(structfile*filp,char_user*buf,size_tcount,loff_t*ppos);ssize_t
32、xxx_write(structfile*filp,char_user*buf,size_tcount,loff_t*ppos);对于两个方法,filp是文件指针,count是请求传输的数据量,buf参数指向数据缓存,ppos指出文件当前的访问位置。read和write方法的buf参数是用户空间指针,用户空间指针在内核空间时可能根本是无效的。因此,它不能被内核代码直接引用。可以通过以下函数访问用户空间:l intcopy_from_user(void*to,constvoid_user*from,intn);l intcopy_to_user(void_user*to,constvoid*fr
33、om,intn);其中,获得与释放信号量,使驱动程序实现并发控制。唤醒读写等待队列,实现驱动阻塞。5、ioctl设备控制函数staticintglobalfifo_ioctl(structinode*inodep,structfile*filp,unsignedintcmd,unsignedlongarg)structglobalfifo_dev*dev=filp-private_data;/*获得设备结构体指针*/switch(cmd)caseFIFO_CLEAR:down(&dev-sem);/*获得信号量*/dev-current_len=0;memset(dev-mem,0,GLOBA
34、LFIFO_SIZE);up(&dev-sem);/*释放信号量*/printk(KERN_INFOglobalfifoissettozeron);break;default:return-EINVAL;return0;驱动除了需要具备读写设备的能力外,还需要具备对硬件控制的能力。例如,要求设备报告错误信息,改变波特率等,常常通过ioctl方法来实现。其函数原型为:int(*ioctl)(structinode*inode,structfile*filp,unsignedintcmd,unsignedlongarg)其中,cmd参数从用户空间传下来的,可选的参数arg以一个unsignedlo
35、ng的形势传递,不管它是一个整数或一个指针。如果cmd命令不涉及数据传输,则第个参数arg的值无任何意义。6、轮询操作staticunsignedintglobalfifo_poll(structfile*filp,poll_table*wait)unsignedintmask=0;structglobalfifo_dev*dev=filp-private_data;/*获得设备结构体指针*/down(&dev-sem);poll_wait(filp,&dev-r_wait,wait);poll_wait(filp,&dev-w_wait,wait);/*fifo非空*/if(dev-curr
36、ent_len!=0)mask|=POLLIN|POLLRDNORM;/*标示数据可获得*/*fifo非满*/if(dev-current_len!=GLOBALFIFO_SIZE)mask|=POLLOUT|POLLWRNORM;/*标示数据可写入*/up(&dev-sem);returnmask;通过轮询操作查询是否可对设备进行无阻塞的访问。7、初始化并注册cdevstaticvoidglobalfifo_setup_cdev(structglobalfifo_dev*dev,intindex)interr,devno=MKDEV(globalfifo_major,index);cdev_
37、init(&dev-cdev,&globalfifo_fops);dev-cdev.owner=THIS_MODULE;err=cdev_add(&dev-cdev,devno,1);if(err)printk(KERN_NOTICEError%daddingLED%d,err,index);字符设备的注册可分为以下三个步骤:l 分配cdev。l 初始化cdev。l 添加cdev。structcdev的分配可使用cdev_alloc()函数来完成:structcdev*cdev_alloc(void)字符设备的初始化:voidcdev_init(structcdev*cdev,conststr
38、uctfile_operations*fops)其中,cdev为待初始化的cdev结构;fops为设备对应的操作函数集。字符设备的添加:intcdev_add(structcdev*p,dev_tdev,unsignedcount)其中,p为待添加到内核的字符设备结构;dev为设备号;count为添加的设备个数。8、文件操作结构体staticconststructfile_operationsglobalfifo_fops=.owner=THIS_MODULE,.read=globalfifo_read,.write=globalfifo_write,.ioctl=globalfifo_ioc
39、tl,.poll=globalfifo_poll,.fasync=globalfifo_fasync,.open=globalfifo_open,.release=globalfifo_release,;该结构体与初始化并注册cdev中的cdev_init(&dev-cdev,&globalfifo_fops);语句建立与cdev的连接。9、设备驱动模块加载函数intglobalfifo_init(void)intret;dev_tdevno=MKDEV(globalfifo_major,0);/*申请设备号*/if(globalfifo_major)ret=register_chrdev_r
40、egion(devno,1,globalfifo);else/*动态申请设备号*/ret=alloc_chrdev_region(&devno,0,1,globalfifo);globalfifo_major=MAJOR(devno);if(retsem);/*初始化信号量*/init_waitqueue_head(&globalfifo_devp-r_wait);/*初始化读等待队列头*/init_waitqueue_head(&globalfifo_devp-w_wait);/*初始化写等待队列头*/自动创建设备文件structclass*globafifo_class;globalfif
41、o_class=class_create(THIS_MODULE,”globalfifo_class”);device_create(globafifo_class,NULL,MKDEV(globalfifo_major,0),NULL,”globafifo”);return0;fail_malloc:unregister_chrdev_region(devno,1);returnret;模块加载函数必须以“module_init(函数名)”的形式被指定。它返回整型值,若初始化成功,应返回0.而初始化失败时,应该返回错误编码。该模块可以实现动态申请设备号、动态申请设备结构体内存。9.1自动创建
42、设备文件自动创建设备文件函数原型为:devfs_register(devfs_handle_tdir,constchar*name,unsignedintflags,unsignedintmajor,unsignedintminor,umode_tmode,void*ops,void*info)该函数实现功能为在指定的目录中创建设备文件。其中,dir:目录名,为空表示在/dev/目录下创建;name:文件名;flags:创建标识;major:主设备号;minor:此设备号;mode:创建模式;ops:操作函数集;info:通常为空。在驱动初始化的代码里调用class_create为该设备创建一
43、个class,再为每个设备调用device_create创建对应的设备。10、模块卸载函数voidglobalfifo_exit(void)cdev_del(&globalfifo_devp-cdev);/*注销cdev*/kfree(globalfifo_devp);/*释放设备结构体内存*/device_destroy(globalfifo_class,MKDEV(globalfifo_major,0);class_destroy(globalfifo_class);unregister_chrdev_region(MKDEV(globalfifo_major,0),1);/释放设备号模块卸载函数在模块卸载的时候执行,不返回任何值,必须以“module_exit(函数名)”的形式来指定。通常来说,模块卸载函数要完成的功能与模块加载函数相反的功能如下所示:l 若模块加载函数注册了XXX,则模块卸载函数应该注销XXX。l 若模块加载函数动态申请了内存,则模块卸载函数应释放该内存。l 若模块加载函数申请了硬件资源的占用,则模块卸载函数应释放这些硬件资源。l 若模块加载函数开启了硬件,则写在函数中一般要关闭它。11、其他代码11.1、必要的头文件#include#include#include#includelinux/err
限制150内