嗨,你好啊,我是猿java
Linux 内核提供了多种机制来实现系统资源的隔离和管理,这篇文章,我们来详细分析两种关键的技术:Namespace 和 Cgroups。
Namespace 详解
Namespace(命名空间,也称名称空间)是 Linux 内核用于隔离系统资源,使得不同的进程组可以拥有各自独立的资源视图。Namespace 的核心思想是通过将系统资源划分为不同的命名空间,进而实现资源的隔离。
Namespace通常包含以下几种类型:
- PID Namespace
- Mount Namespace
- UTS Namespace
- IPC Namespace
- Network Namespace
- User Namespace
PID Namespace
定义
PID Namespace 用于隔离进程 ID 空间,每个 PID Namespace 都有自己独立的进程 ID 号,父 Namespace 可以查看和管理子 Namespace 中的进程,但反之则不行。这种机制使得在容器中运行的进程可以拥有从 1 开始的 PID。
如下指令,可以创建一个新的 PID 命名空间:
# 创建新的 PID Namespace 并运行一个 Bash 进程
unshare -p -f --mount-proc bash
命令解释
- unshare:用于创建并进入一个或多个新的命名空间。
- -p:创建一个新的 PID 命名空间。在新的 PID 命名空间中,进程可以拥有独立的进程 ID 号。
- -f:让 unshare 命令 fork 一个新的进程,并在这个新的进程中执行指定的命令。这样可以确保 unshare 本身不会受到新的命名空间的影响。
- --mount-proc:在新的 PID 命名空间中挂载一个新的 /proc 文件系统。这个选项通常与 -p 选项一起使用,以确保新的 PID 命名空间有一个一致的 /proc 视图。
- bash:在新的命名空间中启动一个新的 Bash shell。
示例
为了更好地说明 Mount Namespace,下面以一个示例进行展示:
1.在终端A中执行以下指令:
# 运行 unshare -p -f --mount-proc bash
unshare -p -f --mount-proc bash
# 在新的 Bash shell 中查看进程 ID,只能看到PID=1的 bash进程和 ps aux进程
ps aux
2.在另一个终端B中执行以下指令:
# 可以看到很多 PID
ps aux
两段指令的终端对比图如下:
通过上面的对比图可以看出,在终端 A中实现了 PID的隔离,ps aux
看不到宿主机中的所有进程,这样可以实现进程级别的安全隔离,即使在新的命名空间中运行的进程出现问题,也不会影响到系统中的其他进程。
使用场景
- 容器技术:PID 命名空间和挂载命名空间在容器技术中非常重要。通过 PID 命名空间,容器可以拥有独立的进程 ID 号,这使得容器中的进程可以独立管理和调试。通过挂载新的 /proc 文件系统,容器可以拥有独立的进程视图,便于监控和管理。
- 安全隔离:通过创建新的 PID 命名空间和挂载新的 /proc 文件系统,可以实现进程级别的安全隔离。这样一来,即使在新的命名空间中运行的进程出现问题,也不会影响到系统中的其他进程。
Mount Namespace
定义
Mount Namespace 用于隔离挂载点。不同的 Mount Namespace 可以拥有各自独立的文件系统视图。这样一来,在一个 Namespace 中对文件系统的修改不会影响到其他 Namespace。
如下指令:系统会创建一个新的挂载命名空间,使得在该命名空间中的挂载操作不会影响到其他的命名空间
# 创建新的 Mount Namespace 并运行一个 Bash 进程
unshare -m bash
当你运行unshare -m bash
时,会发生以下事情:
- 创建新的挂载命名空间:使用 -m 选项,系统创建一个新的挂载命名空间。这个新的命名空间是当前进程的子命名空间,拥有独立的挂载点视图。
- 启动新的 Bash Shell:在这个新的挂载命名空间中启动一个新的 Bash shell。这个 Bash shell 及其子进程将只在这个新的挂载命名空间中运行。
- 独立的挂载操作:在这个新的 Bash shell 中进行的挂载和卸载操作(如 mount 和 umount 命令)将不会影响到其他命名空间中的挂载点。
示例
为了更好地说明 Mount Namespace,下面以一个示例进行展示:
1.在终端A中执行以下指令(子 namespace):
# 运行 unshare -m bash
unshare -m bash
# 在新的挂载命名空间中挂载一个临时文件系统
mount -t tmpfs none /mnt
# 查看挂载点
mount | grep /mnt
2.在另一个终端B中执行以下指令(父 namespace):
# 在父命名空间中,这个挂载点是不可见的
mount | grep /mnt
两段指令的终端对比图如下:
使用场景
- 容器技术:通过挂载命名空间,容器可以拥有独立的文件系统视图,使得容器中的文件系统操作不会影响到宿主系统和其他容器。
- 安全隔离:通过挂载命名空间,可以实现文件系统级别的安全隔离。例如,你可以在一个挂载命名空间中挂载一个只读的文件系统,从而防止进程对文件系统进行修改。
UTS Namespace
定义
UTS (UNIX Time-Sharing) Namespace 用于隔离主机名和域名。不同的 UTS Namespace 可以拥有不同的主机名和域名,这对于容器化应用非常有用。
如下指令:系统会创建一个新的命名空间,用于隔离主机名和域名
# 创建新的 UTS Namespace 并运行一个 Bash 进程
unshare -u bash
hostname new_hostname
指令解释
- unshare:用于创建并进入一个或多个新的命名空间。
- -u:创建一个新的 UTS 命名空间。在新的 UTS 命名空间中,主机名和域名是独立的。
- bash:在新的命名空间中启动一个新的 Bash shell。
- hostname:用于查看或设置系统的主机名。
- new_hostname:要设置的新主机名。
示例
为了更好地说明 UTS Namespace,下面以一个示例进行展示:
1.在终端A 中执行以下指令(子 namespace):
# 创建新的 UTS 命名空间并启动 Bash
unshare -u bash
# 查看当前主机名
hostname
# 修改主机名
hostname new_hostname
# 再次查看主机名,确认修改
hostname
2.在终端B 中执行以下指令(父 namespace):
# 在父命名空间中查看主机名,主机名没有变化
hostname
两段指令的终端对比图如下:
使用场景
- 容器技术:UTS 命名空间在容器技术中非常重要。通过 UTS 命名空间,每个容器可以拥有独立的主机名和域名,这对于多租户环境和隔离应用非常有用。
- 多租户环境:在多租户环境中,不同的租户需要独立的主机名和域名。通过 UTS 命名空间,可以为每个租户创建独立的命名空间,从而实现资源的隔离和独立管理。
IPC Namespace
定义
IPC (Inter-Process Communication) Namespace 用于隔离进程间通信资源,如消息队列、信号量和共享内存。不同的 IPC Namespace 之间的通信资源是隔离的。
如下指令:是一个用于创建新的 IPC(Inter-Process Communication)命名空间并在其中启动一个新的 Bash shell 的命令
# 创建新的 IPC Namespace 并运行一个 Bash 进程
unshare -i bash
# 创建一个新的消息队列
ipcmk -Q
示例
为了更好地说明 UTS Namespace,下面以一个示例进行展示:
1.在终端A 中执行以下指令(子 namespace):
# 创建一个新的 IPC 命名空间并启动一个新的 Bash shell
unshare -i bash
# 创建一个新的消息队列
ipcmk -Q
# 查看当前消息队列
ipcs -q
2.在终端B 中执行以下指令(父 namespace):
# 在父命名空间中查看消息队列,没有新的消息队列,这验证了 IPC 命名空间的隔离效果
ipcs -q
两段指令的终端对比图如下:
使用场景
- 容器技术:通过 IPC 命名空间,每个容器可以拥有独立的进程间通信资源,这对于隔离应用和提高系统安全性非常有用。
- 多租户环境:在多租户环境中,不同的租户需要独立的进程间通信资源。通过 IPC 命名空间,可以为每个租户创建独立的命名空间,从而实现资源的隔离和独立管理。
Network Namespace
定义
Network Namespace 用于隔离网络资源,如网络接口、路由表、防火墙规则等。每个 Network Namespace 可以拥有独立的网络设备和配置。
# 创建新的 Network Namespace 并运行一个 Bash 进程
ip netns add mynamespace
ip netns exec mynamespace bash
示例
为了更好地说明 Network Namespace,下面以一个示例进行展示如何创建和配置 Network Namespace:
1.在终端A 中执行以下指令(子 namespace):
# 创建新的 Network Namespace
ip netns add mynamespace
# 创建一对 veth 设备
ip link add veth0 type veth peer name veth1
# 将 veth1 移动到新的命名空间中
ip link set veth1 netns mynamespace
# 在默认命名空间中设置 veth0
ip addr add 192.168.1.1/24 dev veth0
ip link set veth0 up
# 在新的命名空间中设置 veth1
ip netns exec mynamespace ip addr add 192.168.1.2/24 dev veth1
ip netns exec mynamespace ip link set veth1 up
# 在新的命名空间中设置默认路由
ip netns exec mynamespace ip route add default via 192.168.1.1
2.在终端B 中执行以下指令(父 namespace):
# 在父命名空间可以通过 ping 命令测试与 mynamespace 中的网络连接
ping 192.168.1.2
3.在终端A 中执行以下指令(子 namespace):
# 在 mynamespace 中,可以通过 ping 命令测试与默认命名空间中的网络连接
ip netns exec mynamespace ping 192.168.1.1
三段指令的终端对比图如下:
使用场景
- 容器技术:通过 Network Namespace,每个容器可以拥有独立的网络栈,从而实现网络资源的隔离和独立管理。
- 多租户环境:在多租户环境中,不同的租户需要独立的网络配置和安全策略。通过 Network Namespace,可以为每个租户创建独立的网络栈,从而实现网络资源的隔离和独立管理。
User Namespace
定义
User Namespace 用于隔离用户和用户组 ID。它允许在不同的 Namespace 中使用相同的用户 ID,但这些 ID 在不同的 Namespace 中是独立的。这样一来,即使在容器中运行的进程以 root 身份运行,也不会拥有对宿主系统的 root 权限。
# 创建新的 User Namespace 并运行一个 Bash 进程
unshare -U bash
示例
为了更好地说明 User Namespace,下面以一个示例进行展示如何创建和配置 Network Namespace:
1.在终端A 中执行以下指令(子 namespace):
# 创建新的 User Namespace 并启动 Bash
unshare -U bash
# 在新的命名空间中查看当前用户ID
id
# 在外部命名空间中设置用户ID映射
# 假设当前用户ID为1000
# 需要在另一个终端中运行以下命令
newuidmap $(pgrep -n bash) 0 1000 1
2.在终端B 中执行以下指令(父 namespace):
# 在父空间查看文件的所有者,找不到文件
ls -l /tmp/testfile
3.在终端A 中执行以下指令(子 namespace):
# 在新的命名空间中创建一个文件
touch /tmp/testfile
# 查看文件的所有者
ls -l /tmp/testfile
三段指令的终端对比图如下:
文件的所有者将是新的用户ID映射中的用户,而不是外部命名空间中的用户。
使用场景
- 容器技术:通过 User Namespace,每个容器可以拥有独立的用户和用户组ID映射,从而实现特权分离和安全隔离。这使得容器中的进程可以以root身份运行,同时在宿主系统中仍然是非特权用户。
- 多租户环境:在多租户环境中,不同的租户需要独立的用户和用户组ID映射。通过 User Namespace,可以为每个租户创建独立的用户和用户组ID映射,从而实现资源的隔离和独立管理。
无法 Namespace的资源
尽管 Linux的 Namespace机制提供了对多种系统资源的隔离,但并不是所有的系统资源都能被 Namespace隔离,以下是一些不能被 Namespace隔离的资源及其原因:
- 内核模块: 内核模块(Kernel Modules)在整个系统中是全局共享的。加载或卸载一个内核模块会影响到所有Namespace中的进程。
- 内核参数: 通过
sysctl
命令设置的内核参数(如/proc/sys
下的参数)是全局的,无法在不同的Namespace中进行独立设置。 - 硬件资源:硬件资源是物理存在的,无法通过软件机制进行隔离。
- CPU:尽管Cgroups可以对CPU资源进行分配和限制,但CPU本身是一个物理资源,无法在不同的Namespace中进行隔离。
- 内存:Cgroups可以对内存资源进行分配和限制,但物理内存本身无法在不同的Namespace中进行隔离。
- 磁盘:磁盘设备是物理存在的,无法在不同的Namespace中进行隔离。尽管可以通过Cgroups对磁盘I/O进行限制,但磁盘设备本身是共享的。
- 时间:系统时间(如系统时钟和硬件时钟)在整个系统中是共享的,无法在不同的Namespace中进行独立设置。
- 安全机制:一些系统级的安全机制无法在不同的Namespace中进行隔离。
- SELinux:SELinux(Security-Enhanced Linux)是一种安全模块,它的策略在整个系统中是全局的,无法在不同的Namespace中进行独立设置。
- AppArmor:类似于SELinux,AppArmor也是一种安全机制,它的配置和策略在整个系统中是全局的。
- 系统日志:系统日志(如
/var/log
下的日志文件)在整个系统中是共享的,无法在不同的Namespace中进行独立管理。 - 特殊设备文件:一些特殊的设备文件(如
/dev
下的某些设备文件)在不同的Namespace中是共享的,无法进行隔离。例如,/dev/null
、/dev/zero
等设备文件在整个系统中是全局的。
Cgroups 详解
Cgroups 的基本概念
Cgroups (Control Groups,控制组)是 Linux 内核的一个特性,用于限制、记录和隔离一组进程的资源使用(CPU、内存、磁盘 I/O、网络等)。
Cgroups 通过将进程分组,然后对这些组应用资源限制来工作,核心组件包括:
Cgroup
Cgroup是一个控制组,表示一组进程。Cgroup通过层级结构组织,类似于文件系统的目录结构。每个节点代表一个Cgroup,节点之间的层级关系表示Cgroup的继承关系。
Hierarchy
Hierarchy是Cgroup的组织结构。一个Hierarchy可以包含多个Cgroup,并且每个Hierarchy可以挂载到一个目录中。在同一个Hierarchy中,Cgroup之间存在父子关系,子Cgroup继承父Cgroup的限制和策略。
Subsystem
Subsystem是具体的资源控制器,如CPU、内存、块I/O等。每个Subsystem负责对特定类型的资源进行管理和限制。例如,cpu Subsystem用于控制CPU资源,memory Subsystem用于控制内存资源。
Cgroups 的子系统
Cgroups 支持多种子系统,每种子系统负责不同的资源控制:
- cpu: 控制 CPU 资源的分配。
- cpuacct: 提供 CPU 资源使用的统计信息。
- memory: 控制内存资源的分配和使用。
- blkio: 控制块设备的 I/O 操作。
- net_cls: 控制网络资源的分类。
- devices: 控制设备的访问权限。
- freezer: 暂停和恢复进程。
创建和管理 Cgroups
通过命令行工具 cgcreate
、cgset
和 cgexec
可以方便地创建和管理 Cgroups。
# 创建一个新的 Cgroup
cgcreate -g cpu,memory:/mygroup
# 设置 CPU 使用限制
cgset -r cpu.shares=512 mygroup
# 设置内存使用限制
cgset -r memory.limit_in_bytes=256M mygroup
# 将一个进程加入到 Cgroup
cgexec -g cpu,memory:mygroup /bin/bash
Cgroups v1 & v2
Cgroups有两个版本:v1和v2。Cgroups v1提供了多种独立的子系统,每个子系统可以有独立的层级结构。Cgroups v2则提供了统一的层级结构,所有子系统共享同一个层级。
Cgroups v1
在Cgroups v1中,每个子系统可以有独立的层级结构。例如,可以有一个层级用于CPU资源控制,另一个层级用于内存资源控制。
Cgroups v2
Cgroups v2 是 Cgroups 的第二个版本,提供了更为统一和简化的接口。Cgroups v2 的主要特点包括:
- 统一的层级结构,所有子系统共享同一个层级。
- 更为简化的配置接口,减少了配置的复杂性。
- 提高了资源控制的精度和灵活性。
示例
为了更好地说明 Cgroups,这里以两个示例进行说明
示例1:限制CPU使用
下面是一个示例,展示如何使用Cgroups限制CPU使用:
# 创建一个新的Cgroup
cgcreate -g cpu:/cpulimited
# 设置CPU使用限制
cgset -r cpu.shares=512 cpulimited
# 将一个进程加入到Cgroup
cgexec -g cpu:cpulimited /bin/bash
在这个示例中,我们创建了一个名为cpulimited
的 Cgroup,并设置了CPU使用限制。然后,我们将一个新的Bash进程加入到这个Cgroup中。
示例2:限制内存使用
下面是一个示例,展示如何使用 Cgroups 限制内存使用:
# 创建一个新的Cgroup
cgcreate -g memory:/memlimited
# 设置内存使用限制
cgset -r memory.limit_in_bytes=256M memlimited
# 将一个进程加入到Cgroup
cgexec -g memory:memlimited /bin/bash
在这个示例中,我们创建了一个名为memlimited
的 Cgroup,并设置了内存使用限制。然后,我们将一个新的Bash进程加入到这个Cgroup中。
使用场景
- 容器技术:通过Cgroups,可以为每个容器设置独立的资源限制,从而实现资源的隔离和独立管理。这使得容器中的进程可以在有限的资源范围内运行,而不会影响到宿主系统和其他容器。
- 多租户环境:在多租户环境中,不同的租户需要独立的资源限制和管理策略。通过Cgroups,可以为每个租户创建独立的Cgroup,从而实现资源的隔离和独立管理。
为什么学习?
上面的内容我们详细地分析了 Namespace 和 Cgroups两个技术点,那么,为什么要学习它们,它们有什么实际性的应用?
Namespace 和 Cgroups 的结合使用是容器技术
(比如 Docker)的基础,Namespace 提供了进程级别的隔离,而 Cgroups 则用于资源的分配和限制,通过这两种机制,可以创建高效且安全的容器化环境。
容器的创建
以下是一个简单的示例,展示如何使用 Namespace 和 Cgroups 创建一个容器:
# 创建新的 Namespace
unshare -p -f -m -u -i -n --mount-proc bash
# 设置主机名
hostname container
# 挂载新的文件系统
mount -t tmpfs none /tmp
# 创建新的 Cgroup
cgcreate -g cpu,memory:/container
# 设置 CPU 和内存限制
cgset -r cpu.shares=512 container
cgset -r memory.limit_in_bytes=256M container
# 将当前进程加入到 Cgroup
cgclassify -g cpu,memory:container $$
在上述示例中:
- 首先,创建了一个新的 Namespace
- 然后,设置了主机名并挂载了新的文件系统
- 接着,创建了一个新的 Cgroup,并设置了 CPU 和内存限制
- 最后,将当前进程加入到 Cgroup,这样一个容器就创建完成
总结
本文,我们分析了 Linux 内核提供的两种关键机制:Namespace 和 Cgroups,它们用于实现系统资源的隔离和管理。
Namespace 提供了进程级别的隔离,使得不同的进程组可以拥有各自独立的资源视图,而 Cgroups 则用于资源的分配和限制,通过将进程分组,然后对这些组应用资源限制来工作。结合使用这两种机制,可以创建高效且安全的容器
化环境。
因此,掌握 Namespace 和 Cgroups 两个技术点,可以帮助我们更好地理解容器(比如 Docker)的实现原理。
学习交流
如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。