主要介绍内核抢占的相关概念和具体实现,以及抢占对内核调度和内核竞态和同步的一些影响。
(所用内核版本3.19.3)
1. 基本概念
- 用户抢占和内核抢占
- 用户抢占发生点
- 当从系统调用或者中断上下文返回用户态的时候,会检查need_resched标志,如果被设置则会重新选择用户态task执行
- 内核抢占发生点
- 当从中断上下文返回内核态的时候,检查need_resched标识以及__preemp_count计数,如果标识被设置,并且可抢占,则会触发调度程序preempt_schedule_irq()
- 内核代码由于阻塞等原因直接或间接显示调用schedule,比如preemp_disable时可能会触发preempt_schedule()
- 本质上内核态中的task是共享一个内核地址空间,在同一个core上,从中断返回的task很可能执行和被抢占的task相同的代码,并且两者同时等待各自的资源释放,也可能两者修改同一共享变量,所以会造成死锁或者竞态等;而对于用户态抢占来说,由于每个用户态进程都有独立的地址空间,所以在从内核代码(系统调用或者中断)返回用户态时,由于是不同地址空间的锁或者共享变量,所以不会出现不同地址空间之间的死锁或者竞态,也就没必要检查__preempt_count,是安全的。__preempt_count主要负责内核抢占计数。
- 用户抢占发生点
2. 内核抢占的实现
- percpu变量__preempt_count
抢占计数8位, PREEMPT_MASK => 0x000000ff
软中断计数8位, SOFTIRQ_MASK => 0x0000ff00
硬中断计数4位, HARDIRQ_MASK => 0x000f0000
不可屏蔽中断1位, NMI_MASK => 0x00100000
PREEMPTIVE_ACTIVE(标识内核抢占触发的schedule) => 0x00200000
调度标识1位, PREEMPT_NEED_RESCHED => 0x80000000
__preempt_count的作用
- 抢占计数
- 判断当前所在上下文
- 重新调度标识
thread_info的flags
- thread_info的flags中有一个是TIF_NEED_RESCHED,在系统调用返回,中断返回,以及preempt_disable的时候会检查是否设置,如果设置并且抢占计数为0(可抢占),则会触发重新调度schedule()或者preempt_schedule()或者preempt_schedule_irq()。通常在scheduler_tick中会检查是否设置此标识(每个HZ触发一次),然后在下一次中断返回时检查,如果设置将触发重新调度,而在schedule()中会清除此标识。
// kernel/sched/core.c
// 设置thread_info flags和__preempt_count的need_resched标识
void resched_curr(struct rq *rq)
{
/*省略*/
if (cpu == smp_processor_id()) {
// 设置thread_info的need_resched标识
set_tsk_need_resched(curr);
// 设置抢占计数__preempt_count里的need_resched标识
set_preempt_need_resched();
return;
}
/*省略*/
}
//在schedule()中清除thread_info和__preempt_count中的need_resched标识
static void __sched __schedule(void)
{
/*省略*/
need_resched:
// 关抢占读取percpu变量中当前cpu id,运行队列
preempt_disable();
cpu = smp_processor_id();
rq = cpu_rq(cpu);
rcu_note_context_switch();
prev = rq->curr;
/*省略*/
//关闭本地中断,关闭抢占,获取rq自旋锁
raw_spin_lock_irq(&rq->lock);
switch_count = &prev->nivcsw;
// PREEMPT_ACTIVE 0x00200000
// preempt_count = __preempt_count & (~(0x80000000))
// 如果进程没有处于running的状态或者设置了PREEMPT_ACTIVE标识
//(即本次schedule是由于内核抢占导致),则不会将当前进程移出队列
// 此处PREEMPT_ACTIVE的标识是由中断返回内核空间时调用
// preempt_schdule_irq或者内核空间调用preempt_schedule
// 而设置的,表明是由于内核抢占导致的schedule,此时不会将当前
// 进程从运行队列取出,因为有可能其再也无法重新运行。
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
// 如果有信号不移出run_queue
if (unlikely(signal_pending_state(prev->state, prev))) {
prev