docker系列--namespace解读

前言

理解docker,主要从namesapce,cgroups,联合文件,运行时(runC),网络几个方面。接下来咱们会花一些时间,分别介绍。node

namesapce主要是隔离做用,cgroups主要是资源限制,联合文件主要用于镜像分层存储和管理,runC是运行时,遵循了oci接口,通常来讲基于libcontainer。网络主要是docker单机网络和多主机通讯模式。linux

namespace简介

什么是namespace

Namespace是将内核的全局资源作封装,使得每一个Namespace都有一份独立的资源,所以不一样的进程在各自的Namespace内对同一种资源的使用不会互相干扰。实际上,Linux内核实现namespace的主要目的就是为了实现轻量级虚拟化(容器)服务。在同一个namespace下的进程能够感知彼此的变化,而对外界的进程一无所知。这样就可让容器中的进程产生错觉,仿佛本身置身于一个独立的系统环境中,以此达到独立和隔离的目的。git

这样的解释可能不清楚,举个例子,执行sethostname这个系统调用时,能够改变系统的主机名,这个主机名就是一个内核的全局资源。内核经过实现UTS Namespace,能够将不一样的进程分隔在不一样的UTS Namespace中,在某个Namespace修改主机名时,另外一个Namespace的主机名仍是保持不变。github

目前Linux内核总共实现了6种Namespace:docker

  • IPC:隔离System V IPC和POSIX消息队列。
  • Network:隔离网络资源。
  • Mount:隔离文件系统挂载点。每一个容器能看到不一样的文件系统层次结构。
  • PID:隔离进程ID。
  • UTS:隔离主机名和域名。
  • User:隔离用户ID和组ID。

namespae接口的使用

namespace的API包括clone()、setns()以及unshare(),还有/proc下的部分文件。为了肯定隔离的究竟是哪一种namespace,在使用这些API时,一般须要指定如下六个常数的一个或多个,经过|(位或)操做来实现。你可能已经在上面的表格中注意到,这六个参数分别是CLONE_NEWIPC、CLONE_NEWNS、CLONE_NEWNET、CLONE_NEWPID、CLONE_NEWUSER和CLONE_NEWUTS。json

1: 经过clone()建立新进程的同时建立namespace
使用clone()来建立一个独立namespace的进程是最多见作法,它的调用方式以下。bootstrap

int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);

clone()其实是传统UNIX系统调用fork()的一种更通用的实现方式,它能够经过flags来控制使用多少功能。一共有二十多种CLONE_*的flag(标志位)参数用来控制clone进程的方方面面(如是否与父进程共享虚拟内存等等),下面外面逐一讲解clone函数传入的参数。segmentfault

  • 参数child_func传入子进程运行的程序主函数。
  • 参数child_stack传入子进程使用的栈空间
  • 参数flags表示使用哪些CLONE_*标志位
  • 参数args则可用于传入用户参数

2: 经过setns()加入一个已经存在的namespace
在进程都结束的状况下,也能够经过挂载的形式把namespace保留下来,保留namespace的目的天然是为之后有进程加入作准备。经过setns()系统调用,你的进程从原先的namespace加入咱们准备好的新namespace,使用方法以下。网络

int setns(int fd, int nstype);
  • 参数fd表示咱们要加入的namespace的文件描述符。上文已经提到,它是一个指向/proc/[pid]/ns目录的文件描述符,能够经过直接打开该目录下的连接或者打开一个挂载了该目录下连接的文件获得。
  • 参数nstype让调用者能够去检查fd指向的namespace类型是否符合咱们实际的要求。若是填0表示不检查。

3: 经过unshare()在原先进程上进行namespace隔离
后要提的系统调用是unshare(),它跟clone()很像,不一样的是,unshare()运行在原先的进程上,不须要启动一个新进程,使用方法以下。多线程

int unshare(int flags);

调用unshare()的主要做用就是不启动一个新进程就能够起到隔离的效果,至关于跳出原先的namespace进行操做。这样,你就能够在原进程进行一些须要隔离的操做。Linux中自带的unshare命令,就是经过unshare()系统调用实现的。

各个namespace介绍

  • UTS Namespace

UTS Namespace用于对主机名和域名进行隔离,也就是uname系统调用使用的结构体struct utsname里的nodename和domainname这两个字段,UTS这个名字也是由此而来的。
那么,为何要使用UTS Namespace作隔离?这是由于主机名能够用来代替IP地址,所以,也就可使用主机名在网络上访问某台机器了,若是不作隔离,这个机制在容器里就会出问题。

  • IPC Namespace

IPC是Inter-Process Communication的简写,也就是进程间通讯。Linux提供了不少种进程间通讯的机制,IPC Namespace针对的是SystemV IPC和Posix消息队列。这些IPC机制都会用到标识符,例如用标识符来区别不一样的消息队列,而后两个进程经过标识符找到对应的消息队列进行通讯等。
IPC Namespace能作到的事情是,使相同的标识符在两个Namespace中表明不一样的消息队列,这样也就使得两个Namespace中的进程不能经过IPC进程通讯了。

  • PID Namespace

PID Namespace用于隔离进程PID号,这样一来,不一样的Namespace里的进程PID号就能够是同样的了。

  • Network Namespace

这个Namespace会对网络相关的系统资源进行隔离,每一个Network Namespace都有本身的网络设备、IP地址、路由表、/proc/net目录、端口号等。网络隔离的必要性是很明显的,举一个例子,在没有隔离的状况下,若是两个不一样的容器都想运行同一个Web应用,而这个应用又须要使用80端口,那就会有冲突了。

  • Mount namespace

Mount namespace经过隔离文件系统挂载点对隔离文件系统提供支持,它是历史上第一个Linux namespace,因此它的标识位比较特殊,就是CLONE_NEWNS。隔离后,不一样mount namespace中的文件结构发生变化也互不影响。你能够经过/proc/[pid]/mounts查看到全部挂载在当前namespace中的文件系统,还能够经过/proc/[pid]/mountstats看到mount namespace中文件设备的统计信息,包括挂载文件的名字、文件系统类型、挂载位置等等。

进程在建立mount namespace时,会把当前的文件结构复制给新的namespace。新namespace中的全部mount操做都只影响自身的文件系统,而对外界不会产生任何影响。这样作很是严格地实现了隔离,可是某些状况可能并不适用。好比父节点namespace中的进程挂载了一张CD-ROM,这时子节点namespace拷贝的目录结构就没法自动挂载上这张CD-ROM,由于这种操做会影响到父节点的文件系统。

ps
在mount这块,须要特别注意,挂载的传播性。在实际应用中,很重要。2006 年引入的挂载传播(mount propagation)解决了这个问题,挂载传播定义了挂载对象(mount object)之间的关系,系统用这些关系决定任何挂载对象中的挂载事件如何传播到其余挂载对象。所谓传播事件,是指由一个挂载对象的状态变化致使的其它挂载对象的挂载与解除挂载动做的事件。

  • User Namespace

User Namespace用来隔离用户和组ID,也就是说一个进程在Namespace里的用户和组ID与它在host里的ID能够不同,这样说可能读者还不理解有什么实际的用处。User Namespace最有用的地方在于,host的普通用户进程在容器里能够是0号用户,也就是root用户。这样,进程在容器内能够作各类特权操做,可是它的特权被限定在容器内,离开了这个容器它就只有普通用户的权限了。

代码解读

  • 首先runc中有一个nsenter文件夹,主要是go经过cgo,实现了nsexec等方法。

在Go运行时启动以前,nsenter包注册了一个特殊init构造函数。这让咱们有可能在现有名称空间“setns”,并避免了Go运行时在多线程场景下可能出现的问题。

具体是在runc的main.go中引入:

package main

import (
    "os"
    "runtime"

    "github.com/opencontainers/runc/libcontainer"
    _ "github.com/opencontainers/runc/libcontainer/nsenter"
    "github.com/urfave/cli"
)

func init() {
    if len(os.Args) > 1 && os.Args[1] == "init" {
        runtime.GOMAXPROCS(1)
        runtime.LockOSThread()
    }
}

var initCommand = cli.Command{
    Name:  "init",
    Usage: `initialize the namespaces and launch the process (do not call it outside of runc)`,
    Action: func(context *cli.Context) error {
        factory, _ := libcontainer.New("")
        if err := factory.StartInitialization(); err != nil {
            // as the error is sent back to the parent there is no need to log
            // or write it to stderr because the parent process will handle this
            os.Exit(1)
        }
        panic("libcontainer: container init failed to exec")
    },
}
  • 下面重点讲一下在linux container中namespace的实现。

runc/libcontainer/configs/config.go中定义了container对应的Namespaces。另外对于User Namespaces,还定义了UidMappings和GidMappings for user map。

// Config defines configuration options for executing a process inside a contained environment.
type Config struct {
    ...
 
    // Namespaces specifies the container's namespaces that it should setup when cloning the init process
    // If a namespace is not provided that namespace is shared from the container's parent process
    Namespaces Namespaces `json:"namespaces"`
 
    // UidMappings is an array of User ID mappings for User Namespaces
    UidMappings []IDMap `json:"uid_mappings"`
 
    // GidMappings is an array of Group ID mappings for User Namespaces
    GidMappings []IDMap `json:"gid_mappings"`
 
    ...
}

而Namespaces定义以下:

package configs

import (
    "fmt"
    "os"
    "sync"
)

const (
    NEWNET  NamespaceType = "NEWNET"
    NEWPID  NamespaceType = "NEWPID"
    NEWNS   NamespaceType = "NEWNS"
    NEWUTS  NamespaceType = "NEWUTS"
    NEWIPC  NamespaceType = "NEWIPC"
    NEWUSER NamespaceType = "NEWUSER"
)

var (
    nsLock              sync.Mutex
    supportedNamespaces = make(map[NamespaceType]bool)
)

// NsName converts the namespace type to its filename
func NsName(ns NamespaceType) string {
    switch ns {
    case NEWNET:
        return "net"
    case NEWNS:
        return "mnt"
    case NEWPID:
        return "pid"
    case NEWIPC:
        return "ipc"
    case NEWUSER:
        return "user"
    case NEWUTS:
        return "uts"
    }
    return ""
}

// IsNamespaceSupported returns whether a namespace is available or
// not
func IsNamespaceSupported(ns NamespaceType) bool {
    nsLock.Lock()
    defer nsLock.Unlock()
    supported, ok := supportedNamespaces[ns]
    if ok {
        return supported
    }
    nsFile := NsName(ns)
    // if the namespace type is unknown, just return false
    if nsFile == "" {
        return false
    }
    _, err := os.Stat(fmt.Sprintf("/proc/self/ns/%s", nsFile))
    // a namespace is supported if it exists and we have permissions to read it
    supported = err == nil
    supportedNamespaces[ns] = supported
    return supported
}

func NamespaceTypes() []NamespaceType {
    return []NamespaceType{
        NEWUSER, // Keep user NS always first, don't move it.
        NEWIPC,
        NEWUTS,
        NEWNET,
        NEWPID,
        NEWNS,
    }
}

// Namespace defines configuration for each namespace.  It specifies an
// alternate path that is able to be joined via setns.
type Namespace struct {
    Type NamespaceType `json:"type"`
    Path string        `json:"path"`
}

func (n *Namespace) GetPath(pid int) string {
    return fmt.Sprintf("/proc/%d/ns/%s", pid, NsName(n.Type))
}

func (n *Namespaces) Remove(t NamespaceType) bool {
    i := n.index(t)
    if i == -1 {
        return false
    }
    *n = append((*n)[:i], (*n)[i+1:]...)
    return true
}

func (n *Namespaces) Add(t NamespaceType, path string) {
    i := n.index(t)
    if i == -1 {
        *n = append(*n, Namespace{Type: t, Path: path})
        return
    }
    (*n)[i].Path = path
}

func (n *Namespaces) index(t NamespaceType) int {
    for i, ns := range *n {
        if ns.Type == t {
            return i
        }
    }
    return -1
}

func (n *Namespaces) Contains(t NamespaceType) bool {
    return n.index(t) != -1
}

func (n *Namespaces) PathOf(t NamespaceType) string {
    i := n.index(t)
    if i == -1 {
        return ""
    }
    return (*n)[i].Path
}

runC支持的namespce type包括($nsName) "net"、"mnt"、"pid"、"ipc"、"user"、"uts":

const (
       NEWNET  NamespaceType = "NEWNET"
       NEWPID  NamespaceType = "NEWPID"
       NEWNS   NamespaceType = "NEWNS"
       NEWUTS  NamespaceType = "NEWUTS"
       NEWIPC  NamespaceType = "NEWIPC"
       NEWUSER NamespaceType = "NEWUSER"
)

除了验证 Namespce Type是否在以上常量中,还要去验证 /proc/self/ns/$nsName是否存在而且能够read,都经过时,才认为该Namespace是在当前系统中是被支持的。

// IsNamespaceSupported returns whether a namespace is available or
// not
func IsNamespaceSupported(ns NamespaceType) bool {
       ...
       supported, ok := supportedNamespaces[ns]
       if ok {
              return supported
       }
       ...
       // 除了验证 Namespce Type是都在指定列表中,还要去验证 /proc/self/ns/$nsName是否存在而且能够read
       _, err := os.Stat(fmt.Sprintf("/proc/self/ns/%s", nsFile))
       supported = err == nil
       ...
       return supported
}

在runc/libcontainer/configs/namespaces_syscall.go中,定义了linux clone时这些namespace对应的clone flags。

var namespaceInfo = map[NamespaceType]int{
       NEWNET:  syscall.CLONE_NEWNET,
       NEWNS:   syscall.CLONE_NEWNS,
       NEWUSER: syscall.CLONE_NEWUSER,
       NEWIPC:  syscall.CLONE_NEWIPC,
       NEWUTS:  syscall.CLONE_NEWUTS,
       NEWPID:  syscall.CLONE_NEWPID,
}
 
// CloneFlags parses the container's Namespaces options to set the correct
// flags on clone, unshare. This function returns flags only for new namespaces.
func (n *Namespaces) CloneFlags() uintptr {
       var flag int
       for _, v := range *n {
              if v.Path != "" {
                     continue
              }
              flag |= namespaceInfo[v.Type]
       }
       return uintptr(flag)
}
  • 在容器建立初始化的过程当中,主要执行如下方法:
func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, childPipe *os.File) (*initProcess, error) {
    cmd.Env = append(cmd.Env, "_LIBCONTAINER_INITTYPE="+string(initStandard))
    nsMaps := make(map[configs.NamespaceType]string)
    for _, ns := range c.config.Namespaces {
        if ns.Path != "" {
            nsMaps[ns.Type] = ns.Path
        }
    }
    _, sharePidns := nsMaps[configs.NEWPID]
    data, err := c.bootstrapData(c.config.Namespaces.CloneFlags(), nsMaps)
    if err != nil {
        return nil, err
    }
    return &initProcess{
        cmd:             cmd,
        childPipe:       childPipe,
        parentPipe:      parentPipe,
        manager:         c.cgroupManager,
        intelRdtManager: c.intelRdtManager,
        config:          c.newInitConfig(p),
        container:       c,
        process:         p,
        bootstrapData:   data,
        sharePidns:      sharePidns,
    }, nil
}
相关文章
相关标签/搜索