中断下半部包含:软中断、tasklet、任务队列。
参考这篇文章很好:https://www.csdn.net/gather_2a/MtjaUgxsNTAzNi1ibG9n.html
早先的中断的下半部需要(1)在任意一时刻,系统只能有一个CPU可以执行Bottom Half代码,以防止两个或多个CPU同时来执行Bottom Half函数而相互干扰。因此BH代码的执行是严格“串行化”的。(2)BH函数不允许嵌套。这使得CPU被串行化,这对SMP系统是资源的浪费,在内核升级后引入了软中断,整个softirq机制的设计与实现中自始自终都贯彻了一个思想:“谁触发,谁执行”(Who marks,Who runs),也即触发软中断的那个CPU负责执行它所触发的软中断,而且每个CPU都由它自己的软中断触发与控制机制。这个设计思想也使得softirq 机制充分利用了SMP系统的性能和特点。
这样就可以各个CPU核心可以相互独立的处理软中断,不同的核心上可以处理同一个类型的软中断,但是在同一个核心上仍然不能被软中断打断,软中断处理流程也只能被硬中断打断,即使硬中断在返回时除了任何一个软中断, 在软中断执行流程中都不会处理这个后被触发的软中断。
函 数do_softirq()负责执行数组softirq_vec[32]中设置的软中断服务函数。每个CPU都是通过执行这个函数来执行软中断服务的。由 于同一个CPU上的软中断服务例程不允许嵌套,因此,do_softirq()函数一开始就检查当前CPU是否已经正出在中断服务中,如果是则 do_softirq()函数立即返回。举个例子,假设CPU0正在执行do_softirq()函数,执行过程产生了一个高优先级的硬件中断,于是 CPU0转去执行这个高优先级中断所对应的中断服务程序。总所周知,所有的中断服务程序最后都要跳转到do_IRQ()函数并由它来依次执行中断服务队列 中的ISR,这里我们假定这个高优先级中断的ISR请求触发了一次软中断,于是do_IRQ()函数在退出之前看到有软中断请求,从而调用 do_softirq()函数来服务软中断请求。因此,CPU0再次进入do_softirq()函数(也即do_softirq()函数在CPU0上被 重入了)。但是在这一次进入do_softirq()函数时,它马上发现CPU0此前已经处在中断服务状态中了,因此这一次do_softirq()函数 立即返回。于是,CPU0回到该开始时的do_softirq()函数继续执行,并为高优先级中断的ISR所触发的软中断请求补上一次服务。从这里可以看 出,do_softirq()函数在同一个CPU上的执行是串行的。此处是do_softirq()并不是__do_softirq()函数。
所以软中断是可以被硬件中断打断的,打断之后是否还会继续执行该硬中断所调用的软中断那就不一定了,要看是不是同一种类型的软中断。如下文函数中的解释,执行软中断的过程中会加禁止硬中断后读取pending,然后会将pending置0,这样在硬中断强行打断软中断的情况下,如果这个pending为0则不执行软中断处理函数。那么不同类型的软中断呢?应该是可以执行不同类型的软中断的。从__do_softirq中可以看出,硬中断的禁用和开启只加载了pending置0上,在遍历软中断执行函数的时候,h->action,每个软中断的处理函数结束时都会调用__or_softirq_pending宏再将本类型的软中断类型重新设备到本CPU的掩码上,这样一来又得出一个结论,就是高优先级的软中断仍然可以在低优先级软中断类型之前执行,这个真的厉害。比如:
(1)已知TIMER_SOFTIRQ要比NET_RX_SOFTIRQ优先级要高。
(2)__do_softirq已经执行完TIMER_SOFTIRQ的处理函数,并且TIMER_SOFTIRQ重新注册在pending上,此时开始执行NET_RX_SOFTIRQ的处理函数,这是来了一个时钟硬中断(具体怎么触发这个硬中断不了解),则当前这个软中断被打断。
(3)do__softirq这个函数在检查pending时,发现pending不为0,因为TIMER_SOFTIRQ又被重新注册上,此时认证会进入到__do_softirq函数执行软中断,在此时__do_softirq获得的pending上只会有TIMER_SOFTIRQ类型的软中断,那么就会执行这个软中断处理函数,执行完成后将控制权返回给之前的软中断上继续执行NET_RX_SOFTIRQ。注意此时并不过再次执行之前NET_RX_SOFTIRQ的软中断,因为此时的pending上只有TIMER_SOFTIRQ。
(4)从时间上来看后来的TIMER_SOFTIRQ仍然优先于NET_RX_SOFTIRQ执行完成。
至于软中断是执行在进程上下文还是中断上下文我是这么理解的:对于从硬中断中最后调用遍历的软中断流程是在中断上下文执行的,引文在硬中断处理函数栈最后会调用到__do_softirq函数。对于ksoftirq进程中执行的软中断流程是在进程上下文执行的。
主流程函数:
do_softirq->__do_softirq()
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();//获取当前所需要处理的软中断类型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);//把当前cpu的软中断pending置0,这样下次硬中断或者ksoftirq进程
在调用上do_softirq函数会检查当前的pending,为0则不会再执行__do_softirq函数。
实现了单独一个cpu核心执行同一个类型执行一个软中断。
local_irq_enable();//使能硬中断
h = softirq_vec;
while ((softirq_bit = ffs(pending))) {//遍历所有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);
account_irq_exit_time(current);
__local_bh_enable(SOFTIRQ_OFFSET);
WARN_ON_ONCE(in_interrupt());
tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}
pending为注册的软中断类型,以定时器软中断类型为例看看。
先看获取pending的宏:
设置pending的宏:
下面查看定时器的软中断:
定时器的软中断被注册上。
软中断是一组静态定义的下半部接口,有32个,可以在所有处理器同时执行---即使两个类型相同的也可以。
kernel/softirq.c中定义了一个包含有32个该结构体的数组,
static struct softirq_action softirq_vec[NR_SOFTIRQS],NR_SOFTIRQS = 32.
每个被注册的软中断都占据该数组的一项,因此最多可注册32个软中断。
一个软中断必须被标记后才会执行,这被称为触发软中断。通常,中断处理程序会在返回前标记他的软中断,使其在稍后被执行。在下列地方,待处理的软中断会被检查执行:
不管用什么方法最后都是要调用do_softirq函数,这个函数会循环遍历每一个,调用他们的处理程序。
比如,do_IRQ 函数执行完硬件 ISR 后退出时调用此函数。
//
void irq_exit(void)
{
account_system_vtime(current);
trace_hardirq_exit();
sub_preempt_count(IRQ_EXIT_OFFSET);
//
// 判断当前是否有硬件中断嵌套,并且是否有软中断在
// pending 状态,注意:这里只有两个条件同时满足
// 时,才有可能调用 do_softirq() 进入软中断。也就是
// 说确认当前所有硬件中断处理完成,且有硬件中断安装了
// 软中断处理时理时才会进入。
//
if (!in_interrupt() && local_softirq_pending())
//
// 其实这里就是调用 do_softirq() 执行
//
invoke_softirq();
preempt_enable_no_resched();
}
软中断保留给系统中对时间要求最严格以及最重要的下半部使用。目前,只有两个子系统(网络和scsi)直接只用软中断。此外内核定时器和tasklet都是建立在软中断之上。
在编译期间,通过在linux/ineterrupt.h中定义的一个枚举类型来静态的声明软中断。内核用这些从0开始的索引来表示一种相对优先级。索引号小的软中断在索引号打的软中断之前执行。
在运行时通过调用open_softirq()注册软中断处理函数,如网络子系统中注册自己的软中断:
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
我们平台的netif_receive_skb就有net_rx_action这个软中断处理函数来调用。
软中断处理程序执行的时候,允许相应硬件中断,但是他自己不能休眠。在一个处理程序运行的时候,当前处理器上的软中断被禁止。但其他处理器上扔可以执行别的软中断,包括同种类型的软中断,这样就涉及到共享数据和加锁处理。这点很重要,它也是tasklet更受青睐的原因。单纯的禁止你的软中断处理。
引入软中断的主要原因是其可扩展性,如果不需要扩展到多处理器,那么就使用tasklet吧,tasklet本身也是软中断,只不过同一个处理器的多个实例不能再多个处理器上同时运行。
标记一个软中断的函数raise_softirq(NET_RX_SOFTIRQ),追触发NET_RX_SOFTIRQ软中断,他的处理程序net_rx_action()就会在内核下一次执行软中断时投入运行。
在中断处理程序中触发软中断是最常见的形式。在这种情况下,中断处理程序执行硬件设备的相关操作,然后触发相应的软中断,最后退出。内核在执行完中断处理程序以后,马上就会调用do_softirq函数,于是软中断开始执行下半部的工作。
tasklet是利用软中断实现的一种下半部机制,稍后解释tasklet和软中断有什么不同。
tasklet有两类软中断代表,HI_SOFTIRQ和TASKLET_SOFTIRQ,这两个的唯一区别就是软中断执行的先后顺序。
1、tasklet结构体
struct tasklet_struct
{
struct tasklet_struct *next;//将多个tasklet链接成单向循环链表
unsigned long state;//TASKLET_STATE_SCHED(Tasklet is scheduled for execution) TASKLET_STATE_RUN(Tasklet is running (SMP only))
atomic_t count;//0:激活tasklet 非0:禁用tasklet
void (*func)(unsigned long); //用户自定义函数
unsigned long data; //函数入参
};
state 只能在0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值。
2、调度tasklet
已调度的tasklet和标记触发软中断是一个意思,对应的state状态为TASKLET_STATE_SCHED,已调度的软中断会放在两个单处理器数据结构tesklet_vec(普通takslet)和tasklet_hi_vec(高优先级的tasklet)。这两个数据结构都是由tasklet_struct结构体构成的链表,链表中每个tasklet_struct代表一个不同的tasklet,每个cpu都存在这样两个结构,可以查看这两个变量的定义(https://blog.csdn.net/u011006622/article/details/89499707和https://blog.csdn.net/batoom/article/details/47808019)。
tasklet由tasklet_schedule()和tasklet_hi_schedule()函数进行调度。
tasklet_schedule()执行步骤:
void __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags);
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_restore(flags);
}
我们事先定义了一个tasklet,一般都是申请一块内存或者是一个全局变量,调用tasklet_init()初始化这个tasklet,在某个cpu上调用tasklet_schedule(),会把这个变量的地址插入到当前cpu的tasklet_vec链表头上。
由于大部分tasklet和软中断都是在中断处理程序中被调度也就是设置成待处理状态,所以最近一个中断返回的时候看起来就是执行do_softirq的最佳时机,它会执行相应的软中断处理程序,而这两个处理程序tasklet_action和tasklet_hi_action()就是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)
{
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t))
{//不是加锁,而是将tasklet设置为run
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_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();
}
}
static inline int tasklet_trylock(struct tasklet_struct *t)
{
return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}
//这里设置TASKLET_STATE_RUN的作用。因为每个cpu都有个ksoftirqd线程,软中断和ksoftirqd线程存在竞争关系。
a. 在多处理器系统上,检查tasklet的TASKLET_STATE_RUN标志。
1、如果该标志被设置,说明同类型的一个tasklet正在另一个cpu上运行,因此就把任务描述符重新插入到由tasklet_vec[n]
或tasklet_hi_vec[n]指向的链表中,并再次激活TASKLET_SOFTIRQ软中断。这样,当同类型的其他tasklet在其他cpu上运行时,
这个tasklet就被延迟。
2、如果TASKLET_STATE_RUN标志未被设置,tasklet就没有在其他cpu上运行,就需要设置这个标志,一遍tasklet函数不能
在其他cpu上执行。
b. 通过查看tasklet的count,检查tasklet是否被禁止。如果是,就清TASKLET_STATE_RUN标志,并把描述符重新插到tasklet_vec[n]
或tasklet_hi_vec[n]指向的链表中,然后函数再次激活TASKLET_SOFTIRQ软中断。
c. 如果tasklet被激活,清TASKLET_STATE_SCHED标志,并执行tasklet函数
备注:
这个可以这么分析,多个cpu共同调用tasklet_schedule()函数
软中断和tasklet的区别:
tasklet同一个tasklet在不同cpu上运行是错误的,而软中断同种软中断可以在不同cpu上运行。
每个处理器都有一个辅助处理软中断的内核线程。当内核中出现大量软中断的时候,这些内核进程就会辅助处理他们。
对于软中断的处理,内核会选取合适的时机处理,而在中断处理程序返回时是最常见的,这种情况下当有大量流量进设备时软中断被触发的频率可能很高。更不利的是,处理函数有时还会自行重复出发,这样会导致cpu一直被占用,导致用户空间进程无法获得足够的处理器时间,因此处饥饿状态。
为改进这种情况,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载。这些线程在最低的优先级上运行(nice值是19),这能避免他们跟其他重要的任务抢夺cpu,但他们最终肯定会被执行。
每个处理器都有这样一个线程,所有线程的名字都叫做ksoftirq/n,区别在于n,他对应的处理器的编号。在一个双cpu的机器上就有两个这样的线程,分别叫做ksoftirqd/0和ksoftirqd/1,为了保证只要有空闲的处理器,他们就会处理软中断,所以给每个处理器都分配一个这样的线程。一旦线程被初始化,他就会执行下面这样的死循环:
在中
static int ksoftirqd(void * __bind_cpu)
{
set_user_nice(current, 19);
current->flags |= PF_NOFREEZE;
set_current_state(TASK_INTERRUPTIBLE);
while (!kthread_should_stop()) {
preempt_disable();
if (!local_softirq_pending()) {
preempt_enable_no_resched();
schedule();
preempt_disable();
}
__set_current_state(TASK_RUNNING);
while (local_softirq_pending()) {
/* Preempt disable stops cpu going offline.
If already offline, we'll be on wrong CPU:
don't process */
if (cpu_is_offline((long)__bind_cpu))
goto wait_to_die;
do_softirq(); //调用软中断下半部处理函数
preempt_enable_no_resched();
cond_resched();
preempt_disable();
}
preempt_enable();
set_current_state(TASK_INTERRUPTIBLE);
}
__set_current_state(TASK_RUNNING);
return 0;
wait_to_die:
preempt_enable();
/* Wait for kthread_stop */
set_current_state(TASK_INTERRUPTIBLE);
while (!kthread_should_stop()) {
schedule();
set_current_state(TASK_INTERRUPTIBLE);
}
__set_current_state(TASK_RUNNING);
return 0;
}
从这里可以看出,这个ksoftirqd线程是可以被调度的,同样软中断也不是在中断的上下文中,软中断也是可以调度的。
,