首先来看当网络接收帧到达时,设备如何唤醒驱动。
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)
-
{
-
.....................................
-
-
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);
-
-
if (status & RxComplete)
-
vortex_rx(dev);
-
-
if (status & TxAvailable) {
-
if (vortex_debug > 5)
-
printk(KERN_DEBUG " TX room bit was handled.\n");
-
-
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);
-
-
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);
-
-
-
mod_timer(&vp->timer, jiffies + 1*HZ);
-
break;
-
}
-
-
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内核的设计与实现:
引用
中断处理程序以异步方式执行并且它有可能会打断其他重要代码的执行。因此,它们应该执行得越快越好。
如果当前有一个中断处理程序正在执行,在最好的情况下,与该中断同级的其他中断会被屏蔽,在最坏的情况下,所有其他中断都会被屏蔽。因此,仍应该让它们执行得越快越好。
由于中断处理程序往往需要对硬件进行操作,所以它们通常有很高的时限要求。
中断处理程序不在进程上下文中运行,所以它们不能阻塞。
我的理解就是上半部用来得到数据,而下半部用来处理数据。一切都为了使中断更早结束。
在内核中实现下半部有三种机制,softirq,tasklet和work queue.由于网络设备主要使用前两种(最主要还是软中断),因此work queue就不做介绍了。
tasklet和softirq的主要区别就是tasklet在任何时候相同类型的都只有一个实例,就算在smp上。而softirq则是同时在一个cpu上才只有一个实例。因此使用softirq就要注意锁的实现。
tasklet可以动态的创建,而软中断则是静态创建的。
有时我们需要关闭掉软件中断或者硬件中断。下面就是一些中断(包括软件和硬件的)相关的函数或宏:
由于内核现在是可抢占的,因此开发者必须显示的在很多地方关闭抢占(比如硬件软件中断中等等)。
网络部分代码不直接调用抢占提供的相关api。它是通过一些其他的函数,比如rcu_read_lock,spin_lock等等这些函数间接的调用。
对于每个进程都有一个preempt_count位图变量,他表示了当前进程是否允许被抢占。这个变量能通过preempt_count()来读取,能通过inc_preempt_count和dec_preempt_count来增加和减少引用计数。他被分为三部分.硬件中断部分,软件中断部分和非抢占部分:
接下来我们来看下半部的处理。首先来看软中断。
软终端模式有下面几种类型,其中的优先级是从大到小,也就是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,
-
};
网络部分使用的类型主要是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;
-
-
if (in_interrupt())
-
return;
-
local_irq_save(flags);
-
-
-
pending = local_softirq_pending();
-
-
if (pending)
-
__do_softirq();
-
-
local_irq_restore(flags);
-
}
-
asmlinkage void __do_softirq(void)
-
{
-
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));
-
trace_softirq_enter();
-
-
cpu = smp_processor_id();
-
restart:
-
-
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;
-
-
-
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();
-
-
if (!local_softirq_pending()) {
-
preempt_enable_no_resched();
-
schedule();
-
preempt_disable();
-
}
-
-
-
__set_current_state(TASK_RUNNING);
-
-
while (local_softirq_pending()) {
-
-
-
-
-
if (cpu_is_offline((long)__bind_cpu))
-
goto wait_to_die;
-
-
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();
-
-
set_current_state(TASK_INTERRUPTIBLE);
-
-
while (!kthread_should_stop()) {
-
schedule();
-
set_current_state(TASK_INTERRUPTIBLE);
-
}
-
-
__set_current_state(TASK_RUNNING);
-
return 0;
-
}
一个tasklet也就是一个中断或者其他任务将要稍后执行的函数。它是基于软中断来实现的。而tasklet的软中断类型是HI_SOFTIRQ或者TASKLET_SOFTIRQ.
来看tasklet的结构:
-
struct tasklet_struct
-
{
-
-
struct tasklet_struct *next;
-
-
unsigned long state;
-
-
atomic_t count;
-
-
void (*func)(unsigned long);
-
-
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;
-
-
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