Linux核心概念详解
  • 0. Linux核心概念详解
  • 1. 调试环境
  • 2. Linux 调度器
    • 2.1 任务
    • 2.2 核心概念
      • 2.2.1 核心概念 - 调度实体
      • 2.2.2 核心概念 - 调度类
      • 2.2.3 核心概念 - 调度策略
      • 2.2.4 核心概念 - 运行队列
      • 2.2.5 核心概念 - 优先级
    • 2.3 演进历史
      • 2.3.1 O(n)调度器 - 调度逻辑
      • 2.3.2 O(n)调度器 - 时间分配
      • 2.3.3 O(n)调度器 - 调度时机
      • 2.3.4 O(1)调度器 - 简介
      • 2.3.5 O(1)调度器 - 调度逻辑
      • 2.3.6 O(1)调度器 - 时间分配
      • 2.3.7 RSDL
      • 2.3.8 CFS
    • 2.4 DL调度器
      • 2.4.1 DL调度器 - 调度算法
      • 2.4.2 DL调度器 -核心代码
    • 2.5 RT调度器
    • 2.6 CFS
      • 2.6.1 公平性
      • 2.6.2 调度逻辑
      • 2.6.2.1 调度逻辑 - 数据结构
      • 2.6.2.2 调度逻辑 - vruntime
      • 2.6.2.3 调度逻辑 - 调度周期
      • 2.6.2.4 调度逻辑 - 调度节拍
      • 2.6.2.5 调度逻辑 - 任务抢占
      • 2.6.2.6 调度逻辑 - 调度时机
      • 2.6.3 组调度
      • 2.6.3.1 组调度 - 数据结构
      • 2.6.3.2 组调度 - 调度逻辑
      • 2.6.3.3 组调度 - 时间分配
      • 2.6.3.4 组调度 - 任务组权重
      • 2.6.4 带宽控制
      • 2.6.4.1 带宽控制 - 数据结构
      • 2.6.4.2 带宽控制 - 带宽时间
      • 2.6.4.3 带宽控制 - 挂起与解挂
      • 2.6.4.3 带宽控制 - 定时器
    • 2.7 负载追踪
      • 2.7.1 负载追踪 - 简介
      • 2.7.2 负载追踪 - 数据结构
      • 2.7.3 负载追踪 - 计算负载
      • 2.7.4 负载追踪 - 更新负载
    • 2.8 负载均衡
      • 2.8.1 简介
      • 2.8.2 CPU的拓扑结构
      • 2.8.3 数据结构
      • 2.8.4 算法思路
      • 2.8.5 触发时机
      • 2.8.6 总结
  • 3. LINUX 内存管理
    • 3.1 寻址模式
      • 3.1.1 地址
      • 3.1.2 地址转换
      • 3.1.3 Linux的地址空间
    • 3.2 物理内存
      • 3.2.1 数据结构
      • 3.2.2 初始化
      • 3.2.3 物理内存模型
      • 3.2.4 Buddy System(伙伴系统)
      • 3.2.5 SLAB/SLUB/SLOB
Powered by GitBook
On this page

Was this helpful?

  1. 2. Linux 调度器
  2. 2.3 演进历史

2.3.2 O(n)调度器 - 时间分配

对普通任务而言,为了保证系统在时间分配上的公平性,O(n)调度器按照“轮”来计量调度周期,在每轮调度中,调度器根据各个任务的优先级为其分配时间,当所有任务的时间片都消耗完了之后,本轮度结束,系统开启下一轮调度。在 goodness() 中我们可以看到,如果一个普通任务已经没有了剩余时间,则会直接被忽略。

实时任务不受调度周期的限制,因为实时任务的高优先级最高,调度器要确保只要实时任务处于Ready状态,那么就立刻被调度到,本质上不存在预先分配时间的必须性。

调度器只是用运行时间来完成对 SCHED_RR 任务的轮询控制,函数 schedule() 在遍历就绪队列之前存在如下逻辑:

asmlinkage void schedule(void) {
    if (unlikely(prev->policy == SCHED_RR))
        if (!prev->counter) {
            prev->counter = NICE_TO_TICKS(prev->nice);
            move_last_runqueue(prev);
        }
    }

如果当前 SCHED_RR 任务的运行时间已经耗尽,则重新通过 NICE_TO_TICKS(prev->nice) 重新为其分配时间,并将其移动到队列尾部,这样如果队列中有相同优先级的 SCHED_RR 任务的话,本次就会被选中,从而达到轮询的目的。

如果在遍历队列时没有找到合适的任务,那么说明所有任务的运行时间都已经耗尽,本次调度周期已经结束了,调度器需要为任务分配时间、开启新的一轮调度周期。处理该逻辑的代码如下:

/* file: kernel/sched.c */

asmlinkage void schedule(void) {
    /* 删除前面的代码 */

    /* 1. 遍历队列 */
repeat_schedule:
    next = idle_task(this_cpu);
    c = -1000;
    list_for_each(tmp, &runqueue_head) {
        p = list_entry(tmp, struct task_struct, run_list);
        if (can_schedule(p, this_cpu)) { /* 判断 p 是否可以在 this_cpu 上运行 */
            int weight = goodness(p, this_cpu, prev->active_mm);
            if (weight > c)
                c = weight, next = p;
        }
    }

    /* 2. 通过 c 判断是否有合适的任务被选中 */
    if (unlikely(!c)) {
        struct task_struct *p;

        spin_unlock_irq(&runqueue_lock);
        read_lock(&tasklist_lock);
        /* 遍历系统的所有任务,并为其分配运行时间 */
        for_each_task(p) p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);
        read_unlock(&tasklist_lock);
        spin_lock_irq(&runqueue_lock);
        /* 分配完运行时间后重新调度 */
        goto repeat_schedule;
    }
}

/* 删除后面的代码 */

在下一轮调度周期中,任务的运行时间为 (p->counter >> 1) + NICE_TO_TICKS(p->nice);, 这里涉及两个方面:

  1. p->counter >> 1 上面遍历时不是没有找到合适的任务吗,那这里为何还要关心 p->counter 呢?因为上面遍历的是就绪队列(runqueue_head),所以调度器只是没有找到合适的就绪任务,而这里为任务分配时间片时,针对的是系统所有的任务。 此时系统中处于睡眠状态的任务就可能还有剩余时间,因此我们需要对这些任务的运行时间进行累积,让他们醒过来之后能够有更多的机会运行。为了避免此类任务的运行时间无限累积下去,每次都只累计一半的剩余时间。

  2. NICE_TO_TICKS(p->nice) ticks 就代表着运行时间,宏 NICE_TO_TICKS 根据任务的静态优先级(nice)为其分配运行时间。O(n)调度器的时间分配与CPU 频率也有关,具体逻辑如下:

    /* file: kernel/sched.c */
    
    #if HZ < 200
    #define TICK_SCALE(x) ((x) >> 2)
    #elif HZ < 400
    #define TICK_SCALE(x) ((x) >> 1)
    #elif HZ < 800
    #define TICK_SCALE(x) (x)
    #elif HZ < 1600
    #define TICK_SCALE(x) ((x) << 1)
    #else
    #define TICK_SCALE(x) ((x) << 2)
    #endif
    
    #define NICE_TO_TICKS(nice) (TICK_SCALE(20 - (nice)) + 1)

    从直觉上来说,给任务分配时间应该只与优先级相关,与CPU的频率无关。系统做这种处理的目的是为了将运行时间根据CPU的频率做一个伸缩(scale), 否则如果一个低频CPU给每个任务分配的运行时间很长的话,就会导致一次调度周期很久都不会结束。

Previous2.3.1 O(n)调度器 - 调度逻辑Next2.3.3 O(n)调度器 - 调度时机

Last updated 3 years ago

Was this helpful?