运行实体调研报告
运行实体
运行实体包括中断处理过程、内核线程、系统调用过程、软中断、tasklet、work queue、用户线程等。
其中内核线程、用户线程参与系统线程调度,有自己的task结构体。
内核线程与用户线程则在线程调度时得到调度机会,当线程用完时间片、线程主动让出cpu、中断处理程序执行完毕后进行线程调度,则可能发生线程切换。根据线程调度算法,从线程就绪队列中选择要调度的下一个线程。系统切换上下文,将上一个task状态保存,切换至新的task,根据task结构体信息切换地址空间与调用栈,装载新的cs:ip,跳转执行,完成线程切换。当线程不再符合可调度条件时会被移至阻塞线程队列,当线程满足运行条件后,可重新被移至就绪队列。
系统调用过程一般指用户线程执行系统调用,陷入核心态,执行内核程序,执行完毕后返回用户态的过程。一般采用称作trap,可采用int80或者syscall/sysenter指令实现。简单介绍下中断处理过程,响应中断后,会执行关中断操作将IF中断允许标志至0,将flags、cs等寄存器压入内核栈,保护断点与现场,根据中断类型号在中断向量表中找到对应的中断服务程序入口地址,将地址装入cs:ip,转向中断处理程序。之后执行中断处理服务程序,执行完毕后使用iret指令返回中断。
中断处理过程是系统在开中断且未屏蔽中断的条件下,指令执行完毕后进入中断响应周期,响应外部硬件中断跳转执行中断处理函数的过程,属于top-half。
软中断、tasklet、work queue则属于bottom-half的实现机制,用于处理可延时的中断处理。
tcp/ip网络处理一般属于可延时的中断任务,需要使用bottom-half的机制,linux5.5.19使用的是napi接口+软中断处理机制,所以这里主要分析bottom-half部分。
Bottom-Half
使用原因
在linux运行环境中,如果出现中断请求且处理器一旦接受中断进入中断处理程序,就会打断正在执行的代码,调用中断处理函数。如果在中断处理函数中没有禁止中断,该中断处理函数执行过程中仍有可能被其他中断打断,产生嵌套中断,使得中断处理过程时间增长,且严重增加系统的复杂度。出于这样的原因,大家都希望中断处理函数执行得越快越好。
基于上面的原因,内核将整个的中断处理流程分为了top-half和bottom-half。top-half就是之前所说的关中断的中断处理函数,它能最快的响应中断,并且做一些必须在中断响应之后马上要做的事情。而一些需要在中断处理函数后继续执行的操作,内核建议把它放在bottom-half执行。bottom-half的是一些虽然与中断有相关性但是可以延后执行的开中断的任务,可以使用bottom-half处理非紧急事务。由于bottom-half可以被中断打断,所以执行bottom-half时仍然能够即使处理紧要的中断。
拿网卡来举例,在linux内核中,当网卡一旦接受到数据,网卡会通过中断告诉内核处理数据,内核会在网卡中断处理函数(top-half)执行一些网卡硬件的必要设置,因为这是在中断响应后急切要干的事情。接着,内核调用对应的bottom-half函数来处理网卡接收到的数据,因为数据处理没必要在中断处理函数里面马上执行,可以将中断让出来做更紧迫的事情。
演进
-
最早的bottom-half实现是借用中断向量表的方式,原始的bottom-half机制有几个很大的局限,最重要的一个就是函数个数限制在32个以内,随着系统硬件越来越多,软中断的应用范围越来越大,这个数目显然是不够用的。此外原始的bottom-half在系统中只能存在一个,因此不能并行,无法利用多处理机资源
-
在2.0.x内核里,用task queue(任务队列)的办法对其进行了扩充,解决了32个的数量限制。
-
在2.4版本中内核引入tasklet,tasklet可以并发运行在多个cpu上,解决bottom-half严格串行化不能利用多处理机的问题。
-
后来内核中又引入了统一的softirq软中断体系架构,将旧的tasklet与bottom-half函数接口都集成到了这个框架之中。旧的bottom-half函数接口通过tasklet在软中断中仍然得到保留,在softirq_init初始化时bottom-half函数被绑定到tasklet_vec链表上。至此旧的bottom-half函数接口/tasklet都被统一集成在新的softirq机制下
-
为了适应更灵活的需求,在2.6版本中linux又引入了新的bottom-half机制,工作队列work queue,能够以内核线程的形式运行在可调度的进程上下文中,当需要执行延迟大可能阻塞的操作时可以考虑使用work queue
虽然原始的bottom-half处理机制已经不存在,但从逻辑上仍可以参照原来的划分思想,将完整的中断过程分为top-half与bottom-half
一个工作是放在bottom-half还是放在top-half去执行,可以参考下面4条:
-
如果一个任务对时间非常敏感,将其放在中断处理程序中执行。
-
如果一个任务和硬件相关,将其放在中断处理程序中执行。
-
如果一个任务要保证不被其他中断(特别是相同的中断)打断,将其放在中断处理程序中执行。
-
其他所有任务,考虑放在bottom-half去执行。
分类
旧的bottom-half函数接口已经被整合在新的tasklet中(tasklet现在已经使用softirq实现),linux使用新的软中断(softirq)与工作队列(work queue)机制。

tasklet基于softirq,但是tasklet和softirq又存在一些区别。
| softirq | tasklet | work_queue | |
|---|---|---|---|
| 分配 | softirq是静态定义的 | tasklet既可以静态定义,也可以通过tasklet_init()动态创建。 | 可以动态创建提交任务,在内核初始化时创建等同于cpu数量的内核worker线程 |
| 并发性 | softirq是可重入的,同一类型的软中断可以在多个CPU上并发执行。 | tasklet是不可重入的,tasklet必须串行执行,同一个tasklet不可能同时在两个CPU上运行。tasklet通过TASKLET_STATE_SCHED和TASKLET_STATE_RUN保证串行 | work_queue是可重入的,同一个work可以在多个CPU上并发执行。 |
| 可被抢占 | 中断Y 软中断N 进程N | 同软中断 | 中断Y 软中断Y 进程Y |
| 执行时机 | 中断退出时irq_exit,local_bh_enable(),内核线程ksoftirqd中 | 在软中断softirq运行时得到执行,同软中断 | 在对应的内核线程中得到执行 |
| 局限性 | 软中断回调函数不能睡眠不能阻塞,需要满足可重入性 | 不能睡眠不能阻塞 | 实时性较低,只适合内核中不紧要的工作 |
| 使用场景 | 可延迟的内核事件,如网络报接发 | 不需要考虑并行性的可延迟的内核事件,不需要保持可重入性 | 可阻塞可延迟的事件 |
软中断
使用原因
软中断是预留给系统中对时间要求最为严格最重要的bottom-half使用的,系统将重要的工作在top-half执行,将可延迟的任务放到bottom-half,软中断是bottom-half的一种实现。
软中断的特性包括:
-
产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不会抢占另外一个软中断,唯一可以抢占软中断的是中断处理程序。
-
可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保其数据结构。
-
软中断是在编译期间静态分配的。
-
系统静态定义了若干软中断类型,并且Linux内核开发者不希望用户扩充新的软中断类型。最多可以有32个软中断。
数据结构
软中断描述符
-
软中断描述符
struct softirq_action{ void (*action)(struct softirq_action *);};描述每一种类型的软中断,其中void(*action)是软中断触发时的执行函数。1 struct softirq_action 2 { 3 void (*action)(struct softirq_action *);//一个action函数指针,触发该软中断时就会调用action回调函数来处理这个软中断。 4 };
-
数组softirq_vec[NR_SOFTIRQS]存放软中断描述符
softirq_action,软中断索引号就是该数组的索引,NR_SOFTIRQS是系统支持的软中断最大数量。索引号也表示该类型软中断的执行优先级,对应在
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp; //__cacheline_aligned_in_smp用于将softirq_vec数据结构和L1缓存行对齐。 enum //优先级 { HI_SOFTIRQ=0, /*用于高优先级的tasklet*/ TIMER_SOFTIRQ, /*用于定时器的bottomhalf*/ 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 */ NR_SOFTIRQS };
软中断状态变量
irq_cpustat_t用来描述软件中断状态信息,可以理解为“软中断状态寄存器”,其实是一个unsigned int类型变量__softirq_pending。
每个CPU都保存一个irq_cpustat_t,cpu数量为NR_CPUS
irq_cpustat_t irq_stat[NR_CPUS]相当于每个CPU有一个软中断状态信息变量。
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)
API
-
注册软中断
调用
void open_softirq(int nr, void (*action)(struct softirq_action *)) { softirq_vec[nr].action = action; }
即注册对应类型的处理函数到全局数组softirq_vec中。例如网络发包对应类型为NET_TX_SOFTIRQ的处理函数net_tx_action.
-
触发软中断
void raise_softirq(unsigned int nr)
实际上即以软中断类型nr作为偏移量置位每cpu变量irq_stat[cpu_id]的成员变量__softirq_pending,这也是同一类型软中断可以在多个cpu上并行运行的根本原因。
-
软中断执行函数
do_softirq-->__do_softirq
-
读取当前CPU软中断状态变量
local_softirq_pending()
-
设置当前CPU的特定软中断处于pending状态
__raise_softirq_irqoff()->or_softirq_pending()
-
设置当前CPU的软中断状态变量(设0表示清空)
__do_softirq()->set_softirq_pending()
以下是部分定义:
/* 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))
执行路径
软中断在内核初始化时初始化,一般在收到硬件中断时,在中断处理程序中触发软中断,但等到中断结束时才真正尝试调度执行软中断,或者在local_bh_enable被调用时执行软中断,或者唤醒内核线程ksoftirqd去执行软中断
例如网络类驱动程序,可以在硬件中断中转至对应的驱动程序,处理中断函数触发软中断,退出中断时调度软中断
初始化过程
start_kernel()->softirp_init()打开TASKLET_SOFTIRQ与HI_SOFTIRQ两个tasklet的软中断
asmlinkage void __init start_kernel(void) { .... // 初始化软中断) softirq_init(); time_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); }
其他各种类型的软中断任务初始化函数在系统其他模块初始化时调用,函数如下:
//块设备:blk_softirq_init open_softirq(BLOCK_SOFTIRQ, blk_done_softirq); //网络设备:net_dev_init open_softirq(NET_TX_SOFTIRQ, net_tx_action); open_softirq(NET_RX_SOFTIRQ, net_rx_action); //调度中负载均衡:init_sched_fair_class open_softirq(SCHED_SOFTIRQ, run_rebalance_domains); //任务延时tasklet:softirq_init open_softirq(TASKLET_SOFTIRQ, tasklet_action); open_softirq(HI_SOFTIRQ, tasklet_hi_action); //定时器类处理:init_timers open_softirq(TIMER_SOFTIRQ, run_timer_softirq); //RCU类处理:rcu_init open_softirq(RCU_SOFTIRQ, rcu_process_callbacks);
触发时机
该函数首先调用__softirq_pending置软中断对应位。然后如果in_interrupt()为0,如果当前不在中断上下文中,表示接下来可以执行软中断,这时唤醒ksoftirqd内核线程。
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状态。 } /* * This function must run with irqs disabled! */ inline void raise_softirq_irqoff(unsigned int nr) { __raise_softirq_irqoff(nr); if (!in_interrupt()) wakeup_softirqd();//如果不处于中断上下文中,则尽快执行软中断处理。 }
执行时机
有三种执行时机可以执行软中断处理
-
irq_exit的时候:irq_exit()->
-
local_bh_enable的时候:
-
ksoftirqd内核线程执行函数
三种函数典型调用时机:
在硬件中断退出时会执行irq_exit(),
在执行软中断处理do_softirq()中触发软中断允许开启,调用local_bh_enable函数,后续可能会继续触发do_softirq()或者唤醒ksoftirqd
在执行软中断处理do_softirq()时可能会达成条件唤醒ksoftirqd内核线程
irq_exit
中断处理结束触发irq_exit
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; }
执行软中断处理函数__do_softirq前还需要首先满足两个条件:
-
不在中断中(硬中断、软中断和NMI) 。
-
有软中断处于pending状态。
系统这么设计是为了避免软件中断在中断嵌套中被调用,使得软件中断不能中断中断,并且达到在单个CPU上软件中断不能被重入的目的。我的理解是,在发生中断嵌套的时候,表明这个时候是系统突发繁忙的时候,内核第一要务就是赶紧把中断中的事情处理完成,退出中断嵌套。避免多次嵌套,所以把软件中断推迟到了所有中断处理完成的时候才能触发软件中断。所以在irq_exit时调用软件中断
void irq_exit(void) { ... if (!in_interrupt() && local_softirq_pending())//当前不处于中断上下文,处于进程上下文中且有pending软中断。 invoke_softirq(); ... }
接着调用__do_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内核线程处理。 } }
软中断的开/关
local_bh_disable和local_bh_enable是内核中提供的允许的锁机制,它们组成临界区禁止本地CPU在中断返回前夕执行软中断,local_bh_disable->local_bh_enable这个临界区称为BH临界区(Bottom Half critical region)。
由于local_bh_disable()和local_bh_enable()之间的区域属于软中断上下文,因此当在临界区发生了中断,中断返回前irq_exit()判断当前软中断上下文,因而不能调用和执行pending状态的软中断。
这样驱动代码构造的BH临界区,就不会有新的软中断来骚扰。
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(); }
ksoftirqd内核线程
如果选择唤醒ksoftirqd
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); }
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(); }
该内核线程spawn_ksoftirqd创建于SMP初始化之前,借助smpboot_register_percpu_thread创建了每CPU一个的内核线程ksoftirqd。
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);
在
此处的thread_should_run()即为
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); } } }
执行过程
__do_softirq是软中断处理的核心,主要分为两部分。
-
尽量处理pending状态的softirq函数。按优先级顺序调度
这里分为几步:
获取当前cpu软中断寄存器
关闭允许local_bh_disable,防止执行过程中被其他软中断中断
清空原有的中断寄存器
打开本地中断,使得可以被硬中断中断
开始遍历执行拿到的软中断寄存器,对于每个pending的软中断事件,按优先级(类型号)执行
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))) {//找到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?\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状态softirq之后,在处理过程中又产生了新的软中断,会重新restart进行处理;但如果超出一定条件,则调用 wakeup_softirqd()。
pending = local_softirq_pending();//再次检查是否有软中断产生,在上一次检查至此这段时间有新软中断产生。 if (pending) { if (time_before(jiffies, end) && !need_resched() && --max_restart) goto restart; wakeup_softirqd();//如果上面的条件不满足,则唤醒ksoftirq内核线程来处理软中断。 }
这里主要有三个条件,任一条件触发则结束处理
软中断处理时间超过jiffies,200Hz的系统对应10ms;
当前没有有进程需要调度,即!need_resched();
这种循环超过10次。
这种情况结束处理, 调用wakeup_softirqd()唤醒ksoftirqd内核线程去处理。
-
打开允许软中断local_bh_enable,执行退出软中断操作
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);
Tasklet
tasklet基于软中断,软中断号有两个适用于tasklet:TASKLET_SOFTIRQ和HI_SOFTIRQ
两者优先级不同,HI_SOFTIRQ优先级更高
使用原因
软中断存在一些局限性
-
软中断必须使用可重入函数。
可重入函数 首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。 可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那 么软中断其实是没有必要的。
-
软中断是静态分配的,在内核编译好之后,就不能改变
因此诞生了弥补以上缺点的tasklet。它具有以下特性:
-
一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行,且不能阻塞。
-
多个不同类型的tasklet可以并行在多个CPU上。
-
tasklet就灵活许多,可以在运行时改变(比如添加模块时)。
-
可以动态增加减少,没有数量限制。
tasklet是在两种软中断类型的基础上实现的,因此如果不需要软中断的并行特性,tasklet就是最好的选择。
数据结构
tasklet以链表形式存在,每个cpu有两个软中断号用于tasklet处理,每个中断号事实上各维护了一个tasklet链表,两个链表分别为tasklet_vec和tasklet_hi_vec
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);

下面是tasklet结构体成员
-
next:指向下一个tasklet的指针,说明这个结构体的成员会被加入到一个链表里。 -
state:用于标识tasklet状态,这一个无符号长整数,当前只使用了bit[1]和bit[0]两个状态位。其中,bit[1]=1表示这个tasklet当前正在某个CPU上被执行,它仅对SMP系统才有意义,其作用就是为了防止多个CPU同时执行一个tasklet的情形出现;bit[0]=1表示这个tasklet已经被调度去等待执行了但还没有开始执行,其作用是阻止同一个tasklet在被运行之前被重复调度,考虑如下情况:一个tasklet已经被触发过一次,即调度过一次,但可能还没有来得及被执行。对这两个状态位的宏定义如下所示:可以理解为每个tasklet有一个简单的状态机,
0 -> TASKLET_STATE_SCHED -> TASKLET_STATE_RUN -> 0。 -
count:引用计数,若不为0,则tasklet被禁止,只有当它为0时,tasklet才被激活,也就是说该tasklet的处理函数func才可以被执行,只有设置为激活后,tasklet对应的软中断被raise时该tasklet才会被投入运行。 -
func:是一个函数指针,也是对应这个tasklet的处理函数。 -
data:函数func的参数。这是一个32位的无符号整数,其具体含义可供func函数自行解释,比如将其解释成一个指向某个用户自定义数据结构的地址值。
struct tasklet_struct { struct tasklet_struct *next;//多个tasklet串成一个链表。 unsigned long state;//TASKLET_STATE_SCHED表示tasklet已经被调度,正准备运行;TASKLET_STATE_RUN表示tasklet正在运行中。 atomic_t count;//表示tasklet处于激活状态;非0表示该tasklet被禁止,不允许执行。 void (*func)(unsigned long);//该tasklet处理程序 unsigned long data;//传递给tasklet处理函数的参数 };
API
-
创建tasklet对象
分两种创建tasklet对象的方式:
-
静态创建:使用中定义的两个宏中的一个:
`DECLARE_TASKLET(name, func, data)`或者`DECLARE_TASKLET_DISABLED(name, func, data)`
两个宏之间的区别在于引用计数的初始值设置不同,
DECLARE_TASKLET把创建的tasklet的引用计数设置为0,一开始处于激活状态;DECLARE_TASKLET_DISABLED把创建的tasklet的引用计数设置为1,一开始处于禁止(非激活)状态。 -
动态创建:使用
tasklet_init函数:tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)
我们可以看到,无论是静态方式,还是动态方式,都需要传一个函数地址,这个函数就是每个tasklet自己需要实现的处理函数
func#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; }
-
-
改变一个tasklet对象的状态
tasklet有两个状态位,分别为
TASKLET_STATE_SCHED、TASKLET_STATE_RUN状态位的存在是为了实现tasklet的特性:同一tasklet只能在同一cpu上执行
使用原子操作
test_and_set_bit设置TASKLET_STATE_SCHED状态位test_and_clear_bit清除TASKLET_STATE_SCHED状态位tasklet_trylock设置TASKLET_STATE_RUN状态位tasklet_unlock清除TASKLET_STATE_RUN`状态位影响的是
tasklet_struct结构体的state域。在触发软中断时通过
TASKLET_STATE_SCHED使得同一tasklet只能被同一cpu调度,在执行时会清除TASKLET_STATE_SCHED标志,这使得其他cpu能够调度该tasklet。但此时会尝试设置TASKLET_STATE_RUN,设置成功才能运行该tasklet,保证了同一tasklet只能在同一cpu上运行简单来说,如果对同一个tasklet,如果一个处理器CPU0上正在运行它,另一个处理器CPU1还是有机会调度它,注意仍然保证不会同时运行,但可以在此后的时间被CPU1运行。但与此同时如果在第三个CPU2上又有一个新的想通过的tasklet发生,那只有被丢弃的份了。
-
使能/禁止一个tasklet
tasklet_disable tasklet_enable可以使tasklet激活或者失效,只有激活时回调函数才有可能被执行
使能与禁止操作往往总是成对地被调用的,tasklet_disable()函数如下:
函数tasklet_disable_nosync()也是一个静态inline函数,它简单地通过原子操作将count成员变量的值减1。如下所示(interrupt.h):
static inline void tasklet_disable_nosync(struct tasklet_struct *t) { atomic_inc(&t->count); smp_mb__after_atomic_inc(); }
执行路径
同软中断类似,tasklet在内核初始化时由softirq_init初始化对应的软中断号,在硬件中断中触发tasklet,将其挂到对应的tasklet链表上。
执行时机则完全等同于软中断执行时机:在中断退出时调度软中断,tasklet作为软中断的处理过程按顺序得到执行。同样的,也可以被ksoftirqd内核线程或者在local_bh_enable被调用时调度执行软中断,从而调度执行tasklet
初始化过程
tasklet初始化在start_kernel()->
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); }
触发时机
tasklet_schedule()被触发的时机大多在中断top-half中,然后将工作交给tasklet_schedule()处理,tasklet_schedule()在中断处理程序中锁中断情况下插入当前taskelt到tasklet_vec中,并调用raise_softirq_irqoff(TASKLET_SOFTIRQ)触发TASKLET_SOFTIRQ软中断,待系统调度运行处理软中断时会处理tasklet。
一般一个tasklet用来对同一种中断类型进行后续的处理,所以完全不必要通过动态生成tasklet对象的方式在每次中断到来时重新生成一个tasklet对象来做后半段的处理。事实上Linux内核源码中,几乎所有的tasklet对象都是针对同一类型的中断只产生一个。因此需要锁语义保证其同时只在同一cpu上运行,这里使用的是原子操作标志位方式,相当于一种乐观锁
tasklet_scheduler()中设置了当前tasklet的TASKLET_STATE_SCHED标志位,只要该tasklet没有被执行,那么即使驱动程序多次调用tasklet_schedule()也不起作用,一个tasklet此时只会被一个cpu调度。
因此一旦该tasklet挂入到某个CPU的tasklet_vec后,就必须在该CPU的软中断上下文中执行,直到执行开始前清除了TASKLET_STATE_SCHED标志位,才有机会被调度到其他CPU上(但仍需要竞争RUN标志位才能运行,这是第二道防线)。
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); }
执行过程
执行时机等同于软中断,不再赘述
tasklet依赖软中断执行,软中断执行时会按照软中断状态__softirq_pending来依次执行pending状态的软中断,当执行到TASKLET_SOFTIRQ软中断时,其对应的回调函数tasklet_action()会被调用
tasklet_action()负责执行tasklet链表上的各个tasklet
下面是tasklet_action()执行过程:
先关中断
获取tasklet_vec链表
清空原系统中的链表
开中断
遍历tasklet_vec链表,tasklet_trylock(t)尝试设置TASKLET_STATE_RUN标记位,保证同一tasklet只能同时在一个cpu上运行
如果tasklet_trylock(t)失败,则关中断将其重新挂回系统tasklet链表然后开中断。继续遍历下一个链表项
如果成功则判断该tasklet是否被激活,被激活则执行其对应的回调函数,否则忽略,继续遍历链表直到链表为空
static void tasklet_action(struct softirq_action *a) { struct tasklet_struct *list; local_irq_disable();//先关中断 list = __this_cpu_read(tasklet_vec.head); __this_cpu_write(tasklet_vec.head, NULL); __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(); } }
工作队列
使用原因
上面介绍的可延迟函数运行在中断上下文中(软中断的一个检查点就是do_IRQ退出的时候),于是导致了一些问题:
软中断不能睡眠、不能阻塞。(由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。)
另一方面,可阻塞函数不能用在中断上下文中实现,必须要运行在进程上下文中,例如访问磁盘数据块的函数。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。 因此在2.6版的内核中出现了在内核态运行的工作队列(替代了2.4内核中的任务队列)。它也具有一些可延迟函数的特点(需要被激活和延后执行),但是能够能够在不同的进程间切换,以完成不同的工作。
工作队列使用了一个内核线程处理工作,因此允许重新调度甚至休眠。系统默认的工作线程为events,工作线程是内核线程,因此只能访问内核空间。
实际上,工作队列的本质就是将工作交给内核线程处理,因此其可以用内核线程替换。但是内核线程的创建和销毁对编程者的要求较高,而工作队列实现了内核线程的封装,不易出错,所以我们也推荐使用工作队列。
通常,在工作队列和软中断/tasklet中作出选择非常容易。可使用以下规则:
-
如果推后执行的任务需要睡眠,那么只能选择工作队列。
-
如果推后执行的任务需要延时指定的时间再触发,那么使用工作队列,因为其可以利用timer延时(内核定时器实现)。
-
如果推后执行的任务需要在一个tick之内处理,则使用软中断或tasklet,因为其可以抢占普通进程和内核线程,同时不可睡眠。
-
如果推后执行的任务对延迟的时间没有任何要求,则使用工作队列,此时通常为无关紧要的任务。
数据结构
推后执行的任务叫做工作(work),描述它的数据结构为work_struct ,延迟的工作用delayed_work表示,这些工作以队列结构组织成工作队列(workqueue),其数据结构为workqueue_struct 。每个cpu有一个工作队列和其对应的一个内核线程

struct work_struct { atomic_long_t data; //传递给工作函数的参数 #define WORK_STRUCT_PENDING 0 /* T if work item pending execution */ #define WORK_STRUCT_FLAG_MASK (3UL) #define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK) struct list_head entry; //链表结构,链接同一工作队列上的工作。 work_func_t func; //工作函数,用户自定义实现 #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif }; //工作队列执行函数的原型: void (*work_func_t)(struct work_struct *work); //该函数会由一个工作者线程执行,因此其在进程上下文中,可以睡眠也可以中断。但只能在内核中运行,无法访问用户空间。 struct delayed_work { struct work_struct work; struct timer_list timer; //定时器,用于实现延迟处理 }; struct workqueue_struct { struct cpu_workqueue_struct *cpu_wq; //指针数组,其每个元素为per-cpu的工作队列 struct list_head list; const char *name; int singlethread; //标记是否只创建一个工作者线程 int freezeable; /* Freeze threads during suspend */ int rt; #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif };
每个cpu拥有一个工作队列
struct cpu_workqueue_struct { spinlock_t lock; struct list_head worklist; wait_queue_head_t more_work; struct work_struct *current_work; struct workqueue_struct *wq; struct task_struct *thread; } ____cacheline_aligned;
API
-
静态创建工作
DECLARE_WORK(name,function); //定义正常执行的工作项 DECLARE_DELAYED_WORK(name,function);//定义延后执行的工作项
-
动态创建工作
INIT_WORK(_work, _func) //创建正常执行的工作项 INIT_DELAYED_WORK(_work, _func)//创建延后执行的工作项
-
调度工作队列
在默认情况下,每个CPU均有一个类型为“events”的工作者线程,当调用schedule_work时,这个工作者线程会被唤醒去执行工作链表上的所有工作。
//默认调度 int schedule_work(struct work_struct *work) //调度延迟工作 int schedule_delayed_work(struct delayed_work *dwork,unsigned long delay) //刷新缺省工作队列,/此函数会一直等待,直到队列中的所有工作都被执行。 void flush_scheduled_work(void)
-
取消延迟工作
static inline int cancel_delayed_work(struct delayed_work *work)
执行路径
在内核初始化时,会初始化工作队列,调用workqueue_init会create_worker创建多个(同cpu数量的内核线程用于执行工作队列任务),在运行过程中,只需要将任务或者队列提交给工作队列即可,内核线程会参与线程调度,并在唤醒时周期性执行这些任务

初始化
默认的工作队列和工作者线程由内核初始化时创建:
start_kernel->workqueue_init->create_worker->kthread_create_on_node->__kthread_create_on_node int __init workqueue_init(void) { ........ for_each_possible_cpu(cpu) { for_each_cpu_worker_pool(pool, cpu) { pool->node = cpu_to_node(cpu); } } list_for_each_entry(wq, &workqueues, list) { wq_update_unbound_numa(wq, smp_processor_id(), true); WARN(init_rescuer(wq), "workqueue: failed to create early rescuer for %s", wq->name); } mutex_unlock(&wq_pool_mutex); /* create the initial workers */ for_each_online_cpu(cpu) { for_each_cpu_worker_pool(pool, cpu) { pool->flags &= ~POOL_DISASSOCIATED; BUG_ON(!create_worker(pool)); } } hash_for_each(unbound_pool_hash, bkt, pool, hash_node) BUG_ON(!create_worker(pool)); wq_online = true; wq_watchdog_init(); return 0; }
create_worker
static struct worker *create_worker(struct worker_pool *pool) { ...... worker->task = kthread_create_on_node(worker_thread, worker, pool->node, "kworker/%s", id_buf); ..... return worker; }
TCP/IP 软中断过程分析
网卡接受发送数据包时触发中断,中断处理服务程序中触发软中断,在软中断中完成数据包的解析等后续处理工作,这大大减少了网络传输占用的中断处理时间,提升了系统运行效率。
举个例子,网卡收到数据包会触发硬件中断,在硬件中断中调用驱动程序的中断处理函数,之后触发软中断,在执行软中断时执行对应的action处理函数完成数据包的处理。
软中断号
网络收发数据包注册的软中断号为:
NET_TX_SOFTIRQ//发送 NET_RX_SOFTIRQ//接收
其中在内核初始化过程为:
static int __init net_dev_init(void) { ... open_softirq(Net_TX_SOFTIRQ,net_tx_action); open_softirq(Net_RX_SOFTIRQ,net_rx_action); ... }
NAPI
NAPI是linux新的网卡数据处理API,据说是由于找不到更好的名字,所以就叫NAPI(New API),在2.5之后引入。
简单来说,NAPI是综合中断方式与轮询方式的技术。
中断的好处是响应及时,如果数据量较小,则不会占用太多的CPU事件;缺点是数据量大时,会产生过多中断,而每个中断都要消耗不少的CPU时间,从而导致效率反而不如轮询高。轮询方式与中断方式相反,它更适合处理大量数据,因为每次轮询不需要消耗过多的CPU时间;缺点是即使只接收很少数据或不接收数据时,也要占用CPU时间。
NAPI是两者的结合,数据量低时采用中断,数据量高时采用轮询。平时是中断方式,当有数据到达时,会触发中断处理函数执行,中断处理函数关闭中断开始处理。采用napi收到一个包来后会产生接收中断,但是马上关闭。直到收够了netdev_max_backlog个包(默认300),或者收完mac上所有包后,才再打开接收中断。
很明显,数据量很低与很高时,NAPI可以发挥中断与轮询方式的优点,性能较好。
napi特性:
(1) 支持NAPI的网卡驱动必须提供轮询方法poll()。
(2) NAPI的内核接口为napi_schedule()。
(3)NAPI使用设备内存(或者设备驱动程序的接收环)。
处理过程
每个网络设备(MAC层)都有自己的net_device数据结构,这个结构上有napi_struct。 每当收到数据包时,网络设备驱动会把自己的napi_struct挂到CPU私有变量上。 这样在软中断时,net_rx_action会遍历cpu私有变量的poll_list, 执行上面所挂的napi_struct结构的poll钩子函数,将数据包从驱动传到网络协议栈。
static void net_tx_action(struct softirq_action *h) { ... local_irq_enable(); while (clist) { struct sk_buff *skb = clist; clist = clist->next; WARN_ON(atomic_read(&skb->users)); if (likely(get_kfree_skb_cb(skb)->reason == SKB_REASON_CONSUMED)) trace_consume_skb(skb); else trace_kfree_skb(skb, net_tx_action); __kfree_skb(skb); //释放skb } } if (sd->output_queue) { //发送队列不为空 struct Qdisc *head; local_irq_disable(); head = sd->output_queue; sd->output_queue = NULL; //发送队列置NULL sd->output_queue_tailp = &sd->output_queue; local_irq_enable(); while (head) { struct Qdisc *q = head; spinlock_t *root_lock; head = head->next_sched; //每个队列都有执行机会 root_lock = qdisc_lock(q); if (spin_trylock(root_lock)) { smp_mb__before_atomic(); clear_bit(__QDISC_STATE_SCHED, &q->state); qdisc_run(q); //尝试启动qdisc发送报文 spin_unlock(root_lock); } else { if (!test_bit(__QDISC_STATE_DEACTIVATED, &q->state)) { __netif_reschedule(q); //加锁失败,且qdisc处于active状态,重新放到发送队列,触发软中断 } else { smp_mb__before_atomic(); clear_bit(__QDISC_STATE_SCHED, &q->state); } } } } }
2.net_rx_action函数处理接收队列
static __latent_entropy void net_rx_action(struct softirq_action *h) { ... for (;;) { struct napi_struct *n; if (list_empty(&list)) { if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll)) goto out; break; } n = list_first_entry(&list, struct napi_struct, poll_list); budget -= napi_poll(n, &repoll);//napi poll ... } ... out: __kfree_skb_flush(); }
napi_poll调用了napi_struct上的poll钩子函数
static int napi_poll(struct napi_struct *n, struct list_head *repoll) { void *have; int work, weight; list_del_init(&n->poll_list); have = netpoll_poll_lock(n); weight = n->weight; work = 0; if (test_bit(NAPI_STATE_SCHED, &n->state)) { work = n->poll(n, weight);//钩子函数 trace_napi_poll(n, work, weight); } .... }
接着调用napi_gro_recieve
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb) { gro_result_t ret; skb_mark_napi_id(skb, napi); trace_napi_gro_receive_entry(skb); skb_gro_reset_offset(skb); ret = napi_skb_finish(napi, skb, dev_gro_receive(napi, skb)); trace_napi_gro_receive_exit(ret); return ret; }
调用dev_gro_receive->napi_gro_complete
接着调用netif_receive_skb_internal-> __netif_recieve_skb
调用 __netif_receive_skb_one_core
__netif_receive_skb_core
deliver_ptype_list_skb->deliver_skb向上层分发SKB,调用对应packet_type的钩子函数,根据第二层不同协议来进入不同的钩子函数,重要的有:ip_rcv() arp_rcv(),进入第三层,之后就是我们熟悉的常规的向上分发了,这里不再赘述
static inline int deliver_skb(struct sk_buff *skb, struct packet_type *pt_prev, struct net_device *orig_dev) { if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC))) return -ENOMEM; refcount_inc(&skb->users); return pt_prev->func(skb, skb->dev, pt_prev, orig_dev); }
内核调试追踪
软中断注册与tasklet启动
start_kernel入口
先open_softirq,对应的分别是7、9、1、8号软中断
enum //优先级 { HI_SOFTIRQ=0, /*用于高优先级的tasklet*/ TIMER_SOFTIRQ, /*用于定时器的bottomhalf*/ 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 */ NR_SOFTIRQS };
根据表得知是初始化SCHED、RCU、TIMER、HRTIMER模块时调用开启了对应软中断
然后调用softirq_init打开两个tasklet的软中断

继续运行,收到TIMER计时器的软中断

执行TIMER软中断

接下来在系统初始化过程中又注册了打开了三个软中断4、2、3
打开的是网络层收发与块设备软中断

tasklet被调用执行,执行keyboard_tasklet

ksoftirqd内核线程初始化路径
spawn_ksoftirqd cpuhp_setup_state_nocalls cpuhp_setup_state
cpuhp_setup_state_cpuslocked cpuhp_store_callbacks
对内核线程注册回调函数


工作队列初始化路径


浙公网安备 33010602011771号