> 建议提前阅读 [Linux Cgroups V1 介绍与使用](http://icebergu.com/archives/linux-cgroups-v1)
Runc 可以算是启动创建容器的最后一步,其中设置 Cgroups,隔离 namespaces,配置网络,挂载相应的卷 等一系列操作
![在这里插入图片描述](https://img-blog.csdnimg.cn/2020051313413684.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RBR1UxMzE=,size_16,color_FFFFFF,t_70)本文将主要讲 runc 是如何去操作系统中的 Cgroups,实现对资源的限制和管理的
Runc 支持三种方式来限制管理资源,分别是使用 Cgroups V1, Cgroups V2, Systemd
本文将主要讲解 Cgroups V1, 关于 Cgroups V1 相关的基本概念可以参考 [
Linux Cgroups V1 介绍与使用](https://blog.csdn.net/DAGU131/article/details/106054294)
## Cgroup Manager
Cgroup Manager 是 runc 实现对系统的 cgroup 操作抽象
实现了设置资源的配置,PID 加入到指定`控制组`,`控制组`的销毁,`控制组`中`进程`的暂停恢复,获取配置,获取统计信息等操作
```go
type Manager interface {
Apply(pid int) error // 将 pid 加入到控制组中
Set(container *config.Config) error // 设置控制组的配置
GetCgroups() (*configs.Cgroup, error) // 获取控制组的配置
GetPids() ([]int, error) // 返回控制组(cgroup) 中的 PID, 不包括子控制组
GetAllPids() ([]int, error) // 返回控制组和它的子控制组的所有 PID
GetStats() (*Stats, error) // 获取控制组统计信息
Destroy() error // 删除控制组
GetPaths() map[string]string // 获取保存 cgroup 状态文件的路径
GetUnifiedPath() (string, error) // 如果容器组没有挂载任何控制器(子系统), 则返回值同 GetPaths,否则,返回 error
Freeze(state configs.FreezerState) error // 任务的暂停和恢复
}
```
*`GetStats`* 方法会返回`控制组`中的统计信息,记录了 CPU, Memroy, Blkio 之类的一些状态
```go
type Stats struct {
CpuStats CpuStats `json:"cpu_stats,omitempty"`
MemoryStats MemoryStats `json:"memory_stats,omitempty"`
PidsStats PidsStats `json:"pids_stats,omitempty"`
BlkioStats BlkioStats `json:"blkio_stats,omitempty"`
// the map is in the format "size of hugepage: stats of the hugepage"
HugetlbStats map[string]HugetlbStats `json:"hugetlb_stats,omitempty"`
}
```
*`Set`* 方法在设置`控制组` 资源的时候需要传递 *`config.Config`* ,**实际上该方法只会使用 *`Config.Cgroup.Resources`* 中的数据**
```go
// Config 定义容器的配置
type Config struct {
// ....
Cgroups *Cgroup `json: "cgroup"`
// ....
}
type Cgroup struct {
Path string `json:"path"` // cgroup 路径,相对于`层级`的地址
ScopePrefix string `json:"scope_prefix"` // ScopePrefix describes prefix for the scope name
Paths map[string]string // 所属各个控制器的 cgroup 路径,需要注意该路径是绝对路径
// 包含了各个子系统资源的设置
*Resources
}
type Resources struct {
AllowAllDevices *bool `json:"allow_all_devices,omitempty"`
AllowedDevices []*Device `json:"allowed_devices,omitempty"`
DeniedDevices []*Device `json:"denied_devices,omitempty"`
Devices []*Device `json:"devices"`
Memory int64 `json:"memory"`
MemoryReservation int64 `json:"memory_reservation"`
MemorySwap int64 `json:"memory_swap"`
KernelMemory int64 `json:"kernel_memory"`
KernelMemoryTCP int64 `json:"kernel_memory_tcp"`
CpuShares uint64 `json:"cpu_shares"`
CpuQuota int64 `json:"cpu_quota"`
CpuPeriod uint64 `json:"cpu_period"`
CpuRtRuntime int64 `json:"cpu_rt_quota"`
CpuRtPeriod uint64 `json:"cpu_rt_period"`
CpusetCpus string `json:"cpuset_cpus"`
CpusetMems string `json:"cpuset_mems"`
PidsLimit int64 `json:"pids_limit"`
BlkioWeight uint16 `json:"blkio_weight"`
BlkioLeafWeight uint16 `json:"blkio_leaf_weight"`
BlkioWeightDevice []*WeightDevice `json:"blkio_weight_device"`
BlkioThrottleReadBpsDevice []*ThrottleDevice `json:"blkio_throttle_read_bps_device"`
BlkioThrottleWriteBpsDevice []*ThrottleDevice `json:"blkio_throttle_write_bps_device"`
BlkioThrottleReadIOPSDevice []*ThrottleDevice `json:"blkio_throttle_read_iops_device"`
BlkioThrottleWriteIOPSDevice []*ThrottleDevice `json:"blkio_throttle_write_iops_device"`
Freezer FreezerState `json:"freezer"`
HugetlbLimit []*HugepageLimit `json:"hugetlb_limit"`
OomKillDisable bool `json:"oom_kill_disable"`
MemorySwappiness *uint64 `json:"memory_swappiness"`
NetPrioIfpriomap []*IfPrioMap `json:"net_prio_ifpriomap"`
NetClsClassid uint32 `json:"net_cls_classid_u"`
CpuWeight uint64 `json:"cpu_weight"`
CpuMax string `json:"cpu_max"`
}
```
config.Cgroup 需要注意一下,其中 Path 字段是相对于`层级`挂载路径的`控制器`路径,层级的概念在[Cgroups V1](https://blog.csdn.net/DAGU131/article/details/106054294) 中已经解释
> 比如,我的`控制组`系统中路径是 *`/sys/fs/cgroup/cpu/iceber/cgroup1`*
> 其中/sys/fs/cgroup/cpu 是 Cgroups 的一个层级,他绑定了 cpu 这个`子系统/controller`
> config.Cgroup.Path 就应该是 /iceber/cgroup1
confi.Cgroup.Paths 这个是直接提供每个`子系统`下`控制组`的系统路径,他会屏蔽掉 Path 字段的作用
## Cgroups V1 Manager
```go
// libcontainer/cgroups/fs/apply_raw.go
type Manager struct {
mu sync.Mutex
Cgroups *configs.Cgroup
Rootless bool // ignore permission-related errors
Paths map[string]string // 记录各个子系统下控制组的路径
}
```
我们使用 Manager 直接初始化相应字段就可以了
```go
manger := &Manager{
Cgroups: configCgroup,
}
```
manager.Cgroups 实际会使用到的字段是 configs.Cgroup.Path,configs.Cgroups.Paths,通过这两个字段来找到`子系统`的`控制组`路径
> manager.Cgroups.Resources 字段可能也会使用到
> 比如 Freeze 方法就会利用到 manager.Cgroups.Resources.Freezer 字段
### *`apply`* 方法 将 pid 加入到`控制组`
*`apply`* 的作用是根据 *manager.Cgroups.Path/Paths* 将 pid 参数加入`子系统`的`控制组`中
*`getCgroupData`* 函数会返回 **cgroupData**,而 subsystem 接口的 *`Apply`* 方法便是通过该结构来执行具体的逻辑
**cgroupData** 的 *`path`* 方法可以获取子系统的系统路径
```go
func (m *Manager) Apply(pid int) (err error) {
if m.Cgroups == nil {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
var c = m.Cgroups
d, err := getCgroupData(m.Cgroups, pid)
if err != nil {
return err
}
// m.Paths 记录了各个 subsystem 下控制组的路径
m.Paths = make(map[string]string)
// 判断 cgroups 配置中是否指定子系统下控制组的路径
if c.Paths != nil {
for name, path := range c.Paths {
// 检查子系统是否挂载正常
_, err := d.path(name)
if err != nil {
if cgroups.IsNotFound(err) {
continue
}
return err
}
m.Paths[name] = path
}
// 将 pid 加入到指定控制组中
return cgroups.EnterPid(m.Paths, pid)
}
// 根据cgroups.Path 来获取相应子系统下控制组路径
for _, sys := range m.getSubsystems() {
// 获取子系统下控制组路径
p, err := d.path(sys.Name())
if err != nil {
// 由于安全原因,devices 必须存在
if cgroups.IsNotFound(err) && sys.Name() != "devices" {
continue
}
return err
}
m.Paths[sys.Name()] = p
// 执行具体子系统的 Apply 逻辑
if err := sys.Apply(d); err != nil {
// handle error
}
}
return nil
}
```
我们现在来看一下 getCgroupData 具体做了什么
注意:**cgroupData 其实只针对 manager.Cgroups.Path 有效,因为manager.Cgroups.Paths 已经指定了完整系统路径**
```go
/*
eg: cgroupData 中的数据是这样的
{
root: "sys/fs/cgroup"
innerPath: "/parentCgroup/cgroup1"
pid: 10000
config: *config.Cgroup
}
*/
type cgroupData struct {
root string // Cgroups 的挂载目录
innerPath string // 子系统下控制组的相对目录
config *configs.Cgroup // cgroups 的配置,包含了各个子系统的资源配置,会在子系统设置相关资源时使用
pid int // 加入到控制组的 PID
}
func getCgroupData(c *configs.Cgroup, pid int) (*cgroupData, error) {
// getCgroupRoot 会通过从 /proc/self/mountinfo 查询 Cgroups 挂载点的目录
root, err := getCgroupRoot()
if err != nil {
return nil, err
}
// 要求配置中提供配置组的路径,Path 是相对于 root
if (c.Name != "" || c.Parent != "") && c.Path != "" {
return nil, fmt.Errorf("cgroup: either Path or Name and Parent should be used")
}
// 对路径进行安全处理
cgPath := libcontainerUtils.CleanPath(c.Path)
cgParent := libcontainerUtils.CleanPath(c.Parent)
cgName := libcontainerUtils.CleanPath(c.Name)
// 默认使用 Path, 而 Parent 和 Name 其实已经废除
innerPath := cgPath
if innerPath == "" {
innerPath = filepath.Join(cgParent, cgName)
}
return &cgroupData{
root: root,
innerPath: innerPath,
config: c,
pid: pid,
}, nil
}
```
*`Apply`* 方法最终会调用各个`子系统`的 *`Apply`* 方法,那我们现在来看一下`子系统`的接口定义,以及以 cpu `子系统` 为例的 *`Apply `* 逻辑
## subsystem(子系统)/controller(控制器)
Cgroups 中使用 `子系统/控制器` 来管理和限制具体的资源
runc 提供了 subsystem 接口,定义了控制器需要提供给 Manager 使用的方法
```go
type subsystem interface {
Name() string // 返回子系统的名字
// 创建 cgroupData 指定的控制组,并将 cgroupData.pid 加入到该控制组
Apply(*cgroupData) error
// 设置控制组路径的资源配置
Set(path string, cgroup *configs.Cgroup) error
// 获取指定控制组路径下的资源统计信息
GetStats(path string, stats *cgroups.Stats) error
// 移除 cgroupData 中指定的控制组
Remove(*cgroupData) error
}
// 声明 Manger 会使用的所有子系统
var subsystemsLegacy = subsystemSet{
&CpusetGroup{},
&DevicesGroup{},
&MemoryGroup{},
&CpuGroup{},
&CpuacctGroup{},
&PidsGroup{},
&BlkioGroup{},
&HugetlbGroup{},
&NetClsGroup{},
&NetPrioGroup{},
&PerfEventGroup{},
&FreezerGroup{},
&NameGroup{GroupName: "name=systemd", Join: true},
}
```
我们现在来看一看 cpu 子系统的 *`Apply`* 方法做了哪些事情
```go
type CpuGroup struct{}
func (s *CpuGroup) Apply(d *cgroupData) error {
// 获取 cpu 子系统的控制组地址
path, err := d.path("cpu")
if err != nil && !cgroups.IsNotFound(err) {
return err
}
return s.ApplyDir(path, d.config, d.pid)
}
func (s *CpuGroup) ApplyDir(path string, cgroup *configs.Cgroup, pid int) error {
if path == "" {
return nil
}
// 确保路径存在
if err := os.MkdirAll(path, 0755); err != nil {
return err
}
// 在添加进程加入控制组之前,设置实时资源配置
// 因为如果进程已经进入 SCHED_RR 模式并且没有设置 RT 带宽,再次添加会失败
if err := s.SetRtSched(path, cgroup); err != nil {
return err
}
// 将 pid 加入到控制组中
return cgroups.WriteCgroupProc(path, pid)
}
```
可以看到,实际 Apply 做了两件事,第一件是创建`控制组`的目录,第二件事就是把 PID 加入到`控制组`
我们顺势看一下资源配置是如何设置的,其实就是简单地对控制组下相应配置文件进行修改,并没有什么黑魔法
```go
func (s *CpuGroup) SetRtSched(path string, cgroup *configs.Cgroup) error {
if cgroup.Resources.CpuRtPeriod != 0 {
if err := fscommon.WriteFile(path, "cpu.rt_period_us", strconv.FormatUint(cgroup.Resources.CpuRtPeriod, 10)); err != nil {
return err
}
}
if cgroup.Resources.CpuRtRuntime != 0 {
if err := fscommon.WriteFile(path, "cpu.rt_runtime_us", strconv.FormatInt(cgroup.Resources.CpuRtRuntime, 10)); err != nil {
return err
}
}
return nil
}
func (s *CpuGroup) Set(path string, cgroup *configs.Cgroup) error {
if cgroup.Resources.CpuShares != 0 {
if err := fscommon.WriteFile(path, "cpu.shares", strconv.FormatUint(cgroup.Resources.CpuShares, 10)); err != nil {
return err
}
}
if cgroup.Resources.CpuPeriod != 0 {
if err := fscommon.WriteFile(path, "cpu.cfs_period_us", strconv.FormatUint(cgroup.Resources.CpuPeriod, 10)); err != nil {
return err
}
}
if cgroup.Resources.CpuQuota != 0 {
if err := fscommon.WriteFile(path, "cpu.cfs_quota_us", strconv.FormatInt(cgroup.Resources.CpuQuota, 10)); err != nil {
return err
}
}
return s.SetRtSched(path, cgroup)
}
```
## Manager 再分析
#### *`Freeze`* 暂停/恢复 `控制组` 中任务
实际是操作 freezer 子系统,修改控制组中 `freezer.state` 文件
```go
func (m *Manager) Freeze(state configs.FreezerState) error {
if m.Cgroups == nil {
return errors.New("cannot toggle freezer: cgroups not configured for container")
}
// 获取 Freeze subsystem 的控制组地址
paths := m.GetPaths()
dir := paths["freezer"]
prevState := m.Cgroups.Resources.Freezer
m.Cgroups.Resources.Freezer = state
// 获取 subsystem,修改 freezer.state 文件
freezer, err := m.getSubsystems().Get("freezer")
if err != nil {
return err
}
err = freezer.Set(dir, m.Cgroups)
if err != nil {
m.Cgroups.Resources.Freezer = prevState
return err
}
return nil
}
```
## 总结
实际上 runc 相当于使用了一个方便操作系统 Cgroups 的包,这个包并没有什么黑魔法,只是对`子系统`下的配置文件进行查询和修改
Runc 与 Cgroups