2009年(49)
分类: LINUX
2009-06-08 23:18:00
首先我们要知道为什么中断需要下半部 。我们可以想象一下,如果没有下半部的概念,一个网卡中断过来了以后会是什么样的情况。首先,我们会从网卡硬件buffer中把网卡收到的packet拷贝到系统内存中,然后对这个packet进行TCP/IP协议栈的处理。我们知道TCP/IP协议栈是一个比较复杂的软件模块,里面对packet的处理会经过非常多的步骤,首先是链路层,然后是IP层(这里又包括分片,奇偶校验之类的),然后是TCP层(TCP层的实现相当复杂,会花费比较长的时间对packet进行一些状态或者内容的分析处理),最后通过socket把packet传入用户空间。在传入用户空间之间的这些动作,都必须在中断处理中完成,因为这些操作都是在kernel中的,并且这些操作会花费比较长的时间。在这段时间里,cpu由于进入了中断门,会自动关中断,也就是说cpu不会去响应在这段时间里网卡另外发过来的中断,这样的话很有可能网卡硬件buffer会由于网卡自身的缓存不足而导致丢包 。所以linux为了解决这样的问题,把copy packet这样比较紧急的动作放在了上半部去处理(上半部默认情况下是在关中断中完成的),把协议栈这些不是特别紧急的任务放到了下半部去处理(下半部是在开中断中进行的,有就是说,处理下半部的过程中,允许cpu被其他中断打断)。
二、软件构架和实现
1. 一些基础数据结构
文件softirq.c
/*PER-CPU变量,每个cpu对应一个,描述当前cpu中关于softirq的一些状态,比如是否有softirq挂起需要执行等等*/
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
typedef struct {
unsigned int __softirq_pending; /*32位,对应Linux中32种softirq是否被上半部触发了(为1表示被触发,为0表示未被触发)*/
unsigned long idle_timestamp;
unsigned int __nmi_count; /* arch dependent */
unsigned int apic_timer_irqs; /* arch dependent */
} ____cacheline_aligned irq_cpustat_t;
/*表示softirq最多有32种类型,实际上Linux只用了6种,见文件interrupt.h*/
static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;
/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
frequency threaded job scheduling. For almost all the purposes
tasklets are more than enough. F.e. all serial device BHs et
al. should be converted to tasklets, not to softirqs.
*/
enum
{
HI_SOFTIRQ=0, /*用于高优先级的tasklet*/
TIMER_SOFTIRQ, /*用于定时器的下半部*/
NET_TX_SOFTIRQ,/*用于网络层发包*/
NET_RX_SOFTIRQ, /*用于网络层收包*/
SCSI_SOFTIRQ, /*用于SCSI设备*/
TASKLET_SOFTIRQ /*用于低优先级的tasklet*/
};
struct softirq_action
{
void (*action)(struct softirq_action *); /*softirq的回调函数*/
void *data; /*传入action的参数*/
};
Struct softirq_action是每个softirq的配置结构,一般在系统启动的时候,6个不同的softirq,会通过函数open_softirq()来注册自己的softirq_action,实现很简单:
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
}
关键是传入的函数指针,具体指明了该softirq要实现的功能或要做的动作。
这里分开看下这六个注册点:
Net/core/dev.c中的net_dev_init()里面注册了网络层需要用到的收包和发包的两个softirq:
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
对应的函数指针为net_tx_action和net_rx_action,具体功能就不在本文的范围之内的。
Driver/scsi/scsi.c中init_scsi()注册了SCSI_SOFTIRQ
open_softirq(SCSI_SOFTIRQ, scsi_softirq, NULL);
start_kernel() àsiftirq_init() 中注册了两种tasklet,一种是高优先级的tasklet,一直是低优先级的tasklet:
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
start_kernel() àinit_timers() 中注册了TIMER_SOFTIRQ
open_softirq(TIMER_SOFTIRQ, run_timer_softirq, NULL);
2. softirq运行时机:
系统在运行过程中,会在合适的地方使用函数local_softirq_pending()检查系统是否有softirq需要处理;需要时会调用函数do_softirq()进行处理。这些检查点主要包括以下几个地方:
(1) 中断过程退出函数irq_exit();
(2) 内核线程ksoftirqd;
(3) 内核网络子系统中显示调用;
(4) 函数local_bh_enable().
先前我们分析irq_cpustat_t结构的时候,看到__softirq_pending字段。这是一个32位无符号的变量,对应Linux中32种softirq是否被上半部触发了(为1表示被触发,为0表示未被触发)。那么在softirq运行之前肯定就有地方设置了这个变量的各个位,才会触发到softirq运行。这个触发动作一般是在上半部中进行的,即上半部通过系统,还有下半部需要运行。触发函数如下:
#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)
#define or_softirq_pending(x) (local_softirq_pending() |= (x))
#define local_softirq_pending() \
__IRQ_STAT(smp_processor_id(), __softirq_pending)
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)
由此可以看出, __raise_softirq_irqoff(nr)实际上是把当前cpu的per-cpu变量irq_stat的__softirq_pending 的从右往左数的第nr位置1,这样系统就知道某个softirq需要在某个时刻运行了。
当然,__raise_sofritq_irqoff()被很多地方封装过,不同子系统用自己封装的函数,比如网络子系统就用netif_rx_reschedule和net_rx_action来激活softirq。
3. softirq的执行分析:
我们先提到了softirq在四个地方有可能运行,最常见的就是中断过程退出函数irq_exit(),我们下面分析之,其他的触发点请大家对照代码自行分析:
void irq_exit(void)
{
account_system_vtime(current);
sub_preempt_count(IRQ_EXIT_OFFSET);
/*如果在上半部中设置了per-cpu变量irq_stat的__softirq_pending字段,则运行下半部的处理函数, 从这里也可以看出,上半部和下半部一定是运行在同一个cpu上,因为上半部中是设置了per-cpu变量irq_stat本地cpu副本中的__softirq_pending中的位,而下半部也只是判断本地cpu的__sofrirq_pending中的位。这样有效的利用了cpu cache的特性*/
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
preempt_enable_no_resched();
}
Invoke_sofirq()àdo_softirq()
asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;
/*本地cpu中,softirq不能在中断环境中运行,这个中断环境包括了上半部和下半部,所以这里保证了同一个cpu上,下半部是不会被重入的,但不能保证其他cpu上的同时运行同一个softirq处理。所以编程人员必须让自己编写的softirq处理函数可重入,以防SMP系统中同时运行这些softirq导致数据出现不一致性*/
if (in_interrupt())
return;
local_irq_save(flags); /*关中断*/
pending = local_softirq_pending(); /*取得per-cpu变量irq_stat本地cpu副本的__softirq_pending的值*/
if (pending) /*如果有本地cpu有softirq挂起需要处理,则通过__do_softirq()运行之,否则恢复中断并退出*/
__do_softirq();
local_irq_restore(flags); /*恢复开中断*/
}
关键是__do_softirq()
asmlinkage void __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
pending = local_softirq_pending();
local_bh_disable();
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
/*把per-cpu变量 irq_stat的本地cpu副本的__softirq_pengding字段清0,
表示代码有信心在这一次处理中把本地cpu上挂起的所有softirq都处理掉(有信心只是开个玩笑;)*/
set_softirq_pending(0);
local_irq_enable(); /*保证下半部要在中断打开的情况下进行,否则下半部就失去意义了*/
h = softirq_vec; /*softirq_vec是一个全局数组(有32个元素),存放了32种softirq的处理函数*/
/*遍历unsigned int pengding的每一位,如果有被置为1,则运行对应的下半部处理函数action*/
do {
if (pending & 1) {
/*action是一个处理队列,对于非tasklet的softirq来说只有一个元素,
但对于tasklet来说,就有N个函数需要处理*/
h->action(h);
rcu_bh_qsctr_inc(cpu);
}
h++;
pending >>= 1;
} while (pending);
local_irq_disable(); /*关闭中断*/
/*因为下半部是在开中断的环境中运行的,
所以有可能在运行了softirq A以后,
然后在运行其他的softirq B,
这时又产生A的硬件中断(A和B在同一个cpu中产生),
而在A的上半部中又设置了per-cpu变量irq_stat的本地
cpu副本的irq_stat的__softirq_pending的对应的bit,
所以代码运行到这里又发现__softirq_pending不0,
所以要重做处理。这样的情况最多执行max_restart次,因为
如果不限次数的运行下去,中断就一直不返回,那么进程
就得不到调度,系统性能会大大受影响。所以运行max_restart次以后,
如果这样的情况还在一直发生,那么就唤醒per-cpu thread来专门执行
这些下半部,注意,在per-cpu thread中处理的中断下半部,是可以睡眠
的,但是编程人员无法掌握他编写的softirq处理程序是在irq_exit()中处理还是
在per-cpu thread中处理,所以一般都不会有睡眠的可能(编程人员需要保证
这一点)*/
pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;
if (pending)
wakeup_softirqd();
__local_bh_enable(); /*enable下半部运行*/
}
这里就不画流程图了,关键是要仔细的分析几个中断关闭和中断使能的时机,以及softirq是否可以重入的问题。
三、总结
本文分析了softirq运行的时间点,以及softirq是怎样被cpu调度的。后面还要继续分析tasklet的实现,tasklet实际上就是凌驾在softirq机制上的,它占用了Linux现有6种softirq的2种(优先级最高的和优先级最低的)。
要特别注意的是:softirq处理函数也不能睡眠,因为它也是运行在中断上下文环境中的(不考虑ksoftirqd线程)。