1. 前言
在计算机中,进程的数量远多于cpu的数量,所以就存在,多个进程抢占一个cpu的情况,所以就需要一套规则,决定这些进程被处理的顺序,这就叫做进程调度。
在我的简单理解下,其实就是把进程放在一个队列中,cpu挨个去执行,但是后面知道了进程具有并发性,其实就是,一个cpu在某一时刻,只能处理一个进程,但是cpu并不会处理完这个进程,而是处理很短的时间(毫秒级别), 进程在cpu上跑的时间段,我们称之为时间片。不论处理的怎么样,结束没结束不重要,接着处理下一个,这个进程中间的上下文数据被保存到进程PCB中,然后去排队吧。
这就是并发,虽然在某一时刻,我只在处理一个任务,但是在一个时间段,我就相当于同时处理多个任务。这些任务是被同步推进的。
后面还有进程优先级的概念,就相当于在排队,但是你VIP你就可以按照规则排在前面。
但是对于cpu而言,他只是负责计算,至于这些进程的优先级处理我并不关心,我只想要知道下一个进程是谁。那进程排序的规则是什么,又是谁来维护呢?
2. 进程调度器
进程调度器,它负责计算并决定一个进程何时获取CPU时间以及占用CPU的时长。
不要害怕,这些是我总结的大部分内容的结构图 ,接下来我会一一介绍这些
你应该看到了,进程调度器在最上面,和cpu交互的那个sched_class,Linux设计的一个结构体类型,里面定义了很多抽象的接口(函数指针)。
struct sched_class{
// 链表
const struct sched_class* next;
// 向运行队列添加一个进程
void(*enqueue_task)(struct rq* rq, struct task_struct* p, int flags);
// ...
// 挑选下一个优先级更高的进程
struct task_struct(*pick_next_task)(struct rq* rq, struct task_struct* prev, struct rq_flags* rf);
}
enqueue_task:向运行队列中插入一个进程
pick_next_task:从运行队列中挑选下一个优先级更高的进程
里面类似的函数指针还有很多,实现不同的功能。其次调度器其实是一个链表。进程通用调度器提供了一个模板,调度类其实就是这些类型的实现,以及对这些接口的实现。
3. 进程调度类
为什么要实现这么多的调度类呢,因为不同的使用场景:
- stop调度类,是系统内核线程所使用,用户不能使用,优先级最高,任务一但被执行,它将不能被抢占,不能被切换,其将一直执行下去,直至进程执行完或主动让出cpu。
- 截止日期(dl)调度类,这个任务有最后期限,必须在任务最后期限之前完成,例如播放视频,一秒钟60帧的视频,大概每16毫秒就要播放一帧画面,这个就是最后期限
- 实时(rt)调度类,需要立马执行的任务,需要具有实时的特性,就像驾驶系统的刹车任务,必须要实时响应
- 完全公平(fair)调度类,这些任务都是完全公平的接受“相同”的时间,这个时间其实是虚拟时间,后面会说
- 空闲(ide)调度类,没有其他进程需要执行,就轮到它了
因为每个调度类都有自己的排序规则,所以Linux就使用这种设计:第一层,定义结构体类型,定义抽象的操作接口,比如向运行队列插入一个任务,从运行队列中挑选一个任务;第二层,调度类,根据自身类的特点,实现具体的操作。
通过这样两层,调度器可以从每个调度类的细节实现中抽离出来
4. 进程运行队列
运行队列,顾名思义,运行队列...
调度类,是方法的实现,你需要插入任务啊,还是删除任务,还是选择任务,这些方法都可以通过调度类的函数方法实现,但是没有数据只有方法肯定是不行的。
运行队列,其实就是对各个进程的通过数据结构管理起来,简而言之存放进程的地方
不同的调度类,需要不同的数据结构来进行管理,所以就出现了不同的运行队列,例如实时调度类就要有先进先出队列,环形队列。截止日期调度类和完全公平调度类依赖的数据结构都是红黑树。
5. 进程调度过程
进程的调度是从调用通用调度器开始的,kernel/sched/core.c中定义的schedule()函数。该函数的功能是挑选下一个最佳的可运行任务。schedule()函数中的pick_next_task()遍历调度类中包含的所有对应的函数,并最终选出要运行的下一个最佳任务。
---摘自《精通Linux内核开发》
prev是一个task_struct*类型的指针,task_struct内部包含一个sched_class*类型的指针,指向该进程属于的调度类。
6. CFS完全公平调度类(浅谈)
CFS,这里主要谈谈以下三点:虚拟运行时间(vruntime),权重计算,红黑树排序
前面两个主要是为了CFS的公平和优先级,最后一个决定运行队列的数据结构
如何能够保证在这个类中的所有进程都是完全公平的接受cpu的调度呢,但是还有优先级。你一听,这不是互相矛盾嘛,又要公平,又要有优先级。确实矛盾。但是没办法,就是在有优先级的情况下实现公平。
如果只要公平,那就每个进程都运行相同的时间,如果要优先级,那就你先我后,但是你忘记并发了嘛,必须要每一个进程都要上去跑一会。
所以虚拟运行时间(vruntime)和权重计算就是这么来的。
每一个进程都有一个真实运行时间和虚拟运行时间,真实运行时间,就是你真实在cpu上跑了多少毫秒,vruntime其实是根据真实运行时间和优先级权重的权重计算而来的,然后再红黑树中按照vruntime来进行排序。每次pick_next_task都会选择红黑树最左端的进程。
nice值标识进程的优先级,nice值每减少1,CPU的时间片会增加10%
例如:一个A进程nice值为0,另一个B进程nice值为1,假如A进程的时间片是10ms,它也真实跑了10ms,那么他的vruntime就会加10,而B进程时间片是11ms,它也真实跑了11ms,但是根据权重计算,它的vruntime只会加10.由此实现完全公平。
7. 实时调度类(浅谈)
实时调度类,它的运行队列的数据结构是带头双向链表。
它有两种调度策略:
SCHED_FIFO(先进先出实时调度策略)
进程一旦获得CPU执行权,就会一直运行下去,直到该进程自愿放弃CPU,实时进程按照优先级队列排序
SCHED_RR(轮转实时调度策略)
进程在执行完一个时间片后,即使没有完成任务,也会被迫让出CPU给同一优先级的其他进程,同一优先级的实时进程能够实现时间片的轮转,确保在紧迫性相同的情况下公平分配CPU时间
8. 总结
Linux内核的知识非常多,对于进程调度这一块内容有很多,这篇博客只能带大家揭开内核神秘面纱的一角,希望大家有所收获。
关于进程调度器的代码啊,我建议大家可以看看这篇博客:http://t.csdnimg.cn/ORcS7
完