• 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就被不能继续执行了

  • 相关阅读:
    接口详解
    可空类型
    初学泛型
    结构和类
    触发器
    学习C#异常处理机制
    静飘移
    《Hashtable(散列表)》 集合
    自定义集合类
    Automation伺服程式無法産生物件
  • 原文地址:https://www.cnblogs.com/dream397/p/13998688.html
Copyright © 2020-2023  润新知