OS中interrupt handler的运行和普通的process或者kernel thread不同,它有很多的限制:
1. interrupt handler和其他的code异步运行,并且可能会抢占别的code,比如kernel中比较重要的code,或者其他的interrupt handler,因此interrupt handler必须要尽快结束。
2. interrupt handler在运行时,通常会关闭当前CPU上对应的IRQ line,甚至会关闭当前CPU上所有的IRQ line,因此会影响其他的device和OS交互,所以interrupt handler必须要尽快结束,打开中断。
3. interrupt handler因为要和hardware交互,因此必须尽可能快的被调度。
4. interrupt handler运行在interrupt context,不是process context,因此interrupt handler不能被block,这就限制了interrupt handler中能够使用的函数。
因为上面的这些限制,interrupt handler是经过特殊设计的,只有那些对时间比较敏感的操作(比如hardware产生了数据,需要尽快的copy到memory),会放在interrupt handler中处理,其他对时间不敏感的操作适当延后执行。因此整个对interrupt的处理,其实就分成了两个部分:上半部和下半部。
Bottom Halves
下半部是中断处理的主要部分,因为上面提到的interrupt handler的要求,因此大部分工作都会放到下半部来执行,这样上半部可以尽快的返回。
但是具体哪些工作可以放到下半部,哪些不行,并没有统一的规则,这里提供了部分建议:
1. 如果某些工作对时间非常敏感,必须马上处理,那么就放在上半部做;
2. 如果某些工作和hardware直接相关,那么就放在上半部做;
3. 如果某些工作要求不能同样的中断抢占,那么放在上半部做(因为上半部是关掉这个中断的);
4. 其他的工作,考虑放到下半部来做。
Why Bottom Halves?
最主要的原因,还是因为上半部对时间敏感,只能尽可能少的做事情,把大部分工作交给下半部来做。并且在将来的某个时刻,调度下半部,并且这个时刻是无法指定的,而是由kernel自己决定合适调用下半部,通常情况下,下半部都是在上半部执行完后立即被调用。
需要注意的是,下半部执行的时候,中断都是打开的状态。
A World of Bottom Halves
和上半部只能用interrupt handler实现不同,下半部可以有多种方式来实现。
The Original “Bottom Half”
在kernel早起的版本中,下半部是用的BH interface,这些interface在现在已经不用了。当时的实现,是有一个global的静态创建的32个下半部的list,上半部通过设置32bit中的一个,来决定哪个下半部执行,并且在系统中,同一个时间只能有一个下半部执行。
Task Queues
之后kernel引入了task queue,每个queue里都有一个下半部的list,device driver可以把自己的下半部注册到其中的一个task queue中去,然后有kernel loop去调用其中的下半部函数。这样的问题显而易见,performance不够。
Softirqs and Tasklets
再之后,kernel引入了softirq和tasklet。softirq是一组静态定义好的下半部,这些softirq可以并发的在多个CPU上同时运行,即便是两个相同类型的softirq也可以;tasklet是基于softirq做了实现,两个不同类型的tasklet可以并发的在多个CPU上执行互不影响,但是同样类型的tasklet不能同时运行,这样既能保证tasklet的performance,同时也降低了使用的复杂度。对于绝大多数的下半部来说,tasklet已经足够了,如果对performance有 更高的要求,可以考虑使用softirq,但是softirq只能静态的创建,也就说module driver是无法使用的,而tasklet可以在后续动态的创建和使用。
Softirqs
这里先介绍一下softirq,虽然对于device driver而言用的极少,但是因为tasklet是基于softirq来实现,tasklet用的非常多,所以我们先简单看一下softirq的实现。
Implementing Softirqs
softirq是编译阶段静态创建的,不像tasklet,你不能动态的创建和注册tasklet。softirq使用结构体softirq_action来表示,定义在include/linux/interrupt.h中:
/* 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,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
numbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
struct softirq_action
{
void (*action)(struct softirq_action *);
};
在kernel/softirq.c中,定义了一个softirq的数组:
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
The Softirq Handler
softirq handler的原型是这样的:
void softirq_handler(struct softirq_action *);
//调用
my_softirq->action(my_softirq);
当kernel执行softirq时,就会调用action函数指针,action函数指针就指向一个softirq handler,传递的参数只有一个,就是softirq_action本身。一个softirq不会抢占另外一个softirq,只有interrupt handler会抢占softirq。
Executing Softirqs
通过NR_SOFTIRQS的定义,我们可以看到系统中会存在很多的softirq类型,每个softirq类型是否会被调用,取决于它是否被marked。通常来说,在interrupt handler返回之前,会把需要调用的softirq mark,这个过程称为raising softirq。这样在将来的某个时刻,kernel检查到某个softirq被mark,就会调用它。检查并调用的过程发生在:
1. 在hardware interrupt code返回时。(也就是interrupt handler执行结束,要返回时)
2. 在ksoftirqd线程中。
3. 其他显示指明要检查并调用相应softirq的code中,比如network子系统。
如何在何处检查和调用,最终都会使用do_softirq来执行softirq:
asmlinkage __visible void do_softirq(void)
{
__u32 pending;
unsigned long flags;
if (in_interrupt())
return;
local_irq_save(flags);
pending = local_softirq_pending();
if (pending && !ksoftirqd_running())
do_softirq_own_stack();
local_irq_restore(flags);
}
注意,这里的code是kernel 4.15中的code,和书中使用的code完全不同,是因为4.15中主要通过ksoftirqd线程来做了,从do_softirq的实现也可以看到,如果没有打开ksoftirqd线程,do_softirqd才会自己动手。我们来看ksoftirq:
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
};
可以看到,ksoftirqd的线程函数是run_ksoftirqd,我们看一下实现:
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();
return;
}
local_irq_enable();
}
也很简单,直接调用了__do_softirq函数:
asmlinkage __visible void __softirq_entry __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();
account_irq_enter_time(current);
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
in_hardirq = lockdep_softirq_start();
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);
local_irq_enable();
h = softirq_vec;
while ((softirq_bit = ffs(pending))) {
unsigned int vec_nr;
int prev_count;
h += softirq_bit - 1;
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++;
pending >>= softirq_bit;
}
rcu_bh_qs();
local_irq_disable();
pending = local_softirq_pending();
if (pending) {
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
wakeup_softirqd();
}
lockdep_softirq_end(in_hardirq);
}
这个函数有些长,但是其中的主要逻辑是while循环,通过ffs找到第一个被mark的softirq,然后进入循环,通过调用h->action就调用了对应的softirq函数。整个逻辑都比较简单和清晰,这里也就不赘述了。
Using Softirqs
目前kernel中直接使用softkirq的只有network和block device,其他的device driver理论上都不需要直接使用softirq,使用tasklet就足够了。但是这里还是将一些如何直接使用softirq。
Assigning an Index
要实现自己的softirq,要先增加一个softirq index,也是上面我们看到的NR_SOFTIRQ枚举变量,值越小,就越早被调用。当增加了softirq index以后,就需要向对应的softirq index的entry中注册自己的softirq handler。kernel中存在softirq index以及注释:
Registering Your Handler
在运行时,注册自己的softirq handler,接口如下:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
接收两个参数,第一个softirq index,第二个参数是自己的softirq handler。
在softirq handler运行时,中断是打开的,并且不能睡眠。而且,当前CPU的softirq是关闭的,另外的CPU不受影响,可以任意执行别的softirq。如果某个CPU上正在处理的softirq,又发生了raising,那么另外的CPU可能同时会执行同样的softirq!也就说同样的code在多个CPU上同时执行,所以如果其中使用了全局变量,那么要做好保护,如果handler中真的使用了全局变量,那么可以考虑使用tasklet了,不要使用softirq。一般来说,kernel中使用的softirq都只是访问per CPU的data,也就是多个CPU不会访问同样的变量,也就不用加锁。
Raising Your Softirq
当softirq handler注册完成以后,也就是已经加到了enum list,并且调用了open_softirq,那么softirq handler就可以运行了。在运行之前,需要raise softirq,也就是mark这个softirq,这样当下一次调用do_softirq的时候就能够调用这个softirq handler了。mark一个softirq使用的接口是raise_softirq(),例如如果是network,那么就是:
raise_softirq(NET_TX_SOFTIRQ);
参数就是softirq的index,通过raise_softirq就可以raise一个softirq,当下次kernel执行softirq的时候,就会执行network的softirq handler。我们看一下raise_softirq的实现:
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
可以看到,在真正raise这个softirq之前,需要关闭中断,因为softirq是global的,要做好保护,防止当前CPU在mark softirq的时候,别的CPU也进来。如果在raise_softirq之前,中断已经是关闭的状态,可以直接调用raise_softirq_irqoff:
/*
* This function must run with irqs disabled!
*/
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();
}
可以看到,raise_softirq_irqoff的时候,只是调用了__raise_softirq_irqoff,然后check是否在interrupt context中,如果不是,就wakeup ksoftirqd线程干活。我们看一下__raise_softirq_irqoff的实现:
void __raise_softirq_irqoff(unsigned int nr)
{
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}
可以看到这里只是设置了bitmask,所以说,所谓的raise softirq,只不过是设置bitmask,表明这个softirq的handler需要被调用。
softirq通常是在interrupt handler中调用,当interrupt handler处理完和hardware相关的工作,raise softirq之后,就会退出;当kernel在某个时刻调用do_softirq的时候,就会检查哪些softirq是被mark的,然后调用对应的handler。
Tasklets
tasklet和task没有半毛钱关系。tasklet基于softirq来实现,是中断处理的下半部,但是使用起来要比softirq更方便一些。对于device driver而言,应当使用tasklet,不使用softirq,因为softirq需要修改kernel code,而且主要适用于调用频率比较高,并且高度并行化的场景。
Implementing Tasklets
tasklet是基于softirq实现,也就说,它就是其中的一个softirq。tasklet的softirq有两种:HI_SOFTIRQ和TASKLET_SOFTIRQ,二者的区别在于HI_SOFTIRQ比TASKLET_SOFTIRQ更早被调用。
The Tasklet Structure
tasklet使用数据结构tasklet_struct来表示,一个tasklet_struct表示一个tasklet。定义在include/linux/interrupt.h中:
/* Tasklets --- multithreaded analogue of BHs.
Main feature differing them of generic softirqs: tasklet
is running only on one CPU simultaneously.
Main feature differing them of BHs: different tasklets
may be run simultaneously on different CPUs.
Properties:
* If tasklet_schedule() is called, then tasklet is guaranteed
to be executed on some cpu at least once after this.
* If the tasklet is already scheduled, but its execution is still not
started, it will be executed only once.
* If this tasklet is already running on another CPU (or schedule is called
from tasklet itself), it is rescheduled for later.
* Tasklet is strictly serialized wrt itself, but not
wrt another tasklets. If client needs some intertask synchronization,
he makes it with spinlocks.
*/
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
其中的func就是tasklet的handler,也就是softirq中的handler,data就是调用func时传递给func的参数。state是tasklet的状态,有以下几种值:0, TASKLET_STATE_SCHED, TASKLET_STATE_RUN——TASKLET_STATE_SCHED表示tasklet已经被调度,准备运行;TASKLET_STATE_RUN表示tasklet正在运行。count是tasklet的reference counter,如果不是0,说明tasklet被disable,不能被运行;如果是0,表示可以运行。
Scheduling Tasklets
被调度的tasklet存储在两个per CPU的数据结构中,这两个数据结构分别是:tasklet_vec和tasklet_hi_vec。这两个数据结构都是list,里面是等待被执行的tasklet,tasklet_vec中对应的是TASKLET_SOFTIRQ,tasklet_hi_vec对应的是HI_SOFTIRQ。前者通过tasklet_schedule来调度,后者通过tasklet_hi_schedule来调度,这两个函数的参数都是tasklet_struct的指针,并且功能类似,下面我们以tasklet_schedule为例看一下kernel的实现(仍然基于kernel 4.15):
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
if里,对tasklet的state进行检查,如果发现之前有设置过TASKLET_STATE_SCHED,直接return。如果之前的state是0,或者是TASKLET_STATE_RUN,就会调用__tasklet_schedule,再看一下它的实现:
void __tasklet_schedule(struct tasklet_struct *t)
{
__tasklet_schedule_common(t, &tasklet_vec,
TASKLET_SOFTIRQ);
}
直接调用了__tasklet_schedule_common:
/*
* Tasklets
*/
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);
static void __tasklet_schedule_common(struct tasklet_struct *t,
struct tasklet_head __percpu *headp,
unsigned int softirq_nr)
{
struct tasklet_head *head;
unsigned long flags;
local_irq_save(flags);
head = this_cpu_ptr(headp);
t->next = NULL;
*head->tail = t;
head->tail = &(t->next);
raise_softirq_irqoff(softirq_nr);
local_irq_restore(flags);
}
到这里就很清楚,tasklet_schedule(tasklet_hi_schedule)最终调用了__tasklet_schedule_common,这个函数做的主要工作,就是把tasklet_struct加入到per CPU的tasklet链表中去,普通的tasklet添加到tasklet_vec中,高优先级的tasklet添加到tasklet_hi_vec中,然后调用raise_softirq_irqoff,这里面的实现在之前的章节已经讲过了,这里不再赘述。
所有的softirq执行的时机都是在hardware interrupt handler执行完以后(也就是最后一个interrupt handler返回时),就会调用softirq,优先级从高到底,检查它的mark,如何被raise过,就调用对应的handler。那么tasklet的softirq是如何调用的呢?我们看code:
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);
}
softirq_init是softirq初始化时执行的函数,从这里我们可以看到tasklet的两个softirq分别注册了自己的handler:tasklet_action和tasklet_hi_action。之前的章节中,我们分析了do_softirq是如何遍历所有的softirq,然后执行其中的action,当发现TASKLET_SOFTIRQ或者HI_SOFTIRQ被raise之后,对应的action就会被调用。我们看一下tasklet_action:
static __latent_entropy void tasklet_action(struct softirq_action *a)
{
tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ);
}
直接调用了tasklet_action_common:
static void tasklet_action_common(struct softirq_action *a,
struct tasklet_head *tl_head,
unsigned int softirq_nr)
{
struct tasklet_struct *list;
local_irq_disable();
list = tl_head->head;
tl_head->head = NULL;
tl_head->tail = &tl_head->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;
*tl_head->tail = t;
tl_head->tail = &t->next;
__raise_softirq_irqoff(softirq_nr);
local_irq_enable();
}
}
可以看到,tasklet_action_common主要逻辑也很简单,loop了一遍tasklet_vec,然后调用其中每个tasklet的func。函数刚进来时,先关闭了中断(因为softirq只有在interrupt handler完以后立即执行,所以此时interrupt一定是enable的),然后清空了tasklet_vec(这样tasklet_vec可以尽快被再次使用),然后打开了中断。while循环中,首先拿tasklet的锁(这里的锁其实就是设置TASKLET_STATE_RUN),这里就说明了tasklet只能在一个CPU上执行,不会同时在多个CPU上执行(因为加锁互斥了),拿到锁以后,如果count为0(说明没有被disable),就清掉TASKLET_STATE_SCHED标记,然后调用tasklet的handler,执行完以后解锁tasklet(这里的解锁其实就清TASKLET_STATE_RUN bit),再接着执行下一个。
Using Tasklets
无论什么时候,使用tasklet作为中断的下半部都合适不过。这里就介绍以下tasklet如何在device driver中使用。
Declaring Your Tasklet
首先,在使用之前,你要先定义自己的tasklet实例:
DECLARE_TASKLET(name, func, data)
DECLARE_TASKLET_DISABLED(name, func, data);
上面这种是静态的方式创建自己的tasklet。也就创建一个名字为name的tasklet,handler是func,data是data。这两个接口的区别在于第一个tasklet的count是0,也就是tasklet是enable的状态,第二个接口创建的tasklet count是1,tasklet是disable的状态。比如:
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, dev);
等同于
struct tasklet_struct my_tasklet = {
NULL,
0,
ATOMIC_INIT(0),
my_tasklet_handler,
dev };
也可以动态的创建初始化一个tasklet:
tasklet_init(t, tasklet_handler, dev); /* dynamically as opposed to statically */
Writing Your Tasklet Handler
按照tasklet handler的原型定义自己的handler即可,需要注意的是handler中调用的函数有讲究。原型:
void tasklet_handler(unsigned long data)
在softirq中不能sleep,tasklet中自然也不能sleep,因此像semaphore或者其他会导致block的函数就不能调用。tasklet的handler执行时,所有的中断都是打开的,这一点也要注意,因为你的tasklet handler有可能会被interrupt handler抢占,如果你们有share的resource,此时就要注意加锁保护。另外,同样类型的tasklet不会同时在不同的CPU上运行,这一点和softirq是不一样的,softirq是完全并行的,即便是相同类型的softirq,也可能在多个CPU上执行,但是不同类型的tasklet会并行运行,因此如果你的tasklet handler和别的tasklet handler或者softirq有share resource,此时要注意加锁保护。
Scheduling Your Tasklet
当自己的tasklet需要调度时,就调用tasklet_schedule:
tasklet_schedule(&my_tasklet); /* mark my_tasklet as pending */
当调用了tasklet_schedule以后,tasklet在将来的某个时刻就会被调用一次。如果同一个tasklet被schedule多次:如果这个tasklet的state是TASKLET_STATE_SCHED,也就之前被调度过,但是还没有执行,那么tasklet只会被调用一次;如果tasklet的state是TASKLET_STATE_RUN,说明这个tasklet此时正在某个CPU上执行,那么tasklet的state就会被设置上TASKLET_STATE_SCHED,等下次softirq执行时,tasklet就会被再次调度。tasklet在执行时,会和schedule它的在同一个CPU上执行,回忆以下,当调用tasklet_schedule的时候,tasklet是不是被添加到一个per CPU的tasklet list?这样的好处可以充分利用CPU的cache。
可以使用tasklet_disable来关闭某个tasklet,如果此时这个tasklet正在某个CPU上执行,那么会等到tasklet执行完才会真正disable它。如果使用tasklet_disable_nosync,就直接关闭tasklet,不会等它在某个CPU上执行完才disable 它。我们看一下code:
static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
atomic_inc(&t->count);
smp_mb__after_atomic();
}
static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t);
tasklet_unlock_wait(t);
smp_mb();
}
可以看到tasklet_disable和tasklet_disable_nosync相比,多做了两个事情:tasklet_unlock_wait和smp_mb。我们看一下tasklet_unlock_wait做了啥:
static inline void tasklet_unlock_wait(struct tasklet_struct *t)
{
while (test_bit(TASKLET_STATE_RUN, &(t)->state)) { barrier(); }
}
可以看到是一个while循环,一直等到tasklet的state中TASKLET_STATE_RUN被清掉才会终止。
如果想要enable一个tasklet,就需要调用tasklet_enable。
static inline void tasklet_enable(struct tasklet_struct *t)
{
smp_mb__before_atomic();
atomic_dec(&t->count);
}
tasklet_enable只是减小了tasklet的count,这就说明disable和enable要配对使用,而且disable了多少次,就要enable多少次。
如果想要把tasklet从queue中移除,也就说不想再继续schedule这个tasklet,那么可以使用接口tasklet_kill:
void tasklet_kill(struct tasklet_struct *t)
{
if (in_interrupt())
pr_notice("Attempt to kill tasklet from interrupt\n");
while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
do {
yield();
} while (test_bit(TASKLET_STATE_SCHED, &t->state));
}
tasklet_unlock_wait(t);
clear_bit(TASKLET_STATE_SCHED, &t->state);
}
这个while循环的作用,是等待tasklet的状态由TASKLET_STATE_SCHED变为TASKLET_STATE_RUN。第一个while的条件是当tasklet此时的状态是TASKLET_STATE_SCHED为true,此时进入循环,内层的循环一直检查TASKLET_STATE_SCHED是否被清掉,如果没有,调用yield放弃CPU,等到TASKLET_STATE_SCHED被清掉,两层循环都会结束;tasklet_unlock_wait中则会等待TASKLET_STATE_RUN这个标被清掉,也就是等待tasklet执行完毕,当执行完以后,tasklet的state被清为0,表示不会被调度。
可以看到这个函数是会进入休眠的,所以不能在tasklet中调用它。
ksoftirqd
softirq的处理需要ksoftirqd线程的参与,这些线程会在系统中softirq比较多的时候协助处理softirq。我们知道,softirq的处理入口主要是在interrupt handler处理完成之后,紧接着就会开始处理softirq,如果系统中的softirq产生的速度特别快,而且有些softirq在处理时,会再次把自己raise,这样之后会再次调用它的handler,碰到这样的场景,那么就会有一些问题。如果让CPU一直处理这些新产生的softirq,那么user application可能就会饥饿;而如果延迟处理,让他们等待下次interrupt时再处理,有可能会碰到系统没有硬件产生中断,而系统又是idle的状态,这样之前产生的softirq反而得不到执行。
最后实现的方法是引入ksoftirqd线程,ksoftirq线程每个CPU各有一个,并且优先级设置的极低,主要的目的就是防止突然增多的softirq对系统的performance,尤其是其他比较重要的code产生影响,通过kthread的方式来调度,可以达到均衡。并且当系统是idle的时候,softirq也能够被及时的处理。
我们看一下ksoftirqd的实现(执行函数):
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();
return;
}
local_irq_enable();
}
在ksoftirqd处理softirq时,要关闭当前CPU的中断,防止正在处理softirq时被抢占,然后check是否有pending的softirq,如果有调用__do_softirq开始干活,干完活以后打开中断,调用cond_resched再返回。cond_resched先不看,这个只有系统配置为禁止抢占的情况下才有实现。__do_softirq上面也已经看过,这里不再赘述。
另外,其实interrupt context直接处理softirq时,如果刚处理完发现又有新的pending,interrupt context会:
1. 最多循环处理2ms
2. 最多循环MAX_SOFTIRQ_RESTART次。(MAX_SOFTIRQ_RESTART在4.15中是10)
通过这两个条件,可以保证,某些高频率的softirq可以尽快的被处理一部分,并且无论如何,都不会block interrupt context太久。
通过这样的约束,结合ksoftirqd的实现,就能做到softirq的高效处理。
这里有个疑惑需要研究研究:按照ksoftirqd实现,它就是一个普通的kthread,那就说,某些softirq(或者tasklet)可能会在kthread的上下文中执行,kthread的context其实就是个process context啊(有task_struct,但是没有mm),那就意味着softirq handler可能运行在process context中,而不是interrupt context中。
Work Queues
work queue和我们之前看到的所有bottom half都不一样,因为它是运行在process context中,也就是说它可以sleep,可以等I/O,可以分配memory,可以等待semaphore等等,使用work queue对下半部的实现几乎没有要求,kernel的绝大部分函数都能调用。
work queue也需要kernel来调度,并且调度的时间是不确定的。从这些方便看,使用work queue完全可以使用自己创建的kthread来代替,是的,kernel的实现也是通过per CPU的work thread来实现的,不过从device driver的角度看,直接使用work queue可以免去创建kthread和维护它的麻烦,稍微方便一些。(其实也不尽然 :( )
Implementing Work Queues
关于kernel的work queue的实现,这里又做了解释,默认情况下每个CPU都有一个kworker的kthread,名字为kworker/0:0,类似与这种的。这些kworker都从当前CPU的work queue里读取work struct,然后执行。这里没啥好说的。不过kworker的名字有点奇怪:
if (pool->cpu >= 0)
snprintf(id_buf, sizeof(id_buf), "%d:%d%s", pool->cpu, id,
pool->attrs->nice < 0 ? "H" : "");
else
snprintf(id_buf, sizeof(id_buf), "u%d:%d", pool->id, id);
worker->task = kthread_create_on_node(worker_thread, worker, pool->node,
"kworker/%s", id_buf);
从系统中看到的kworker不止一个,而是多个,但是有些nice值是不一样的,看上去优先级不同,应该是有不同的作用和目的。
Data Structures Representing the Threads
kernel的worker thread用数据结构workqueue_struct来表示:
/*
* The externally visible workqueue. It relays the issued work items to
* the appropriate worker_pool through its pool_workqueues.
*/
struct workqueue_struct {
struct list_head pwqs; /* WR: all pwqs of this wq */
struct list_head list; /* PR: list of all workqueues */
struct mutex mutex; /* protects this wq */
int work_color; /* WQ: current work color */
int flush_color; /* WQ: current flush color */
atomic_t nr_pwqs_to_flush; /* flush in progress */
struct wq_flusher *first_flusher; /* WQ: first flusher */
struct list_head flusher_queue; /* WQ: flush waiters */
struct list_head flusher_overflow; /* WQ: flush overflow list */
struct list_head maydays; /* MD: pwqs requesting rescue */
struct worker *rescuer; /* I: rescue worker */
int nr_drainers; /* WQ: drain in progress */
int saved_max_active; /* WQ: saved pwq max_active */
struct workqueue_attrs *unbound_attrs; /* PW: only for unbound wqs */
struct pool_workqueue *dfl_pwq; /* PW: only for unbound wqs */
#ifdef CONFIG_SYSFS
struct wq_device *wq_dev; /* I: for sysfs interface */
#endif
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
char name[WQ_NAME_LEN]; /* I: workqueue name */
/*
* Destruction of workqueue_struct is sched-RCU protected to allow
* walking the workqueues list without grabbing wq_pool_mutex.
* This is used to dump all workqueues from sysrq.
*/
struct rcu_head rcu;
/* hot fields used during command issue, aligned to cacheline */
unsigned int flags ____cacheline_aligned; /* WQ: WQ_* flags */
struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */
struct pool_workqueue __rcu *numa_pwq_tbl[]; /* PWR: unbound pwqs indexed by node */
};
这个workqueue_struct就代表一个CPU中的worker thread,每个CPU上有一个。其中最重要的结构体是pool_workqueue:
/*
* The per-pool workqueue. While queued, the lower WORK_STRUCT_FLAG_BITS
* of work_struct->data are used for flags and the remaining high bits
* point to the pwq; thus, pwqs need to be aligned at two's power of the
* number of flag bits.
*/
struct pool_workqueue {
struct worker_pool *pool; /* I: the associated pool */
struct workqueue_struct *wq; /* I: the owning workqueue */
int work_color; /* L: current color */
int flush_color; /* L: flushing color */
int refcnt; /* L: reference count */
int nr_in_flight[WORK_NR_COLORS];
/* L: nr of in_flight works */
int nr_active; /* L: nr of active works */
int max_active; /* L: max active works */
struct list_head delayed_works; /* L: delayed works */
struct list_head pwqs_node; /* WR: node on wq->pwqs */
struct list_head mayday_node; /* MD: node on wq->maydays */
/*
* Release of unbound pwq is punted to system_wq. See put_pwq()
* and pwq_unbound_release_workfn() for details. pool_workqueue
* itself is also sched-RCU protected so that the first pwq can be
* determined without grabbing wq->mutex.
*/
struct work_struct unbound_release_work;
struct rcu_head rcu;
} __aligned(1 << WORK_STRUCT_FLAG_BITS);
pool_workqueue中有一个pool,里面记录各种各样待执行的work struct。一个worker thread对应一个workqueue_struct,一个workqueue_struct对应一个CPU。(但是一个workqueue_struct应该对应多个worker thread?)
Data Structures Representing the Work
上面说过,一个workqueue_struct在每个CPU上都对应一个worker thread,这个worker thread主要的工作就是检查是否有pending的work_struct,如果有就执行,如果没有就sleep,worker thread的执行函数是worker_thread:
static int worker_thread(void *__worker)
{
struct worker *worker = __worker;
struct worker_pool *pool = worker->pool;
/* tell the scheduler that this is a workqueue worker */
worker->task->flags |= PF_WQ_WORKER;
woke_up:
spin_lock_irq(&pool->lock);
worker_leave_idle(worker);
recheck:
/* no more worker necessary? */
if (!need_more_worker(pool))
goto sleep;
/* do we need to manage? */
if (unlikely(!may_start_working(pool)) && manage_workers(worker))
goto recheck;
do {
struct work_struct *work =
list_first_entry(&pool->worklist,
struct work_struct, entry);
pool->watchdog_ts = jiffies;
if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED))) {
/* optimization path, not strictly necessary */
process_one_work(worker, work);
if (unlikely(!list_empty(&worker->scheduled)))
process_scheduled_works(worker);
} else {
move_linked_works(work, &worker->scheduled, NULL);
process_scheduled_works(worker);
}
} while (keep_working(pool));
worker_set_flags(worker, WORKER_PREP);
sleep:
worker_enter_idle(worker);
__set_current_state(TASK_IDLE);
spin_unlock_irq(&pool->lock);
schedule();
goto woke_up;
}
worker_thread中的主要逻辑就是查询当前worker_pool中是否有待执行的work_struct,如果有就执行,如果没有就调用schedule让kernel调度别的process。
除了这个worker_thread之外,每个单独的调度单位是work_struct:
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
所有的work_struct都会被放到某个worker thread中的struct list,当worker thread被唤醒,就开始从struct list中执行work_struct,执行一个从list中就删除一个,直到所有的work_struct都被执行完毕。
worker_thread中执行单个work_struct的函数是process_one_work,其中就直接调用了work->func,也就是work_struct中设定好的function。
Work Queue Implementation Summary
上面这个图是书中的,kernel 4.15中的work queue 实现和书中的描述有些变化,这里也讲一下kernel 4.15目前的实现。首先,基本流程和大部分数据结构是类似的,只是略有变化。
workqueue_struct的意义没有变化,一个workqueue_struct仍然代表一个work queue,这个work queue可以由device driver自己创建,kernel中自己已经创建了一个global的work queue,当device driver不指定work queue时,就会使用这个kernel自己创建的。当device driver创建work queue时,kernel首先会创建一个work queue结构体,这一个结构体就包含了多个pool_workqueue(看结构体定义是一个pool_workqueue,但是这是per cpu的,所以你懂的),这个pool_workqueue中又有一个worker_pool,这个worker_pool中又有一个worklist,这个worklist才是真正存放work_struct的地方(好多层。。。)。
Using Work Queues
work queue使用起来比较简单。
Creating Work
可以静态创建,也可以动态创建:
//静态创建
DECLARE_WORK(name, void (*func)(void *), void *data);
//动态创建
INIT_WORK(struct work_struct *work, void (*func)(void *), void *data);
Your Work Queue Handler
work struct的handler原型如下:
void work_handler(void *data)
当work struct被调用时,work queue对应的某个CPU上的thread就会调用work_handler,因此work_handler是运行在process context上的。
Scheduling Work
当work struct创建出来以后,就可以schedule它了,接口很简单:
schedule_work(&work);
schedule_work会把work_struct放到当前CPU对应的work queue中(workqueue_struct -> pool_workqueue -> worker_pool),当对应的worker thread被唤醒时就会执行它。
可以指定delay,在将来的某个时刻才执行这个work_struct:
schedule_delayed_work(&work, delay);
Flushing Work
work_struct被schedule以后就会执行,但是有时候需要我们手动flush work_struct,以保证我们继续后续的操作之前,所有的work_struct都被执行了。
void flush_scheduled_work(void);
上面的接口就可以显示flush work queue(kernel自己的work queue)。你也可以flush某个特定的work queue:
void flush_workqueue(struct workqueue_struct *wq);
flush会等待所有work struct都执行才会返回,在等待的时候会sleep,因此只能在process context中调用。要注意的是,flush不会等待delay的work struct,因为等待的时间可能会很长,此时只能cancel delay 的work struct:
int cancel_delayed_work(struct work_struct *work);
Creating New Work Queues
如果kernel自己的work queue无法满足需求(尤其是performance),device driver可以选择自己创建work queue:
struct workqueue_struct *create_workqueue(const char *name);
name用来设置worker thread的名字。create_workqueue会在每个CPU上各创建一个worker thread,当使用device driver创建的work queue时,使用它的接口也会发生变化:
static inline bool queue_work(struct workqueue_struct *wq,
struct work_struct *work)
{
return queue_work_on(WORK_CPU_UNBOUND, wq, work);
}
static inline bool queue_delayed_work(struct workqueue_struct *wq,
struct delayed_work *dwork,
unsigned long delay)
{
return queue_delayed_work_on(WORK_CPU_UNBOUND, wq, dwork, delay);
}
flush特定的work queue:
void flush_workqueue(struct workqueue_struct *wq);
Which Bottom Half Should I Use?
到目前位置,实现中断下半部的方式我们讲了三种:softirq,tasklet,work struct。其中tasklet是基于softirq的,因此极其相似;work struct是完全不同的实现,所以下半部严格来说只有两种。
选择哪一种下半部,取决于driver的需求,根据driver功能的不同,或者不同的特点,选择合适的下半部。
softirq,从它本身的性质看,比较适合高度并行化的工作,因为相同type的softirq可以同时运行在多个CPU上,高度并行化的工作可以充分利用softirq的这种性质;如果涉及到共享的数据,那就需要加锁保护,破坏了softirq并行化的机制。
tasklet,基于softirq,但是和softirq又有不同,最大的不同点在于相同的tasklet不支持同时运行在多个CPU上,这样对于device driver而言更加适合,因为tasklet handler往往需要处理数据,串行执行,可以保证driver在handler中不用加锁。
如果下半部只能运行在process context中,那么work queue就是唯一的选择了。work queue相较于softirq和tasklet来说,因为使用了kthread,所以涉及到context switch,导致性能不如softirq和tasklet。
总结来说,driver绝大部分情况只需要在tasklet和work queue之间做出选择:如果下半部需要在将来的某个时刻调用,或者其中会导致sleep,就使用work queue,否则使用tasklet。
Locking Between the Bottom Halves
tasklet中互斥访问的实现比较简单,因为kernel本身保证了同样的tasklet不会在多个CPU上执行,但是如果不同类型的tasklet之间有share 数据,这些数据需要加锁保护;
softirq是高并发的,即便同样类型的softirq,也会在多个CPU上执行,因此凡是softirq中有share数据的情况,都必须加锁保护;
如果process context code和bottom half code有share数据,就需要关闭bottom half,并且在访问share data之前加锁保护,这样可以防止local CPU和SMP上的数据保护,并且防止死锁。
如果interrupt context code和bottom half code有share数据,就需要关闭中断并且访问share data之前加锁保护,这样可以防止local CPU和SMP上的数据保护,并且防止死锁。
在work queue中的共享数据也需要加锁保护,work queue因为是运行在kernel thread中,因此它的锁机制和process context没有区别。
Disabling Bottom Halves
通常情况下,只关闭下半部是不够的,还要加锁。如果你是在kernel core code中,一般只要关闭下半部就够了。调用local_bh_disable可以关闭所有的中断下半部,包括softirq和tasklet,然后使用local_bh_enable来打开中断下半部。
disable和enable可以嵌套调用,disable了多少次就要enable多少次才可以。
static inline void local_bh_disable(void)
{
__local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}
static inline void local_bh_enable(void)
{
__local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}
注意,这个并不影响work queue的执行,因为它是在process context中执行的。在enable的时候,如果发现有pending的bottom half就会开始执行:
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();
}