内核中的中断处理

首先来看当网络接收帧到达时,设备如何唤醒驱动。


1 轮询
也就是内核不断的监测相应的寄存器,从而得到是否有网络帧到来。

2中断
当有数据时,设备执行一个硬件中断,然后内核调用相应的处理函数。这种处理当网络在高负载的情况时,效率会很低(中断太频繁)。并且会引起receive-livelock.这是因为内核处理输入帧分为了两部分,一部分是驱动复制帧到输入队列,一部分是内核执行相关代码。第一部分的优先级比第二部分高。这时在高负载的情况下会出现输入队列由于队列已满而阻塞,而已复制的帧由于中断太频繁而无法占用cpu。。

3在一个中断执行多个帧
老的处理方法,也就是上面的处理方法,就是每个帧都会产生中断,而且每次进入中断都要关闭中断。现在内核新的NAPI接口所做的是,在第一次硬件中断之后,关闭中断,然后进入轮询处理,这样就大大的降低了高负载下中断太频繁的缺点.

3定时器驱动中断

这种方法是上一种方法的增强,不过需要硬件的支持。这种方法是驱动驱使设备在规定间隔内产生中断。然后handler监测是否有帧已经抵达,从而在一次处理多个帧(硬件的存储器内)。而这个定时器必须是硬件的。所以说必须硬件支持定时器。


相关的中断注册函数请看我前面的blog:

http://simohayha.iteye.com/blogs/361971

接下来看3c59x.c的中断处理函数 vortex_interrupt.这个函数在probe函数里面通过request_irq注册为中断handler。

这里要注意,网络设备有可能一个中断会包含多个原因(也就是下面的status变量)


static irqreturn_t
vortex_interrupt(int irq, void *dev_id)
{
.....................................
///这里也就是轮询的最大次数,默认是32.
int work_done = max_interrupt_work;
int handled = 0;

ioaddr = vp->ioaddr;
spin_lock(&vp->lock);
///读取寄存器从而得到当前的中断状态。
status = ioread16(ioaddr + EL3_STATUS);

if (vortex_debug > 6)
printk("vortex_interrupt(). status=0x%4x\n", status);
..................................................

do {
if (vortex_debug > 5)
printk(KERN_DEBUG "%s: In interrupt loop, status %4.4x.\n",
dev->name, status);
///RxComplete代表着新的帧已经接收,驱动可以去取了。因此调用vortex_rx得到数据,在这个函数里面会屏蔽掉中断,虽然屏蔽掉中断,我们的函数依然可以轮询硬件的中断寄存器,从而继续得到相应的中断状态。
if (status & RxComplete)
vortex_rx(dev);

if (status & TxAvailable) {
if (vortex_debug > 5)
printk(KERN_DEBUG " TX room bit was handled.\n");
/* There's room in the FIFO for a full-sized packet. */
iowrite16(AckIntr | TxAvailable, ioaddr + EL3_CMD);
netif_wake_queue (dev);
}

......................................................................

///进入最后一次轮询
if (--work_done < 0) {
printk(KERN_WARNING "%s: Too much work in interrupt, status "
"%4.4x.\n", dev->name, status);
/* Disable all pending interrupts. */
do {
vp->deferred |= status;
iowrite16(SetStatusEnb | (~vp->deferred & vp->status_enable),
ioaddr + EL3_CMD);
iowrite16(AckIntr | (vp->deferred & 0x7ff), ioaddr + EL3_CMD);
} while ((status = ioread16(ioaddr + EL3_CMD)) & IntLatch);
/* The timer will reenable interrupts. */
///打开中断然后跳出循环
mod_timer(&vp->timer, jiffies + 1*HZ);
break;
}
/* Acknowledge the IRQ. */
iowrite16(AckIntr | IntReq | IntLatch, ioaddr + EL3_CMD);
///这里是判断条件,当有未决中断,并且新的网络帧已经可以接受,我们就会一直循环.
} while ((status = ioread16(ioaddr + EL3_STATUS)) & (IntLatch | RxComplete));

if (vortex_debug > 4)
printk(KERN_DEBUG "%s: exiting interrupt, status %4.4x.\n",
dev->name, status);
handler_exit:
spin_unlock(&vp->lock);
return IRQ_RETVAL(handled);
}


接下来来看上下半部

我们要知道,内核处理中断的机制,在linux内核中,将中断的处理分为上半部和下半部,这里上半部的处理是在中断上下文中,而下半部的处理则不是。也就是说当上半部处理完后就会直接打开中断,下半部可以在开中断下执行。之所以要分为上下半部,是由于下面几个原因(摘抄自linux内核的设计与实现:

[quote]
中断处理程序以异步方式执行并且它有可能会打断其他重要代码的执行。因此,它们应该执行得越快越好。
如果当前有一个中断处理程序正在执行,在最好的情况下,与该中断同级的其他中断会被屏蔽,在最坏的情况下,所有其他中断都会被屏蔽。因此,仍应该让它们执行得越快越好。
由于中断处理程序往往需要对硬件进行操作,所以它们通常有很高的时限要求。
中断处理程序不在进程上下文中运行,所以它们不能阻塞。[/quote]

我的理解就是上半部用来得到数据,而下半部用来处理数据。一切都为了使中断更早结束。

在内核中实现下半部有三种机制,softirq,tasklet和work queue.由于网络设备主要使用前两种(最主要还是软中断),因此work queue就不做介绍了。

tasklet和softirq的主要区别就是tasklet在任何时候相同类型的都只有一个实例,就算在smp上。而softirq则是同时在一个cpu上才只有一个实例。因此使用softirq就要注意锁的实现。

tasklet可以动态的创建,而软中断则是静态创建的。

有时我们需要关闭掉软件中断或者硬件中断。下面就是一些中断(包括软件和硬件的)相关的函数或宏:


[img]/upload/attachment/100015/f13c308c-ce23-300e-ab66-4ccc6775cb58.jpg[/img]


由于内核现在是可抢占的,因此开发者必须显示的在很多地方关闭抢占(比如硬件软件中断中等等)。


网络部分代码不直接调用抢占提供的相关api。它是通过一些其他的函数,比如rcu_read_lock,spin_lock等等这些函数间接的调用。

对于每个进程都有一个preempt_count位图变量,他表示了当前进程是否允许被抢占。这个变量能通过preempt_count()来读取,能通过inc_preempt_count和dec_preempt_count来增加和减少引用计数。他被分为三部分.硬件中断部分,软件中断部分和非抢占部分:


[img]/upload/attachment/100019/2be003be-59aa-3154-95a1-eaf610632887.jpg[/img]

接下来我们来看下半部的处理。首先来看软中断。

软终端模式有下面几种类型,其中的优先级是从大到小,也就是HI_SOFTIRQ的优先级最高:

enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
#ifdef CONFIG_HIGH_RES_TIMERS
HRTIMER_SOFTIRQ,
#endif
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
};


网络部分使用的类型主要是NET_TX_SOFTIRQ和NET_RX_SOFTIRQ.软中断主要运行在开中断(硬件中断)的情况下,并且内核不允许在一个cpu上已经挂起的软中断,然后再次请求此软中断(可以同时在多个cpu上,可以运行相同的软中断)。每个软中断都包含一个softnet_data数据结构,它存储了当前软中断的状态。

软中断通过open_irq来注册:

static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;


void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}


这里softirq_vec是一个全局的向量,存储软中断的信息。软中断能通过__raise_softirq_irqoff,raise_softirq_irqoff和raise_softirq来对软中断进行排队的。

为了防止软中断独占cpu资源,内核有一个每个cpu都独有的一个软中断线程,ksoftirqd_cpu0,等等。。


在下列的时刻,软中断会被执行和检测:

1 从一个硬件中断代码返回
2 在ksoftirqd(后面会介绍)中执行
3 显式检测和执行待处理的软中断代码(网络部分)

在网络设备的代码中,我们一般通过raise_softirq(上面的那张图中),来将一个软中断挂起,从而在下次处理时执行此软中断。

而不管怎么样,软中断都要通过do_softirq来处理。下面我们来看它的代码:

asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;
///如果当前cpu正在处理一个软中断或者硬件中断,直接返回
if (in_interrupt())
return;
local_irq_save(flags);

///得到当前的cpu是否有未决软中断(也就是需要执行的0.
pending = local_softirq_pending();
///然后调用处理函数
if (pending)
__do_softirq();

local_irq_restore(flags);
}



asmlinkage void __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
///由于在这个函数执行过程中,可能会出现新的挂起软中断,为了防止软中断独占cpu,因此这里只执行固定的次数的轮询。
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));
trace_softirq_enter();

cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);

///打开软中断
local_irq_enable();
///当前的全局软中断变量
h = softirq_vec;

do {
if (pending & 1) {
///调用相应处理函数
h->action(h);

rcu_bh_qsctr_inc(cpu);
}
h++;
pending >>= 1;
} while (pending);

local_irq_disable();

///得到当前的未决中断的个数
pending = local_softirq_pending();
///如果轮询次数已到最大值,则跳出循环
if (pending && --max_restart)
goto restart;

///如果还有未处理的未决软中断则调用wakeup_softirq唤醒内核软中断处理线程。
if (pending)
wakeup_softirqd();

trace_softirq_exit();

account_system_vtime(current);
_local_bh_enable();
}



来看ksoftirqd的源码;

static int ksoftirqd(void * __bind_cpu)
{
///设置内核线程的运行状态
set_current_state(TASK_INTERRUPTIBLE);

///开始进入循环,执行软中断处理
while (!kthread_should_stop()) {
preempt_disable();
///没有未决软中断,此时让出cpu。然后关闭抢占。
if (!local_softirq_pending()) {
preempt_enable_no_resched();
schedule();
preempt_disable();
}

///设置线程状态。
__set_current_state(TASK_RUNNING);
///进入处理软中断
while (local_softirq_pending()) {
/* Preempt disable stops cpu going offline.
If already offline, we'll be on wrong CPU:
don't process */
///线程执行太长时间,并且被请求释放cpu。比如定时器中断等等。此时跳转到wait__to_die,下面我会介绍.
if (cpu_is_offline((long)__bind_cpu))
goto wait_to_die;
///调用do_softirq处理软中断
do_softirq();
preempt_enable_no_resched();
cond_resched();
preempt_disable();
}
preempt_enable();
set_current_state(TASK_INTERRUPTIBLE);
}
__set_current_state(TASK_RUNNING);
return 0;

wait_to_die:
///打开抢占
preempt_enable();
/* Wait for kthread_stop */
set_current_state(TASK_INTERRUPTIBLE);
///如果当前的线程是否应该被停止,如果不是,则释放cpu。设置相关状态。
while (!kthread_should_stop()) {
schedule();
set_current_state(TASK_INTERRUPTIBLE);
}
///设置状态为running,等待稍后resume.
__set_current_state(TASK_RUNNING);
return 0;
}



一个tasklet也就是一个中断或者其他任务将要稍后执行的函数。它是基于软中断来实现的。而tasklet的软中断类型是HI_SOFTIRQ或者TASKLET_SOFTIRQ.

来看tasklet的结构:
struct tasklet_struct
{
///相同的cpu上的挂起的中断的链表
struct tasklet_struct *next;
///当前tasklet的状态(位图表示)
unsigned long state;
///引用计数
atomic_t count;
///所需执行的函数
void (*func)(unsigned long);
///可能传递给fucn的数据
unsigned long data;
};



每个cpu都会有两个tasklet链表,一个是HI_SOFTIRQ一个是TASKLET_SOFTIRQ类型的:

static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);



接下来我们来看tasklet的这两种类型的软中断的初始化:

void __init softirq_init(void)
{
int cpu;
///每个cpu初始化两个tasklet链表
for_each_possible_cpu(cpu) {
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;
}
///注册软中断
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}


这里要提到的是,网络设备的软中断的注册是在net_dev_init中注册的。

HI_SOFTIRQ这个最高的优先级只在声卡设备驱动中使用。网卡中更多使用TASKLET_SOFTIRQ(这个优先级比网络的哪两个低).

最后我们来介绍一下cpu_chain,他也就是把cpu的一些信息通知给这条链上的子系统。。比如当cpu初始化完成后,我们才能启动软中断线程。这里对应的事件就是CPU_ONLINE,更多的事件需要去看notifier.h
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值