调度器

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/wugsh15/article/details/52514737

一. 调度器概述(内核3.18)

1.调度时机:

         a.调用cond_resche()时。

         b.显式调用schedule()时。

         c.从系统调用或者异常中断返回用户空间时。

         d.从中断上下文返回到用户空间时。

 

2.struct task_group 调度组

         linux可以以以下两种方式进行进程的分组:

  • 用户ID:按照进程的USER ID进行分组,在对应的/sys/kernel/uid/目录下会生成一个cpu.share的文件,可以通过配置该文件来配置用户所占CPU时间比例。
  • cgourp(control group):生成组用于限制其所有进程,比如我生成一个组(生成后此组为空,里面没有进程),设置其CPU使用率为10%,并把一个进程丢进这个组中,那么这个进程最多只能使用CPU的10%,如果我们将多个进程丢进这个组,这个组的所有进程平分这个10%。

3.struct sched_entity  调度实体

         a.调度实体可以是一个进程或一个进程组,比如:struct rb_node

         vruntime= 实际运行时间 *(NICE_0_LOAD/权重)

 

4.struct sched_class 调度类

         task_struct—>sched_class(调度器每次调度处理时,就通过当前进程的调度类函数进程操作,大大提高了可移植性和易修改性)

 

二. 初始化

 

1.sched_init:

在start_kernel中对调度器进行初始化的函数就是sched_init,其主要工作为

  • 对相关数据结构分配内存
  • 初始化root_task_group
  • 初始化每个CPU的rq队列(包括其中的cfs队列和实时进程队列)
  • 将init_task进程转变为idle进程

 

三.新进程加入

1.sched_fork()

copy_process()里面有一个函数专门用于进程调度的初始化,就是sched_fork().

在sched_fork()函数中,主要工作如下:

 •获取当前CPU号

 •禁止内核抢占(这里基本就是关闭了抢占,因为执行到这里已经是内核态,又禁止了被抢占)

 •初始化进程p的一些变量(实时进程和普通进程通用的那些变量)

 •设置进程p的状态为TASK_RUNNING(这一步很关键,因为只有处于TASK_RUNNING状态下的进程才会被调度器放入队列中)

 •根据父进程和clone_flags参数设置进程p的优先级和权重。

 •根据进程p的优先级设置其调度类(实时进程优先级:0~99  普通进程优先级:100~139)

 •根据调度类进行进程p类型相关的初始化(这里就实现了实时进程和普通进程独有的变量进行初始化)

 •设置进程p的当前CPU为此CPU。

 •初始化进程p禁止内核抢占(因为当CPU执行到进程p时,进程p还需要进行一些初始化)

 •使能内核抢占

 

2. task_fork_fair()

      在实时进程的调度类中是没有特定的task_fork()函数的,而普通进程使用cfs策略时会调用到task_fork_fair()函数.

task_fork_fair()函数中主要就是设置进程p的虚拟运行时间和所处的cfs队列.

 

3.wake_up_new_task()

         do_fork()中的wake_up_new_task(p)将进程加入到队列中。

 

4.enqueue_task_fair()

         wake_up_new_task()函数中,将进程加入到运行队列的函数为activate_task(),activate_task()函数最后会调用到新进程调度类中的enqueue_task指针所指函数.enqueue_task_fair()函数中又使用了enqueue_entity()函数进行操作.

 

需要注意的几点:

  • 新创建的进程先会进行调度相关的结构体和变量初始化,其中会根据不同的类型进行不同的调度类操作,此时并没有加入到队列中。
  • 当新进程创建完毕后,它的父进程会将其运行状态置为TASK_RUNNING,并加入到运行队列中。
  • 加入运行队列时系统会根据CPU的负载情况放入不同的CPU队列中。

 

四.运行

   1. 系统定时器


        在内核中,会使用strut clock_event_device结构描述硬件上的定时器,每个硬件定时器都有其自己的精度,会根据精度每隔一段时间产生一个时钟中断。而系统会让每个CPU使用一个tick_device描述系统当前使用的硬件定时器(因为每个CPU都有其自己的运行队列),通过tick_device所使用的硬件时钟中断进行时钟滴答(jiffies)的累加(只会有一个CPU负责这件事),并且在中断中也会调用调度器,而我们在驱动中常用的低精度定时器就是通过判断jiffies实现的。而当使用高精度定时器(hrtimer)时,情况则不一样,hrtimer会生成一个普通的高精度定时器,在这个定时器中回调函数是调度器,其设置的间隔时间同时钟滴答一样。

  所以在系统中,每一次时钟滴答都会使调度器判断一次是否需要进行调度。

2. 时钟中断

         a.tick_handle_periodic()

此函数主要工作是执行tick_periodic()函数,然后判断时钟中断是单触发模式还是循环触发模式,如果是循环触发模式,则直接返回,如果是单触发模式,则执行如下操作:

  • 计算下一次触发时间
  • 设置下次触发时间
  • 如果设置下次触发时间失败,则根据timekeeper等待下次tick_periodic()函数执行时间。
  • 返回第一步

b.tick_periodic()

  # tick_device 周期性调用此函数

  # 更新jffies和当前进程

  # 只有一个CPU是负责更新jffies的,其他的CPU只会更新当前自己的进程


注意:

1.      check_preempt_tick()函数就是用来判断进程是否需要被调度的,其判断的标准有两个:

先判断当前进程的实际运行时间是否超过CPU分配给这个进程的CPU时间,如果超过,则需要调度。

再判断当前进程的vruntime是否大于下个进程的vruntime,如果大于,则需要调度。

2.      在pick_next_task()中完全体现了进程优先级的概念,首先会先判断是否所有进程都处于cfs队列中,如果不是,则表明有比普通进程更高优先级的进程(包括实时进程)。内核中是将调度类重优先级高到低进行排列,然后选择时从最高优先级的调度类开始找是否有进程需要调度,如果没有会转到下一优先级调度类.

展开阅读全文

没有更多推荐了,返回首页