《linux设备驱动中常用函数.docx》由会员分享,可在线阅读,更多相关《linux设备驱动中常用函数.docx(14页珍藏版)》请在三一办公上搜索。
1、linux设备驱动中常用函数Linux2.6设备驱动常用的接口函数 -字符设备 刚开始,学习linux驱动,觉得linux驱动很难,有字符设备,块设备,网络设备,针对每一种设备其接口函数,驱动的架构都不一样。这么多函数,要每一个的熟悉,那可多难啦!可后来发现linux驱动有很多规律可循,驱动的基本框架都差不多,再就是一些通用的模块。 基本的架构里包括:加载,卸载,常用的读写,打开,关闭,这是那种那基本的咯。利用这些基本的功能,当然无法实现一个系统。比方说:当多个执行单元对资源进行访问时,会引发竞态;当执行单元获取不到资源时,它是阻塞还是非阻塞?当突然间来了中断,该怎么办?还有内存管理,异步通知
2、。而linux针对这些问题提供了一系列的接口函数和模板框架。这样,在实际驱动设计中,根据具体的要求,选择不同的模块来实现其功能需求。 觉得能熟练理解,运用这些函数,是写号linux设备驱动的第一步。因为是设备驱动,是与最底层的设备打交道,就必须要熟悉底层设备的一些特性,例如字符设备,块设备等。系统提供的接口函数,功能模块就像是工具,能够根据不同的底层设备的的一些特性,选择不同的工具,方能在linux驱动中游刃有余。 最后就是调试,这可是最头疼的事。在调试过程中,总会遇到这样,那样的问题。怎样能更快,更好的发现并解决这些问题,就是一个人的道行咯!我个人觉得: 发现问题比解决问题更难! 时好时坏的
3、东西,最纠结! 看得见的错误比看不见的错误好解决! 一:Fops结构体中函数: ssize_t (*read) (struct file *, char _user *, size_t, loff_t *); 用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL(Invalid argument) 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 signed size 类型, 常常是目标平台本地的整数类型). ssize_t (*write) (struct file *, const char _user *, size_t, loff_t
4、*); 发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数 loff_t (*llseek) (struct file *, loff_t, int); llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个long offset, 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在file 结构 一节中描述). int (*ope
5、n) (struct inode *, struct file *); 尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知. int (*release) (struct inode *, struct file *); 在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL. int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); ioctl 系统调用提供了发出设备特定命令的方
6、法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, 设备无这样的 ioctl), 系统调用返回一个错误. int (*mmap) (struct file *, struct vm_area_struct *); mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV. unsigned int (*poll) (struct file *, struct poll_table_st
7、ruct *); poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写。 二:驱动的基本架构 1:模块加载 创建设备号: MAJOR(dev_t dev):根据设备号dev获得主设备号; MINOR(dev_t dev):根据设备号dev获得次设备号; MKDEV(int major, int
8、minor):根据主设备号major和次设备号minor构建设备号。 可以通过以下方法来创建设备号: dev_t mydev; mydev=MKDEV(50,0); 我们也可以由mydev得到major 和minor number. int major,minor; major=MAJOR(mydev); minor=MINOR(mydev); dev_t类型: 在内核中,dev_t类型用来保存设备编号包括主设备号和次设备号。在内核2.6.0中,dev_t是一个32位的数,其中高12位表示主设备号,低20位表示次设备号。 申请设备号 内核提供了三个函数来注册一组字符设备编号,这三个函数分别是
9、register_chrdev_region、alloc_chrdev_region 。这三个函数都会调用一个共用的 _register_chrdev_region 函数来注册一组设备编号范围。 静态申请: register_chrdev_region(dev_t first,unsigned int count,char *name) First :要分配的设备编号范围的初始值(次设备号常设为0); Count:连续编号范围. Name:编号相关联的设备名称. (/proc/devices); 成功时返回 0 ,失败时返回负数。 动态分配: alloc_chrdev_region(dev_t
10、 *dev,unsigned int firstminor,unsigned int count,char*name); *dev:存放返回的设备号; Firstminor : 通常为0; Count:连续编号范围. Name:编号相关联的设备名称. (/proc/devices); 初始化设备结构体 一个 cdev 一般它有两种定义初始化方式:静态的和动态的。 静态内存定义初始化: struct cdev my_cdev; cdev_init(&my_cdev, &fops); my_cdev.owner = THIS_MODULE; 动态内存定义初始化: struct cdev *my_c
11、dev = cdev_alloc; my_cdev-ops = &fops; my_cdev-owner = THIS_MODULE; 初始化 cdev 后,需要把它添加到系统中去。为此可以调用 cdev_add 函数。传入 cdev 结构的指针,起始设备编号,以及设备编号范围。 cdev_add(struct cdev *p, dev_t dev, unsigned count); 2:模块卸载: 注销设备:cdev_del(struct cdev *p); 释放设备号:unregister_chrdev_region(dev_t from, unsigned count); 三:中断 1:
12、申请中断:int request_irq(unsigned int irq, void (*handler)(int irq, void *dev_id, struct pt_regs *regs), unsigned long irqflags, const char * devname, oid *dev_id ); irq: 要申请的硬件中断号。在Intel平台,范围是015。 handler: 向系统登记的中断处理函数。这是一个回调函数,中断发生时,系统掉用这个函数,传入的参数包括硬件中断号,device id,寄存器值。dev_id就是下面的request_irq时传递给系统的参数d
13、ev_id。 irqflags: 中断处理的一些属性。比较重要的有SA_INTERRUPT,标明中断处理程序是快速处理程序还是慢速处理程序。快速处理程序被调用时屏蔽所有中断。慢速处理程序不屏蔽。还有一个 SA_SHIRQ属性,设置了以后运行多个设备共享中断。 dev_id: 中断共享时会用到。一般设置为这个设备的device结构本身或者NULL。中断处理程序可以用dev_id找到相应的控制这个中断的设备,或者用irq2dev_map找到中断对应的设备。 2:释放中断:void free_irq(unsigned int irq, void *dev_id); irq: 是将要注销掉的中断服务函
14、数的中断号; dev_id: 指定与 request_irq 函数中使用的 dev_id 值相同的值。 3:中断处理程序的架构 为了在中断执行时间尽可能短和中断处理需要完成尽可能多的工作间寻找一个平衡点,linux将中断处理程序分为两个半部:顶半部和底半部。顶半部完成尽可能少的比较紧急的任务,而底半部通常做了中断处理程序中所有工作,而且可以被新的中断打断。尽管系统将中断处理程序分为两部分,但并可以僵化的认为中断处理程序中一定要分为上下两半部。 通常底半部机制主要有tasklet、工作队列和软中断 tasklet 工作队列 Struct work_struct xxx_wq;void xxx_d
15、o_work(unsigned long);INIT_WORK(&xxx_wq,(void(*)(void *)xxx_do_work,NULL);定义工作队列并关联函数void xxx_do_work(unsigned long).中断中断处理函数底半部irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs).schedule_work(&xxx_wq);.int _init xxx_int(void).result = request_irq(xxx_irq, xxx_interrupt, SA_INTER
16、RUPT, xxx, NULL);.void _exit xxx_exit(void).free_irq(xxx_irq, xxx_interrupt);.中断处理函数顶半部设备驱动模块加载函数设备驱动模块卸载函数四:阻塞与轮询 阻塞,执行单元在不能获得资源时便挂起,直到获得可操作的条件后才进行操作。非阻塞,执行单元在不能获得资源时便直接返回。 用户空间Read/write内核空间Xxx_read/xxx_write阻塞挂起不能获得资源直接返回非阻塞硬件设备那么这儿就有一个问题:如何将进程挂起,挂起的进程又如何唤醒? 定义等待队列头 Wait_queue_head_t my_queue; 初始
17、化等待队列头 Init_waitqueue_head(&my_queue); DECLARE_WAIT_QUEUE_HEAD(name); 定义等待队列 DECLARE_WAITQUEUE(name, tsk); 添加/移除等待队列 Void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); Void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); 等待事件 Wait_event(queue, condition); W
18、ait_event_interruptible(queue, condition); Wait_event_timeout(queue, condition, timeout); Wait_event_interruptible_timeout(queue, condition, timeout); 唤醒队列 Void wake_up(wait_queue_head_t *queue); Void wake_up_interruptible(wait_queue_head_t *queue); 在等待队列上睡眠 Sleep_on(wait_queue_head_t *q); Interrupt
19、ible_sleep_on(wait_queue_head_t *q); 轮询:在应用程序中使用非阻塞IO,来查询设备是否可以进行无阻塞访问,通常会用到系统调用函数select/poll函数,而这两个函数最终回去调用设备驱动中的poll函数。 设备驱动中poll函数原型: unsigned int (*poll)(struct file *filp, struct poll_table *wait); 这个函数要进行下面两项工作。首先,对可能引起设备文件状态变化的等待队列调用poll_wait,将对应的等待队列头添加到poll_table.然后,返回表示是否能对设备进行无阻塞读写访问的掩码。在
20、上面提到了一个poll_wait函数,它的原型: void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait); 它的作用就是把当前进程添加到wait参数指定的等待列表(poll_table)中。需要注意的是这个函数是不会引起阻塞的。 五:内存/IO 清楚几个概念:IO空间,内存空间。IO端口,IO内存。 寄存器和内存的区别 寄存器和内存都是可以用来读写的,但寄存器的操作时有副作用,称之为 读取一个寄存器可能导致寄存器中的内容发生变化,比如在一些设备的中断状态寄存器中,读取了寄存器后会自动清零 IO空
21、间和内存空间 并不是所有的体系结构都有IO空间这个定义的,我所了解的只有X86体系上有,而ARM体系结构就没有这种区别, 在X86上,IO空间和内存是独立的,他们各自有各自的总线,并且IO空间一般是64K,即16位,内存空间为4G,可见他们的差别是很大。 IO 端口和IO内存 在有了IO空间的概念后,就有IO端口和IO内存 当一个寄存器或内存位于IO空间时候,称之为IO端口 当一个寄存器或内存位于内存空间时候,称之为IO内存 一:申请内存 1:用户空间申请内存 void *malloc(size_t size) /返回类型为空指针类型 void free(void *ptr); Malloc函
22、数用来在堆中申请内存空间,free函数释放原先申请的内存空间。Malloc函数是在内存的动态存储区中分配一个长度为 size 字节的连续空间. 2:内核空间申请内存 在内核空间中,设计到申请内存的函数有kmalloc,_get_free_page和vmalloc函数。kmalloc和_get_free_page申请的内存位于物理内存映射区域,在物理上是连续的,而且与真实的物理地址只有一个固定的偏移。而vmalloc是在虚拟内存空间中申请一片连续的区域,而这片区域在物理地址上,不一定连续。 Void *kmalloc(size_t size, int flags); _get_free_page
23、s(unsigned int flags, unsigned int order); Void *vmalloc(unsigned long size); 释放内存,分别对应为: Void kfree(void *ptr); Void free_page(unsigned long addr); Void vfree(void *ptr); 二:IO端口和IO内存的访问 对于IO端口,和IO内存给如何访问呢? 以下是IO内存的访问流程,IO端口的访问流程和IO内存,原理上是相通的,只是接口函数不一样。 Request_mem_region申请内存空间并做映射IoremapReadw write
24、wReadb writeb通过read,write来访问io内存Iounmap释放内存空间Release_mem_region六:并发 当多个执行单元,同时并行执行,在获取共享资源时,可能发生冲突,而这种冲突,就叫竞态。可是该如何处理这种冲突呢?而解决这种冲突的途径是:让个执行单元对共享资源的互斥访问。主要方法有:信号量,自旋锁,另外还用到中断屏蔽,原子操作,互斥体。 进程1进程n共享资源导致竞态解决办法申请保护共享资源释放保护1:信号量: 定义信号量: Struct semaphore sem; 初始化信号量: Void sema_init(struct semaphore *sem, in
25、t val); Void init_MUTEX(struct semaphore *sem); 初始化一个互斥信号量,把sem的值设置为1. Void init_MUTEX_LOCKED(struct semaphore *sem); 初始化一个互斥信号量,把sem的值设置为0. 以下宏是定义并初始化信号量的快捷方式。 DECLARE_MUTEX(name); DECLARE_MUTEX_LOCKED(name); 获得信号量: Void down(struct semaphore *sem);/不可中断 Void down_interruptible(struct semaphore *se
26、m);/可中断 Int down_trylock(struct semaphore *sem); 如果能立刻得到,则获取信号量并返回0,否则,返回非0值,它不会导致调用者休眠。 释放信号量: Int up(struct semaphore *sem); 释放信号量,唤醒等待者 2:自旋锁: 定义自旋锁: Spinlock_t lock; 初始化自旋锁: Spin_lock_init(&lock); 获得自旋锁: Spin_lock(lock); Spin_trylock(lock); 释放自旋锁: Spin_unlock(&lock); 利用这些函数,可以很好的解决的竞态,但是有一问题:如果在
27、实际设计过程中,的确存在多个执行单元同时对共享资源读操作时(同时写操作,不现实),那该怎么办呢?觉得linux系统的整个架构真是完善,考虑到了问题的方方面面,着实佩服这些始作俑者。 读写自旋锁,读写信号量就解决了这个问题,其中读自旋锁,读信号量为共享的,也就是说,一个执行单元访问这段共享资源时,另一个执行单元也要访问时,是被允许的;而写自旋锁,写信号量,则是互斥的! 暂时所写的驱动程序中,还没用到这个,先了解一下的。 还有一个问题:既然自旋锁和信号量都是用来解决竞态的,那么他们有什么区别,什么时候该用自旋锁?什么时候该用信号量呢? 自旋锁:可以理解为,当执行单元访问不到共享资源时,它就会一直在等待,自旋,原地打转的意思,一直处于忙状态。通常适用于访问共享资源的时间比较短。 信号量:当执行单元访问不到共享资源时,它就会休眠,直到获取信号量时,被唤醒。通常是用于访问共享资源的时间比较长。 七:异步 区分异步和同步:简单的说,同步,就是当发出某个请求时,如果请求没有响应,我一直等你;异步,就是在发出某个请求后,如果没有回应,我可以做自己的事情,当请求响应后,才开始相关处理。 在驱动的测试中,还没有用到异步。就先了解下。