总结:内核目前完全实现了隔离队列和消息队列。 该参数表示要添加的文件描述符。 为消息队列提供多种进程间通信机制。 所谓的传播事件是指一个已安装对象中的状态更改导致其他已安装对象中的安装和卸载行为的事件。
简介
主要从命名空间、cgroup、协作文件、运行时(runC)和网络方面了解docker。 接下来,我将花时间分别介绍一下他们。
Docker系列--命名空间解读
docker系列--cgroups解读
Docker系列--] unionfs解读
docker系列--runC解读
docker系列--网络模式解读
namesapce主要用于隔离,cgroups主要用于资源限制,联合文件主要用于镜像的分层存储和管理。 runC 是一个遵循 oci 接口的运行时,通常基于 libcontainer。 组网主要是docker独立组网和多主机通信方式。
命名空间概述什么是命名空间?
命名空间封装了内核的全局资源,使得每个命名空间都有独立的资源,因此不同的进程使用相同的资源。 。 各自命名空间内的资源不会互相干扰。 事实上,Linux内核实现命名空间的主要目的是实现轻量级虚拟化(容器)服务。 同一命名空间内的进程可以感知彼此的变化,但对外部进程一无所知。 这给了容器内的进程一种独立系统环境的错觉,提供了独立性和隔离性。
这个解释可能会令人困惑。 例如,您可以通过发出 sethostname 系统调用来更改系统的主机名。 该主机名供内部使用全球核心资源。 通过实现UTS命名空间,内核可以将不同的进程分离到不同的UTS命名空间中。 如果一个命名空间更改其主机名,则另一命名空间的主机名不会更改。
目前Linux内核总共实现了6个命名空间。
IPC:分离System V IPC和POSIX消息队列。
网络:独立的网络资源。
挂载:隔离文件系统挂载点。 每个容器显示不同的文件系统层次结构。
PID:隔离进程ID。
UTS:分隔主机名和域名。
用户:单独的用户 ID 和组 ID。
使用命名空间接口
命名空间API包括clone()、setns()和unshare()以及/proc下的几个文件。 使用这些 API 时,您通常需要指定以下六个常量中的一个或多个来确定哪些命名空间是隔离的: (按位或)运算。 您可能已经注意到上表中的六个参数是 CLONE_NEWIPC、CLONE_NEWNS、CLONE_NEWNET、CLONE_NEWPID、CLONE_NEWUSER 和 CLONE_NEWUTS。
1:使用clone()创建新进程,同时创建命名空间
使用clone()创建具有独立命名空间的进程是最常用的方法及其调用方法如下。
int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);
clone()实际上是传统UNIX中的一个更一般的执行系统调用fork()。 您可以通过标志控制使用的函数数量。 CLONE_* fla 有超过 20 种类型g(标志位)参数用于控制克隆进程的各个方面,例如是否与父进程共享虚拟内存。 下面对传递给clone函数的参数进行一一解释。
参数child_func传递给子进程执行的程序的main函数。
参数child_stack传递给子进程使用的堆栈区域。
参数flags指示使用了哪些CLONE_*标志位。
参数args 可用于传递用户参数。
2:通过setns()添加现有命名空间
您还可以通过在进程退出时挂载命名空间来保留命名空间。 保留命名空间的目的显然是为了将来使用。 准备好参与这个过程。 通过 setns() 系统调用,进程加入从其原始命名空间准备的新命名空间。 使用方法如下。
int setns(int fd, int nstype);
参数fd代表要添加的命名空间的文件描述符。 前面提到,这是一个指向/proc/[pid]/ns目录的文件描述符,可以通过直接打开该目录下的链接或者打开挂载该目录下链接的文件来获取。
参数nstype允许调用者检查fd指向的命名空间类型是否满足其实际要求。 如果输入0,则不进行检查。
3:用unshare()隔离原进程中的命名空间
下面描述的系统调用是unshare(),它与clone()非常相似。 不同之处在于执行了 unshare()。 无需在原有流程的基础上启动新流程。 使用方法如下。
int unshare(int flags);
调用unshare()的主要作用是达到隔离效果,无需启动新进程,相当于跳出命名空间。 操作。 。 这样就可以执行一些原来进程中需要隔离的操作。 在 Linux 上unshare命令是通过unshare()系统调用实现的。
各个命名空间概述
UTS命名空间
UTS命名空间用于分隔主机名和域名。 这是uname 使用的结构utsname。 系统调用。 UTS 名称来自两个字段:节点名称和域名。
那么为什么要使用 UTS 命名空间进行隔离呢?这是因为您可以使用主机名而不是 IP 地址。 因此,您可以使用主机名访问网络上的特定计算机。 如果不做好隔离,这种机制就会导致容器内部出现问题。
IPC 命名空间
IPC 代表-进程间通信。 Linux 提供了许多进程间通信的机制。 IPC 命名空间以 SystemV IPC 和 Posix 消息队列为目标。 所有这些 IPC 机制都使用标识符。 例如,使用标识符来区分不同的消息队列,两个进程使用标识符来查找对应的消息队列进行通信等。
IPC命名空间的作用是允许相同的标识符代表两个不同的消息队列。 命名空间。 这可以防止两个命名空间中的进程通过 IPC 进程进行通信。
PID命名空间
PID命名空间用于分隔进程PID号,允许不同命名空间下的进程PID号相同。
网络命名空间
该命名空间分隔与网络相关的系统资源。 每个网络命名空间都有自己的网络设备、IP 地址、路由表和 /proc/net 目录。 、端口号等 网络分离的必要性是显而易见的。 例如,如果您想在两个不同的容器中运行同一个容器而不分开,b 如果您的应用程序要求此应用程序使用端口 80,则会发生冲突。
挂载命名空间
挂载命名空间通过隔离文件系统挂载点提供对隔离文件系统的支持。 由于这是第一个 Linux 命名空间,因此它的标识位相对特殊。 CLONE_NEWNS。 分离后,不同挂载命名空间的文件结构变化不会互相影响。 可以通过/proc/[pid]/mounts查看当前命名空间下挂载的所有文件系统。 您还可以通过 /proc/[pid]/mountstats 查看包含已挂载文件的挂载命名空间中的文件设备的统计信息。 名称、文件系统类型、挂载位置等
当进程创建挂载命名空间时,当前文件结构将被复制到新的命名空间。 新命名空间中的所有挂载操作仅影响命名空间自己的文件系统,而不影响外界。 这提供了非常严格的分离,但在某些情况下可能并非如此。 例如,父节点命名空间中的进程挂载 CD-ROM。 目前该操作影响父节点,因此子节点namespace复制的目录结构无法自动挂载CD-ROM。 文件系统。
附加说明:
对于坐骑,应特别注意坐骑传播。 在实际应用中这一点非常重要。 2006 年推出的安装传播解决了这个问题。 安装传播定义了安装对象之间的关系。 系统使用这些关系来确定已安装对象的安装事件如何传播到其他已安装对象。 所谓的传播事件是指一个已安装对象中的状态更改导致其他已安装对象中的安装和卸载行为的事件。
用户命名空间
用户命名空间用于分隔用户ID和组ID。命名空间中进程的用户 ID 和组 ID 可以与主机中的进程 ID 不同。 读者可能不明白这个的实际用法。 用户命名空间最有用的一点是,它们允许主机上的普通用户进程成为容器中的用户 0 或 root 用户。 这样,进程可以在容器内执行各种特权操作,但其权限仅限于容器内。 离开容器时,仅授予普通用户权限。
解读代码
首先runc有一个nsenter文件夹,主要是go通过cgo实现nsexec等方法。
在 Go 运行时启动之前,nsenter 包会注册一个特殊的 init 构造函数。 这允许您在现有命名空间内进行“设置”,并避免 Go 运行时在多线程场景中的潜在问题。
在runc的main.go中具体引入:
package mainimport ( "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) 运行时.LockOSThread() }}var initCommand = cli.Command{ Name: "init",Usage: `初始化命名空间并启动进程(不要外部调用)runc)`, action: func(context *cli.Context) error {factory, _ := libcontainer.New("") if err := Factory.StartInitialization(); // 错误被发送回// 父进程处理此 os.Exit(1),因此无需记录它或将其写入 stderr。 } Panic("libcontainer:container init 执行失败") },}
接下来,我们将重点关注在 Linux 容器中实现命名空间。
容器对应的命名空间定义在runc/libcontainer/configs/config.go中。 此外,对于用户命名空间,还定义了用户映射的 UidMappings 和 GidMappings。
// Config 定义在所包含的环境中运行进程的配置选项。 type Config struct { ... // Namespaces 指定容器的命名空间。克隆 init 进程 // 如果没有提供命名空间,则将从容器的父进程共享命名空间 Namespaces Namespaces `json:"namespaces"` // UidMappings 是用户命名空间的用户 ID 映射数组 UidMappings []IDMap ` json :"uid_mappings"` // GidMappings 是用户命名空间的组 ID 映射数组。 GidMappings []IDMap `json:"gid_mappings"` ...}
命名空间定义如下。
package configsimport ( "fmt" "os" "sync") const ( NEWNET NamespaceType = "NEWNET" NEWPID NamespaceType = "NEWPID" NEWNS NamespaceType = "NEWNS" NEWUTS NamespaceType = "NEWUTS" NEWIPC NamespaceType = " NEWIPC" NEWUSER NamespaceType = "NEWUSER")var ( nsLocksync.MutexsupportedNamespaces = make(map[NamespaceType]bool))// NsName 将名称空间类型转换为其文件名 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 返回命名空间是否可用,或者 // notfunc IsNamespaceSupported(ns NamespaceType) bool { nsLock.Lock() defer nsLock.Unlock() 支持, ok :=supportedNamespaces[ns] if ok { return support } nsFile := NsName(ns) // 如果命名空间类型未知,则返回 false if nsFile == "" { return false } _, err := os.Stat(fmt.Sprintf("/proc/self/ns/%s", nsFile)) //如果命名空间存在并且您有权读取它,则支持该命名空间。 supported = err == nil SupportedNamespaces[ns] = 支持 return Supported}func NamespaceTypes() []NamespaceType { return []NamespaceType{ NEWUSER, // 始终保持用户 NS 首先,不要移动它。 NEWIPC, NEWUTS, NEWNET, NEWPID, NEWNS, }}//命名空间定义了每个命名空间的配置。 这指定了可以通过 // setns.type 绑定的备用路径。 命名空间结构体 { 类型 NamespaceType `json:"type"` 路径字符串 `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, 路径字符串) { 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 支持的命名空间类型包括 ($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 是否属于上述常量。此外,您还必须: verify /proc/ 如果 self/ns/$nsName 存在并且可以读取,则认为当前系统支持该命名空间。 p>
// IsNamespaceSupported 返回该命名空间是否可用或不起作用。 NamespaceType) bool { ...supported, ok :=supportedNamespaces[ns] if ok { return Supported } ... // 除了检查命名空间类型是否在提供的列表中之外, // 还会检查 proc/self/ ns/ $nsName 存在并且可以读取吗? _, err := os.Stat(fmt.Sprintf("/proc/self/ns/%s", nsFile))supported = err == nil .. return Supported}
runc/libcontainer 在 /configs/namespaces_syscall.go 中,这些名称是在 Linux 克隆期间定义的。克隆标志对应空格。
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: 系统调用。 CLONE_NEWPID,} // CloneFlags 解析容器的命名空间选项,为克隆、取消共享设置正确的标志。 // 该函数仅返回新的命名空间标志。 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,配置:c.newInitConfig(p),容器:c,进程:p,引导数据:数据,sharePidns:sharePidns,},nil}
评论前必须登录!
注册