4. 周期性调度器(Periodic Scheduler):基于Tick的调度决策

周期性调度器是Linux内核实现时间共享与抢占决策的核心引擎。它由时钟中断(Timer Tick)驱动,在每个调度周期(通常为1ms或4ms)触发一次,负责检查当前进程的时间片是否耗尽,并决定是否需要重新调度。本章深入剖析scheduler_tick()函数的入口逻辑与进程时间片递减机制。

4.1 基于Tick的调度决策模型

在CFS(完全公平调度器)中,Tick不再是固定时间片,而是触发vruntime更新与调度点检查的脉冲信号。其核心决策流程如下:

核心原则:每个Tick到来时,内核更新当前进程的vruntime,并判断是否超过当前红黑树中最左节点的vruntime。若超过,则设置TIF_NEED_RESCHED标志,等待返回用户态时触发抢占。
基于Tick的调度决策流程 时钟中断触发 scheduler_tick() 入口 更新当前进程vruntime 检查是否需要重新调度 设置 TIF_NEED_RESCHED 继续执行当前进程

4.2 scheduler_tick() 函数入口

scheduler_tick()是周期性调度器的核心入口函数,定义在kernel/sched/core.c中。它由时钟中断处理程序timer_interrupt() → update_process_times() → scheduler_tick() 路径调用。

// kernel/sched/core.c (简化示意)
void scheduler_tick(void)
{
    int cpu = smp_processor_id();
    struct rq *rq = cpu_rq(cpu);
    struct task_struct *curr = rq->curr;

    // 1. 更新当前CPU的运行队列统计
    update_rq_clock(rq);

    // 2. 调用调度类的task_tick方法
    curr->sched_class->task_tick(rq, curr, 0);

    // 3. 检查是否需要触发负载均衡
    trigger_load_balance(rq);

    // 4. 更新CPU的全局时间统计
    update_cpu_load_active(rq);
}
关键点: scheduler_tick()不直接执行进程切换,而是通过task_tick回调更新进程时间信息,并设置TIF_NEED_RESCHED标志。真正的上下文切换发生在中断返回用户态时。

4.3 进程时间片递减逻辑

对于CFS调度类,task_tick回调指向task_tick_fair()。该函数实现时间片递减的核心逻辑:

// kernel/sched/fair.c (简化示意)
static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
    struct cfs_rq *cfs_rq = task_cfs_rq(curr);
    struct sched_entity *se = &curr->se;

    // 1. 更新当前调度实体的vruntime
    update_curr(cfs_rq);

    // 2. 检查是否应该抢占
    if (cfs_rq->nr_running > 1)
        check_preempt_tick(cfs_rq, curr);
}

update_curr()函数完成vruntime的增量计算:

static void update_curr(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    u64 now = rq_clock_task(rq_of(cfs_rq));
    u64 delta_exec;

    // 计算自上次更新以来的实际执行时间
    delta_exec = now - curr->exec_start;
    curr->exec_start = now;

    // 更新sum_exec_runtime
    curr->sum_exec_runtime += delta_exec;

    // 更新vruntime:delta_exec * (NICE_0_LOAD / se->load.weight)
    curr->vruntime += calc_delta_fair(delta_exec, &curr->load);

    // 更新cfs_rq的min_vruntime
    update_min_vruntime(cfs_rq);
}

4.4 时间片递减与抢占检查

check_preempt_tick()函数负责判断当前进程是否应该被抢占:

检查条件 触发动作 说明
vruntime差值 > 调度粒度 设置TIF_NEED_RESCHED 当前进程运行时间过长,应让出CPU
当前进程时间片耗尽 设置TIF_NEED_RESCHED 基于sysctl_sched_min_granularity检查
红黑树最左节点vruntime更小 设置TIF_NEED_RESCHED 有更值得运行的进程等待
以上条件均不满足 继续执行 当前进程仍是最优选择
check_preempt_tick() 决策逻辑 check_preempt_tick() vruntime差值 > 粒度? 是 否 设置TIF_NEED_RESCHED 检查红黑树最左节点 最左节点vruntime更小? 设置TIF_NEED_RESCHED 继续执行当前进程

4.5 时间片递减的关键参数

周期性调度器的时间片行为受以下内核参数控制:

参数名 默认值 作用
sysctl_sched_min_granularity 0.75 ms 进程最小执行时间片,防止频繁切换
sysctl_sched_latency 6 ms 目标调度延迟,决定时间片分配总量
sysctl_sched_wakeup_granularity 1.0 ms 唤醒抢占阈值,防止小任务被频繁抢占
HZ 250 / 1000 时钟中断频率,决定Tick间隔
注意: 在HZ=1000的系统中,每1ms触发一次scheduler_tick()。如果sysctl_sched_min_granularity设置为0.75ms,则进程至少运行1个Tick(1ms)后才可能被抢占,实际最小时间片受Tick粒度约束。

4.6 周期性调度器的完整调用链

从时钟中断到调度决策的完整路径如下:

  1. 硬件时钟中断 → CPU进入中断处理程序
  2. timer_interrupt() → 调用do_timer()更新全局jiffies
  3. update_process_times() → 调用scheduler_tick()
  4. scheduler_tick() → 调用task_tick_fair()
  5. task_tick_fair() → 调用update_curr()更新vruntime
  6. check_preempt_tick() → 判断是否设置TIF_NEED_RESCHED
  7. 中断返回 → 检查TIF_NEED_RESCHED,触发schedule()
周期性调度器完整调用链 时钟中断 timer_interrupt update_process_times scheduler_tick task_tick_fair check_preempt 最终:设置TIF_NEED_RESCHED → 中断返回时触发schedule() 完成进程上下文切换

4.7 总结与关键要点

  • 周期性调度器由时钟中断驱动,每个Tick触发一次scheduler_tick()
  • 时间片递减通过update_curr()更新vruntime实现,而非传统固定时间片计数器
  • 抢占决策基于vruntime差值比较,由check_preempt_tick()完成
  • 延迟设置:TIF_NEED_RESCHED标志仅在中断返回时触发实际切换
  • 参数调优:sysctl_sched_min_granularity和sysctl_sched_latency控制调度粒度