1 软中断概述
软中断是实现中断下半部的一种手段,与2.5以前版本的下半段机制不同。软中断可以同时运行在不同的CPU上。
1.1 软中断的表示
内核中用结构体softirq_action表示一个软中断。软中断是一组静态定义的接口,有32个。但是内核(2.6.34)中只实现了10个。可用的软中断的个数用NR_SOFTIRQ表示,NR_SOFTIRQ=10,软中断的类型用一个枚举体表示。这里需要注意的是,32个软中断体现在位掩码是unsigned int 类型。
static struct softirq_action softirq_vec[NR_SOFTIRQS] ;
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
struct softirq_action
{
void (*action)(struct softirq_action *);
};
2 软中断相关的数据结构
2.1 thread_info的preempt_count字段
preempt_count 是一个32位的int型,共分为5个字段
宏in_interrupt检测软中断计数器 硬中断计数器 和 NMI掩码,只要这三个字段任意一个字段不为0,就表示进程处于中断上下文。
#define in_irq() (hardirq_count())
#define in_softirq() (softirq_count())
#define in_interrupt() (irq_count())
2.2 pending位掩码
每个CPU上都有一个irq_stat结构,irq_stat中的__softirq_pending是一个32位的掩码,为1表示该软中断已经激活,正等待处理。为0表示软中断被禁止。在do_irq中被使用。
内核使用local_softirq_pending得到当前CPU上的位掩码
#define local_softirq_pending() percpu_read(irq_stat.__softirq_pending)
#define set_softirq_pending(x) percpu_write(irq_stat.__softirq_pending, (x))
#define or_softirq_pending(x) percpu_or(irq_stat.__softirq_pending, (x))
irq_cpustat_t irq_stat[NR_CPUS]
typedef struct {
...
unsigned int __softirq_pending;
...
}irq_cpustat_t;
2.3 软中断栈
进程的内核栈的大小根据编译时选项不同,可以是4K或者8K。如果是8K堆栈,中断,异常和软中断(softirq) 共享这个堆栈。如果选择4K堆栈,则内核堆栈 硬中断堆栈 软中断堆栈各自使用一个4K空间。关于软中断堆栈,后面在软中断处理时再详细说明。
#ifdef CONFIG_4KSTACKS
static DEFINE_PER_CPU_PAGE_ALIGNED(union irq_ctx, softirq_stack);
union irq_ctx {
struct thread_info tinfo;
u32 stack[THREAD_SIZE/sizeof(u32)];
} __attribute__((aligned(PAGE_SIZE)));
3 软中断的初始化
内核使用open_softirq初始化一个软中断,nr是代表软中断类型的常量,action指向一个软中断处理函数
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
4 软中断的触发(raise softirq)
触发就是将位掩码pending的相应位 置1的过程。内核使用raise_softirq完成触发软中断,nr是要触发的软中断类型。值的注意的是,中断的触发 发生关闭硬中断的情况下。
触发软中断的过程中,如果该进程未处于中断上下文,说明当前进程处于进程上下文中,那么我们直接调用wakeup_softirqd调度ksoftirqd即可。
反之,如果当前处于中断上下文中或软中断被禁止使用,那么就不必调度内核线程,在中断处理后期irq_exit中,会调用invoke_softirq()处理软中断。
实际的工作是交给or_softirq_pending(1UL << (nr)); 完成的,该函数通过位操作将指定为和pending相加。
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
/*
* If we're in an interrupt or softirq, we're done
* (this also catches softirq-disabled code). We will
* actually run the softirq once we return from
* the irq or softirq.
*
* Otherwise we wake up ksoftirqd to make sure we
* schedule the softirq soon.
* 如果不在中断上下文中
*/
if (!in_interrupt())
wakeup_softirqd();
}
#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); }
5. 软中断的处理
5.1 处理软中断的时机
1 在do_irq的末期(irq_exit)
如果当前进程没有处于中断上下文中并且本地CPU上还有没有处理的软中断,那么就调用invoke_softirq()处理软中断。
#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
# define invoke_softirq() __do_softirq()
#else
# define invoke_softirq() do_softirq()
#endif
void irq_exit(void)
{
account_system_vtime(current);
trace_hardirq_exit();
sub_preempt_count(IRQ_EXIT_OFFSET);
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
rcu_irq_exit();
#ifdef CONFIG_NO_HZ
/* Make sure that timer wheel updates are propagated */
if (idle_cpu(smp_processor_id()) && !in_interrupt() && !need_resched())
tick_nohz_stop_sched_tick(0);
#endif
preempt_enable_no_resched();
}
#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
# define invoke_softirq() __do_softirq()
#else
# define invoke_softirq() do_softirq()
#endif
程序5-1 irq_exit
2 当软中断被重复触发超过10次时,内核会调用wakeup_softirqd()唤醒内核线程ksoftirqd去处理软中断。
5.2 软中断的处理
1 do_softirq
根据内核堆栈大小,有两种do_softirq,一种是通用do_irq,另一种是架构相关的do_irq(arch/x86/kernel/irq_32.c)。
通用的do_irq的工作流程
1 判断当前是否处于硬中断上下文中或者软中断被禁用,如果是那么直接返回
2 保存Eflags 然后关闭本地硬件中断
3 获取本地CPU的位掩码pending 如果有待处理的软中断就调用__do_irq
4 从__do_irq返回恢复Eflags
asmlinkage void do_softirq(void)
{
//用来保存位掩码的局部变量
__u32 pending;
//保存Eflags寄存器的局部变量
unsigned long flags;
//如果do_softirq在中断上下文中被调用 或 软中断被禁止使用 那么不处理软中断
//直接返回
if (in_interrupt())
return;
//将Eflags保存到flags中 然后关硬件中断
local_irq_save(flags);
//获取本地CPU上的位掩码
pending = local_softirq_pending();
if (pending)
__do_softirq();
//将flags
local_irq_restore(flags);
}
x86_32架构下使用4K软中断堆栈的do_softirq处理流程
1 类似于通用的do_softirq,如果在中断上下文中或者软中断被禁止使用就立即返回。然后关外部中断
2 如果本地CPU上存在待处理的软中断就开始对软中断堆栈的处理,关键是令isp指向软中断堆栈的栈底。然后在软中断栈上调用call_on_stack。call_on_stack是一段内联汇编,其主要目的是完成从内核栈到软中断栈的切换。先将esp保存到ebx中,使用call指令跳转到__do_softirq子例程,子例程返回时再恢复esp。
asmlinkage void do_softirq(void)
{
unsigned long flags;
struct thread_info *curctx;
union irq_ctx *irqctx;
u32 *isp;
if (in_interrupt())
return;
local_irq_save(flags);
if (local_softirq_pending()) {
//curctx指向当前进程的thread_info结构
curctx = current_thread_info();
//irqctx包含一个软中断堆和thread_info结构
irqctx = __get_cpu_var(softirq_ctx);
//触发硬中断和软中断是同一个进程所以将threadinfo的task指针统一
irqctx->tinfo.task = curctx->task;
//从内核堆栈切换到软中断堆栈 需要保存内核堆栈的栈指针寄存器内容
irqctx->tinfo.previous_esp = current_stack_pointer;
/* build the stack frame on the softirq stack */
//isp指向软中断栈底
isp = (u32 *) ((char *)irqctx + sizeof(*irqctx));
//在软中断堆栈上调用__do_softirq
call_on_stack(__do_softirq, isp);
}
local_irq_restore(flags);
}
static void call_on_stack(void *func, void *stack)
{
//call *%%edi 间接绝对近调用 偏移地址存储在edi寄存器中
//指令执行时 先将eip入栈 然后将edi-->eip
//先交换ebx and esp
//然后调用__do_softirq
//然后将ebx-->esp 恢复xchgl之前的esp
//输出约束将ebx --> stack
asm volatile("xchgl %%ebx,%%esp \n"
"call *%%edi \n"
"movl %%ebx,%%esp \n"
: "=b" (stack)
: "0" (stack),
"D"(func)
: "memory", "cc", "edx", "ecx", "eax");
}
2 __do_softirq
软中断的处理实际上是由 __do_softirq完成,整体的思路是遍历pending,如果某一位不为空表示本地CPU上有待处理的软中断,然出调用软中断的处理函数。
开始处理软中断前,内核要调用__local_bh_disable(通过将preempt_count的软中断计数器加1)关闭下半部。如前所说,处理软中断的时机不止一个,内核要保证在本地CPU上软中断的处理是串行的。
另外在处理软中断的循环结束时,内核还要检测是否有重复触发的软中断。先调用local_softirq_pending()获取位掩码pending,然后根据pending继续处理软中断,不过这种重复处理不能超过10次(MAX_SOFTIRQ_RESTART),一旦超过10次,内核就会唤醒ksoftirqd
#define MAX_SOFTIRQ_RESTART 10
asmlinkage void __do_softirq(void)
{
// softirq_action表示一个软中断
struct softirq_action *h;
// 局部变量pending 保存待处理软中断位图
__u32 pending;
// 软中断的重启次数
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
//获取本地CPU上所有待处理的软中断
pending = local_softirq_pending();
account_system_vtime(current);
//关闭下半部中断
__local_bh_disable((unsigned long)__builtin_return_address(0));
lockdep_softirq_enter();
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
//pending已经保存了所有带出软中断的状态 所以将pending bitmask clear
set_softirq_pending(0);
// 开中断
local_irq_enable();
//h指向第一类软中断
h = softirq_vec;
do {
//先处理第一类软中断
if (pending & 1) {
int prev_count = preempt_count();
kstat_incr_softirqs_this_cpu(h - softirq_vec);
trace_softirq_entry(h, softirq_vec);
//调用软中断处理程序
h->action(h);
trace_softirq_exit(h, softirq_vec);
if (unlikely(prev_count != preempt_count())) {
printk(KERN_ERR "huh, entered softirq %td %s %p"
"with preempt_count %08x,"
" exited with %08x?\n", h - softirq_vec,
softirq_to_name[h - softirq_vec],
h->action, prev_count, preempt_count());
preempt_count() = prev_count;
}
rcu_bh_qs(cpu);
}
h++;
pending >>= 1;
} while (pending);
//关闭本地CPU上的硬中断
local_irq_disable();
//获取位掩码
pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;
if (pending)
wakeup_softirqd();
lockdep_softirq_exit();
account_system_vtime(current);
//将软中断计数器加1,激活软中断
_local_bh_enable();
}
6 ksoftirqd内核线程
6.1 ksoftirqd
在内核处理类似NET_RX_SOFTIRQ的软中断时,如果有大量等待处理的数据包。就会不断的调用
__raise_softirq_irqoff(NET_RX_SOFTIRQ)重复触发软中断NET_RX_SOFTIRQ。这样做会导致用户进程的” 饥饿问题 “ (长时间无法获得CPU)。
针对这种问题内核使用内核线程ksoftirqd去处理自行触发次数超过10次的软中断。
6.2 ksoftirqd的实现
static int ksoftirqd(void * __bind_cpu)
{
//ksoftirqd的优先级最低(nice = 19)
set_user_nice(current, 19);
//将ksoftirqd设置为不可冻结
current->flags |= PF_NOFREEZE;
//设置ksoftirqd为可中断状态
set_current_state(TASK_INTERRUPTIBLE);
while (!kthread_should_stop()) {
//如果没有待处理的软中断则 调度别的进程
if (!local_softirq_pending())
schedule();
__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 */
//关闭内核抢占
preempt_disable();
//处理软中断
do_softirq();
//开启内核抢占
preempt_enable();
//cond_resched()的目的是提高系统实时性, 主动放弃cpu供优先级更高的任务使用
cond_resched();
}
set_current_state(TASK_INTERRUPTIBLE);
}
__set_current_state(TASK_RUNNING);
return 0;
}
程序6-1 ksoftirqd主功能函数
out:
local_irq_enable();
return;
softnet_break:
__get_cpu_var(netdev_rx_stat).time_squeeze++;
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
goto out;
程序6-2 net_rx_action
void wakeup_softirqd(void)
{
/* Interrupts are disabled: no need to stop preemption */
struct task_struct *tsk = __get_cpu_var(ksoftirqd);
if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}
程序6-3 wakeup_softirqd
还未解决的问题
set_current_state和 __set_current_state的区别
PF_NOFREEZE
cond_resched()
为什么在do_softirq开始处理软中断时要关闭硬件中断