$ docker run -it busybox /bin/sh
-it 参数告诉了 Docker 项目在启动容器后,需要给我们分配一个文本输入/输出环境,也就是TTY,跟容器的标准输入相关联,这样我们就可以和这个Docker容器进行交互了,而/bin/sh就是我们要在Docker容器里运行的程序。
所以,上面这条指令翻译成人类的语言就是:请帮我启动一个容器,在容器里执行/bin/sh,并给我分配一个命令行终端跟这个容器交互。
这样,我的linux机器就变成了一个宿主机,而一个运行着/bin/sh的容器,就跑在这个宿主机里面。
/ # ps
PID USER TIME COMMAND
1 root 0:00 /bin/sh
10 root 0:00 ps
可以看到,我们在Docker里最开始执行的/bin/sh,即使这个容器内部的第一号进程(PID=1),而这个容器里一共只有两个进程在运行,这就意味着,前面执行的/bin/sh,以及我们刚刚执行的ps,已经被Docker隔离在了一个跟宿主机完全不同的世界当中。
本来,每档我们在宿主机上运行一个/bin/sh程序,操作系统都会给它分配一个进程编号,比如PID=100,这个编号是进程的唯一标识,就像员工的工牌一样,所以PID=100,可以粗略地理解这个/bin/sh是我们公司里的地100个员工,而第一个员工就自然是老板这样统领全局的人物。
而现在,我们要通过Docker把这个/bin/sh程序运行在一个容器当中,这时候,Docker就会在这个第100个员工入职时给施一个“障眼法”,让他永远看不到前面的99个员工,更看不到老板,这样,他就会错误地以为自己就是公司里的第一个员工。
这种机制,其实就是对被隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程编号,比如PID=1,可实际上,他们在宿主机的操作系统里,还是原来的第100号进程。
这种技术,就是linux里面的namespace机制,而namespace的使用方法也非常有意思:它其实只是linux创建新进程的一个可选参数,我们知道,在Linux系统中创建线程的系统调用是clone(),比如:
int pid = clone(main_function, stack_size, SIGCHLD, NULL);
这个系统调用就会为我们创建一个新的进程,并且返回它的进程号PID。
而当我们用clone()系统调用创建一个新进程时,就可以在参数重指定CLONE_NEWPID参数,比如:
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
这时,新创建的这个继承将会“看到”一个全新的进程空间,在这个进程空间里,它的PID是1,之所以说“看到”,是因为这只是一个”障眼法“,在宿主机真实的进程空间里,这个进程的PID还是真实的数值,比如:101
当然,我们还可以多次执行上面的 clone() 调用,这样就会创建多个 PID Namespace,而每个 Namespace 里的应用进程,都会认为自己是当前容器里的第 1 号进程,它们既看不到宿主机里真正的进程空间,也看不到其他 PID Namespace 里的具体情况。
除了我们刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行“障眼法”操作。
比如,Mount Namespace,用于让被隔离进程只看到当前 Namespace 里的挂载点信息;Network Namespace,用于让被隔离进程看到当前 Namespace 里的网络设备和配置。
所以,Docker 容器这个听起来玄而又玄的概念,实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。
所以说,容器其实是一种特殊的进程而已。