目录:
关键词: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=0,------------------------最高优先级的软中断类型 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(0);-----------------------------------------清除软中断寄存器__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 - 1;---------------------------------------根据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? ", 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 0; } 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 (1) { 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, 0, ATOMIC_INIT(0), func, data }----count初始化为0,表示tasklet处于激活状态 #define DECLARE_TASKLET_DISABLED(name, func, data) --------------------count初始化为1,表示tasklet处于关闭状态 struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data } void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data) { t->next = NULL; t->state = 0; atomic_set(&t->count, 0);-------------------------------------------这里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 - 1);------计数减去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中 |