深入研究Linux名称空间

在本系列文章中,我们将仔细考虑容器中的主要成分之一-名称空间。 在此过程中,我们将创建一个更简单的docker run克隆-我们自己的程序,该程序将在命令输入处接收命令(及其参数,如果有的话),并扩展其执行容器,使其与系统的其余部分隔离,类似于您将执行的方式docker run 从图像运行。


什么是名称空间?


Linux名称空间是操作系统中资源的抽象。 我们可以将名称空间视为一个盒子。 此框包含取决于框类型(名称空间)的系统资源。 当前有七种名称空间:Cgroups,IPC,Network,Mount,PID,User,UTS。


例如,网络名称空间包括与网络相关的系统资源,例如网络接口(例如wlan0eth0 ),路由表等;安装名称空间包括系统中的文件和目录,PID包含进程ID等。 。 因此,网络名称空间AB的两个实例(对应于我们的类比中的两个相同类型的框)可以包含不同的资源-也许A包含wlan0 ,而B包含eth0和路由表的单独副本。


命名空间不是您需要安装的某些其他功能或库,例如,使用apt软件包管理器。 它们由Linux内核本身提供,已经是在系统上运行任何进程的必要条件。 在任何给定的时间点,任何进程P恰好属于每种类型的名称空间的一个实例。 因此,当他需要说“更新系统中的路由表”时,Linux向他显示当时他所属的名称空间路由表的副本。


这是为了什么


绝对没事……当然,我只是在开玩笑。 盒子的一大优点是您可以在盒子中添加和删除东西,而这不会影响其他盒子的内容。 这与命名空间的想法相同-P进程可能“疯狂”并执行sudo rm –rf / ,但是属于另一个Mount命名空间的另一个Q进程将不受影响,因为它们使用这些文件的单独副本。


请注意,名称空间中包含的资源不一定是唯一副本。 在某些情况下,有意或由于安全漏洞而发生的情况下,两个或多个名称空间将包含相同的副本,例如,相同的文件。 因此,在一个Mount命名空间中对此文件所做的更改实际上将在所有其他引用它的Mount命名空间中可见。 因此,我们将放弃我们的抽屉类比,因为该物品不能同时放在两个不同的盒子中。


限制是一个问题


我们可以看到进程所属的名称空间! 通常对于Linux,它们显示为该进程的/proc/$pid/ns目录中的文件,其进程ID $pid


 $ ls -l /proc/$$/ns total 0 lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 cgroup -> cgroup:[4026531835] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 ipc -> ipc:[4026531839] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 mnt -> mnt:[4026531840] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 net -> net:[4026531957] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 pid -> pid:[4026531836] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 user -> user:[4026531837] lrwxrwxrwx 1 iffy iffy 0 May 18 12:53 uts -> uts:[4026531838] 

您可以打开另一个终端,执行相同的命令,这应该给您相同的结果。 如前所述,这是因为该进程必须属于某个名称空间(名称空间),并且在我们明确指定哪个名称空间之前,Linux默认将其添加到名称空间中。


让我们稍微参与一下。 在第二个终端中,我们可以执行以下操作:


 $ hostname iffy $ sudo unshare -u bash $ ls -l /proc/$$/ns lrwxrwxrwx 1 root root 0 May 18 13:04 cgroup -> cgroup:[4026531835] lrwxrwxrwx 1 root root 0 May 18 13:04 ipc -> ipc:[4026531839] lrwxrwxrwx 1 root root 0 May 18 13:04 mnt -> mnt:[4026531840] lrwxrwxrwx 1 root root 0 May 18 13:04 net -> net:[4026531957] lrwxrwxrwx 1 root root 0 May 18 13:04 pid -> pid:[4026531836] lrwxrwxrwx 1 root root 0 May 18 13:04 user -> user:[4026531837] lrwxrwxrwx 1 root root 0 May 18 13:04 uts -> uts:[4026532474] $ hostname iffy $ hostname coke $ hostname coke 

unshare命令在新名称空间中启动程序(可选)。 -u标志告诉她在新的UTS名称空间中运行bash 。 请注意,我们新的bash进程指向另一个uts文件,而其他所有文件均保持不变。


创建新的名称空间通常需要超级用户访问权限。 unshare 我们将假定 unshare 和我们的实现都是使用 sudo 执行的

我们刚做的结果之一就是现在我们可以从新的bash进程中更改系统主机名,而这不会影响系统中的任何其他进程。 您可以通过在第一个终端中运行hostname并查看该主机名在那里没有更改来验证这一点。


但是,例如,什么是容器?


希望现在您对命名空间可以做什么有所了解。 您可以假定容器本质上是具有不同于其他进程名称空间的普通进程,这是正确的。 实际上,这是一个配额。 没有配额的容器不需要属于每种类型的唯一命名空间-它可以共享其中的一些。


例如,当您键入docker run --net=host redis ,您要做的就是告诉docker run --net=host redis不要为redis进程创建新的Network名称空间。 并且,正如我们已经看到的,Linux将像其他任何常规过程一样,将该过程添加为默认网络名称空间的参与者。 因此,从网络角度来看,redis过程与其他所有人完全相同。 这不仅是一个网络配置选项, docker run允许您对大多数现有名称空间进行此类更改。 这就引出了一个问题,什么是容器? 是否有一个容器使用使用除一个公共命名空间之外的所有命名空间的进程? 通常,容器带有通过名称空间实现隔离的概念:进程与其他人共享的名称空间和资源越少, 隔离度就越高,这才是真正重要的。


隔离度


在本文的其余部分中,我们将为程序奠定基础,我们将其称为isolateisolate将命令作为参数,并在一个新进程中启动它,该进程与系统的其余部分隔离,并受其自己的名称空间限制。 在以下文章中,我们将研究为isolate启动的process命令添加对单个名称空间的支持。


根据应用程序,我们将重点关注用户,装载,PID和网络名称空间。 在我们完成之后,其余的实现将相对微不足道(实际上,我们将在程序的初始实现中在此处添加UTS支持)。 例如,对Cgroups的考虑超出了本系列的范围(对cgroups的研究,cgroups 用于控制进程可以使用多少资源的容器的另一个组件 )。


命名空间可能很快就可以使用,在探索每个命名空间时可以使用多种不同的方式,但是我们不能一次全部选择它们。 我们将仅讨论与我们正在开发的程序相关的那些方式。 为了了解配置此名称空间所需的步骤,每篇文章都将在控制台中对相关名称空间进行一些实验。 结果,我们已经对要实现的目标有了一个想法,然后将进行isolate的相应实现。


为了避免帖子的代码重载,我们将不包括诸如辅助功能之类的对于理解实现不是必需的东西。 您可以在Github上找到完整的源代码。

实作


这篇文章的源代码可以在这里找到 。 我们的isolate实现将是一个简单的程序,该程序从stdin读取一条带有命令的行,并克隆一个使用指定参数执行该行的新进程。 使用该命令克隆的进程将以其自己的UTS命名空间运行,就像我们之前使用unshare 。 在接下来的文章中,我们将看到名称空间不一定能在盒子里正常工作(或至少提供隔离),并且我们需要在创建名称空间之后(但在实际运行命令之前)执行一些配置,以便命令真正地独立运行。


这种名称空间的create-configure组合将需要主isolate进程与命令的子进程之间进行一些交互。 因此,这里主要工作的一部分将是配置两个进程之间的连接通道-在我们的例子中,由于其简单性,我们将使用Linux管道


我们需要做三件事:


  1. 创建一个基本的isolate进程,从stdin读取数据。
  2. 克隆一个新进程,该进程将在新的UTS命名空间中运行命令。
  3. 配置管道,以使命令执行过程仅在从主过程接收到名称空间配置已完成的信号后才开始其启动。

这是基本过程:


 int main(int argc, char **argv) { struct params params; memset(&params, 0, sizeof(struct params)); parse_args(argc, argv, &params); //         . if (pipe(params.fd) < 0) die("Failed to create pipe: %m"); //   . int clone_flags = SIGCHLD | CLONE_NEWUTS ; int cmd_pid = clone(cmd_exec, cmd_stack + STACKSIZE, clone_flags, &params); if (cmd_pid < 0) die("Failed to clone: %m\n"); //      . int pipe = params.fd[1]; //      namespace ... //   ,     . if (write(pipe, "OK", 2) != 2) die("Failed to write to pipe: %m"); if (close(pipe)) die("Failed to close pipe: %m"); if (waitpid(cmd_pid, NULL, 0) == -1) die("Failed to wait pid %d: %m\n", cmd_pid); return 0; } 

注意我们传递到clone调用中的clone_flags 。 看到在自己的名称空间中创建流程有多容易吗? 我们需要做的就是为名称空间类型设置一个标志( CLONE_NEWUTS标志对应于UTS命名空间),Linux将负责其余的工作。


接下来,命令进程在启动之前需要一个信号:


 static int cmd_exec(void *arg) { //   cmd   isolate . if (prctl(PR_SET_PDEATHSIG, SIGKILL)) die("cannot PR_SET_PDEATHSIG for child process: %m\n"); struct params *params = (struct params*) arg; //   ' '   . await_setup(params->fd[0]); char **argv = params->argv; char *cmd = argv[0]; printf("===========%s============\n", cmd); if (execvp(cmd, argv) == -1) die("Failed to exec %s: %m\n", cmd); die("¯\\_(ツ)_/¯"); return 1; } 

最后,我们可以尝试运行此命令:


 $ ./isolate sh ===========sh============ $ ls isolate isolate.c isolate.o Makefile $ hostname iffy $ hostname coke $ hostname coke #     ,      

现在, isolate不仅仅是一个简单地使团队分叉的程序(我们有一个适用于我们的UTS)。 在下一篇文章中,我们将通过检查User命名空间并使isolate在其自己的User命名空间中执行命令来采取下一步。 在那里,我们将看到我们实际上需要做一些工作,以便拥有可以在其中执行命令的可用命名空间。

Source: https://habr.com/ru/post/zh-CN458462/


All Articles