Linux中断管理 (2)软中断和tasklet

目录:

Linux中断管理

Linux中断管理 (1)Linux中断管理机制

Linux中断管理 (2)软中断和tasklet

Linux中断管理 (3)workqueue工作队列

关键词:TASKLET_SOFTIRQ、HI_SOFTIRQ、softirq_action、ksoftirqd、tasklet、BH。

软中断以及基于软中断的tasklet、工作队列,包括中断线程化都属于下半部机制,为什么需要下半部机制呢?

1.硬件中断处理程序以异步方式执行,会打断其它重要代码执行,因此为了避免打断事件太久,硬件中断程序需要尽快执行完成。

2.硬件中断处理程序通常在关中断情况下执行,即关闭了本地CPU所有中断响应。关中断之后,本地CPU不能再响应中断,因此硬件中断处理程序必须尽快执行完成。

1. SoftIRQ软中断

1.1 软中断数据结构

软中断是预留给系统中对时间要求最为严格最重要的下半部使用的,系统静态定义了若干软终端类型,并且Linux内核开发者不希望用户扩充新的软终端类型。

这里的优先级对应在__do_softirq()中执行action的顺序,低位优先得到执行。

enum
{
HI_SOFTIRQ=,------------------------最高优先级的软中断类型
TIMER_SOFTIRQ,-----------------------Timer定时器软中断
NET_TX_SOFTIRQ,----------------------发送网络数据包软中断
NET_RX_SOFTIRQ,----------------------接收网络数据包软中断
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,----------------块设备软中断
TASKLET_SOFTIRQ,---------------------专门为tasklet机制准备的软中断
SCHED_SOFTIRQ,-----------------------进程调度以及负载均衡软中断
HRTIMER_SOFTIRQ,---------------------高精度定时器软中断
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */----RCU服务软中断 NR_SOFTIRQS
};

struct softirq_action数据结构用于描述软中断,并且定义了softirq_vec[]来表示每一个软中断对应的描述符,软中断所以号就是该数组的索引。

NR_SOFTIRQS是系统支持的软中断最大数量。

__cacheline_aligned_in_smp用于将softirq_vec数据结构和L1缓存行对齐。

struct softirq_action
{
void (*action)(struct softirq_action *);----------------------只有一个action函数指针,当触发了该软中断,就会调用action回调函数来处理这个软中断。
}; static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

irq_cpustat_t用来描述软件中断状态信息,可以理解为“软中断状态寄存器”,其实是一个unsigned int类型变量__softirq_pending。

irq_cpustat_t irq_stat[NR_CPUS]相当于每个CPU有一个软中断状态信息变量。

local_softirq_pending()读取当前CPU软中断状态,如果不为0说明有软中断未处理。

or_softirq_pending()用于设置当前CPU的特定软中断处于pending状态,在__raise_softirq_irqoff()中设置。

set_softirq_pending()可以个整个CPU软中断状态复位,常在__do_softirq()函数中执行。

typedef struct {
unsigned int __softirq_pending;
} ____cacheline_aligned irq_cpustat_t; extern irq_cpustat_t irq_stat[]; /* defined in asm/hardirq.h */
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member) /* arch independent irq_stat fields */
#define local_softirq_pending() \
__IRQ_STAT(smp_processor_id(), __softirq_pending)----------------------获取当前CPU的软中断状态

#define set_softirq_pending(x) (local_softirq_pending() = (x))
#define or_softirq_pending(x) (local_softirq_pending() |= (x))

1.2 软中断注册和触发

通过调用open_softirq()函数可以注册一个软中断,其中参数nr是软中断的序号。

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

raise_softirq()函数主动触发一个软中断API接口函数,首先设置__softirq_pending置软中断对应位,然后如果in_interrupt()为0,则唤醒ksoftirqd内核线程。

/*
* This function must run with irqs disabled!
*/
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
if (!in_interrupt())
wakeup_softirqd();-------------------------------------如果不处于中断上下文中,则尽快执行软中断处理。
} void raise_softirq(unsigned int nr)
{
unsigned long flags; local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
} void __raise_softirq_irqoff(unsigned int nr)
{
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);-----------------------------置位nr位的软中断,表示此软中断处于pending状态。
}

1.3 软中断执行

软中断执行机会:

一个是在irq_exit的时候:irq_exit()->invoke_softirq()->wakeup_softirq()->唤醒ksoftirqd内核线程

一个是在local_bh_enable的时候:local_bh_enable()->__local_bh_enable()->do_softirq()->__do_softirq(CONFIG_PREEMPT_RT_FULL)-->wkeup_softirq(在长时间执行softirq后,启动ksoftirq)

还有一种是ksoftirqd内核线程执行函数run_ksoftirqd()中调用__do_softirq(),一般有wake_up_process()唤醒。

软中断的执行一个重要场景是在中断退出时irq_exit(),irq_exit()首先检查是否处于进程上下文中且有pending状态的软中断,然后将工作交给invoke_softirq()

int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
bool lookup, struct pt_regs *regs)
{
...
irq_enter();
...
irq_exit();
set_irq_regs(old_regs);
return ret;
} void irq_exit(void)
{
...
if (!in_interrupt() && local_softirq_pending())-------------------------in_interrupt()为0表示当前不处于中断上下文,处于进程上下文中。local_softirq_pending()非0,表示有pending软中断。
invoke_softirq();
...
} static inline void invoke_softirq(void)
{
if (!force_irqthreads) {
/*
* We can safely execute softirq on the current stack if
* it is the irq stack, because it should be near empty
* at this stage.
*/__do_softirq();-----------------------------------------------------首先遍历执行处于pending状态的软中断函数;如果超出一定条件,将工作交给ksoftirqd处理。
} else {
wakeup_softirqd();--------------------------------------------------强制线程化情况,唤醒ksoftirqd内核线程处理。
}
}

__do_softirq是软中断处理的核心,主要分为两部分。

第一部分,尽量处理pending状态的softirq函数。

第二部分,在处理完当前pending状态softirq之后,在处理过程中又产生了新的软中断,会重新restart进行处理;但如果超出一定条件,则交给ksoftirqd内核线程去处理。

asmlinkage __visible void __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
unsigned long old_flags = current->flags;
int max_restart = MAX_SOFTIRQ_RESTART;
struct softirq_action *h;
bool in_hardirq;
__u32 pending;
int softirq_bit; /*
* Mask out PF_MEMALLOC s current task context is borrowed for the
* softirq. A softirq handled such as network RX might set PF_MEMALLOC
* again if the socket is related to swap
*/
current->flags &= ~PF_MEMALLOC; pending = local_softirq_pending();------------------------------获取当前CPU的软中断寄存器__softirq_pending值到局部变量pending。
account_irq_enter_time(current); __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);----------------增加preempt_count中的softirq域计数,表明当前在软中断上下文中。
in_hardirq = lockdep_softirq_start(); restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending();-----------------------------------------清除软中断寄存器__softirq_pending。 local_irq_enable();---------------------------------------------打开本地中断 h = softirq_vec;------------------------------------------------指向softirq_vec第一个元素,即软中断HI_SOFTIRQ对应的处理函数。 while ((softirq_bit = ffs(pending))) {--------------------------ffs()找到pending中第一个置位的比特位,返回值是第一个为1的位序号。这里的位是从低位开始,这也和优先级相吻合,低位优先得到执行。如果没有则返回0,退出循环。
unsigned int vec_nr;
int prev_count; h += softirq_bit - ;---------------------------------------根据sofrirq_bit找到对应的软中断描述符,即软中断处理函数。 vec_nr = h - softirq_vec;-----------------------------------软中断序号
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())) {
pr_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_set(prev_count);
}
h++;-------------------------------------------------------h递增,指向下一个软中断
pending >>= softirq_bit;-----------------------------------pending右移softirq_bit位
} rcu_bh_qs();
local_irq_disable();-------------------------------------------关闭本地中断 pending = local_softirq_pending();-----------------------------再次检查是否有软中断产生,在上一次检查至此这段时间有新软中断产生。
if (pending) {
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)-----------------------------------------再次触发软中断执行的三个条件:1.软中断处理时间不超过2jiffies,200Hz的系统对应10ms;2.当前没有有进程需要调度,即!need_resched();3.这种循环不超过10次。
goto restart; wakeup_softirqd();-----------------------------------------如果上面的条件不满足,则唤醒ksoftirq内核线程来处理软中断。
} lockdep_softirq_end(in_hardirq);
account_irq_exit_time(current);
__local_bh_enable(SOFTIRQ_OFFSET);----------------------------减少preempt_count的softirq域计数,和前面增加计数呼应。表示这段代码处于软中断上下文。
WARN_ON_ONCE(in_interrupt());
tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}

wakeup_softirq()首先获取当前CPU的ksoftirqd线程的task_struct。

如果当前task不处于TASK_RUNNING,则去唤醒此进程。

static void wakeup_softirqd(void)
{
/* Interrupts are disabled: no need to stop preemption */
struct task_struct *tsk = __this_cpu_read(ksoftirqd); if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}

1.4 ksoftirqd内核线程的创建

spawn_ksoftirqd创建于SMP初始化之前,借助smpboot_register_percpu_thread创建了每CPU内核线程ksoftirqd/xx。

static struct notifier_block cpu_nfb = {
.notifier_call = cpu_callback
}; static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
}; static __init int spawn_ksoftirqd(void)
{
register_cpu_notifier(&cpu_nfb); BUG_ON(smpboot_register_percpu_thread(&softirq_threads)); return ;
}
early_initcall(spawn_ksoftirqd);

smpboot_thread_fn()函数中首先判断thread_should_run(),然后再决定是否需要执行thread_fn()。

此处的thread_should_run()即为ksoftirqd_should_run(),返回1表示有softirq处于pending,那么就会执行run_ksoftirqd()。

static int
__smpboot_create_thread(struct smp_hotplug_thread *ht, unsigned int cpu)
{
...
tsk = kthread_create_on_cpu(smpboot_thread_fn, td, cpu,
ht->thread_comm);
...
} static int smpboot_thread_fn(void *data)
{
struct smpboot_thread_data *td = data;
struct smp_hotplug_thread *ht = td->ht; while () {
set_current_state(TASK_INTERRUPTIBLE);
...
if (!ht->thread_should_run(td->cpu)) {
preempt_enable_no_resched();
schedule();
} else {
__set_current_state(TASK_RUNNING);
preempt_enable();
ht->thread_fn(td->cpu);
}
}
}

run_ksoftirqd()在此判断是否有softirq处于pending状态,然后调用__do_softirq()处理软中断。

static int ksoftirqd_should_run(unsigned int cpu)
{
return local_softirq_pending();
} static void run_ksoftirqd(unsigned int cpu)
{
local_irq_disable();
if (local_softirq_pending()) {
/*
* We can safely run softirq on inline stack, as we are not deep
* in the task stack here.
*/
__do_softirq();
local_irq_enable();
cond_resched_rcu_qs();
return;
}
local_irq_enable();
}

2. tasklet

tasklet是利用软中断实现的一种下半部机制,本质上是一个软中断变种,运行在软中断上下文中。

2.1 tasklet数据结构

struct tasklet_struct是tasklet描述符。

struct tasklet_struct
{
struct tasklet_struct *next;------------------多个tasklet串成一个链表。
unsigned long state;--------------------------TASKLET_STATE_SCHED表示tasklet已经被调度,正准备运行;TASKLET_STATE_RUN表示tasklet正在运行中。
atomic_t count;-------------------------------0表示tasklet处于激活状态;非0表示该tasklet被禁止,不允许执行。
void (*func)(unsigned long);------------------该tasklet处理程序
unsigned long data;---------------------------传递给tasklet处理函数的参数
};

每个CPU维护着两个tasklet链表,tasklet_vec用于普通优先级,tasklet_hi_vec用于高优先级;它们都是per-CPU变量。

struct tasklet_head {
struct tasklet_struct *head;
struct tasklet_struct **tail;
}; static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);

2.2 tasklet初始化

tasklet初始化在start_kernel()->softirq_init()中进行,初始化tasklet_vec和tasklet_hi_vec两个链表,并注册TASKLET_SOFTIRQ和HI_SOFTIRQ两个软中断。

那么软中断TASKLET_SOFTIRQ/HI_SOFTIRQ和tasklet_vec/tasklet_hi_vec有什么关系呢?

他们通过tasklet_action()/tasklet_hi_action()联系起来。

asmlinkage __visible void __init start_kernel(void)
{
...
softirq_init();
...
} 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);
}

两种静态初始化、一种动态初始化方法

#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, , ATOMIC_INIT(), func, data }----count初始化为0,表示tasklet处于激活状态 #define DECLARE_TASKLET_DISABLED(name, func, data) \--------------------count初始化为1,表示tasklet处于关闭状态
struct tasklet_struct name = { NULL, , ATOMIC_INIT(), func, data } void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->next = NULL;
t->state = ;
atomic_set(&t->count, );-------------------------------------------这里count为0,表示tasklet处于激活状态
t->func = func;
t->data = data;
}

2.3 tasklet调度和执行

tasklet_schedule()被调用的时机大多在中断上半部中,然后将工作交给__tasklet_schedule()处理。

__tasklet_schedule()锁中断情况下插入当前taskelt到tasklet_vec中,并触发TASKLET_SOFTIRQ软中断。

tasklet_scheduler()中设置了当前tasklet的TASKLET_STATE_SCHED标志位,只要该tasklet没有被执行,那么即使驱动程序多次调用tasklet_schedule()也不起作用。

因此一旦该tasklet挂入到某个CPU的tasklet_vec后,就必须在该CPU的软中断上下文中执行,直到执行完毕并清除了TASKLET_STATE_SCHED标志位,才有机会到其他CPU上运行。

static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))-----------置TASKLET_STATE_SCHED位,如果原来未被置位,则调用__tasklet_schedule()。
__tasklet_schedule(t);
} void __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags; local_irq_save(flags);
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;-------------------------将t挂入到tasklet_vec链表中
__this_cpu_write(tasklet_vec.tail, &(t->next));
raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_restore(flags);
}

软中断执行时会按照软中断状态__softirq_pending来依次执行pending状态的软中断,当执行到TASKLET_SOFTIRQ软中断时,调用tasklet_action()。

static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list; local_irq_disable();
list = __this_cpu_read(tasklet_vec.head);--------------------在关中断情况下读取tasklet_vec立案表头作为临时链表list
__this_cpu_write(tasklet_vec.head, NULL);--------------------重新初始化tasklet_vec
__this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head));
local_irq_enable(); while (list) {-----------------------------------------------开中断情况下遍历tasklet_vec链表,所以tasklet是开中断的
struct tasklet_struct *t = list; list = list->next; if (tasklet_trylock(t)) {--------------------------------如果返回false,表示当前tasklet已经在其他CPU上运行,这一轮将会跳过此tasklet。确保同一个tasklet只能在一个CPU上运行。
if (!atomic_read(&t->count)) {-----------------------表示当前tasklet处于激活状态
if (!test_and_clear_bit(TASKLET_STATE_SCHED,
&t->state))--------------------------清TASKLET_STATE_SCHED位;如果原来没有被置位,则返回0,触发BUG()。
BUG();
t->func(t->data);--------------------------------执行当前tasklet处理函数
tasklet_unlock(t);
continue;----------------------------------------跳到while继续遍历余下的tasklet
}
tasklet_unlock(t);
} local_irq_disable();------------------------------------此种情况说明即将要执行tasklet时,发现该tasklet已经在别的CPU上运行。
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;-----------------把当前tasklet挂入到当前CPU的tasklet_vec中,等待下一次触发时再执行。
__this_cpu_write(tasklet_vec.tail, &(t->next));
__raise_softirq_irqoff(TASKLET_SOFTIRQ);----------------再次置TASKLET_SOFTIRQ位
local_irq_enable();
}
}

HI_SOFTIRQ类型的tasklet和上面基本对称,只是tasklet_vec换成了tasklet_hi_vec,TASKLET_SOFTIRQ换成了HI_SOFTIRQ。

static inline void tasklet_hi_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_hi_schedule(t);
} void __tasklet_hi_schedule(struct tasklet_struct *t)
{
unsigned long flags; local_irq_save(flags);
t->next = NULL;
*__this_cpu_read(tasklet_hi_vec.tail) = t;
__this_cpu_write(tasklet_hi_vec.tail, &(t->next));
raise_softirq_irqoff(HI_SOFTIRQ);
local_irq_restore(flags);
}
static void tasklet_hi_action(struct softirq_action *a)
{
struct tasklet_struct *list; local_irq_disable();
list = __this_cpu_read(tasklet_hi_vec.head);
__this_cpu_write(tasklet_hi_vec.head, NULL);
__this_cpu_write(tasklet_hi_vec.tail, this_cpu_ptr(&tasklet_hi_vec.head));
local_irq_enable(); while (list) {
struct tasklet_struct *t = list; list = list->next; if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
if (!test_and_clear_bit(TASKLET_STATE_SCHED,
&t->state))
BUG();
t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
} local_irq_disable();
t->next = NULL;
*__this_cpu_read(tasklet_hi_vec.tail) = t;
__this_cpu_write(tasklet_hi_vec.tail, &(t->next));
__raise_softirq_irqoff(HI_SOFTIRQ);
local_irq_enable();
}
}

3. local_bh_disable/local_bh_enable

local_bh_disable和local_bh_enable是内核中提供的关闭软中断的锁机制,它们组成临界区禁止本地CPU在中断返回前夕执行软终端,这个临界区称为BH临界区(Bottom Half critical region)。

由于local_bh_disable()和local_bh_enable()之间的区域属于软中断上下文,因此当在临界区发生了中断,中断返回前irq_exit()判断当前软中断上下文,因而不能调用和执行pending状态的软中断。

这样驱动代码构造的BH临界区,就不会有新的软中断来骚扰。

static inline void local_bh_disable(void)
{
__local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);-----------------增加softirq域计数,表示内核状态进入了软中断上下文(softirq context)
}
static __always_inline void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
{
preempt_count_add(cnt);-----------------------------------------------增加softirq域计数
barrier();------------------------------------------------------------防止编译器做优化
}
#define SOFTIRQ_OFFSET (1UL << SOFTIRQ_SHIFT)
#define SOFTIRQ_DISABLE_OFFSET (2 * SOFTIRQ_OFFSET)

local_bh_enable关闭BH临界区,并判断是否可以执行软中断处理。

static inline void local_bh_enable(void)
{
__local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
} void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
WARN_ON_ONCE(in_irq() || irqs_disabled());-----------------------------中断中不能构造BH临界区,irqs_disabled()返回true说明处于关中断状态,也不适合BH操作。
#ifdef CONFIG_TRACE_IRQFLAGS
local_irq_disable();
#endif
/*
* Are softirqs going to be turned on now:
*/
if (softirq_count() == SOFTIRQ_DISABLE_OFFSET)
trace_softirqs_on(ip);
/*
* Keep preemption disabled until we are done with
* softirq processing:
*/
preempt_count_sub(cnt - );------计数减去SOFTIRQ_DISABLE_OFFSET-1,留1表示关闭本地CPU抢占,接下来调用do_softirq()时不希望被其他高优先级任务抢占了或者当前任务被迁移到其它CPU上。 if (unlikely(!in_interrupt() && local_softirq_pending())) {
/*
* Run softirq if any pending. And do it in its own stack
* as we may be calling this deep in a task call stack already.
*/
do_softirq();-------------------------------------------------------非中断上下文环境中执行软中断
} preempt_count_dec();----------------------------------------------------打开抢占
#ifdef CONFIG_TRACE_IRQFLAGS
local_irq_enable();
#endif
preempt_check_resched();
}

local_bh_disabled()/local_bh_enable()是关BH接口API,运行在进程上下文中。

4. 小结

tasklet基于softirq,但是tasklet和softirq又存在一些区别。

  softirq tasklet
分配 softirq是静态定义的 tasklet既可以静态定义,也可以通过tasklet_init()动态创建。
并发性 softirq是可重入的,同一类型的软中断可以在多个CPU上并发执行。

tasklet是不可重入的,tasklet必须串行执行,同一个tasklet不可能同时在两个CPU上运行。

tasklet通过TASKLET_STATE_SCHED和TASKLET_STATE_RUN保证串行

运行

softirq运行在开中断环境下。

软中断回调函数不能睡眠,因为软中断可能处于中断上下文中,睡眠导致Linux无法调度。

软中断的执行时机可能在中断返回时,即退出中断上下文时。或者local_bh_enable()中。

taskelt执行时机在softirq中
上一篇:HTML精确定位:scrollLeft,scrollWidth,clientWidth,offsetWidth


下一篇:转 awk统计nginx每天访问最高的接口