进程:
一旦“程序”被执行起来,它就从磁盘上的二进制文件,变成了计算机内存中的数据、寄存器里的值、堆栈中的指令、被打开的文件,以及各种设备的状态信息的一个集合。像这样一个程序运行起来后的计算机执行环境的总和称为进程
静态表现:存储在磁盘中的代码
动态表现:运行时计算机数据和状态的总和
容器的核心技术:通过约束和修改进程的动态表现,从而创造一个边界
涉及到的系统调用:
- clone() – 实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离。
- unshare() – 使某进程脱离某个namespace
- setns() – 把某进程加入到某个namespace
一 修改进程视图:namespace
namespace类型:
- PID Namespace
- Mount Namespace
- UTS Namespace
- IPC Namespace
- Network Namespace
- User Namespace
原理:在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。
二 约束:Cgroup
Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。此外,Cgroups 还能够对进程进行优先级设置、审计,以及将进程挂起和恢复等操作
操作:
#比如控制cpu
cd /sys/fs/cgroup/cpu
#创建一个控制组,也就是一个目录,会自动生成可以控制的配置文件
mkdir container
#比如可以用来限制进程在长度为 cfs_period 的一段时间内,只能被分配到总量为 cfs_quota 的 CPU 时间。
echo 20000 > /sys/fs/cgroup/cpu/container/cfs_quota_us
#20000 代表20ms,cpu.cfs_period_us一般是100ms,表示100ms内,被该控制组限制的进程只能使用 20 ms 的 CPU 时间
echo 被控制进程的pid > /sys/fs/cgroup/cpu/container/tasks
Linux Cgroups 的设计还是比较易用的,简单粗暴地理解呢,它就是一个子系统目录加上一组资源限制文件的组合。而对于 Docker 等 Linux 容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的 PID 填写到对应控制组的 tasks 文件中就可以了。
注意: Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效。
三 rootfs 根文件系统
切换进程的根目录(Change Root)。
- 拷贝必要的文件
- pivot_root 系统调用,如果系统不支持,使用 chroot。
需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。
同一台机器上的所有容器,都共享宿主机操作系统的内核。
docker安装
- 更新软件包索引:首先,使用以下命令更新系统的软件包索引:
sudo yum check-update
- 安装需要的软件:安装一些必要的软件包,以便使用 Docker 的软件源:
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
- 设置 Docker 软件源:使用以下命令设置 Docker CE 软件源:
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
- 安装 Docker CE:现在,可以使用以下命令安装 Docker CE:
sudo yum install docker-ce
- 启动 Docker 服务:安装完成后,可以启动 Docker 服务并设置开机自启:
sudo systemctl start docker sudo systemctl enable docker
- 验证 Docker 安装:最后,您可以运行以下命令来验证 Docker 是否成功安装:
sudo docker run hello-world
如果 Docker 安装正确,将会输出 “Hello from Docker!” 的信息。
- 配置用户权限(可选):如果您希望无需使用 sudo 来运行 Docker 命令,可以将当前用户添加到 Docker 用户组中。首先,执行以下命令将当前用户添加到 Docker 用户组:
sudo usermod -aG docker $USER
docker镜像的结构
- 只读层:最基础的文件
- init层:可动态配置读写,但是不能commit的层
- 读写层:增量修改可以commit和push的层
Dockerfile
# 使用官方提供的Python开发镜像作为基础镜像
FROM python:2.7-slim
# 将工作目录切换为/app
WORKDIR /app
# 将当前目录下的所有内容复制到/app下
ADD . /app
# 使用pip命令安装这个应用所需要的依赖
RUN pip install --trusted-host pypi.python.org -r requirements.txt
# 允许外界访问容器的80端口
EXPOSE 80
# 设置环境变量
ENV NAME World
# 设置容器进程为:python app.py,即:这个Python应用的启动命令
CMD ["python", "app.py"]
默认情况下,Docker 会提供一个隐含的 ENTRYPOINT,即:/bin/sh -c。所以,在不指定 ENTRYPOINT 时,比如这个例子里,实际上运行在容器里的完整进程是:/bin/sh -c "python app.py",即 CMD 的内容就是 ENTRYPOINT 的参数
注意:Dockerfile 中的每个原语执行后,都会生成一个对应的镜像层。即使原语本身并没有明显地修改文件的操作(比如,ENV 原语),它对应的层也会存在。只不过在外界看来,这个层是空的。
可以使用 docker commit 指令,把一个正在运行的容器,直接提交为一个镜像
docker常用命令:
#镜像相关命令:
docker images:列出本地主机上的镜像列表。
docker pull <image_name>:从 Docker 镜像仓库下载镜像。
docker build -t <image_name> <dir_path>:根据 Dockerfile 构建镜像。
docker push <image_name>:将本地镜像推送到镜像仓库。
#容器相关命令:
docker ps:列出正在运行的容器。
docker ps -a:列出所有容器,包括已停止的容器。
docker run <image_name>:根据镜像创建并启动一个容器。
docker stop <container_id>:停止一个运行中的容器。
docker rm <container_id>:删除一个已停止的容器。
#日志和进入容器命令:
docker logs <container_id>:查看容器的日志。
docker exec -it <container_id> <command>:在运行中的容器中执行命令,并进入容器的 shell 终端。
#网络相关命令:
docker network ls:列出 Docker 网络。
docker network create <network_name>:创建一个自定义网络。
docker network connect <network_name> <container_name>:将容器连接到指定网络。
#数据卷相关命令:
docker volume ls:列出 Docker 数据卷。
docker volume create <volume_name>:创建一个数据卷。
docker volume inspect <volume_name>:查看数据卷的详细信息。
docker volume rm <volume_name>:删除一个数据卷。
#查看docker容器运行的进程号
docker inspect --format '{{ .State.Pid }}' 4ddf4638572d
25686
docker exec 是怎么做到进入容器里的?
每个进程的每种 Linux Namespace,都在它对应的 /proc/[进程号]/ns 下有一个对应的虚拟文件,并且链接到一个真实的 Namespace 文件上。
setns() 的 Linux 系统调用
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE);} while (0)
int main(int argc, char *argv[]) {
int fd;
fd = open(argv[1], O_RDONLY);
if (setns(fd, 0) == -1) {
errExit("setns");
}
execvp(argv[2], &argv[2]);
errExit("execvp");
}
- argv[1],即当前进程要加入的 Namespace 文件的路径/proc/25686/ns/net
- argv[2]则是你要在这个 Namespace 里运行的进程 /bin/bash。
open() 系统调用打开了指定的 Namespace 文件,并把这个文件的描述符 fd 交给 setns() 使用。在 setns() 执行后,当前进程就加入了这个文件对应的 Linux Namespace 当中了。
数据卷Volumn
$ docker run -v /test ...
$ docker run -v /home:/test ...
- 命令1:由于你并没有显示声明宿主机目录,那么 Docker 就会默认在宿主机上创建一个临时目录 /var/lib/docker/volumes/[VOLUME_ID]/_data,然后把它挂载到容器的 /test 目录上。
- 命令2,Docker 就直接把宿主机的 /home 目录挂载到容器的 /test 目录上。
原理:在 rootfs 准备好之后,在执行 chroot 之前,把 Volume 指定的宿主机目录(比如 /home 目录),挂载到指定的容器目录(比如 /test 目录)在宿主机上对应的目录(即 /var/lib/docker/aufs/mnt/[可读写层 ID]/test)上,这个 Volume 的挂载工作就完成了。
由于执行这个挂载操作时,“容器进程”已经创建了,也就意味着此时 Mount Namespace 已经开启了。所以,这个挂载事件只在这个容器里可见。你在宿主机上,是看不见容器内部的这个挂载点的。这就保证了容器的隔离性不会被 Volume 打破。
注意:这里提到的"容器进程",是 Docker 创建的一个容器初始化进程 (dockerinit),而不是应用进程 (ENTRYPOINT + CMD)。dockerinit 会负责完成根目录的准备、挂载设备和目录、配置 hostname 等一系列需要在容器内进行的初始化操作。最后,它通过 execv() 系统调用,让应用进程取代自己,成为容器里的 PID=1 的进程。
另外,由于容器的镜像操作,比如 docker commit,都是发生在宿主机空间的。而由于 Mount Namespace 的隔离作用,宿主机并不知道这个绑定挂载的存在。所以,在宿主机看来,容器中可读写层的 /test 目录(/var/lib/docker/aufs/mnt/[可读写层 ID]/test),始终是空的。所以虽然这个 /test 目录里的内容,挂载在容器 rootfs 的可读写层,但是不会被 docker commit 提交
Linux 的绑定挂载(bind mount)机制。它的主要作用就是,允许你将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上。并且,这时你在该挂载点上进行的任何操作,只是发生在被挂载的目录或者文件上,而原挂载点的内容则会被隐藏起来且不受影响。
容器和虚拟机的对比
- 虚拟机实现了更深层次的资源隔离,而docker还是依赖宿主机操作系统的隔离性,多个容器共享的是同一个宿主机的操作系统内核。不能跨操作系统运行docker容器。另外还有很多对象是不能被namespace化的,比如时间
- 虚拟机必须要创建一个完整的操作系统,带来额外的资源消耗与占用,docker更加轻量级(敏捷,高性能)
扩展博客:Docker基础技术:Linux Namespace(上) | 酷 壳 - CoolShell
遗留问题:
1 容器和应用共生死,生命周期
2 /proc文件系统,不了解cgroup的限制,在容器里读取到的 CPU 核数、可用内存等信息都是宿主机上的数据