摘要:目前內核總共實現了種隔離和消息隊列。參數表示我們要加入的的文件描述符。提供了很多種進程間通信的機制,針對的是和消息隊列。所謂傳播事件,是指由一個掛載對象的狀態變化導致的其它掛載對象的掛載與解除掛載動作的事件。
前言
理解docker,主要從namesapce,cgroups,聯合文件,運行時(runC),網絡幾個方面。接下來我們會花一些時間,分別介紹。
docker系列--namespace解讀
docker系列--cgroups解讀
docker系列--unionfs解讀
docker系列--runC解讀
docker系列--網絡模式解讀
namesapce主要是隔離作用,cgroups主要是資源限制,聯合文件主要用于鏡像分層存儲和管理,runC是運行時,遵循了oci接口,一般來說基于libcontainer。網絡主要是docker單機網絡和多主機通信模式。
namespace簡介 什么是namespaceNamespace是將內核的全局資源做封裝,使得每個Namespace都有一份獨立的資源,因此不同的進程在各自的Namespace內對同一種資源的使用不會互相干擾。實際上,Linux內核實現namespace的主要目的就是為了實現輕量級虛擬化(容器)服務。在同一個namespace下的進程可以感知彼此的變化,而對外界的進程一無所知。這樣就可以讓容器中的進程產生錯覺,仿佛自己置身于一個獨立的系統環境中,以此達到獨立和隔離的目的。
這樣的解釋可能不清楚,舉個例子,執行sethostname這個系統調用時,可以改變系統的主機名,這個主機名就是一個內核的全局資源。內核通過實現UTS Namespace,可以將不同的進程分隔在不同的UTS Namespace中,在某個Namespace修改主機名時,另一個Namespace的主機名還是保持不變。
目前Linux內核總共實現了6種Namespace:
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。
1: 通過clone()創建新進程的同時創建namespace
使用clone()來創建一個獨立namespace的進程是最常見做法,它的調用方式如下。
int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);
clone()實際上是傳統UNIX系統調用fork()的一種更通用的實現方式,它可以通過flags來控制使用多少功能。一共有二十多種CLONE_*的flag(標志位)參數用來控制clone進程的方方面面(如是否與父進程共享虛擬內存等等),下面外面逐一講解clone函數傳入的參數。
參數child_func傳入子進程運行的程序主函數。
參數child_stack傳入子進程使用的??臻g
參數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 }
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/33059.html
摘要:目前內核總共實現了種隔離和消息隊列。參數表示我們要加入的的文件描述符。提供了很多種進程間通信的機制,針對的是和消息隊列。所謂傳播事件,是指由一個掛載對象的狀態變化導致的其它掛載對象的掛載與解除掛載動作的事件。 前言 理解docker,主要從namesapce,cgroups,聯合文件,運行時(runC),網絡幾個方面。接下來我們會花一些時間,分別介紹。 docker系列--names...
摘要:目前內核總共實現了種隔離和消息隊列。參數表示我們要加入的的文件描述符。提供了很多種進程間通信的機制,針對的是和消息隊列。所謂傳播事件,是指由一個掛載對象的狀態變化導致的其它掛載對象的掛載與解除掛載動作的事件。 前言 理解docker,主要從namesapce,cgroups,聯合文件,運行時(runC),網絡幾個方面。接下來我們會花一些時間,分別介紹。 docker系列--names...
摘要:網絡主要是單機網絡和多主機通信模式。下面分別介紹一下的各個網絡模式。設計的網絡模型。是以對定義的元數據。用戶可以通過定義這樣的元數據來自定義和驅動的行為。 前言 理解docker,主要從namesapce,cgroups,聯合文件,運行時(runC),網絡幾個方面。接下來我們會花一些時間,分別介紹。 docker系列--namespace解讀 docker系列--cgroups解讀 ...
摘要:網絡主要是單機網絡和多主機通信模式。下面分別介紹一下的各個網絡模式。設計的網絡模型。是以對定義的元數據。用戶可以通過定義這樣的元數據來自定義和驅動的行為。 前言 理解docker,主要從namesapce,cgroups,聯合文件,運行時(runC),網絡幾個方面。接下來我們會花一些時間,分別介紹。 docker系列--namespace解讀 docker系列--cgroups解讀 ...
摘要:網絡主要是單機網絡和多主機通信模式。下面分別介紹一下的各個網絡模式。設計的網絡模型。是以對定義的元數據。用戶可以通過定義這樣的元數據來自定義和驅動的行為。 前言 理解docker,主要從namesapce,cgroups,聯合文件,運行時(runC),網絡幾個方面。接下來我們會花一些時間,分別介紹。 docker系列--namespace解讀 docker系列--cgroups解讀 ...
閱讀 3077·2023-04-26 00:53
閱讀 3522·2021-11-19 09:58
閱讀 1693·2021-09-29 09:35
閱讀 3279·2021-09-28 09:46
閱讀 3851·2021-09-22 15:38
閱讀 2691·2019-08-30 15:55
閱讀 3006·2019-08-23 14:10
閱讀 3822·2019-08-22 18:17