阿谁难题是15年明白提出的,扫了下迄今(2022.05)的大部分发问都在申明docker是甚么与是不是用,那我阿谁发问就来弥补上docker大致上是是不是同时实现的,用100行C/C++把思绪捋一下。用不着看文本的能间接戳代码:
详细来说新闻稿,我阿谁发问罢了那份搬运,同时实现全然参照2018年的GOTO Conference:
Containers From Scratch • Liz Rice • GOTO 2018www.youtube.com/watch?app=desktop&v=8fi7uSYlOdc&feature=youtu.be阿谁音频桑翁数十行Go同时实现了两个UTS、pid、filesystem隔断的“Minidocker”,并能限造资本的接纳。有关cgroup的部门我小我不是颇感兴趣,所以传话为C/C++的时候只同时实现了后半部门。
好,假设你能承受:
英语片头/日文版Go论述的同时实现音频而非文本我建议你间接去看原音频,不要听我传话
假设你:
嫌英语费事对Go恶感,但看不懂C/C++用不着看音频那继续
一点儿有关docker是甚么、较之其他VM区别、有甚么用的介绍,不感兴趣的埃唐佩县方可最常指的说法,“docker是一类轻量、民主化级VM”,但那种论述其实不能全然解答疑惑,用专有名词去申明专有名词也像没说:简而言之的“轻量、民主化级VM”又tm是甚么?
假设对通俗的VM是如何也没甚么概念,那“docker是一类xx的VM”就更没意义了,因为显然没参照的尺度。
好比说linux的kvm[1],他能借助于硬体的帮忙以相对(应用软件办法)较高的开收同时实现同x86(x86)上的全并行计算。译者成人话,就是你能在x86_linux(host OS)上起两个捷伊x86_linux(guest OS)交互式机,全然与host分立的功课控造系统,并且阿谁功课控造系统那类不晓得并行计算的存在。当guest OS来到某些继续施行鸿沟必要host OS介入时,会发作极度trap到host OS里,有kvm处置,再恢复guest OS继续施行。阿谁“会发作极度trap到host OS里”就必要硬体的撑持,好比说Intel VT或是AMD-V;而“同x86”(在那里就是x86了)是说,你不克不及借助于kvm来同时实如今x86上跑两个arm_linux的功课控造系统,只能交互式出x86_linux。
再好比说qemu-system[2]的并行计算,好吧那只不外是emulation,就能做到cross-architecture的VM,你能在x86下面随意跑mips或是risc-v的分立OS,但那是应用软件同时实现的,简而言之是动态十进造译者(DBT[3], Dynamic Binary Translation),所以跟kvm那种性能全然没法比,不外假设你是在x86上交互式x86,qemu也能借助于kvm来加速。
那么docker与下面那些较之如说何?详细来说docker其实不会运转两个分立的功课控造系统,而罢了给民主化供给更多了一类假象,使民主化运转得就仿佛在两个分立的功课控造系统上。那也是为甚么docker被称为民主化级、轻量的VM:他显然不必要另起两个功课控造系统。
难题来了,没分立的功课控造系统,那还能起到VM的感化吗?
阿谁难题能反过来想:实的有需要隔断出两个完好的功课控造系统吗?
其实不。良多时候只不外他们罢了想减轻收集办理的承担:为流程供给更多两个同一的“运转天然情况”。那简而言之的“运转天然情况”,简而言之是控造系统库、运转时、第三方库之类的文档。假设他们仅仅想连结文档天然情况的一致,只必要供给更多那份分立的Kaysersberg方可,其实不必要替代功课控造系统。
当然,两个流程也不罢了看见文档,他还看见当前大部分民主化、收集路由器接纳之类的一系列控造系统重要信息,他们要同时能隔断那些控造系统重要信息,包管每个民主化都看见、认为本身是分立运转的。
大部分那些,只不外都不至于非得另起两个功课控造系统,因为Mach那类就供给更多一些根底设备(namespace、cgroup等)来到达那一效果,那也是docker得以同时实现的根底。
从两个“计算器”起头他们从同时实现两个“计算器”起头,也即把他们传递给它的指示间接继续施行,而不供给更多任何的隔断功用,阿谁流程的名字就叫mocker吧(笑。
借助于execv一族的表达式,那一点儿能很容易同时实现:把指示中的参数丢给execv表达式方可。他们模拟docker run 来同时实现两个mocker run (省略头文档与一些新闻稿):
int main(int argc, char **argv) { if (argc < 3) { cerr << "Too few arguments" << endl; exit(-1); } if (!strcmp(argv[1], "run")) { run(argc - 2, &argv[2]); } } static void run(int argc, char **argv) { cout << "Running " << cmd(argc, argv) << endl; execvp(argv[0], argv); } static string cmd(int argc, char **argv) { string cmd = ""; for (int i = 0; i < argc; i++) { cmd.append(argv[i] + string(" ")); } return cmd; }能简单运转个ls:
user@linux:~/mini-docker$ make g++ main.cc -o mocker user@linux:~/mini-docker$ ./mocker run ls -l Running ls -l total 28 -rw-rw-r-- 1 user user 46 May 13 15:04 Makefile -rw-rw-r-- 1 user user 767 May 13 15:18 main.cc -rwxrwxr-x 1 user user 18856 May 13 15:18 mocker能运转个bash,比照父子民主化的pid:
user@linux:~/mini-docker$ ps PID TTY TIME CMD 513 pts/0 00:00:00 bash 1302 pts/0 00:00:00 ps user@linux:~/mini-docker$ ./mocker run /bin/bash Running /bin/bash user@linux:~/mini-docker$ ps PID TTY TIME CMD 513 pts/0 00:00:00 bash 1303 pts/0 00:00:00 bash 1309 pts/0 00:00:00 ps user@linux:~/mini-docker$ exit exit user@linux:~/mini-docker$ ps PID TTY TIME CMD 513 pts/0 00:00:00 bash 1310 pts/0 00:00:00 ps为了不让bash笼盖掉mocker父民主化,他们修改run,多加两个fork,将execv放到子民主化中:
static void run(int argc, char **argv) { cout << "Running " << cmd(argc, argv) << endl; pid_t child_pid = fork(); if (child_pid < 0) { cerr << "Fail to fork" << endl; return; } if (child_pid) { if(waitpid(child_pid, NULL, 0) < 0) { cerr << "Fail to wait for child" << endl; } else { cout << "Child terminated" << endl; } } else { run_child(argc, argv); } } static void run_child(int argc, char **argv) { if (execvp(argv[0], argv)) { cerr << "Fail to exec" << endl; } }得到的输出与先前是类似的:
user@linux:~/mini-docker$ ps PID TTY TIME CMD 513 pts/0 00:00:00 bash 1740 pts/0 00:00:00 ps user@linux:~/mini-docker$ ./mocker run /bin/bash Running /bin/bash user@linux:~/mini-docker$ ps PID TTY TIME CMD 513 pts/0 00:00:00 bash 1741 pts/0 00:00:00 mocker 1742 pts/0 00:00:00 bash 1748 pts/0 00:00:00 ps user@linux:~/mini-docker$ exit exit Child terminated不外在那里他们会看见子民主化的ps 相较之前多显示了两个mocker民主化,那是因为他们把bash的继续施行放到了fork后的子民主化中,而父民主化的mocker也就得以保留下来,期待子民主化的退出,借助于ps能看见民主化关系:
user@linux:~/Documents/mini-docker$ ps axjf | grep pts 2452 2463 2463 2463 pts/0 5646 Ss 1000 0:00 | \_ bash 2463 4436 4436 2463 pts/0 5646 S 1000 0:00 | \_ ./mocker run /bin/bash 4436 4437 4437 2463 pts/0 5646 S 1000 0:00 | \_ /bin/bash 4437 5646 5646 2463 pts/0 5646 R+ 1000 0:00 | \_ ps axjf 4437 5647 5646 2463 pts/0 5646 S+ 1000 0:00 | \_ grep --color=auto pts UTS namespace接下来,他们同时实现对UTS的隔断。那里的简而言之UTS起到的感化是,显示主机名(hostname)。好比说他们能在host中看见本身的hostname:
user@linux:~$ hostname linux同样,在他们的container中也能看见hostname:
user@linux:~/mini-docker$ ./mocker run /bin/bash Running /bin/bash user@linux:~/mini-docker$ hostname linux user@linux:~/mini-docker$ exit exit Child terminated假设他们在container中修改了hostname,那么在host中他们也会察觉到那一修改:
user@linux:~/mini-docker$ hostname linux user@linux:~/mini-docker$ ./mocker run /bin/bash Running /bin/bash user@linux:~/mini-docker$ sudo hostname container user@linux:~/mini-docker$ hostname container user@linux:~/mini-docker$ exit exit Child terminated user@linux:~/mini-docker$ hostname container user@linux:~/mini-docker$ ./mocker run /bin/bash Running /bin/bash user@container:~/mini-docker$hostname的改动不会立马在@ 后展示出来,但当他们再起两个子民主化bash 时,hostname就已被替代为先前container修改的值了。
他们当然不希望container中的行为影响到host,那接下来他们借助于namespace手艺隔断UTS。从代码的角度他们只要对子民主化部门略微改动:
static void run_child(int argc, char **argv) { int flags = CLONE_NEWUTS; if (unshare(flags) < 0) { cerr << "Fail to unshare in child" << endl; exit(-1); } if (execvp(argv[0], argv)) { cerr << "Fail to exec" << endl; } }他们稍后再申明那是在做甚么,先看看那点改动能否奏效:
user@linux:~/mini-docker$ make g++ main.cc -o mocker user@linux:~/mini-docker$ hostname linux user@linux:~/mini-docker$ sudo ./mocker run /bin/bash Running /bin/bash root@linux:/home/user/mini-docker# hostname container root@linux:/home/user/mini-docker# hostname container root@linux:/home/user/mini-docker# exit exit Child terminated user@linux:~/mini-docker$ hostname linuxchild对hostname的修改,没影响到host,他们同时实现了对UTS的隔断(用特权级是出于unshare挪用的要求)。为了便利区分子民主化的hostname,也为了让阿谁流程看起来正式一些,他们在启动container(也就是他们的阿谁子民主化)的时候主动修改hostname,使之在末端上展示出来:
const char *child_hostname = "container"; static void run_child(int argc, char **argv) { int flags = CLONE_NEWUTS; if (unshare(flags) < 0) { cerr << "Fail to unshare in child" << endl; exit(-1); } if (sethostname(child_hostname, strlen(child_hostname)) < 0) { cerr << "Fail to change hostname" << endl; exit(-1); } if (execvp(argv[0], argv)) { cerr << "Fail to exec" << endl; } }看起来还实像那么回事,对吧?
user@linux:~/mini-docker$ make g++ main.cc -o mocker user@linux:~/mini-docker$ hostname linux user@linux:~/mini-docker$ sudo ./mocker run /bin/bash Running /bin/bash root@container:/home/user/mini-docker# hostname container root@container:/home/user/mini-docker# exit exit Child terminated user@linux:~/mini-docker$ hostname linux Linux kernel namespace如今他们回过甚来看看阿谁unshare,那简而言之的namespace是干嘛的。
类比于C++的namespace,Mach中的namespace手艺也是为了限造“你能看见甚么”。namespace也有差别的类别[4],从mount到pid再到收集之类,uts也是此中之一。差别的namespace类别负责限造民主化在“某一方面”都能看见甚么。按照差别的需求,阿谁“某一方面”能是主机名(好比说下面他们隔断的hostname),也能是挂载或是pid之类。
最曲不雅也是最早引入Mach的是mount namespace[5],用于同时实现挂载方面的隔断。把差别民主化放在差别的mount namespace中,如许一来你在两个mount namespace中停止的挂载操做,其他mount namespace中的民主化看不到,以此同时实现挂载方面的隔断。
控造系统启动时,会创建两个global initial mount namespace,大部分民主化都在此中[5]。当某个民主化主动要求创建两个捷伊mount namespace时,它和它的子民主化就城市放到阿谁捷伊mount namespace中,同时实现隔断,同时能想象到,那些差别的mount namespace之间也会像民主化的父子层级关系一样,构成本身的层级关系并由kernel负责维护。
详细到同时实现上,每个mount namespace城市由两个mnt_namespace[6]kernel object负责维护,当他们在停止诸如open等涉及pathname resolution的syscall时,会颠末当前民主化对应的mnt_namepace “译者”,得到阿谁民主化最末看见的pathname对应的inode。当发作unshare[7]时,按照传入的flag,差别的namespace(mount、pid、uts之类)实例被创建/拷贝,并修改当前民主化的指向,同时实现差别namespace间对system resource view的隔断。
类似mount,UTS namespace则是隔断了差别民主化的hostname view,如许一来他们修改了container的UTS namespace中的主机名时,host民主化namespace中的主机名就不会遭到影响了。
PID namespace接下来他们试着隔断pid namespace。
下面他们在container中继续施行ps时能看见container的pid是从host侧pid增长得来的。好比说他们能在流程中打印:
static void run(int argc, char **argv) { cout << "Parent running " << cmd(argc, argv) << " as " << getpid() << endl; pid_t child_pid = fork(); ... } static void run_child(int argc, char **argv) { cout << "Child running " << cmd(argc, argv) << " as " << getpid() << endl; int flags = CLONE_NEWUTS; ... }运转成果:
user@linux:~/mini-docker$ make g++ main.cc -o mocker user@linux:~/mini-docker$ sudo ./mocker run /bin/bash Parent running /bin/bash as 10929 Child running /bin/bash as 10930 root@container:/home/user/mini-docker#然而他们希望container获得两个全捷伊pid空间,如斯一来他们的container的pid应该是1。为了同时实现那一点儿,他们仍借助于unshare创建两个捷伊PID namespace:
static void run(int argc, char **argv) { cout << "Parent running " << cmd(argc, argv) << " as " << getpid() << endl; if (unshare(CLONE_NEWPID) < 0) { cerr << "Fail to unshare PID namespace" << endl; exit(-1); } pid_t child_pid = fork(); if (child_pid < 0) {ptx cerr << "Fail to fork child" << endl; return; } if (child_pid) { if(waitpid(child_pid, NULL, 0) < 0) { cerr << "Fail to wait for child" << endl; } } else { run_child(argc, argv); } }那时候再次运转他们就能看见,container实的以pid 1运转了:
user@linux:~/mini-docker$ make g++ main.cc -o mocker user@linux:~/mini-docker$ sudo ./mocker run /bin/bash Parent running /bin/bash as 11713 Child running /bin/bash as 1 root@container:/home/ptx/mini-docker#于是你测验考试ps,却发现咦,涛声照旧了,妈的咋又不灵了,pid咋又归去了?
root@container:/home/user/mini-docker# ps PID TTY TIME CMD 11712 pts/0 00:00:00 sudo 11713 pts/0 00:00:00 mocker 11714 pts/0 00:00:00 bash 11764 pts/0 00:00:00 ps别急,那是一般的,那涉及到ps是是不是打印民主化重要信息的。ps是通过读取 /proc目次下的pseudo filesystem来获取当前民主化重要信息的[8],固然他们将container放到了两个捷伊PID namespace中,但在读取/proc目次时得到的重要信息仍然与host民主化是不异的,而那一重要信息仍然是基于原先的PID namespace维护的,所以他们ps看见的pid就又归去了。不外那其实不耽搁他们的pid在本身民主化的视角中是1,因为getpid其实不依赖于/proc 中的重要信息。不论是不是说,他们仍然是胜利地隔断了pid view的,罢了不那么明显罢了。
别的或许你会发现他们挪用unshare创建PID namespace是在父民主化中停止的,而不像之前UTS那样在子民主化中挪用。那只不外是kernel同时实现方面的难题,创建PID namespace的民主化那类不会被放入捷伊PID namespace中[9],而是他的子民主化们才会进入捷伊PID namespace,而fork等表达式创建的第两个子民主化会以pid 1做为阿谁PID namespace的intial process,领受orphaned民主化之类[10]。
Filesystem isolation对container来说filesystem的隔断算长短常重要的一部门,简而言之的依赖天然情况恰是通过差别的Kaysersberg来同时实现的。不外在谈阿谁难题之前,我必需要廓清一点儿,就是差别的mount namespace那类不克不及供给更多filesystem的隔断。
咦,刚不是才说mount namespace能供给更多差别的mount view吗,是不是又不克不及了?
也许阿谁比方不太得当,但mount isolation与filesystem isolation间的区别就比如数学家关心两个方程解能否存在,解能否独一,但工程人员关心阿谁方程的解是几。
mount isolation关心两个mount point被挂载在两个mount namespace中时,会不会也呈现在其他mount namespace中(share、private之类),而filesystem isolation关心的是阿谁mount point里的内容挂载到哪里去了,里面拆的是甚么内容。
光说或许很难理解,无妨通过两个例子来看。假设我很巧,手边正好有两个ubuntu filesystem的拷贝在~目次下:
user@linux:~$ ls ubuntu-fs/ bin dev lib32 lost+found opt run srv tmp var boot etc lib64 media proc sbin swapfile ubuntu-fs.tar cdrom lib libx32 mnt root snap sys usr当然,那此中的/proc、/sys等pseudo filesystem都是空的,因为他们只拷贝了实正的磁盘文档。
他们能对子民主化代码做简单的修改,将container对整个filesystem的view限造在阿谁目次下:
static void run_child(int argc, char **argv) { cout << "Child running " << cmd(argc, argv) << " as " << getpid() << endl; int flags = CLONE_NEWUTS; if (unshare(flags) < 0) { cerr << "Fail to unshare in child" << endl; exit(-1); } if (chroot("../ubuntu-fs") < 0) { cerr << "Fail to chroot" << endl; exit(-1); } if (chdir("/") < 0) { cerr << "Fail to chdir to /" << endl; exit(-1); } if (sethostname(child_hostname, strlen(child_hostname)) < 0) { cerr << "Fail to change hostname" << endl; exit(-1); } if (execvp(argv[0], argv)) { cerr << "Fail to exec" << endl; } }在那部门中他们只做了两处修改:chroot和chdir。此中十分核心的就是阿谁chroot,它的功用是修改当前民主化及后续子民主化眼中的根目次,让后续的absolute path文档拜候全都从阿谁捷伊根目次起头[11],而他们那里就把它改到了他们的文档控造系统镜像;紧跟着chroot他们用chdir将目次切换到新设置的/,看起来更像一回事了:
user@linux:~/mini-docker$ make g++ main.cc -o mocker ptx@linux:~/mini-docker$ sudo ./mocker run /bin/bash Parent running /bin/bash as 14418 Child running /bin/bash as 1 root@container:/# ls bin boot cdrom dev etc lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin snap srv swapfile sys tmp ubuntu-fs.tar usr var但要强调的是,chroot并非两个面向平安的表达式,而罢了为了view isolation,你能很随便地通过相对目次拜候trick[12]来逃出阿谁根目次。
原先的ubuntu-fs目次中有两个ubuntu-fs.tar文档, 在那里他们也又在container的根目次里看见它了,如斯一来他们也同时实现了对文档控造系统的隔断:
/proc固然做到了filesystem isolation,他们仍然未处理/proc目次的难题,那时他们运转ps痛快就起头报错了:
user@linux:~/mini-docker$ make g++ main.cc -o mocker user@linux:~/mini-docker$ sudo ./mocker run /bin/bash Parent running /bin/bash as 17636 Child running /bin/bash as 1 root@container:/# ps Error, do this: mount -t proc proc /proc那是因为他们将根目次切换到ubuntu-fs后,后续的proc拜候也全都是基于阿谁basepath了,而很明显,ubuntu-fs下的/proc里是甚么都没的。
不外那能通过两个简单的挂载来处理:
static void run_child(int argc, char **argv) { cout << "Child running " << cmd(argc, argv) << " as " << getpid() << endl; int flags = CLONE_NEWUTS; if (unshare(flags) < 0) { cerr << "Fail to unshare in child" << endl; exit(-1); } if (chroot("../ubuntu-fs") < 0) { cerr << "Fail to chroot" << endl; exit(-1); } if (chdir("/") < 0) { cerr << "Fail to chdir to /" << endl; exit(-1); } if (mount("proc", "proc", "proc", 0, NULL) < 0) { cerr << "Fail to mount /proc" << endl; exit(-1); } if (sethostname(child_hostname, strlen(child_hostname)) < 0) { cerr << "Fail to change hostname" << endl; exit(-1); } if (execvp(argv[0], argv)) { cerr << "Fail to exec" << endl; } }运转发现说哎那就好啦,就不报错啦,就能显示啦,以至还表现出来pid隔断啦:
user@linux:~/mini-docker$ make g++ main.cc -o mocker ptx@linux:~/mini-docker$ sudo ./mocker run /bin/bash Parent running /bin/bash as 18685 Child running /bin/bash as 1 root@container:/# ps PID TTY TIME CMD 1 ? 00:00:00 bash 8 ? 00:00:00 ps root@container:/# ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 12916 4220 ? S 23:18 0:00 /bin/bash root 9 0.0 0.0 14568 3452 ? R+ 23:18 0:00 ps aux还能在container中查看挂载过的/proc
root@container:/# cat /proc/mounts proc /proc proc rw,relatime 0 0但别急着快乐,假设你那时候回到host查看大部分挂载过的proc文档控造系统会发现:
user@linux:~/mini-docker$ cat /proc/mounts | grep ^proc proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0 proc /home/user/ubuntu-fs/proc proc rw,relatime 0 0阿谁挂载点也呈现在了host中。也即,固然他们让container获得一致的pid isolation view,但没能将阿谁mount point那类的存在从host中隔断出来。
他们退出container,umount掉container挂载的/proc,来看看最初的mount namespace难题。
root@container:/# exit exit user@linux:~/mini-docker$ sudo umount ../ubuntu-fs/proc Mount namespace较之于其他部门,mount的难题一些,比力曲不雅的表现是,你没法通过简单地加两个flag同时实现预期中的mount point isolation,也就是说:
static void run_child(int argc, char **argv) { cout << "Child running " << cmd(argc, argv) << " as " << getpid() << endl; int flags = CLONE_NEWUTS | CLONE_NEWNS; if (unshare(flags) < 0) { cerr << "Fail to unshare in child" << endl; exit(-1); } if (chroot("../ubuntu-fs") < 0) { cerr << "Fail to chroot" << endl; exit(-1); } if (chdir("/") < 0) { cerr << "Fail to chdir to /" << endl; exit(-1); } if (mount("proc", "proc", "proc", 0, NULL) < 0) { cerr << "Fail to mount /proc" << endl; exit(-1); } if (sethostname(child_hostname, strlen(child_hostname)) < 0) { cerr << "Fail to change hostname" << endl; exit(-1); } if (execvp(argv[0], argv)) { cerr << "Fail to exec" << endl; } }类比CLONE_NEWUTS、CLONE_NEWPID仅通过给unshare加两个CLONE_NEWNS是没法子同时实现对mount point的isolation的(能运转试一试),必要从头设置根目次mount point的propagation type(对“为甚么必要那几行”不是言简意赅就能申明清的,我把阿谁放在末尾,有兴趣的话能去看看,但那里就不展开了):
static void run_child(int argc, char **argv) { cout << "Child running " << cmd(argc, argv) << " as " << getpid() << endl; int flags = CLONE_NEWUTS | CLONE_NEWNS; if (unshare(flags) < 0) { cerr << "Fail to unshare in child" << endl; exit(-1); } if (mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL) < 0) { cerr << "Fail to mount /" << endl; exit(-1); } if (chroot("../ubuntu-fs") < 0) { cerr << "Fail to chroot" << endl; exit(-1); } if (chdir("/") < 0) { cerr << "Fail to chdir to /" << endl; exit(-1); } if (mount("proc", "proc", "proc", 0, NULL) < 0) { cerr << "Fail to mount /proc" << endl; exit(-1); } if (sethostname(child_hostname, strlen(child_hostname)) < 0) { cerr << "Fail to change hostname" << endl; exit(-1); } if (execvp(argv[0], argv)) { cerr << "Fail to exec" << endl; } }如斯一来他们运转container的时候挂载/proc就不会propagate到host view,container的/procmount point visibility被限造在了container的mount namespace中。不外他们仍然能在container运转着的时候在host中看见阿谁mount namespace的存在:
user@linux:~/mini-docker$ ps axjf | grep /bin/bash 2420 21908 21907 2420 pts/0 21907 S+ 1000 0:00 | \_ grep --color=auto /bin/bash 4031 4136 4136 4136 pts/1 21777 Ss 1000 0:00 | | \_ /usr/bin/bash 4136 21775 21775 4136 pts/1 21777 S 0 0:00 | | \_ sudo ./mocker run /bin/bash 21775 21776 21775 4136 pts/1 21777 S 0 0:00 | | \_ ./mocker run /bin/bash 21776 21777 21777 4136 pts/1 21777 S+ 0 0:00 | | \_ /bin/bash user@linux:~/mini-docker$ sudo lsns -t mnt | grep 21777 4026533509 mnt 1 21777 root /bin/bash 一些设法此次搬运的原音频是我在20年看见的,其时就觉得长短常好的资本,即使你对namespace、cgroup所知甚少(在其时我以至是不晓得有那两项手艺存在的),也能大致理解她在说甚么。用到的Go也是很简单的语法,只要对C系的语言有所领会应该就看不懂。
比来也是出于研究必要从头回忆那方面的内容,闲暇之余在知乎搜了一圈发现到如今也没人提到过阿谁资本,其实是太可惜了。
在知乎总能看见有人分享很好的资本,此次我也来奉献点力量,希望关于想更进一步领会容器手艺的人,那篇发问能几处理他们的一些疑惑~
附:有关remount root mount point的讨论 Mount namespace propagation & shared subtree那部门的讨论次要是参照了lwn上的那篇文章:
在一起头提到mount namespace的时候,他们说他隔断了差别mount namespace间mount point可见性的隔断。然而在一起头kernel同时实现了阿谁feature的时候,人们发现如许会招致一些易用性的难题,次要是因为mount namespace供给更多的隔断有点强过甚了。
最曲不雅的两个例子是,假设他们clone两个捷伊mount namepsace而不chroot,全然隔断会招致他们在原先的mount namespace下挂载两个捷伊设备时,捷伊mount namespace全然看不到阿谁设备,成果就是他们不能不手动在大部分必要的mount namespace中手动挂载一次。也就是说,他们在希望差别mount namespace隔断的同时也希望它们之间能同时实现部门mount point的共享。
出于下面那些考虑,后续的Mach同时实现了shared subtree feature,借助于阿谁feature来同时实现主动的mount/unmount事务传布。简而言之,每个mount point会带有两个propagation type,它决定阿谁mount point下的间接mount point(也即“mount point下的mount point创建/删除能否propagate不归我管”)创建/删除如何propagate到其他mount points。
propagation type分为四类,别离是:
MS_SHARED:在当前mount point下的创建或删除捷伊mount point会propagate到“peer group”中大部分其他mount point。也就是说,在那些其他mount point下也能看见心创建或删除的mount point。那里的“peer group”是是不是来的先按下不表。MS_PRIVATE:与MS_SHARED全然相反,任安在当前mount point下创建/删除的mount point都不会对其他mount point可见MS_SLAVE:阿谁mount point是某个“peer group”(称为master)的slave,类似于单向的share,master中的改动会propagate到slave中,反之则是不成见的。MS_UNBINDABLE:那两个不克不及被bind的private mount point,也即除了具备MS_PRIVATE外,它不克不及被当做其他bind mount的source mount point[13]那里值得一提的是,propagation type感化的对象是mount point而非mount namespace,共享的对象也是mount point非mount namespace。因为可能存在两个mount point X,在不异mount namespace下有两个bind mount point Y被bind到了X上,另两个mount namespace下有另两个bind mount point Z也被bind到了X上,那么此时它们各自下的mount point创建/删除propagation只跟它们的propagation type有关,而与是不是在不异的mount namespace无关。
然而假设他们希望通过/proc/mounts或是findmnt查看大部分的mount point以及对应的propagation type,就只能看见当前民主化所属的mount namespace下的mount point了。好比说sudo findmnt -o TARGET,PROPAGATION,他们当然不成能看见其他mount namespace的/挂载到哪里以及是甚么type。假设实的希望那么做,也只能找到两个属于目的mount namespace的pid,去它的/proc下查看那么peer group是是不是产生的呢?有两种路子:
bind mount到两个MS_SHARED的mount point,如许捷伊mount point就和原先的mount point在两个peer group中了发作mount namespace clone时,根据以下规则确定捷伊mount namespace中各个mount point的propagation type[14]:A cloned namespace contains all the mounts as that of the parent namespace.
Lets say A and B are the corresponding mounts in the parent and the child namespace.
If A is shared, then B is also shared and A and B propagate to each other.
If A is a slave mount of Z, then B is also the slave mount of Z.
If A is a private mount, then B is a private mount too.
If A is unbindable mount, then B is a unbindable mount too.也即原先MS_SHARED的mount point在clone后的mount point也主动进入它的peer group。
默认行为说了那么多,听起来很烦琐,但为了理解他们到底要处理甚么难题,那些background是需要的。
他们回过甚来考虑两个难题:当创建(而非clone或是bind)两个捷伊mount point的时候,它的默认propagation type是甚么?
在linux中是根据如下规则来确定的:
假设阿谁mount point不是root,且它的parent mount point的propagation type是MS_SHARED,那么它也是MS_SHARED的不然一律MS_PRIVATE很明显根据阿谁规则,在最起头挂载root的时候它的propagation type会是MS_PRIVATE,并且假设他们不显式地新闻稿,那么后续的创建大部分mount point城市是MS_PRIVATE。
而他们适才也说了,像诸如设备的挂载,他们只不外反而希望它们能主动propagate到差别的其他mount namespace所clone出来的对应mount point,从阿谁角度来看MS_SHARED反而是两个更好的default type。
考虑到下面的种种原因,systemd会将控造系统中初始mount namespace的mount point全数设置为MS_SHARED,因而在现实的linux发行版中,默认的propagation type只不外是MS_SHARED。
不外关于像unshare[15]如许的东西(是command line tool的unshare而不是他们下面挪用的库表达式的阿谁unshare),它的本意就是将捷伊民主化放在两个全然隔断的mount namespace中继续施行,因而在启动两个捷伊mount namespace中,它会从头将大部分mount point全数设置为MS_PRIVATE,就类似于clone完mount namespace后立即:
mount --make-rprivate /递归地将根目次下的大部分mount point重置为MS_PRIVATE。
回到他们的那一处代码中的修改来:
int flags = CLONE_NEWUTS | CLONE_NEWNS; if (unshare(flags) < 0) { cerr << "Fail to unshare in child" << endl; exit(-1); } if (mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL) < 0) { cerr << "Fail to mount /" << endl; exit(-1); }在那里他们必需要弄清mount之前发作了甚么。他们clone了两个mount namespace,而先前mount namespace中mount point的propagation type能在host中看见:
user@linux:~$ sudo findmnt -o TARGET,PROPAGATION TARGET PROPAGATION / shared ├─/sys shared │ ├─/sys/kernel/security shared ... └─/sys/kernel/config shared ├─/proc shared │ └─/proc/sys/fs/binfmt_misc shared │ └─/proc/sys/fs/binfmt_misc shared ├─/dev shared │ ├─/dev/pts shared │ ├─/dev/shm shared │ ├─/dev/hugepages shared │ └─/dev/mqueue shared ...因而当他们clone了namespace后,root mount point会是MS_SHARED形态,此时他们chroot并挂载/proc时,因为新mount namespace的root mount point与host mount namespace在不异peer group中,他们在root mount point下发作的immediate mount会反映到host namespace中,成果就是他们在host中也不雅测到了两个捷伊proc mount point。值得留意的是,他们固然将container的根目次给chroot到了ubuntu-fs下,却不会影响ubuntu-fs/proc是root mount point的immediate child mount point阿谁事实。因为root mount point和根目次并非一回事(类似mount point hierarchy vs. directory hierarchy),chroot罢了改动了container视角中的根目次位置,却没改动ubuntu-fs不是两个mount point的事实,也因而root mount point的propagation type会摆布container中/proc在peer group中的可见性(是不是觉得车轱辘话在来回说。。
为了让/proc的挂载对其他mount point(那里也即其他mount namespace)不成见,他们用类似unsharecommand line tool的做法:
mount --make-rslave /将根目次下的大部分mount point递归地设置为MS_SLAVE(MS_PRIVATE也可,MS_LAVE能让host的mount操做对container单向可见),于是他们传递给mount的flag就是MS_SLAVE和MS_REC。
参照^KVM https://www.linux-kvm.org/^qemu system emulation https://www.qemu.org/docs/master/system/^Dynamic binary translation https://en.wikipedia.org/wiki/Binary_translation#Dynamic_binary_translation^namespaces(7) — Linux manual page https://man7.org/linux/man-pages/man7/namespaces.7.html^abMount namespaces and shared subtrees https://lwn.net/Articles/689856/^https://elixir.bootlin.com/linux/v5.15.31/source/fs/mount.h#L8^unshare(2) — Linux manual page https://man7.org/linux/man-pages/man2/unshare.2.html^ps(1) — Linux manual page https://man7.org/linux/man-pages/man1/ps.1.html#NOTES^https://man7.org/linux/man-pages/man2/unshare.2.html#DESCRIPTION^https://man7.org/linux/man-pages/man7/pid_namespaces.7.html#DESCRIPTION^chroot(2) — Linux manual page https://man7.org/linux/man-pages/man2/chroot.2.html^https://man7.org/linux/man-pages/man2/chroot.2.html#DESCRIPTION^What is a bind mount? https://unix.stackexchange.com/questions/198590/what-is-a-bind-mount^Shared Subtrees https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt^unshare(1) — Linux manual page https://man7.org/linux/man-pages/man1/unshare.1.html