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