写点什么

cri-o 技术探秘 3

用户头像
xumc
关注
发布于: 2021 年 05 月 16 日
cri-o技术探秘3

pinns

在探秘 1 中我们提到,namespace 相关的是由 NewPodNamespaces 函数来完成的,我们可以在 NewPodNamespaces 实现看到,创建 namespace 使用过调用 pinns 命令来完成的。pinns 是 cri-o 内部的一个组件,使用 C 语言编写,位于 pinns 文件夹,主要逻辑是使用 unshare 系统调用来创建 namespace。目前 pinns 支持四种 namespace 的创建:


  1. ipc

  2. uts

  3. user

  4. net


默认情况下, 会在var/run/目录下创建 namespace。 比如 net, 创建的 namespace 格式为/var/run/netns/${uuid}.


我们知道sysctl用于运行时配置内核参数,这些参数位于/proc/sys 目录下。sysctl 配置与显示在/proc/sys 目录中的内核参数.可以用 sysctl 来设置或重新设置联网功能,如 IP 转发、IP 碎片去除以及源路由检查等。用户只需要编辑/etc/sysctl.conf 文件,即可手工或自动执行由 sysctl 控制的功能。在容器技术中, 新启动的 pod 同样要考虑 sysctl 的设置,这样新启动的 pod 就会使用传入的 sysctl 设置。


如果不想使用某一种 namespace 或多种 namespace,我们可以通过在 pinns 参数中将这些 namespace 参数值都设置为 host。


如果通过 pinns 创建 namespace 失败的时候, 使用 unix.Unmount(ns.Path, unix.MNT_DETACH)删除所有创建的 namespace。如果 NewPodNamespace 过程中出现错误, 则调用 Namespace.Remove 方法来清理,本质上 Namespace.Remove 也是调用 unix.Unmount 来完成 remove 操作的。


如果存在 IDMappings 参数,则将创建的 namespace path 的 UID 和 GID 设置为 root user 的 UID 和 GID。相关函数为 chownDirToIDPair。


internal/config/nsmgr/nsmgr.go:68// NewPodNamespaces creates new namespaces for a pod.// It's responsible for running pinns and creating the Namespace objects.// The caller is responsible for cleaning up the namespaces by calling Namespace.Remove().func (mgr *NamespaceManager) NewPodNamespaces(cfg *PodNamespacesConfig) ([]Namespace, error) {  if cfg == nil {    return nil, errors.New("PodNamespacesConfig cannot be nil")  }  if len(cfg.Namespaces) == 0 {    return []Namespace{}, nil  }
typeToArg := map[NSType]string{ IPCNS: "--ipc", UTSNS: "--uts", USERNS: "--user", NETNS: "--net", }
pinnedNamespace := uuid.New().String() pinnsArgs := []string{ "-d", mgr.namespacesDir, "-f", pinnedNamespace, }
if len(cfg.Sysctls) != 0 { pinnsArgs = append(pinnsArgs, "-s", getSysctlForPinns(cfg.Sysctls)) }
var rootPair idtools.IDPair if cfg.IDMappings != nil { rootPair = cfg.IDMappings.RootPair() }
for _, ns := range cfg.Namespaces { arg, ok := typeToArg[ns.Type] if !ok { return nil, errors.Errorf("Invalid namespace type: %s", ns.Type) } if ns.Host { arg += "=host" } pinnsArgs = append(pinnsArgs, arg) ns.Path = filepath.Join(mgr.namespacesDir, string(ns.Type)+"ns", pinnedNamespace) if cfg.IDMappings != nil { if err := chownDirToIDPair(ns.Path, rootPair); err != nil { return nil, err } } }
if cfg.IDMappings != nil { pinnsArgs = append(pinnsArgs, "--uid-mapping="+getMappingsForPinns(cfg.IDMappings.UIDs()), "--gid-mapping="+getMappingsForPinns(cfg.IDMappings.GIDs())) }
logrus.Debugf("calling pinns with %v", pinnsArgs) output, err := exec.Command(mgr.pinnsPath, pinnsArgs...).CombinedOutput() if err != nil { logrus.Warnf("pinns %v failed: %s (%v)", pinnsArgs, string(output), err) // cleanup the mounts for _, ns := range cfg.Namespaces { if mErr := unix.Unmount(ns.Path, unix.MNT_DETACH); mErr != nil && mErr != unix.EINVAL { logrus.Warnf("failed to unmount %s: %v", ns.Path, mErr) } }
return nil, fmt.Errorf("failed to pin namespaces %v: %s %v", cfg.Namespaces, output, err) }
returnedNamespaces := make([]Namespace, 0, len(cfg.Namespaces)) for _, ns := range cfg.Namespaces { ns, err := GetNamespace(ns.Path, ns.Type) if err != nil { for _, nsToClose := range returnedNamespaces { if err2 := nsToClose.Remove(); err2 != nil { logrus.Errorf("failed to remove namespace after failed to create: %v", err2) } } return nil, err }
returnedNamespaces = append(returnedNamespaces, ns) } return returnedNamespaces, nil}
复制代码


如果仔细阅读代码, 可以看到, pid namespace 并不是在 pinns 里面来创建的,这是为什么呢?带着这个问题,我们一步一步来阅读 pinns 的 C 语言代码。


当传入 pinns 中的参数为-U=host 的时候,pinns 会认为不创建 user namespace,这个时候回直接通过 unshare 系统调用来创建 namespace。


if (!bind_user) {    /* Use pid=0 to indicate using the current process.  */    pid = 0;
if (unshare(unshare_flags) < 0) { pexit("Failed to unshare namespaces"); }} else { /* if we create a user namespace, we need a new process. */ ...}
复制代码


否则,我们会创建一个新的进程来完成 unshare 的操作。fork 新进程完成 unshare 操作比较复杂,我们通过下面的示意图来解释:


不论是直接 unshare 还是通过新的子进程 unshare 后,还需要有两个工作需要完成。


  1. configure_sysctls。这个比较简单,直接通过 write_sysctl_to_file 函数将 sysctl 的数据写入到/proc/sys 下的对应文件内。

  2. bind 各种 namespace。通过mount(ns_path, bind_path, NULL, MS_BIND, NULL)调用来将/var/run/${type}ns/${uuid}/proc/${pid}/ns/${ns name}或者/proc/self/ns/${ns name} 绑定起来。


下面我们来做一个实验,使用下面的命令创建一个 pod 及其里面的 container。


POD_ID=$(crictl runp test/testdata/sandbox_config.json)CONTAINER_ID=$(crictl create $POD_ID test/testdata/container_redis.json test/testdata/sandbox_config.json)crictl start $CONTAINER_ID
复制代码


查找 config.json 里面的 namespace 部分配置,从中可以看到, path 就是我们通过 pinns 创建出来的 path。


cat /var/lib/containers/storage/overlay-containers/d0c271290e70c7e472cf20149083a41331fe08e8abd71ee2275175a7f983f464/userdata/config.json...    "namespaces": [      {        "type": "pid"      },      {        "type": "network",        "path": "/var/run/netns/ddc51324-914a-4b64-ae8a-3fa16972db36"      },      {        "type": "ipc",        "path": "/var/run/ipcns/ddc51324-914a-4b64-ae8a-3fa16972db36"      },      {        "type": "uts",        "path": "/var/run/utsns/ddc51324-914a-4b64-ae8a-3fa16972db36"      },      {        "type": "mount"      }    ],...
复制代码


使用 crictl exec $CONTAINER_ID ps aux在 container 中查看 pid 是 1.


    PID   USER     TIME   COMMAND    1 redis      0:01 redis-server   57 root       0:00 ps aux
复制代码


我们再次在这个 pod 下面运行另外一个 container。


CONTAINER_ID2=$(crictl create $POD_ID test/testdata/container_sleep.json test/testdata/sandbox_config.json)crictl start $CONTAINER_ID2
复制代码


使用 crictl exec $CONTAINER_ID2 ps aux在 container 中查看 pid 仍然是 1.


PID   USER     TIME   COMMAND    1 root       0:00 /bin/sleep 6000 6000    9 root       0:00 ps aux
复制代码


由此可见 pid namespace 是每个 container 都会创建一份的,并不是 pod level 的,所以在创建 pod 的时候不需要由 pinns 创建 pid namespace。实际上 pid namespace 是在创建 container 的时候由 runc 来创建的。

发布于: 2021 年 05 月 16 日阅读数: 28
用户头像

xumc

关注

golang攻城狮 2017.12.15 加入

FreeWheel 码农一枚

评论

发布
暂无评论
cri-o技术探秘3