一、调度机制概述
Linux内核的任务调度主要基于两个核心机制:
- 直接调用schedule():主动让出CPU
- 设置TIF_NEED_RESCHED标志:延迟调度,在合适的时机检查该标志并调用schedule()
核心调度函数__schedule()(位于kernel/sched/core.c)的注释清楚地说明了驱动调度器进入的主要方式:
c
/*
* __schedule() is the main scheduler function.
*
* The main means of driving the scheduler and thus entering this function are:
*
* 1. Explicit blocking: mutex, semaphore, waitqueue, etc.
*
* 2. TIF_NEED_RESCHED flag is checked on interrupt and userspace return
* paths. For example, see arch/x86/entry_64.S.
*
* To drive preemption between tasks, the scheduler sets the flag in timer
* interrupt handler sched_tick().
*
* 3. Wakeups don't really cause entry into schedule(). They add a
* task to the run-queue and that's it.
*/
二、主动调度(Voluntary Scheduling)
1. schedule() 直接调用
位置 : kernel/sched/core.c
c
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
sched_submit_work(tsk);
do {
preempt_disable();
__schedule(SM_NONE);
sched_preempt_enable_no_resched();
} while (need_resched());
sched_update_worker(tsk);
}
触发场景:
- 任务主动阻塞(等待资源)
- 内核同步原语(mutex, semaphore, completion等)
- 等待队列操作
2. sched_yield() 系统调用
位置 : kernel/sched/syscalls.c
c
SYSCALL_DEFINE0(sched_yield)
{
do_sched_yield();
return 0;
}
static void do_sched_yield(void)
{
struct rq_flags rf;
struct rq *rq;
rq = this_rq_lock_irq(&rf);
schedstat_inc(rq->yld_count);
current->sched_class->yield_task(rq);
preempt_disable();
rq_unlock_irq(rq, &rf);
sched_preempt_enable_no_resched();
schedule(); // 直接调用调度器
}
触发条件 : 用户空间调用sched_yield()系统调用,主动让出CPU。
3. cond_resched() 条件调度
位置 : kernel/sched/core.c
c
int __sched __cond_resched(void)
{
if (should_resched(0) && !irqs_disabled()) {
preempt_schedule_common();
return 1;
}
...
return 0;
}
触发条件:
- 仅在非抢占式内核(CONFIG_PREEMPTION=n)中有效
- 内核代码显式调用
cond_resched()检查是否需要调度 - 常用于长循环中提供调度点
4. 睡眠/阻塞导致的调度
示例: mutex_lock
位置 : kernel/locking/mutex.c
c
// 在mutex_lock中,当无法获取锁时
set_current_state(state);
for (;;) {
if (__mutex_trylock(lock))
goto acquired;
...
schedule_preempt_disabled(); // 调用调度器
...
}
触发场景:
- 互斥锁竞争
- 信号量操作
- 等待队列(waitqueue)
- 完成量(completion)
- IO等待
三、抢占式调度(Preemptive Scheduling)
1. 用户抢占(User Preemption)
触发时机: 从内核返回用户空间时检查TIF_NEED_RESCHED标志
位置 : kernel/entry/common.c
c
__always_inline unsigned long exit_to_user_mode_loop(struct pt_regs *regs,
unsigned long ti_work)
{
while (ti_work & EXIT_TO_USER_MODE_WORK) {
local_irq_enable_exit_to_user(ti_work);
if (ti_work & (_TIF_NEED_RESCHED | _TIF_NEED_RESCHED_LAZY))
schedule(); // 返回用户空间前调度
...
ti_work = read_thread_flags();
}
return ti_work;
}
调用路径:
syscall_exit_to_user_mode()
-> __syscall_exit_to_user_mode_work()
-> exit_to_user_mode_prepare()
-> exit_to_user_mode_loop() // 检查并调度
2. 内核抢占(Kernel Preemption)
前提: 内核配置了CONFIG_PREEMPTION
2.1 preempt_schedule() - preempt_enable时抢占
位置 : kernel/sched/core.c
c
/*
* This is the entry point to schedule() from in-kernel preemption
* off of preempt_enable.
*/
asmlinkage __visible void __sched notrace preempt_schedule(void)
{
/*
* If there is a non-zero preempt_count or interrupts are disabled,
* we do not want to preempt the current task. Just return..
*/
if (likely(!preemptible()))
return;
preempt_schedule_common();
}
触发机制 :
位置 : include/linux/preempt.h
c
#define preempt_enable() \
do { \
barrier(); \
if (unlikely(preempt_count_dec_and_test())) \
__preempt_schedule(); \ // 抢占点
barrier(); \
} while (0)
当preempt_count减为0且TIF_NEED_RESCHED被设置时,触发抢占调度。
2.2 preempt_schedule_irq() - 中断返回内核态时抢占
位置 : kernel/sched/core.c
c
asmlinkage __visible void __sched preempt_schedule_irq(void)
{
enum ctx_state prev_state;
/* Catch callers which need to be fixed */
BUG_ON(preempt_count() || !irqs_disabled());
prev_state = exception_enter();
do {
preempt_disable();
local_irq_enable();
__schedule(SM_PREEMPT);
local_irq_disable();
sched_preempt_enable_no_resched();
} while (need_resched());
exception_exit(prev_state);
}
调用位置 : kernel/entry/common.c
c
void raw_irqentry_exit_cond_resched(void)
{
if (!preempt_count()) {
rcu_irq_exit_check_preempt();
if (need_resched())
preempt_schedule_irq(); // 中断返回时抢占
}
}
触发条件:
- 从中断返回内核态
- preempt_count为0
- TIF_NEED_RESCHED标志被设置
四、时钟中断触发的调度
1. sched_tick() - 时钟滴答处理
位置 : kernel/sched/core.c
c
void sched_tick(void)
{
int cpu = smp_processor_id();
struct rq *rq = cpu_rq(cpu);
struct task_struct *donor;
struct rq_flags rf;
...
rq_lock(rq, &rf);
donor = rq->donor;
update_rq_clock(rq);
...
// 调用调度类的task_tick方法
donor->sched_class->task_tick(rq, donor, 0);
...
}
调用路径:
timer_interrupt
-> update_process_times() // kernel/time/timer.c
-> sched_tick()
2. CFS调度类的task_tick_fair()
位置 : kernel/sched/fair.c
c
static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
struct cfs_rq *cfs_rq;
struct sched_entity *se = &curr->se;
for_each_sched_entity(se) {
cfs_rq = cfs_rq_of(se);
entity_tick(cfs_rq, se, queued);
}
...
}
entity_tick()调用update_curr()检查时间片 :
位置 : kernel/sched/fair.c
c
static void update_curr(struct cfs_rq *cfs_rq)
{
...
if (cfs_rq->nr_queued == 1)
return;
if (resched || did_preempt_short(cfs_rq, curr)) {
resched_curr_lazy(rq); // 设置重调度标志
clear_buddies(cfs_rq, curr);
}
}
3. RT调度类的task_tick_rt()
位置 : kernel/sched/rt.c
c
static void task_tick_rt(struct rq *rq, struct task_struct *p, int queued)
{
...
// RR任务的时间片管理
if (p->policy != SCHED_RR)
return;
if (--p->rt.time_slice)
return;
p->rt.time_slice = sched_rr_timeslice;
// 时间片用完,重新排队并设置重调度标志
for_each_sched_rt_entity(rt_se) {
if (rt_se->run_list.prev != rt_se->run_list.next) {
requeue_task_rt(rq, p, 0);
resched_curr(rq); // 设置重调度标志
return;
}
}
}
五、睡眠/唤醒导致的调度
1. 进程唤醒 - try_to_wake_up()
位置 : kernel/sched/core.c
c
int try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
...
// 将任务加入运行队列
// 如果被唤醒的任务优先级更高,触发抢占
if (p->sched_class->task_woken)
p->sched_class->task_woken(rq, p);
...
}
wake_up_process() :
位置 : kernel/sched/core.c
c
int wake_up_process(struct task_struct *p)
{
return try_to_wake_up(p, TASK_NORMAL, 0);
}
2. 唤醒抢占检查 - wakeup_preempt()
位置 : kernel/sched/core.c
c
void wakeup_preempt(struct rq *rq, struct task_struct *p, int flags)
{
struct task_struct *donor = rq->donor;
if (p->sched_class == donor->sched_class)
donor->sched_class->wakeup_preempt(rq, p, flags);
else if (sched_class_above(p->sched_class, donor->sched_class))
resched_curr(rq); // 高优先级调度类,立即抢占
...
}
3. CFS唤醒抢占检查 - check_preempt_wakeup_fair()
位置 : kernel/sched/fair.c
c
static void check_preempt_wakeup_fair(struct rq *rq, struct task_struct *p, int flags)
{
...
// 检查新唤醒的任务是否应该抢占当前任务
if (pick_eevdf(cfs_rq) == pse)
goto preempt;
return;
preempt:
resched_curr_lazy(rq); // 设置重调度标志
}
六、resched_curr() - 设置重调度标志
位置 : kernel/sched/core.c
c
/*
* reshed_curr - mark rq's current task 'to be rescheduled now'.
*
* On UP this means the setting of the need_resched flag, on SMP it
* might also involve a cross-CPU call to trigger the scheduler on
* the target CPU.
*/
static void __resched_curr(struct rq *rq, int tif)
{
struct task_struct *curr = rq->curr;
struct thread_info *cti = task_thread_info(curr);
int cpu;
...
if (cpu == smp_processor_id()) {
set_ti_thread_flag(cti, tif); // 本地CPU直接设置标志
if (tif == TIF_NEED_RESCHED)
set_preempt_need_resched();
return;
}
if (set_nr_and_not_polling(cti, tif)) {
if (tif == TIF_NEED_RESCHED)
smp_send_reschedule(cpu); // 远程CPU发送IPI
}
...
}
void resched_curr(struct rq *rq)
{
__resched_curr(rq, TIF_NEED_RESCHED);
}
七、其他调度时机
1. 优先级变更
当任务的优先级改变时,可能需要重新调度:
位置 : kernel/sched/fair.c
c
static void prio_changed_fair(struct rq *rq, struct task_struct *p, int oldprio)
{
...
if (task_current_donor(rq, p)) {
if (p->prio > oldprio)
resched_curr(rq); // 优先级降低,需要调度
} else
wakeup_preempt(rq, p, 0);
}
2. 负载均衡
当进行CPU间负载均衡时,可能触发调度:
位置 : kernel/sched/core.c
c
// 在sched_tick()中
#ifdef CONFIG_SMP
if (!scx_switched_all()) {
rq->idle_balance = idle_cpu(cpu);
sched_balance_trigger(rq); // 触发负载均衡
}
#endif
3. 进程创建
新进程创建后唤醒时:
位置 : kernel/sched/core.c
c
// wake_up_new_task()中
wakeup_preempt(rq, p, 0); // 检查是否需要抢占
4. cgroup调度组变更
当任务在cgroup调度组之间移动时,可能触发调度。
八、调度时机总结表
| 调度类型 | 触发机制 | 调用路径 | 配置依赖 |
|---|---|---|---|
| 主动调度 | schedule()直接调用 | 阻塞、等待、yield | 无 |
| 条件调度 | cond_resched() | 内核长循环中 | 非抢占内核 |
| 用户抢占 | 返回用户空间 | exit_to_user_mode_loop() | 无 |
| 内核抢占 | preempt_enable() | __preempt_schedule() | CONFIG_PREEMPTION |
| 中断返回抢占 | 中断返回内核态 | preempt_schedule_irq() | CONFIG_PREEMPTION |
| 时钟中断调度 | sched_tick() | task_tick_xxx() | 无 |
| 唤醒调度 | try_to_wake_up() | wakeup_preempt() | 无 |
| 优先级变更 | __sched_setscheduler() | prio_changed_xxx() | 无 |
九、关键标志位
位置 : arch/x86/include/asm/thread_info.h
c
#define TIF_NEED_RESCHED 3 /* rescheduling necessary */
#define TIF_NEED_RESCHED_LAZY 4 /* Lazy rescheduling needed */
这些标志位存储在thread_info->flags中,用于标识是否需要进行调度。TIF_NEED_RESCHED_LAZY用于延迟抢占模式。
十、调度检查点总结
Linux内核在以下位置检查TIF_NEED_RESCHED标志:
- 返回用户空间时: 系统调用返回、异常返回、中断返回用户态
- preempt_enable()时: 抢占计数器归零且开启内核抢占
- 中断返回内核态时: preempt_schedule_irq()
- 显式调用cond_resched()时: 非抢占内核中的显式调度点
十一、核心设计理念
调度器的核心设计理念是:在需要调度时设置标志位,在安全的检查点进行实际的上下文切换,这既保证了系统的响应性,又确保了内核数据结构的一致性。
这种设计确保了:
- 安全性: 只在内核数据结构一致的点进行上下文切换
- 响应性: 通过抢占机制保证高优先级任务及时响应
- 灵活性: 支持多种调度策略和配置选项
总结
Linux内核任务调度的主要时机总结如下:
- 主动调度 - 进程直接调用schedule()让出CPU,包括:
- 阻塞操作(mutex、semaphore、waitqueue等)
- sched_yield()系统调用
- cond_resched()条件调度
- 用户抢占 - 从内核返回用户空间时检查TIF_NEED_RESCHED标志
- 内核抢占 - 需配置CONFIG_PREEMPTION:
- preempt_enable()时检查抢占
- 中断返回内核态时抢占
- 时钟中断调度 - sched_tick()检测时间片耗尽
- 唤醒调度 - 高优先级进程被唤醒时触发抢占
- 其他时机 - 优先级变更、负载均衡、进程创建等