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
}
相關文章
相關標籤/搜索