学习docker得首先知道docker的起源,docker是由虚拟机延伸出来的。
虚拟机主要是主机级虚拟机。
有两种类型:
Type I:
直接在硬件平台上装一个虚拟机管理器,也就是说在硬件上是不用装宿主机模式的,直接上虚拟机管理器
没有任何主机是直接跑在硬件上,所有操作系统都是跑在虚拟机上的叫做类型一
实现机制:
虚拟出来的应该是独立的硬件平台,因此用户想使用虚拟机就必须在自己在虚拟机上部署一个完整意义上的自操作系统(自己在上面装一个内核,内核上有用户空间,用户空间里面跑进程)
内核核心作用在于资源分配和管理,真正在用户空间跑的应用程序才是能产生生成力的,比如国内提供的web服务,内核上很少运行web服务,我们熟悉的nginx,httpd都是运行在用户空间的,真正能产生生产力的是用户空间的应用进程,而不是出于通用目的而设计的资源管理平台,操作系统内核。但是内核是必须有的,我们要想在一组硬件平台上使用软件程序,现在的软件基本都是针对于内核的系统调用和库调用变化的,必须安装库,安装内核,安装环境,有些进程为了协调也必须用内核来实现。
Type II:
VirtualBox开源虚拟机软件
VMware Workstation KVM
首先有宿主机,在物理机上先装一个主机操作系统(Host OS)
也称为叫宿主机操作系统,在宿主机上装一个VM(virtual machinemanager)
如果只运行一个nignx服务,我们就得安装一个内核
从二进制上考虑,如果一个进程想要运行需要实现两级分配和调派
这里自己虚拟机的内核就已经实现了一次所谓的虚拟化cpu调度以及IO的调度以及IO的管理,然而真正的虚拟机本身也是被宿主机内核管理的应用层的进程或者抽象层的进程,因此还需要被我们宿主机内核或者虚拟机管理器再调度一次,这里面浪费资源,额外的开销很大。这就是主机级虚拟机的弊端,我们有时候创建虚拟机就是为了运行一个或几个有限的富有生产责任的进程而已,为此代价太大,我们需要减少中间层和中间环节,有效提高效率。
要想提升效率,可以抽调虚拟机中间的内核,只保留进程,但会出现问题,加虚拟机目的是为了隔离,如果要运行两个nginx监听80端口,但是只有一个80端口。如果是不同ip地址还可以实现,假设只有一个IP地址对外通信运行两个nginx,甚至多个nginx,他们没有办法使用相同套接字,使用默认80,虚拟机可以实现。但创建虚拟机的目的如果只是为了运行进程为啥不在宿主机上运行,而非要在虚拟机,创建虚拟机的目的是为了资源隔离使用,如果进程出现问题最多损坏虚拟机环境而已,隔离是真正追求的目标,我们虽然抽调内核但是需要让每一个相应的进程或者每一组进程彼此之间是互相不可达的,就好像运行在两个不同的虚拟机甚至是物理机一样互不干扰,只不过恰巧共享同一层底层资源而已,因此追求的环境变成这个样子:
隔离出来的进程跑在隔离环境内,进程跑在用户空间,内核提供的是内核空间,我们需要进程跑在隔离环境中,其实我们隔离的是用户空间。把用户空间隔离成多组,彼此之间互相不干扰、一个用户空间内只运行一个或部分进程,但无论怎么隔离一定有一个像zenke kvm一样,甚至一般都是第一个是有特权的我们通过它来管理其他的用户空间,随后我们启动进程让进程运行在用户空间当中,那么众多用户空间能共享底层同一个内核,是由同一个内核管理的,但是在自己运行时候能够看到的边界却是自己所属的用户空间的边界,这种隔离没有主机级虚拟化隔离的彻底。用户空间拿来放进程,给进程提供运行环境并且还能保护其内核的进程不受其他进程的干扰就叫做容器
在这里要补充一个概念:(LXC)
LXC为Linux Container的简写。Linux Container容器是一种内核虚拟化技术,可以提供轻量级的虚拟化,以便隔离进程和资源,而且不需要提供指令解释机制以及全虚拟化的其他复杂性。相当于C++中的NameSpace。
容器有效地将由单个操作系统管理的资源划分到孤立的组中,以更好地在孤立的组之间平衡有冲突的资源使用需求。与传统虚拟化技术相比,它的优势在于:
(1)与宿主机使用同一个内核,性能损耗小;
(2)不需要指令级模拟;
(3)不需要即时(Just-in-time)编译;
(4)容器可以在CPU核心的本地运行指令,不需要任何专门的解释机制;
(5)避免了准虚拟化和系统调用替换中的复杂性;
(6)轻量级隔离,在隔离的同时还提供共享机制,以实现容器与宿主机的资源共享。
总结:Linux Container是一种轻量级的虚拟化的手段。
Sourceforge上有LXC这个开源项目。LXC项目本身只是一个为用户提供一个用户空间的工具集,用来使用和管理LXC容器。
LXC真正的实现则是靠Linux内核的相关特性,LXC项目只是对此做了整合。
基于容器的虚拟化技术起源于所谓的资源容器和安全容器。
LXC在资源管理方面依赖于Linux内核的cgroups子系统,cgroups子系统是Linux内核提供的一个基于进程组的资源管理的框架,可以为特定的进程组限定可以使用的资源。
LXC在隔离控制方面依赖于Linux内核的namespace特性,具体而言就是在clone时加入相应的flag(NEWNS NEWPID等等)。关于namespace和cgroups在后面进行详细介绍。
容器之前叫jail,目的就是为了能运行一个进程,不受其他的干扰
jail复刻到linux系统变成了vserver,vserver的核心是chroot,chroot切根,真正的根是整个文件系统的根,实现切根之后,加入我们在一个子目录下也创建一个hfs定义的,应该具有根目录下子目录的结构,chroot就可以把那个子目录当根一样来使用,用户空间里的进程也以为它就是根了,这就是切根。
chroot就是jail,但是它不能真正实现宿主机的特权用户空间或者说磁盘用户空间的隔离,因为chroot它所隔离的仅仅是你看上去的空间,因为底层还是同一个内核,隔离背后用户到底运行在特权模式还是非特权模式,如果需要访问模式访问指定资源的时候我们应该怎么植入,表明看上去只是chroot,其实背后是一堆技术的支撑
关于chroot的介绍:
chroot命令用来在指定的根目录下运行指令。chroot,即 change root directory (更改 root 目录)。在 linux 系统中,系统默认的目录结构都是以/,即是以根 (root) 开始的。而在使用 chroot 之后,系统的目录结构将以指定的位置作为/位置。
在经过 chroot 之后,系统读取到的目录和文件将不在是旧系统根下的而是新根下(即被指定的新的位置)的目录结构和文件,因此它带来的好处大致有以下3个:
增加了系统的安全性,限制了用户的权力;
在经过 chroot 之后,在新根下将访问不到旧系统的根目录结构和文件,这样就增强了系统的安全性。这个一般是在登录 (login) 前使用 chroot,以此达到用户不能访问一些特定的文件。
建立一个与原系统隔离的系统目录结构,方便用户的开发;
使用 chroot 后,系统读取的是新根下的目录和文件,这是一个与原系统根下文件不相关的目录结构。在这个新的环境中,可以用来测试软件的静态编译以及一些与系统不相关的独立开发。
切换系统的根目录位置,引导 Linux 系统启动以及急救系统等。
chroot 的作用就是切换系统的根位置,而这个作用最为明显的是在系统初始引导磁盘的处理过程中使用,从初始 RAM 磁盘 (initrd) 切换系统的根位置并执行真正的 init。另外,当系统出现一些问题时,我们也可以使用 chroot 来切换到一个临时的系统。
实现切根以后,使用chroot就能把子目录当根使用
一个用户空间它应该看到有这些组件:UTS(主机名和域名),Mount(根文件系统)每一个用户空间都要有自己独立的根文件系统,IPC(进程间通信的专用通道),IPC可以使用共享内存,semaphore。semaphore就是共享进程,一个进程把数据释放在内存空间中,另外一个就可以从里面获取基于unix socket文件的bash,这种空间就没有经过其他的配置,它压根就是通过内存来实现的。两个空间的进程运行在一个内存中,物理硬件只有一个,两个空间是否能用IPC通信呢?不可以,不然隔离就没有意义了,所以得隔离IPC,IPC在同一个用户空间內是隔离的,跨用户空间就不行。事实上,在底层内核本身管理时,整个内核之间任意任何进程之间可直接使用IPC通信的,只要有用户空间有独立的容器,我们就得把资源给它分隔开。
PID,在每一个用户空间中每一个进程它应该会从属于某个进程,因为进程都应该是有它的父进程所创建的,如果一个进程没有父进程,它应该是这个用户空间的init,一个系统运行无非就是两棵树,进程树和文件系统树。对当前用户空间来讲,既然认为自己是当前用户空间唯一的,就要给它一个假象,要么自己是init,要么它从属于某个init,否则这个空间内的进程将无法被管理。所谓,子进程由它的父进程所创建,子进程的终止和回收也是由他的父进程所操作的,如果init结束了,在init结束之前它要把子子孙孙全部送走,然后它才能放心离开,关机。在每一个用户空间当中,既然要自己独立管理,每一个用户空间都应该有自己的init,事实上对init只有一个,所以得对每一个用户空间做一个假的init,要么就叫init,要么在用户空间只能存在一个进程,只要进程一崩溃,这个空户空间就消失了,否则只要有多个进程就应该同属于某一个上帝使者init来管理,从另一个角度来讲,进程数所看到自己的id号是1,要么自己从属的某个id号是1,意味着有自己独立的进程数,叫做pid,也要互相隔离,pid为1的是init,在内核上真正id为1的只能有一个,但我们不得不为每一个用户空间伪造一个,就是为init机制创建一个假的幻象,在内核中能识别它们,但需要去伪装一个出来。
用户,用户组。每一个用户空间都需要有root,但这个root与以往的root不同,因为在一个内核上只能有一个root,如果每个用户空间都有root就麻烦了,可以随意删除,管理别的用户空间,隔离失去意义。给每一个用户空间给它伪装出一个root,这个root只能实现这样的效果在真正的系统上它只是一个普通用户,但是一旦对于用户空间来讲,我们可以把它伪装为id为0,只能在当前用户空间内为所欲为,把用户空间所有权都给它并把它伪装成一个用户空间root,在宿主机系统上,内核上它依然是一个普通用户。类似于把一个子目录它的属组都改成某个用户,然后chroot过去,他就对这个chroot根里面有所有权限,但是需要把它们让进程看起来是root才行,所以用户空间用户,用户组也需要做隔离。
ip地址。每一个用户空间都以为自己是这个系统上的唯一用户空间,所以应该有自己的ip地址,能不能都监听同一个80端口,运行nginx,所以从这个角度来讲每一个用户空间就相当于一个虚拟机,应该能看到自己专用的网卡,网络接口,有自己专有的TCP/IP协议栈,有自己专用的整个套接字平面,端口空间是0-635,这里面所有都可以用,另外一个用户空间进程也是,它看到的端口空间假如说是TCP,0-635的,假如没有被同一用户空间其他用户占用,它就一定可以用。两个容器之间进程可能还需要网络相互通信,每一个用户空间都以为自己是这个网络中运行的计算机,两个容器就是两个计算机,他们之间应该是能通过网络进行正常通信的。而内核级TCP/IP协议上是只有一个的,现在需要给每个用户空间创建一个可用的。内核级资源都是独立,只有一组,内核在最初设计时就是为了支撑单个用户空间的运行的。后来有这种运行所谓jail,vserver,所以出现了多个用户空间。
事实上上述每一种资源只要能在内核级切分为多个互相隔离的环境就把它称为名称空间Namespace。
Linux Namespace是Linux提供的一种内核级别环境隔离的方法,关于隔离的概念其实大家早已接触过:比如在光盘修复模式下,可以用chroot切换到其他的文件系统,chroot提供了一种简单的隔离模式:chroot内部的文件系统无法访问外部的内容。Linux Namespace在此基础上又提供了很多其他隔离机制。
当前,Linux 支持6种不同类型的命名空间。它们的出现,使用户创建的进程能够与系统分离得更加彻底,从而不需要使用更多的底层虚拟化技术。
主要是三个系统调用
clone() – 实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离。
unshare() – 使某进程脱离某个namespace
setns() – 把某进程加入到某个namespace