Docker:感觉自己萌萌哒
Posted K8S中文社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Docker:感觉自己萌萌哒相关的知识,希望对你有一定的参考价值。
神奇的Docker
Docker可谓是近些年越来越受欢迎的一个工具。至于Docker是个什么玩意儿,私以为没必要介绍了,不知道的自然不会对这个文章感兴趣,感兴趣的自然也有办法知道Docker是个什么玩意儿。
那就直接进入正题吧。
都知道,在Docker容器里运行的应用程序,是工作在一个虚拟的环境里的,在这个环境里,进程ID、文件系统、网络等等,全都是“假象”,都是Docker通过某种方式“捏造”出来的。像个沙箱,程序只知道傻乎乎地在其中运行,并不知道自己已经处在Matrix之中了。
Docker能够对程序所访问的资源进行偷梁换柱,而程序丝毫没有察觉,依旧感觉自己萌萌哒。那,Docker是怎么做到这么神奇的一件事的呢?
Prerequisites
了解Docker的功能
Linux编程
使用搜索引擎的能力
脑子
Linux namespace
与其是说Doccker是怎么做到这一件事的,不如说是Linux kernel已经提供了做这么一个事的可能性。明白这一点很重要。Docker之所以能够实现这么神奇的一个效果,归功于Linux kernel开发者们早已在内核中提供了能够实现这样功能的接口。
也就是说,Docker实际上是在内核提供的这个功能上做了丰富的封装,让内核提供的这个feature能够更好地被使用而已。
那什么是namespace呢?命名空间?嗯,命名空间。这么说还是有些难以理解。或者说,这里讲的namespace就像是程序世界里的VR。
人类通过自身的感官感知这个世界,当我们戴上VR头盔的时候,我们看到的是另外一个世界,是一个和没有戴头盔的小伙伴不同的世界。
而程序则是通过syscall去感知和自己所在的世界,也是通过syscall和自己所在的世界进行交互。syscall就像是它们的眼睛、皮肤和手。至于什么是syscall,这个就说来话长了,syscall充当着程序和OS内核之间交互的桥梁,程序访问文件、进行网络通讯等,都需要经过syscall。
而Docker则是通过namespace这个内核提供的feature,给程序戴上了VR头盔,让程序以为自己在一个鲜花于草坪的世界里,实际上周围都是臭水沟。
clone才是一切的根源
在Linux中,clone这个系统调用可以用来创建一个新的进程,在man中可以看到clone这个函数的原型:
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
clone具体具备些什么功能,可以参考Linux编程相关的资料。clone能干啥呢?克隆?可以这么理解,就是它能够“生”出一个活着的(正在运行的)程序,也就是创建一个新的进程。
不知道从哪个版本开始,Linux内核为clone这个syscall提供了好几个与namespace相关的选项,参见man clone,这些选项可以从flags参数传入,这几个选项分别是:
CLONE_NEWIPC
CLONE_NEWNET
CLONE_NEWNS
CLONE_NEWPID
CLONE_NEWUTS
CLONE_NEWUSER
也就是在使用clone的时候可以传入这些选项:
pid_t child_pid = clone(child_func, child_stack, CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
这些选项都能起个什么作用呢?程序在用clone创建新的进程的时候,我们可以告诉clone,新产生的小宝宝需要终生佩戴一个什么样的VR设备,让他在我们“捏造”的“幻境”中了此一生。
看着像是个悲剧。
拿CLONE_NEWPID开刀
当然了,直接看man clone就知道这些个选项到底起个什么作用了。这里还是找个demo来试一下:
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/utsname.h>
#include <unistd.h>
#define STACK_4M (4 * 1024 * 1024)
static char child_stack[STACK_4M];
static int child_func() {
printf("PID: %ld\n", (long)getpid());
printf("PPID: %ld\n", (long)getppid());
return 0;
}
int main() {
pid_t child_pid = clone(child_func, child_stack + STACK_4M, SIGCHLD, NULL);
printf("clone() = %ld\n", (long)child_pid);
waitpid(child_pid, NULL, 0);
return 0;
}
程序启动后,通过clone调用,创建了一个进程,新的进程运行后所执行的代码由child_func指定。在上述例子的clone中第三个参数并没有指定任何一个和namespace相关的参数。也就是说这是个幸运儿,他出生后是活在真实的世界中的。所以你可以看到这样一个输出:
clone() = 2276
PID: 2276
PPID: 2275
具体的数字视情况而定,关键是clone的返回值要和PID相等,这就妥了。clone返回的是子进程的ID,也就对应着child_func这段子进程的代码中getpid的返回值,这两者应该一致。
也就是说,这个demo中,爸爸还是知道谁是自己的孩子的,孩子也知道谁是自己的爸爸的。
好,那么在clone函数中加入新的flag,把刚生下来的孩子直接扔到VR中长大吧,这样他也就不认得自己的爸爸了。
pid_t child_pid = clone(child_func, child_stack + STACK_4M,
CLONE_NEWPID | CLONE_NEWUSER | SIGCHLD, NULL);
这里在clone参数中添加了两个选项:
CLONE_NEWPID
CLONE_NEWUSER
也就是说,在创建进程的时候产生一个新的PID namespace和user namespace。那么,新产生的进程所看到的PID和用户相关的“世界”就是由clone捏造的VR世界了。程序运行后输出如下:
clone() = 2533
PID: 1
PPID: 0
这下,新产生的进程已经不认得真实的自己了(进程获取自己的PID得到的是1),也不认得自己的爸爸了(获取父进程的ID得到的是0)。但是它的爸爸还是认得儿子的(父进程中clone的返回值依旧还像个正常的PID)。
我们在child_func里再加入一行代码:
system("whoami");
打印出子进程当前所在的用户吧。运行一看:
clone() = 2625
PID: 1
PPID: 0
nobody
这家伙连创建自己的上帝(用户)都不知道了,这归功于clone在创建子进程的时候指定了CLONE_NEWUSER选项,即子进程在一个新的user namespace中,它也感知不到namespace之外的用户。
为了更进一步验证在新的namespace中会发生些什么事情,我们再对child_func进行进一步修改:
static int child_func() {
printf("PID: %ld\n", (long)getpid());
printf("PPID: %ld\n", (long)getppid());
pid_t pid = fork();
if(pid == 0) {
char * const args[] = { "/bin/bash", NULL};
execv(args[0], args);
} else {
wait(NULL);
}
return 0;
}
这个函数中会fork出一个新的进程,并在其中启动一个bash,这样,我们就可以在新的namespace中自由地验证对namespace的各种猜想了。
程序启动后,你看到的界面则是这样的:
clone() = 2824
PID: 1
PPID: 0
nobody@ubuntu-14:~$
这会儿,程序打开了一个新的shell,里头的用户是nobody(因为创建新进程后没有做用户相关的设定)。
这里的用户和前面例子中whoami显示出来的用户一致,也就是说这个shell目前也是运行在“VR环境”中。
在当前这个shell中执行与PID和用户相关的命令,我们看到的是一个全新的环境:
nobody@ubuntu-14:~$ echo $$
2
nobody@ubuntu-14:~$ whoami
nobody
而退出这个shell之后,执行的结果如下:
vagrant@ubuntu-14:~/shared$ echo $$
2712
vagrant@ubuntu-14:~/shared$ whoami
vagrant
这就说明,在child_func中启动的shell,运行在一个完全不同的环境之中,这个环境之中的的程序看不到外面的世界。它不知道系统中还运行了什么东西,也不知道还有些什么用户。
那我运行个ps aux或者htop呢?
结果出乎意料,说好的是运行在独立的环境中呢?说好的看不到外面的世界呢?咋这就把外面的一堆程序都列出来了?
还有
其实,这里并不是namespace的锅,而是ps和htop这样的命令,是通过挂载于/proc下的文件系统来获取信息的,而我们只给clone指定了CLONE_NEWPID和CLONE_NEWUSER两个namespace相关的参数。
那根据man clone,就得再加上CLONE_NEWNS咯。
别急,加上CLONE_NEWNS还没完事儿,还得将新的环境下的proc给重新mount上去,这样才算真正完事儿了。
也就是说,clone这下应该这么用了:
pid_t child_pid = clone(child_func, child_stack + STACK_4M,
CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | SIGCHLD, NULL);
并且,在child_func中加入挂载proc的代码:
static int child_fn() {
printf("PID: %ld\n", (long)getpid());
printf("PPID: %ld\n", (long)getppid());
char *mount_point = "/proc";
mkdir(mount_point, 0555);
if(mount("proc", mount_point, "proc", 0, NULL) == -1) {
printf("error when mount\n");
}
pid_t pid = fork();
if(pid == 0) {
char * const args[] = { "/bin/bash", NULL};
execv(args[0], args);
} else {
wait(NULL);
}
return 0;
}
这样,重新编译并运行程序,打开在新的namespace下的shell,执行ps aux看到的将是这样:
nobody@ubuntu-14:~$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
nobody 1 0.0 0.0 5228 88 pts/0 S 13:48 0:00 ./a.out
nobody 2 1.0 0.4 24024 5072 pts/0 S 13:48 0:00 /bin/bash
nobody 15 0.0 0.1 18452 1252 pts/0 R+ 13:48 0:00 ps aux
对,这下我们的整个shell看起来就像是运行在一个非常干净的系统中一样的了,没有任何多余的进程。也就是说,在新的namespace中运行的各种程序,都成功地被“VR”给欺骗了。
而实际上,我们在原有的环境中执行ps aux后看到的进程,肯定不止这么点的。
总结
通过现象的对比和代码,我们也算体会了一把namespace的作用,就像一个沙盒,能够建立出一个干净、隔离的新环境,让应用运行在新的环境下,其中的程序只能看到沙箱之中的内容,无法感知也无法干涉到外面的世界。
除了上述例子中使用到的参数,clone还有几个用于控制其它namespace的参数,比如网络、进程间通讯等namespace,这些选项告诉了clone分别应该在哪些地方为子进程建立隔离的namespace。
而Docker中实现虚拟化功能的核心之一,也就正是这里所说到的namespace。
The end。文中若有疏漏,还望指教。
作者昵称:不是油条,全幹工程师
原文:
https://zhuanlan.zhihu.com/p/28273941
---END---
K8S培训推荐
Kubernetes线下实战培训,采用3+1新的培训模式(3天线下实战培训,1年内可免费再次参加),资深一线讲师,实操环境实践,现场答疑互动,培训内容覆盖:Docker架构、镜像、数据存储、网络、以及最佳实践。Kubernetes实战内容,Kubernetes设计、Pod、常用对象操作,Kuberentes调度系统、QoS、Helm、网络、存储、CI/CD、日志监控等。<>
北京站、上海站:1月4-6日; 咨询/报名:曹辉/15999647409
以上是关于Docker:感觉自己萌萌哒的主要内容,如果未能解决你的问题,请参考以下文章