首页 > 其他分享 >Cgroup指南

Cgroup指南

时间:2024-12-16 14:36:18浏览次数:19  
标签:指南 cgroups Cgroup 层次结构 线程 cgroup 进程 子系统

Cgroup指南

cgroup提供给子系统的接口

  • 在每个 cgroup 中存储一些任意状态数据
  • 在 cgroup 文件系统中为每个 cgroup 提供一些属性文件,可用于查看或修改此状态数据或任何其他状态详细信息
  • 接受或拒绝将进程附加到给定 cgroup 的请求
  • 接受或拒绝创建新组作为现有组的子组的请求
  • 当某个 cgroup 中的任何进程分叉或退出时收到通知

没有会计的子系统

调试cgroup子系统:debug子系统

使单个组或整个 cgroup 系统的大量内部详细信息通过 cgroup 文件系统中的虚拟文件可见。

网络包过滤:net_cl、net_prio子系统(无需配置、读取当前cgroup)

net_prio使用 cgroups 核心为每个组提供的序列号 ( cgroup->id) 并将其存储在 sk_cgrp_prioidx属性中。这对于每个 cgroup 都是唯一的。
net_cl允许为每个 cgroup 明确指定一个标识号,并将其存储在sk_classid属性中。这对于每个 cgroup 不一定是唯一的。
这两个不同的组标识符用于三个不同的任务:

  1. net_cl设置的sk_classid可以与iptables一起使用,以根据哪个cgroup拥有原始套接字来选择性地过滤数据包。
  2. sk_classid还可以用于网络调度过程中的数据包分类。包分类器可以根据cgroup和其他细节做出决策,这些决策可以影响各种调度细节,包括设置每个消息的优先级。
  3. sk_cgrp_prioidx纯粹用于设置网络数据包的优先级,因此当使用SO_PRIORITY套接字选项或任何其他方式时,它将覆盖优先级集。使用sk_classid和包分类器也可以达到类似的效果。然而,根据引入net_prio子系统的提交,包分类器并不总是足够的,特别是对于启用数据中心桥接(DCB)的系统。

设备允许或拒绝访问:devices子系统(下推配置、读取当前cgroup)

设备子系统强制对部分或全部设备专用文件(即块设备和字符设备)的访问进行访问控制。默认情况下,每个组可以允许或拒绝所有访问,然后分别有一个拒绝或允许访问的异常列表。

更新异常列表的代码通过拒绝父节点不允许的更改或将更改传播给子节点,确保子节点永远不会拥有比父节点更多的权限。这意味着在执行权限检查时,只需要检查一个组中的默认值和异常。不需要遍历树检查祖先是否也允许访问,因为祖先施加的任何规则都已经存在于每个组中。

这里有一个明显的权衡,更新权限的任务变得更加复杂,以保持测试权限的任务简单。由于前者通常比后者发生的频率要低得多,因此这是一种明智的权衡。

设备子系统作为这种权衡的选项范围的中间点。任何cgroup中的配置都被下推到所有子节点,一直下推到层次结构的叶子节点,但不会再进一步。当各个进程需要做出访问决策时,它们仍然需要引用相关的cgroup。

进程的冻结或解冻:freezer子系统(下推配置到cgroup下级进程)

freezer子系统有完全不同的需求,因此它在范围内处于不同的位置。这个子系统提供了一个“冷冻室”。每个c组中的state”文件可以写入“FROZEN”或“thaw”。

为了实现进程的冻结或解冻,冷冻子系统沿着c组层次结构向下走,就像设备子系统一样,将所有后代组标记为冻结或解冻。那么它必须更进一步。进程不会定期检查它们的c组是否被冻结,所以freeze必须从所有的后代c组走到所有的成员进程,并显式地将它们移到freeze中或再次将它们移出。冻结是通过向每个进程发送一个虚拟信号来安排的,因为信号处理代码会检查c组是否被标记为冻结并相应地采取行动。为了确保没有进程逃脱冻结,当进程分叉时,freeze请求通知,这样它就可以捕获新创建的进程——它是唯一这样做的子系统。

因此,冷冻机占据了层级管理光谱的一个极端,它迫使配置从层级一直到流程。

perf_event子系统(配置到指定cgroup、向上遍历获取配置)

性能工具收集一组进程的各种性能数据。这个集合可以是系统上的所有进程,所有由特定用户拥有的进程,所有从某个特定父进程继承的进程,或者使用perf_event cgroup子系统,所有在某个特定cgroup中的进程。
perf_event子系统使用cgroup_is_descendant()函数,该函数只遍历->父链接,直到找到匹配项或根。
对于perf,我们可以看到配置根本不会被推到层次结构中。每当需要一个控制决策时(例如,是否应该计算这个事件?),代码就会从过程中沿着树向上查找答案。

限制进程使用的CPU:cpuset子系统

cpuset cgroup子系统在任何具有多个cpu的机器上都很有用,特别是在NUMA机器上,NUMA机器具有多个节点,并且节点内部和节点之间的访问速度通常差别很大。

cpuset确定了一组处理器,组中的每个进程可以在这些处理器上运行,它还确定了一组内存节点,组中的进程可以从中分配内存。

执行集合涉及两种完全不同的方法。这在将进程从一个c组移动到另一个c组时最为明显。如果允许的处理器集不同,则可以轻松地将进程放在允许其运行的新处理器的运行队列中。如果允许的内存节点集发生了变化,那么将所有内存从一个节点迁移到另一个节点绝非易事(因此这种迁移是可选的)。

cpuset在适当的时候将对父类所做的任何更改传播到所有子类。
cpuset没有请求进程fork的通知
cpuset有时也会沿着层次结构向上查找合适的父节点。

这样的一种情况是,当一个进程发现自己在一个c组中,它的集合中没有工作CPU时,可能是由于CPU处于脱机状态。
    
另一种情况是,高优先级内存分配发现mems_allowed集中的所有节点都耗尽了它们的空闲内存。
    
在这两种情况下,可以使用授予祖先节点的一些资源来摆脱困境。这两种情况都可以通过在每个c组中维护一组最新的“紧急”处理器和内存节点来处理。

带有会计的子系统

Linux和Unix对计算资源使用情况的想法并不陌生。即使在V6 Unix中,也会计算每个进程的CPU时间,并且可以使用times()系统调用访问运行总数。
在2.10BSD中,计算的资源集增加到包括内存使用、页面错误、磁盘I/O和其他统计信息。与CPU时间一样,当等待子进程时,这些时间被收集到父进程中。它们可以通过getrusage()系统调用访问。
与getrusage()一起出现的是setrlimit(),它可以对其中一些资源的使用施加限制,例如CPU时间和内存使用。这些限制只适用于单个进程,而不适用于一个组:一个组的统计数据只有在进程退出时才会累积,而这时施加限制已经太晚了。
下边五个子系统,每个子系统都以某种方式涉及到整个组的计算,因此支持setrlimit()永远无法支持的一类限制。

CPU使用时间统计:cpuacct子系统

Cpuacct是最简单的会计子系统,部分原因是它所做的一切都是会计;它根本不施加控制。
统计信息:
cgroup中所有进程使用的总CPU时间
此信息是按每个cpu收集的,可以按每个cpu报告,也可以将所有cpu的总和报告。
cgroup中所有进程使用的总CPU时间分解为“用户”时间和“系统”时间。

从2.6.29开始,这些统计信息是分层收集的。每当将某些用法添加到一个组时,它也会添加到该组的所有祖先。因此,每个组的使用情况是扩展组中所有进程的使用情况的总和,包括子组成员的进程。

cpuacct,更新只发生在调度程序事件(scheduler event)或计时器刻度(timer tick)上,在繁忙的机器上通常是每毫秒一次或更多次。

跟踪和限制内存使用

跟踪和限制内存使用涉及两个cgroup子系统:memory和hugetlb。

数据结构

res_counter
在include/linux/res_counter.h中声明并在kernel/res_counter.c中实现的res_counter,存储了一些任意资源的使用情况以及限制和“软限制”。
它还包括一个高水位标记,记录使用率达到的最高水平,以及一个失败计数,跟踪对额外资源的请求被拒绝的次数。
res_counter包含一个用于管理并发访问的自旋锁和一个父指针。

res_counter使用位置
memory cgroup子系统分配了三个res_counter,一个用于用户进程内存使用,一个用于内存和交换内存使用的总和,另一个用于内核代表进程使用的内存。
hugetlb cgroup子系统分配的一个res_counter(它计算了在大页面中分配的内存)
这意味着当内存和hugetlb子系统同时启用时,有四个额外的父指针。

逻辑

当进程请求各种内存资源中的一个时,res_counter代码将遍历父指针,检查是否达到限制并更新每个祖先的使用情况。这需要在每个级别上使用自旋锁,所以这不是一个便宜的操作,特别是如果层次结构非常深的话。在Linux中,内存分配通常是一项高度优化的操作,使用每cpu空闲列表以及批处理分配和释放来尽量减少每分配成本。分配内存并不总是一个高频操作,但有时确实是;如果可能的话,这些时间应该仍然表现良好。因此,为多个嵌套的cgroup使用一系列自旋锁来更新每个内存分配的记帐听起来不像是一个好主意。幸运的是,这不是内存子系统所做的事情。

当授权少于32页的内存分配请求时(大多数请求都是针对一页的),内存控制器将请求res_counter批准一个完整的32页。如果该请求被批准,则超出实际需求的部分将记录在每个CPU的“库存”中,该库存记录了哪个c组最后在每个CPU上进行了分配以及已批准的超额部分。如果请求未被批准,则请求实际分配的页面数。同一进程在同一CPU上的后续分配将使用库存中的剩余资源,直到库存减少到零,此时将请求另一个32页的授权。如果调度更改导致来自不同c组的另一个进程在该CPU上分配内存,那么旧的库存将被返回,并为新的c组请求新的库存。释放也是批处理的,尽管机制完全不同,可能是因为释放通常发生在更大的批处理中,而且释放永远不会失败。释放的批处理使用每个进程(而不是每个cpu)计数器,该计数器必须由释放内存的代码显式启用。这是一系列的调用:

mem_cgroup_uncharge_start()
repeat mem_cgroup_uncharge_page()
mem_cgroup_uncharge_end()

这里将使用uncharge批处理,而单独的mem_cgroup_uncharge_page()则不会。

控制调度程序控制普通和cgroup进程时间:cpu子系统

数据结构

cpu子系统创建了struct sched_entity结构的并行层次结构,调度器使用它来存储比例权重和虚拟运行时。
实际上有很多这样的层次结构,每个CPU一个。

逻辑

cpu子系统允许在每个组上施加最大cpu带宽。这与目前讨论的调度优先级完全不同。带宽是以每实时的CPU时间来衡量的。
在为每个组设置配额和周期时,子系统检查施加给任何父组的限制是否总是足以允许所有子组充分利用其配额。如果不满足,那么更改将被拒绝。
带宽限制的实际实现主要是在sched_entity上下文中完成的。当调度器更新每个sched_entity使用了多少虚拟时间时,它还会更新带宽使用情况,并检查节流是否合适。

块io:blkio子系统

Blkio允许注册各种“策略”,它们的行为很像cgroup子系统,因为它们会被告知cgroup层次结构的变化,并且它们可以向cgroup虚拟文件系统添加文件。它们不能禁止进程的移动,也不能被告知fork()或exit()。

在Linux 3.15中有两个块策略:“throttle”和“cfq-iosched”。

  1. blkio子系统为每个c组添加一个新的ID号。blkio ID号使用64位并且永远不会被重用,而cgroup-core ID号只是一个int(通常是32位)并且可以被重用。在添加blkio ID一年多之后,一个非常相似的serial_nr确实被添加到cgroup核心中,尽管blkio还没有被修改以使用它。阅读代码时请注意:blkio在内部称为blkcg。
  2. 在cfq-iosched策略中,为每个c组分配不同的权重,类似于CPU调度器计算的权重,以平衡来自该组的请求与来自同级组的请求的调度。与blkio不同的是,每个c组也可以有一个leaf_weight,用于平衡来自该组中进程的请求和来自子组中的进程的请求。

这样做的最终效果是,当非叶子组包含进程时,cfq-iosched策略假装这些进程确实在一个虚拟子组中,并使用leaf_weight为该虚拟子组分配权重。如果我们将这一点与我们之前探讨的层次结构的不同方面相比较,那么cfq-iosched似乎清楚地表明,“组织”层次结构不是cfq-iosched想要处理的东西,而所有东西都应该或将被视为“分类”层次结构。CPU调度器似乎并不关心这个问题。内部组中的进程是相互对照的,并且作为一个整体针对任何子组进行调度。目前还不清楚哪种调度方法是正确的,但如果它们是一致的就好了。实现一致性的一种方法是禁止非叶子组包含进程。正如我们将在本系列后面看到的那样,正在进行的工作正是为了达到这个目的。

cgroup层次结构

3.16 Linux内核将引入“统一层次结构”,这里先讨论经典层次结构

“经典”cgroup层次结构

可以有几个单独的cgroup层次结构,每个层次结构以根cgroup的形式开始它的生命,它最初拥有所有进程。

这个根节点是通过挂载一个“cgroup”虚拟文件系统的实例来创建的,所有对层次结构的进一步修改都是通过对这个文件系统的操作来实现的,特别是用mkdir创建cgroups,用rmdir删除cgroups,用mv重命名同一个父目录中的cgroup。

一旦创建了cgroup,就可以通过将进程id号写入特殊文件来在它们之间移动进程。当一个适当的特权用户向c组写入一个PID号时。进程,该进程从当前所在的c组移动到目标c组。

当创建一个层次结构时,它与一组固定的c组子系统相关联。可以更改该集合,但前提是层次结构在根下没有子组,因此,对于大多数实际用途,它是固定的。每个子系统最多可以连接到一个层次结构,因此,从任何给定子系统的角度来看,只有一个层次结构,但是不能对其他子系统可能看到的情况做出假设。

因此,有可能有12个不同的层次结构,每个子系统一个,或者有所有12个子系统的单一层次结构,或者两者之间的任何其他组合。还可以有任意数量的层次结构,每个层次结构都不附加任何子系统。这样的层次结构不允许对各个cgroup中的进程进行任何控制,但它允许跟踪相关的进程集。

Systemd利用这个特性,创建了一个挂载在 /sys/fs/cgroup/systemd 的cgroup树,没有控制器子系统。它包含一个user.slice 子层次结构,首先按用户,然后按会话对登录会话产生的进程进行分类。如下:

/sys/fs/cgroup/systemd/user.slice/user-1000.slice/session-1.scope

这里表示一个cgroup,其中包含与UID为1000的用户的第一次登录会话相关的所有进程(slice和scope是systemd特有的术语)。

这些“会话作用域”似乎恢复了V7 Unix中原始进程组的一个值—明确标识哪些进程属于哪个登录会话。这在单用户桌面上可能不是很有趣,但在更大的多用户机器上可能非常有价值。虽然不能直接控制组,但我们可以通过查看c组来准确地知道其中有哪些进程(如果有的话)。部分文件。如果进程分叉的速度不是太快,你甚至可以给所有的进程发信号,比如:

kill $(cat cgroup.procs)

选择的专制

也许传统的等级制度最大的问题是选择的专制。在子系统可以组合的不同方式中似乎有很多灵活性:一些在一个层次结构中,一些在另一个层次结构中,在第三个层次结构中根本没有。问题在于,这种选择一旦做出,就会影响整个系统,而且很难改变。如果一种需求要求子系统的特定安排,而另一种需求要求不同的安排,那么这两种需求都不能在同一台主机上得到满足。当容器用于在一台主机上支持单独的管理域时,这尤其是个问题。所有管理域必须看到相同的c组子系统到层次结构的关联。

这表明需要商定一个标准。显而易见的选择是有一个单一的层次结构(这是“统一层次结构”方法的方向)或每个子系统有一个单独的层次结构(这非常接近我的openSUSE 13.1笔记本的默认设置:只有cpu和cpuacct结合在一起)。到目前为止,我们已经学习了关于c组子系统的所有知识,我们可能能够理解保持子系统分开或在一起的一些含义。

相当多的子系统不执行任何计算,或者当它们执行时,不使用该计算来施加任何控制。它们是debug、net_cl、net_perf、device、freeze、perf_event、cpuset和cpuacct。这些都不需要大量使用层次结构,而且在几乎所有情况下,层次结构提供的功能都可以单独实现。

perf_event子系统和使用它的perf程序就是一个很好的例子。perf工具可以收集一组进程的性能数据,并提供选择这些进程的各种方法,其中一种方法是指定UID。当给定UID时,perf不只是将其传递给内核以请求所有匹配的进程,而是检查/proc文件系统中列出的所有进程,选择具有给定UID的进程,并要求内核单独监视其中的每个进程。

perf_event子系统对层次结构的唯一用途是将子组收集到更大的组中,这样perf就可以识别一个更大的组,并收集该组下所有组中的所有进程的数据。由于perf可以以与基于UID选择进程类似的方式识别它感兴趣的所有叶子组(无论它们是否在一个更大的组中),从而达到完全相同的效果,因此层次结构实际上只是一个小小的便利—而不是一个重要的特性。出于类似的原因,列出的其他子系统可以在没有任何层次结构的情况下轻松管理。

在这些子系统中,层次结构有两种用途,不能轻易地抹去。第一个是cpuset子系统。它有时会在层次结构中向上查找以在紧急情况下使用的额外资源。这个特性是对层次结构的内在依赖。正如我们在第一次检查这个子系统时所注意到的,可以很容易地提供类似的功能,而不依赖于层次结构,所以这是一个小例外。

另一种用途在设备子系统中最为明显。它不涉及任何强加的控制,而是涉及允许的配置:父组拒绝的访问,则子组也不能允许访问。这种层次结构的使用与其说是为了对流程进行分类,不如说是为了进行管理控制。它允许上层设置低层必须遵循的策略。管理层次结构可以非常有效地分配权限,无论是分配给用户组、单个用户,还是分配给可能拥有自己的用户集的容器。拥有一个单一的管理层次结构(可能基于systemd默认提供的层次结构)是一个非常自然的选择,并且非常适合所有这些非会计子系统。将它们中的任何一个分开似乎都很难证明是合理的。

网络流量控制——另一个控制层次

其余的子系统是最值得称为“资源控制器”的子系统,它们管理内存(包括hugetlb)、CPU和块i /O资源。为了理解这些问题,我们可以转移一下注意力,看看网络资源是如何管理的。
网络流量当然可以从资源共享和使用限制中受益,但是在我们对不同的c组子系统的探索中,我们没有看到任何网络资源控制的证据,当然不像我们对块I/O和CPU资源所做的那样。这一点尤其重要,因为正如前一篇文章中提到的,有一个证明多重层次结构的理由是,可能确实需要将网络资源(例如CPU资源)与其他资源分开管理。
网络流量实际上是由一个单独的层次结构管理的,这个层次结构甚至与cgroups分开。为了理解它,我们至少需要简要介绍一下网络流量控制(NTC)。NTC机制由tc程序管理。该工具允许将“排队规则”(或“qdisc”)附加到每个网络接口。一些qdisc是“分类的”,这些qdisc下面可以附加其他qdisc,每个“类”包一个。如果这些二级qdisc中的任何一个也是有类的,那么就有可能进一步升级,以此类推。这意味着可以有一个qdisc层次结构,或者多个层次结构,每个网络接口一个。
tc程序还允许配置“过滤器”。这些过滤器指导如何将网络数据包分配给不同的类(从而分配给不同的队列)。过滤器可以键入各种值,包括数据包中的字节数、用于数据包的协议,或者生成数据包的套接字(对当前的讨论很重要)。net_cl cgroup子系统可以为每个cgroup分配一个“类ID”,由该cgroup中的进程创建的套接字继承,并且这个类ID用于将数据包分类到不同的网络队列中。
每个数据包将被各种过滤器分类到树中的一个队列中,然后向上传播到根,可能被节流(例如通过令牌桶过滤器,tbf, qdisc)或被竞争性调度(例如通过随机公平队列,sfq, qdisc)。一旦到达根,它就被传输了。
这个例子强调了使用层次结构(甚至是单独的层次结构)来管理资源的调度和限制的价值。它还向我们表明,它不需要是一个单独的c组层次结构。资源本地层次结构可以完美地满足需求,在这种情况下,不需要单独的c组层次结构
每个主要的资源控制器(CPU、内存、块I/O和网络I/O)都维护单独的层次结构来管理它们的资源。对于前三个层次结构,这些层次结构通过cgroups进行管理,但对于网络,它们是单独管理的。这个观察可能表明这里存在两种不同的层次结构:一些用于跟踪资源,另一些(可能是一个“管理层次结构”)用于跟踪过程。

Documentation/cgroups/cgroups.txt中的例子似乎承认跟踪进程的单一层次结构的可能性,但担心它“可能导致……激增的cgroup”。如果我们在前面描述的systemd层次结构中包含net_cl子系统,我们可能需要在每个会话中为可能需要的不同网络类创建几个子组。如果其他子系统(例如cpu或blkio)希望在每个会话中进行不同的分类,则可能导致cgroup的组合爆炸。这是否真的是一个问题取决于内部实现细节,因此我们将把对这个问题的进一步讨论推迟到下一篇文章,这篇文章将确切地关注这个主题。
cgroups层次结构在NTC层次结构中不明显的一个特性是,当使用容器时,可以将部分层次结构委托给单独的管理域。通过仅在某个容器的命名空间中挂载cgroup层次结构的子树,容器被限制为仅影响该子树。但是,这样的容器将不受将类id分配给不同cgroup的限制。这似乎可以规避任何预期的隔离。
通过网络,可以使用虚拟化和间接解决这个问题。可以向容器提供“第一个”虚拟网络接口,它可以按照自己的喜好进行配置。来自容器的流量被路由到真实接口,并可以根据其来自的容器进行分类。类似的方案可以用于块I/O,但是如果没有完全类似kvm的虚拟化,CPU或内存资源管理就无法达到相同的效果。这将需要一种不同的管理委托方法,例如cgroups提供的显式子挂载支持。

怎样才算太过独立?

正如我们上次提到的,会计资源控制器需要对c组祖先的可见性来有效地实施速率限制,并且需要对c组兄弟的可见性来实现公平共享,因此整个层次结构对这些子系统确实很重要。
如果我们以NTC为例,可以认为这些层次结构应该为每个资源分开。NTC在这方面比cgroups走得更远,它允许每个接口有单独的层次结构。可以想象,对于不同的块设备(交换/数据库/日志),Blkio可能需要不同的调度结构,但cgroup不支持。
然而,过度分离资源控制是有代价的,正如(根据某些人的说法)微内核所提倡的分离资源管理是有代价的。这种代价就是缺乏“有效的合作”,许泰俊认为这是统一等级制度的部分理由。
当进程写入文件时,数据将首先进入页缓存,从而消耗内存。稍后,该内存将被写入存储,从而消耗一些块i /O带宽,或者可能是一些网络带宽。所以这些子系统并不是完全分开的。
当内存写入时,它很可能不会被最初写入数据的进程所写入,甚至不会被同一c组中的任何其他进程所写入。那么,如何准确地计算这个block-I/O使用情况呢?
内存cgroup子系统将额外的信息附加到内存的每个页面,以便它知道在页面被释放时向哪里发送退款。当最终写入页面时,我们似乎可以将I/O使用情况计入同一个c组,但是有一个问题。该cgroup与内存子系统相关联,因此可以处于完全不同的层次结构中。用于内存记帐的cgroup对blkio子系统来说可能没有意义。

有几种不同的方法可以解决这种分离:

  • 记录每个页面的进程ID,并使用它来确定应该为内存使用和块i /O使用收取多少费用,因为这两个子系统都理解PID。一个问题是,流程的寿命可能非常短。当一个进程退出时,我们需要将其未完成的资源消耗转移到其他进程或c组,或者直接丢弃它们。这与我们在CPU调度器中看到的问题类似,在CPU调度器中,只计算一个进程不会轻易地为进程组带来适当的公平性。有效地保留未偿指控可能是一个挑战。
  • 创建一些其他标识符,可以安全地任意长时间存在,可以与多个进程相关联,并且可以被每个不同的c组子系统使用。这实际上是“额外的间接层次”,众所周知,它可以解决计算机科学中的任何问题。连接net_cl子系统和NTC的类ID就是这样一个标识符的例子。虽然可以有多个层次结构,每个接口一个,但是只有一个类ID标识符的名称空间。
  • 为每个页面存储多个标识符,一个用于内存使用,一个用于I/O吞吐量。用于为内存控制器存储额外的每页信息的结构体page_cgroup结构目前在64位系统上每页花费128位—64位用于指向所属cgroup的指针,64位用于标记,其中3个已定义。如果可以使用数组索引来代替指针,并且认为10亿个组就足够了,那么可以在一半的空间中存储两个索引和一个额外的位。索引是否能够以足够的效率使用是留给感兴趣的读者的另一个练习。

此问题的良好解决方案可能适用于其他情况:一个进程代表另一个进程消耗资源的任何情况。Linux中的md RAID驱动程序通常会将I/O请求直接传递到发起请求的进程上下文中的底层设备。在其他情况下,一些工作需要由助手进程完成,然后由它提交请求。目前,执行该工作的CPU时间和请求所消耗的I/O吞吐量由md承担,而不是由发起进程承担。如果一些“消费者”标识符或标识符可以附加到每个I/O请求,那么md和其他类似的驱动程序将有一些机会相应地分配资源费用。

不幸的是,在目前的实现中没有很好的解决方案。虽然存在过度分离的代价,但这些代价不能通过简单地将所有子系统附加到同一层次结构中来减轻。在当前的实现中,最好将记帐子系统(cpu、block、内存和hugetlb)保持在单独的层次结构中,承认网络已经由于NTC而具有单独的层次结构,并将所有非记帐子系统保持在一个管理层次结构中。这将依赖于智能工具在需要时有效地组合不同的cgroup。

回答


我们现在可以回答本系列前一篇文章中出现的更多问题。一个是如何命名的问题。正如我们在上面看到的,这是启动mkdir命令的进程的责任。这与作业控制进程组和会话形成对比,当进程调用setsid()或setpgid(0,0)时,内核以(几乎)任意的方式为其分配名称。这种差异可能是微妙的,但它似乎确实说明了预期的权力结构。对于作业控制过程组,形成新组的决定来自新组的成员内部。对于集团来说,这个决定预计将来自外部。前面,我们观察到在cgroups层次结构中体现管理层次结构似乎很有意义。从外部分配名称的事实与该观察结果一致。另一个问题是是否有可能从一个群体逃到另一个群体。由于移动进程涉及将进程ID写入cgroup文件系统中的文件,因此可以通过使用正常的文件系统访问检查对该文件具有写访问权限的任何进程来完成此操作。当将PID写入该文件时,将进一步检查执行写入操作的进程的所有者是否也是正在添加的进程的所有者,或者是否具有特权。这意味着任何用户都可以将他们的任何进程移动到他们对cgroup有写访问权限的任何组中。过程,不管跨越多少层次结构。换句话说,我们可以限制将流程移动到哪里,但对从哪里移动的控制要少得多。一个c组只能被认为是“封闭的”,如果它的所有进程的所有者被禁止将一个进程移动到它之外的任何c组中。就像前面看到的层次结构操作一样,当把cgroup层次结构看作一个文件系统时,这些操作是有意义的,但是当把它看作一个分类方案时,这些操作就不那么有意义了。

问题

第一个问题,是否真的需要使用不同的层次结构来管理不同的资源。NTC提供的灵活性是远远超出需求,它是否树立了一个有价值的榜样?第二个问题是,如果不同的需求被强加于单一的层次结构,组合爆炸的可能性,以及其成本是否与价值不成比例。在任何一种情况下,我们都需要清楚地了解如何正确地向导致某些服务流程消耗各种资源的请求的发起者收费。在这些问题中,中间的问题可能是最简单的:拥有多个cgroup的实现成本究竟是多少?所以下次我们将讨论这个话题,当我们看到各种数据结构将所有东西联系在一起时。

内部观察

如果不探索代码,看看我们能找到什么模式,那么对Linux控制组这样丰富的东西的访问是不完整的。

我们已经简单地了解了几个cgroup子系统,所以这次的重点是cgroups的核心,特别是进程和cgroup之间的互联。我们要记住的一个问题是不同方法的比较成本。上次,我们看到了一个问题,即尝试使用单一层次结构并仍然允许对多个资源进行独立分类可能会导致组的组合爆炸。如果有Q个管理组,例如登录会话,并且在每个组中,我们可能希望对N种不同的资源以M种不同的方式对进程进行分类,那么我们可能需要Q x M x N个不同的组。如果我们允许每个资源有一个单独的层次结构,那么就只有Q + M x N个组。那么问题来了:单个层次结构的简单性是否超过了大量cgroup的开销?

管理层次结构本身不太可能特别有趣——基本的树数据结构在大多数计算机科学课程中都有介绍,cgroup层次结构的形状不太可能与此类结构有很大区别。有趣的问题是进程如何连接到cgroup。正如我们在探索各种cgroup子系统时所发现的,有时需要将进程映射到与特定子系统相关的cgroup,有时还需要获得cgroup中所有进程的列表。实现这些映射的数据结构将是我们的重点。
然而,有必要澄清一下。cgroup实际上不是进程组,尽管我们已经多次这样描述过。它们实际上是一组线程。因此,我们将通过了解线程和进程如何相互连接以及如何连接到一些相关对象来阐明它们之间的区别。

进程、线程间的连接

在Linux中,我们在4.4BSD中第一次看到的三层结构(会话、进程组和进程)在进程下面增加了一个额外的层次:线程。结果是一个类似于下图所示的层次结构。线程有自己的执行上下文和自己的“线程ID”编号,但可以与同一进程中的其他线程共享大多数其他细节,特别是地址空间、一组信号处理程序和进程ID编号。

在内部,线程由struct task_struct表示,有时称为任务。不幸的是,进程有时也称为任务——有一个do_each_pid_thread()宏,相当合理地遍历线程。用于遍历进程的匹配宏是do_each_pid_task()(两个宏都定义在pid.h中)。术语“进程”在某种程度上更可靠,但是,由于术语PID(或“进程ID”)有时用于线程、进程、进程组和会话,因此有时使用更精确的术语“线程组”会更安全。

如果会话、进程组、进程和线程这四个对象都以统一的方式管理,那将是非常优雅的。虽然几乎是这样,但仍然对线程进行了一些特殊的处理。虽然这可能不能说明所有处理上的差异,但线程有两个属性使它们真正不同。首先,进程中总是有一个线程,称为“group_leader”。这提供了一些明确的指向,并说明“这就是过程”。group_leader的线程ID是整个进程的进程ID。会话确实有一个领导进程,但只是在弱意义上,因为这个进程可以在会话的其余部分之前退出;进程组根本没有任何类型的leader。其次,线程只能通过退出的方式离开进程——线程不可能移动到不同的进程,不像进程组,进程可以从一个进程组移动到另一个进程组。这对锁有重要意义。

自从Linux获得了“PID命名空间”的概念,即同一个进程在不同的命名空间中可以有不同的PID,就有了一个“struct PID”(图中的蓝色),它将前3个对象类型连接在一起。这次讨论的重要成员

蓝色部分定义:

enum pid_type {PIDTYPE_PID, PIDTYPE_PGID, PIDTYPE_SID, PIDTYPE_MAX};
struct pid
{
	atomic_t count;
	unsigned int level;
	/* lists of tasks that use this pid */
	struct hlist_head tasks[PIDTYPE_MAX];
	struct rcu_head rcu;
	struct upid numbers[1];
};

图中黄色部分定义:

struct task_struct {
    ...
    struct pid			*thread_pid;
    struct hlist_node		pid_links[PIDTYPE_MAX];
    ...
}

image
每个PID都有task_struct的3个链表(hlist_head),通过每个task_struct中的3个hlist_nodes链接在一起。

会话和进程组列表(PIDTYPE_SID和PIDTYPE_PGID)实际上是进程的列表,只有线程组组长出现在这些列表中。

PIDTYPE_PID列表不是一个线程组中所有线程的列表,而是一个仅包含以该PID作为线程ID的线程(如果它仍然存在)的列表。使用一个列表只包含一个元素似乎很奇怪,但这是有原因的。当进程中的一个线程进行exec()系统调用(任何一种方式)时,进程中的所有其他线程都会被杀死(SIGKILL),执行exec()的线程会接管线程组组长的身份(特别是PID)。这可能导致两个不同的线程在短时间内具有相同的线程ID。有了一个PIDTYPE_PID链表,就可以做到这一点。

进程中的线程列表是单独管理的,使用字段:

struct task_struct {
    ...
    struct list_head		thread_group;
    ...
}

前3个链表在结构pid中有一个不同的“head”,然后是一些“nodes”,每个结构task_struct中有一个结点。
这个线程列表形成了一个没有头也没有尾的循环(用红色表示,没有head和node的区别)。这是一个微妙但重要的问题,可能令人惊讶的是,对于其他三个列表来说不是问题。

因为线程只有在被销毁时才会从线程组(也就是进程)中移除,所以在没有强锁的情况下遍历链表是安全的。轻量级的RCU read lock就足够了。当一个线程从链表中移除时,它会保持在中间位置,时间足够长,以至于任何正在遍历链表的代码以及当前正在查看被删除线程的代码仍然可以继续到达末尾——尽管如此,鉴于链表是一个循环,我们实际上应该说它到达了循环开始的位置。

这种情况的一个潜在问题是,如果一些代码遍历线程列表,并从一个在迭代结束前退出的线程开始,它将永远不会找到它的起点,并可能永远循环下去。从线程组leader开始总是安全的(因为在所有其他线程退出之前,它仍然是一个“僵尸”),但API中没有任何东西可以强制该起点。fill_stats_for_tgid()就是这种可疑用法的一个例子,它将所有线程的一些统计信息累积到一个列表中。如果这个请求是针对一个不是线程组长的PID(这很奇怪,但很有可能),那么如果线程在错误的时间退出,它可能会遇到问题。根据Oleg Nesterov(在Linux 3.14开发期间写作)的说法,“这个线程链接几乎每个无锁的使用都是错误的”。

因此,在Linux 3.14中引入了一种新的链接(不在图中),它有一个不同的thread_head,保存在signal_struct中,其中包含了一些特定于进程(而不是线程)的字段。我觉得有点失望,这个head没有像其他head一样出现在struct pid中,但这可能没有太多实际好处。在适当的时候,所有用户都应该离开thread_group链接,该链接可以丢弃。

这个问题提醒我们,锁既微妙又重要,所以必须正确理解。对于我们在这里看到的所有链表,以及将进程的子进程连接在一起的children/sibling链表,以及将所有进程连接在一起的init_task/tasks链表,都有一个reader/writer自旋锁,称为tasklist_lock,用于保护所有的访问和修改。在没有该锁的情况下,唯一允许的链表访问是跟踪进程中的线程(最好使用新的thread_head链表),并遍历从init_task开始的所有任务组leader。只有RCU读锁是安全的,因为线程不会从这些链表移动到另一个链表。
有人认为该tasklist_lock有问题。其中一个问题是,多个重叠的读取器会让需要获得写访问权限的进程挨饿。再次来自Oleg Nesterov(在对这个补丁集的回复中):“每个人似乎都同意tasklist[_lock]应该终止”。
将所有进程和线程的链表设置为rcu安全是一个有趣的练习。这当然不会是微不足道的,尽管Thomas Gleixner在四年前就提出了,但它仍然没有实现。但考虑到VFS的目录项(dentry)缓存通常可以通过RCU访问,似乎也可以对进程树做一些处理。

Cgroup间的连接

cgroups使线程和进程之间的各种关联变得更加有趣。将cgroups形成一个层次结构的链接并不令人惊讶(对于3.16,它基本上重新安排了,但原理没有改变):

struct task_struct {
    ...
    struct list_head sibling;   /* my parent's children */
    struct list_head children;  /* my children */
    struct cgroup *parent;      /* my parent */
    ...
}

更有趣的是线程和cgroup之间的关联。如前所述,可以有多个cgroup层次结构,每个线程都属于其中的一个c组。这需要一个M x N的映射,所以几个列表是不够的。所需的映射是使用两个中间数据结构实现的:css_set和cgrp_cset_link。
当进程分支时,子进程将与父进程处于相同的cgroups中。虽然任何一个过程都可以移动,但它们通常不是。这意味着进程集合(以及其中的线程)都位于同一组cgroup中是很常见的。为了利用这种共性,存在结构体css_set。它标识了一组cgroup (css代表“cgroup subsystem state”),每个线程都精确地依附于一个css_set。所有的css_set在一个散列表中链接在一起,以便当进程或线程移动到一个新的cgroup时,如果已有的css_set与所需的cgroup集合一起存在,则可以重用它。
当相似的线程和进程链接到一个css_set时,我们仍然有M x N的映射问题,只是M不再是线程的数量,而是现在数量少得多的css_set。为了将不同的cgroup链接到不同的css_set,我们有一个恰当的命名cgrp_cset_link:

struct cgroup {
    struct cgroup_subsys_state self;
    struct list_head cset_links;
}
struct cgrp_cset_link {
struct cgroup       *cgrp;
struct css_set      *cset;
struct list_head    cset_link;
struct list_head    cgrp_link;//连接到css_set->cgrp_links
};

image
对于每个css_set,每个层次结构都有一个cgrp_cset_link。每个cgroup都有一个cset_links字段,它将该cgroup的所有cgrp_cset_links连接在一起,可以从中找到该cgroup中的所有线程。类似地,每个css_set都有一个cgrp_links字段,从中可以找到包含该css_set中任何线程的所有cgroups(如果所有这些list_head让你头晕,makelinux对Linux链表有一个相当好的介绍)。
这个数据结构相当有效地将所有线程和所有cgroup链接在一起。它是否如此有效是另一个问题。这对于找到一个线程的所有cgroup,或者一个cgroup的所有线程来说是非常好的。它远不适合查找cgroup, cgroup为特定进程提供了net_cl子系统(用于确定分配给新套接字的类ID)。
为了满足这个需求,有一些额外的链接(没有在上图中显示)。每个css_set都包含一个指针数组,由子系统编号索引,提供了到每个子系统相关的cgroup的直接链接,绕过cgrp_css_link结构。

enum cgroup_subsys_id {
#include <linux/cgroup_subsys.h>
	CGROUP_SUBSYS_COUNT,
};
struct css_set {
    ...
    struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
    struct list_head e_cset_node[CGROUP_SUBSYS_COUNT];
    struct list_head cgrp_links;//连接多个cgrp_cset_link->cgrp_link
    ...
}

如果我们现在转向用于管理这些链表的锁,会有一些意外情况。首先,虽然有一个reader/writer锁保护所有这些链接,就像进程和任务那样,但这个锁(css_set_rwsem)是一个信号量而不是自旋锁。这令人惊讶,因为信号量通常用于锁可以保持较长时间的情况,因此等待进程可能需要睡眠(而不是旋转)。回顾该锁的历史,可以发现直到最近它还是一个自旋锁,并且在进程中被修改为信号量,以逐步整理某些代码。这似乎很可能再次成为自旋锁。

另一个惊喜更有趣:cgroups添加了另一个锁,可以阻止线程加入或离开线程组:group_rwsem。每个线程组都有一个这样的锁(存储在signal_struct中)。它很少被独占使用(即写),因此不太可能影响性能,但一般来说,避免新锁是最好的。但是,这确实提出了一个问题:为什么我们需要这个新锁来保护线程列表?
之所以需要该锁,是因为cgroups是线程组,而不是进程组。这意味着,在将进程添加到c组时,必须分别添加每个单独的线程,并且在从一个c组删除或添加到另一个c组时,必须保持线程列表的稳定。如果cgroup是真正的进程组,则不需要单独移动线程,该锁可以被丢弃。列出线程的价值,而不仅仅是列出进程(或组长),在代码中并不明显。对于各个子系统是否可以利用该函数来区分不同的线程,从这个角度对其进行分析,留给感兴趣的读者作为练习。

我们学到了什么?

研究数据结构的大部分价值只是充实一幅图像,让我们了解事物是如何连接在一起的,这样我们就可以推断出任何可能的变化的后果。尽管如此,我们可以从这次探索中获得一些具体的经验教训。
首先,我们有一个提醒,术语可能具有挑战性,在解释我们在内核代码中读到的内容时应该小心。task通常就是任务, MAX通常就是最大值。但并非总是如此。

其次,锁可能是棘手的。通常,最好尽可能避免使用锁,在数据结构简单优雅时,使用锁会更容易。降低tasklist_lock的影响是可能的,而且显然是可取的。如果有必要,减少cgroups锁的影响会更加困难,因为数据结构更复杂。
最后,这种复杂性的一个关键点是多链接的cgroup_cset_link结构的大量出现。如果像上次讨论的那样,我们有一个单一的层次结构并创建多个cgroup,那么就会有和cgroup一样多的cgroup,以允许不同资源的不同进程分类的不同组合。

换句话说,我们担心的组合爆炸是不可避免的。它可能在许多cgroup中是显式的,也可能在许多cgroup_cset_link结构中是隐式的,但它仍然存在。目前,cgroups是比link结构大得多的结构,而cgroup_cset_links是自动创建的,不需要mkdir请求,因此增加link的开销可能比增加cgroups的开销小。
不过,这确实表明,我们可以用另一种方式来表达这种担忧。与其担心激增,不如关注cgroups的大小以及可以使用什么机制自动创建cgroups
这个谜题留下了一个开放的问题,我们来到了我们可以在Linux 3.15中发现的关于cgroups的结尾,Linux 3.15是写作时的当前版本。我们还没有完全完成。下一部分(也是最后一部分)将超越3.15,找出所提议的统一层次结构可能带来的价值,并尝试将所有核心问题呈现为一个连贯的画面。

统一和超越

在最后一部分中,我们有两个机会测试我们的新技能。
一个明显的第一个目标是“统一层次结构”,它在Linux 3.16中作为开发者预览版可用,最近在这些页面中已经介绍过。如果您还没有这样做,现在可能是时候回过头来重读这篇文章,看看我们发现的各种问题是否(以及如何)得到了解决。如果你想彻底了解,也可以阅读unified-hierarchy.txt文档。
你可以先把你认为重要的事情列出来。它还可以帮助列出一些需要注意的设计模式或反模式。我认为在之前关于“Unix历史的幽灵”的系列文章中确定了四个有用的方法:充分利用合并设计不可修复的设计高维护的设计,所有这些都在上一篇文章的最后进行了总结。

对统一的层次结构的打分

统一的层次结构 B

几乎无可争议的是,经典cgroup允许的层次结构数量过多。不太清楚将数字减少到1是否理想。在我们的调查中,我们发现了层次结构的不同用途:一些子系统向下施加控制,另一些子系统向上收集账目。这些不同的用途涉及不同的实现关注点。有争议的是,他们为不同的等级制度辩护。
统一的层次结构显然是为了消除过度的重复,这是好事。它似乎没有承认不同的子系统可能真的有不兼容的需求,但它还没有完全关闭分离层次结构的大门。所以这方面应该得到B——做得很好,但还有改进的余地。

进程只允许在叶子上 C+

统一的层次结构要求进程只存在于树的叶子节点中。这种执行方法有点笨拙。叶子是“任何不将任何子系统扩展到子节点的节点”,在层次结构中创建新层次时需要两个步骤。首先必须向下移动进程,然后子系统才能向下扩展。

这种复杂性达到的最终结果无论如何都是可能的(系统管理员和工具可以很容易地选择将进程保留在叶子进程中),因此,在很大程度上是无趣的。不清楚内核是否需要强制执行只在叶子进程中的健全策略,也不清楚内核是否需要强制执行文件系统根目录对大多数用户来说是只读的健全策略。

我本想给这个议题一个C(太复杂),但设计上有一个缺陷需要强调。进程被排除在内部的cgroup之外,根cgroup除外,显然是因为根c组需要“特殊处理”。这个异常实际上会导致C+的分数,原因将在后面说明。

驯服混乱的子系统 D

我们已经看到,c组子系统和功能元素之间的关联是相当混乱的。这并不是一个新的观察:在2011年的内核峰会上,Paul Turner说:
谷歌将根据自己的经验,将许多控制器拆开,重新制作成更好的形式。

虽然这种重写可能太多了,但如果我们能够弱化当前子系统的划分,希望能够出现更有意义的分组(可能是在进程的控制和其他资源的控制之间),那就太好了。统一的层次结构似乎很好地满足了这一需求,但不幸的是,它走向了相反的方向。子系统的列表现在出现在整个cgroups文件系统的cgroup中。控制器和cgroup。subtree_control文件。的确,属性文件已经以它们的子系统命名,但有一个冷冻库。无论“freeze”是一个独立的子系统还是一个功能元素,状态文件都是有意义的。

cgroup.controllers显式列出已启用的子系统,有效地巩固了当前的结构,所以这个问题从我这里得到了D。

提供资源使用者ID B

我们在第5部分中看到,内存中的页可以在释放内存时确定谁获得退款,但不能在写入内容时确定谁需要支付IO费用。通过坚持所有子系统使用单一层次结构,单个cgroup可以作为所有资源类型的资源消费者ID。这显然是这个问题的一个解决方案,但很难判断它是否是一个好的解决方案(不同的资源可能非常不同),所以我现在保留判断,只给B。

进程或线程 A

经典cgroups允许进程中的单个线程在不同的cgroups中。想象一个可靠的用例是困难的,但并非完全不可能。

cpuset控制器可以将进程限制为NUMA系统中的一组cpu,以及一组内存节点。前一种限制可以施加到使用sched_setaffinity()系统调用或taskset程序的任何线程,而不涉及cgroup。但是内存节点集只能通过cgroups进行配置。在不同的线程(共享一个地址空间)上施加不同的内存节点是没有多大意义的,所以这并不能证明每个线程都有cgroups是正确的,但还有其他只能通过cgroups设置的值。

Linux调度器允许以比传统的40点“nice”级别更精细的粒度设置线程的优先级。它允许每个线程或组有一个“权重”,其范围约为100,000(权重= 1024 * 0.8nice,大约)。这个权重只能使用cgroups来设置。如果你想要对单个线程进行精细的控制,你需要在cgroups中使用线程。

这两个都是我称之为“procfs问题”的例子。由于没有明确的设计指南,procfs文件系统成为了一种向内核添加各种临时功能的方式,而很少需要进行设计审查。因此,它处理的远不止进程。类似地,cgroups似乎允许对功能进行“后门”访问,这完全不是特定于控制进程组的功能。如果thread-in-cgroups的唯一用途是从这些后门中获益,那么禁止它们可能会鼓励更好的API设计。

统一层次结构正是这样做的,只允许进程(也称为线程组)处于不同的cgroup中。这似乎是个好主意,但也提出了一个问题:我们到底应该控制什么?线程?过程?别的吗?不管答案是什么,放弃对移动单个线程的支持似乎是一个好主意,所以这得到了a。

实现简单性 A

统一的等级制度只是一个漫长过程中的一步。代码中有很多改进,导致了目前的状态,但只有删除一些旧的功能,这些更改的全部价值才能完全实现。具体是什么时候还不得而知。
我们所知道的是,只有进程(而不是线程)最终需要在cgroup中,并且它们只需要在单个cgroup中。这当然会简化程序,所以A是理所当然的。

总结

上面分配的不太理想的分数可能有几个原因,尤其是我自己的个人偏见。最重要的一个原因几乎肯定是建立统一层次结构的基础。像许多第一次实现一样,cgroups确实不是很好:层次结构的作用和子系统的目的很混乱。

统一层次结构的前提之一是我们必须以某种形式与control groups保持一致。Tejun Heo可能更喜欢“像会议或项目组那样的过程树”上的不同层次的结构,但惋惜“这艘船很久以前就起航了”。就在一年多以前,发生了一件可能不那么令人沮丧的事情。

自动组调度

正如2010年底所报道的,除了cgroups之外,还有其他方法来控制进程组。利用为cgroups开发的组调度支持,Mike Galbraith创建了一种不同的自动机制,将进程分组以进行调度。

标准的Unix调度器以及大多数后继者都试图对进程做到公平,但进程不一定是公平的最佳关注点。在我还是学生时使用的AUSAM Unix变体(澳大利亚Unix Share Accounting方法,后来演变为Share II)中,公平性首先针对用户,因此一个运行6个进程(当时的本地限制)的学生不会比另一个只运行一个进程的学生获得更多的CPU时间。在现代开发人员的桌面上,“作业”(在作业控制的意义上-一个进程组)是一个非常合乎逻辑的分组。不同的工作(浏览器、游戏、make - j40)可以在平等的基础上合理地相互竞争,一个工作中的进程或线程应该合理地相互竞争,但作为个体,不应该与其他线程竞争。

使用进程组进行自动调度有两个问题,在记录自动组调度历史的邮件列表线程中提出。这些问题是由不同的人提出的,得到了不同的回应。

第一种是由Linus Torvalds提出的,它认为进程组的粒度太细,无法达到该目的。创建一个新的调度组确实会有一些成本,因此频繁地创建调度组可能会导致无法接受的速度慢。不幸的是,没有任何记录表明有人测量这种代价(尽管有一些来自Linus的鼓励),只有一个关于“太频繁”的模糊评估——介于“shell中每个命令调用一次”和“每秒成千上万次”之间。

这种说法从未受到真正的挑战。最后的实现使用了“会话”而不是“进程组”,当然创建进程组的次数较少。然而,这似乎不是正确的分组。如果你运行:

    make -j 40 >& log &

为了编译你的项目,然后用freeze -bubble来打发时间——两者都在同一个终端窗口——你的游戏将与40个进程竞争,而不是一个作业。

如果同时创建了一个调度器组,测试fork()+exec()的开销是相当容易的:/bin/env /bin/echo hello和/bin/setsid /bin/echo hello会做完全相同的事情,只是后者会创建一个新的会话,因此会创建一个新的调度器组(如果两者都是在shell脚本中运行,而不是在交互式shell中运行):

time bash -c 'for i in {1..10000}; do /usr/bin/setsid /bin/echo hi ; done > /dev/null'
time bash -c 'for i in {1..10000}; do /usr/bin/env /bin/echo hi ; done > /dev/null'

两者的区别当然造成了干扰

第二个问题是由Lennart Poettering提出的:“在桌面上,这是完全不相关的。”在这种说法提出的时候,它在很大程度上是正确的,因为自动分组是在“控制tty”的基础上完成的,而大多数桌面应用程序同样没有控制tty。视频编辑器和浏览器将在同一个调度组中,因此一个使用的多个渲染线程可能会淹没另一个使用的单线程。在讨论结束时,这在不同程度上也是正确的。自动分组现在是基于“会话”完成的,而且大多数桌面会话管理器不会将每个应用程序放入不同的会话中。正在开发的一个会话管理器做到了这一点:systemd已经根据需要使用了setsid()。

尽管Lennart的评论没有得到很好的反响,但他当时正在开发一款软件,可以很容易地为更大的用户群体带来自动组调度的好处。似乎没有人意识到这一点。

但是,回到主要的故事,从自动组调度的关键教训是,cgroups的工作启发了调度器中的一些有用的功能,这些功能可以完全独立于cgroups使用。在进程处于非根cgroup(从cpu子系统的角度来看)时,它是按照cgroups进行调度的。当它在根目录时,它根据自动组进行调度(除非自动组已禁用)。这就是为什么统一的层次结构允许进程保持在层次结构的根中是积极的,即使根不再是叶子节点。这意味着独立的资源管理可以与cgroups并行开发,并且cgroups和非cgroups管理可以在同一个系统上进行。这就引出了第二个挑战。

我们有一些最初的cgroups开发人员没有的东西:多年的经验和工作代码。这是一笔财富,我们应该能够把它转化为我们的优势。因此,要测试您对资源管理的新理解,挑战是:受调度的自动组的启发,您如何在Linux中与cgroups一起实现资源管理和进程控制,但独立于cgroups?一旦你想清楚了,你可以回来和我比较你的结果。别担心,你做完的时候我们还在。

hindsight groups:通过对比突出一些问题。

对比是帮助我们更清楚地看待事物的有力工具。因此,为了展示我认为重要的问题,我把它们放在了不同的环境中。hindsight groups,一个反映其起源的名字,有时不同是为了表明观点,有时不同只是为了不同。hindsight groups关注的是:他们只是关于进程组的限制。任何不符合描述的需求都需要在别处实现

在hindsight groups(或“hgroups”)中,控制的基本单元是进程组,由交互式shell、systemd以及任何其他会话管理器创建。仍然可以使用prlimit()或类似的命令对单个进程进行控制,但是控制组的粒度并不比进程组更细。

为这些进程组提供管理结构,在PID层次结构中增加了一个新级别。在会话和进程组上面引入了“进程域”。进程最初位于域0中。处于零域并且单独处于其会话和其进程组中的进程可以调用set_domainid()来启动一个从属于零域的新域,从而创建一个两级的域层次结构。当创建一个新的PID命名空间时,包含启动进程的命名空间在新命名空间中显示为域0,该命名空间中的新域从属于本地域0,从而建立一个多层次的层次结构。

由域形成的层次结构强烈地约束着过程。一旦进入一个域,进程就不能离开这个域。每个域都与一个进程组相关联——即创建它的进程的进程组。同一域中的所有其他进程组都被认为从属于第一个进程组。这实际上将所有进程组置于一个层次结构中。这是一个组织层次结构,而不是一个分类层次结构。它提供了结构化分组,如“登录会话”、“容器”或“作业”。它根据进程执行的任务而不是它们的行为方式来收集进程。

有了这个为进程组定义得更强的新角色,就有了一个新的数据结构,它是按进程组分配的,很像每个进程分配的signal_struct。它包含一组适用于该组中的进程的限制。其中一些机制,如设备的访问控制列表(类似于设备cgroup子系统提供的列表),在进程需要检查某些操作是否被允许,而cgroups未配置或仅提供“根”cgroup时,会引用该列表。其他的,比如可能使用的一组cpu,需要在进程组中的所有进程和线程发生变化时推送给它们。这是通过向所有进程发送一个虚拟信号来统一完成的(类似于冷冻器cgroup子系统所采取的方法)。在处理虚拟信号期间,进程将根据进程组中的限制更新其本地理解。

任何具有适当用户ID或超级用户权限的进程都可以修改对每个进程组的限制。然而,一个进程只能给它自己的进程组,以及层次结构中高于它的每个进程组的额外权限(即减少限制)。修改不会通过内核向下传播,但用户空间的工具可以可靠地传播限制的解除或施加。

一个重要的限制确定了进程不能执行的某些操作,如果进程尝试执行,会导致阻塞。设置这个限制可以有效地冻结组中的所有进程,就像cgroups冻结器一样。另一种设置仅在进程试图创建新进程组时冻结进程。这使得可以以无竞争的方式对域中的所有进程组施加一些限制。

各种共享资源:内存、CPU、网络和块I/O,每种资源都有特定的需求,并被单独管理。他们从这些分组和等级制度中受益,但他们不受其束缚。

网络和块I/O有一些相似之处,因为它们通常涉及限制或共享数据吞吐量。它们也很容易虚拟化,因此子域可以被授予访问虚拟设备的权限,该虚拟设备将数据路由到真实设备。他们可以有多个独立的设备来管理,并有除了所涉及的过程之外的其他关注点。网络系统需要管理自己的链路控制流量,也可能需要管理从其他接口转发的流量。块I/O子系统已经在内部区分了元数据(使用REQ_META标志)和其他数据,因此需要以不同的方式对请求进行分类。

因此,这两个系统有自己的队列管理结构,hgroup不知道这些结构。各种排队算法可以根据发起域对请求进行分类,或者支持对个别进程进行标记(类似于cgroups网络类子系统),但这超出了hgroups的兴趣范围。

内存使用管理与其他共享资源有很大的不同,因为它是在空间上而不是在时间上测量的。对于其他三个(网络、块I/O、CPU),进程可以随时启动或停止使用资源,也可以暂时禁止使用资源,而不会产生不良影响。对于内存来说,除非在一段重要的时间内持续可用,否则资源是无用的。

这意味着,就像我们在前面的文章中看到的那样,内存必须被计费给某个持续了相当一段时间的实体。这也意味着很难强制实行比例分担。cgroups内存控制器施加了两个限制:一个不能超过的硬限制,以及一个仅在内存非常紧张时施加的软限制。改变软限制并不会真正影响共享的比例,而是会影响在必须释放内存时所造成的痛苦比例。

进程域可以完美地满足持久性和施加限制的这两个需求,它们的作用与mem子系统使用的层次结构中的cgroup几乎相同。每个进程组中使用的内存资源由包含域负责,如果有包含域,则由该包含域的包含域负责。如果达到任何限制,则分配失败并启动内存回收。与cgroups一样,也有硬限制和软限制。

特权进程可以将从属域内任何进程组的内存记录重定向到其他域,从而使该进程组内的使用由其他域负责。例如,即使主hgroups结构不能识别用户,也可以使用该机制强制指定属于某个用户的所有域的整体内存限制。在任何情况下,一个PID号(当前为24位)就足以标识内存资源的所有者。这将允许将两个甚至三个不同资源的标识符附加到一个请求或一页内存,以适当地进行后续处理。

施加CPU吞吐量限制的方式与施加内存分配限制的方式几乎相同。唯一的区别是,可以对本地进程组施加限制,也可以对域施加限制。通过适当的特权进程,可以提高或降低限制。

CPU调度可能是最复杂的资源管理器。调度组大致按照域/进程组/进程的层次结构形成,但在每个层次上分组是可选的。如果对域0启用分组,那么将对每个域中的进程进行分组,并对这些组彼此进行调度。如果没有启用,那么每个域中的各个进程组都会互相调度,创建的结果与当前的自动组非常相似。与内存资源一样,有特权的进程可以将进程组调度到其他进程组的上下文中。

在单用户系统上,域调度很可能被禁用,而顶层调度将在进程组之间进行。在多用户系统中,域级调度的额外开销可能是合理的。在容器中,可以在每个容器中独立地做出相同的选择。

这种在每个级别上独立启用CPU调度的方法,有点类似于统一层次结构(unified hierarchy)在不同级别上可选启用不同子系统的方法。不过,它更通用,因为启用的级别集合不需要是连续的。

Hgroups与cgroups和auto-groups相比,CPU调度还有另一个重要的区别。自动组调度的一个问题是,它改变了使用nice使程序以较低优先级运行的效果。nice不再真正工作的事实已经被报道了,但还没有修复。似乎有些回归比其他回归更不重要,尽管可能没有在正确的论坛上报告。

问题在于,每个调度组都有一个独立于组内进程的优先级。当你设置某个进程的友好性时,它只会使它对同一组中的进程友好(自动组使用相同的会话)。当一个用户有多个会话时(这就是自动组的全部意义),它们不能轻易地友好相处。

Hgroups不负责设置优先级,只负责施加限制。它对进程组的限制是设置该组优先级权重的上界。一个组的有效权重是活跃成员权重的总和(或者可能是最大值),前提是不超过上限。这允许低优先级进程继续对所有其他用户友好,而不仅仅是同一调度组中的用户。在没有低优先级进程的情况下,它的工作原理与目前的方案大致相同。

结语

我确实发现这次冒险非常有教育意义,我很感谢你能加入我。它实现了深刻理解的目标,但我还不能说它是否会实现提高娱乐的目标。当cgroups故事的下一章被揭示时,我准备兴奋或沮丧;激动的或厌恶的;挑战的或肯定的但我最不想做的就是无聊。

标签:指南,cgroups,Cgroup,层次结构,线程,cgroup,进程,子系统
From: https://www.cnblogs.com/fanguang/p/18610067

相关文章

  • Ubuntu 20.04 & 24.04 双网卡 Bond 配置指南
    前言:在现代服务器管理中,网络的稳定性和可靠性至关重要。为了提高网络的冗余性和负载能力,我们经常需要配置多个网络接口以实现链路聚合或故障转移。Ubuntu系统自17.10版本起,引入了Netplan作为新的网络配置抽象化工具,它提供了一种简洁的YAML文件格式来管理网络配置。本指南旨在为Ubu......
  • GoPro 13 & GoPro Quik 不完全踩坑指南 All In One
    GoPro13&GoProQuik不完全踩坑指南AllInOneGoPro13&GoProQuikAppbugsAllInOnebugsGoProQuik编辑大文件11GB,导出视频时候提示磁盘空间过低bugGoPro会把录制时间过长/或文件过大的视频,自动按照12GB大小分割成多段视频,导致后期编辑视频......
  • JupyterLab安装指南
    JupyterLab安装指南新建虚拟环境在base环境或者新建一个虚拟环境安装Jupyter。#安装虚拟环境,[py39]为新建的环境名condacreate-npy39python=3.9#激活环境condaactivatepy39#退出环境condadeactivate#复制虚拟环境condacreate-n新的环境名--clo......
  • 探索SAP HANA Cloud Vector Engine:从设置到自查询的全面指南
    探索SAPHANACloudVectorEngine:从设置到自查询的全面指南在数据科学和人工智能的世界中,SAPHANACloudVectorEngine提供了强大的向量存储能力。本文将深入探讨如何使用SAPHANACloud向量引擎,设置向量存储及其自查询功能。引言在处理大量数据时,能够高效地存储和检索......
  • Envoy 进阶指南(上):从入门到核心功能全掌握
    文章目录1.Envoy入门1.1什么是Envoy1.2Envoy的核心功能1.3Envoy术语1.4设计目标1.5Sidecar模式2.初识Envoy2.1安装Envoy2.2简单示例了解Envoy2.3管理视图1.Envoy入门1.1什么是EnvoyEnvoy是一款CNCF旗下的开源项目,由Lyft开源。Envoy采用C++实现,......
  • 2024年最新最全网络安全护网行动指南【附零基础入门教程】_网络安全靶场整个业务指南
    前言随着《网络安全法》和《等级保护制度条例2.0》的颁布,国内企业的网络安全建设需与时俱进,要更加注重业务场景的安全性并合理部署网络安全硬件产品,严防死守“网络安全”底线。“HW行动”大幕开启,国联易安誓为政府、企事业单位网络安全护航!,网络安全形势变得尤为复杂严峻。......
  • 微信授权全链路打通指南
    近期,我在致力于打造自己的小程序产品时,迎来了一项关键性的进展——微信相关授权流程的完整实现。从用户登录到权限获取,我们细致入微地梳理并实现了每一项授权机制,确保了用户体验的流畅与安全。微信小程序授权授权流程:用户在小程序中点击登录按钮,触发wx.login()获取code。......
  • 织梦怎么修改网站文字,织梦CMS网站文字修改指南
    在织梦CMS中修改网站文字通常涉及编辑模板文件或内容管理模块。以下是详细的步骤:登录后台管理系统:使用管理员账号登录织梦CMS的后台管理系统。编辑模板文件:进入“模板”->“默认模板管理”。找到需要修改的模板文件,通常是index.htm或其他相关的HTML文件。打开模板......
  • 网站主页修改方案怎么写,网站主页修改方案编写指南
    编写网站主页修改方案需要详细规划和描述修改的具体内容和步骤。以下是详细的步骤和建议:确定修改目标:明确修改的目的和预期效果。例如,提升用户体验、增加转化率等。分析现状:分析当前主页存在的问题和不足。收集用户反馈和数据分析结果。设计新主页:设计新的主页......
  • Easysearch Java SDK 2.0.x 使用指南(一)
    各位Easysearch的小伙伴们,我们前一阵刚把easysearch-client更新到了2.0.2版本!借此详细介绍下新版客户端的使用。新版客户端和1.0版本相比,完全重构,抛弃了旧版客户端的一些历史包袱,从里到外都焕然一新!不管是刚入门的小白还是经验丰富的老司机,2.0.x客户端都能让你开发效率......