Linux内核之 进程调度
上一篇我们提到过进程状态,而进程调度主要是针对TASK_RUNNING运行状态进行调度,因为其他状态是不可执行比如睡眠,不需要调度。
1、进程调度概念
进程调度程序,简称调度程序,它是确保进程能有效工作的一个内核子系统。调度程序负责决定哪个进程投入运行,何时运行以及运行多长时间。
多任务
多任务操作系统是指能同时并发执行多个进程的操作系统。
多任务系统划分为两类:非抢占式多任务(cooperative multitasking)和抢占式多任务(preemptive multitasking)。
非抢占式是一种协作的方式,一个进程一直执行直到任务结束或者主动退出才切换到下一个进程。
抢占式是大部分操作系统采用的方式,是指给每个进程分配一个时间片(time slice),当时运行时间达到规定的时间时则会切换到下一个进程。
2、调度策略
上面提到的时间片策略是比较传统的方式,后面Linux系统进行了多次改进,比如O(1)算法、电梯算法和CFS等。那么改进的动机和依据是什么呢,我们来看看。
2.1 I/O 消耗型和 CPU 消耗型
进程根据资源使用可以分为这两大类。
I/O 消耗型:进程的大部分时间用来进行 I/O 的请求或者等待,比如键盘。这种类型的进程经常处于可以运行的状态,但是都只是运行一点点时间,绝大多数的时间都在处于阻塞(睡眠)的状态。
CPU 消耗型:进程的大部分时间用在执行代码上即CPU运算,比如开启 Matlab 做一个大型的运算。除非被抢占,否则它们可以一直运行,所以它们没有太多的 I/O 需求。调度策略往往是尽量降低他们的调度频率,而延长其运行时间。
当然这种划分不是绝对的,一般的应用程序同时包含两种行为。
所以调度策略通常需要在两个矛盾的目标中寻求平衡:进程响应迅速(响应时间短)和最大系统利用率(高吞吐量)。
Linux 系统为了提升响应的速度,倾向于优先调度 I/O 消耗型。
2.2 进程优先级
调度算法中最基本的一类就是基于优先级的调度,根据进程的价值(重要性)和对处理器时间的需求来对进程分级的想法。简单的说是优先级高的先运行,低的后运行。
Linux采用了两种不同的优先级范围。
(1)nice值
它的范围从-20到+19,默认值0。越大的nice值优先级越低,19优先级最低,-20优先级最高。ps -ef命令中,NI标记就是进程对应的nice值。
这是普通进程的优先级。
(2)实时优先级
范围是 0~99,与 nice 值相反,值越大优先级越高。
这是实时进程的优先级,相对普通进程的,所以任何实时进程的优先级都高于普通进程的优先级。
时间片是一个数值,它表明在抢占前所能持续运行的时间。调度策略必须规定一个默认的时间片,这并非易事。因为时间片过长I/O消耗型的线程得不到及时响应,而太短CPU消耗型的需要频繁被切换,吞吐量会下降。而最新的Linux调度策略CFS不采用固定的时间片,而是采用了处理器的使用比。我们接下来详细介绍。
3、调度算法
Linux调度器是以分类(模块化)的方式提供的,即对不同类型的进程进行分组并且分别选择相应的算法。
这种调度结构被称为调度器类(scheduler classes),它允许不同的可动态添加的调度算法并存,调度属于自己范畴的进程。
如下图Linux调度器包含了多种调度器类。
这些调度器类的优先级顺序为: Stop_Task > Real_Time > Fair > Idle_Task。
开发者可以根据己的设计需求把所属的Task配置到不同的scheduler classes中。其中的Real_Time和Fair是最常用的,也对应了我们上面提到的实时进程和普通进程。
3.1 完全公平调度
Fair调度使用的完全公平调度器(Completely Fair Scheduler,CFS)。
这是一个针对普通进程的调度类,在Linux中称为SCHED_NORMAL(在POSIX中称为SCHED_OTHER)。
传统的时间片方式是每个进程固定一个时间,那么当进程个数变化时,整个调度周期顺延。时间片还会跟着系统定时器节拍随时改变,那么整个周期再次跟着变化。那么优先级低的进程可能迟迟得不到调度。
而CFS把整个调度周期的时间固定,该周期叫目标延迟(target latency),也不再采用时间片,而是根据每个进程的nice值得到的权重再计算得到处理器比例,进而得到进程自己的时间。该时间和节拍没有任何关系,也可以精确到ns。例如“目标延迟”设置为20ms,2个进程各10毫秒,如果4个进程则是各5毫秒。如果100个进程呢,是不是就是0.2毫秒呢?
不一定,CFS引入了一个关键特性:最小粒度。即每个进程获得时间片的最小值,默认是1毫秒。
为了公平起见,CFS总是选择运行最少(vruntime)的进程作为下一个运行进程。所以这样照顾了I/O消耗型短时间处理的需求,也将更多时间留给了CPU消耗型的程序。确实解决了多进程环境下因延迟带来的不公平性。
vruntime虚拟实时
在 CFS 中,给每一个进程安排了一个虚拟时钟vruntime(virtual runtime),这个变量并非直接等于他的绝对运行时间,而是根据运行时间放大或者缩小一个比例,CFS使用这个vruntime 来代表一个进程的运行时间。如果一个进程得以执行,那么他的vruntime将不断增大,直到它没有执行。没有执行的进程的vruntime不变。调度器为了体现绝对的完全公平的调度原则,总是选择vruntime最小的进程,让其投入执行。他们被维护到一个以vruntime为顺序的红黑树rbtree中,每次去取最小的vruntime的进程(最左侧的叶子节点)来投入运行。实际运行时间到vruntime的计算公式为:
[ vruntime = 实际运行时间 * 1024 / 进程权重 ]
这里的1024代表nice值为0的进程权重。所有的进程都以nice为0的权重1024作为基准,计算自己的vruntime。
挑选的进程进行运行了,它运行多久?进程运行的时间是根据进程的权重进行分配。
[ 分配给进程的运行时间 = 调度周期 *(进程权重 / 所有进程权重之和) ]
虚拟运行时间是通过进程的实际运行时间和进程权重(weight)计算出来的。在CFS调度器中,将进程优先级这个概念弱化,而是强调进程的权重。一个进程的权重越大,则说明这个进程更需要运行,因此它的虚拟运行时间就越小,这样被调度的机会就越大。
关于nice和进程权重以及vruntime之间的计算方式非常复杂。有兴趣的可以在网上搜索或者看源码。
总之,nice对时间片的作用不再是算数加权,而是几何加权。
3.2 实时调度策略
实时调度策略分为两种:SCHED_FIFO 和 SCHED_RR。
这两种实时进程都比任何普通进程的优先级更高(SCHED_NORMAL),都会比他们更先得到调度。
SCHED_FIFO:一个这种类型的进程出于可执行的状态,就会一直执行,直到它自己被阻塞或者主动放弃 CPU;它不基于时间片,可以一直执行下去,只有更高优先级的SCHED_FIFO或者SCHED_RR才能抢占它的任务,如果有两个同样优先级的SCHED_FIFO任务,它们会轮流执行,其他低优先级的只有等它们变为不可执行状态,才有机会执行。
SCHED_RR:与SCHED_FIFO大致相同,只是SCHED_RR级的进程在耗尽事先分配给它的时间后就不能再执行了。所以SCHED_RR是带有时间片的SCHED_FIFO:一种实时轮流调度(Realtime Robin)算法。
上述两种实时算法实现的都是静态优先级。内核不为实时进程计算动态优先级,保证给定的优先级的实时进程总能够抢占比他优先级低的进程。
4、调度的实现
进程调度的主要入口点是函数schedule(),即实现进程切换的功能:选择哪个进程可以运行,何时投入运行。
该函数的核心是for()循环,它以优先级为序,从最高的优先级调度类开始,遍历所有的调度类。
进程状态可以分为可执行和不可执行,分别放入不同的结构中。可执行的进程放在红黑树中,而不可执行的放在等待队列。
一个进程可能在两种结构中不断移动。
比如读文件操作,在执行工作时,处在红黑树中,当读完时可能需要等待磁盘,这时会把自己标记成休眠状态,从红黑树中移出,放入等待队列,然后调用schedule()选择和执行一个其他进程。而当磁盘作业完成时,又会被唤醒,进程再次设置为可执行状态,然后从等待队列中移到红黑树中。
4.1 抢占与上下文切换
上下文切换,就是从一个可执行进程切换到另一个可执行进程,由context_switch()函数处理。每一个新的进程被选出来准备投入运行的时候,schedule()就会调用该函数。
自愿切换意味着进程需要等待某种资源,强制切换则与抢占(Preemption)有关。
抢占(Preemption)是指内核强行切换正在CPU上运行的进程,在抢占的过程中并不需要得到进程的配合,在随后的某个时刻被抢占的进程还可以恢复运行。发生抢占的原因主要有:进程的时间片用完了,或者优先级更高的进程来争夺CPU了。
抢占的过程分两步,第一步触发抢占,第二步执行抢占,这两步中间不一定是连续的,有些特殊情况下甚至会间隔相当长的时间:
- 触发抢占:给正在CPU上运行的当前进程设置一个请求重新调度的标志(TIF_NEED_RESCHED),仅此而已,此时进程并没有切换。
- 执行抢占:在随后的某个时刻,内核会检查TIF_NEED_RESCHED标志并调用schedule()执行抢占。
抢占只在某些特定的时机发生,这是内核的代码决定的。
(1)触发抢占的时机
每个进程都包含一个TIF_NEED_RESCHED标志,内核根据这个标志判断该进程是否应该被抢占,设置TIF_NEED_RESCHED标志就意味着触发抢占。
直接设置TIF_NEED_RESCHED标志的函数是set_tsk_need_resched();
触发抢占的函数是resched_task()。
TIF_NEED_RESCHED标志什么时候被设置呢?在以下时刻:
周期性的时钟中断
时钟中断处理函数会调用scheduler_tick(),这是调度器核心层(scheduler core)的函数,它通过调度类(scheduling class)的task_tick方法检查进程的时间片是否耗尽,如果耗尽则触发抢占。
唤醒进程的时候
当进程被唤醒的时候,如果优先级高于CPU上的当前进程,就会触发抢占。相应的内核代码中,try_to_wake_up()最终通过check_preempt_curr()检查是否触发抢占。
新进程创建的时候
如果新进程的优先级高于CPU上的当前进程,会触发抢占。相应的调度器核心层代码是sched_fork(),它再通过调度类的task_fork方法触发抢占。
进程修改nice值的时候
如果进程修改nice值导致优先级高于CPU上的当前进程,也会触发抢占。内核代码参见 set_user_nice()。
进行负载均衡的时候
在多CPU的系统上,进程调度器尽量使各个CPU之间的负载保持均衡,而负载均衡操作可能会需要触发抢占。
不同的调度类有不同的负载均衡算法,涉及的核心代码也不一样,比如CFS类在load_balance()中触发抢占;RT类的负载均衡基于overload,如果当前运行队列中的RT进程超过一个,就调用push_rt_task()把进程推给别的CPU,在这里会触发抢占。
(2)执行抢占的时机
触发抢占通过设置进程的TIF_NEED_RESCHED标志告诉调度器需要进行抢占操作了,但是真正执行抢占还要等内核代码发现这个标志才行,而内核代码只在设定的几个点上检查TIF_NEED_RESCHED标志,这也就是执行抢占的时机。
抢占如果发生在进程处于用户态的时候,称为User Preemption(用户态抢占);如果发生在进程处于内核态的时候,则称为Kernel Preemption(内核态抢占)。
执行User Preemption(用户态抢占)的时机
- 从系统调用(syscall)返回用户态时;
- 从中断处理程序返回用户态时;
执行Kernel Preemption(内核态抢占)的时机
Linux在2.6版本之后就支持内核抢占了,但是请注意,具体取决于内核编译时的选项:
CONFIG_PREEMPT_NONE=y
不允许内核抢占。这是SLES的默认选项。
CONFIG_PREEMPT_VOLUNTARY=y
在一些耗时较长的内核代码中主动调用cond_resched()让出CPU。这是RHEL的默认选项。
CONFIG_PREEMPT=y
允许完全内核抢占。
在 CONFIG_PREEMPT=y 的前提下,内核态抢占的时机是:
- 中断处理程序返回内核空间之前会检查TIF_NEED_RESCHED标志,如果置位则调用preempt_schedule_irq()执行抢占。preempt_schedule_irq()是对schedule()的包装。
- 当内核从non-preemptible(禁止抢占)状态变成preemptible(允许抢占)的时候;在preempt_enable()中,会最终调用preempt_schedule()来执行抢占。preempt_schedule()是对schedule()的包装。
“抢占”这一部分来自网上,条理比书上更清晰,但是和书上也稍有差别,大体一致,不影响整体理解。
参考资料:
《Linux内核设计与实现》原书第三版
https://blog.csdn.net/zhoutaopower/article/details/86290196
Linux内核之 进程调度的更多相关文章
- 深入理解Linux内核-进程调度
1.什么时候进行进程切换 调度策略目标:1.进程响应尽量快:2.后台作业吞吐量尽量高:3.尽可能避免进程饥饿:4.低优先级和高优先级进程需要尽量调和. 调度策略:决定什么时候选择什么进程运行的规则.基 ...
- 【读书笔记】《Linux内核设计与实现》进程管理与进程调度
大学跟老师做嵌入式项目,写过I2C的设备驱动,但对Linux内核的了解也仅限于此.Android系统许多导致root的漏洞都是内核中的,研究起来很有趣,但看相关的分析文章总感觉隔着一层窗户纸,不能完全 ...
- free-electrons linux内核和驱动
操作系统的三个作用:1.管理硬件资源:2.提供独立于架构和硬件的可移植的软件接口3.处理不同应用对相同硬件资源的同时访问 系统调用接口是稳定的,系统调用由c函数库封装,用户程序基本不需要直接调用系统函 ...
- linux内核的组成,王明学learn
linux内核的组成 一.linux内核源代码目录结构 arch: 包含和硬件体系结构相关的代码, 每种平台占一个相应的目录, 如 i386.ARM.PowerPC.MIPS 等. block:块设备 ...
- Linux 内核同步机制
本文将就自己对内核同步机制的一些简要理解,做出一份自己的总结文档. Linux内部,为了提供对共享资源的互斥访问,提供了一系列的方法,下面简要的一一介绍. Technorati 标签: ...
- Linux设备驱动开发详解-Note(5)---Linux 内核及内核编程(1)
Linux 内核及内核编程(1) 成于坚持,败于止步 Linux 2.6 内核的特点 Linux 2.6 相对于 Linux 2.4 有相当大的改进,主要体现在如下几个方面. 1.新的调度器 2.6 ...
- 对于Linux内核执行过程的理解(基于fork、execve、schedule等函数)
382 + 原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/ 一.实验环境 win10 -> VMware -> Ubuntu1 ...
- Linux内核同步
Linux内核剖析 之 内核同步 主要内容 1.内核请求何时以交错(interleave)的方式执行以及交错程度如何. 2.内核所实现的基本同步机制. 3.通常情况下如何使用内核提供的同步机制. 内核 ...
- 【内核】嵌入式linux内核的五个子系统
Perface Linux内核主要由进程调度(SCHED).内存管理(MM).虚拟文件系统(VFS).网络接口(NET)和进程间通信(IPC)5个子系统组成,如图1所示. 图1 Linux内核的组成部 ...
随机推荐
- xenomai内核解析之信号signal(一)---Linux信号机制
版权声明:本文为本文为博主原创文章,转载请注明出处.如有错误,欢迎指正.博客地址:https://www.cnblogs.com/wsg1100/ 目录 1. Linux信号 1.1注册信号处理函数 ...
- vue & 百度地图:使用百度地图
index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> &l ...
- [leetcode/lintcode 题解] 微软面试题:股票价格跨度
编写一个 StockSpanner 类,它收集某些股票的每日报价,并返回该股票当日价格的跨度. 今天股票价格的跨度被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天). 例如 ...
- $0.\dot{9}=1,是指以1为极限,而非初等数学的相等“=”$
$注:文中的讨论,没有使用严格的 \epsilon 极限定义,而是简单假设$ 按照中小学的定义,整数,有限小数,无限循环小数是有理数.无限不循环小数是无理数. $\frac{1}{3}=0.\dot{ ...
- Lua学习入门(代码块)
). if then else if a < then b = else b = end ). if elseif else then if a < then b = elseif a = ...
- 第十二章 类加载器&反射
12.1.类加载器 12.1.1.类加载 当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过类的加载.类的连接.类的初始化这三个步骤来对类进行初始化.如果不出现意外情况,JVM将会连续完成 ...
- Jmeter 中 CSV 如何参数化测试数据并实现自动断言
当我们使用Jmeter工具进行接口测试,可利用CSV Data Set Config配置元件,对测试数据进行参数化,循环读取csv文档中每一行测试用例数据,来实现接口自动化.此种情况下,很多测试工程师 ...
- PHP array_diff_uassoc() 函数
实例 比较两个数组的键名和键值(使用用户自定义函数比较键名),并返回差集: <?phpfunction myfunction($a,$b){if ($a===$b){return 0;}retu ...
- PHP date_create_from_format() 函数
------------恢复内容开始------------ 实例 返回一个根据指定格式进行格式化的新的 DateTime 对象: <?php$date=date_create_from_for ...
- PHP timezone_name_from_abbr() 函数
------------恢复内容开始------------ 实例 根据时区缩略语返回时区名称: <?phpecho timezone_name_from_abbr("EST" ...