中断子系统分析(三)
前面我们有分析过中断服务程序。中断服务程序是中断子系统中必不可少的一部分,因为中断产生的原因就是硬件设备本身状态发生了改变,中断系统的目的也就是让系统去处理硬件设备。如果没有中断服务程序,那么中断就是一个无意义的事件。但是中断服务程序也存在一些局限性:
中断处理程序是异步执行的,它打断了系统原来正在做的事情,如果打断时间过长,会对系统造成不可估量的影响。
中断服务程序在处理过程当中,最好的情况是当前中断线会被屏蔽,最坏的情况(设置了IRQF_DISABLED)是当前处理器所有中断都被屏蔽,当前中断线被全局屏蔽。我们知道,中断被屏蔽是一件很不好的事情,因为系统没法与硬件交流。
中断服务程序不能在进程上下文中运行,所以它们不能阻塞(因为中断的优先级是最高的,如果阻塞了的话进程是没法得到处理器的)。
因此,中断服务程序的执行时间越短越好。内核提供了一个机制:将中断处理流程划分成两部分,上半部和下半部。中断服务例程是上半部,它主要完成一些硬件操作:如对中断的到达进行确认,从硬件中读取数据,设置硬件状态等。这些工作对时间非常敏感,所以把它们放在中断上半部处理。其他的工作都可以放在下半部去处理了。在中断的下半部中对中断是可以响应的,而且有下半部机制是可以睡眠的,这对处理中断事件来说是非常有帮助的,比如对于某些设备的中断处理中需要分配内存,这就可能导致处理线程的睡眠了。中断上下半的不存在很严格的规定什么操作必须在上部分或者下部分完成,总的来说可遵循以下规则:
如果一个任务对时间非常敏感,将其放在中断处理程序当中
如果一个任务和硬件相关,将其放在中断处理程序当中
如果一个任务要保证不能被其他中断打断(特别是相同的中断),将其放在中断处理程序当。
其他的任务都可以在下半部去执行。
首先来了解下下半部在什么时候触发吧!
在第一篇中有分析过,中断产生后会执行到gic.c中的gic_handle_irq->handle_IRQ(irqnr, regs){
struct pt_regs *old_regs = set_irq_regs(regs);
irq_enter();
if (unlikely(irq >= nr_irqs)) {
if (printk_ratelimit())
printk(KERN_WARNING "Bad IRQ%u\n", irq);
ack_bad_irq(irq);
} else {
generic_handle_irq(irq);
}
/* AT91 specific workaround */
irq_finish(irq);
irq_exit();
set_irq_regs(old_regs);
}
其中ge neric_handle_irq(irq)就是执行中断服务程序,中断服务程序执行完成之后在退出中断处理流程时有irq_exit()
{
account_system_vtime(current);
trace_hardirq_exit();
sub_preempt_count(IRQ_EXIT_OFFSET);
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
……..
}
在invoke_softirq()中就是执行中断下半部了。
首先我们分析下执行中断下半部的条件,它需要满足两个条件:
1:当前不能处于硬件中断或者软件中断过程中
2:必须有软中断处于pending状态。
第一个条件也就是!in_interrupt() 判断当前是否处于硬中断或软中断处理状态下:
展开in_interrupt():
preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
| NMI_MASK))
在内核中有定义:
#define preempt_count() (current_thread_info()->preempt_count)
#define PREEMPT_MASK (__IRQ_MASK(PREEMPT_BITS) << PREEMPT_SHIFT)
#define SOFTIRQ_MASK (__IRQ_MASK(SOFTIRQ_BITS) << SOFTIRQ_SHIFT)
#define HARDIRQ_MASK (__IRQ_MASK(HARDIRQ_BITS) << HARDIRQ_SHIFT)
#define NMI_MASK (__IRQ_MASK(NMI_BITS) << NMI_SHIFT)
Preempt_count的数值表示当前是否可以进行进程调度。当preempt_count为0时表示可以,非0时不行。
HARDIRQ_SHIFT SOFTIRQ_SHIFT
23 15 7 0
如上表所示,reempt_count的8-15位表示需要处理软中断个数,16-23位表示需要处理硬中断数量,也就是说是否存在嵌套的硬中断或者软中断。在中断服务程序执行之前有irq_enter()->__irq_enter():
#define __irq_enter() \
do { \
account_system_vtime(current); \
add_preempt_count(HARDIRQ_OFFSET); \
trace_hardirq_enter(); \
} while (0)
分析add_preempt_count(HARDIRQ_OFFSET)
# define add_preempt_count(val) do { preempt_count() += (val); } while (0)
也就是在中断服务程序执行之前会对preempt_count作一个add HARDIRQ_OFFSET表示有一个硬件中断需要处理,而在中断服务程序执行完之后有sub_preempt_count(IRQ_EXIT_OFFSET)表示有一个硬件中断处理好了,这时候会对preempt_count作一个sub HARDIRQ_OFFSET的操作。然后这时候去判断preempt_count的硬件中断位,如果不为0则表示还有硬件中断没有处理,需要优先处理硬件中断,因此这时候便不方便马上去处理软中断了。
同样对于SOFTIRQ_SHIFT的判断也一样。
第二个条件:需要有未响应的软件中断。这个比较好理解,如果没有未响应的软件中断,那我们执行啥去。
如果条件满足,那么在invoke_softirq()中就会去执行中断的下半部。
Linux中主要的下半部机制有:软中断,tasklet,工作队列。
软中断:
Linux内核在softirq.c中定义了一个静态数组来保存软中断:
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
struct softirq_action
{
void (*action)(struct softirq_action *);
};
从结构的定义我们可以得知,软中断结构中保存了其对应的软中断处理函数,并且传入一个软中断结构作为参数,通常将软中断结构本身的指针作为软中断处理函数的参数,如:
One_softirq->action(one_softirq);
NR_SOFTIRQS表示当前系统中的软中断的个数,在
中定义了一个枚举类型来静态声明软中断:
enum
{
HI_SOFTIRQ=0, 优先级高的tasklets
TIMER_SOFTIRQ, 定时器的下半部
NET_TX_SOFTIRQ, 发送网络数据包
NET_RX_SOFTIRQ, 接收网络数据包
BLOCK_SOFTIRQ, BLOCK装置
BLOCK_IOPOLL_SOFTIRQ, BLOCK I/O轮询
TASKLET_SOFTIRQ, 正常优先级的tasklets
SCHED_SOFTIRQ, 调度程序
HRTIMER_SOFTIRQ, 高分辨率的定时器
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
以上定义的软中断类型根据其值的大小对应着其优先级的高低,值 越小优先级越高,比如HI_SOFTIRQ的值最小,它的优先级是最高的,当系统准备执行软中断时,会从先执行HI_SOFTIRQ类型的软中断(如果有注册此软中断)。
同样,我们也可以定义自己的软中断,比如我们可以定义一个MY_SOFTIRQ,将它放在RCU_SOFTIRQ后面,然后将内核编译一遍,这样我们就可以在新的内核当中使用我们自己定义的软中断类型了。当然,软中断类型数量最多不要超过32个。
软中断的注册:
Open_softirq(HI_SOFTIRQ,mywork);
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
由代码可知,注册一个软中断就是初始化以软中断类型为索引的软中断数组元素,也就是将软中断处理函数保存在softirq_vec数组中第软中断类型值个元素。
软中断执行:
接上面的触发下半部分析,invoke_softirq()->do_softirq()->__do_softirq()
{
struct softirq_action *h;
__u32 pending;
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
pending = local_softirq_pending();//保存着未响应软中断的位图
account_system_vtime(current);
__local_bh_disable((unsigned long)__builtin_return_address(0),
SOFTIRQ_OFFSET);
lockdep_softirq_enter();
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);//将系统实际中断位图清0,因为它已被保存在pending当中。
local_irq_enable();//开中断
h = softirq_vec;//指向软中断数组的首元素
do {
if (pending & 1) {
unsigned int vec_nr = h - softirq_vec;
int prev_count = preempt_count();
kstat_incr_softirqs_this_cpu(vec_nr);
trace_softirq_entry(vec_nr);
h->action(h);
trace_softirq_exit(vec_nr);
if (unlikely(prev_count != preempt_count())) {
printk(KERN_ERR "huh, entered softirq %u %s %p"
"with preempt_count %08x,"
" exited with %08x?\n", vec_nr,
softirq_to_name[vec_nr], h->action,
prev_count, preempt_count());
preempt_count() = prev_count;
}
rcu_bh_qs(cpu);
}
h++;
pending >>= 1;
} while (pending);
/*在这个do while{}循环中就是软中断的执行过程了,它检查并执行所有还未执行的软中断。它用一个32位的变量pending去保存未执行的软中断位图:如果pending的第n位被置1,则表示第n位对应类型的软中断待处理。
指针h首先指向softirq_vec的第一项
如果pending的第一位为1,则h->action(h)执行。
H指向softirq_vec中的下一个元素,pending右移一位,则h指向softirq_vec的第二个元素,而pending原来的第二位变成了最低位。如果pending不为0,表示还有软中断没有执行。再判断此时pending的最低位是否置1,如果置1,执行h指向的action,不置1,则继续判断pending的下一位。
如此循环,直到pending为0,此时表示所有未响应的软中断均已处理了。
*/
local_irq_disable();//关中断
pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;
/*
因为软中断执行过程中中断是打开的,所以有可能处理软中断过程中有中断产生,而在中断处理函数中又有可能开启新的软中断,因此需要处理新注册的软中断。当然这里也用max_restart来控制了处理新的软中断的次数不得多于10次,要不在中断中一直注册新的软中断,而中断处理函数执行完后就重新处理未响应的软中断,那么其他进程也会一直拿不到CPU了
*/
if (pending)
wakeup_softirqd();
/*
如果10次之后还有软中断未处理,则唤醒系统线程ksoftirqd去处理。
*/
……
}
我们也可以自己触发软中断:
Raise_softirq(HI_SOFTIRQ);
它最终也是调用wakeup_softirqd()通过唤醒系统线程ksoftirqd去处理软中断。
软中断处理函数在执行的时候,相同类型的软中断仍可以其他处理器上执行,因此,我们必须保证软中断处理程序中的共享数据的保护。
Tasklet:
Tasklet是利用软中断实现的一种下半部机制。前面我们有说过软中断有这两种类型:HI_SOFTIRQ和TASKLET_SOFTIRQ。Tasklet实际上就是这两种类型的软中断,它们的区别也就是执行的先后。但tasklet跟软中断又有不同,软中断可以在所有处理器上同时执行,即使是相同类型的也可以,而tasklet是两个不同类型的tasklet可以在不同的处理器上同时执行,但相同类型的tasklet是不能同时执行的。
Tasklet由tasklet_struct结构表示:
struct tasklet_struct
{
struct tasklet_struct *next;//链表中的下一个tasklet
unsigned long state;//tasklet的状态
atomic_t count;//引用计数
void (*func)(unsigned long);//tasklet处理函数
unsigned long data;//传递给tasklet处理函数的参数
};
结构中state只有两种状态:
TASKLET_STATE_SCHED tasklet已被调度,正准备投入运行
TASKLET_STATE_RUN 表明该tasklet正在运行
Count是tasklet的引用计数,如果它不为0,则tasklet被禁止,不允许运行,只有它为0时tasklet才能被激活,并置TASKLET_STATE_SCHED状态,可调度运行。
在大多数情况下,tasklet比软中断更适合作为下半部机制,因为它可以动态创建,而软中断的个数是固定的(32个),而且能更好的保护处理函数的共享数据(相同类型的tasklet不能同时执行)。
Tasklet的使用:
静态的声明一个tasklet:
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
这个宏会根据括号中的名称静态的创建一个tasklet,并初始化好各成员。
也可以使用内核接口tasklet_init(),它的作用和上面的宏是一样的,根据括号中的参数创建并初始化一个tasklet:
{
t->next = NULL;
t->state = 0;
atomic_set(&t->count, 0);
t->func = func;
t->data = data;
}
对于tasklet的处理函数func,由于是靠软中断实现,所以它在处理过程中是不能睡眠的。
Tasklet的调度:
当tasklet初始化好之后,就可以被调度了,通常使用tasklet_schedule()来调度我们的tasklet:
Tasklet_schedule(&my_tasklet);
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
在tasklet_schedule()中,首先判断要调度的tasklet的state位,如果state为0(在以上的初始化中它是为0的)则设置TASKLET_STATE_SCHED状态,然后调用__tasklet_schedule(t):
{
unsigned long flags;
local_irq_save(flags);
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_restore(flags);
}
在这里可以看到,当调度tasklet的时候首先会关闭本地中断,然后将tasklet挂在链表tasklet_vec上,再调度软中断TASKLET_SOFTIRQ,最后打开本地中断。
Tasklet两种软中断类型:TASKLET_SOFTIRQ和HI_SOFTIRQ,在调度的时候分别挂载在两个单处理器数据结构上:tasklet_vec和tasklet_hi_vec。在内核中有它们的定义:
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
在raise_softirq_irqoff(TASKLET_SOFTIRQ)中会将TASKLET_SOFTIRQ类型设置在系统的未响应软中断位图中:raise_softirq_irqoff()-> __raise_softirq_irqoff()->or_softirq_pending(1UL << nr)。这样在下一次软中断处理do_softirq()时会处理我们的tasklet了。我们再来看看系统是如何处理TASKLET_SOFTIRQ类型的软中断的。
在softirq_init()中有:
{
int cpu;
for_each_possible_cpu(cpu) {
int i;
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
for (i = 0; i < NR_SOFTIRQS; i++)
INIT_LIST_HEAD(&per_cpu(softirq_work_list[i], cpu));
}
register_hotcpu_notifier(&remote_softirq_cpu_notifier);
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
我们可以看到,系统在软中断初始化函数中会对每个处理器都初始化一个tasklet_vec和tasklet_hi_vec链表,并且注册好TASKLET_SOFTIRQ和HI_SOFTIRQ类型的软中断和它们 处理函数tasklet_action和tasklet_hi_action。我们再来看看tasklet_action类型的软中断是如何处理的。
{
struct tasklet_struct *list;
local_irq_disable();//关闭本地中断
list = __this_cpu_read(tasklet_vec.head);//获取当前处理器的tasklet_vec链表头,并将它保存在list指针当中。
__this_cpu_write(tasklet_vec.head, NULL);
__this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head);
local_irq_enable();//打开本地中断
while (list) {
struct tasklet_struct *t = list;
list = list->next;//在第一次循环中指向tasklet_vec中的第一个未处理的tasklet
if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))//清除tasklet的TASKLET_STATE_SCHED位,并返回原来值,用来判断该tasklet有被调度。
BUG();
t->func(t->data);//执行tasklet的处理函数
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();
}
}
整个tasklet的流程差不多就是这样了,tasklet是一个比较常用的下半部机制,当然它也有它的不足之处:不能睡眠,因此不能调用许多内核接口如申请内存等,这就限制了它的使用范围。如果在中断下半部需要睡眠那应该怎么办呢?linux同样提供了一种可以睡眠的下半部机制:工作队列。
欲知工作队列如何,请看下回分解!
阅读(2294) | 评论(0) | 转发(0) |