linux 进程隔离Namespace 学习

一、linux namespace 介绍

1.1、概念

Linux Namespace是Linux内核提供的一种机制,它用于隔离不同进程的资源视图,使得每个进程都拥有独立的资源空间,从而实现进程之间的隔离和资源管理

Linux Namespace的设计目标是为了解决多个进程之间资源冲突的问题,提供一种轻量级的虚拟化技术。通过使用Namespace,可以在一个物理主机上创建多个独立的虚拟环境,每个环境都有自己的进程、文件系统、网络和用户视图。其提供了一种抽象机制,将原本全局共享的资源隔离成不同的集合,集合中的成员独享其原本全局共享的资源。如下图:
在这里插入图片描述

进程 A 和进程 B 分别属于两个不同的 Namespace,那么进程 A 将可以使用 Linux 内核提供的所有 Namespace 资源:如独立的主机名,独立的文件系统,独立的进程编号等等。同样地,进程 B 也可以使用同类资源,但其资源与进程 A 使用的资源相互隔离,彼此无法感知。

从用户的角度来看,每一个命名空间应该像一台单独的 Linux计算机一样,有自己的 init进程 (PID为 1),其他进程的PID依次递增, A和B空间都有PID为l的init进程, 子命名空间的进程映 射到父命名空间的进程上,父命名空间可以知道每一个子命名空间的运行状态,而子命名空间与子命名空间之间是隔离的。

1.2、虚拟化相关概念

1.2.1、常见的进程级虚拟化技术:
  • chroot(Change Root)chroot是一种将进程的根目录修改为指定目录的技术。它通过限制进程的文件系统访问范围,使得进程只能在指定的目录树中运行。这样可以实现一定程度的进程隔离。

  • Linux容器(Linux Containers,LXC)LXC是一种操作系统级虚拟化技术,它使用Linux内核中的命名空间(namespaces)和控制组(cgroups)等特性,实现了对进程的隔离。通过创建和管理多个容器实例,每个容器都拥有自己的文件系统、网络和进程空间,可以实现进程级别的隔离和资源控制。

  • Docker:Docker是基于LXC的一层封装,提供了更高级别的容器管理和部署工具。它通过使用镜像(Images)和容器(Containers)的概念,使得应用程序的打包、分发和部署更加方便。Docker在LXC的基础上添加了一些额外的功能和工具,使得容器的使用更加简单和高效。

  • systemd-nspawnsystemd-nspawnSystemd项目中的一个工具,它基于Linux命名空间和chroot,提供了一个简单的容器环境。它可以启动一个进程并将其隔离在一个独立的文件系统环境中,实现了进程的隔离和资源控制。

1.2.2、常见linux 虚拟化技术

完全虚拟化(Full Virtualization):
完全虚拟化技术通过在物理硬件上运行一个虚拟机监视器(Hypervisor),来模拟一个完整的虚拟硬件环境。在完全虚拟化中,虚拟机操作系统不需要进行修改,可以运行未经修改的操作系统。代表产品有:

  • KVM(Kernel-based Virtual Machine):KVM是一个开源的完全虚拟化解决方案,它基于Linux内核提供了虚拟化的能力。KVM使用QEMU(Quick Emulator)作为虚拟机监视器,并通过硬件虚拟化扩展(如Intel的VT-x和AMD的AMD-V)提供硬件加速。

半虚拟化(Para-virtualization):
半虚拟化技术在虚拟机内部对操作系统进行修改,使其能够与虚拟化层进行通信和协作,从而提高性能和效率。代表产品有:

  • XenXen是一个开源的半虚拟化解决方案,它可以在不修改操作系统的情况下运行虚拟机。Xen使用一种称为"Xen插入式内核"的方法,通过修改操作系统内核,使其与Hypervisor进行通信,实现半虚拟化。

容器虚拟化(Containerization):
容器虚拟化技术通过在操作系统级别创建隔离的容器实例,实现应用程序和依赖的隔离。容器共享操作系统内核,因此比虚拟机更轻量级和高效。代表产品有:

  • Docker:Docker是一种流行的容器化平台,它使用容器镜像(Images)来打包应用程序及其依赖,并通过Docker引擎在宿主机上运行容器实例。Docker提供了方便的构建、分发和部署工具,使得容器的使用变得简单和高效。
  • LXC(Linux Containers):LXC是一种轻量级的容器化解决方案,它利用Linux内核中的命名空间(namespaces)和控制组(cgroups)等特性,实现了对进程的隔离。LXC提供了一个容器运行时环境,可以在其中运行独立的用户空间实例

硬件辅助虚拟化(Hardware-assisted Virtualization):
硬件辅助虚拟化技术利用物理处理器中的虚拟化扩展,如Intel的VT-x和AMD的AMD-V,提供对虚拟化的硬件支持,提高虚拟机的性能和效率。上述的KVM和Xen也是硬件辅助虚拟化的解决方案。

轻量级虚拟化(Lightweight Virtualization):
轻量级虚拟化技术是一种特殊形式的虚拟化,它通过在操作系统级别利用命名空间(namespaces)和控制组(cgroups)等特性,实现对进程的隔离和资源控制。代表产品有:

  • Docker:除了作为容器化平台,Docker也提供了一种轻量级的虚拟化方式。Docker容器可以在宿主机上以独立的进程运行,具有隔离的文件系统、网络和进程空间。

1.3、linux namespace 发展历史

Linux Namespace的发展历史可以追溯到2002年,最早是由Eric W. Biederman提出并实现。

以下是Linux Namespace的主要发展历程:

  • 2002年:最早的Linux Namespace实现由Eric W. Biederman在2.4内核版本中引入,包括Mount NamespaceUTS Namespace
  • 2006年:Eric W. Biederman和Serge E. Hallyn在2.6.24版本中引入PID Namespace,允许每个Namespace拥有独立的进程ID空间。
  • 2008年:Eric W. Biederman和Serge E. Hallyn在2.6.29版本中引入Network Namespace,实现了独立的网络隔离。
  • 2013年:Docker公司推出了Docker容器平台,基于Linux Namespace和Cgroups实现了轻量级的容器虚拟化技术,引发了容器技术的热潮。
  • 2016年:Linux Kernel 4.6版本中引入了IPC NamespaceUser Namespace,分别实现了进程间通信和用户隔离。
  • 2017年:Linux Kernel 4.11版本中引入了CGROUP Namespace,允许每个Namespace拥有独立的资源限制。

随着时间的推移,Linux Namespace逐渐成为Linux内核中的重要特性,为容器化技术的发展提供了基础。它提供了一种灵活且轻量级的隔离机制,使得可以在单个主机上创建多个独立的虚拟环境,实现了资源的隔离和管理。

1.4、linux namespace 作用

Linux Namespace的作用包括:

  • 进程隔离:Linux Namespace可以将不同进程隔离开,每个进程在自己的Namespace中运行,不受其他进程的影响,具有独立的进程视图。
  • 文件系统隔离:通过Mount Namespace,每个进程可以拥有自己的文件系统挂载点,实现文件系统的隔离。
  • 网络隔离:通过Network Namespace,每个进程可以拥有独立的网络设备、IP地址、路由表和防火墙规则,实现网络的隔离。
  • 进程间通信隔离:通过IPC Namespace,实现进程间通信(IPC)的隔离,使得不同的进程在不同的Namespace中无法直接通信。
  • 用户和用户组隔离:通过User Namespace,每个进程可以拥有独立的用户和用户组视图,从而实现用户和用户组的隔离。
  • 进程资源限制:通过PID Namespace和CGROUP Namespace,可以限制进程的资源使用,如CPU、内存、磁盘IO等,实现资源的管理和隔离。

通过使用Linux Namespace,可以在一个物理主机上创建多个独立的虚拟环境,每个环境都有自己的进程、文件系统、网络和资源限制,从而实现进程间的隔离和资源管理,提高系统的安全性和性能。
在这里插入图片描述

二、namespace 的主要函数

主要函数列表如下:

名称作用
clone()用于创建新进程并指定新的命名空间。可以使用clone()函数的CLONE_NEWPIDCLONE_NEWNSCLONE_NEWNETCLONE_NEWIPCCLONE_NEWUSER等标志来创建新的PID、Mount、Network、IPCUser Namespace
unshare()用于在运行中创建新的命名空间。可以使用unshare()函数的CLONE_NEWPIDCLONE_NEWNSCLONE_NEWNETCLONE_NEWIPCCLONE_NEWUSER等标志来创建新的PID、Mount、Network、IPCUser Namespace。不同于clone()函数,unshare()函数不会创建新的进程,而是将当前进程切换到新的命名空间。
setns()用于将进程加入到已存在的命名空间中,实现命名空间的共享。可以通过指定命名空间的文件描述符和命名空间类型来将进程加入到相应的命名空间中。
mount()用于在指定的Mount Namespace中挂载文件系统。可以指定挂载点、文件系统类型和挂载选项等参数来进行挂载操作。
umount()用于在指定的Mount Namespace中卸载文件系统。可以指定要卸载的挂载点来完成卸载操作。
unshare(CLONE_NEWCGROUP)用于切换进程到一个新的CGROUP Namespace,实现进程的资源限制和控制。
netns()用于创建或管理Network Namespace。通过指定相应的命令行参数,可以创建、删除、查看或切换到不同的Network Namespace
ip netns命令命令行工具用于创建、删除、查看或切换Network Namespace
ipc_namespace()用于创建或管理IPC Namespace
user_namespace()用于创建或管理User Namespace

2.1、clone()

clone()函数是Linux中用于创建新进程的系统调用之一,它具有创建新的命名空间的能力clone()是在C语言库中定义的一个封装(wrapper)函数,它负责创建新进程的堆栈而且调用对编程者隐藏的clone()系统调用。clone()实际上是 linux 系统调用fork()的一种更通用的实现方式,它能够经过flags来控制使用多少功能。一共有 20 多种CLONE_开头的falg(标志位)参数用来控制 clone 进程的方方面面(好比是否与父进程共享虚拟内存等)。以下是clone()函数的详细介绍:

#include <sched.h>

int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...);

clone()函数的第一个参数fn是一个函数指针,它指向新进程将要执行的函数。这个函数具有如下原型:int fn(void *arg)。新进程将从指定函数的入口点开始执行。当这个函数返回时,子进程终止。该函数返回一个整数,表示子进程的退出代码。

child_stack参数是新进程的栈,可以是已分配的内存空间,也可以是NULL。如果child_stackNULL,则新进程使用与父进程相同的栈。

flags参数是一个位掩码,用于设置新进程的行为和属性。常用的标志包括:

  • CLONE_NEWPID:创建新的PID Namespace,使得新进程在单独的进程视图中运行,独立于父进程和其他进程。
  • CLONE_NEWNS:创建新的Mount Namespace,使得新进程在单独的文件系统视图中运行,独立于父进程和其他进程。
  • CLONE_NEWNET:创建新的Network Namespace,使得新进程在单独的网络视图中运行,独立于父进程和其他进程。
  • CLONE_NEWIPC:创建新的IPC Namespace,使得新进程在单独的IPC资源视图中运行,独立于父进程和其他进程。
  • CLONE_NEWUSER:创建新的User Namespace,使得新进程在单独的用户和用户组视图中运行,独立于父进程和其他进程。

arg参数是传递给新进程的参数。

clone()函数的返回值是新进程的ID(PID),如果出现错误,则返回-1

通过指定不同的标志位,可以在clone()函数中创建不同类型的命名空间,实现进程的隔离和资源管理。这为容器化技术的实现提供了基础。

2.2、setns()函数

setns()函数是Linux中用于将进程加入到已存在的命名空间中的系统调用之一。

#define _GNU_SOURCE
#include <sched.h>

int setns(int fd, int nstype);

setns()函数用于将进程加入到已存在的命名空间中。它接受两个参数:

  • fd:一个打开的文件描述符,指向已存在的命名空间。这个文件描述符可以通过打开 /proc/[pid]/ns/[namespace_type] 文件获取。namespace_type 表示命名空间的类型,如 pid、mnt、net、ipc、uts 等。
  • nstype:指定要加入的命名空间的类型。这个值应与 namespace_type 对应,例如 CLONE_NEWPID 对应 pidCLONE_NEWNS 对应 mntCLONE_NEWNET 对应 net 等。

setns()函数的返回值是一个整数,表示操作的成功与否。如果成功,则返回0;否则返回-1,并设置相应的错误码。

使用setns()函数可以将当前进程加入到已存在的命名空间中,从而共享命名空间中的资源和上下文。这在某些场景中非常有用,特别是在容器化技术中,可以使多个容器共享相同的命名空间。在 docker 中,使用 docker exec 命令在已经运行着的容器中执行新的命令就须要用到 setns() 函数。

2.3、unshare()函数

unshare()函数可以用于使进程脱离指定的命名空间。通过调用unshare()函数并指定相应的命名空间选项,可以将当前进程从指定的命名空间中分离出来,使其成为新命名空间的首个成员。

#define _GNU_SOURCE
#include <sched.h>

int unshare(int flags);

flags:指定要创建的命名空间的类型和相关选项。可以通过按位或运算符将多个选项组合在一起。常见的选项有:

  • CLONE_NEWPID:创建新的PID命名空间。
  • CLONE_NEWNET:创建新的网络命名空间。
  • CLONE_NEWNS:创建新的挂载命名空间。
  • CLONE_NEWIPC:创建新的IPC命名空间。
  • CLONE_NEWUTS:创建新的UTS命名空间(用于主机名和域名)。

unshare()函数的返回值是一个整数,表示操作的成功与否。如果成功,则返回0;否则返回-1,并设置相应的错误码。

使用unshare()函数可以在当前进程中创建新的命名空间,并将其作为首个成员。这使得进程可以在新的命名空间中拥有独立的资源和上下文。这在容器化和隔离技术中非常有用,可以实现进程的隔离和资源隔离。

需要注意的是,unshare()函数只能创建新的命名空间,而不能将进程加入到已存在的命名空间中。要将进程加入到已存在的命名空间中,可以使用 setns() 函数。

unshare命令
unshare命令用于创建新的命名空间,并使当前进程成为新命名空间的首个成员。

unshare [options] [command [arguments...]]

unshare命令可以带有一些选项和参数:

参数解释
-m--mount创建新的挂载命名空间。
-u--uts创建新的UTS命名空间。
-i--ipc创建新的IPC命名空间。
-n--net创建新的网络命名空间。
-p--pid创建新的PID命名空间。
-U--user创建新的用户命名空间。
-C--cgroup创建新的控制组命名空间。
-f--fork在创建新命名空间后,立即执行一个子命令(command)。
-r--map-root-user将用户命名空间中的root用户映射到当前用户。
-s--setgroups设置用户命名空间中的附加组。
-h--help显示帮助信息。

unshare命令用于创建新的命名空间,并使当前进程成为新命名空间的首个成员。可以通过选项来指定要创建的命名空间类型。在创建新命名空间后,还可以通过指定一个子命令来在新命名空间中执行特定的操作。

例如,执行以下命令将创建一个新的网络命名空间,并在该命名空间中执行bash命令:

$ unshare -n bash

# 查找新建命名空间
$ ps -ef | grep bash
root          1      0  0 15:47 pts/0    00:00:00 /bin/bash
root         38      0  0 15:47 pts/1    00:00:00 /bin/bash
root         84     38  0 15:48 pts/1    00:00:00 bash
root        139     84  0 15:49 pts/1    00:00:00 grep --color=auto bash

$ ls -l /proc/84/ns
total 0
lrwxrwxrwx 1 root root 0 Sep  4 15:49 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Sep  4 15:49 ipc -> ipc:[4026533180]
lrwxrwxrwx 1 root root 0 Sep  4 15:49 mnt -> mnt:[4026533178]
lrwxrwxrwx 1 root root 0 Sep  4 15:49 net -> net:[4026534452]
lrwxrwxrwx 1 root root 0 Sep  4 15:49 pid -> pid:[4026533181]
lrwxrwxrwx 1 root root 0 Sep  4 15:49 pid_for_children -> pid:[4026533181]
lrwxrwxrwx 1 root root 0 Sep  4 15:49 time -> time:[4026531834]
lrwxrwxrwx 1 root root 0 Sep  4 15:49 time_for_children -> time:[4026531834]
lrwxrwxrwx 1 root root 0 Sep  4 15:49 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Sep  4 15:49 uts -> uts:[4026533179]

执行该命令后,系统将创建一个新的网络命名空间,并将当前进程(包括子进程)置于该命名空间中。然后,将启动一个新的bash shell,并在新网络命名空间中执行。

通过使用unshare命令,可以方便地创建新的命名空间,并在新命名空间中执行特定的操作。这对于进行进程隔离、资源隔离以及容器化等任务非常有用。

三、namespace 分类

Namespace 类型系统调用参数PageIsolates内核版本
MountCLONE_NEWNSmount_namespacesMount points 隔离文件系统挂载点2.4.19
UTSCLONE_NEWUTSuts_namespacesHostname and NIS domain name 隔离主机名和域名信息2.6.19
IPCCLONE_NEWIPCipc_namespacesSystem V IPC,POSIX message queues 隔离进程间通信2.6.19
PIDCLONE_NEWPIDpid_namespacesProcess IDs 隔离进程的ID2.6.24
NetworkCLONE_NEWNETnetwork_namespacesNetwork devices,stacks, ports, etc. 隔离网络资源2.6.29
UserCLONE_NEWUSERuser_namespacesUser and group IDs 隔离用户和用户组的ID3.8
CgroupCLONE_NEWCGROUPcgroup_namespacesCgroup root directory4.6
TimeCLONE_NEWTIMEtime_namespacesBoot and monotonic clocks5.6

3.1、Mount Namespace

Mount Namespace用来隔离文件系统的挂载点,不同Mount Namesace的进程拥有不同的挂载点,同时也拥有了不同的文件系统视图。Mount Namespace是历史上第一个支持的Namespacemount Namespace为进程提供了一个文件层次视图,在Mount Namespace中调用mount()umount()仅仅只会影响当前Namespace内的文件系统,而对全局的文件系统是没有影响的。实际上,Mount Namespace是基于chroot的不断改良才被发明出来的,chroot可以算是 Linux 中第一个 Namespace。那么上面被挂载在容器根目录上、用来为容器镜像提供隔离后执行环境的文件系统,就是所谓的容器镜像,也被叫做 rootfs(根文件系统)

3.1.1、chroot介绍

chroot(Change Root)通过修改当前进程的根目录,来创建一个新的文件系统环境。下面是chroot的原理介绍:

chroot系统调用:chroot命令是通过chroot系统调用来实现的。chroot系统调用的原型如下:

int chroot(const char *path);

该系统调用将进程的根目录更改为指定的目录。注意,只有具有足够特权的用户可以使用chroot命令,通常需要使用root用户或者具有sudo权限的用户

修改根目录:chroot命令在执行时,会将指定的目录通过chroot系统调用传递给内核。内核会将当前进程的根目录更改为指定的目录,进程以这个目录作为新的根目录。

文件和目录访问:一旦chroot命令执行成功,进程以新的根目录作为基准进行文件和目录的访问。进程只能访问新的根目录及其子目录下的文件和目录,对于根目录以外的文件和目录则无法访问

隔离环境:使用chroot命令创建的新的文件系统环境是隔离的,进程在这个环境中运行时无法访问或修改主机系统的文件和目录。这样可以提供一定程度的安全性和隔离性,特别是在系统修复、软件开发和测试等场景中。

需要注意的是,chroot只是修改了进程的根目录,并不能完全隔离进程。进程仍然可以通过其他方式访问和修改主机系统的资源。

3.1.2、rootfs介绍

rootfs(根文件系统)是Linux文件系统的最顶层,包含了操作系统的基本文件和目录结构。在引导过程中,rootfs是最早被挂载的文件系统,它是系统启动的基础

rootfs通常以一个镜像文件或者一个设备文件的形式存在,它包含了操作系统所需的核心文件和目录,如/bin、/sbin、/etc等。这些文件和目录是构成系统的基础,包括系统初始化脚本、核心命令等。

在Linux系统中,rootfs是只读的,它由操作系统提供并在系统启动时挂载到根目录("/")。一旦系统启动,rootfs会被切换为可读写模式,此后可以通过写入其他文件系统来改变系统的状态。这种只读的rootfs设计可以保护系统的核心文件和目录,避免意外的修改和损坏。

在容器化技术中,每个容器都有自己的rootfs,容器的应用程序和依赖都存在于该文件系统中。容器可以通过挂载其他文件系统或目录来扩展其功能,但它们的根文件系统仍然是只读的,保证了容器的隔离性和安全性。

3.1.3、Mount Propagation挂载传播

挂载传播决定了这个挂载操作对于其他进程的可见性和影响。挂载传播定义了挂载对象(mount object)之间的关系,系统利用这些关系决定任何挂载对象中的挂载事件传播到其它挂载对象。所谓传播事件,就是一个挂载对象状态变化导致的其它挂载对象的挂载与解除挂载动作的事件。

  • 如果两个挂载对象具有共享关系(share relationship),那么一个挂载对象的挂载事件会传播到另一个挂载对象,反之亦然。
  • 如果两个挂载对象形成从属关系(master slave),那么一个挂载对象的挂载事件会传播到另一个挂载对象,但反之不行。在这种关系中,从属对象是事件的接受者。

一个挂载状态可以为如下的其中一种:

  • 共享状态(shared)
  • 从属状态(slave)
  • 共享/从属状态(shared and slave)
  • 私有挂载(private)
  • 不可绑定挂载(unbindable)

传播事件的挂载对象称为共享挂载(shared mount);接收传播事件的挂载对象称为从属挂载(slave mount)。既不传播也不接收传播事件的挂载对象称为私有挂载(private mount)。另一种特殊的挂载对象称为不可绑定的挂载(unbindable mount),它们与私有挂载相似,但是不允许执行绑定挂载,即创建 mount namespace 时这块文件对象不可被复制。
在这里插入图片描述

共享挂载的应用场景非常明显,就是为了文件数据的共享所必须的一种挂载方式;从属挂载更大的意义在于一些“只读”场景;私有挂载则是纯粹的隔离,作为独立个体存在;不可绑定挂载则有助于防止没必要的文件拷贝。

默认情况下,所有挂载都是私有的。从共享挂载克隆的挂载对象也是共享的挂载,它们互相传播挂载事件。
从属挂载克隆的挂载对象也是从属的挂载,它也从属于原来的从属挂载的主挂载对象。

mount --make-shared /mntS      # 将挂载点设置为共享关系属性
mount --make-private /mntP     # 将挂载点设置为私有关系属性
mount --make-slave /mntY       # 将挂载点设置为从属关系属性
mount --make-unbindable /mntU  # 将挂载点设置为不可绑定属性
3.1.4、挂载信息的查看

/proc/[pid]/mounts
文件中每行数据的含义为:

字段含义
挂载源(Source)表示文件系统的挂载点,可以是设备文件路径(如/dev/sda1)、网络路径(如//server/share)或特殊文件系统(如proc、sysfs)。
挂载点(Mount Point)表示挂载源被挂载到的目录路径。
文件系统类型(Filesystem Type)指示挂载的文件系统类型,如ext4、tmpfs、proc、sysfs等。
挂载选项(Mount Options)表示挂载时使用的选项,多个选项以逗号分隔。
使用的主设备号(Major Device Number)表示挂载设备的主设备号。仅适用于块设备文件。
使用的次设备号(Minor Device Number)表示挂载设备的次设备号。仅适用于块设备文件

/proc/[pid]/mounts文件的内容可以提供有关特定进程的挂载信息,帮助用户了解进程所使用的文件系统和挂载选项等信息。可以通过读取该文件来获取进程的挂载点和文件系统类型等相关信息。

需要注意的是,/proc/[pid]/mounts文件是只读的,只能用于查看挂载信息,不能修改其中的内容。
常见挂载选项:

选项含义
rw挂载为可读写的文件系统。可以在该文件系统上进行读取和写入操作。
ro挂载为只读的文件系统。只能在该文件系统上进行读取操作,不能进行写入操作。
relatime更新访问时间,但仅在访问时间超过修改时间时更新。相较于atime选项,relatime选项减少了对文件系统的访问次数,提高了性能。
async异步写入。文件系统将写入操作放入缓冲区,然后立即返回,而不等待写入操作完成。
sync同步写入。文件系统在执行写入操作时会等待写入操作完成,然后再返回。
noexec不允许在该文件系统上执行可执行文件。即禁止在该文件系统上运行程序。
nodev不允许在该文件系统上创建设备文件。即禁止在该文件系统上创建字符设备或块设备。
nosuid禁止设置setuid和setgid位。即禁止在该文件系统上设置可执行文件的setuid和setgid权限。
noatime不更新访问时间。即不会更新文件或目录的访问时间戳。
nodiratime不更新目录的访问时间。即只有文件的访问时间会被更新。
indexBtrfs文件系统中用于禁用或启用文件系统的索引功能
metacopyBtrfs文件系统中用于启用元数据拷贝功能

/proc/[pid]/mountinfo

字段含义
Mount ID挂载的ID号,用于唯一标识每个挂载点。
Parent ID父挂载点的ID号,表示当前挂载点的父级挂载点。
Major:Minor挂载的设备的主次设备号。
Root挂载点的根目录。
Mount Point设备或文件系统被挂载到的目标路径。
Mount Options挂载时使用的选项,多个选项以逗号分隔。
Optional Fields可选字段,可能包含一些附加的挂载信息,如安全标签(security label)、备份目录(backup directory)等。
Filesystem Type文件系统的类型,如ext4、tmpfs、proc等。
Mount Source设备或文件系统的源路径。
Super Options超级选项,用于指定与挂载点相关的额外选项。
Subtree Options子树选项,用于指定与挂载点子树相关的选项。

proc/[pid]/mountinfo文件提供了有关挂载点的详细信息,包括设备、文件系统类型、挂载路径、挂载选项等。通过读取该文件,可以了解系统中的挂载点及其相关信息,对于了解文件系统的结构和配置非常有用。

/proc/[pid]/mountstats

字段含义
device挂载设备的路径或标识符。
path挂载点的路径。
type文件系统类型。
mountinfo与挂载点相关的详细信息的文件路径。
mountsource挂载点的源路径。
superoptions超级选项,用于指定与挂载点相关的额外选项。
options挂载选项,用于指定挂载时使用的选项。
age挂载与卸载之间的时间差,以秒为单位。
opts挂载选项的统计信息。
mount_time挂载点的挂载时间,以纳秒为单位。
umount_time挂载点的卸载时间,以纳秒为单位。
num_mounts挂载点的总挂载次数。
num_mnt_errs挂载点的挂载错误次数。
num_mount_errors挂载点的严重错误次数。
num_mounts_succeed挂载点的成功挂载次数。
num_umounts挂载点的总卸载次数。
num_umount_errors挂载点的卸载错误次数。

/proc/[pid]/mountstats文件提供了有关挂载点的统计信息,包括挂载次数、挂载选项的使用情况、挂载时间等。通过读取该文件,可以了解挂载点的使用情况和性能统计,对于监控和调优文件系统的挂载点非常有用。

3.1.5、在docker中的应用

Docker 支持 Mount(挂载)命名空间是从早期版本开始的,确切地说是从 Docker 0.9 版本开始引入的。

在 Docker 中,通过 --mount-v 选项可以将主机上的一个目录挂载到容器中,或将一个数据卷挂载到容器中。这样,容器内的进程仅能看到容器内部的挂载点,而不会影响到宿主机或其他容器。

例如,可以运行以下命令创建一个具有独立 Mount 命名空间的容器,并将主机上的 /data 目录挂载到容器的 /mnt 目录:

docker run --name mycontainer --mount type=bind,source=/data,target=/mnt -it ubuntu /bin/bash

这样,在容器中访问 /mnt 目录时,实际上是访问主机上的 /data 目录。

需要注意的是,Mount 命名空间只对新创建的容器有效,并不会影响已经运行的容器。因此,在启动容器时,需要明确指定 --mount 或 -v 选项来设置挂载点。

3.2、UTS Namespaces

UTS(Unix Time-sharing System)用于隔离主机名和域名等与系统标识相关的信息。每个UTS命名空间都有自己的主机名和域名,使得进程可以在不同的UTS命名空间中有不同的系统标识。以下是UTS Namespaces的几个关键特性:
主机名隔离:UTS命名空间允许每个命名空间拥有自己独立的主机名。这意味着在不同的UTS命名空间中运行的进程可以有不同的主机名,使得它们看起来像在不同的系统中运行。

域名隔离:UTS命名空间还允许每个命名空间拥有自己独立的域名。这对于网络隔离和多租户环境非常有用。每个命名空间可以具有自己的域名,使得命名空间之间的网络通信更加独立和安全。

进程标识隔离:UTS命名空间不仅隔离了主机名和域名,还隔离了进程的PID和进程组ID。这意味着在不同的UTS命名空间中运行的进程可以拥有不同的PID和进程组ID,避免与其他命名空间或宿主机中的进程冲突。

命名空间间隔离:UTS命名空间是Linux中的一种命名空间类型,它与其他命名空间(如PID命名空间、网络命名空间等)相互独立。这意味着在UTS命名空间中的更改不会影响其他命名空间,也不会影响宿主机上的进程。

容器化支持:UTS命名空间是容器化平台(如Docker、Kubernetes等)中的重要组成部分之一。通过为每个容器创建独立的UTS命名空间,可以为每个容器设置不同的主机名和域名,使得容器在网络中可以像独立的虚拟机一样运行。

3.2.1、主机名(hostname)

在 Linux 系统中,主机名(hostname)是用于标识和识别计算机的名称。它是一个字符串,用于在网络中唯一标识设备或节点。

主机名通常由两部分组成:主机名和域名。主机名是计算机在局域网内的唯一标识符,而域名是用于在更大的网络环境中进行全局唯一标识的部分。例如,对于主机名 mycomputer 和域名 example.com,完整的主机名可以是 mycomputer.example.com

主机名在 Linux 中有重要的作用:

  • 识别计算机:主机名可以帮助用户和管理员识别和区分不同的计算机。
  • 网络通信:主机名用于标识计算机在网络上的身份,例如在局域网内进行通信和访问。
  • 系统配置:主机名在系统配置中起到重要作用,例如在配置文件中引用和识别计算机。

在 Linux 中,你可以使用以下命令来查看和设置主机名:

  • hostname:用于查看计算机的当前主机名。
    $ hostname
    mycomputer
    
  • hostnamectl:用于查看和设置主机名的详细信息。在一些最新的 Linux 发行版中,hostnamectl 命令提供了更多的主机名管理选项,包括设置静态主机名和临时主机名等。
    $ hostnamectl
     Static hostname: mycomputer
     Transient hostname: temporary-hostname
           Icon name: computer-desktop
             Chassis: desktop
          Machine ID: 1234567890abcdef1234567890abcdef
             Boot ID: abcdef12-3456-7890-abcd-ef1234567890
    Operating System: Ubuntu 20.04.1 LTS
              Kernel: Linux 5.4.0-52-generic
        Architecture: x86_64
          Virtualization: kvm
    

要设置主机名,你可以使用以下命令:

hostnamectl set-hostname [new_hostname]  # 设置永久的主机名。
hostname [new_hostname] # 设置临时的主机名,重新启动后会重置为默认值。

需要注意的是,更改主机名可能需要 root 或者管理员权限。在更改主机名之后,你需要重启或者重新加载网络服务以使更改生效。

请注意,主机名不应该包含特殊字符、空格或标点符号,并且应该遵循特定的命名约定和规则。常见的命名约定包括使用小写字母、数字、连字符 “-” 等。确保为主机名选择有意义的名称以提高可读性和管理性。

3.2.2、域名(domain)

在Linux中,域名(Domain Name)是用来在互联网上唯一标识和定位计算机或服务器的名称。域名通过域名系统(Domain Name System,DNS)进行解析,将域名转换为对应的IP地址。

域名由多个部分组成,以点号分隔。从右向左,域名的结构依次为顶级域名(Top-Level Domain,TLD)、二级域名(Second-Level Domain,SLD)和子域名(Subdomain)。

顶级域名是域名结构中的最高级别,通常表示国家、地区或特定的组织类型。常见的顶级域名包括.com、.org、.net、.edu、.gov、.cn等。

二级域名是在顶级域名之下的第二个层级,它通常表示特定的组织、公司、机构或网站。例如,对于域名example.com"example"就是二级域名。

子域名是在二级域名之下的更低层级,用于进一步细分和组织网站结构。例如,www.example.com中的"www"就是一个子域名。

在Linux中,域名的使用与网络通信密切相关。通过域名,用户可以通过浏览器访问网站、发送电子邮件、执行远程连接等。

要使用域名进行网络通信,需要进行域名解析。域名解析将域名转换为对应的IP地址,以便计算机能够通过IP地址找到目标主机并建立连接。域名解析可以通过本地的 /etc/hosts 文件进行,也可以通过配置DNS服务器来实现。

域名的选择和注册需要通过域名注册商进行。通常,你需要选择一个独特且易于记忆的域名,并确保该域名尚未被他人注册。

需要注意的是,在Linux系统中,域名通常与主机名(hostname)相关联。主机名用于在局域网内标识计算机,而域名则用于在更大的网络环境中全局唯一标识计算机。两者共同构成完整的主机域名(Fully Qualified Domain Name,FQDN)。

3.2.3、DNS

DNS(Domain Name System)是互联网中的一种分布式命名系统,它用于将域名转换为对应的IP地址,并支持其他与域名相关的信息。

DNS 的主要功能是将人类可读的域名转换为计算机可理解的IP地址。在进行网络通信时,计算机需要知道目标主机的IP地址才能建立连接。而域名作为人类友好的标识,使我们能够轻松地记住和使用网站的名称。

DNS 由多个层级组成,包括根域名服务器、顶级域名服务器、权威域名服务器和本地域名服务器。这些服务器共同构成了一个分布式的系统,用于处理域名解析请求。

当我们在浏览器中输入一个域名时,本地域名服务器首先会查询自己的缓存,如果缓存中没有对应的IP地址,则本地域名服务器将向根域名服务器发送查询请求。根域名服务器返回顶级域名服务器的地址,然后本地域名服务器向顶级域名服务器发送查询请求。顶级域名服务器返回权威域名服务器的地址,最终本地域名服务器向权威域名服务器发送查询请求,并获取到该域名对应的IP地址。本地域名服务器将IP地址返回给计算机,使其能够建立连接。

除了域名解析,DNS 还支持其他功能,例如:

  • 反向解析:将IP地址转换为对应的域名。
  • 域名注册:在域名注册商处注册可用的域名。
  • 邮件交换记录(MX 记录):指定处理域名相关的电子邮件的邮件服务器。
  • 别名记录(CNAME 记录):为一个域名指定另一个域名作为其别名。
  • IP 地址分配(DHCP):通过 DNS 服务器分配 IP 地址给网络中的设备。

DNS 在互联网中起着至关重要的作用,使我们能够轻松地使用域名进行网络通信和访问。它提供了一个高效和可靠的机制来解析和管理域名。

3.2.4、/proc/sys/kernel/hostname/etc/hostname区别

/proc/sys/kernel/hostname:这个文件是一个虚拟文件,它提供了当前系统的主机名。可以通过读取这个文件来获取当前系统的主机名。同时,也可以通过将新的主机名写入这个文件来更改系统的主机名。然而,这种更改只是临时的,不会在系统重启后保持。当系统重新启动时,将会从其他地方(如 /etc/hostname)读取主机名。

/etc/hostname:这个文件是保存系统永久主机名的配置文件。在系统启动时,会从这个文件中读取主机名,并将其设置为系统的主机名。这个文件中的主机名是永久保存的,即使系统重新启动,主机名也会保持不变。如果需要更改系统的主机名,可以通过编辑这个文件,并将新的主机名保存在其中。

通常情况下,建议使用 /etc/hostname 文件来配置系统的主机名,因为它提供了永久性的设定。而 /proc/sys/kernel/hostname 文件则可以用来临时更改主机名,但这些更改在系统重启后会失效。

3.2.5、在docker中的应用

Docker 支持 UTS(Unix Timesharing System)命名空间是从早期版本开始的,准确来说是从 Docker 0.9 版本开始引入的。

在 Docker 中,通过 --hostname 选项可以设置容器的主机名。例如,可以运行以下命令创建一个具有独立 UTS 命名空间的容器,并设置其主机名为 “mycontainer”:

$ docker run --name mycontainer --hostname mycomputer -it ubuntu /bin/bash

[root@mycomputer /]
$ hostname
mycomputer

这样,在容器中执行 hostname 命令将显示为 “mycomputer”,而不会影响宿主机或其他容器的主机名。

需要注意的是,UTS 命名空间只对新创建的容器有效,并不会影响已经运行的容器。因此,在启动容器时,需要明确指定 --hostname 选项来设置容器的主机名。

总结起来,Docker 从早期版本开始就支持 UTS 命名空间,通过 --hostname 选项可以设置容器的主机名,实现独立的 UTS 命名空间。

3.3、IPC Namespaces

IPC(Inter-Process Communication)命名空间用于隔离进程间通信资源。IPC命名空间允许每个命名空间内的进程拥有独立的IPC资源,如消息队列、共享内存和信号量。

IPC命名空间通过将进程组织在不同的命名空间中来实现隔离,每个命名空间都有自己的IPC资源集合。这意味着在不同的IPC命名空间中运行的进程无法直接访问其他命名空间中的IPC资源,从而提供了一定程度的隔离性和安全性。

使用IPC命名空间可以实现以下特性:

  • 独立的消息队列:每个IPC命名空间内的进程可以创建和使用独立的消息队列,这样在不同命名空间中的进程可以通过消息队列进行进程间通信(IPC)。

  • 独立的共享内存:每个IPC命名空间内的进程可以创建和使用独立的共享内存区域,这样在不同命名空间中的进程可以共享数据而不会与其他命名空间冲突。

  • 独立的信号量:每个IPC命名空间内的进程可以拥有独立的信号量集合,这样在不同命名空间中的进程可以使用信号量进行同步操作而不会受到其他命名空间的干扰。

IPC命名空间在容器化和虚拟化环境中特别有用,可以实现不同容器或虚拟机之间的隔离性,使它们可以独立地进行进程间通信,而不会干扰其他容器或虚拟机中的进程。这种隔离性有助于提高安全性、性能和资源管理。

需要注意的是,IPC命名空间的隔离性只对新创建的容器有效,并不会影响已经运行的容器。因此,在启动容器时,需要明确指定–ipc选项来控制IPC命名空间的隔离性。
IPC命名空间的隔离性只对新创建的进程和其子进程生效,并不会影响已经存在的进程。因此,在启动容器或虚拟机时,通常会使用相关工具(如Docker、Podman等)来创建新的IPC命名空间,并将进程置于新的命名空间中。这样可以确保容器或虚拟机内的进程能够享有独立的IPC资源。

3.3.1、在docker中的应用

Docker支持IPC(Inter-Process Communication)命名空间是从Docker 1.10版本开始引入的。在这个版本之前,Docker默认是共享主机的IPC命名空间的,即容器与主机使用相同的IPC资源。

从Docker 1.10版本开始,可以使用--ipc选项来控制IPC命名空间的隔离性。具体来说,可以在运行容器时使用以下选项来配置IPC命名空间的设置:

  • --ipc=host:使用主机的IPC命名空间,与主机共享IPC资源。这是默认的设置。
  • --ipc=container:<container_id或container_name>:与指定容器共享IPC命名空间,容器之间可以共享IPC资源。
  • --ipc=private:创建一个新的独立的IPC命名空间,容器之间的IPC资源是隔离的。例如,可以运行以下命令创建一个新的容器,并将其放置在独立的IPC命名空间中:
    docker run --name mycontainer --ipc=private -it ubuntu /bin/bash
    
    这样,该容器将拥有自己独立的IPC资源,与其他容器和主机的IPC资源隔离开来。

需要注意的是,IPC命名空间的隔离性只对新创建的容器有效,并不会影响已经运行的容器。因此,在启动容器时,需要明确指定–ipc选项来控制IPC命名空间的隔离性。

3.4、PID Namespaces

PID(Process ID)命名空间用于隔离进程ID空间。每个进程在PID命名空间中都有唯一的PID标识符,使进程在不同的命名空间中看起来像是具有不同的PID。
PID namespace 隔离非常实用,它对进程 PID 重新标号,即两个不同 namespace 下的进程可以有同一个 PID。每个 PID namespace 都有自己的计数程序。内核为所有的 PID namespace 维护了一个树状结构,最顶层的是系统初始时创建的,我们称之为 root namespace。他创建的新 PID namespace 就称之为 child namespace(树的子节点),而原先的 PID namespace 就是新创建的 PID namespace 的 parent namespace(树的父节点)。通过这种方式,不同的 PID namespaces 会形成一个等级体系。所属的父节点可以看到子节点中的进程,并可以通过信号等方式对子节点中的进程产生影响。反过来,子节点不能看到父节点 PID namespace 中的任何内容
类似于如下:

$ pstree -p 68131
nginx(68131)─┬─nginx(68132)
             ├─nginx(68133)
             ├─nginx(68134)
             ├─nginx(68135)
             ├─nginx(68136)
             ├─nginx(68137)
             ├─nginx(68139)
             └─nginx(68140)

每个 PID namespace 中的第一个进程“PID 1“,都会像传统 Linux 中的init进程一样拥有特权,起特殊作用。

一个 namespace 中的进程,不可能通过 kill 或 ptrace 影响父节点或者兄弟节点中的进程,因为其他节点的 PID 在这个 namespace 中没有任何意义。
如果你在新的 PID namespace 中重新挂载 /proc 文件系统,会发现其下只显示同属一个 PID namespace 中的其他进程。
在 root namespace 中可以看到所有的进程,并且递归包含所有子节点中的进程。
到这里,可能你已经联想到一种在外部监控 Docker 中运行程序的方法了,就是监控 Docker Daemon 所在的 PID namespace 下的所有进程即其子进程,再进行筛选即可。

3.4.1、获取容器pid的方法

docker inspect
如下:

$ docker inspect -f '{{.State.Pid}}' dc2efaef50ad
79272

docker top
如下:

$ docker top dc2efaef50ad
UID     PID     PPID    C       STIME     TTY     TIME      CMD
root    79272   79193   0       Jul27     ?       00:00:00  /pause

PPID 为PID的父进程,可以通过pstree -p查看树状结构

$ pstree -p 79193  
containerd-shim(79193)─┬─pause(79272)
                       ├─{containerd-shim}(79195)
                       ├─{containerd-shim}(79196)
                       ├─{containerd-shim}(79197)
                       ├─{containerd-shim}(79202)
                       ├─{containerd-shim}(79203)
                       ├─{containerd-shim}(79204)
                       ├─{containerd-shim}(79205)
                       └─{containerd-shim}(80654)

ps -fp 知道PID情况下查看详情的办法

$ ps -fp 79272
UID         PID   PPID  C STIME TTY          TIME CMD
root      79272  79193  0 Jul27 ?        00:00:00 /pause
3.4.2、PPID 为0的情况

在大多数操作系统中,PPID(Parent Process ID)为0表示该进程的父进程已经终止或不可用。进程的PPID为0通常有以下两种情况:

  • 孤儿进程:当一个进程的父进程终止或异常退出时,操作系统会将该进程的PPID设置为0,即使在终止前它可能有一个有效的父进程。这样的进程被称为孤儿进程,因为它失去了父进程的管理和控制。孤儿进程通常由init进程(PID为1)接管,并成为它的子进程。

  • 守护进程:守护进程是一种在后台运行的进程,通常是由操作系统启动并没有一个交互式的终端。守护进程的PPID通常为0,因为它们没有一个常驻的父进程。它们被设计为在系统启动时自动启动,并在后台执行系统任务或服务。

需要注意的是,PPID为0并不意味着进程没有父进程,而是表示其父进程无效或不可用。这是操作系统中的一种特殊情况,允许系统管理孤儿进程或守护进程的生命周期。

3.4.3、PID namespace 中的 init 进程

在PID(Process ID)命名空间中,每个命名空间都有一个独立的init进程。init进程是PID命名空间中的第一个进程,其PID为1。

在Linux中,init进程负责启动和管理系统中的所有其他进程。它是系统的第一个用户空间进程,是整个进程树的根。其他所有进程都是由init进程直接或间接创建的。

在PID命名空间中,每个命名空间都有自己的init进程。每个init进程只负责其所在命名空间中的进程管理,而不会对其他命名空间中的init进程产生影响。

PID命名空间的init进程具有以下特点:

  • PID为1:在每个PID命名空间中,init进程的PID都是1,是进程树的根节点。

  • 管理子进程:init进程负责管理和监控其所在命名空间中的其他进程。它会接收子进程的退出状态,并根据需要启动、停止或重新启动其他进程。

  • 接管孤儿进程:如果其他进程的父进程终止或异常退出,init进程会接管这些孤儿进程,并成为它们的新父进程。

PID namespace 维护这样一个树状结构,非常有利于系统的资源监控与回收。Docker 启动时,第一个进程也是这样,实现了进程监控和资源回收,它就是 dockerinit。

3.4.4、PID namespace 中的init 进程与信号

PID namespace 中的 init 进程如此特殊,自然内核也为他赋予了特权——信号屏蔽。如果 init 中没有写处理某个信号的代码逻辑,那么与 init 在同一个 PID namespace 下的进程(即使有超级权限)发送给它的该信号都会被屏蔽。这个功能的主要作用是防止 init 进程被误杀。
那么其父节点 PID namespace 中的进程发送同样的信号会被忽略吗?父节点中的进程发送的信号,如果不是 SIGKILL(销毁进程)或 SIGSTOP(暂停进程)也会被忽略。但如果发送 SIGKILL 或 SIGSTOP,子节点的 init 会强制执行(无法通过代码捕捉进行特殊处理),也就是说父节点中的进程有权终止子节点中的进程。
一旦 init 进程被销毁,同一 PID namespace 中的其他进程也会随之接收到 SIGKILL 信号而被销毁。理论上,该 PID namespace 自然也就不复存在了。但是如果 /proc/[pid]/ns/pid 处于被挂载或者打开状态,namespace 就会被保留下来。然而,保留下来的 namespace 无法通过 setns() 或者 fork() 创建进程,所以实际上并没有什么作用。
Docker 一旦启动就有进程在运行,不存在不包含任何进程的 Docker。

3.4.5、dockerinit 进程

dockerinit进程是Docker容器内的初始化进程。它是在Docker容器启动时作为容器的第一个进程运行的。

在Docker中,容器内的初始化进程负责完成一些初始化工作,例如设置容器的环境变量、启动服务进程等。dockerinit进程的主要功能包括:

  • 设置容器的PID命名空间:dockerinit进程会创建一个新的PID命名空间,并成为该命名空间中的第一个进程(PID为1),作为整个容器的初始化进程树的根。

  • 处理容器的生命周期:dockerinit进程会监控容器内其他进程的状态,例如检测容器内的主要进程是否终止,并在必要时重新启动它们。它还会处理容器的终止信号,并确保容器正常退出。

  • 执行容器的启动脚本或初始化命令:dockerinit进程在启动时会执行容器的启动脚本或初始化命令,以完成容器的初始化配置。这可能包括设置环境变量、启动服务进程、挂载数据卷等操作。

需要注意的是,dockerinit进程只在Docker容器内部存在,它是由Docker引擎自动创建和管理的,对于用户来说一般不需要直接关注它。它在容器启动时自动运行,并在容器终止时自动退出。

3.4.6、 PID namespace 中的 unshare()setns()

unshare()setns() 这两个 API在 PID namespace 中使用时,也有一些特别之处需要注意。
unshare() 允许用户在原有进程中建立 namespace 进行隔离。但是创建了 PID namespace 后,原先 unshare() 调用者进程并不进入新的 PID namespace,接下来创建的子进程才会进入新的 namespace,这个子进程也就随之成为新 namespace 中的 init 进程。
类似的,调用 setns() 创建新 PID namespace 时,调用者进程也不进入新的 PID namespace,而是随后创建的子进程进入。

为什么创建其他 namespace 时 unshare()setns() 会直接进入新的 namespace 而唯独 PID namespace 不是如此呢?因为调用 getpid() 函数得到的 PID 是根据调用者所在的 PID namespace 而决定返回哪个 PID,进入新的 PID namespace 会导致 PID 产生变化。而对用户态的程序和库函数来说,他们都认为进程的 PID 是一个常量,PID 的变化会引起这些进程奔溃。
换句话说,一旦程序进程创建以后,那么它的 PID namespace 的关系就确定下来了,进程不会变更他们对应的 PID namespace。

3.4.7、在docker中使用

Docker 支持 PID(Process ID)命名空间是从较早的版本开始的,准确来说是从 Docker 0.9 版本开始引入的。

在 Docker 中,通过 --pid 选项可以设置容器的 PID 命名空间。例如,可以运行以下命令创建一个具有独立 PID 命名空间的容器:

docker run --name mycontainer --pid=container:container_id -it ubuntu /bin/bash

这样,在容器中运行的进程将只能看到容器内部的进程,而无法看到宿主机或其他容器的进程。

需要注意的是,PID 命名空间只对新创建的容器有效,并不会影响已经运行的容器。因此,在启动容器时,需要明确指定 --pid 选项来设置容器的 PID 命名空间。

--pid 选项可以接受以下几种参数:

  • host: 使用宿主机的 PID 命名空间,即与宿主机共享相同的进程视图。
  • container:container_id: 使用另一个容器的 PID 命名空间,即与指定容器共享相同的进程视图。

以下是使用–pid选项的示例:

使用宿主机的 PID 命名空间:

docker run --name mycontainer --pid=host -it ubuntu /bin/bash

使用另一个容器的 PID 命名空间:

docker run --name mycontainer --pid=container:container_id -it ubuntu /bin/bash

需要注意的是,使用--pid选项会对容器内的进程可见性产生影响。在使用--pid=host时,容器内的进程和宿主机共享同一套进程 ID,对于容器内的进程来说,宿主机上的进程就像是自己的进程一样可见。而使用--pid=container:container_id时,容器内的进程将与指定容器共享同一套进程 ID,可以看到相同 PID 命名空间中的进程。

3.5、Network Namespace

Network namespace 主要提供了关于网络资源的隔离,包括网络设备、IPv4 和 IPv6 协议栈、IP 路由表、防火墙、/proc/net 目录、/sys/class/net 目录、端口(socket)等等。一个物理的网络设备最多存在在一个 network namespace 中,你可以通过创建 veth pair(虚拟网络设备对:有两端,类似管道,如果数据从一端传入另一端也能接收到,反之亦然)在不同的 network namespace 间创建通道,以此达到通信的目的。

一般情况下,物理网络设备都分配在最初的 root namespace(表示系统默认的 namespace,在 PID namespace 中已经提及)中。但是如果你有多块物理网卡,也可以把其中一块或多块分配给新创建的 network namespace。需要注意的是,当新创建的 network namespace 被释放时(所有内部的进程都终止并且 namespace 文件没有被挂载或打开),在这个 namespace 中的物理网卡会返回到 root namespace 而非创建该进程的父进程所在的 network namespace。

当我们说到 network namespace 时,其实我们指的未必是真正的网络隔离,而是把网络独立出来,给外部用户一种透明的感觉,仿佛跟另外一个网络实体在进行通信。为了达到这个目的,容器的经典做法就是创建一个 veth pair,一端放置在新的 namespace 中,通常命名为 eth0,一端放在原先的 namespace 中连接物理网络设备,再通过网桥把别的设备连接进来或者进行路由转发,以此网络实现通信的目的。

在建立起 veth pair 之前,新旧 namespace 该如何通信呢?答案是 pipe(管道)。我们以 Docker Daemon 在启动容器 dockerinit 的过程为例。Docker Daemon 在宿主机上负责创建这个 veth pair,通过 netlink 调用,把一端绑定到 docker0 网桥上,一端连进新建的 network namespace 进程中。建立的过程中,Docker Daemon 和 dockerinit 就通过 pipe 进行通信,当 Docker Daemon 完成 veth-pair 的创建之前,dockerinit 在管道的另一端循环等待,直到管道另一端传来 Docker Daemon 关于 veth 设备的信息,并关闭管道。dockerinit 才结束等待的过程,并把它的eth0启动起来。整个效果类似下图所示。
在这里插入图片描述
在这里插入图片描述

3.5.1、Network Namespace的特点和用途
  • 隔离网络栈:每个网络命名空间内都有自己独立的网络栈。这意味着在不同的命名空间中,可以有不同的网络设备、IP地址、路由表和网络配置。每个命名空间中的进程只能看到本命名空间内的网络设备和配置,无法直接访问其他命名空间的网络资源。

  • 虚拟网络设备:每个网络命名空间都有自己的虚拟网络设备,如虚拟网卡(veth pair)。虚拟网卡成对出现,一端连接到当前命名空间中的网络栈,另一端连接到其他命名空间或宿主机的网络栈,实现网络通信。

  • 路由和网络配置:每个网络命名空间都有自己的路由表和网络配置。这意味着在不同的命名空间中,可以有不同的网络路由策略和网络配置参数,如IP地址、子网掩码、网关等。

  • 命名空间的继承:在Linux中,进程创建子进程时,子进程会继承父进程的命名空间。因此,子进程将与父进程共享相同的网络命名空间,除非在子进程中显式地创建一个新的网络命名空间。

3.5.2、Network Namespace网络建立与配置

创建一个 test_ns 的Network Namespace。

ip netns add test_ns

当 ip 命令工具创建一个 network namespace 时,会默认创建一个回环设备(loopback interface:lo),并在 /var/run/netns 目录下绑定一个挂载点,这就保证了就算 network namespace 中没有进程在运行也不会被释放,也给系统管理员对新创建的 network namespace 进行配置提供了充足的时间。

$ ll /var/run/netns
-r--r--r-- 1 root root 0 Sep 12 14:11 test_ns

通过 ip netns exec 命令可以在新创建的 network namespace 下运行网络管理命令。

$ ip netns exec test_ns ip link list
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

上面的命令为我们展示了新建的 namespace 下可见的网络链接,可以看到状态是 DOWN, 需要再通过命令去启动。可以看到,此时执行 ping 命令是无效的。

$ ip netns exec test_ns ping 127.0.0.1
connect: Network is unreachable

启动命令如下,可以看到启动后再测试就可以 ping 通

$ ip netns exec test_ns ip link set dev lo up

$ ip netns exec test_ns ping 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.015 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.017 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.015 ms
64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.016 ms
64 bytes from 127.0.0.1: icmp_seq=5 ttl=64 time=0.016 ms
64 bytes from 127.0.0.1: icmp_seq=6 ttl=64 time=0.015 ms
^C
--- 127.0.0.1 ping statistics ---
6 packets transmitted, 6 received, 0% packet loss, time 5149ms
rtt min/avg/max/mdev = 0.015/0.015/0.017/0.004 ms

这样只是启动了本地的回环,要实现与外部 namespace 进行通信还需要再建一个网络设备对,命令如下:

$ ip link add veth0 type veth peer name veth1
$ ip link set veth1 netns test_ns
$ ip netns exec test_ns ifconfig veth1 10.1.1.1/24 up
$ ifconfig veth0 10.1.1.2/24 up
  • 第一条命令创建了一个网络设备对,所有发送到 veth0 的包 veth1 也能接收到,反之亦然。
  • 第二条命令则是把 veth1 这一端分配到 test_ns 这个 network namespace。
  • 第三、第四条命令分别给 test_ns 内部和外部的网络设备配置 IP,veth1 的 IP 为 10.1.1.1,veth0 的 IP 为 10.1.1.2。

此时两边就可以互相连通了,效果如下:

$ ping 10.1.1.1
PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.
64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=0.033 ms
64 bytes from 10.1.1.1: icmp_seq=2 ttl=64 time=0.025 ms
^C
--- 10.1.1.1 ping statistics ---
11 packets transmitted, 11 received, 0% packet loss, time 10222ms
rtt min/avg/max/mdev = 0.019/0.025/0.039/0.008 ms

$ ip netns exec test_ns ping 10.1.1.2
PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.
64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.022 ms
64 bytes from 10.1.1.2: icmp_seq=2 ttl=64 time=0.024 ms
^C
--- 10.1.1.2 ping statistics ---
6 packets transmitted, 6 received, 0% packet loss, time 5146ms
rtt min/avg/max/mdev = 0.022/0.025/0.028/0.002 ms

可以通过下面的命令查看,新的 test_ns 有着自己独立的路由和 iptables

$ ip netns exec test_ns route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
10.1.1.0        0.0.0.0         255.255.255.0   U     0      0        0 veth1

$ ip netns exec test_ns iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination        

路由表中只有一条通向 10.1.1.2 的规则,此时如果要连接外网肯定是不可能的,你可以通过建立网桥或者 NAT 映射来决定这个问题。
可以通过下面的命令删除这个 network namespace:

$ ip netns delete test_ns

这条命令会移除之前的挂载,但是如果 namespace 本身还有进程运行,namespace 还会存在下去,直到进程运行结束。
通过 network namespace 我们可以了解到,实际上内核创建了 network namespace 以后,真的是得到了一个被隔离的网络。但是我们实际上需要的不是这种完全的隔离,而是一个对用户来说透明独立的网络实体,我们需要与这个实体通信。

3.5.3、在docker中的应用

Docker 从较早的版本开始就支持 Network Namespace(网络命名空间)。准确来说,从 Docker 0.9 版本开始引入了对网络命名空间的支持。

在 Docker 中,每个容器都有自己的独立网络命名空间。当创建一个容器时,Docker 会为该容器创建一个网络命名空间,并在该命名空间中配置容器的网络设备、IP地址、路由表等网络资源。这样,每个容器就可以拥有独立和隔离的网络环境,与其他容器和宿主机的网络是隔离开的。

Docker 提供了一套网络驱动程序,用于管理容器的网络连接、网络配置和网络隔离。不同的网络驱动程序可以实现不同的网络隔离和连接方式,例如 bridge、overlay、host、macvlan 等。

通过 Network Namespace 的支持,Docker 可以为容器提供独立和隔离的网络环境,使容器内的进程能够与其他容器或宿主机的进程进行网络通信,同时保持网络的隔离和安全性。

docker run --network 选项用于指定容器的网络模式。通过该选项,可以控制容器与其他容器或宿主机之间的网络连接方式和隔离级别。

--network 选项可以接受以下几种参数:

  • bridge(默认值): 使用 Docker 的默认网络模式,即创建一个新的桥接网络,并将容器连接到该网络中。这个网络会自动在宿主机上创建一个网桥,容器可以通过该网桥与其他容器和宿主机进行通信。
  • host: 使用宿主机的网络命名空间,即与宿主机共享网络栈。容器将直接使用宿主机的网络接口,与宿主机拥有相同的 IP 地址和网络配置,容器与宿主机共享网络环境,网络性能较高。
  • none: 容器不连接到任何网络。这意味着容器无法进行网络通信,只能通过进程间通信(IPC)或其他手段与宿主机或其他容器进行通信。
  • container:container_name_or_id: 使用另一个容器的网络命名空间,即与指定容器共享网络栈。容器将与指定容器使用相同的网络配置,包括 IP 地址和网络设备。

以下是使用--network选项的示例:

使用默认的桥接网络模式:

docker run --name mycontainer --network=bridge -it ubuntu /bin/bash

使用宿主机网络模式:

docker run --name mycontainer --network=host -it ubuntu /bin/bash

不连接到任何网络:

docker run --name mycontainer --network=none -it ubuntu /bin/bash

使用其他容器的网络模式:

docker run --name mycontainer --network=container:container_name_or_id -it ubuntu /bin/bash

需要注意的是,使用--network选项会对容器的网络配置和可见性产生影响。不同的网络模式可以实现不同的网络隔离和连接方式,适用于不同的使用场景。

3.6、User Namespaces

用户命名空间(User Namespace)用于隔离用户和用户组的视图。它允许在一个命名空间中重新映射用户和用户组的ID,从而实现用户和用户组的隔离和管理。User namespace 主要隔离了安全相关的标识符(identifiers)和属性(attributes),包括用户ID、用户组ID、root目录、 key(指密钥)以及特殊权限。说得通俗一点,一个普通用户的进程通过clone() 创建的新进程在新user namespace 中可以拥有不同的用户和用户组。这意味着一个进程在容器外属于一个没有特权的普通用户,但是他创建的容器进程却属于拥有所有权限的超级用户,这个技术为容器提供了极大的自由。用户命名空间的主要目的是提供更安全的环境,使不同用户在同一系统上运行时能够相互隔离,避免权限冲突和攻击。以下是用户命名空间的一些重要概念和特性:

用户ID映射:用户命名空间允许重新映射用户ID(UID)和组ID(GID)到不同的值,称为用户ID映射。这样做可以在命名空间中创建和管理独立的用户和用户组。例如,一个用户在一个用户命名空间中可能有ID为0的特权用户,但在全局命名空间中却没有这个特权。

用户权限隔离:用户命名空间隔离了用户和用户组的权限,使得在不同的命名空间中用户的权限不会影响其他命名空间的用户。这提供了更好的安全性和隔离性,防止不同用户之间的权限冲突,以及减少攻击面。

文件系统隔离:用户命名空间还可以隔离文件系统的视图,使得在不同命名空间中的用户可以有独立的文件系统,并且对文件和目录的访问权限不会相互干扰。

容器化和虚拟化:用户命名空间是实现容器化和虚拟化的关键组成部分。它允许不同的容器或虚拟机在同一主机上运行,每个容器或虚拟机都有自己独立的用户和用户组。

3.6.1、查看user ns方法:
$ lsns -t user
        NS TYPE  NPROCS PID USER COMMAND
4026531837 user     263   1 root /usr/lib/systemd/systemd --system --deserialize 17

$ ls /proc/1/ns/user 
/proc/1/ns/user

$ ll /proc/1/ns/user 
lrwxrwxrwx 1 root root 0 Sep  1 10:21 /proc/1/ns/user -> user:[4026531837]

$ readlink /proc/1/ns/user 
user:[4026531837]
3.6.2、User Namespaces的创建和销毁
# 使用unshare -U /bin/bash 创建新的shell会话
$ lsns -t user
        NS TYPE  NPROCS PID USER COMMAND
4026531837 user       3   1 root /bin/bash

[root@ce31e508d31c /]
$ unshare -U sh
whoami: cannot find name for user ID 65534

[I have no name!@ce31e508d31c /]
$ sudo ip netns add test
sudo: /etc/sudo.conf is owned by uid 65534, should be 0
sudo: /usr/bin/sudo must be owned by uid 0 and have the setuid bit set
whoami: cannot find name for user ID 65534

[I have no name!@ce31e508d31c /]
$ lsns -t user
        NS TYPE  NPROCS   PID USER  COMMAND
4026534745 user       2   155 65534 sh
whoami: cannot find name for user ID 65534

具有足够权限的用户(如root用户)才能销毁用户命名空间。

# root用户下查看
$ lsns -t user
        NS TYPE  NPROCS   PID USER COMMAND
4026531837 user     261     1 root /usr/lib/systemd/systemd --system --deserialize 17
4026534745 user       1   613 root sh

$ kill -9 613

$ lsns -t user
        NS TYPE  NPROCS PID USER COMMAND
4026531837 user     263   1 root /usr/lib/systemd/systemd --system --deserialize 17
3.6.3、在docker中使用

docker 支持 USER(用户)命名空间是从较新的版本开始的,准确地说是从 Docker 1.10 版本开始引入的。

在 Docker 中,通过 --user 选项可以设置容器内进程的用户和组。例如,可以运行以下命令创建一个具有独立 USER 命名空间的容器,并将容器内的进程配置为以 UID 1000 和 GID 1000 运行:

docker run --name mycontainer --user 1000:1000 -it ubuntu /bin/bash

这样,在容器中运行的进程将以指定的用户和组身份运行。注意,这个用户和组的映射仅在容器内部有效,不会影响宿主机或其他容器。

需要注意的是,USER 命名空间只对新创建的容器有效,并不会影响已经运行的容器。因此,在启动容器时,需要明确指定 --user 选项来设置容器内进程的用户和组。

四、namespace 生命周期

每个命名空间都有自己的生命周期,可以创建、运行、销毁和释放。

下面是Linux命名空间的典型生命周期:

1、创建命名空间:

使用系统调用(如clone()unshare())创建新的命名空间。通过指定不同的标识符,可以创建各种类型的命名空间,例如PID命名空间、网络命名空间、挂载命名空间等。

2、运行命名空间:

在创建命名空间后,可以将进程加入到该命名空间中。通过调用setns()系统调用或使用nsenter命令,可以将进程从父命名空间切换到新的命名空间。在命名空间中,进程可以访问和修改属于该命名空间的资源。

3、销毁命名空间:

当不再需要命名空间时,可以将其销毁。通过调用unshare()系统调用或使用ip netns delete等命令,可以销毁命名空间。销毁命名空间将释放该命名空间所占用的资源,并将其中的进程重新归入到原始的父命名空间中。

需要注意的是,命名空间可以被继承和共享。例如,一个进程创建了一个新的PID命名空间,并在其中启动了一个子进程,那么该子进程将成为新命名空间的一部分。此外,命名空间可以通过不同的手段进行通信和共享资源,如Unix域套接字或Mount命名空间的绑定挂载点。

总结起来,Linux命名空间的生命周期涉及创建、运行、销毁和释放操作,通过这些操作可以实现进程资源的隔离和管理。

命名空间可能不会被销毁的情况

1、进程仍在命名空间中运行:

如果有一个或多个进程仍在使用命名空间,并且没有退出或切换到其他命名空间,那么该命名空间将保持存在。只有在最后一个进程退出或切换到其他命名空间时,命名空间才会被销毁。

2、子命名空间的存在:

如果一个命名空间是另一个命名空间的子命名空间,并且子命名空间仍然活跃,那么父命名空间将一直保持存在。只有当父命名空间和所有子命名空间中的进程都退出或切换到其他命名空间时,该命名空间才会被销毁。

3、共享命名空间:

如果一个命名空间被多个进程共享,并且这些进程仍在活跃状态,那么该命名空间将一直保持存在。只有当最后一个共享此命名空间的进程退出或切换到其他命名空间时,该命名空间才会被销毁。

需要注意的是,命名空间的生命周期取决于其中的进程和命名空间之间的关系。只要还有进程活跃或者仍有命名空间之间的继承或共享关系存在,命名空间就会被保留下来。因此,确保在不再需要使用命名空间时进行适当的清理操作非常重要,以避免资源的浪费和潜在的问题。

五、查看进程所属的 namespace

5.1、lsns

lsns命令不需要任何参数,它会列出当前系统上存在的所有命名空间的信息。每个命名空间都有一行输出,包含以下信息:

  • NS TYPE:命名空间的类型,如pid、mnt、net、ipc、uts、cgroup等。
  • NS ID:命名空间的ID,用于标识不同的命名空间。
  • NPROCS:命名空间中当前运行的进程数。
  • FLAGS:命名空间的标志位,表示命名空间的属性和状态。

lsns命令的一些常用选项包括:

  • -a--all:显示所有命名空间,包括未被引用的命名空间。
  • -t TYPE--type TYPE:只显示指定类型的命名空间。
  • -n NAMESPACE--namespace NAMESPACE:只显示指定命名空间ID或名称的命名空间信息。
$ lsns
        NS TYPE  NPROCS    PID USER COMMAND
4026531836 pid      217      1 root /usr/lib/systemd/systemd --system --deserialize 17
4026531837 user     262      1 root /usr/lib/systemd/systemd --system --deserialize 17
4026531838 uts      221      1 root /usr/lib/systemd/systemd --system --deserialize 17
4026531839 ipc      217      1 root /usr/lib/systemd/systemd --system --deserialize 17
4026531840 mnt      206      1 root /usr/lib/systemd/systemd --system --deserialize 17
4026531860 mnt        1     50 root kdevtmpfs
4026531992 net      227      1 root /usr/lib/systemd/systemd --system --deserialize 17
4026532380 mnt        9  68131 root nginx: master process /usr/sbin/nginx
4026532384 mnt        1  79144 root /pause
4026532385 uts        1  79144 root /pause
4026532386 ipc        2  79144 root /pause
4026532387 pid        1  79144 root /pause
4026532389 net        2  79144 root /pause
4026532427 mnt        1   9682 ntp  /usr/sbin/ntpd -u ntp:ntp -g
4026532464 mnt        1  79146 root /pause
4026532466 uts        1  79146 root /pause

5.2、ls -l /proc/[pid]/ns

$ ls -l /proc/99997/ns
total 0
lrwxrwxrwx 1 root root 0 Sep  4 14:31 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Sep  1 10:21 ipc -> ipc:[4026532661]
lrwxrwxrwx 1 root root 0 Sep  1 10:21 mnt -> mnt:[4026532659]
lrwxrwxrwx 1 root root 0 Sep  1 10:21 net -> net:[4026532664]
lrwxrwxrwx 1 root root 0 Sep  1 10:21 pid -> pid:[4026532662]
lrwxrwxrwx 1 root root 0 Sep  4 14:31 pid_for_children -> pid:[4026532662]
lrwxrwxrwx 1 root root 0 Sep  4 14:31 time -> time:[4026531834]
lrwxrwxrwx 1 root root 0 Sep  4 14:31 time_for_children -> time:[4026531834]
lrwxrwxrwx 1 root root 0 Sep  1 10:21 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Sep  1 10:21 uts -> uts:[4026532660]

每行输出中,第一个字段表示符号链接的权限和属性,第九个字段表示符号链接的目标。目标的格式为[type]:[namespace_id],其中type表示命名空间的类型,namespace_id则是命名空间的ID。

通过执行ls -l /proc/[pid]/ns命令,可以查看特定进程所属的所有命名空间的符号链接。这样可以了解进程当前所在的命名空间以及命名空间的类型和ID。

5.3、pstree -p

pstree -p命令用于以树状结构显示进程及其子进程,并显示它们的PID(进程ID)。

$ pstree -p | grep nginx
           |-nginx(68131)-+-nginx(68132)
           |              |-nginx(68133)
           |              |-nginx(68134)
           |              |-nginx(68135)
           |              |-nginx(68136)
           |              |-nginx(68137)
           |              |-nginx(68139)
           |              `-nginx(68140)

5.4、ip netns list

参考文档

1、https://blog.csdn.net/y3over/article/details/128863060

2、https://www.cnblogs.com/sally-zhou/p/13398260.html

3、http://www.taodudu.cc/news/show-320037.html?action=onClick

4、http://www.360doc.com/content/21/0803/11/31115656_989326901.shtml

5、http://www.noobyard.com/article/p-nqmbazhv-s.html

6、https://www.cnblogs.com/sparkdev/p/8214455.html

7、https://blog.csdn.net/key_3_feng/article/details/129942638

8、https://www.51cto.com/article/632950.html

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linuxnamespace是一种操作系统级别的隔离机制,它允许将全局资源抽象为独立的命名空间,使得每个命空间内的进程只能看到自己所属的资源,而不会感知到其他命名空间中的资源。这种隔离机制可以提供更高的安全性、可靠性和性能。 Linux提供了多种类型的namespace,包括: 1. PID namespace:每个PID namespace都有自己独立的进程ID空间,使得在不同的PID namespace中运行的进程无法看到其他PID namespace中的进程。 2. Network namespace:每个Network namespace都有自己独立的网络栈,包括网络设备、IP地址、路由表等,使得在不同的Network namespace中运行的进程无法直接通信。 3. Mount namespace:每个Mount namespace都有自己独立的文件系统挂载点,使得在不同的Mount namespace中运行的进程无法访问其他Mount namespace中的文件系统。 4. UTS namespace:每个UTS namespace都有自己独立的主机名和域名,使得在不同的UTS namespace中运行的进程可以有不同的主机名和域名。 5. IPC namespace:每个IPC namespace都有自己独立的System V IPC对象(如消息队列、信号量、共享内存),使得在不同的IPC namespace中运行的进程无法访问其他IPC namespace中的对象。 6. User namespace:每个User namespace都有自己独立的用户和用户组映射,使得在不同的User namespace中运行的进程可以有不同的用户和用户组身份。 通过使用这些namespace,可以实现各种隔离场景,如容器化技术(如Docker)就是基于namespace实现的。它可以提供更高的安全性和资源隔离,使得不同的应用程序可以在同一台机器上运行而互不干扰。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值