Name:OWEN
E-mail:embeddedgood@163.com
QQ:27549805
5.1中断
中断本质上是一种特殊的电信号,由硬件设备发向处理器,处理器接收到中断后,会马上向操作系统反映此信号的到来,然后就由OS复杂处理这些新到来的数据。硬件设备生成中断的时候并不考虑与处理器的时钟同步,也就是说中断可以随时产生。
定义:
所谓中断是指CPU在执行程序的过程中,出现了某些突发事件时CPU必须暂停执行当前的程序,转去处理突发事件,处理完毕后CPU又返回原程序被中断的位置并继续执行。
分类:
根据中断的来源:分为内部中断和外部中断。内部中断的中断源来自CPU内部(软件中断指令,溢出等),外部中断的中断源来自CPU外部,由外设提出请求。
根据是否屏蔽:分为可屏蔽中断与不屏蔽中断(NMI),可屏蔽中断可通过屏蔽字屏蔽,屏蔽后该中断不再得到响应。
根据中断入口跳转方式的不同:分为向量中断和非向量中断。采用向量中断的CPU通常为不同的中断分配不同的中断号。非向量中断的多个中断共享一个入口地址。进入该入口地址后再通过软件判断中断标志来识别具体是哪个中断。就是说,向量中断是由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址。
5.2中断处理程序
中断处理程序与其他内和函数的真正区别在于:
中断处理程序是被内核调用来响应中断的,而它们运行于我们称之为中断上下文的特殊上下文中。
前和后半部(上半部和下半部)
中断处理的一个主要问题是如何在处理中进行长时间的任务. 常常大量的工作必须响应一个设备中断来完成, 但是中断处理需要很快完成并且不使中断阻塞太长. 这 2 个需要(工作和速度)彼此冲突。
Linux (许多其他系统一起)解决这个问题通过将中断处理分为 2 半. 所谓的前半部是实际响应中断的函数 -- 你使用 request_irq 注册的那个. 后半部是由前半部调度来延后执行的函数, 在一个更安全的时间. 最大的不同在前半部处理和后半部之间是所有的中断在后半部执行时都使能——这就是为什么它在一个更安全时间运行. 在典型的场景中, 前半部保存设备数据到一个设备特定的缓存, 调度它的后半部, 并且退出: 这个操作非常快. 后半部接着进行任何其他需要的工作, 例如唤醒进程, 启动另一个 I/O 操作, 等等. 这种设置允许前半部来服务一个新中断而同时后半部仍然在工作。
5.3注册中断处理程序
中断处理程序是管理硬件的驱动程序的组成部分。每个设备都有相关的驱动程序,如果设备使用中断,那么相应的驱动程序就注册一个中断处理程序。驱动程序可以通过下面的函数注册并激活一个中断处理程序,以便处理中断:
int request_irq(unsigned int irq,
irqreturn_t (*handler)(int, void *, struct pt_regs *),
unsigned long flags,
const char *dev_name,
void *dev_id);
(1)第一个参数irq表示要分配的中断号。对某些设备,这个值是先定的,对大多数设备来说,这个值是可以通过探测获取,也可以动态确定。
(2)第二个参数handler是一个指针,指向处理这个中断的实际中断处理程序。只要操作系统一接收到中断,该函数就被调用。
(3)第三个参数irqflags可以为0,也可能是下列一个或多个标志的位掩码:SA_INTERRUPT:此标志表明给定的中断处理程序是一个快速中断处理程序。
SA_SAMPLE_RANDOM:此标志表明这个设备产生的中断对内核熵池有贡献。
SA_SHIRQ:此标志表明可以在多个中断处理程序之间共享中断线。
(4)第四个参数devname是与中断相关的设备的ASCII文本表示法。这些名字会被/proc/irq和proc/interrupt文件使用,以便与用户通信。
(5)第五个参数dev_id主要用于共享中断线。用于区分共享中断线上的各个中断处理程序。内核每次调用中断处理程序时,都会把这个指针传递给它。实践中往往会通过它传递驱动程序的设备结构:这个指针是唯一的,而且有可能在中断处理程序内及设备模式中被用到。
request_irq()成功执行会返回0,如果返回非0值,就表示有错误发生,在这种情况下,指定的中断处理程序不会被注册,最常见的错误是-EBUSY,它表示给定的中断线已经在使用(或者当前用户或者你没有指定SA_SHIRQ())。
注意:request_irq()函数可能会睡眠,因此,不能在中断上下文或其它不允许阻塞的代码中调用该函数,在睡眠不安全的上下文中可以安全的调用request_irq()函数,是一种常见错误。
为什么request_irq()会引起睡眠?
在注册的过程中,内核需要在/proc/irq文件创建一个与中断对应的项,函数proc_mkdir()就是用来创建这个新的procfs项的,proc_makedir()通过调用函数proc_create()对这个新的profs项进行设置,而proc_create()会调用函数kmalloc()来请求分配内存,而我们知道函数kmalloc()是可以睡眠的。
释放中断处理程序
释放中断线,可以调用:
void free_irq(unsigned int irq, void *dev_id);
如果指定的中断线不是共享的,那么,该函数删除处理程序的同时将禁用这条中断线,如果中断线是共享的,则仅删除dec_id所对应的处理程序,而这条中断线本身只有在删除了最后一个处理程序才会被禁用,由此可以看出为什么唯一的dev_id如此重要,对于共享的中断线,需要一个唯一的信息来区分其上面的多个处理程序,并通过它来保证函数free_irq()能够正确的删除指定的处理程序,不管在哪种情况下(共享或不共享),如果dev_id非空,它都必须与需要的删除的处理程序相匹配。
注意:必须从进程上下文中调用free_irq()。
5.4编写中断处理程序
一个典型的中断处理程序声明:???????????????????????????????????????????????????????????????????????????????????????????????????
static irqreturn_t intr_handle(int irq, void *dev_id, struct pt_regs *regs??????????????????????????????????????????
注意,它的类型与request_irq()参数中handler所要求的参数类型相匹配(handler函数有一个类型为irqreturn_t的返回值)。第一个参数irq就是这个处理程序要响应的中断的中断线号。dev_id是一个通用的指针,它与在中断注册时传递给request_irq()的参数dev_id必须一致。dev_id也有可能指向中断处理程序使用的一个数据结构,对每个设备而言,设备结构都是唯一的,而且可能在中断处理程序中也用的到,因此,也通常会把设备结构传递给dev_id。
参数regs是一个指向结构的指针,该结构包含处理中断之前处理器的寄存器和状态,除了调试以外,很少用到。
中断处理程序的返回值是一个特殊类型:irqreturn_t。它可能返回两个特殊的值:IRQ_NONE和IRQ_HANDLED。irqreturn_t这个返回类型实际上就是一个int型,之所以使用这些特殊值是为了与早期的内核保持兼容。
中断处理程序通常会标记static,因为它从来不会被别的文件中的代码直接调用。
5.4.1共享的中断处理程序
共享的处理程序与非共享的处理程序在注册和运行方式上比较相似,但差异主要有以下几点:
(1)request_irq()的参数flags必须设置SA_SHIRQ标志。
(2)对每个注册的中断处理程序来说,dev_id参数必须唯一。而指向任一设备结构体的指针就可以满足这一要求;通常会选择设备结构,因为它是唯一的,而且中断处理程序可能会用到它。
(3)中断处理程序必须能够区分它的设备是否真的产生中断。这既需要硬件支持,也需要处理程序中有相关的处理逻辑。如果硬件不支持这一功能,它就没法知道到底是与它对应的设备发出了这个中断,还是共享这条中断线的其它设备发出了这个中断。
指定SA_SHIRQ标志以调用request_irq()时,只有在以下两种情况下才可能成功,一是中断线当前未被注册,二是在该线上的所有已注册处理程序都指定了SA_SHIRQ。
内核接收一个中断后,它将依次调用在该中断线上注册的每一个处理程序,因此,一个处理程序必须知道它是否应该为这个中断负责,如果与它相关的设备并没有产生中断,那么,处理程序应该立即退出。
5.4.3中断上下文
当执行一个中断处理程序或下半部时,内核处于中断上下文(interrput context)中。让我们先回忆一下进程上下文,进程上下文是一种内核所处的操作模式,此时内核代表进程执行——例如,执行系统调用或运行内核线程,在进程上下文中,可以通过current宏去关联当前进程,此外,因为进程是以进程上下文的形式连接到内核中的,因此,在进程上下文可以睡眠,也可以调用调度程序。
与之相反,中断上下文和进程并没有什么瓜葛,与current宏 也是不相干的(尽管它会指向被中断的进程),因为没有进程的背景,所以中断上下文不可以睡眠——否则又怎能对它重新调度呢?因此,不能从中断上下文中调用 某些函数,如果一个函数睡眠,就不能在你的中断处理程序中使用它——这是对什么样的函数可以在中断处理程序中的使用的限制。
有 一点非常重要:中断处理程序打断了其它代码(甚至可能是打断了在其他中断线上的另一个中断处理程序)。正是因为这种异步执行的特性,所以所有的中断处理程 序必须尽可能的迅速,简洁,尽量把工作从中断处理程序中分离出来,放在下半部执行,因为下半部可以在更合适的时间运行。
最后,中断处理程序并不具有自己的栈,相反,它共享被中断进程的内核栈,如果没有正在运行的进程,它将使用idle进程的栈,因为中断处理程序共享别人的堆栈所以它们在栈中获取空间时必须非常节省,当然,内核栈本来就很有限(内核栈在32位体系结构上是8KB,在64位体系结构上是16KB,执行中的进程上下文和产生的所有中断都共享内核栈)。因此所有的内核代码都应该谨慎利用它。
5.5中断处理机制的实现
中断处理系统在Linux中的实现是非常依赖于体系结构的。实现依赖于处理器、所使用的中断控制器的类型、体系结构的设计及机器本身。
对 于中断从硬件到内核的路由,设备产生中断,通过总线把电信号发送给中断控制器。如果中断是激活的,那么中断控制器就会把中断发往处理器。在大多数体系结构 中,这个工作就是通过电信号给处理器的特定管脚发送一个信号,处理器会立即停止它正在做的事情,关闭中断系统,然后跳转到内存中预定义的位置开始执行那里 的代码。这个预定以的位置是内核设置的,是中断处理程序的入口点。
在内核中,中断的旅程开始于预定义入口点,这类似于系统调用通过预定义的异常句柄进入内核。对于每一个中断线,处理器都会跳到对应的一个唯一的位置。至此,内核知道了所接受的中断的IRQ号。初始入口点只是在栈中保存这个号,并存放当前寄存器的值(这些值属于被中断的任务),然后,内核调用函数do_IRQ()。
5.6中断控制
Linux内核提供了一组接口用于操作机器上的中断状态。这些接口为我们提供了能够禁止当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力,这些例程都是与体系结构相关的,可以在<asm/system.h>和<asm/irq.h>中找到。 下面给出一些中断控制方法:
local_irq_disable()????????? 禁止本地中断传送
local_irq_enable()????????? 激活本地中断传送
local_irq_save()????? 保存本地中断传递的当前状态,然后禁止本地中断传递
local_irq_restore()??? 恢复本地中断传递到给定的状态
disable_irq()????????????? 禁止给定中断线,并确保该函数返回之前在该中断线上没有处理程序在运行
disable_irq_nosynoc()?????? 禁止给定中断线
enable_irq()?????????????????????? 激活给定中断线
irqs_disabled()?????????????? 如果本地中断传递被禁止,则返回非0,否则返回0
in_interrupt()???????????????? 如果在中断上下文中,则返回非0,如果在进程上下文中,则返回0
in_irq()??????????????????????? 如果当前正在执行中断处理程序,则返回非0,否则,返回0
6.1下半部
下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。对于在上半部和下半部之间划分工作,尽管不存在某种严格的规则,但还是有一些提示可供借鉴:(1)如果一个任务对时间非常敏感,将其放在中断处理程序中执行。(2)如果一个任务和硬件相关,将其放在中断处理程序中执行。(3)如果一个任务要保证不被其它中断打断,将其放在中断处理程序中执行。(4)其它所有任务,考虑放在下半部执行。当我们开始尝试写自己的驱动程序的时候,读一下别人的中断处理程序和相应的下半部会令你受益匪浅。现在的问题是:下半部具体放到以后的什么时候去做呢?下半部并不需要指明一个确切时间,只要把这些任务推迟一点,让他们在系统不太繁忙并且中断恢复后执行就可以了。通常下半部在中断处理程序一返回就会马上执行。下半部执行的关键在于当它们运行的时候,允许响应所有中断。
6.1.1为什么要用下半部?
因为在中断处理程序运行的时候,当前的中断线会被屏蔽,如果一个处理程序是SA_INTERRUPT类型,它执行的时候会禁止所有本地中断(而且把本地中断线全局屏蔽掉),再加上中断处理程序要与其它程序——甚至是其它的中断处理程序——异步执行。
具体放到以后什么时候去做呢?
在这里,“以后”仅仅用来强调不是“马上”而已,下半部并不需要指明一个确切时间,只是把这些任务推迟一点,让它们在系统不太繁忙并且中断恢复后执行就可以了,通常下半部在中断处理程序一返回就会马上执行,下半部执行的关键在于当它们运行的时候,允许响应所有的中断。
6.2软中断
软中断是用软件方式模拟硬件中断的概念,实现宏观上的异步执行效果,tasklet也是基于软中断实现的。
异步通知所基于的信号也类似于中断。
硬中断是外部设备对CPU的中断
软中断通常是硬中断服务程序对内核的中断。
信号则是由内核(或其它进程)对某个进程的中断。
6.2.1软中断的实现
软中断是在编译期间静态分配的。不像tasklet那样能被动态的注册或去除。软中断由softirq_action结构表示,它定义在<linux/interrupt.h>中:
struct softirq_action {
void( *action)(struct softirq_action *);?????????
Void*date;???????????? /传递给函数的参数*/
} ;
在kernel/softirq.c中定义了一个包含有32个该结构体的数组。????
static strcut softirq_action softirq_vec[32]; 每个注册的软中断都占据该数组中的一项。
(1)?????? 软中断处理程序:
软中断处理程序action的函数原型如下:
void softirq_handler(struct softirq_action *)
当内核运行一个软中断处理程序的时候,它就会执行这个action函数,其唯一的参数为指向相应的softirq_action结构体的指针。
一个软中断不会抢占另外一个软中断,实际上,唯一可以抢占软中断的是中断处理程序,不过,其它的软中断——甚至是相同类型的软中断——可以在其它处理器上同时执行。
(2)?????? 执行软中断:
一个注册的软中断必须在被标记后才会执行。这被称作触发软中断(raising the softirq)。通常,中断处理程序会在返回前标记它的软中断,使其在稍后被执行。软中断被标记后,可以用softirq_pending()检查到这个标记并按照索引号将softirq_pending()的返回值的相应位置1。
在合适的时刻,该软中断就会运行,在下列地方,待处理的软中断会被检查和执行:
在处理完一个硬中断以后
在ksoftirqd内核线程中
在那些显式检查和执行待处理的软中断的代码中,如网络子系统中
不管是用什么办法唤起,软中断都要在do_softirq()中执行,该函数很简单,如果有待处理的软中断,do_softirq()会遍历每一个,调用它们的处理程序。
软中断在do_softirq()中执行。do_softirq()经过简化后的核心部分:
u32 pending = sofeirq_pending(cpu);
if(pending) {
struct softirq_action *h = softirq_vec;
softirq_pending(cpu) = 0;
do {
if(pending&1) h->action(h);??? //调用action函数
h++;
pending>>=1;
}while(pending);
}
6.2.2使用软中断
软中断保留给系统中对时间要求最严格以及最重要的下半部使用。内核定时器和tasklets都是建立在软中断上的,如果你想加入一个新的软中断,首先要想想为什么用tasklet实现不了,tasklet可以动态生成,由于它们对加锁的要求不高,所以使用起来也很方便,当然,对于时间要求养并能自己高效的完成加锁工作的应用,软中断会是正确的选择。
1、? 分配索引:在编译期间,可以通过<linux/interrupt.h>中定义的一个枚举类型来静态的声明软中断。
2、? 注册处理程序:接着,在运行时通过调用open_softirq()注册软件中断处理程序,该函数有三个参数:索引号、处理函数和data域存放的数值。例如网络子系统,通过以下方式注册自己的软中断:
open_softirq(NET_TX_SOFTIRQ, net_tx_action,NULL);
open_softirq(NET_TX_SOFTIRQ, net_rx_action,NULL);
软中断处理程序的执行的时候,允许响应中断,但自己不能睡眠。
3、? 触发你的软中断:
通过在枚举类型的列表中添加新项以及调用open_softirq()进行注册以后,新的软中断处理程序就能够运行。raise_softirq()函数可以将一个软中断设置为挂起状态,让他在下次调用do_softirq()函数时投入运行。一个例子:
raise_softirq(NET_TX_SOFTIRQ);
这会触发NET_TX_SOFTIRQ软中断。它的处理程序net_tx_action()就会在内核下一次执行软中断时投入运行。该函数在触发一个软中断前要禁止中断,触发后再恢复回原来的状态。在中断处理程序中触发软中断是最常见的形式。这样,内核在执行完中断处理程序后,马上就会调用do_softirq。于是软中断开始执行中断处理程序留给它去完成的剩余任务。
6.3 Tasklet
tasklet是利用软中断实现的一种下半部机制。它和进程没有任何关系。它和软中断本质上很相似,行为表现也相近,但是,它的接口更简单,锁保护也要求较低。
软中断和tasklet怎样选择呢?
通常你应该用tasklet,软中断一般用的很少,它只在那些执行频率很高和连续性要求很高的情况下才需要,而tasklet却有更广泛的用途。
6.3.1 Tasklet的实现
因为tasklet是通过软中断实现的,所以它本身也是软中断。
(1)tasklet结构体:tasklet由tasklet_struct结构表示。每个结构体单独代表一个tasklet,它在<linux/interrupt.h>中定义:
struct tasklet_struct {
struct task_struct?? *next;?????
unsigned?? long?? state;????????
atomic_t??? count;????????????????
void (*func) (unsigned long);????
unsigned long data;?????????????????
};
结构体中的func成员是tasklet的处理程序,data是它唯一的参数。state成员只能在0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值。TASKLET_STATE_SCHED表明tasklet已经被调度,正准备投入运行,TASKLET_STATE_RUN表示该tasklet正在运行。只有count为0时,tasklet才被激活,否则被允许,不允许执行。
调度tasklet
已调度的tasklet存放在两个单处理器数据结构:tasklet_vec和task_hi_vec中。它们都是由tasklet_struct结构体构成的链表。链表中的每个tasklet_struct代表一个不同的tasklet。
tasklet是由tasklet_schedule()和tasklet_hi_schedule()函数进行调度的,它们接受一个指向tasklet_struct结构的指针作为参数。
Tasklet的实现通过软中断来实现的,tasklet_schedule()调度函数执行一些初始工作,紧接着唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,这样在下一次调用do_softirq()时就会执行tasklet。
那么do_softirq()函数什么时候执行呢?
do_softirq()会尽可能早的在下一个合适的时机执行,由于大部分tasklet和软中断都是在中断处理程序中被设置成待处理状态,所以最近一个中断返回的时候看起来就是执行do_softirq()的最佳时机。因为TASKLET_SOFTIRQ和HI_SOFTIRQ已经被触发了,所以do_softirq会执行相应的软中断处理程序。
Tasklet_action()和Tasklet_hi_action()两个处理程序就是tasklet处理的核心。
总结:所有的Tasklets都通过重复运用TASKLET_SOFTIRQ或HI_SOFTIRQ这两个软中断实现,当一个tasklet被调度时,内核就会唤起这两个软中断中的一个,随后,该软中断会被特定的函数处理,执行所有已调度的tasklet,这个函数保证同一时间里只有一个给定类别的tasklet会被执行(但其它不同类型的tasklet可以同时执行),所有这些复杂性都被一个简洁的接口隐藏起来了。
6.3.2使用tasklet
声明你自己的tasklet
可以静态创建,也可以动态创建,分别对应直接引用和间接引用。选择哪种方式取决于你到底是有(或者是想要)一个对tasklet的直接引用还是间接引用,静态创建一个tasklet(也就是有一个直接引用),使用下面<linux/interrupt.h>中定义的两个宏中的一个:
DECLARE_TASKLET(name, func, data)
实现了定义名称为name的tasklet并将其与func这个函数绑定,而传入这个函数的参数为data。
DECLARE_TASKLET_DISABLED(name, func, data);
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, dev);运行代码实际上等价于:
struct tasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0), my_tasklet_handler, dev };??? 这样就创建了一个名为my_tasklet,处理程序为tasklet_handler并且已经被激活的tasklet。
还可以通过一个间接引用(一个指针)赋给一个动态创建的tasklet_struct结构的方式来初始化一个tasklet:
Tasklet_init(t,tasklet_handler,dev);
编写你自己的tasklet处理程序
必须符合规定的函数类型:???????????????????????????????????????????????
void tasklet_handler(unsigned long data)
因为是靠软件中断实现,所以tasklet不能睡眠,这意味着你不能在tasklet中使用信号量或者其它什么阻塞式的函数。
调度你自己的tasklet
通过调用task_schedule()函数并传递给它相应的tasklet_struct的指针,该tasklet就会被调度以便执行。
tasklet_schedule(&my_tasklet);?
下面我们看一下软中断和tasklet的异同:
在前期准备工作上,首先要给软中断分配索引,而tasklet则要用宏对处理程序声明。在给软中断分配索引后,还要通过open_softirq()函数来注册处理程序。这样看来,tasklet是一步到位,直接到了处理函数,而软中断需要做更多工作。接下来软中断要等待触发(raise_softirq()或raise_softirq_irqoff),而tasklet则是等待tasklet_schedule()和tasklet_hi_schedule()对其进行调度。两者虽然在命名上不同,但殊途同归,最终的结果都是等待do_softirq()去执行处理函数,即将下半部设置为待执行状态以便稍后执行。另外,在tasklet的tasklet_schedule()中,需要完成的动作之一便是唤起(触发)TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,说明tasklet仍然是基于软中断的。在进入do_softirq()之后,所做的工作仍然有所不同,不再论述。
软中断和工作队列都是异步发生的(就是说,在中断处理返回的时候)
6.4工作队列
工作队列(work queue)是另外一种将工作推后执行的形式,他和我们前面讨论过的其他形式完全不同。工作队列可以把工作推后,交由一个内核线程去执行——这个下半部总是会在进程上下文执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势,最重要的是工作队列允许重新调度甚至是睡眠。
如果你需要用一个可以重新调度的实体来执行你的下半部处理,你应该使用工作队列,它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠,这意味着你在你需要获得大量的内存时,在你需要获取信号量时,在你需要执行阻塞式的IO操作时,它都会非常有用,如果你不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet吧!
6.4.1工作队列的实现
工 作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的其他任务。它创建的这些内核线程被称作工作者线程。工作 队列可以让驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个默认的工作者线程来处理这些工作。因此,工作队列最基 本的表现形式就转变成了一个把需要推后执行的任务交给特定的通用线程这样一个接口。
表示线程的数据结构
工作者线程用workqueue_struct结构表示:
struct workqueue_struct {
struct cpu_workqueue_struct cpu_wq[NR_CPUS];
const char *name;
struct?? list_head?? list;
};
该结构内是一个由cpu_workqueue_struct结构组成的数组,定义在kernel/workqueue.c中,数组的每一项对应一个系统中的处理器。每个工作者线程都对应这样的cpu_workqueue_struct结构体。cpu_workqueue_struct是kernel/workqueue.c中的核心数据结构:???
struct cpu_workqueue_struct???? {
spinlock_t??? lock;????????????
long??? romove_sequeue;
long?? insert_sequeue;????
wait_queue_head_t??? more_work;
wait_queue_head_t???? work_done;
struct?? workqueue_struct?? *wq;??????????
task_t??? *thread;????????????????????????????????
int?? run_depth;????????????????????????????????????
};
由此可以看出,每个工作者线程类型关联一个自己的workqueue_struct。在该结构体里面,给每个线程分配一个cpu_workqueue_struct,因而也就是给每个处理器分配一个,因为每个处理器都有一个该类型的线程。
表示工作的数据结构
所有工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread()函数。在它初始化完以后,这个函数(worker_thread)开始休眠。当有操作被插入到队列的时候,线程就会被唤醒,以便执行这些操作。当没有剩余的操作时,它又会继续睡眠。
工作用<linux/workqueue.h>中定义的work_struct结构体表示:??
struct??? work_struct {
unsigned?? long?? pending;???????
struct?? list_head entry;????????????
void?? (* func) (void *);??????????????
void?? *wq_data;????????????????????????
struct timer_list timer;??????????????
};
这些结构体被连接成链表,在每个处理器的每种类型的队列都对应这样一个链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。当工作完毕时,他会将相应的work_struct对象从链表中移去,当链表上不再有对象的时候,它就会继续睡眠。
6.4.2使用工作队列
(1)创建推后的工作
首先要做的是实际创建一些需要推后执行的工作。可以通过DECLARE_WORK在编译时静态的创建该结构体:
DECLARE_WORK(name, void (*func) (void *), void *data);
这样就会静态的创建一个名为name,处理函数为func,参数为data的work_struct结构体。也可以在运行时通过指针创建一个工作:
INIT_WORK(struct work_struct *work, void (*func)(void *), void?? *data);
这样就动态的初始化了一个由work指向的工作。
(2)工作队列的处理函数
原型是:void?? work_handler(void?? *data)
这个函数会由一个工作者线程执行,因此,函数会运行在进程上下文中,默认情况下,允许响应中断,并且不持有任何锁,如果需要,函数可以睡眠,注意的是,尽管操作处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射,通常在系统调用发生时,内核会代表用户空间的进程运行,此时它才能访问用户空间,也只有在此时它才会映射用户空间的内存。
(3)对工作进行调度
现在工作已经创建,我们可以调度它了,要把给定工作的处理函数提交给默认的events工作线程,只需调用: schedule_work(&work); work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。
(4)刷新操作
刷新工作队列的函数就是确保在卸载模块之前,要确保一些操作已经执行完毕了,该函数如下:
Void flush_scheduled_work(void);
该函数会一直等待,直到队列中所有对象都被执行以后才返回,在等待所以待处理的工作执行的时候,该函数会进入休眠状态,所以只能在进程上下文中使用它。
(5)创建新的工作队列
当缺省的队列不能满足你的需要时,你应该创建一个新的工作队列和与之对应的工作者线程。
6.5下半部之间的选择
1 从设计的角度考虑
软中断提供的执行序列化的保障最少,这就要求软中断必须采取一些步骤确保共享数据的安全。如果被考察的代码本身多线索化的工作就做得非常好,软中断就很好,对于时间要求严格和执行频率很高的话,它执行的也快。如果代码本身多线索化的工作就做得不充分,就选择tasklet比较好,由于两个同种类型的tasklet不能同时执行,实现起来也很简单一些。
2 如果你需要把任务推到进程上下文中完成,只能选择工作队列。
如果不需要睡眠,那么软中断和工作队列就更合适。工作队列造成的开销最大,因为他要牵扯到内核线程甚至是上下文切换。
3 说到易用性,工作队列最好,使用缺省的events队列简直不费吹灰之力。接下来就tasklet。他的的接口很简单,最后才是软中断,它必须静态创建。
6.6在下半部之间加锁
使用tasklet的一个好处是在于它自己负责执行的序列化保障,两个相同类型的tasklet不允许同时执行,即使在不同的处理器上也不行,意味着你无须考虑相同类型的tasklet内部的同步问题。当然,tasklet之间的同步(两个不同类型的tasklet共享同一数据时)需要正确使用锁机制。
因为软中断根本不保障执行序列化,(即使相同类型的软中断也有可能有两个实例在同时执行)所以所有的共享数据都需要合适的锁。
如果进程上下文和一个下半部共享数据,在访问这些数据之前,你需要禁止下半部的处理并得到锁的使用权,所做的这些是为了本地和SMP的保护并且防止死锁的出现。
如果中断上下文和一个下半部共享数据,在访问数据之前,你需要禁止中断并得到锁的使用权,所做的这些是为了本地和SMP的保护并且防止死锁的出现。
任何在工作队列中被共享的数据也需要使用锁机制,其中有关锁的要点和在一般内核代码中没什么区别,因为工作队列本来就是在进程上下文中执行的。
禁止下半部
一般单纯禁止下半部的处理是不够的,为了保证共享数据的安全,更常见的做法是先得到一个锁然后在禁止下半部的处理,驱动程序中通常使用的都是这种方法。
E-mail:embeddedgood@163.com
QQ:27549805
5.1中断
中断本质上是一种特殊的电信号,由硬件设备发向处理器,处理器接收到中断后,会马上向操作系统反映此信号的到来,然后就由OS复杂处理这些新到来的数据。硬件设备生成中断的时候并不考虑与处理器的时钟同步,也就是说中断可以随时产生。
定义:
所谓中断是指CPU在执行程序的过程中,出现了某些突发事件时CPU必须暂停执行当前的程序,转去处理突发事件,处理完毕后CPU又返回原程序被中断的位置并继续执行。
分类:
根据中断的来源:分为内部中断和外部中断。内部中断的中断源来自CPU内部(软件中断指令,溢出等),外部中断的中断源来自CPU外部,由外设提出请求。
根据是否屏蔽:分为可屏蔽中断与不屏蔽中断(NMI),可屏蔽中断可通过屏蔽字屏蔽,屏蔽后该中断不再得到响应。
根据中断入口跳转方式的不同:分为向量中断和非向量中断。采用向量中断的CPU通常为不同的中断分配不同的中断号。非向量中断的多个中断共享一个入口地址。进入该入口地址后再通过软件判断中断标志来识别具体是哪个中断。就是说,向量中断是由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址。
5.2中断处理程序
中断处理程序与其他内和函数的真正区别在于:
中断处理程序是被内核调用来响应中断的,而它们运行于我们称之为中断上下文的特殊上下文中。
前和后半部(上半部和下半部)
中断处理的一个主要问题是如何在处理中进行长时间的任务. 常常大量的工作必须响应一个设备中断来完成, 但是中断处理需要很快完成并且不使中断阻塞太长. 这 2 个需要(工作和速度)彼此冲突。
Linux (许多其他系统一起)解决这个问题通过将中断处理分为 2 半. 所谓的前半部是实际响应中断的函数 -- 你使用 request_irq 注册的那个. 后半部是由前半部调度来延后执行的函数, 在一个更安全的时间. 最大的不同在前半部处理和后半部之间是所有的中断在后半部执行时都使能——这就是为什么它在一个更安全时间运行. 在典型的场景中, 前半部保存设备数据到一个设备特定的缓存, 调度它的后半部, 并且退出: 这个操作非常快. 后半部接着进行任何其他需要的工作, 例如唤醒进程, 启动另一个 I/O 操作, 等等. 这种设置允许前半部来服务一个新中断而同时后半部仍然在工作。
5.3注册中断处理程序
中断处理程序是管理硬件的驱动程序的组成部分。每个设备都有相关的驱动程序,如果设备使用中断,那么相应的驱动程序就注册一个中断处理程序。驱动程序可以通过下面的函数注册并激活一个中断处理程序,以便处理中断:
int request_irq(unsigned int irq,
irqreturn_t (*handler)(int, void *, struct pt_regs *),
unsigned long flags,
const char *dev_name,
void *dev_id);
(1)第一个参数irq表示要分配的中断号。对某些设备,这个值是先定的,对大多数设备来说,这个值是可以通过探测获取,也可以动态确定。
(2)第二个参数handler是一个指针,指向处理这个中断的实际中断处理程序。只要操作系统一接收到中断,该函数就被调用。
(3)第三个参数irqflags可以为0,也可能是下列一个或多个标志的位掩码:SA_INTERRUPT:此标志表明给定的中断处理程序是一个快速中断处理程序。
SA_SAMPLE_RANDOM:此标志表明这个设备产生的中断对内核熵池有贡献。
SA_SHIRQ:此标志表明可以在多个中断处理程序之间共享中断线。
(4)第四个参数devname是与中断相关的设备的ASCII文本表示法。这些名字会被/proc/irq和proc/interrupt文件使用,以便与用户通信。
(5)第五个参数dev_id主要用于共享中断线。用于区分共享中断线上的各个中断处理程序。内核每次调用中断处理程序时,都会把这个指针传递给它。实践中往往会通过它传递驱动程序的设备结构:这个指针是唯一的,而且有可能在中断处理程序内及设备模式中被用到。
request_irq()成功执行会返回0,如果返回非0值,就表示有错误发生,在这种情况下,指定的中断处理程序不会被注册,最常见的错误是-EBUSY,它表示给定的中断线已经在使用(或者当前用户或者你没有指定SA_SHIRQ())。
注意:request_irq()函数可能会睡眠,因此,不能在中断上下文或其它不允许阻塞的代码中调用该函数,在睡眠不安全的上下文中可以安全的调用request_irq()函数,是一种常见错误。
为什么request_irq()会引起睡眠?
在注册的过程中,内核需要在/proc/irq文件创建一个与中断对应的项,函数proc_mkdir()就是用来创建这个新的procfs项的,proc_makedir()通过调用函数proc_create()对这个新的profs项进行设置,而proc_create()会调用函数kmalloc()来请求分配内存,而我们知道函数kmalloc()是可以睡眠的。
释放中断处理程序
释放中断线,可以调用:
void free_irq(unsigned int irq, void *dev_id);
如果指定的中断线不是共享的,那么,该函数删除处理程序的同时将禁用这条中断线,如果中断线是共享的,则仅删除dec_id所对应的处理程序,而这条中断线本身只有在删除了最后一个处理程序才会被禁用,由此可以看出为什么唯一的dev_id如此重要,对于共享的中断线,需要一个唯一的信息来区分其上面的多个处理程序,并通过它来保证函数free_irq()能够正确的删除指定的处理程序,不管在哪种情况下(共享或不共享),如果dev_id非空,它都必须与需要的删除的处理程序相匹配。
注意:必须从进程上下文中调用free_irq()。
5.4编写中断处理程序
一个典型的中断处理程序声明:???????????????????????????????????????????????????????????????????????????????????????????????????
static irqreturn_t intr_handle(int irq, void *dev_id, struct pt_regs *regs??????????????????????????????????????????
注意,它的类型与request_irq()参数中handler所要求的参数类型相匹配(handler函数有一个类型为irqreturn_t的返回值)。第一个参数irq就是这个处理程序要响应的中断的中断线号。dev_id是一个通用的指针,它与在中断注册时传递给request_irq()的参数dev_id必须一致。dev_id也有可能指向中断处理程序使用的一个数据结构,对每个设备而言,设备结构都是唯一的,而且可能在中断处理程序中也用的到,因此,也通常会把设备结构传递给dev_id。
参数regs是一个指向结构的指针,该结构包含处理中断之前处理器的寄存器和状态,除了调试以外,很少用到。
中断处理程序的返回值是一个特殊类型:irqreturn_t。它可能返回两个特殊的值:IRQ_NONE和IRQ_HANDLED。irqreturn_t这个返回类型实际上就是一个int型,之所以使用这些特殊值是为了与早期的内核保持兼容。
中断处理程序通常会标记static,因为它从来不会被别的文件中的代码直接调用。
5.4.1共享的中断处理程序
共享的处理程序与非共享的处理程序在注册和运行方式上比较相似,但差异主要有以下几点:
(1)request_irq()的参数flags必须设置SA_SHIRQ标志。
(2)对每个注册的中断处理程序来说,dev_id参数必须唯一。而指向任一设备结构体的指针就可以满足这一要求;通常会选择设备结构,因为它是唯一的,而且中断处理程序可能会用到它。
(3)中断处理程序必须能够区分它的设备是否真的产生中断。这既需要硬件支持,也需要处理程序中有相关的处理逻辑。如果硬件不支持这一功能,它就没法知道到底是与它对应的设备发出了这个中断,还是共享这条中断线的其它设备发出了这个中断。
指定SA_SHIRQ标志以调用request_irq()时,只有在以下两种情况下才可能成功,一是中断线当前未被注册,二是在该线上的所有已注册处理程序都指定了SA_SHIRQ。
内核接收一个中断后,它将依次调用在该中断线上注册的每一个处理程序,因此,一个处理程序必须知道它是否应该为这个中断负责,如果与它相关的设备并没有产生中断,那么,处理程序应该立即退出。
5.4.3中断上下文
当执行一个中断处理程序或下半部时,内核处于中断上下文(interrput context)中。让我们先回忆一下进程上下文,进程上下文是一种内核所处的操作模式,此时内核代表进程执行——例如,执行系统调用或运行内核线程,在进程上下文中,可以通过current宏去关联当前进程,此外,因为进程是以进程上下文的形式连接到内核中的,因此,在进程上下文可以睡眠,也可以调用调度程序。
与之相反,中断上下文和进程并没有什么瓜葛,与current宏 也是不相干的(尽管它会指向被中断的进程),因为没有进程的背景,所以中断上下文不可以睡眠——否则又怎能对它重新调度呢?因此,不能从中断上下文中调用 某些函数,如果一个函数睡眠,就不能在你的中断处理程序中使用它——这是对什么样的函数可以在中断处理程序中的使用的限制。
有 一点非常重要:中断处理程序打断了其它代码(甚至可能是打断了在其他中断线上的另一个中断处理程序)。正是因为这种异步执行的特性,所以所有的中断处理程 序必须尽可能的迅速,简洁,尽量把工作从中断处理程序中分离出来,放在下半部执行,因为下半部可以在更合适的时间运行。
最后,中断处理程序并不具有自己的栈,相反,它共享被中断进程的内核栈,如果没有正在运行的进程,它将使用idle进程的栈,因为中断处理程序共享别人的堆栈所以它们在栈中获取空间时必须非常节省,当然,内核栈本来就很有限(内核栈在32位体系结构上是8KB,在64位体系结构上是16KB,执行中的进程上下文和产生的所有中断都共享内核栈)。因此所有的内核代码都应该谨慎利用它。
5.5中断处理机制的实现
中断处理系统在Linux中的实现是非常依赖于体系结构的。实现依赖于处理器、所使用的中断控制器的类型、体系结构的设计及机器本身。
对 于中断从硬件到内核的路由,设备产生中断,通过总线把电信号发送给中断控制器。如果中断是激活的,那么中断控制器就会把中断发往处理器。在大多数体系结构 中,这个工作就是通过电信号给处理器的特定管脚发送一个信号,处理器会立即停止它正在做的事情,关闭中断系统,然后跳转到内存中预定义的位置开始执行那里 的代码。这个预定以的位置是内核设置的,是中断处理程序的入口点。
在内核中,中断的旅程开始于预定义入口点,这类似于系统调用通过预定义的异常句柄进入内核。对于每一个中断线,处理器都会跳到对应的一个唯一的位置。至此,内核知道了所接受的中断的IRQ号。初始入口点只是在栈中保存这个号,并存放当前寄存器的值(这些值属于被中断的任务),然后,内核调用函数do_IRQ()。
5.6中断控制
Linux内核提供了一组接口用于操作机器上的中断状态。这些接口为我们提供了能够禁止当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力,这些例程都是与体系结构相关的,可以在<asm/system.h>和<asm/irq.h>中找到。 下面给出一些中断控制方法:
local_irq_disable()????????? 禁止本地中断传送
local_irq_enable()????????? 激活本地中断传送
local_irq_save()????? 保存本地中断传递的当前状态,然后禁止本地中断传递
local_irq_restore()??? 恢复本地中断传递到给定的状态
disable_irq()????????????? 禁止给定中断线,并确保该函数返回之前在该中断线上没有处理程序在运行
disable_irq_nosynoc()?????? 禁止给定中断线
enable_irq()?????????????????????? 激活给定中断线
irqs_disabled()?????????????? 如果本地中断传递被禁止,则返回非0,否则返回0
in_interrupt()???????????????? 如果在中断上下文中,则返回非0,如果在进程上下文中,则返回0
in_irq()??????????????????????? 如果当前正在执行中断处理程序,则返回非0,否则,返回0
6.1下半部
下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。对于在上半部和下半部之间划分工作,尽管不存在某种严格的规则,但还是有一些提示可供借鉴:(1)如果一个任务对时间非常敏感,将其放在中断处理程序中执行。(2)如果一个任务和硬件相关,将其放在中断处理程序中执行。(3)如果一个任务要保证不被其它中断打断,将其放在中断处理程序中执行。(4)其它所有任务,考虑放在下半部执行。当我们开始尝试写自己的驱动程序的时候,读一下别人的中断处理程序和相应的下半部会令你受益匪浅。现在的问题是:下半部具体放到以后的什么时候去做呢?下半部并不需要指明一个确切时间,只要把这些任务推迟一点,让他们在系统不太繁忙并且中断恢复后执行就可以了。通常下半部在中断处理程序一返回就会马上执行。下半部执行的关键在于当它们运行的时候,允许响应所有中断。
6.1.1为什么要用下半部?
因为在中断处理程序运行的时候,当前的中断线会被屏蔽,如果一个处理程序是SA_INTERRUPT类型,它执行的时候会禁止所有本地中断(而且把本地中断线全局屏蔽掉),再加上中断处理程序要与其它程序——甚至是其它的中断处理程序——异步执行。
具体放到以后什么时候去做呢?
在这里,“以后”仅仅用来强调不是“马上”而已,下半部并不需要指明一个确切时间,只是把这些任务推迟一点,让它们在系统不太繁忙并且中断恢复后执行就可以了,通常下半部在中断处理程序一返回就会马上执行,下半部执行的关键在于当它们运行的时候,允许响应所有的中断。
6.2软中断
软中断是用软件方式模拟硬件中断的概念,实现宏观上的异步执行效果,tasklet也是基于软中断实现的。
异步通知所基于的信号也类似于中断。
硬中断是外部设备对CPU的中断
软中断通常是硬中断服务程序对内核的中断。
信号则是由内核(或其它进程)对某个进程的中断。
6.2.1软中断的实现
软中断是在编译期间静态分配的。不像tasklet那样能被动态的注册或去除。软中断由softirq_action结构表示,它定义在<linux/interrupt.h>中:
struct softirq_action {
void( *action)(struct softirq_action *);?????????
Void*date;???????????? /传递给函数的参数*/
} ;
在kernel/softirq.c中定义了一个包含有32个该结构体的数组。????
static strcut softirq_action softirq_vec[32]; 每个注册的软中断都占据该数组中的一项。
(1)?????? 软中断处理程序:
软中断处理程序action的函数原型如下:
void softirq_handler(struct softirq_action *)
当内核运行一个软中断处理程序的时候,它就会执行这个action函数,其唯一的参数为指向相应的softirq_action结构体的指针。
一个软中断不会抢占另外一个软中断,实际上,唯一可以抢占软中断的是中断处理程序,不过,其它的软中断——甚至是相同类型的软中断——可以在其它处理器上同时执行。
(2)?????? 执行软中断:
一个注册的软中断必须在被标记后才会执行。这被称作触发软中断(raising the softirq)。通常,中断处理程序会在返回前标记它的软中断,使其在稍后被执行。软中断被标记后,可以用softirq_pending()检查到这个标记并按照索引号将softirq_pending()的返回值的相应位置1。
在合适的时刻,该软中断就会运行,在下列地方,待处理的软中断会被检查和执行:
在处理完一个硬中断以后
在ksoftirqd内核线程中
在那些显式检查和执行待处理的软中断的代码中,如网络子系统中
不管是用什么办法唤起,软中断都要在do_softirq()中执行,该函数很简单,如果有待处理的软中断,do_softirq()会遍历每一个,调用它们的处理程序。
软中断在do_softirq()中执行。do_softirq()经过简化后的核心部分:
u32 pending = sofeirq_pending(cpu);
if(pending) {
struct softirq_action *h = softirq_vec;
softirq_pending(cpu) = 0;
do {
if(pending&1) h->action(h);??? //调用action函数
h++;
pending>>=1;
}while(pending);
}
6.2.2使用软中断
软中断保留给系统中对时间要求最严格以及最重要的下半部使用。内核定时器和tasklets都是建立在软中断上的,如果你想加入一个新的软中断,首先要想想为什么用tasklet实现不了,tasklet可以动态生成,由于它们对加锁的要求不高,所以使用起来也很方便,当然,对于时间要求养并能自己高效的完成加锁工作的应用,软中断会是正确的选择。
1、? 分配索引:在编译期间,可以通过<linux/interrupt.h>中定义的一个枚举类型来静态的声明软中断。
2、? 注册处理程序:接着,在运行时通过调用open_softirq()注册软件中断处理程序,该函数有三个参数:索引号、处理函数和data域存放的数值。例如网络子系统,通过以下方式注册自己的软中断:
open_softirq(NET_TX_SOFTIRQ, net_tx_action,NULL);
open_softirq(NET_TX_SOFTIRQ, net_rx_action,NULL);
软中断处理程序的执行的时候,允许响应中断,但自己不能睡眠。
3、? 触发你的软中断:
通过在枚举类型的列表中添加新项以及调用open_softirq()进行注册以后,新的软中断处理程序就能够运行。raise_softirq()函数可以将一个软中断设置为挂起状态,让他在下次调用do_softirq()函数时投入运行。一个例子:
raise_softirq(NET_TX_SOFTIRQ);
这会触发NET_TX_SOFTIRQ软中断。它的处理程序net_tx_action()就会在内核下一次执行软中断时投入运行。该函数在触发一个软中断前要禁止中断,触发后再恢复回原来的状态。在中断处理程序中触发软中断是最常见的形式。这样,内核在执行完中断处理程序后,马上就会调用do_softirq。于是软中断开始执行中断处理程序留给它去完成的剩余任务。
6.3 Tasklet
tasklet是利用软中断实现的一种下半部机制。它和进程没有任何关系。它和软中断本质上很相似,行为表现也相近,但是,它的接口更简单,锁保护也要求较低。
软中断和tasklet怎样选择呢?
通常你应该用tasklet,软中断一般用的很少,它只在那些执行频率很高和连续性要求很高的情况下才需要,而tasklet却有更广泛的用途。
6.3.1 Tasklet的实现
因为tasklet是通过软中断实现的,所以它本身也是软中断。
(1)tasklet结构体:tasklet由tasklet_struct结构表示。每个结构体单独代表一个tasklet,它在<linux/interrupt.h>中定义:
struct tasklet_struct {
struct task_struct?? *next;?????
unsigned?? long?? state;????????
atomic_t??? count;????????????????
void (*func) (unsigned long);????
unsigned long data;?????????????????
};
结构体中的func成员是tasklet的处理程序,data是它唯一的参数。state成员只能在0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值。TASKLET_STATE_SCHED表明tasklet已经被调度,正准备投入运行,TASKLET_STATE_RUN表示该tasklet正在运行。只有count为0时,tasklet才被激活,否则被允许,不允许执行。
调度tasklet
已调度的tasklet存放在两个单处理器数据结构:tasklet_vec和task_hi_vec中。它们都是由tasklet_struct结构体构成的链表。链表中的每个tasklet_struct代表一个不同的tasklet。
tasklet是由tasklet_schedule()和tasklet_hi_schedule()函数进行调度的,它们接受一个指向tasklet_struct结构的指针作为参数。
Tasklet的实现通过软中断来实现的,tasklet_schedule()调度函数执行一些初始工作,紧接着唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,这样在下一次调用do_softirq()时就会执行tasklet。
那么do_softirq()函数什么时候执行呢?
do_softirq()会尽可能早的在下一个合适的时机执行,由于大部分tasklet和软中断都是在中断处理程序中被设置成待处理状态,所以最近一个中断返回的时候看起来就是执行do_softirq()的最佳时机。因为TASKLET_SOFTIRQ和HI_SOFTIRQ已经被触发了,所以do_softirq会执行相应的软中断处理程序。
Tasklet_action()和Tasklet_hi_action()两个处理程序就是tasklet处理的核心。
总结:所有的Tasklets都通过重复运用TASKLET_SOFTIRQ或HI_SOFTIRQ这两个软中断实现,当一个tasklet被调度时,内核就会唤起这两个软中断中的一个,随后,该软中断会被特定的函数处理,执行所有已调度的tasklet,这个函数保证同一时间里只有一个给定类别的tasklet会被执行(但其它不同类型的tasklet可以同时执行),所有这些复杂性都被一个简洁的接口隐藏起来了。
6.3.2使用tasklet
声明你自己的tasklet
可以静态创建,也可以动态创建,分别对应直接引用和间接引用。选择哪种方式取决于你到底是有(或者是想要)一个对tasklet的直接引用还是间接引用,静态创建一个tasklet(也就是有一个直接引用),使用下面<linux/interrupt.h>中定义的两个宏中的一个:
DECLARE_TASKLET(name, func, data)
实现了定义名称为name的tasklet并将其与func这个函数绑定,而传入这个函数的参数为data。
DECLARE_TASKLET_DISABLED(name, func, data);
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, dev);运行代码实际上等价于:
struct tasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0), my_tasklet_handler, dev };??? 这样就创建了一个名为my_tasklet,处理程序为tasklet_handler并且已经被激活的tasklet。
还可以通过一个间接引用(一个指针)赋给一个动态创建的tasklet_struct结构的方式来初始化一个tasklet:
Tasklet_init(t,tasklet_handler,dev);
编写你自己的tasklet处理程序
必须符合规定的函数类型:???????????????????????????????????????????????
void tasklet_handler(unsigned long data)
因为是靠软件中断实现,所以tasklet不能睡眠,这意味着你不能在tasklet中使用信号量或者其它什么阻塞式的函数。
调度你自己的tasklet
通过调用task_schedule()函数并传递给它相应的tasklet_struct的指针,该tasklet就会被调度以便执行。
tasklet_schedule(&my_tasklet);?
下面我们看一下软中断和tasklet的异同:
在前期准备工作上,首先要给软中断分配索引,而tasklet则要用宏对处理程序声明。在给软中断分配索引后,还要通过open_softirq()函数来注册处理程序。这样看来,tasklet是一步到位,直接到了处理函数,而软中断需要做更多工作。接下来软中断要等待触发(raise_softirq()或raise_softirq_irqoff),而tasklet则是等待tasklet_schedule()和tasklet_hi_schedule()对其进行调度。两者虽然在命名上不同,但殊途同归,最终的结果都是等待do_softirq()去执行处理函数,即将下半部设置为待执行状态以便稍后执行。另外,在tasklet的tasklet_schedule()中,需要完成的动作之一便是唤起(触发)TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,说明tasklet仍然是基于软中断的。在进入do_softirq()之后,所做的工作仍然有所不同,不再论述。
软中断和工作队列都是异步发生的(就是说,在中断处理返回的时候)
6.4工作队列
工作队列(work queue)是另外一种将工作推后执行的形式,他和我们前面讨论过的其他形式完全不同。工作队列可以把工作推后,交由一个内核线程去执行——这个下半部总是会在进程上下文执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势,最重要的是工作队列允许重新调度甚至是睡眠。
如果你需要用一个可以重新调度的实体来执行你的下半部处理,你应该使用工作队列,它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠,这意味着你在你需要获得大量的内存时,在你需要获取信号量时,在你需要执行阻塞式的IO操作时,它都会非常有用,如果你不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet吧!
6.4.1工作队列的实现
工 作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的其他任务。它创建的这些内核线程被称作工作者线程。工作 队列可以让驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个默认的工作者线程来处理这些工作。因此,工作队列最基 本的表现形式就转变成了一个把需要推后执行的任务交给特定的通用线程这样一个接口。
表示线程的数据结构
工作者线程用workqueue_struct结构表示:
struct workqueue_struct {
struct cpu_workqueue_struct cpu_wq[NR_CPUS];
const char *name;
struct?? list_head?? list;
};
该结构内是一个由cpu_workqueue_struct结构组成的数组,定义在kernel/workqueue.c中,数组的每一项对应一个系统中的处理器。每个工作者线程都对应这样的cpu_workqueue_struct结构体。cpu_workqueue_struct是kernel/workqueue.c中的核心数据结构:???
struct cpu_workqueue_struct???? {
spinlock_t??? lock;????????????
long??? romove_sequeue;
long?? insert_sequeue;????
wait_queue_head_t??? more_work;
wait_queue_head_t???? work_done;
struct?? workqueue_struct?? *wq;??????????
task_t??? *thread;????????????????????????????????
int?? run_depth;????????????????????????????????????
};
由此可以看出,每个工作者线程类型关联一个自己的workqueue_struct。在该结构体里面,给每个线程分配一个cpu_workqueue_struct,因而也就是给每个处理器分配一个,因为每个处理器都有一个该类型的线程。
表示工作的数据结构
所有工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread()函数。在它初始化完以后,这个函数(worker_thread)开始休眠。当有操作被插入到队列的时候,线程就会被唤醒,以便执行这些操作。当没有剩余的操作时,它又会继续睡眠。
工作用<linux/workqueue.h>中定义的work_struct结构体表示:??
struct??? work_struct {
unsigned?? long?? pending;???????
struct?? list_head entry;????????????
void?? (* func) (void *);??????????????
void?? *wq_data;????????????????????????
struct timer_list timer;??????????????
};
这些结构体被连接成链表,在每个处理器的每种类型的队列都对应这样一个链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。当工作完毕时,他会将相应的work_struct对象从链表中移去,当链表上不再有对象的时候,它就会继续睡眠。
6.4.2使用工作队列
(1)创建推后的工作
首先要做的是实际创建一些需要推后执行的工作。可以通过DECLARE_WORK在编译时静态的创建该结构体:
DECLARE_WORK(name, void (*func) (void *), void *data);
这样就会静态的创建一个名为name,处理函数为func,参数为data的work_struct结构体。也可以在运行时通过指针创建一个工作:
INIT_WORK(struct work_struct *work, void (*func)(void *), void?? *data);
这样就动态的初始化了一个由work指向的工作。
(2)工作队列的处理函数
原型是:void?? work_handler(void?? *data)
这个函数会由一个工作者线程执行,因此,函数会运行在进程上下文中,默认情况下,允许响应中断,并且不持有任何锁,如果需要,函数可以睡眠,注意的是,尽管操作处理函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射,通常在系统调用发生时,内核会代表用户空间的进程运行,此时它才能访问用户空间,也只有在此时它才会映射用户空间的内存。
(3)对工作进行调度
现在工作已经创建,我们可以调度它了,要把给定工作的处理函数提交给默认的events工作线程,只需调用: schedule_work(&work); work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。
(4)刷新操作
刷新工作队列的函数就是确保在卸载模块之前,要确保一些操作已经执行完毕了,该函数如下:
Void flush_scheduled_work(void);
该函数会一直等待,直到队列中所有对象都被执行以后才返回,在等待所以待处理的工作执行的时候,该函数会进入休眠状态,所以只能在进程上下文中使用它。
(5)创建新的工作队列
当缺省的队列不能满足你的需要时,你应该创建一个新的工作队列和与之对应的工作者线程。
6.5下半部之间的选择
1 从设计的角度考虑
软中断提供的执行序列化的保障最少,这就要求软中断必须采取一些步骤确保共享数据的安全。如果被考察的代码本身多线索化的工作就做得非常好,软中断就很好,对于时间要求严格和执行频率很高的话,它执行的也快。如果代码本身多线索化的工作就做得不充分,就选择tasklet比较好,由于两个同种类型的tasklet不能同时执行,实现起来也很简单一些。
2 如果你需要把任务推到进程上下文中完成,只能选择工作队列。
如果不需要睡眠,那么软中断和工作队列就更合适。工作队列造成的开销最大,因为他要牵扯到内核线程甚至是上下文切换。
3 说到易用性,工作队列最好,使用缺省的events队列简直不费吹灰之力。接下来就tasklet。他的的接口很简单,最后才是软中断,它必须静态创建。
6.6在下半部之间加锁
使用tasklet的一个好处是在于它自己负责执行的序列化保障,两个相同类型的tasklet不允许同时执行,即使在不同的处理器上也不行,意味着你无须考虑相同类型的tasklet内部的同步问题。当然,tasklet之间的同步(两个不同类型的tasklet共享同一数据时)需要正确使用锁机制。
因为软中断根本不保障执行序列化,(即使相同类型的软中断也有可能有两个实例在同时执行)所以所有的共享数据都需要合适的锁。
如果进程上下文和一个下半部共享数据,在访问这些数据之前,你需要禁止下半部的处理并得到锁的使用权,所做的这些是为了本地和SMP的保护并且防止死锁的出现。
如果中断上下文和一个下半部共享数据,在访问数据之前,你需要禁止中断并得到锁的使用权,所做的这些是为了本地和SMP的保护并且防止死锁的出现。
任何在工作队列中被共享的数据也需要使用锁机制,其中有关锁的要点和在一般内核代码中没什么区别,因为工作队列本来就是在进程上下文中执行的。
禁止下半部
一般单纯禁止下半部的处理是不够的,为了保证共享数据的安全,更常见的做法是先得到一个锁然后在禁止下半部的处理,驱动程序中通常使用的都是这种方法。
<script type="text/javascript" id="wumiiRelatedItems"> </script>