内核抢占和中断
一 综述
抢占是内核对进程的管理:当高优先级的任务因中断而成为就绪,特定的低优先级进程将让出CPU,而那个高优先级的进程得到CPU。 可以这样(简单地)说,中断是因为硬件,而抢占是因为中断带来特定事件的发生。
二 先导知识
2.1 上下文
一般来说,CPU在任何时刻都处于以下三种情况之一:
(1) 运行于用户空间,执行用户进程;
(2) 运行于内核空间,处于进程上下文;
(3) 运行于内核空间,处于中断上下文。
应用程序通过系统调用陷入内核,此时处于进程上下文
。现代几乎所有的CPU体系结构都支持中断。当外部设备产生中断,向CPU发送一个异步信号,CPU调用相应的中断处理程序来处理该中断,此时CPU处于中断上下文
。
在进程上下文
中,可以通过current关联相应的任务。进程以进程上下文的形式运行在内核空间,可以发生睡眠,所以在进程上下文中,可以使用信号量(semaphore)。实际上,内核经常在进程上下文中使用信号量来完成任务之间的同步,当然也可以使用锁。
中断上下文
不属于任何进程,它与current没有任何关系(尽管此时current指向被中断的进程)。由于没有进程背景,在中断上下文中不能发生睡眠,否则又如何对它进行调度。所以在中断上下文中只能使用锁进行同步,正是因为这个原因,中断上下文也叫做原子上下文(atomic context)(关于同步可以参考同步)。在中断处理程序中,通常会禁止同一中断,甚至会禁止整个本地中断,所以中断处理程序应该尽可能迅速,所以又把中断处理分成上部和下部(关于中断)。
2.2 上下文切换
上下文切换,也就是从一个可执行进程切换到另一个可执行进程。上下文切换由函数context_switch()函数完成,该函数位于kernel/sched.c中,它由进程调度函数schedule()调用。
static inline
task_t * context_switch(runqueue_t *rq, task_t *prev, task_t *next)
{
struct mm_struct *mm = next->mm;
struct mm_struct *oldmm = prev->active_mm;
if (unlikely(!mm)) {
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next);
} else
switch_mm(oldmm, mm, next);
if (unlikely(!prev->mm)) {
prev->active_mm = NULL;
WARN_ON(rq->prev_mm);
rq->prev_mm = oldmm;
}
/* Here we just switch the register state and the stack. */
switch_to(prev, next, prev);
return prev;
}
其中,switch_mm()将虚拟内存映射到新的进程;switch_to完成最终的进程切换,它保存原进程的所有寄存器信息,恢复新进程的所有寄存器信息,并执行新的进程。无论何时,内核想要进行任务切换,都通过调用schedule()完成任务切换。
三 内核可抢占以及可抢占和可中断的区别
首先被中断不是被抢占,中断和抢占是两个概念。
抢占必须涉及进程上下文的切换,而中断是在中断上下文。
所谓可抢占抢的是进程上下文,人人都争取上台。
可中断指的是是否可以中断当前CPU而进入我的中断处理函数。
如果内核是不可抢占的(比如说2.4的内核),一旦切进内核态,只要代码不是主动释放CPU它就可以一直占着CPU。虽不可抢占,但若此时发生中断,代码还是要交出CPU,但是中断返回之后,代码又能霸占CPU了,此为可中断但不可抢占
。
如果内核是可抢占的(比如2.6或之后的内核),上述情况就不会发生了。
四 抢占
抢占也分用户抢占和内核抢占。详解请参考抢占。
4.1 用户抢占
当内核即将返回用户空间时,内核会检查need_resched是否设置,如果设置,则调用schedule(),此时,发生用户抢占。一般来说,用户抢占发生几下情况:
(1) 从系统调用返回用户空间
;
(2) 从中断(异常)处理程序返回用户空间
。
4.2 内核抢占
内核从2.6开始就支持内核抢占,对于非内核抢占系统,内核代码可以一直执行,直到完成,也就是说当进程处于内核态时,是不能被抢占的(当然,运行于内核态的进程可以主动放弃CPU,比如,在系统调用服务例程中,由于内核代码由于等待资源而放弃CPU,这种情况叫做计划性进程切换(planned process switch))
。但是,对于由异步事件(比如中断)引起的进程切换,抢占式内核与非抢占式是有区别的,对于前者叫做强制性进程切换(forced process switch)。
为了支持内核抢占,内核引入了preempt_count字段,该计数初始值为0,每当使用锁时加1,释放锁时减1。当preempt_count为0时,表示内核可以被安全的抢占,大于0时,则禁止内核抢占。
当从中断返回内核空间时,内核会检preempt_count和need_resched的值(返回用户空间时只需要检查need_resched)
,如查preempt_count为0且need_resched设置,则调用schedule(),完成任务抢占。
一般来说,内核抢占发生以下情况:
-
当一个中断处理例程退出,在返回到内核态时。preempt_count为0且need_resched置位。这是隐式的调用schedule()函数。
-
当内核代码再一次具有可抢占性的时候,如解锁(spin_unlock_bh)及使能软中断(local_bh_enable)等,此时当preempt_count从正整数变为0时。这也是隐式的调用schedule()函数
-
如果内核中的任务显式的调用schedule(), 任务主动放弃CPU使用权。
-
如果内核中的任务阻塞(这同样也会导致调用schedule())。任务主动放弃CPU使用权。
五 中断
详细可参考中断
5.1 中断方式分为同步中断和异步中断
中断方式分为同步中断和异步中断
。同步中断是由CPU控制单元产生的,“同步”是指只有在一条指令执行完毕后,CPU才会发出中断,而不是发生在代码指令执行期间,比如系统调用。而异步中断是由其他硬件设备依照CPU时钟信号产生的,即意味着中断能够在指令之间发生,例如键盘中断。有一个知识点值得了解:内核态能够触发的唯一异常就是缺页异常,其他的都是用户态触发的。
5.2 中断过程分为上下两个部分
中断过程分为上下两个部分:中断上半部、下半部。
上半部处理紧要事情。下半部处理不那么要紧的任务。比如:网卡上来的数据推进协议栈,然后推给上层应用程序。
当前的内核有三种下半部的实现方式:softirq、tasklet、working queue。
名称 | 上下文 | 特点 | 概述 |
---|---|---|---|
Softirq | 中断上下文 | 可中断不可睡眠 | 速度最快。同一个Softirq可能会同时运行在多个核上,必须非常小心的处理数据同步 |
Tasklet | 中断上下文 | 可中断不可睡眠 | 基于Softirq实现,同一类的Tasklet不会被同时运行,编程代价小 |
Working quere | 进程上下文 | 可中断可睡眠 | 基于内核线程实现 |
参考链接:https://www.cnblogs.com/hustcat/archive/2009/08/31/1557507.html