在容器中,命名空间为单个集群内的资源分组提供了一种机制。资源的名称在命名空间内需要唯一,但在不同的命名空间内可以重复。基于命名空间的范围仅适用于命名空间对象(例如部署、服务等),而不适用于集群范围的对象(例如存储类、节点、持久卷等)。换句话说,通过使用命名空间,容器可以在同一集群内实现多租户的隔离,使不同的用户或团队可以独立管理其拥有的资源,而不会与其他用户或团队的资源产生冲突。需要注意的是,命名空间并不能完全保证资源的隔离性和安全性,因此在使用过程中需要遵循最佳实践和安全原则。
广泛使用的命名空间类型有七种。这些命名空间类型可以单独使用,也可以组合使用,从而实现不同程度的进程隔离和资源隔离。在容器化和虚拟化等场景下,命名空间被广泛应用,可以提高系统的安全性、可管理性和可扩展性。
- Mount Namespace:控制文件系统的挂载点和挂载视图,可以在命名空间内挂载文件系统,但对外部不可见,并且可以放宽根文件系统的限制,实现文件系统隔离。/proc/[pid]/mounts,/proc/[pid]/mountinfo和/proc/[pid]/mountstats文件 提供的视图对应于包含PID [pid]进程的Mount命名空间。 使用CLONE_NEWNS标志的clone(2)或unshare(2)创建新的Mount命名空间。创建新的Mount命名空间时,其挂载列表的初始化如下:如果使用clone(2)创建命名空间,则子命名空间的挂载列表是父进程的Mount命名空间中挂载列表的副本。如果使用unshare(2)创建命名空间,则新命名空间的挂载列表是调用者先前Mount命名空间中挂载列表的副本。在任何一个Mount命名空间中对挂载列表的后续修改(mount(2)和umount(2))默认情况下不会影响在其他命名空间中看到的挂载列表。
Mount Namespace的实现位于Linux内核的fs/mount_namespace.c文件中,其中包含了相关的数据结构和函数实现。具体来说,Mount Namespace的实现主要依赖于struct vfsmount和struct mnt_namespace两个数据结构。其中,struct vfsmount表示一个文件系统的挂载点,包括该挂载点的路径、文件系统类型、文件系统超级块等信息。而struct mnt_namespace则表示一个Mount Namespace,其中包含了一个文件系统的层次结构以及与之关联的挂载点列表。该文件中还包含了多个与Mount Namespace相关的函数实现,例如:- copy_mnt_ns():复制一个Mount Namespace
-
mnt_init():初始化一个Mount Namespace
-
mntput():卸载一个挂载点
-
do_mount():挂载一个文件系统 除此之外, 还定义了多个宏和常量,例如:- MNT_DETACH:用于卸载挂载点的标志
-
MNT_EXPIRE:用于过期挂载点的标志
-
MNT_NS_INTERNAL:表示一个Mount Namespace的标志Mount Namespace的实现涉及多个系统调用,包括clone()、unshare()、mount()、umount()等。其中,clone()和unshare()可以创建或分离一个新的Mount Namespace,从而实现文件系统的隔离;mount()和umount()可以挂载或卸载文件系统,而这些操作只会影响当前Mount Namespace内的文件系统,不会影响其他Namespace或主机文件系统。
此外,fs/proc_namespace.c文件包含了用于将Mount Namespace信息暴露给/proc文件系统的实现代码。具体来说,该文件中定义了一个名为proc_ns_operations的结构体,包含了用于访问Mount Namespace信息的多个函数指针。例如:- proc_ns_read():读取Mount Namespace的信息并将其格式化输出到/proc//ns/mnt文件中 -
proc_ns_follow_link():获取当前进程的Mount Namespace,并将其转换为一个符号链接,使其可以被其他进程访问此外,该文件还包含了proc_ns_inode_operations结构体和proc_ns_dir_inode_operations结构体,用于定义/proc//ns/mnt文件和其父目录的Inode操作。需要注意的是,/proc文件系统是一个伪文件系统,它不对应任何物理设备,而是通过内核代码实现的。因此,/proc//ns/mnt文件实际上是一个虚拟文件,用于访问Mount Namespace的信息。
总的来说,Mount Namespace的实现通过对文件系统挂载点和视图的隔离,使得不同的进程或容器可以拥有自己独立的文件系统视图,从而增强了系统的安全性和隔离性。
- UTS Namespace:隔离主机名和域名等系统标识符,使得进程可以有自己独立的主机名和域名。主机名和NIS域名标识符是使用sethostname(2)和setdomainname(2)设置的,并且可以使用uname(2)、gethostname(2)和getdomainname(2)检索。对这些标识符所做的更改对同一UTS Namespace中的所有其他进程可见,但对其他UTS Namespace中的进程不可见。当进程使用clone(2)或unshare(2)和CLONE_NEWUTS标志创建新的UTS Namespace时,新的UTS Namespace的主机名和域名将从调用者的UTS Namespace中的相应值复制。使用UTS Namespace需要配置了CONFIG_UTS_NS选项的内核。
UTS Namespace的实现位于Linux内核的kernel/utsname.c文件中。该代码定义了用于管理UTS命名空间的函数,包括创建、克隆和释放它们。它还定义了用于获取和释放UTS命名空间引用的函数。该代码中一些重要的函数包括:- copy_utsname():此函数复制任务的UTS命名空间,如果指定了CLONE_NEWUTS标志,则克隆它。如果创建了一个新的命名空间,则返回它,并放置旧的命名空间。
-
free_uts_ns():此函数释放UTS命名空间及其相关资源,包括其用户命名空间、inode编号和为命名空间分配的内存。
-
utsns_get():此函数获取任务的UTS命名空间的引用。
-
utsns_put():此函数释放对UTS命名空间的引用。
-
utsns_install():此函数为任务集安装一个新的UTS命名空间,并检查旧和新命名空间的用户命名空间是否具有CAP_SYS_ADMIN特权。
- IPC Namespace:隔离 System V IPC 和 POSIX 消息队列,使得进程只能与同一命名空间内的 IPC 进行通信。IPC(Inter-Process Communication,进程间通信)是一种用于在进程之间传递数据的技术。Linux提供了多种IPC机制,包括信号量、消息队列和共享内存等。这些机制都是在内核中实现的,并且在全局范围内共享。在多个进程需要进行IPC通信的情况下,这可能会导致竞争和安全问题。为了解决这个问题,Linux引入了IPC Namespace,允许进程在不同的命名空间中使用IPC机制,这样就可以隔离不同进程之间的IPC通信,从而提高了安全性和可靠性。使用IPC Namespace可以通过clone()或unshare()系统调用创建。当使用CLONE_NEWIPC标志创建新的IPC Namespace时,新的命名空间将从调用进程的IPC Namespace中复制相应的IPC对象。与其他命名空间类型类似,IPC Namespace可以使用命令行工具(如ipcmk和ipcs)和编程接口(如IPC API)进行管理。每个 IPC Namespace 中都具有以下不同的 /proc 接口:- /proc/sys/fs/mqueue 中的 POSIX 消息队列接口。
-
/proc/sys/kernel 中的 System V IPC 接口,即 msgmax、msgmnb、msgmni、sem、shmall、shmmax、shmmni 和 shm_rmid_forced。
-
/proc/sysvipc 中的 System V IPC 接口。当一个 IPC Namespace 被销毁时(即最后一个属于该 Namespace 的进程终止时),该 Namespace 中的所有 IPC 对象都会自动被销毁。使用 IPC Namespace 需要配置了 CONFIG_IPC_NS 选项的内核。
IPC Namespace的实现位于Linux内核的ipc/namespace.c文件中。 IPC(Inter-Process Communication)命名空间是Linux内核中的一种机制,用于隔离进程间通信相关的系统资源。 代码中包括创建、复制和删除IPC命名空间的功能。代码中包括以下函数:- inc_ipc_namespaces:用于增加IPC命名空间计数器。每个用户命名空间都有一个IPC命名空间计数器,用于限制该命名空间可以创建的IPC命名空间数量。 -
dec_ipc_namespaces:用于减少IPC命名空间计数器。
-
create_ipc_ns:用于创建新的IPC命名空间。它首先获取一个新的IPC命名空间对象,并将其与当前进程关联。然后,它会对IPC命名空间计数器进行检查,以确保当前用户命名空间中可以创建更多的IPC命名空间。如果计数器值已经达到了限制,则创建失败并返回错误。
-
copy_ipcs:用于创建IPC命名空间副本。它首先获取当前IPC命名空间的副本,并将其与当前进程关联。然后,它会复制IPC命名空间中的所有IPC资源,并将它们与新的IPC命名空间关联。
-
free_ipcs:用于删除IPC命名空间中的所有IPC资源。它首先删除IPC命名空间中的所有IPC资源,然后将其与当前进程解除关联。最后,它会释放IPC命名空间对象的内存空间。
-
free_ipc_ns:用于删除IPC命名空间本身。
-
put_ipc_ns:用于减少IPC命名空间引用计数。如果引用计数为零,则删除IPC命名空间。此外,该代码还包括用于管理IPC命名空间计数器的结构和IPC命名空间操作的结构体。
- PID Namespace:隔离进程树,使得进程只能看到同一命名空间内的进程。PID Namespace会让在容器内使用“ps”命令会显示不同的结果,并且可以限制信号的使用。PID 命名空间(PID namespace)隔离进程 ID 号码空间,这意味着处于不同 PID 命名空间的进程可以拥有相同的 PID。PID 命名空间允许容器提供一些功能,例如挂起/恢复容器中的一组进程,并在进程内部保持相同的 PID 的情况下将容器迁移到新的主机上。在新的 PID 命名空间中,PID 从 1 开始,有点像一个独立的系统,调用 fork(2)、vfork(2) 或 clone(2) 将会生成带有唯一 PID 的进程。使用 PID 命名空间需要内核配置了 CONFIG_PID_NS 选项。
在新的命名空间中创建的第一个进程(即使用带有 CLONE_NEWPID 标志的 clone(2) 创建的进程,或者在调用使用 CLONE_NEWPID 标志的 unshare(2) 后创建的第一个子进程)具有 PID 1,并且是命名空间的“init”进程。该进程成为任何因为驻留在此 PID 命名空间中的进程终止而变成孤儿的子进程的父进程。如果 PID 命名空间的“init”进程终止,内核将通过 SIGKILL 信号终止命名空间中的所有进程。这种行为反映出“init”进程对 PID 命名空间的正确操作是必不可少的。在这种情况下,进入此 PID 命名空间的 fork(2) 将失败,并显示 ENOMEM 错误;无法在已终止“init”进程的 PID 命名空间中创建新进程。这种情况可能会发生,例如,当进程使用 /proc/[pid]/ns/pid 文件描述符打开某个进程的名称空间,以 setns(2) 进入该名称空间时。另一个可能的情况是在调用 unshare(2) 后:如果由 fork(2) 创建的第一个子进程随后终止,则随后的 fork(2) 调用将失败并显示 ENOMEM 错误。只有“init”进程建立了信号处理程序,其他 PID 命名空间成员才能向“init”进程发送信号。即使对于特权进程,此限制也适用,以防止 PID 命名空间的其他成员意外终止“init”进程。
PID命名空间可以嵌套。每个PID命名空间都有一个父级,除了初始(“root”)PID命名空间。PID命名空间的父级是使用clone(2)或unshare(2)创建命名空间的进程的PID命名空间。因此,PID命名空间形成树,所有命名空间最终都可以追溯到根命名空间。自Linux 3.7以来,内核将PID命名空间的最大嵌套深度限制为32。一个进程在其PID命名空间中可见,并且对于直接祖先PID命名空间中的每个进程以及跟踪到根PID命名空间,其他进程也是可见的。在这种情况下,“可见”意味着一个进程可以成为另一个进程使用指定进程ID的系统调用的操作目标。相反,子PID命名空间中的进程无法看到父进程和进一步删除的祖先命名空间中的进程。NS_GET_PARENT ioctl(2) 操作可用于发现 PID 命名空间之间的父子关系。进程的PID名称空间成员身份是在进程创建时确定的,之后无法更改。这意味着,进程之间的父子关系反映了PID名称空间之间的父子关系:进程的父进程要么在同一名称空间中,要么在直接父PID名称空间中。进程只能使用CLONE_NEWPID标志调用一次unshare(2)。在执行此操作后, /proc/PID/ns/pid_for_children符号链接将为空,直到在名称空间中创建第一个子进程。/proc/sys/kernel/ns_last_pid 显示在此PID命名空间中分配的最后一个PID。当分配下一个PID时,内核将搜索大于此值的最低未分配PID,并在随后读取此文件时显示该PID。对于拥有CAP_SYS_ADMIN 或 CAP_CHECKPOINT_RESTORE 功能的进程(在拥有PID命名空间的用户命名空间中),该文件是可写的。这使得可以确定分配给在此PID命名空间内创建的下一个进程的PID。
PID Namespace的实现主要位于Linux内核的kernel/pid_namespace.c文件中。- create_pid_namespace()函数是创建新PID命名空间的主要入口点。它以一个用户命名空间和一个现有的PID命名空间作为参数,并创建一个新的PID命名空间,该命名空间是现有命名空间的子命名空间。它通过为新命名空间分配内存并初始化各种字段来实现此目的,例如IDR(通过ID查找PID的关联数组)和用于分配PID的缓存。它还增加了给定用户命名空间的PID命名空间计数器。
-
copy_pid_ns()函数用于创建现有PID命名空间的副本。它以一个用户命名空间和一个现有的PID命名空间作为参数,并创建一个现有命名空间的副本。如果给定的标志参数中未设置CLONE_NEWPID标志,则该函数仅返回对现有命名空间的引用。如果当前任务的活动PID命名空间与现有PID命名空间不同,则该函数返回一个错误。否则,它通过调用create_pid_namespace()创建一个新的命名空间。
-
put_pid_ns()函数用于释放对PID命名空间的引用。它会减少命名空间上的引用计数,如果计数减少到零,则销毁命名空间并释放任何关联的资源。该函数用于其他内核模块调用。
-
zap_pid_ns_processes()函数用于移除给定PID命名空间中的所有进程。它通过遍历系统中的所有进程并检查每个进程是否属于给定的命名空间来实现此目的。如果是,则该函数发送SIGKILL信号以终止该进程。该函数在销毁PID命名空间时调用。
-
Network Namespace:提供“虚假”的网络接口,隔离网络设备、IPv4和IPv6协议栈、IP路由表、防火墙规则、/proc/net目录(它是指向/proc/PID/net的符号链接)、/sys/class/net目录、/proc/sys/net目录下的各种文件、端口号(套接字)等网络资源, 由父容器连接外部网络,但受权限限制。此外,网络命名空间还隔离了UNIX域抽象套接字命名空间。一个物理网络设备只能在一个网络命名空间中存在。当网络命名空间被释放时(即在命名空间中的最后一个进程终止时),其物理网络设备会被移回到初始网络命名空间(而不是进程的父命名空间)。一个虚拟网络(veth(4))设备对提供了类似管道的抽象,可以用于在网络命名空间之间创建隧道,并可用于在另一个命名空间中创建与物理网络设备的桥接。当一个命名空间被释放时,它包含的veth(4)设备将被销毁。使用网络命名空间需要配置了CONFIG_NET_NS选项的内核。
NET Namespace core的实现在net/core/net_namespace.c中,ipv4的实现在net/ipv4/netns.c中等。net_namespace.c文件包含与网络命名空间相关的各种函数和数据结构。以下是文件中的一些关键部分:1. struct net:该结构表示一个网络命名空间。它包含指向各种与网络相关的数据结构的指针,例如网络设备列表、路由表和防火墙规则。 -
struct net init_net:这是在内核启动时创建的初始网络命名空间。所有没有明确与特定命名空间关联的网络相关操作都在初始命名空间中执行。
-
static DEFINE_RWLOCK(net_namespace_list_lock):这是一个读写锁,用于保护网络命名空间列表。
-
struct net *copy_net_ns(unsigned long flags, struct user_namespace *user_ns, struct net *old_net):此函数创建一个新的网络命名空间,该命名空间是现有命名空间(old_net)的副本。新命名空间与指定的用户命名空间(user_ns)相关联,flags参数用于指定各种选项,例如是否复制命名空间的网络接口。
-
void net_drop_ns(struct net *net):此函数在网络命名空间不再使用时调用。它释放与命名空间相关联的资源并将其从命名空间列表中删除。
-
int __net_init net_ns_init(struct net *net):此函数初始化新创建的网络命名空间。它设置命名空间的网络接口、路由表、防火墙规则和其他与网络相关的资源。这些仅是net_namespace.c中的一些函数和数据结构。该文件还包含用于管理网络命名空间的各种其他函数,例如用于比较网络命名空间的net_eq()、用于通过其ID检索网络命名空间的get_net()和用于迭代所有网络命名空间的net_for_each()。
-
User Namespace:可以提供“虚假”的root用户,隔离用户和用户组 ID,使得进程只能看到同一命名空间内的用户和用户组的根目录、密钥等, 但受限于父容器的权限。进程在用户命名空间内外的用户和组ID可以是不同的。特别地,一个进程在用户命名空间外可以有一个正常的非特权用户ID,同时在命名空间内可以有一个用户ID为0;换句话说,该进程在用户命名空间内具有完全的特权进行操作,但在命名空间外部的操作是非特权的。用户命名空间可以被嵌套。也就是说,每个用户命名空间(除了初始的“根”命名空间)都有一个父用户命名空间,并且可以有零个或多个子用户命名空间。父用户命名空间是通过调用unshare(2)或带有CLONE_NEWUSER标志的clone(2)创建用户命名空间的进程的用户命名空间。每个进程都是精确地属于一个用户命名空间。通过fork(2)或不带CLONE_NEWUSER标志的clone(2)创建的进程是与其父进程属于相同用户命名空间的成员。如果进程在特定命名空间中具有CAP_SYS_ADMIN,则单线程进程可以使用setns(2)加入另一个用户命名空间;这样做,它将在该命名空间中获得完整的能力集。带有CLONE_NEWUSER标志的clone(2)或unshare(2)调用将新的子进程(对于clone(2))或调用者(对于unshare(2))变为由调用创建的新用户命名空间的成员。NS_GET_PARENT ioctl(2)操作可用于发现用户命名空间之间的父子关系。对execve(2)的调用将按照通常的方式重新计算进程的能力(参见capabilities(7))。因此,除非进程在命名空间中具有用户ID 0,或可执行文件具有非空可继承的能力掩码,否则进程将失去所有能力。因为调用者在调用setns(2)后不再具有其原始用户命名空间中的能力,所以无法通过一对setns(2)调用在保留其用户命名空间成员身份的情况下重置其“securebits”标志。有许多特权操作会影响不与任何命名空间类型相关联的资源,例如更改系统(即日历)时间(由CAP_SYS_TIME管理),加载内核模块(由CAP_SYS_MODULE管理)和创建设备(由CAP_MKNOD管理)。只有在初始用户命名空间中具有特权的进程才能执行此类操作。在拥有进程挂载命名空间的用户命名空间中持有CAP_SYS_ADMIN允许该进程创建绑定挂载点和挂载以下类型的文件系统:- /proc(自Linux 3.8以来)
-
/sys(自Linux 3.8以来)
-
devpts(自Linux 3.9以来)
-
tmpfs(自Linux 3.9以来)
-
ramfs(自Linux 3.9以来)
-
mqueue(自Linux 3.9以来)
-
bpf(自Linux 4.4以来)
-
overlayfs(自Linux 5.11以来)在拥有进程cgroup命名空间的用户命名空间中持有CAP_SYS_ADMIN允许(自Linux 4.6以来)该进程挂载cgroup版本2文件系统和cgroup版本1命名层次结构(即使用“none,name =”选项挂载的cgroup文件系统)。在拥有进程PID命名空间的用户命名空间中持有CAP_SYS_ADMIN允许(自Linux 3.8以来)该进程挂载/proc文件系统。但是请注意,只有在初始用户命名空间中持有CAP_SYS_ADMIN的进程才能挂载基于块的文件系统。
- Cgroup Namespace:隔离控制组,使得进程只能看到同一命名空间内的控制组。Cgroup命名空间通过/proc/[pid]/cgroup和/proc/[pid]/mountinfo文件虚拟化了进程cgroup的视图。每个cgroup命名空间都有自己的一组cgroup根目录。这些根目录是对应记录中显示的相对位置的基准点,在/proc/[pid]/cgroup文件中。当进程使用clone(2)或unshare(2)并设置CLONE_NEWCGROUP标志创建新的cgroup命名空间时,其当前的cgroup目录成为新命名空间的cgroup根目录。在从/proc/[pid]/cgroup读取“目标”进程的cgroup成员身份时,每个记录的第三个字段中显示的路径名将相对于读取进程的相应cgroup层次结构的根目录。cgroup namespace 和安全隔离无关,主要用于资源隔离。