Linux-进程调度器

1. 前言 

        在计算机中,进程的数量远多于cpu的数量,所以就存在,多个进程抢占一个cpu的情况,所以就需要一套规则,决定这些进程被处理的顺序,这就叫做进程调度。

        在我的简单理解下,其实就是把进程放在一个队列中,cpu挨个去执行,但是后面知道了进程具有并发性,其实就是,一个cpu在某一时刻,只能处理一个进程,但是cpu并不会处理完这个进程,而是处理很短的时间(毫秒级别), 进程在cpu上跑的时间段,我们称之为时间片。不论处理的怎么样,结束没结束不重要,接着处理下一个,这个进程中间的上下文数据被保存到进程PCB中,然后去排队吧。

5e0106a7c5b34d41b826fee154b7e028.png

        这就是并发,虽然在某一时刻,我只在处理一个任务,但是在一个时间段,我就相当于同时处理多个任务。这些任务是被同步推进的。

        后面还有进程优先级的概念,就相当于在排队,但是你VIP你就可以按照规则排在前面。

        但是对于cpu而言,他只是负责计算,至于这些进程的优先级处理我并不关心,我只想要知道下一个进程是谁。那进程排序的规则是什么,又是谁来维护呢?

2. 进程调度器

        进程调度器,它负责计算并决定一个进程何时获取CPU时间以及占用CPU的时长。

a74bb365b5b44fefaf7a5b9bc763cb7d.png

        不要害怕,这些是我总结的大部分内容的结构图 ,接下来我会一一介绍这些

        你应该看到了,进程调度器在最上面,和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. 进程运行队列

        运行队列,顾名思义,运行队列...

        调度类,是方法的实现,你需要插入任务啊,还是删除任务,还是选择任务,这些方法都可以通过调度类的函数方法实现,但是没有数据只有方法肯定是不行的。

        运行队列,其实就是对各个进程的通过数据结构管理起来,简而言之存放进程的地方

5cff9994499a4b16bd1d9ad1cfa2f8af.png

        不同的调度类,需要不同的数据结构来进行管理,所以就出现了不同的运行队列,例如实时调度类就要有先进先出队列,环形队列。截止日期调度类和完全公平调度类依赖的数据结构都是红黑树。 

5. 进程调度过程

        进程的调度是从调用通用调度器开始的,kernel/sched/core.c中定义的schedule()函数。该函数的功能是挑选下一个最佳的可运行任务。schedule()函数中的pick_next_task()遍历调度类中包含的所有对应的函数,并最终选出要运行的下一个最佳任务。        

---摘自《精通Linux内核开发》

c31f3b032dfa4a219c1c0fdd99af6013.png

        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

完 

 

  • 37
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值