runc cgroup CreateLibcontainerConfig & linuxContainer

// needsSetupDev returns true if /dev needs to be set up.
func needsSetupDev(config *configs.Config) bool {
        for _, m := range config.Mounts {
                if m.Device == "bind" && libcontainerUtils.CleanPath(m.Destination) == "/dev" {
                        return false
                }
        }
        return true
}

// prepareRootfs sets up the devices, mount points, and filesystems for use
// inside a new mount namespace. It doesn't set anything as ro. You must call
// finalizeRootfs after this function to finish setting up the rootfs.
func prepareRootfs(pipe io.ReadWriter, iConfig *initConfig) (err error) {
        config := iConfig.Config
        if err := prepareRoot(config); err != nil {
                return newSystemErrorWithCause(err, "preparing rootfs")
        }

        hasCgroupns := config.Namespaces.Contains(configs.NEWCGROUP)
        setupDev := needsSetupDev(config)
        for _, m := range config.Mounts {
                for _, precmd := range m.PremountCmds {
                        if err := mountCmd(precmd); err != nil {
                                return newSystemErrorWithCause(err, "running premount command")
                        }
                }
                if err := mountToRootfs(m, config.Rootfs, config.MountLabel, hasCgroupns); err != nil {
                        return newSystemErrorWithCausef(err, "mounting %q to rootfs at %q", m.Source, m.Destination)
                }

                for _, postcmd := range m.PostmountCmds {
                        if err := mountCmd(postcmd); err != nil {
                                return newSystemErrorWithCause(err, "running postmount command")
                        }
                }

kata agent

func (a *agentGRPC) CreateContainer(ctx context.Context, req *pb.CreateContainerRequest) (resp *gpb.Empty, err error) {
        if err := a.createContainerChecks(req); err != nil {
                return emptyResp, err
        }

        // Convert the OCI specification into a libcontainer configuration.
        config, err := specconv.CreateLibcontainerConfig(&specconv.CreateOpts{
                CgroupName:   req.ContainerId,
                NoNewKeyring: true,
                Spec:         ociSpec,
                NoPivotRoot:  a.sandbox.noPivotRoot,
        })
        if err != nil {
                return emptyResp, err
        }

        // apply rlimits
        config.Rlimits = posixRlimitsToRlimits(ociSpec.Process.Rlimits)

        // Update libcontainer configuration for specific cases not handled
        // by the specconv converter.
        if err = a.updateContainerConfig(ociSpec, config, ctr); err != nil {
                return emptyResp, err
        }

        return a.finishCreateContainer(ctr, req, config)
}

首先调用container, err := createContainer(context, id, spec)创建容器, 之后填充runner结构r。

func createContainer(context *cli.Context, id string, spec *specs.Spec) (libcontainer.Container, error) {
    rootless, err := isRootless(context)
    if err != nil {
        return nil, err
    }
    config, err := specconv.CreateLibcontainerConfig(&specconv.CreateOpts{
        CgroupName:       id,
        UseSystemdCgroup: context.GlobalBool("systemd-cgroup"),
        NoPivotRoot:      context.Bool("no-pivot"),
        NoNewKeyring:     context.Bool("no-new-keyring"),
        Spec:             spec,
        Rootless:         rootless,
    })
    if err != nil {
        return nil, err
    }

    factory, err := loadFactory(context)
    if err != nil {
        return nil, err
    }
    return factory.Create(id, config)
}

注意factory, err := loadFactory(context)和factory.Create(id, config),这两个就是我们上面提到的factory.go。由工厂来根据配置config创建具体容器。

package main

import (
  "fmt"
  "io/ioutil"
  "os"
  "os/exec"
  "path"
  "strconv"
  "syscall"
)

// 挂载了memory subsystem的hierarchy的根目录位置
const cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory"

func main() {
  if os.Args[0] == "/proc/self/exe" {
    // 容器进程
    fmt.Printf("current pid %d
", syscall.Getpid())
    cmd := exec.Command("sh", "-c", `stress --vm-bytes 200m --vm-keep -m 1`)
    cmd.SysProcAttr = &syscall.SysProcAttr{}
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    if err := cmd.Run(); err != nil {
      fmt.Println(err)
      os.Exit(1)
    }
  }

  cmd := exec.Command("/proc/self/exe")
  cmd.SysProcAttr = &syscall.SysProcAttr{
    Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
  }
  cmd.Stdin = os.Stdin
  cmd.Stdout = os.Stdout
  cmd.Stderr = os.Stderr

  if err := cmd.Run(); err != nil {
    fmt.Println("Error", err)
    os.Exit(1)
  } else {
    // 得到fork出来进程映射在外部命名空间的pid
    fmt.Printf("%v
", cmd.Process.Pid)
    // 在系统默认创建挂载了 memory subsystem 的hierarchy上创建cgroup
    os.Mkdir(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit"), 0755)
    // 将容器进程加入到这个cgroup中
    ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "tasks"), []byte(strconv.Itoa(cmd.Process.Pid)), 0644)
    // 限制cgroup进程使用
    ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "memory.limit_in_bytes"), []byte("100m"), 0644)
    cmd.Process.Wait()
  }
}
func (m *Manager) Apply(pid int) (err error) {
    if m.Cgroups == nil {                       // 全局 cgroup 配置是否存在检测
        return nil
    }
  //...
    var c = m.Cgroups
    d, err := getCgroupData(m.Cgroups, pid)     // +获取与构建 cgroupData 对象
  //...
    m.Paths = make(map[string]string)
  // 如果全局配置存在 cgroup paths 配置,
    if c.Paths != nil {                        
        for name, path := range c.Paths {
            _, err := d.path(name)                 // 查找子系统的 cgroup path 是否存在
            if err != nil {
                if cgroups.IsNotFound(err) {
                    continue
                }
                return err
            }
            m.Paths[name] = path
        }
        return cgroups.EnterPid(m.Paths, pid)    // 将 pid 写入子系统的 cgroup.procs 文件
    }

  // 遍历所有 cgroup 子系统,将配置应用 cgroup 资源限制
    for _, sys := range subsystems {
        p, err := d.path(sys.Name())             // 查找子系统的 cgroup path
        if err != nil {
          //...
            return err
        }
        m.Paths[sys.Name()] = p                 
    if err := sys.Apply(d); err != nil {     // 各子系统 apply() 方法调用
    //...
    }
    return nil
}

Namespaces

Linux内核实现了namespace,进而实现了轻量级虚拟化服务,在同一个namespace下的进程可以感知彼此的变化,但是不能看到其他的进程,从而达到了环境隔离的目的。namespace有6项隔离,分别是UTS(Unix Time-sharing System, 主机和域名), IPC(InterProcess Comms, 信号量、消息队列和共享内存), PID(Process IDs, 进程编号), Network(网络设备,网络栈,端口等), Mount(挂载点[文件系统]), User(用户和用户组)。

C语言中可以通过clone()指定flags参数,在创建进程的同时创建namespace。Linux内核版本3.8之后的用户可以通过ls -l /proc/?/ns查看当前进程指向的namespace编号。(?表示当前运行的进程ID号)

UTS

先创建一个UTS隔离的新进程,这里使用了 Sirupsen的logrus库,可以通过go get github.com/sirupsen/logrus获取

package main
import (
        "os"
        "os/exec"
        "syscall"
        "github.com/sirupsen/logrus"
)
func main() {
        if len(os.Args) < 2 {
                logrus.Errorf("missing commands")
                return
        }
        switch os.Args[1] {
        case "run":
                run()
        default:
                logrus.Errorf("wrong command")
                return
        }
}
func run() {
        logrus.Infof("Running %v", os.Args[2:])
        cmd := exec.Command(os.Args[2], os.Args[3:]...)
        cmd.Stdin = os.Stdin
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr
        cmd.SysProcAttr = &syscall.SysProcAttr{
                Cloneflags: syscall.CLONE_NEWUTS,
        }
        check(cmd.Run())
}
func check(err error) {
        if err != nil {
                logrus.Errorln(err)
        }
}

在 Linux 环境下执行

$ go run main.go run sh
INFO[0000] Running [sh]
root@ubuntu-14:~/shared#

此时在一个新的进程中执行了sh命令,由于指定了flag syscall.CLONE_NEWUTS, 此时已经与之前的进程不在同一个UTS namespace中了。在新sh和原sh中分别执行ls -l /proc/?/ns进行验证

原sh:

$ ls -l /proc/?/ns
total 0
lrwxrwxrwx 1 root root 0 Sep  2 16:26 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Sep  2 16:26 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Sep  2 16:26 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Sep  2 16:26 net -> net:[4026531957]
lrwxrwxrwx 1 root root 0 Sep  2 16:26 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Sep  2 16:26 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Sep  2 16:26 uts -> uts:[4026531838]

新sh:

root@ubuntu-14:~/shared# ls -l /proc/?/ns
total 0
lrwxrwxrwx 1 root root 0 Sep  2 16:26 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Sep  2 16:26 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Sep  2 16:26 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Sep  2 16:26 net -> net:[4026531957]
lrwxrwxrwx 1 root root 0 Sep  2 16:26 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Sep  2 16:26 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Sep  2 16:26 uts -> uts:[4026532197]

可以看到这里两个只有uts所指向的ID不同,因为之前只指定UTS的隔离。在新sh中执行hostname newhost更改当前的hostname, 可以看到这里的hostname已经被改成了newhost, 但是原来的sh中依然是ubuntu-14, 同样证明UTS隔离成功了。

为了在启动sh的同时就能够将其hostname修改为新的hostname,下面将run()函数拆分成run()child()。将这个过程分成创建新的namespace和修改hostname两步,这样就可以保证修改namespace的时候已经在新的namespace中了,避免修改主机的hostname。这里的/proc/self/exe就是当前正在执行的命令,在这里就是go run main.go

func run() {
        logrus.Info("Setting up...")
        cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
        cmd.Stdin = os.Stdin
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr
        cmd.SysProcAttr = &syscall.SysProcAttr{
                Cloneflags: syscall.CLONE_NEWUTS,
        }
        check(cmd.Run())
}
func child() {
        logrus.Infof("Running %v", os.Args[2:])
        cmd := exec.Command(os.Args[2], os.Args[3:]...)
        cmd.Stdin = os.Stdin
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr
        check(syscall.Sethostname([]byte("newhost")))
        check(cmd.Run())
}

然后对main()函数进行相应的修改

func main() {
        if len(os.Args) < 2 {
                logrus.Errorf("missing commands")
                return
        }
        switch os.Args[1] {
        case "run":
                run()
        case "child":
                child()
        default:
                logrus.Errorf("wrong command")
                return
        }
}

再次执行命令可以看到进入时hostname已经是newhost了

$ go run main.go run sh
INFO[0000] Setting up...
INFO[0000] Running [sh]
root@newhost:~/shared#

PID

为了进行PID的隔离将run()函数中cmd.SysProcAttr修改为

Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID,

此时再次运行,并执行ps查看当前进程,发现和主机上一样,并没有被隔离。这是因为ps总是查看/proc,如果要进行隔离,则需要修改根目录root。

下面获取一个unix文件系统,可以选择docker的busybox镜像,并将其导出。

docker pull busybox
docker run -d busybox top -b

此时获得刚刚的容器的containerID,然后执行

docekr export -o busybox.tar <刚才容器的ID>

即可在当前目录下得到一个busybox的压缩包,用

mkdir busybox
tar -xf busybox.tar -C busybox/

解压即可得到我们需要的文件系统

查看一下busybox目录

$ ls busybox
bin  dev  etc  home  proc  root  sys  tmp  usr  var

接下来通过syscall.Chroot()将root修改为busybox的目录,然后在进入shell之后通过os.Chdir()切换到新的根目录下,然后通过syscall.Mount("proc", "proc", "proc", 0, "")挂载虚拟文件系统proc(proc是一个伪文件系统,只存在于内存中,以文件系统的方式为访问系统内核数据的操作提供接口,/proc目录下的文件记录了正在运行的进程的相关信息), 运行结束之后还要卸载刚才挂载的proc

修改之后的代码

func child() {
        ...
        check(syscall.Sethostname([]byte("newhost")))
        check(syscall.Chroot("/root/busybox"))
        check(os.Chdir("/"))
        // func Mount(source string, target string, fstype string, flags uintptr, data string) (err error)
        // 前三个参数分别是文件系统的名字,挂载到的路径,文件系统的类型
        check(syscall.Mount("proc", "proc", "proc", 0, ""))
        check(cmd.Run())
        check(syscall.Unmount("proc", 0))
}

修改之后再次执行,并使用ps查看当前namespace下进程的情况,得到了期望的状态

go run test.go run sh
INFO[0000] Setting up...
INFO[0000] Running [sh]
/ # ps
PID   USER     TIME   COMMAND
    1 root       0:00 /proc/self/exe child sh
    4 root       0:00 sh
    5 root       0:00 ps
/ #

child()中再挂载一个tmpfs,将代码改为

...
check(syscall.Mount("proc", "proc", "proc", 0, ""))
check(syscall.Mount("tempdir", "temp", "tmpfs", 0, ""))
check(cmd.Run())
check(syscall.Unmount("proc", 0))
check(syscall.Unmount("temp", 0))

执行go run main.go run sh后使用mount查看已挂载的文件系统

/ # mount
proc on /proc type proc (rw,relatime)
tempdir on /temp type tmpfs (rw,relatime)

继续执行touch /temp/HELLO在temp目录下创建一个文件。然后在主机中执行ls /root/busybox/temp可以看到刚刚创建的文件。这是因为现在还没有添加挂载点的隔离。

Cloneflags更新为Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,再次重复上面的步骤,主机中将不能再看到容器内创建的文件。这里mount point的隔离所使用的flag是CLONE_NEWNS,因为它是Linux实现的第一个namespace, 人们也没有意识到将来会有更多的namespace。

此时在主机上再调用mount也不能看到容器中的挂载情况,但是可以通过/proc/<pid>/mounts这个文件查看。

在容器中执行sleep 1000创建一个耗时1000秒的进程。然后在主机上通过pidof sleep获取这个进程的pid,接下来查看这个进程的挂载情况。

$ pidof sleep
4286
$ cat /proc/4286/mounts
proc /proc proc rw,relatime 0 0
tempdir /temp tmpfs rw,relatime 0 0

/proc/<pid>/下的文件还记录了这个进程的其他信息,比如/proc/<pid>/environ记录了它的环境变量,可以通过cat /proc/<pid>/environ | tr ' ' ''查看,tr ' ' ''去掉字符间多余的空格。

Cgroups

cgroups可以用于限制namespace隔离起来的资源,为资源设置权重,计算使用量,操控任务启停

Cgroups组件

  • cgroup: cgroup是对进程分组管理的一种机制,一个cgroup包含一组进程,并可以在这个cgroup上增加Subsystem的配置
  • Subsystem: 资源控制的模块,包括
    • blkio: 块设备io控制
    • cpu:CPU调度策略
    • cpuacct: 进程的CPU占用
    • cpuset: 进程可使用的CPU和内存
    • devices: 控制进程对内存的访问
    • freezer: 挂起和恢复进程
    • memory: 控制进程的内存占用
    • net_cls: 将网络包分类,使traffic controller可以区分出网络包来自哪个cgroup并做限流和监控
    • net_prio: 设置进程产生的网络流量的优先级
    • ns:使cgroup中的进程在新的namespace中fork新进程时创建出一个新的cgroup(包含新的namespace中的进程)
  • hierarchy: 将一组cgroup变成树状结构,便于Cgroups继承。

资源限制

可以通过mount | grep cgroup查看已挂载的subsystem。cgroup相关的文件在/sys/fs/cgroup下,如果使用了docker的话在这个目录下还会有一个docker目录,其中是docker的cgroup的相关文件

定义一个新的函数cg(), 限制容器的最大进程数

func cg() {
        cgPath := "/sys/fs/cgroup/"
        pidsPath := filepath.Join(cgPath, "pids")
        // 在/sys/fs/cgroup/pids下创建container目录
        os.Mkdir(filepath.Join(pidsPath, "container"), 0755)
        // 设置最大进程数目为20
        check(ioutil.WriteFile(filepath.Join(pidsPath, "container/pids.max"), []byte("20"), 0700))
        // 将notify_on_release值设为1,当cgroup不再包含任何任务的时候将执行release_agent的内容
        check(ioutil.WriteFile(filepath.Join(pidsPath, "container/notify_on_release"), []byte("1"), 0700))
        // 加入当前正在执行的进程
        check(ioutil.WriteFile(filepath.Join(pidsPath, "container/pids.procs"), []byte(strconv.Itoa(os.Getpid())), 0700))
}

child()函数中调用cg()进行资源限制

func child() {
        ...
        cmd := exec.Command(os.Args[2], os.Args[3:]...)
        cg()
        cmd.Stdin = os.Stdin
        ...
}

运行go run main.go run sh后在主机中的/sys/fs/cgroup/pids/container下可以看到刚刚进行的限制的内容。

编写一个脚本进行测试。这里将创建100个执行sleep的进程

d() { sleep 1000; }
for i in $(seq 1 100)
do
    echo "sleep $i
"
    d&
done

下面在容器中执行这个脚本test.sh

/ # sh test.sh
sleep 1

sleep 2

sleep 3

sleep 4

sleep 5

sleep 6

sleep 7

sleep 8

sleep 9

sleep 10

sleep 11

sleep 12

sleep 13

sleep 14

sleep 15

test.sh: line 7: can't fork
/ # test.shtest.shtest.shtest.shtest.shtest.shtest.shtest.shtest.sh: : : : : : line line line line : line : line 7777line 7line 7: : : : 7: : 7: can't forkline : can't forkcan't fork: can't forkcan't forkcan't forkcan't fork
7can't fork
:
can't fork
test.sh: line 7: can't fork
test.sh: line 7: can't fork
test.sh: line 7: can't fork
test.sh: line 7: can't fork
test.sh: line 7: can't fork

可以看到在执行过程中只调用了15次sleep就被不能继续执行了

原文地址:https://www.cnblogs.com/dream397/p/13998688.html