在操作系统中,如果一个新的进程就绪的时候的优先级比正在运行的进程的优先级更高,那么就会发生抢占,通过抢占,打断正在执行的进程,让高优先级的进程得到机会运行。
对于早期的linux内核,默认是不开启抢占模式,当一个进程高优先级进程需要运行的时候,必须等到当前正在运行的较低优先级的进程运行完毕后才能运行,在这里,等待低优先级进程的时间也会算在高优先级进程的响应时间中去。这对一些具有实时性要求的业务来说是不可接受的。(排队的时间为什么要算成自己运行的时间)

现代的linux版本内核默认都是开启抢占的,也就意味着,一个高优先级进程进来的时候,系统会在预设的抢占点上,尽可能的让高优先级任务得到运行,这样就最小化了高优先级任务的排队时间,从而提高了高优先级进程的响应能力

从整体的运行视角上来看,假设正在运行的任务是A,高优先级任务是B,那么状态就是B抢占了A,如下

操作系统的操作并不是代表着高优先级任务B就绪就立马发生的抢占行为,而是正在运行的任务A主动提供了被抢占时机。通俗的说法是任务A让出(yield)了cpu。在linux中,实际上是置为了TIF_NEED_RESCHED标志
操作系统在调度器分配给自己的时间片运行完毕之后,主动由定时器检查并调用scheduler_tick函数,然后调用task_tick,entity_tick。entity_tick会调用check_preempt_tick来触发抢占,简单代码流程如下
update_process_times scheduler_tick task_tick task_tick_fair entity_tick update_curr curr->vruntime += calc_delta_fair(delta_exec, curr); check_preempt_tick resched_curr set_tsk_need_resched set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
我们关注check_preempt_tick函数
static void check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr) { ideal_runtime = sched_slice(cfs_rq, curr); delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime; if (delta_exec > ideal_runtime) { resched_curr(rq_of(cfs_rq)); return; } delta = curr->vruntime - se->vruntime; if (delta > ideal_runtime) resched_curr(rq_of(cfs_rq)); }
检查vruntime是不是超过granularity太久,发生抢占
新唤醒的进程优先级高于正在运行的进程时,通过wake_up_new_task/wake_up_process/wake_up_state三类接口来唤醒正在运行的任务让它触发抢占。代码流程如下
wake_up_new_task--->check_preempt_curr--->resched_curr wake_up_process wake_up_state try_to_wake_up ttwu_queue ttwu_do_activate ttwu_do_wakeup check_preempt_curr resched_curr
我们关注check_preempt_curr函数
static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags) { if (unlikely(task_has_idle_policy(curr)) && likely(!task_has_idle_policy(p))) goto preempt; preempt: resched_curr(rq); }
如果处于idle则发生抢占
上面已经是大部分场景了,还有负载均衡,进程优先级修改,中断,系统调用等时机都是调度的时机,这里就不一一举例了。代码很容易查到
当抢占发生的时候,preempt_count会自加,同时解除抢占的时候preempt_count自减。preempt_count维持平衡,同时也是调试抢占的关键变量
#define preempt_count_add(val) __preempt_count_add(val) #define preempt_count_sub(val) __preempt_count_sub(val) #define preempt_count_dec_and_test() __preempt_count_dec_and_test() #endif #define __preempt_count_inc() __preempt_count_add(1) #define __preempt_count_dec() __preempt_count_sub(1) #define preempt_count_inc() preempt_count_add(1) #define preempt_count_dec() preempt_count_sub(1) #ifdef CONFIG_PREEMPT_COUNT #define preempt_disable() \ do { \ preempt_count_inc(); \ barrier(); \ } while (0) #define sched_preempt_enable_no_resched() \ do { \ barrier(); \ preempt_count_dec(); \ } while (0) #define preempt_enable_no_resched() sched_preempt_enable_no_resched() #define preemptible() (preempt_count() == 0 && !irqs_disabled()) #ifdef CONFIG_PREEMPTION #define preempt_enable() \ do { \ barrier(); \ if (unlikely(preempt_count_dec_and_test())) \ __preempt_schedule(); \ } while (0) #define preempt_enable_notrace() \ do { \ barrier(); \ if (unlikely(__preempt_count_dec_and_test())) \ __preempt_schedule_notrace(); \ } while (0) #define preempt_check_resched() \ do { \ if (should_resched(0)) \ __preempt_schedule(); \ } while (0)
在这里preempt_disable和preempt_enable经常和spinlock搭配,这样可以使得每次spinlock结束的时候,也是一次调度时机
执行抢占就是调用schedule函数,下面列出来几个时机
系统调用完成之后,主动调用schedule,代码流程如下
syscall_exit_to_user_mode exit_to_user_mode_prepare exit_to_user_mode_loop schedule()
中断结束也会主动调用schedule(
irqentry_exit irqentry_exit_cond_resched preempt_schedule_irq __schedule(True)
wait_event __wait_event ___wait_event schedule()
其他常见都是少见常见了,可以通过代码找出来,这里就不一一介绍了。