探索Linux命名空间和控制组:实现资源隔离与管理的双重利器

Posted by Binbin Zhang on Thu, Aug 3, 2023

介绍

Linux 命名空间**(Namespace)**

Linux 命名空间是一种隔离机制,允许将全局系统资源划分为多个独立的、相互隔离的部分,使得在不同的命名空间中运行的进程感知不到其他命名空间的存在。从而实现了对进程、网络、文件系统、IPC(进程间通信)等资源的隔离,减少了潜在的安全风险。例如,在容器中运行应用程序可以避免对主机系统的直接影响,从而提高了系统的安全性。

Linux 控制组(Cgroups)

控制组是一种资源管理机制,允许对进程组或任务组应用资源限制和优先级设置。它可以用来限制一组进程的资源使用,如 CPU、内存、磁盘 I/O 等,从而实现资源的分配和控制。

简单来说 Cgroups 可以理解为是房子的土地面积,限制了房子的大小 ,而 Namespace 是房子的墙,与邻居互相隔离。

图片

通过使用命名空间和控制组,可以更有效地使用系统资源,避免资源浪费,并确保关键任务获得足够的资源支持,从而提高系统性能和效率。这些功能对于现代的云计算和容器化部署是至关重要的。最典型的容器技术 Docker 就是利用 namespace 和 cgroup 实现的。

Linux 命名空间(Namespace)

命名空间类型

下面是 Linux 提供的 Namespace 类型,通过这些命名空间的组合,可以实现复杂的隔离和虚拟化配置,下面会详细介绍。

Namespace类型 系统调用参数 描述
Mount Namespace(mnt) CLONE_NEWNS 隔离各个进程的挂载点试图
UTS Namespace(uts) CLONE_NEWUTS 隔离NodeName和
DomainName
PID Namespace(pid) CLONE_NEWPID 隔离进程ID
Network Namespace(net) CLONE_NEWNET 隔离网络设备
IPC Namespace(ipc) CLONE_NEWIPC 隔离System V IPC
(Inter-Process
Communication)
和 POSIX 消息队列,
包括共享内存、信号量、
消息队列等
User Namespace(user) CLONE_NEWUSER 隔离用户和用户组 ID
Cgroup Namespace (cgroup)
CLONE_NEWCGROUP 隔离Cgroup 根目录

PID 命名空间

Linux PID 命名空间是 Linux Namespace 的一种类型,用于隔离进程 ID。在一个 PID 命名空间中,每个进程拥有独立的进程 ID,这样在不同的命名空间中可以有相同的进程 ID,而不会产生冲突。每个子 PID 命名空间中都有 PID 为 1 的 init 进程,对应父命名空间中的进程,父命名空间对子命名空间运行状态是不隔离的,但是每一个子命名空间是互相隔离的。

如下图:在子命名空间 A 和 B 中都有一个进程 ID=1 的 init 进程,这两个进程实际上是父命名空间的 55 号和 66 号进程 ID,虚拟化出来的空间而已。

图片

UTS 命名空间

Linux UTS 命名空间用于隔离主机名和域名。在 UTS 命名空间中,每个进程可以拥有独立的主机名和域名(nodename,domainname),这样可以在不同的命名空间中拥有不同的标识,从而实现了主机名和域名的隔离。

nodename: 是用于标识主机的独特名称,通常也被称为主机名。它用于在网络中唯一地标识一台计算机 domainname: 是主机的域名部分,通常用于标识所属的网络域。域名通常由多个部分组成,按照从右到左的顺序,每个部分之间用点号 “.” 分隔。域名用于将主机名与特定的网络域关联起来,从而帮助在全球范围内定位和访问计算机 在容器技术中,利用 UTS Namespace 隔离后,容器内的进程可以拥有独立的主机名和域名,而不会与宿主系统或其他容器中的进程产生冲突。这样,容器内的应用程序可以认为它们在独立的主机中运行,从而更容易进行配置和管理。

Mount 命名空间

Linux Mount Namespace 用于隔离文件系统挂载点。通过 Mount Namespace,不同的进程可以在不同的挂载点上看到不同的文件系统层次结构,即使在同一台主机上运行。这种隔离使得进程在一个 Mount Namespace 中的挂载操作对其他 Mount Namespace 中的进程不可见,从而实现了文件系统层面的隔离。

在容器技术中,利用 Mount Namespace 隔离后,容器内部的文件系统挂载与宿主系统和其他容器相互隔离。这样,每个容器可以拥有独立的文件系统视图,容器内的进程只能访问自己的文件系统层次结构,而无法访问其他容器或宿主系统的文件系统。

Network 命名空间

Linux Network Namespace 用于隔离网络栈。通过 Network Namespace,不同的进程可以拥有独立的网络设备、IP 地址、路由表、网络连接和网络命名空间中的其他网络资源。这种隔离使得进程在一个 Network Namespace 中的网络配置和状态对其他 Network Namespace 中的进程不可见,从而实现了网络层面的隔离。

在容器技术中,利用 Network Namespace 隔离后,容器内部的进程拥有独立的网络环境,从而使得容器在网络上彼此隔离。每个容器可以有自己的网络设备、IP 地址、路由表和网络连接,容器之间不会干扰彼此,也不会干扰宿主系统。

User 命名空间

Linux User Namespace 用于隔离用户和用户组 ID。通过 User Namespace,不同的进程可以拥有独立的用户和用户组 ID,这样可以在不同的命名空间中拥有不同的身份标识,从而实现了用户和用户组的隔离。

在容器技术中,利用 User Namespace 隔离后,容器内的进程可以拥有独立的用户和用户组 ID,而不会与宿主系统或其他容器中的用户产生冲突。这样,容器内的应用程序可以以普通用户身份运行,而不需要在宿主系统中创建相同的用户账号。

在 Docker 中默认是不启用 User Namespace 隔离的,主要是因为开启后需要做很多特殊的配合和管理,例如隔离后容器内的用户和宿主上的用户已经不是相同的身份了,那么可能会影响访问文件系统。

IPC 命名空间

Linux IPC 命名空间用于隔离进程间通信资源。在 IPC 命名空间中,每个命名空间都有独立的 IPC 资源,如消息队列、信号量和共享内存,使得不同命名空间中的进程无法直接访问其他命名空间的 IPC 资源,从而实现了 IPC 资源的隔离。

在容器技术中,利用 IPC Namespace 隔离后,容器内的进程拥有独立的 IPC 资源,从而避免不同容器之间的进程干扰和资源冲突。每个容器都可以有自己的 IPC 命名空间,使得容器内的进程在进行进程间通信时只能访问属于同一命名空间的 IPC 资源,而无法直接访问其他容器的 IPC 资源。

实战

创建和管理命名空间

在 Linux 系统中提供了以下几种常用的创建和管理命名空间的 API:

  1. clone:使用 clone 系统调用创建一个新进程时可以通过指定一个或多个上面列出的命名空间标志参数来创建新的命名空间,并且新进程的子进程也会默认被包含在新的命名空间内
  2. unshare:使用 unshare 系统调用将一个已存在的进程放入新的命名空间。它可以指定一个或多个上面列出的命名空间标志参数,创建具有指定类型的命名空间,并将当前进程或其他指定进程放入其中
  3. setns: 使用 setns 系统调用允许进程将自己放入已经存在的命名空间中,而无需创建新的进程。通过 setns 系统调用,进程可以切换到指定类型的命名空间中,与其他已存在于该命名空间中的进程共享同一个隔离环境

隔离进程

在这段代码中执行 sh 命令,并设置了系统调用 clone flage 参数为 CLONE_NEWPID,意思是当执行 main 方法时会创建一个新的进程(sh)并创建了 PID 命名空间,使 sh 进程与 main 进程隔离。

 1package main
 2import (
 3   "log"
 4   "os"
 5   "os/exec"
 6   "syscall"
 7)
 8func main() {
 9   cmd := exec.Command("sh")
10   cmd.SysProcAttr = &syscall.SysProcAttr{
11      Cloneflags: syscall.CLONE_NEWPID,
12   }
13   cmd.Stdin = os.Stdin
14   cmd.Stdout = os.Stdout
15   cmd.Stderr = os.Stderr
16   if err := cmd.Run(); err != nil {
17      log.Fatal(err)
18   }
19}

当执行 go run main.go 后,打开新的 shell 页面,执行 ps aux 看一下启动的进程号=156

图片

然后利用 pstree 看一下进程树,可以发现通过 main 调用起来的 sh 命令进程 ID=678

图片

那么我们回到执行 go run main.go 的 shell 页面中,执行 ehco $$,可以发现当前进程 ID=1,这可以证明,在新的 PID 命名空间下进程 ID 1 映射的就是进程 ID 678 , 从而可以确认进程已经被成功隔离。

图片

隔离网络

在上一段代码的基础上,我们只需要新增系统调用 clone flage 参数 CLONE_NEWNET,当执行 main 方法时会创建一个新的进程(sh)并创建了 PID 和 NET 命名空间,使 sh 进程与 main 进程&网络同时隔离。

 1package main
 2import (
 3   "log"
 4   "os"
 5   "os/exec"
 6   "syscall"
 7)
 8func main() {
 9   cmd := exec.Command("sh")
10   cmd.SysProcAttr = &syscall.SysProcAttr{
11      Cloneflags: syscall.CLONE_NEWPID | syscall.CLONE_NEWNET,
12   }
13   cmd.Stdin = os.Stdin
14   cmd.Stdout = os.Stdout
15   cmd.Stderr = os.Stderr
16   if err := cmd.Run(); err != nil {
17      log.Fatal(err)
18   }
19}

当执行 go run main.go 后,打开新的 shell 页面,执行 ifconfig,可以看到网络相关的信息