程序运行起来就是进程。所谓的容器技术,其实就是对进程做一个限制,让他有边界。专业的说法是约束和修改进程的动态表现。
具体来说就是1. 启用 Linux Namespace 配置;2. 设置指定的 Cgroups 参数;3. 切换进程的根目录(Change Root)。
隔离与限制
约束进程依靠的是cgroup技术,隔离,或者叫修改进程的动态表现,依靠的是namespace技术。
隔离
修改进程的动态表现指的是它的“视线”受到了限制,看不到自己“不该看到的”进程、硬盘设备等。
进程通过clone系统调用创建。
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
如果我们在创建的时候给定参数,进程自身就会看不见其他的进程,认为自己是1号进程。而对OS来说,这个新创建的进程该是多少号,就是多少号。除了pid,Linux 操作系统还提供了 Mount、UTS、 IPC、Network 和 User 这些 Namespace。
约束
隔离主要是强调让进程看不见不该看的东西。但事实上这种“看不见”是能力很弱的。容器中的进程终究是进程,有很大地可能影响宿主机。比如吃掉大量资源啊、修改系统时间啊,等等。
为了不让容器中的进程肆意妄为,我们需要Linux 控制组。它能限制进程使用的资源的上限。英文名叫Cgroups。
Cgroups会表现成文件系统的样子。我们想限制进程能使用的资源,其实修改对应的文件就好了。
容器还需要什么?
容器还需要自己的文件系统。
从之前的namespace知识可以了解到,我们可以为进程单独的mount一个文件系统。只有它自己能使用这个文件系统。注意,只通过mount namespace修改挂载点,进程看到的文件系统是不会变化的。需要重新做一下挂载才行。docker linux帮我们自动做了这些事情。我们单独mount的文件系统,应该是操作系统原有的文件系统格式一样,这样看起来才像沙箱。我们把这种文件系统叫做rootfs。
rootfs很小,也就20M左右。但是我们有的时候希望对文件系统做些修改,并进行保留。比如我们想在文件系统里搭一个golang环境,并且希望其他朋友可以复用,这就要求我们保存我们的文件系统,这怎么办呢?
答案是通过layer结构解决这个问题。我们每一组修改都会在原有的文件系统上多一个层。这种增量更新方式让我们不需要更改原有文件系统,就能保存更新。可读写层会保存我们的操作(惰性的)。初始时为空,增加修改删除文件会反映在这个层上。
删除怎么反映呢?其实是通过创建一个.wh.xxx文件去shadow只读层的文件这种方式来反映的。修改的话是通过cooy on write。修改文件系统会把文件复制到可读写层进行修改。由于查找文件是自上而下的,所以这些修改会被看到。init层也是可以修改的,通常保存一些与当前容器相关的内容,不希望被保存。