和所有的time sharing 系统一样,Linux通过短时间内进程切换来模拟实现多个进程同时运行的
假象
。前面我们讨论了进程的基本实现,本章关注进程的调度。
Scheduling Policy
Linux的进程调度基于时间片的,每个进程运行一段时间后被换下。然而并没有那么简单,还需要根据进程的优先级进行调度。进程分为三个类型:
Interactive processes 交互性的应用如文本编辑器,这类程序的响应时间必须快一些,因为调度引入的延迟有需要短
Batch processes 运行在后台,不与用户交互,因此慢一点也没啥关系
Real-time processes 有严格调度要求,这种类型的进程不能被低优先级的进程所阻塞并且也需要保证响应时间要比较固定。
对于real time process的描述比较模糊,另外的定义可以参考:
https://tldp.org/LDP/tlk/kernel/processes.html
This is the scheduling policy that will be applied to this process.
There are two types of Linux process, normal and real time. Real time processes have a higher priority than all of the other processes. If there is a real time process ready to run, it will always run first
. Real time processes may have two types of
policy
,
round robin
and
first in first out
. In
round robin
scheduling, each runnable real time process is run in turn and in
first in, first out
scheduling each runnable process is run in the order that it is in on the run queue and that order is never changed.
Process Preemption
Linux 进程是可抢占的,当一个进程进入到
TASK_RUNNING
状态时,内核会确认该进程的priority是否大于此时的current来决定是否换下当前进程转而去执行刚刚成为runnable的进程。当然进程随着时间片结束也会被中断执行,设置进程的
TIF_NEED_RESCHED
flag,在时钟的中断处理函数中调用scheduler。
一个具体的例子,假设此时系统中只有一个文本编辑器(text editor)和compiler,文本编辑器是交互式程序,调度的优先级必然高于其他进程,但是按下键位的频率很低(无论你打字多块和计算机比起来都是很慢了),所以文本编辑器大部分时间处于被挂起状态。然而,当按下键位时,内核发现文本编辑器的优先级比编译器高多了,所以将编译器进程的flag设置为
TIF_NEED_RESCHED
,在中断结束后让scheduler将文本编辑器换下去。等待文本编辑器处理好了输入的文字,又陷入了挂起状态等待下一次的keypress到来。
注意,被换下去的进程并不是处于被挂起的状态,它仍然是
TASK_RUNNING
状态,
只不过它不再占有CPU
。
Note
: 这里说的中断结束后被换下去的逻辑是,进程被设置了
TIF_NEED_RESCHED
,所有的中断处理函数最终会调用
ret_from_inter
和
ret_from_exp
两者最终都会判断进程是否被设置了
TIF_NEED_RESCHED
进而确定是否换下进程,参考中断这一章。
How Long Must a Quantum Last?
时间片到底多长呢?这实际上是一个需要权衡的值,假设一次进程切换需要5ms,时间片为10ms,那么一个时间片一半都用于进程切换,十分浪费。如果时间片过长,那么系统的并发性就很差,因为就绪进程需要等待很久才可以执行。一个常见的误区是,认为过长的时间片对于交互式应用的效果不好,然而这是不对的,如同前面所讨论的,交互式应用有更高的优先级,
处于runnable状态会对现在的进程进行抢占
。
The Scheduling Algorithm
早期的Linux调度器实现十分简单,就是遍历所有可以runnable的进程,根据它们的优先级选择一个最合适的进程运行。显然该调度器的效果很差,随着进程数量的增加,调度的时间会被延长。在Linux 2.6当中的调度算法更加复杂,因为它
将选择进程
的过程是常数时间的,无论实际可运行的进程有多少,它在多CPU以及调度交互式应用和batch processes都有了很好的提升。当没有进程执行到时候,scheduler总会执行swapper进程,它的pid为0,每个CPU都有自己的swapper进程。
进程会根据下面三种调度策略调度,书上对于这小部分的描述不好,
https://man7.org/linux/man-pages/man7/sched.7.html更合适
SCHED_FIFO: First-In, First-Out,进程可以持续的运行除非它主动放弃CPU或者有优先级更高的进程抢占。被抢占时,它加入到它所处的优先级的run queue的头部,当优先级比高的进程都执行完毕后恢复执行。
SCHED_RR: 和FIFO差不多,但是当时间片用完以后它会被换下,
并且加入到它所处的run queue的尾部
。
SCHED_NORMAL: A conventional, time-shared process.
Scheduling of Conventional Processes
每个普通进程有一个静态优先级(static priority),可选的取值是100(最高优先级)到139(最低优先级),新创建的进程会从父进程继承static priority,用户可以用过
nice()
和
setpriority()
来调整。
Base time quantum
static priority 决定了进程的时间片,由一下的表达计算:
根据表达式可知,static priority更高的进程有更长的时间片因此可以占据CPU的时间越长。下图是常见的进程优先级、它的nice值以及时间片长度:
Dynamic priority and average sleep time
dynamic priority的作用是调度器决定哪一个进程去执行,根据如下的表达式计算:
bonus在0到10之间浮动,小于5将会降低dynamic priority,反之则增加。bonus的取值根据进程的过去的信息,准确的说是根据他的average sleep time,指的是它过去多少时间用于sleeping。下面是休眠时间和bonus之间的关系,granularity的作用后面会讲述:
scheduler根据average sleep time来判断进程是否为交互性进程(interactive process),表达式如下:
该表达式成立那么就是一个交互性进程。
Active and expired processes
在前面的描述中,我们说过static priority决定进程可用的时间片,但是高优先级的进程不应该彻底的阻塞低优先级机场的执行。为了避免低优先级进程的饥饿,当一个进程用完了它的时间片,可以被低优先级并且时间片还没有用完的进程所替代。为了实现这一目的,调度器又将进程划分为两种类型:
Active processes 处于runnable状态并且时间片还没有用完的进程
Expired processes 处于runnable的进程,但是时间片用完了,只有等所有的active进程运行结束以后才可以继续运行
不过实际的情况会稍微复杂一些,详情见书中文档。
Scheduling of Real-Time Processes
Data Structures Used by the Scheduler
在进程一章中,我们说过进程都是以双向链表组织在一起的,所有处于
TASK_RUNNING
状态的进程所在的链表称为runqueue。
The runqueue Data Structure
runqueue
是Linux 2.6调度器中最重要的数据结构。
每个CPU都有自己的runqueue,存在一个runqueues的per-CPU variable
,
this_rq()
宏返回当前CPU的runqueue,
cpu_rq(n)
返回nth cpu的runqueue。
struct runqueue
源码位于kernel/sched.c,其中最重要是管理线程的双向链表,
任何一个进程只能属于一个runqueue
,不过后面我们会学习到,一个进程可以从一个runqueue迁移到另外一个runqueue。runqueue 结构里的
arrays
成员变量是一个长度为2的
struct prio_array_t
数组,源码如下: