全部博文(489)
分类:
2012-05-20 21:46:33
原文地址:《linux设备驱动开发详解》读书笔记五 作者:jerry20000
根据中断的来源可分为:外部中断和内部中断;
根据中断是否可屏蔽分为:可屏蔽中断与不可屏蔽中断(NMI);
根据中断入口跳转方式的不同分为:向量中断和非向量中断。
2、中断处理程序架构
设备的中断会打断内核中进程的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽可能的短小精悍。为了解决这一矛盾,linux将中断处理程序分解为两个半部:顶半部和底半部。顶半部完成尽可能少的比较紧急的功能。下图描述了linux内核的中断处理机制。
3、linux中断编程之顶半部机制
与Linux设备驱动程序中断处理相关的函数有申请和释放IRQ(中断请求)函数,即request_irq和free_irq,这两个函数在头文件include/linux/interrupt.h文件中声明:
该函数的作用是注册一个IRQ,其中参数irq是要申请的硬件中断号,参数handler是向系统登记的中断处理函数,是一个回调函数,中断发生时系统调用这个函数,参数dev_id是设备的ID,参数flags是中断处理的属性,若设置为SA_INTERRUPT,表明中断处理程序是FRQ(快速中断请求),FRQ程序被调用时屏蔽所有中断,而IRQ程序被调用时不屏蔽FRQ。若设为SA_SHIRQ,则多个设备共享中断,dev_id在中断共享时会用到。参数dev_name是定义传递给request_irq的字符串,用来在/proc/interrupts中显示中断的拥有者。
该函数的作用是释放一个IRQ,一般是在退出设备或关闭设备时调用。
该函数的作用是打开一个IRQ,允许该IRQ产生中断。
该函数的作用是关闭一个IRQ,禁止该IRQ产生中断。
4、底半部机制
Linux实现底半部中断的机制主要有tasklet,工作队列和软中断。
4.1 tasklet
定义tasklet及其处理函数,并将两者关联:
DECLARE_TASKLET定义名称为my_tasklet的tasklet并将其与my_tasklet_func函数绑定,传入这个函数的参数为data。在需要调度tasklet的时候引用tasklet_schedule函数就能使系统在适当时候运行:
使用tasklet作为下半部的处理中断的设备驱动程序模板如下:
点击(此处)折叠或打开
4.2 工作队列介绍
工作队列(work queue)是另外一种将中断的部分工作推后的一种方式,它可以实现一些tasklet不能实现的工作,比如工作队列机制可以睡眠。这种差异的本质原因是,在工作队列机制中,将推后的工作交给一个称之为工作者线程(worker thread)的内核线程去完成(单核下一般会交给默认的线程events/0)。因此,在该机制中,当内核在执行中断的剩余工作时就处在进程上下文(process context)中。也就是说由工作队列所执行的中断代码会表现出进程的一些特性,最典型的就是可以重新调度甚至睡眠。
对于tasklet机制(中断处理程序也是如此),内核在执行时处于中断上下文(interrupt context)中。而中断上下文与进程毫无瓜葛,所以在中断上下文中就不能睡眠。因此,当推后的那部分中断程序需要睡眠时,工作队列毫无疑问是最佳选择;否则用tasklet。
(1)工作队列的实现
工作队列work_struct结构体,位于/include/linux/workqueue.h
这些结构被连接成链表,当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。可以通过DECLARE_WORK在编译时静态地创建该结构,以完成推后的工作。
(2)工作的创建(静态方法)
而后边这个宏为一下内容:
其为参数data赋值的宏定义为:
这样就会静态地创建一个名为n,待执行函数为f,参数为data的work_struct结构。
(3)工作的创建(动态方法)
在运行时通过指针创建一个工作:
这会动态地初始化一个由work指向的工作队列,并将其与处理函数绑定。宏原型为:
在需要调度的时候引用类似tasklet_schedule()函数的相应调度工作队列执行的函数schedule_work(),如:
如果有时候并不希望工作马上就被执行,而是希望它经过一段延迟以后再执行。在这种情况下,可以调度指定的时间后执行函数:
函数原型为:
其中是以delayed_work为结构体的指针,而这个结构体的定义是在work_struct结构体的基础上增加了一项timer_list结构体。
这样,便使预设的工作队列直到delay指定的时钟节拍用完以后才会执行。
(4)模板
使用工作队列处理中断下半部的设备驱动程序模板如下:
点击(此处)折叠或打开
一个实验全程如下:
点击(此处)折叠或打开
4.3 软中断
由于此书说,“驱动的编写者不会也不宜直接使用softirq”,故这里也不作讲解。大家可以参考这篇文章来学习软中断:
5、内核定时器编程
Linux内核2.4版中去掉了老版本内核中的静态定时器机制,而只留下动态定时器。相应地在timer_bh()函数中也不再通过run_old_timers()函数来运行老式的静态定时器。动态定时器与静态定时器这二个概念是相对于Linux内核定时器机制的可扩展功能而言的,动态定时器是指内核的定时器队列是可以动态变化的,然而就定时器本身而言,二者并无本质的区别。考虑到静态定时器机制的能力有限,因此Linux内核2.4版中完全去掉了以前的静态定时器机制。
5.1 Linux内核对定时器的描述
Linux在include/linux/timer.h头文件中定义了数据结构timer_list来描述一个内核定时器:
各数据成员的含义如下:
(1)双向链表元素list:用来将多个定时器连接成一条双向循环队列。
(2)expires:指定定时器到期的时间,这个时间被表示成自系统启动以来的时钟滴答计数(也即时钟节拍数)。当一个定时器的expires值小于或等于jiffies变量时,我们就说这个定时器已经超时或到期了。在初始化一个定时器后,通常把它的expires域设置成当前expires变量的当前值加上某个时间间隔值(以时钟滴答次数计)。
(3)函数指针function:指向一个可执行函数。当定时器到期时,内核就执行function所指定的函数。而data域则被内核用作function函数的调用参数。
5.2 Linux内核对定时器初始化
内核函数init_timer()用来初始化一个定时器。实际上,这个初始化函数仅仅将结构中的list成员初始化为空。如下所示(include/linux/timer.h):
5.3 时间比较操作
在定时器应用中经常需要比较两个时间值,以确定timer是否超时,所以Linux内核在timer.h头文件中定义了4个时间关系比较操作宏。这里我们说时刻a在时刻b之后,就意味着时间值a≥b。Linux强烈推荐用户使用它所定义的下列4个时间比较操作宏(include/linux/timer.h):
5.4 动态内核定时器的实现
Linux是怎样为其内核定时器机制提供动态扩展能力的呢?其关键就在于“定时器向量”的概念。所谓“定时器向量”就是指这样一条双向循环定时器队列(对列中的每一个元素都是一个timer_list结构):对列中的所有定时器都在同一个时刻到期,也即对列中的每一个timer_list结构都具有相同的expires值。显然,可以用一个timer_list结构类型的指针来表示一个定时器向量。
显然,定时器expires成员的值与jiffies变量的差值决定了一个定时器将在多长时间后到期。在32位系统中,这个时间差值的最大值应该是0xffffffff。因此如果是基于“定时器向量”基本定义,内核将至少要维护0xffffffff个timer_list结构类型的指针,这显然是不现实的。
另一方面,从内核本身这个角度看,它所关心的定时器显然不是那些已经过期而被执行过的定时器(这些定时器完全可以被丢弃),也不是那些要经过很长时间才会到期的定时器,而是那些当前已经到期或者马上就要到期的定时器(注意!时间间隔是以滴答次数为计数单位的)。
基于上述考虑,并假定一个定时器要经过interval个时钟滴答后才到期(interval=expires-jiffies),则Linux采用了下列思想来实现其动态内核定时器机制:对于那些0≤interval≤255的定时器,Linux严格按照定时器向量的基本语义来组织这些定时器,也即Linux内核最关心那些在接下来的255个时钟节拍内就要到期的定时器,因此将它们按照各自不同的expires值组织成256个定时器向量。而对于那些256≤interval≤0xffffffff的定时器,由于他们离到期还有一段时间,因此内核并不关心他们,而是将它们以一种扩展的定时器向量语义(或称为“松散的定时器向量语义”)进行组织。所谓“松散的定时器向量语义”就是指:各定时器的expires值可以互不相同的一个定时器队列。
具体的组织方案可以分为两大部分:
(1)对于内核最关心的、interval值在[0,255]之间的前256个定时器向量,内核是这样组织它们的:这256个定时器向量被组织在一起组成一个定时器向量数组,并作为数据结构timer_vec_root的一部分,该数据结构定义在kernel/timer.c文件中,如下述代码段所示:
点击(此处)折叠或打开
基于数据结构timer_vec_root,Linux定义了一个全局变量tv1,以表示内核所关心的前256个定时器向量。这样内核在处理是否有到期定时器时,它就只从定时器向量数组tv1.vec[256]中的某个定时器向量内进行扫描。而tv1的index字段则指定当前正在扫描定时器向量数组tv1.vec[256]中的哪一个定时器向量,也即该数组的索引,其初值为0,最大值为255(以256为模)。每个时钟节拍时index字段都会加1。显然,index字段所指定的定时器向量tv1.vec[index]中包含了当前时钟节拍内已经到期的所有动态定时器。而定时器向量tv1.vec[index+k]则包含了接下来第k个时钟节拍时刻将到期的所有动态定时器。当index值又重新变为0时,就意味着内核已经扫描了tv1变量中的所有256个定时器向量。在这种情况下就必须将那些以松散定时器向量语义来组织的定时器向量补充到tv1中来。
(2)而对于内核不关心的、interval值在[0xff,0xffffffff]之间的定时器,它们的到期紧迫程度也随其interval值的不同而不同。显然interval值越小,定时器紧迫程度也越高。因此在将它们以松散定时器向量进行组织时也应该区别对待。通常,定时器的interval值越小,它所处的定时器向量的松散度也就越低(也即向量中的各定时器的expires值相差越小);而interval值越大,它所处的定时器向量的松散度也就越大(也即向量中的各定时器的expires值相差越大)。
内核规定,对于那些满足条件:0x100≤interval≤0x3fff的定时器,只要表达式(interval>>8)具有相同值的定时器都将被组织在同一个松散定时器向量中。因此,为组织所有满足条件0x100≤interval≤0x3fff的定时器,就需要26=64个松散定时器向量。同样地,为方便起见,这64个松散定时器向量也放在一起形成数组,并作为数据结构timer_vec的一部分。基于数据结构timer_vec,Linux定义了全局变量tv2,来表示这64条松散定时器向量。如上述代码段所示。
对于那些满足条件0x4000≤interval≤0xfffff的定时器,只要表达式(interval>>8+6)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x4000≤interval≤0xfffff的定时器,也需要26=64个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv3全局变量来表示这64个松散定时器向量。
对于那些满足条件0x100000≤interval≤0x3ffffff的定时器,只要表达式(interval>>8+6+6)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x100000≤interval≤0x3ffffff的定时器,也需要26=64个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv4全局变量来表示这64个松散定时器向量。
对于那些满足条件0x4000000≤interval≤0xffffffff的定时器,只要表达式(interval>>8+6+6+6)的值相同的定时器都将被放在同一个松散定时器向量中。同样,要组织所有满足条件0x4000000≤interval≤0xffffffff的定时器,也需要26=64个松散定时器向量。类似地,这64个松散定时器向量也可以用一个timer_vec结构来描述,相应地Linux定义了tv5全局变量来表示这64个松散定时器向量。
最后,为了引用方便,Linux定义了一个指针数组tvecs[],来分别指向tv1、tv2、…、tv5结构变量。如上述代码所示。
6、内核延时
6.1短延时
内核函数 ndelay, udelay, 以及 mdelay 对于短延时好用, 分别延后执行指定的纳秒数, 微秒数或者毫秒数.它们的原型是:
这些函数的实际实现在
重要的是记住这3个延时函数是忙等待;因而在延迟过程中无法运行其他任务。因此,应当在没有其他实用方法的情况下才使用这些函数。
另一个方法获得毫秒(和更长)延时而不用涉及到忙等待. 文件
前 2 个函数使调用进程进入睡眠给定的毫秒数. 一个对 msleep 的调用是不可中断的; 你能确保进程睡眠至少给定的毫秒数. 如果你的驱动位于一个等待队列并且你想唤醒来打断睡眠, 使用 msleep_interruptible. 从 msleep_interruptible 的返回值正常地是 0; 如果, 但是, 这个进程被提早唤醒, 返回值是在初始请求睡眠周期中剩余的毫秒数. 对 ssleep 的调用使进程进入一个不可中断的睡眠给定的秒数.
6.2长延时
长延时有三种方法:忙等待,让出处理器,超时(睡着延时)。前两种方法(在LDD3上有详细讲解)一般不建议使用,这里只讲第三种方法睡着延时。
实现延迟的最好方法应该是让内核为我们完成相应的工作。
(1)若驱动使用一个等待队列来等待某些其他事件,并想确保它在一个特定时间段内运行,可使用:
/*这些函数在给定队列上睡眠, 但是它们在超时(以 jiffies 表示)到后返回。如果超时,函数返回 0; 如果这个进程被其他事件唤醒,则返回以 jiffies 表示的剩余的延迟实现;返回值从不会是负值*/
(2)为了实现进程在超时到期时被唤醒而又不等待特定事件(避免声明和使用一个多余的等待队列头),内核提供了 schedule_timeout 函数:
/*timeout 是要延时的 jiffies 数。除非这个函数在给定的 timeout 流失前返回,否则返回值是 0 。schedule_timeout 要求调用者首先设置当前的进程状态。为获得一个不可中断的延迟, 可使用 TASK_UNINTERRUPTIBLE 代替。如果你忘记改变当前进程的状态, 调用 schedule_time 如同调用 shcedule,建立一个不用的定时器。一个典型调用如下:
参考文献:http://blog.csdn.net/jianchi88/article/details/7200012