下进程与线程的调度策略

***#linux 下进程与线程的调度策略#
进程被操作系统创建,并需要相当多的“开支”,进程包含如下程序资源和程序执行状态信息:

  1. 进程ID,进程群组ID,用户ID,群组ID
  2. 环境
  3. 工作目录
  4. 程序指令
  5. 寄存器
  6. 文件描述符
  7. 信号动作
  8. 共享库
  9. 进程间通信工具(例如消息队列,管道,信号量,共享内存)
    线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。
    线程使用和在进程内的生存,仍由操作系统来安排并且独立的实体来运行,很大程度上是因为它们为可执行代码的存在复制了刚刚好的基本资源。
    这个独立的控制流之所以可以实现,是因为线程维护着如下的东西:
  10. 栈指针
  11. 寄存器
  12. 调度属性(例如规则和优先级)
  13. 等待序列和阻塞信号
  14. 线程拥有的数据
    所以,总的来说,Unix环境里的线程有如下特点:
    它生存在进程中,并使用进程资源;
    拥有它自己独立的控制流,前提是只要它的父进程还存在,并且OS支持它;
    它仅仅复制可以使它自己调度的必要的资源;
    它可能会同其它与之同等独立的线程分享进程资源;
    如果父进程死掉那么它也会死掉——或者类似的事情;
    它是轻量级的,因为大部分的开支已经在它的进程创建时完成了。
    因为在同一进程内的线程分享资源,所以:
    一个线程对共享的系统资源做出的改变(例如关闭一个文件)会被所有的其它线程看到;
    指向同一地址的两个指针的数据是相同的;
    对同一块内存进行读写操作是可行的,但需要程序员作明确的同步处理操作。
    ##进程的调度策略##
  • 关于进程的优先级
    进程的优先级有2种度量方法,一种是nice值,一种是实时优先级。
    nice值的范围是-20~+19,值越大优先级越低,也就是说nice值为-20的进程优先级最大。
    实时优先级的范围是0~99,与nice值的定义相反,实时优先级是值越大优先级越高。
    实时进程都是一些对响应时间要求比较高的进程,因此系统中有实时优先级高的进程处于运行队列的话,它们会抢占一般的进程的运行时间。

进程的2种优先级会让人不好理解,到底哪个优先级更优先?一个进程同时有2种优先级怎么办?
其实linux的内核早就有了解决办法。
对于第一个问题,到底哪个优先级更优先?
答案是实时优先级高于nice值,在内核中,实时优先级的范围是 0~MAX_RT_PRIO-1 MAX_RT_PRIO的定义参见 include/linux/sched.h
1611 #define MAX_USER_RT_PRIO 100
1612 #define MAX_RT_PRIO MAX_USER_RT_PRIO
nice值在内核中的范围是 MAX_RT_PRIO~MAX_RT_PRIO+40 即 MAX_RT_PRIO~MAX_PRIO
1614 #define MAX_PRIO (MAX_RT_PRIO + 40)

第二个问题,一个进程同时有2种优先级怎么办?
答案很简单,就是一个进程不可能有2个优先级。一个进程有了实时优先级就没有Nice值,有了Nice值就没有实时优先级。
我们可以通过以下命令查看进程的实时优先级和Nice值:(其中RTPRIO是实时优先级,NI是Nice值)

$ ps -eo state,uid,pid,ppid,rtprio,ni,time,comm
S   UID   PID  PPID RTPRIO  NI     TIME COMMAND
S     0     1     0      -   0 00:00:00 systemd
S     0     2     0      -   0 00:00:00 kthreadd
S     0     3     2      -   0 00:00:00 ksoftirqd/0
S     0     6     2     99   - 00:00:00 migration/0
S     0     7     2     99   - 00:00:00 watchdog/0
S     0     8     2     99   - 00:00:00 migration/1
S     0    10     2      -   0 00:00:00 ksoftirqd/1
S     0    12     2     99   - 00:00:00 watchdog/1
S     0    13     2     99   - 00:00:00 migration/2
S     0    15     2      -   0 00:00:00 ksoftirqd/2
S     0    16     2     99   - 00:00:00 watchdog/2
S     0    17     2     99   - 00:00:00 migration/3
S     0    19     2      -   0 00:00:00 ksoftirqd/3
S     0    20     2     99   - 00:00:00 watchdog/3
S     0    21     2      - -20 00:00:00 cpuset
S     0    22     2      - -20 00:00:00 khelper
  • 关于时间片
    有了优先级,可以决定谁先运行了。但是对于调度程序来说,并不是运行一次就结束了,还必须知道间隔多久进行下次调度。
    于是就有了时间片的概念。时间片是一个数值,表示一个进程被抢占前能持续运行的时间。
    也可以认为是进程在下次调度发生前运行的时间(除非进程主动放弃CPU,或者有实时进程来抢占CPU)。
    时间片的大小设置并不简单,设大了,系统响应变慢(调度周期长);设小了,进程频繁切换带来的处理器消耗。默认的时间片一般是10ms

  • 调度实现原理(基于优先级和时间片)
    下面举个直观的例子来说明:
    假设系统中只有3个进程ProcessA(NI=+10),ProcessB(NI=0),ProcessC(NI=-10),NI表示进程的nice值,时间片=10ms

  1. 调度前,把进程优先级按一定的权重映射成时间片(这里假设优先级高一级相当于多5msCPU时间)。
    假设ProcessA分配了一个时间片10ms,那么ProcessB的优先级比ProcessA高10(nice值越小优先级越高),ProcessB应该分配105+10=60ms,以此类推,ProcessC分配205+10=110ms
  2. 开始调度时,优先调度分配CPU时间多的进程。由于ProcessA(10ms),ProcessB(60ms),ProcessC(110ms)。显然先调度ProcessC
  3. 10ms(一个时间片)后,再次调度时,ProcessA(10ms),ProcessB(60ms),ProcessC(100ms)。ProcessC刚运行了10ms,所以变成100ms。此时仍然先调度ProcessC
  4. 再调度4次后(4个时间片),ProcessA(10ms),ProcessB(60ms),ProcessC(60ms)。此时ProcessB和ProcessC的CPU时间一样,这时得看ProcessB和ProcessC谁在CPU运行队列的前面,假设ProcessB在前面,则调度ProcessB
  5. 10ms(一个时间片)后,ProcessA(10ms),ProcessB(50ms),ProcessC(60ms)。再次调度ProcessC
  6. ProcessB和ProcessC交替运行,直至ProcessA(10ms),ProcessB(10ms),ProcessC(10ms)。
    这时得看ProcessA,ProcessB,ProcessC谁在CPU运行队列的前面就先调度谁。这里假设调度ProcessA
  7. 10ms(一个时间片)后,ProcessA(时间片用完后退出),ProcessB(10ms),ProcessC(10ms)。
  8. 再过2个时间片,ProcessB和ProcessC也运行完退出。
    这个例子很简单,主要是为了说明调度的原理,实际的调度算法虽然不会这么简单,但是基本的实现原理也是类似的:
    1)确定每个进程能占用多少CPU时间(这里确定CPU时间的算法有很多,根据不同的需求会不一样)
    2)占用CPU时间多的先运行
    3)运行完后,扣除运行进程的CPU时间,再回到 1)
  • Linux上调度实现的方法
    Linux上的调度算法是不断发展的,在2.6.23内核以后,采用了“完全公平调度算法”,简称CFS。
    CFS算法在分配每个进程的CPU时间时,不是分配给它们一个绝对的CPU时间,而是根据进程的优先级分配给它们一个占用CPU时间的百分比。
    比如ProcessA(NI=1),ProcessB(NI=3),ProcessC(NI=6),在CFS算法中,分别占用CPU的百分比为:ProcessA(10%),ProcessB(30%),ProcessC(60%)
    因为总共是100%,ProcessB的优先级是ProcessA的3倍,ProcessC的优先级是ProcessA的6倍。

Linux上的CFS算法主要有以下步骤:(还是以ProcessA(10%),ProcessB(30%),ProcessC(60%)为例)
1)计算每个进程的vruntime(注1),通过update_curr()函数更新进程的vruntime。
2)选择具有最小vruntime的进程投入运行。(注2)
3)进程运行完后,更新进程的vruntime,转入步骤2) (注3)

注1. 这里的vruntime是进程虚拟运行的时间的总和。vruntime定义在:kernel/sched_fair.c 文件的 struct sched_entity 中。

注2. 这里有点不好理解,根据vruntime来选择要运行的进程,似乎和每个进程所占的CPU时间百分比没有关系了。
1)比如先运行ProcessC,(vr是vruntime的缩写),则10ms后:ProcessA(vr=0),ProcessB(vr=0),ProcessC(vr=10)
2)那么下次调度只能运行ProcessA或者ProcessB。(因为会选择具有最小vruntime的进程)
长时间来看的话,ProcessA、ProcessB、ProcessC是公平的交替运行的,和优先级没有关系。
而实际上vruntime并不是实际的运行时间,它是实际运行时间进行加权运算后的结果。
比如上面3个进程中ProcessA(10%)只分配了CPU总的处理时间的10%,那么ProcessA运行10ms的话,它的vruntime会增加100ms。
以此类推,ProcessB运行10ms的话,它的vruntime会增加(100/3)ms,ProcessC运行10ms的话,它的vruntime会增加(100/6)ms。
实际的运行时,由于ProcessC的vruntime增加的最慢,所以它会获得最多的CPU处理时间。
上面的加权算法是我自己为了理解方便简化的,Linux对vruntime的加权方法还得去看源码-

注3.Linux为了能快速的找到具有最小vruntime,将所有的进程的存储在一个红黑树中。这样树的最左边的叶子节点就是具有最小vruntime的进程,新的进程加入或有旧的进程退出时都会更新这棵树。

其实Linux上的调度器是以模块方式提供的,每个调度器有不同的优先级,所以可以同时存在多种调度算法。
每个进程可以选择自己的调度器,Linux调度时,首先按调度器的优先级选择一个调度器,再选择这个调度器下的进程。

  1. 调度相关的系统调用
    调度相关的系统调用主要有2类:
  1. 与调度策略和进程优先级相关 (就是上面的提到的各种参数,优先级,时间片等等) - 下表中的前8个
  2. 与处理器相关 - 下表中的最后3个
    系统调用
    描述
    nice()
    设置进程的nice值
    sched_setscheduler()
    设置进程的调度策略,即设置进程采取何种调度算法
    sched_getscheduler()
    获取进程的调度算法
    sched_setparam()
    设置进程的实时优先级
    sched_getparam()
    获取进程的实时优先级
    sched_get_priority_max()
    获取实时优先级的最大值,由于用户权限的问题,非root用户并不能设置实时优先级为99
    sched_get_priority_min()
    获取实时优先级的最小值,理由与上面类似
    sched_rr_get_interval()
    获取进程的时间片
    sched_setaffinity()
    设置进程的处理亲和力,其实就是保存在task_struct中的cpu_allowed这个掩码标志。该掩码的每一位对应一个系统中可用的处理器,默认所有位都被设置,即该进程可以再系统中所有处理器上执行。
    用户可以通过此函数设置不同的掩码,使得进程只能在系统中某一个或某几个处理器上运行。
    sched_getaffinity()
    获取进程的处理亲和力
    sched_yield()
    暂时让出处理器
    ##线程的调度策略##
  1. SCHED_OTHER 分时调度策略,(默认的)
  2. SCHED_FIFO实时调度策略,先到先服务
  3. SCHED_RR实时调度策略,时间片轮转
    实时进程将得到优先调用,实时进程根据实时优先级决定调度权值,分时进程则通过nice和counter值决定权值,nice越小,counter越大,被调度的概率越大,也就是曾经使用了cpu最少的进程将会得到优先调度。

SHCED_RR和SCHED_FIFO的不同:
当采用SHCED_RR策略的进程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平。
SCHED_FIFO一旦占用cpu则一直运行。一直运行直到有 更高优先级任务到达或自己放弃 。
如果有相同优先级的实时进程(根据优先级计算的调度权值是一样的)已经准备好,FIFO时必须等待该进程主动放弃后才可以运行这个优先级相同的任务。而RR可以让每个任务都执行一段时间。
相同点:
RR和FIFO都只用于实时任务。
创建时优先级大于0(1-99)。
按照可抢占优先级调度算法进行。
就绪态的实时任务立即抢占非实时任务。
当所有任务都采用分时调度策略时(SCHED_OTHER):

  1. 创建任务指定采用分时调度策略,并指定优先级nice值(-20~19)。
  2. 将根据每个任务的nice值确定在cpu上的执行时间( counter )。
  3. 如果没有等待资源,则将该任务加入到就绪队列中。
  4. 调度程序遍历就绪队列中的任务,通过对每个任务动态优先级的计算(counter+20-nice)结果,选择计算结果最大的一个去运行,当这个时间片用完后(counter减至0)或者主动放弃cpu时,该任务将被放在就绪队列末尾(时间片用完)或等待队列(因等待资源而放弃cpu)中。
  5. 此时调度程序重复上面计算过程,转到第4步。
  6. 重点内容当调度程序发现所有就绪任务计算所得的权值都为不大于0时,重复第2步。

当所有任务都采用FIFO调度策略时(SCHED_FIFO):
1.创建进程时指定采用FIFO,并设置实时优先级rt_priority(1-99)。
2.如果没有等待资源,则将该任务加入到就绪队列中。
3.调度程序遍历就绪队列,根据实时优先级计算调度权值,选择权值最高的任务使用cpu, 该FIFO任务将一直占有cpu直到有优先级更高的任务就绪(即使优先级相同也不行)或者主动放弃(等待资源)。
4.调度程序发现有优先级更高的任务到达(高优先级任务可能被中断或定时器任务唤醒,再或被当前运行的任务唤醒,等等),则调度程序立即在当前任务堆栈中保存当前cpu寄存器的所有数据,重新从高优先级任务的堆栈中加载寄存器数据到cpu,此时高优先级的任务开始运行。重复第3步。
5.如果当前任务因等待资源而主动放弃cpu使用权,则该任务将从就绪队列中删除,加入等待队列,此时重复第3步。
当所有任务都采用RR调度策略(SCHED_RR)时:
1.创建任务时指定调度参数为RR, 并设置任务的实时优先级和nice值(nice值将会转换为该任务的时间片的长度)。
2.如果没有等待资源,则将该任务加入到就绪队列中。
3.调度程序遍历就绪队列,根据实时优先级计算调度权值,选择权值最高的任务使用cpu。
4. 如果就绪队列中的RR任务时间片为0,则会根据nice值设置该任务的时间片,同时将该任务放入就绪队列的末尾 。重复步骤3。
5.当前任务由于等待资源而主动退出cpu,则其加入等待队列中。重复步骤3。***

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章