容器与进程的关系
一个程序是以二进制的方式存储于计算机的磁盘上的,当程序开始运行时,会将数据加载到内存中,CPU执行程序的指令并结合各种寄存器,协调修改状态,这一过程就是 进程
容器是一种应用沙盒技术,利用容器把应用包裹起来,使得应用与应用之间能够互不干扰,而且通过包装完整应用能够使得应用更好的迁移然后快速部署。
如何实现这个包裹应用以及实现应用隔离是容器技术的核心问题, Linux的 Namespace 技术以及 Cgroups很好的解决了这个问题
Docker也是核心也是借助这两项技术,通过约束和修改进程 的动态表现,从而为整个应用创造出了"边界"
Cgroups技术是用来制造约束的主要手段,Namespace技术则是用来修改进程视图的主要方法
当我们启动一个docker容器,并使用进入命令行交互界面
arduino
docker run -it busybox /bin/sh
进入这个容器执行ps指令
bash
/ # ps
PID USER TIME COMMAND
1 root 0:00 /bin/sh
6 root 0:00 ps
可以发现在docker里最开始执行的/bin/sh成为了这个容器的1号进程,可以看到这个容器里面的进程以及宿主机的进程号进行了隔离,容器里面的进程已经看不到宿主机中的其他进程
在宿主机查看docker容器
csharp
[root@bogon cgroup]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
50ad203d48d4 busybox "/bin/sh" 4 minutes ago Up 4 minutes intelligent_hawking
[root@bogon cgroup]# docker top 50ad203d48d4
UID PID PPID C STIME TTY TIME CMD
root 2841 2822 0 02:10 pts/0 00:00:00 /bin/sh
容器中的/bin/sh在宿主机上以2841号进程在运行,但是在容器内部它是1号进程,所以容器运行的本质是一种"特殊的进程"
这就是Linux PID Namespace机制,这个机制可以在创建进程的时候,让进程可以重新编号成1号进程,在unix系统内,1号进程是有着特权,屏蔽信号,检查所有进程状态,回收子进程等,这就是PID Namespace隔离进程视图的意义所在
clone()系统调用模拟构建容器
其实PID Namespace是作为创建一个进程的可选参数,在Linux系统中我们创建进程的系统调用是clone(),这里借用耗耳叔的代码,创建一个应用进程并在该进程利用clone调用生成子进程:
arduino
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
/* 定义一个给 clone 用的栈,栈大小1M */
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
"/bin/bash",
NULL
};
int container_main(void* arg)
{
printf("Container [%5d] - inside the container!\n", getpid());
execv(container_args[0], container_args);
printf("Something's wrong!\n");
return 1;
}
int main()
{
printf("Parent - start a container!\n");
int container_pid = clone(container_main, container_stack+STACK_SIZE,
CLONE_NEWPID| SIGCHLD, NULL);
waitpid(container_pid, NULL, 0);
printf("Parent - container stopped!\n");
return 0;
}
运行该程序
css
[root@bogon ~]# ./test
Parent - start a container!
Container [ 1] - inside the container!
可以发现在创建的子进程中pid已经成为了1。
除了PID Namespace之外 Linux namespace可以分为以下几个分类:
Mount Namespace(挂载命名空间) | 允许进程在不同的命名空间中看到不同的文件系统挂载点和层次结构。 |
---|---|
UTS Namespace(UTS命名空间) | 允许进程在不同的命名空间中具有不同的主机名和域名。 |
PID Namespace(PID命名空间) | 允许进程在不同的命名空间中具有不同的PID。 |
Network Namespace(网络命名空间) | 每个Network Namespace都有自己的网络设备、IP地址、路由表和网络连接,允许进程在不同的命名空间中具有不同的网络环境。 |
IPC Namespace(IPC命名空间) | 每个IPC Namespace都有自己的System V IPC对象(如消息队列、共享内存和信号量),允许进程在不同的命名空间中具有不同的IPC资源。 |
User Namespace(用户命名空间) | 每个User Namespace都有自己的用户和用户组标识符(UID和GID),允许进程在不同的命名空间中具有不同的用户身份。 |
Cgroup Namespace(控制组命名空间) | 每个Cgroup Namespace都有自己的控制组层次结构,允许进程在不同的命名空间中具有不同的资源控制策略。 |
这些命名空间可以独立地创建、销毁和隔离,从而为进程提供了一种轻量级的虚拟化机制,使得不同的进程可以在相互独立的环境中运行,而不会相互干扰。
Cgroup
Cgroup是Linux内核用来限制,控制与分离一个进程组群的资源(如:CPU,内存,磁盘输入输出等)
Linux把Cgroup实现成了一个file system, 在系统中查看 /sys/fs/cgroup目录可以看到cgroup目前提供的能力
sql
[root@bogon cgroup]# ll /sys/fs/cgroup/
total 0
dr-xr-xr-x. 6 root root 0 Dec 13 22:25 blkio
lrwxrwxrwx. 1 root root 11 Dec 13 22:25 cpu -> cpu,cpuacct
lrwxrwxrwx. 1 root root 11 Dec 13 22:25 cpuacct -> cpu,cpuacct
dr-xr-xr-x. 6 root root 0 Dec 13 22:25 cpu,cpuacct
dr-xr-xr-x. 3 root root 0 Dec 13 22:25 cpuset
dr-xr-xr-x. 6 root root 0 Dec 13 22:25 devices
dr-xr-xr-x. 3 root root 0 Dec 13 22:25 freezer
dr-xr-xr-x. 3 root root 0 Dec 13 22:25 hugetlb
dr-xr-xr-x. 6 root root 0 Dec 13 22:25 memory
lrwxrwxrwx. 1 root root 16 Dec 13 22:25 net_cls -> net_cls,net_prio
dr-xr-xr-x. 3 root root 0 Dec 13 22:25 net_cls,net_prio
lrwxrwxrwx. 1 root root 16 Dec 13 22:25 net_prio -> net_cls,net_prio
dr-xr-xr-x. 3 root root 0 Dec 13 22:25 perf_event
dr-xr-xr-x. 6 root root 0 Dec 13 22:25 pids
dr-xr-xr-x. 3 root root 0 Dec 13 22:25 rdma
dr-xr-xr-x. 6 root root 0 Dec 13 22:25 systemd
在各个子目录下可以去建立目录,在对应的子目录下建立的目录就可以控制进程的资源
以一个控制进程的CPU分配时间为例
1.首先在/sys/fs/cgroup/cpu中创建一个testcg目录
sql
[root@bogon cpu]# mkdir testcg
[root@bogon cpu]# cd testcg/
[root@bogon testcg]# ll
total 0
-rw-r--r--. 1 root root 0 Dec 14 04:25 cgroup.clone_children
-rw-r--r--. 1 root root 0 Dec 14 04:25 cgroup.procs
-r--r--r--. 1 root root 0 Dec 14 04:25 cpuacct.stat
-rw-r--r--. 1 root root 0 Dec 14 04:25 cpuacct.usage
-r--r--r--. 1 root root 0 Dec 14 04:25 cpuacct.usage_all
-r--r--r--. 1 root root 0 Dec 14 04:25 cpuacct.usage_percpu
-r--r--r--. 1 root root 0 Dec 14 04:25 cpuacct.usage_percpu_sys
-r--r--r--. 1 root root 0 Dec 14 04:25 cpuacct.usage_percpu_user
-r--r--r--. 1 root root 0 Dec 14 04:25 cpuacct.usage_sys
-r--r--r--. 1 root root 0 Dec 14 04:25 cpuacct.usage_user
-rw-r--r--. 1 root root 0 Dec 14 04:25 cpu.cfs_period_us
-rw-r--r--. 1 root root 0 Dec 14 04:25 cpu.cfs_quota_us
-rw-r--r--. 1 root root 0 Dec 14 04:25 cpu.rt_period_us
-rw-r--r--. 1 root root 0 Dec 14 04:25 cpu.rt_runtime_us
-rw-r--r--. 1 root root 0 Dec 14 04:25 cpu.shares
-r--r--r--. 1 root root 0 Dec 14 04:25 cpu.stat
-rw-r--r--. 1 root root 0 Dec 14 04:25 notify_on_release
-rw-r--r--. 1 root root 0 Dec 14 04:25 tasks
在CPU目录下创建完testcg目录后会发现testcg目录同时出现了控制cpu资源的文件,其中tasks是控制对应进程的进程号,当然目前是为空的,所以现在这个目录不控制任何进程的CPU分配
2.创建一个能够CPU打满的死循环程序
arduino
#include <stdio.h>
int main(void)
{
int i = 0;
for(;;) i++;
return 0;
}
3.运行该程序之后,使用top命令, 对应的该程序的进程为3979,已经打满了cpu了
perl
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3979 root 20 0 4232 800 732 R 97.3 0.0 0:09.37 testCgroup
4.在之前创建的/sys/fs/cgroup/testcg目录中实现对该程序的限制
ruby
//1.将要控制的进程ID输入进入task
echo 3979 >> /sys/fs/cgroup/cpu/testcg/tasks
//2.限制该进程的CPU时间分配 20000us == 20ms ==》20%
//CPU的时间分配是按照100ms来的,所以这里的20ms就是20%CPU占用时间
echo 20000 > /sys/fs/cgroup/cpu/testcg/cpu.cfs_quota_us
5.输入之后继续使用top查看进程,发现确实被限制住了
perl
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3979 root 20 0 4232 800 732 R 19.6 0.0 1:20.80 testCgroup
上述的例子就是Cgroup对进程的控制,docker容器对资源的限制也是同理,当我们启动一个容器的在/sys/fs/cgroup下的各个子目录都会有一个docker目录
csharp
[root@bogon cpu]# ls /sys/fs/cgroup/cpu
cgroup.clone_children cpuacct.usage_all cpuacct.usage_user cpu.shares release_agent
cgroup.procs cpuacct.usage_percpu cpu.cfs_period_us cpu.stat system.slice
cgroup.sane_behavior cpuacct.usage_percpu_sys cpu.cfs_quota_us docker tasks
cpuacct.stat cpuacct.usage_percpu_user cpu.rt_period_us init.scope user.slice
cpuacct.usage cpuacct.usage_sys cpu.rt_runtime_us notify_on_release
查看docker目录以及运行的容器
sql
[root@bogon docker]# ls /sys/fs/cgroup/cpu/docker/
drwxr-xr-x. 2 root root 0 Dec 14 04:40 c91ecf1a2a52399a56d14326bf1964b27be408229217c5a67628b26f7f0ea51c
-rw-r--r--. 1 root root 0 Dec 14 03:11 cgroup.clone_children
-rw-r--r--. 1 root root 0 Dec 14 03:11 cgroup.procs
-r--r--r--. 1 root root 0 Dec 14 03:11 cpuacct.stat
-rw-r--r--. 1 root root 0 Dec 14 03:11 cpuacct.usage
-r--r--r--. 1 root root 0 Dec 14 03:11 cpuacct.usage_all
-r--r--r--. 1 root root 0 Dec 14 03:11 cpuacct.usage_percpu
-r--r--r--. 1 root root 0 Dec 14 03:11 cpuacct.usage_percpu_sys
-r--r--r--. 1 root root 0 Dec 14 03:11 cpuacct.usage_percpu_user
-r--r--r--. 1 root root 0 Dec 14 03:11 cpuacct.usage_sys
-r--r--r--. 1 root root 0 Dec 14 03:11 cpuacct.usage_user
-rw-r--r--. 1 root root 0 Dec 14 03:11 cpu.cfs_period_us
-rw-r--r--. 1 root root 0 Dec 14 03:11 cpu.cfs_quota_us
-rw-r--r--. 1 root root 0 Dec 14 03:11 cpu.rt_period_us
-rw-r--r--. 1 root root 0 Dec 14 03:11 cpu.rt_runtime_us
-rw-r--r--. 1 root root 0 Dec 14 03:11 cpu.shares
-r--r--r--. 1 root root 0 Dec 14 03:11 cpu.stat
-rw-r--r--. 1 root root 0 Dec 14 03:11 notify_on_release
-rw-r--r--. 1 root root 0 Dec 14 03:11 tasks
//查看当前启动的容器
[root@bogon docker]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c91ecf1a2a52 busybox "/bin/sh" 4 minutes ago Up 4 minutes nostalgic_leakey
查看当前运行的容器,会发现docker目录下第一个对应的子目录就是我们运行的容器目录,在该目录下,会发现一样有之前testcg中对应的控制文件,这个目录就与之前testcg控制进程的作用是一样。
在该目录中查看其控制的进程号以及容器运行时在宿主机的进程
yaml
[root@bogon c91ecf1a2a52399a56d14326bf1964b27be408229217c5a67628b26f7f0ea51c]# cat tasks
4239
docker top c91ecf1a2a52
UID PID PPID C STIME TTY TIME CMD
root 4239 4218 0 04:40 pts/0 00:00:00 /bin/sh
发现是一一对应的,这也就解释了Docker是利用Cgroup来控制容器的资源分配的了
容器与虚拟机的区分
通过上述的解释,就很容易理解的容器与虚拟机的区别了,容器运行的本质的还是一个进程,应用程序借助宿主机的内核来运行的。而虚拟化就必须运行一个完成Guest OS才能执行程序,这带来的不可避免的资源消耗和占用。用户应用在虚拟机中对宿主机的系统调用,会经过虚拟化软件这一层拦截和处理,计算资源,I/O等损耗非常大
相比之下的容器的"敏捷 "和"高性能 "是相比于虚拟机的最大的优势, 不过有利带来的弊端也很明显,就是容器的隔离性并不如虚拟机那么彻底
因为容器是共享宿主机的内核的,当一个容器镜像需要的是高版本内核,但是宿主机时低版本的内核时,运行就会出现问题,虚拟机并不存在这样的问题,它是有单独的系统的,并不会出现这种问题。
其次在Linux内核中,我们是通过Namespace技术来修改进程视图的,但是也有很多资源是没有办法Namespace化来隔离的,比如时间,当一个容器修改了通过系统调用修改了时间,那么整个宿主机时间都会被修改,这显然是不行的
当然现在也有基于虚拟化的容器以及独立内核容器的技术,如何在隔离与性能之间取得平衡还有待研究
经过上述就能发现,其实docker的作用其实是可以被替代的,这也就不难理解为什么现在的K8S使用了CRI和容器运行时取代之前的docker了,与docker解耦能够使得K8S适应更多的容器类型