前面我们学习了调度器的设计需要关注的几个点,在这里复习下:

  1. 吞吐量(对应的是CPU消耗型进程)
  2. 响应速度(对应的是IO消耗型进程)
  3. 公平性,确保每个进程都可以有机会运行到
  4. 移动设备的功耗

Linux中调度器的设计,引入的概念

  1. 普通进程和实时进程使用优先级区分,0-99表示实时进程,100-139表示普通进程
  2. 实时进程采用两种调度策略SCHED_RR或者SCHED_FIFO
  3. 普通进程采用nice值进行动态调整普通进程的优先级
  4. 经常睡眠的进程尝试增大下优先级,经常长占CPU的适当减少优先级

本节我们先来学习Linux早期的调度算法的设计,先从最早的调度器算法开始,此调度器时间复杂度是O(n),所以也可以称为O(n)调度算法。我们选择的内核版本是linux-2.4.19。

O(n)调度器的实现原理

O(n)代表的是寻找一个合适的进程的时间复杂度。调度器定义了个runqueue的运行队列,将进程的状态变为Running的都会添加到此运行队列中,当然了不管是实时进程,还是普通进程都会添加到这个运行队列中。当需要从运行队列中需要一个合适的进程运行时,则就需要从队列的头遍历到尾部,所以说寻找一个合适进程的时间复杂度是O(n),当运行队列中的进程数目逐渐增大,则调度器的效率就会出现明显的下降。

运行队列中的进程是没有次序的,实时进程和普通进程是杂乱无章的在里面排序的。当需要调度器选择下一个进程的时候,则就需要从头遍历,比较每个进程的优先级,优先级高的先运行。当然了只有当实时进程运行完毕才可能轮到普通进程运行的。

struct task_struct结构

struct task_struct {long counter;long nice;unsigned long policy;int processor;unsigned long cpus_runnable, cpus_allowed;
}
  • counter代表的是进程的时间片,就是进程在一个调度周期中可与运行的时间。
  • nice代表这个进程的静态优先级。通过宏NICE_TO_TICKS,可以将对应的nice值转化为对应的时间片,存储在counter中
  • policy就是进程的调度策略,实时进程采用的是SCHED_RR或者SCHED_FIFO。普通进程采用的是SCHED_OTHER
    • SCHED_RR:同等优先级采用轮转的方式,不同优先级还是高优先级先调度
    • SCHED_FIFO:同等优先级采用先来后到的次序,就是先调度的进程如果没运行完毕,后面的只能排队。不同优先级还是高优先级的优先。如果高优先级的实时进程没运行完,低优先级的也是不能运行的。
  • pocessor: 代表当前进程运行在那个处理器上,会在SMP系统中使用
  • cpu_allowed:代表当前进程允许在那些CPU上可以运行。

时间片的计算

O(n)调度器采用的是TICK的方式,根据对应进程的nice值来计算对应的时间片的。

#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)

nice值的取值范围是-20 ~ +19, 取值越小优先级越高。进程的默认nice值是0,则进程默认的静态优先级就等于20.

我们以100HZ来计算下,各个nice值下一个进程可以占用的时间片。

nice值 -20 -10 0 +10 +19
100HZ 11tick 8tick 6tick 3tick 1tick
时间片 110ms 80ms 60ms 30ms 10ms

当然了这些时间片是根据静态的优先级计算出来的,当进程运行起来后会对睡眠的进程做一个补偿的。

O(n)调度器算法核心

从运行队列中选择一个优先级最高的进程

 list_for_each(tmp, &runqueue_head) {p = list_entry(tmp, struct task_struct, run_list);if (can_schedule(p, this_cpu)) {int weight = goodness(p, this_cpu, prev->active_mm);if (weight > c)c = weight, next = p;}}

就是从runqueue运行队列中逐个遍历,can_schedule函数用于判断当前进程是否可以在this_cpu上运行,是针对SMP系统的。

最主要的核心算法是在goodness函数中找出优先级最高的一个进程。

static inline int goodness(struct task_struct * p, int this_cpu, struct mm_struct *this_mm)
{/** Non-RT process - normal case first.*/if (p->policy == SCHED_OTHER) {weight = p->counter;if (!weight)goto out;#ifdef CONFIG_SMP/* Give a largish advantage to the same processor...   *//* (this is equivalent to penalizing other processors) */if (p->processor == this_cpu)weight += PROC_CHANGE_PENALTY;
#endif/* .. and a slight advantage to the current MM */if (p->mm == this_mm || !p->mm)weight += 1;weight += 20 - p->nice;goto out;}}
  • 上面的代码片段是针对普通进程的。如果调度策略是SCHED_OTHER,则对应的是普通进程。如果weigt=0代表的是此进程已经没有时间片了,则直接跳出
  • 在SMP系统中,如果此进程之前是在当前CPU上运行,因为Cache缓存的特性,会给此类CPU增加对应的时间片,相对应是给惩罚其他进程
  • 如果此进程和当前进程共享一个mm_struct结构,或者当前进程是是内核线程,则增加时间片。
  • 正常情况下普通进程的动态优先级=剩余的时间片+进程的静态优先级

实时进程则是简单粗暴,直接是在实时进程的静态优先级上加上1000,因为每个实时进程的静态优先级是不一样的。

weight = 1000 + p->rt_priority;

进程时间片的初始化

随着时间的推移,所有的进程的时间片可能都会运行,这时候就需要对所有的进程进行一次时间片的初始化动作

 /* Do we need to re-calculate counters? */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;}

也就是当从运行队列中没有找到可以运行的进程时,这时候会对所有的进程重新初始化counter。当然了一直的睡眠的进程可能时间片没有运行完,则需要将睡眠进程剩余的时间片加上。但是为了防止睡眠的IO消耗型进程优先级累计过高,则需要对半分。

时间片更新

系统中的tick中断会来更新当前进程的时间片的。

void update_process_times(int user_tick)
{struct task_struct *p = current;int cpu = smp_processor_id(), system = user_tick ^ 1;update_one_process(p, user_tick, system, cpu);if (p->pid) {if (--p->counter <= 0) {p->counter = 0;p->need_resched = 1;}if (p->nice > 0)kstat.per_cpu_nice[cpu] += user_tick;elsekstat.per_cpu_user[cpu] += user_tick;kstat.per_cpu_system[cpu] += system;} else if (local_bh_count(cpu) || local_irq_count(cpu) > 1)kstat.per_cpu_system[cpu] += system;
}

当每次tick中断到来之时,会对counter减1的。如果counter的值为0,则表示时间片已经用光,则需要设置need_resced的标志,在调度点会去判断当前进程是否设置此值,如果设置则进行调度。

O(n)调度器面临的问题

  • 时间复杂度问题,时间复杂度是O(n),当系统中的进程很少的时候性能还可以,但是当系统中的进程逐渐增多,选择下一个进程的时间则是逐渐增大。而且当系统中无可运行的进程时,重新初始化进程的时间片也是相当耗时,在系统中进程很多的情况系下。
  • SMP扩展问题。当需要picknext下一个进程时,需要对整个runqueue队列进行加锁的操作,spin_lock_irq(&runqueue_lock);当系统中进程数目比较多的时候,则在临界区的时间就比较长,导致其余的CPU自旋比较浪费
  • 实时进程的运行效率问题,因为实时进程和普通进程在一个列表中,每次查实时进程时,都需要全部扫描整个列表,导致实时进程不是很“实时”
  • CPU资源浪费问题:因为系统中只有一个runqueue,则当运行队列中的进程少于CPU的个数时,其余的CPU则几乎是idle状态,浪费资源
  • cache缓存问题:当系统中的进程逐渐减少时,原先在CPU1上运行的进程,不得不在CPU2上运行,导致在CPU2上运行时,cacheline则几乎是空白的,影响效率。
  • 总之O(n)调度器有很多问题,不过有问题肯定要解决的。所以在Linux2.6引入了O(1)的调度器。

Linux O(n)调度器相关推荐

  1. 【Linux 内核】调度器 ② ( sched_class 调度类结构体源码 | 源码路径 linux-5.6.18\kernel\sched\sched.h )

    文章目录 一.调度器 二.sched_class 调度类结构体 一.调度器 上一篇博客 [Linux 内核]调度器 ( 调度器概念 | 调度器目的 | 调度器主要工作 | 调度器位置 | 进程优先级 ...

  2. 服务器io修改,更改 Linux I/O 调度器来改善服务器性能

    为了从 Linux 服务器榨取尽可能多的性能,请了解如何更改 I/O 调度器以满足你的需求. Linux I/O 调度器()控制内核提交读写请求给磁盘的方式.自从 2.6 内核以来,管理员已经能够更改 ...

  3. Linux进程调度 - 实时调度器 LoyenWang

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...

  4. Linux进程调度-deadline调度器

    Linux内核中定义了5个调度器类,分别对应5个调度器,调度优先级顺序由高到低依次为:stop_sched_class.dl_sched_class.rt_sched_class.fair_sched ...

  5. 【Linux 内核】调度器 ⑥ ( task_woken 函数 | set_cpus_allowed 函数 | rq_online 函数 | rq_offline 函数 )

    文章目录 一.task_woken 函数 ( 唤醒阻塞进程 ) 二.set_cpus_allowed 函数 ( 修改进程在 CPU 中的亲和力 ) 三.rq_online 函数 ( 启动执行队列 ) ...

  6. 【Linux 内核】调度器 ⑤ ( put_prev_task、set_next_task 函数 | select_task_rq 函数 | migrate_task_rq 函数 )

    文章目录 一.put_prev_task.set_next_task 函数 ( 进程放入执行队列 ) 二.select_task_rq 函数 ( 为进程选择 CPU ) 三.migrate_task_ ...

  7. 【Linux 内核】调度器 ④ ( sched_class 调度类结构体分析 | yield_task 函数 | heck_preempt_curr 函数 | task_struct 函数 )

    文章目录 一.yield_task 函数 ( 放弃 CPU 执行权限 ) 二.check_preempt_curr 函数 ( 检查进程是否可以被抢占 ) 三.task_struct 函数 ( 选择运行 ...

  8. 【Linux 内核】调度器 ③ ( sched_class 调度类结构体分析 | next 字段 | enqueue_task 函数 | dequeue_task 函数 )

    文章目录 一.next 字段 ( 指向链表中的下一个调度类 ) 二.enqueue_task 函数 ( 将进程加入执行队列 ) 三.dequeue_task 函数 ( 从执行队列中删除进程 ) Lin ...

  9. 【Linux 内核】调度器 ① ( 调度器概念 | 调度器目的 | 调度器主要工作 | 调度器位置 | 进程优先级 | 抢占式调度器 | Linux 进程状态 | Linux 内核进程状态 )

    文章目录 一.调度器 0.调度器概念 1.调度器目的 2.调度器主要工作 3.调度器位置 4.进程优先级 5.抢占式调度器 二.Linux 内核进程状态 API 简介 三.Linux 进程状态 一.调 ...

  10. (6)Linux进程调度-实时调度器

    目录 背景 1. 概述 2. 数据结构 3. 流程分析 3.1 运行时统计数据 3.2 组调度 3.3 带宽控制 3.4 调度器函数分析 3.4.1 pick_next_task_rt 3.4.2 e ...

最新文章

  1. 傅里叶变换才是本质?谷歌这项研究GPU上快7倍、TPU上快2倍
  2. CF 67A - Partial Teacher
  3. synchronized 面试五连击
  4. discuz!5.5.0安装方法及常见问题解决
  5. 【无码专区6】球与盒子(数学线性筛)
  6. Web前端JavaScript笔记(4)节点
  7. 记一次线程池任务执行异常
  8. 洛谷——P1554 梦中的统计
  9. python 递归函数例子
  10. 湘潭大学计算机网络安全学院,省委网信办与湘潭大学共建网络空间安全学院签约暨揭牌仪式举行...
  11. position的属性(sticky属性)
  12. vue前端进阶之SSR篇 --- 搭建简单的SSR框架
  13. python基本写法_Python的表达式写法
  14. python 沪江_Python基础篇 -- 字符串
  15. 西安面试第一天面试问题总结
  16. Mysql数据库乱码解决方案
  17. [转载]使用JDBC创建数据库对象
  18. 简易版GameFramework游戏框架搭建教程(一)前言
  19. 一次尝试绕过ClassLoader双亲委派的实验
  20. java基于微信小程序的公交线路查询系统 uniapp 小程序

热门文章

  1. 线段树相关(研究总结,线段树)
  2. ASP.NET偷懒大法三 (利用Attribute特性简化多查询条件拼接sql语句的麻烦)
  3. 剖析Disruptor:为什么会这么快?(二)神奇的缓存行填充
  4. hadoop1.0集群搭建
  5. ado.net 实体类_数据访问类
  6. 一个按照行来截取显示文章摘要的函数
  7. 动态修改ViewPagerIndicator CustomTabPageIndicator Tab标签文字颜色
  8. 【短语学习】True(False) Positives (Negatives) 的含义和翻译
  9. 海南航空宁波到重庆的变态机票
  10. Spring之AOP理解