哈喽,我是子牙老师。今天咱们继续聊那个有趣的话题:算出Linux内核会调度哪个进程
Linux内核中的CFS调度器的调度实体有两种:单个进程、进程组。关于调度器如何调度进程,之前已写过一篇文章。公众号用户可直接查看《Linux内核如何调度进程》。非公众号用户关注公众号【硬核子牙】查看。本篇文章着重谈调度器如何调度进程组
你可能想问:为什么会出现组调度?试想如果是这样的场景:用户A创建了9个进程,用户B创建了1个进程,如果调度器只能调度单个进程,会出现什么样的结果?如果进程的nice值相同,那CFS调度器就会公平的分配CPU时间片,每个进程获得10%的CPU时间片。对于进程来说,这是公平的。但是对于用户来说,这是不公平的,用户A获得了90%的时间片,用户B只获得了10%的时间片
那如何解决这个问题呢?组调度应运而生!将用户A的所有进程打包成一个组作为调度实体,将用户B的所有进程打包成一个组作为调度实体。用户A调度实体与用户B调度实体平分CPU时间片,各50%。用户A调度实体中的9个进程再平分这50%的时间片,用户B调度实体中的那1个进程,就很幸运,一个人独享那50%
讲到这,你应该知道组调度存在的意义了。在Linux内核中,有两套组调度机制:一套是自动组机制,autogroup,这套机制的出现,让Linux之父linus大为赞叹;一套是可供用户配置的cgroups
关于组调度,本篇文章会讲到这些:
- 调度组在Linux内核中是如何存在的
- autogroup机制是什么
- cgroups机制是什么
- 进程是何时加入调度组的
- 调度组是何时加入CFS调度队列的
- 调度组的vruntime是如何计算的
- 调度组是如何被调度器调度到的
- 调度组中的进程如何分配CPU时间片
以下,enjoy~
调度组与进程
进程对应的结构体是task_struct,调度组对应的数组结构是task_group,它俩是如何关联的呢?
如何通过进程自己的task_struct找到它的调度组呢?通过属性sched_task_group
如何通过调度组task_group找到它下属的task_struct呢?每个task_group关联CPU个数个CFS调度队列cfs_rq,task_group下属的task_struct,都挂在这个cfs_rq中的红黑树上
这里面有两个问题需要探讨:一、task_group为什么关联的不是一个cfs_rq,而是CPU个数个?二、这个cfs_rq跟上篇文章提到的cfs_rq是一个意思吗?
先回答第一个问题,因为调度组task_group与进程task_struct是一对多的关系,而不同的进程会被不同的CPU调度,如何只有一个cfs_rq,为了防止一个进程被多个CPU调度,就需要加锁访问,为了避免锁开销,就使用CPU个数个cfs_rq解决,每个CPU一个cfs_rq,CPU调度进程的时候,从自己的cfs_rq中去找即可
第二个问题,这里先不讲,本文后面内容会讲到,接着往下看吧
两套组调度机制
autogroup、cgroups与task_group之间是如何关联的呢?
autogroup是什么?就是为shell终端设计的自动分组机制。打开一个新的shell终端,就会创建一个autogroup结构体。shell终端对应的bash,就是这个autogroup的第一个进程,在该shell终端上执行的所有命令,都是bash的子进程,挂在这个autogroup关联的task_group中的cfs_rq中的红黑树上
cgroups是什么?用于控制进程的硬件资源,比如能使用多少内存、IO带宽、网络带宽、绑定CPU运行、获得CPU时间片多少等。容器中用的比较多。比如docker就是使用cgroups限制容器进程使用的硬件资源
cgroups是如何创建的呢?是在目录/sys/fs/cgroups下创建新目录的时候
在cgroups目录下创建目录,为什么会调用到vfs_mkdir呢?因为在创建目录的时候,Linux内核会检测到这是个挂载目录
就会去vfs中找注册的相关函数去调用
至此,两套组调度机制与task_group之间的关系就讲完了。
注意!注意!注意!一个task_group不可能同时属于两套组调度机制。我只是为了方便,画在了一张图里
调度组与调度器
现在我们已经知道task_struct、task_group、cgroup、autogroup是如何关联的,但是目前还没有跟调度器关联,CPU是调度不到的。那task_group是如何与调度器关联的呢?
在Linux内核中,有一个根task_group,即root_task_group,它是后面创建的所有task_group的parent
task_group有两个非常重要的属性:se、cfs_rq。一般把task_struct中的se称为task se,把task_group中的se称为group se。se就是调度实体。se中的属性run_node就是调度队列cfs_rq中的红黑树中的节点
task_group中的se、cfs_rq,都不是一个,而是CPU个数个,原因前面已经解释过了。
root_task_group中的se数组为空,因为root_task_group不是调度实体。root_task_group中的cfs_rq指向默认cfs调度队列。即rq.cfs_rq = root_task_group.cfs_rq[cpuid]。如果不创建新的task_group,所有的进程都是root_task_group的组员,都挂在rq.cfs_rq中的红黑树上
因为rq.cfs_rq是默认调度队列,所以新创建的task_group.se要挂到rq.cfs_rq中的红黑树中才能被调度到,那什么时候挂上去呢?是task_group创建的时候?还是task_group中迎来第一个组员进程的时候?答案是迎来第一个组员进程的时候
总结一下,root_task_group的se数组为空,cfs_rq数组指向每个CPU默认的调度队列cfs_rq。其他task_group的se都要挂到CPU的默认调度队列中充当调度实体才能被调度到,只是不是创建的时候挂,而是往task_group中加入组员进程的时候挂。普通的task_group都有自己的cfs_rq数组,每个cfs_rq对应一个CPU
至此,与调度组相关的数据结构,及之间的关系就全部讲完了。接下来开始讲调度组的调度相关知识
举个例子
举个例子帮助大家理解前面讲的
如果你看到的进程树长这样,在Linux内核中是如何存储的呢?
Linux内核启动的时候,会创建一个root_task_group,然后对其初始化
Linux内核启动完成后,程序会不断的创建进程,新进程的sched_task_group都是root_task_group。为什么呢?因为1号进程的是,后面创建的进程都是copy自1号进程。这个sched_task_group可以更改
Linux内核会将进程systemd、clion、make都丢到CPU的默认调度队列cfs_rq中,即root_task_group.cfs_rq[cpuid]中的红黑树中
这时候用户A打开了一个shell终端,此时会创建一个autogroup及一个task_group,将task_group中的parent指向root_task_group,为该task_group创建一个cfs_rq,将shell终端对应的bash进程丢进去,然后将该task_group se挂到CPU默认队列cfs_rq上去。后续在这个shell终端中执行sleep会创建三个子进程,丢到该task_group关联的cfs_rq中的红黑树上
用户B也打开了一个shell终端,执行情况跟用户A一样
调度组的vruntime
vruntime的值决定了task_group se在cfs_rq中红黑树的位置,决定了何时会被调度到。那调度组的vruntime怎么算呢?很简单,取组员进程中vruntime最小的值,合情合理!
那调度组中的每个进程能获得多少CPU时间片呢?以开头的例子来说,用户A创建了9个进程,用户B创建了1个进程
上面的计算是基于理想情况,现实中需要考虑带宽、负载的影响,后面写文章详谈。关注公众号【硬核子牙】,第一时间获取文章动态
调度调度组
调度器是如何进行组调度的呢?看核心代码
这段代码的逻辑是:
- 从当前运行的进程所在的cfs_rq中,取vruntime最小的调度实体se
- 调用函数group_cfs_rq获取调度实体的cfs_rq
- 如果se是进程,cfs_rq为NULL,循环结束,该se关联的进程就是下一个要调度的进程
- 如果se是task_group,那cfs_rq就是该task_group关联的调度队列,while成立,调到步骤2,循环执行
是不是理解了各个数据结构之间的关系,理解这里就非常easy了
总结
Linux内核中有五种调度器,按优先级从高到低依次是:停机调度器、限期调度器、实时调度器、公平调度器、空闲调度器,与我们息息相关的是公平调度器CFS
CFS中的调度实体,可以是进程,也可以是进程组。你可能疑惑,怎么没提到线程?其实Linux内核层面没有线程的概念,线程的本质是轻量级进程,所以线程在Linux内核中也是task_struct,所以机制是一样的
基本上来说,你把我写得这两篇文章完全吃透,CFS源码你就可以轻轻松松读懂。CFS还剩边缘的知识,下一篇文章会全部讲到。关注公众号【硬核子牙】,第一时间获取文章动态