共享内核
先看一下Linux下启动一个进程的大概过程,当在Shell里面启动一个程序的时候,会发生如下过程:
- Shell运行系统调用,通知内核启动一个指定位置上的程序
- 内核加载指定位置上的可执行文件以及它所依赖的动态库
- 初始化进程的地址空间,并把可执行文件映射到相应的内存地址(如果文件比较大的话,不会把整个可执行程序一次性加载到内存中,只是映射过去,然后利用内存缺页中断在需要的时候将所需的内容加载到内存中去)
- 开始调度进程(简单点说就是内核会让这个进程时不时的运行一会儿)
从上面的过程可以看出,内核唯一需要和应用层达成一致的是可执行程序的文件格式,不然内核就没法加载程序并调度进程。 当进程开始运行后,进程和内核的交互就是系统调用,当进程需要访问由内核管理的资源时,采用软件中断的方式和内核交互,每个系统调用都有一个中断号,并且这个号不会随着内核版本变化而变化。
关于内核所支持的系统调用请参考这里,系统调用因为涉及到用户态到内核态的切换,所以非常耗时,但Linux里面有一项叫做VDSO的技术,可以将内核态的一些地址直接映射到所有进程的用户态空间,于是系统调用就跟访问普通的变量一样了,比如我们常用的gettimeofday函数,就不需要切换到内核态,直接读取内核映射到进程内存空间的内容就可以了,非常快。
从上面可以看出,Linux内核和应用层进程之间的关系是松耦合的,只要保证两个条件:
- 内核能识别应用层程序的格式
- 应用层需要的系统调用内核能支持
由于Linux下可执行文件和动态库的格式以及系统调用的接口都比较稳定,所以不同的Linux发行版在大部分情况下都可以共享同一个内核。一般来说,新的内核兼容老的Linux发行版,但太老的内核不一定支持新的Linux发行版。如果你的应用对内核有特殊需求,那么应该考虑一下用容器是否是一个明智的选择。
容器与进程
容器的本质是一个或多个进程以及他们所能访问的资源的集合。启动一个容器的步骤大概就是:
-
配置好相关资源,如内存、磁盘、网络等。 配置资源就是往系统中添加一些配置,非常快
-
初始化容器所用到的文件目录结构
由于Linux下有COW(copy on write)的文件系统,如Btrfs、aufs,所以可以很快的根据镜像生成容器的文件系统目录结构。
-
启动进程
和启动一个普通的进程没有区别,对Linux内核来说,所有的应用层进程都是一样的从上面可以看出启动容器的过程中没有耗时的操作,这也是为什么容器能在毫秒级别启动起来的原因
由于Namespace和CGroups已经是Linux内核的一部分了,所以应用层运行的进程一定会属于某个Namespace和CGroups(如果没有指定,就属于默认的Namespace和CGroups),也就是说,就算我们不用Docker,所有的进程都已经运行在默认容器中了。对内核来说,默认容器中运行的进程和Docker创建的容器中运行的进程没有什么区别,就是他们所属的容器号不一样。
namespace
Namespace是对全局系统资源的一种封装隔离,使得处于不同namespace的进程拥有独立的全局系统资源,改变一个namespace中的系统资源只会影响当前namespace里的进程,对其他namespace中的进程没有影响。
目前,Linux内核里面实现了7种不同类型的namespace。
名称 宏定义 隔离内容
Cgroup CLONE_NEWCGROUP Cgroup root directory (since Linux 4.6)
IPC CLONE_NEWIPC System V IPC, POSIX message queues (since Linux 2.6.19)
Network CLONE_NEWNET Network devices, stacks, ports, etc. (since Linux 2.6.24)
Mount CLONE_NEWNS Mount points (since Linux 2.4.19)
PID CLONE_NEWPID Process IDs (since Linux 2.6.24)
User CLONE_NEWUSER User and group IDs (started in Linux 2.6.23 and completed in Linux 3.8)
UTS CLONE_NEWUTS Hostname and NIS domain name (since Linux 2.6.19)
- UTS namespace用来隔离系统的hostname以及NIS domain name。
- IPC namespace用来隔离System V IPC objects和POSIX message queues。其中System V IPC objects包含Message queues、Semaphore sets和Shared memory segments.
- Mount namespace用来隔离文件系统的挂载点, 使得不同的mount namespace拥有自己独立的挂载点信息,不同的namespace之间不会相互影响,这对于构建用户或者容器自己的文件系统目录非常有用。当前进程所在mount namespace里的所有挂载信息可以在/proc/[pid]/mounts、/proc/[pid]/mountinfo和/proc/[pid]/mountstats里面找到。
- PID namespaces用来隔离进程的ID空间,使得不同pid namespace里的进程ID可以重复且相互之间不影响。
- Network namespace用来隔离网络设备, IP地址, 端口等. 每个namespace将会有自己独立的网络栈,路由表,防火墙规则,socket等。
- User namespace用来隔离user权限相关的Linux资源,包括user IDs and group IDs,keys , 和capabilities.这是目前实现的namespace中最复杂的一个,因为user和权限息息相关,而权限又事关容器的安全,所以稍有不慎,就会出安全问题。
使用namespace创建一个容器
- busybox,准备container的根目录
# 创建一个单独的目录,后续所有的操作都在该目录下进行,目录名称无特殊要求
dev@ubuntu:~$ mkdir chroot && cd chroot
dev@ubuntu:~/chroot$ wget https://busybox.net/downloads/binaries/1.21.1/busybox-x86_64
# 创建new_root/bin目录,new_root将会是新容器的根目录,bin目录用来放busybox#由于/bin默认就在PATH中,所以里面放的程序都可以直接在shell里面执行,不需要带完整的路径
dev@ubuntu:~/chroot$ mkdir -p new_root/bin
dev@ubuntu:~/chroot$ chmod +x ./busybox-x86_64
dev@ubuntu:~/chroot$ mv busybox-x86_64 new_root/bin/busybox
# 安装busybox到bin目录,不安装的话每次执行ls命令都需要使用上面那种格式: busybox ls#安装之后就会创建一个ls到busybox的硬链接,这样执行ls的时候就不用再输入前面的busybox了
dev@ubuntu:~/chroot$ ./new_root/bin/busybox --install ./new_root/bin/
dev@ubuntu:~/chroot$ ./new_root/bin/ls
new_root
# 使用chroot命令,切换根目录
dev@ubuntu:~/chroot$ sudo chroot ./new_root/ sh
# 切换成功,由于new_root下面只有busybox,没有任何配置文件,#所以shell的提示符里只包含当前目录#尝试运行几个命令,一切正常
/ # ls
bin
/ # which ls
/bin/ls
/ # hostname
ubuntu
/ # id
uid=0 gid=0 groups=0
/ # exit
dev@ubuntu:~/chroot$
- 创建容器并配置
# 新建/data目录用来在主机和容器之间共享数据
dev@ubuntu:~/chroot$ sudo mkdir /data
dev@ubuntu:~/chroot$ sudo chown dev:dev /data
dev@ubuntu:~/chroot$ touch /data/001
# 创建新的容器,指定所有namespace相关的参数,这里--propagation private是为了让容器里的mount point都变成private的,这是因为pivot_root命令需要原来根目录的挂载点为private,只有我们需要在host和container之间共享挂载信息的时候,才需要使用shared或者slave类型
dev@ubuntu:~/chroot$ unshare --user --mount --ipc --pid --net --uts -r --fork --propagation private bash
root@ubuntu:~/chroot# hostname container01
root@ubuntu:~/chroot# exec bash
# 创建old_root用于pivot_root命令,创建data目录用于绑定/data目录
root@container01:~/chroot# mkdir -p ./new_root/old_root/ ./new_root/data/
# 由于pivot_root命令要求老的根目录和新的根目录不能在同一个挂载点下,#所以这里利用bind mount,在原地创建一个新的挂载点
root@container01:~/chroot# mount --bind ./new_root/ ./new_root/
# 将/data目录绑定到new_root/data,这样pivot_root后,就能访问/data下的东西了
root@container01:~/chroot# mount --bind /data ./new_root/data
# 进入new_root目录,然后切换根目录
root@container01:~/chroot# cd new_root/
root@container01:~/chroot/new_root# pivot_root ./ ./old_root/
# 但shell提示符里显示的当前目录还是原来的目录,没有切换到‘/’下,#这是因为当前运行的shell还是host里面的bash
root@container01:~/chroot/new_root# ls
bin data old_root
# 重新加载new_root下面的shell,这样contianer和host就没有关系了,#从shell提示符中可以看出,当前目录已经变成了‘/’
root@container01:~/chroot/new_root# exec sh
/ # #由于没有/etc目录,也就没有相关的profile,于是shell的提示符里面只包含当前路径。
#设置PS1环境变量,让shell提示符好看点,这里直接写了root在提示符里面,#是因为我们新的container里面没有账号相关的配置文件,#虽然系统知道当前账号的ID是0,但不知道账号的用户名是什么。
/ # export PS1='root@$(hostname):$(pwd)# '
root@container01:/#
# 没有/etc目录,没有user相关的配置文件,所以不知道ID为0的用户名是什么
root@container01:/# whoami
whoami: unknown uid 0
# mount命令依赖于/proc目录,所以这里mount操作失败
root@container01:/# mount
mount: no /proc/mounts
# 重新mount /proc
root@container01:/# mkdir /proc
root@container01:/# mount -t proc none /proc
# 这时可以看到所有的mount信息了,从host复制过来的mount信息都挂载在/old_root目录下
root@container01:/# mount
/dev/mapper/ubuntu--vg-root on /old_root type ext4 (rw,relatime,errors=remount-ro,data=ordered)
udev on /old_root/dev type devtmpfs (rw,nosuid,relatime,size=1005080k,nr_inodes=251270,mode=755)
devpts on /old_root/dev/pts type devpts (rw,nosuid,noexec,relatime,mode=600,ptmxmode=000)
tmpfs on /old_root/dev/shm type tmpfs (rw,nosuid,nodev)
......
/dev/mapper/ubuntu--vg-root on / type ext4 (rw,relatime,errors=remount-ro,data=ordered)
/dev/mapper/ubuntu--vg-root on /data type ext4 (rw,relatime,errors=remount-ro,data=ordered)
none on /proc type proc (rw,nodev,relatime)
# umount掉/old_root下的所有mount point
root@container01:/# umount -l /old_root#这时候就只剩下根目录,/proc,/data三个挂载点了
root@container01:/# mount
/dev/mapper/ubuntu--vg-root on / type ext4 (rw,relatime,errors=remount-ro,data=ordered)
/dev/mapper/ubuntu--vg-root on /data type ext4 (rw,relatime,errors=remount-ro,data=ordered)
none on /proc type proc (rw,nodev,relatime)
# 试试cd命令,提示失败, $HOME还是指向老的/home/dev,#除了$HOME之外,还有其他一些环境变量也有同样的问题。#这主要是由于我们新的container中缺少配置文件,导致很多环境变量没有更新。
root@container01:/# cd
sh: cd: can not cd to /home/dev
# 试试ps,显示的是container里面启动的进程
root@container01:/# ps
PID USER TIME COMMAND
1 0 0:00 sh
55 0 0:00 ps
#touch文件001成功
root@container01:/# touch /data/001#新创建一个002文件
root@container01:/# touch /data/002
root@container01:/# ls /data
001 002
#退出contianer01,在/data目录能看到我们上面在container01种创建的002文件
root@container01:/# exit
dev@ubuntu:~/chroot$ ls /data
001 002
查看namespace
系统中的每个进程都有/proc/[pid]/ns/这样一个目录,里面包含了这个进程所属namespace的信息,里面每个文件的描述符都可以用来作为setns函数的参数。
[root@YZ-25-65-49 ns]# ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 root root 0 Nov 27 17:09 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Nov 27 17:09 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Nov 27 17:09 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0 Nov 27 17:09 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Nov 27 17:09 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Nov 27 17:09 uts -> uts:[4026531838]
namespace tool
nsenter
[root@con01 ns]# nsenter -h
Usage:
nsenter [options] <program> [<argument>...]
Run a program with namespaces of other processes.
Options:
-t, --target <pid> target process to get namespaces from
-m, --mount[=<file>] enter mount namespace
-u, --uts[=<file>] enter UTS namespace (hostname etc)
-i, --ipc[=<file>] enter System V IPC namespace
-n, --net[=<file>] enter network namespace
-p, --pid[=<file>] enter pid namespace
-U, --user[=<file>] enter user namespace
-S, --setuid <uid> set uid in entered namespace
-G, --setgid <gid> set gid in entered namespace
--preserve-credentials do not touch uids or gids
-r, --root[=<dir>] set the root directory
-w, --wd[=<dir>] set the working directory
-F, --no-fork do not fork before execing <program>
-Z, --follow-context set SELinux context according to --target PID
-h, --help display this help and exit
-V, --version output version information and exit
# 试着加入第二个shell窗口里面bash的uts和ipc namespace
# -t后面跟pid用来指定加入哪个进程所在的namespace
# 这里27668是第二个shell中正在运行的bash的pid
# 加入成功后将运行/bin/bash
dev@ubuntu:~$ sudo nsenter -t 27668 -u -i /bin/bash
unshare
[root@YZ-25-65-49 ns]# unshare -h
Usage:
unshare [options] <program> [<argument>...]
Run a program with some namespaces unshared from the parent.
Options:
-m, --mount unshare mounts namespace
-u, --uts unshare UTS namespace (hostname etc)
-i, --ipc unshare System V IPC namespace
-n, --net unshare network namespace
-p, --pid unshare pid namespace
-U, --user unshare user namespace
-f, --fork fork before launching <program>
--mount-proc[=<dir>] mount proc filesystem first (implies --mount)
-r, --map-root-user map current user to root (implies --user)
--propagation <slave|shared|private|unchanged>
modify mount propagation in mount namespace
-s, --setgroups allow|deny control the setgroups syscall in user namespaces
-h, --help display this help and exit
-V, --version output version information and exit
For more details see unshare(1).
# 运行unshare创建新的ipc和uts namespace,并且在新的namespace中启动bash
# 这里-i表示启动新的ipc namespace,-u表示启动新的utsnamespace
dev@ubuntu:~$ sudo unshare -iu /bin/bash
问题
[root@YZ-25-58-1 ns]# cat /proc/sys/user/max_user_namespaces
0
[root@YZ-25-58-1 ns]# unshare --user --mount --ipc --pid --net --uts -r --fork --propagation private bash
unshare: unshare failed: Invalid argument
[root@YZ-25-58-1 ns]# echo 640 > /proc/sys/user/max_user_namespaces
cgroup
概述
cgroup和namespace类似,也是将进程进行分组,但它的目的和namespace不一样,namespace是为了隔离进程组之间的资源,而cgroup是为了对一组进程进行统一的资源监控和限制。
cgroup是Linux下的一种将进程按组进行管理的机制,在用户层看来,cgroup技术就是把系统中的所有进程组织成一颗一颗独立的树,每棵树都包含系统的所有进程,树的每个节点是一个进程组,而每颗树又和一个或者多个subsystem关联,树的作用是将进程分组,而subsystem的作用就是对这些组进行操作。cgroup主要包括下面两部分:
- subsystem 一个subsystem就是一个内核模块,他被关联到一颗cgroup树之后,就会在树的每个节点(进程组)上做具体的操作。subsystem经常被称作"resource controller",因为它主要被用来调度或者限制每个进程组的资源,但是这个说法不完全准确,因为有时我们将进程分组只是为了做一些监控,观察一下他们的状态,比如perf_event subsystem。到目前为止,Linux支持12种subsystem,比如限制CPU的使用时间,限制使用的内存,统计CPU的使用情况,冻结和恢复一组进程等,后续会对它们一一进行介绍。
- hierarchy 一个hierarchy可以理解为一棵cgroup树,树的每个节点就是一个进程组,每棵树都会与零到多个subsystem关联。在一颗树里面,会包含Linux系统中的所有进程,但每个进程只能属于一个节点(进程组)。系统中可以有很多颗cgroup树,每棵树都和不同的subsystem关联,一个进程可以属于多颗树,即一个进程可以属于多个进程组,只是这些进程组和不同的subsystem关联。目前Linux支持12种subsystem,如果不考虑不与任何subsystem关联的情况(systemd就属于这种情况),Linux里面最多可以建12颗cgroup树,每棵树关联一个subsystem,当然也可以只建一棵树,然后让这棵树关联所有的subsystem。当一颗cgroup树不和任何subsystem关联的时候,意味着这棵树只是将进程进行分组,至于要在分组的基础上做些什么,将由应用程序自己决定,systemd就是一个这样的例子。
目前Linux支持下面12种subsystem
- cpu (since Linux 2.6.24; CONFIG_CGROUP_SCHED) 用来限制cgroup的CPU使用率。
- cpuacct (since Linux 2.6.24; CONFIG_CGROUP_CPUACCT) 统计cgroup的CPU的使用率。
- cpuset (since Linux 2.6.24; CONFIG_CPUSETS) 绑定cgroup到指定CPUs和NUMA节点。
- memory (since Linux 2.6.25; CONFIG_MEMCG) 统计和限制cgroup的内存的使用率,包括process memory, kernel memory, 和swap。
- devices (since Linux 2.6.26; CONFIG_CGROUP_DEVICE) 限制cgroup创建(mknod)和访问设备的权限。
- freezer (since Linux 2.6.28; CONFIG_CGROUP_FREEZER) suspend和restore一个cgroup中的所有进程。
- net_cls (since Linux 2.6.29; CONFIG_CGROUP_NET_CLASSID) 将一个cgroup中进程创建的所有网络包加上一个classid标记,用于tc和iptables。 只对发出去的网络包生效,对收到的网络包不起作用。
- blkio (since Linux 2.6.33; CONFIG_BLK_CGROUP) 限制cgroup访问块设备的IO速度。
- perf_event (since Linux 2.6.39; CONFIG_CGROUP_PERF) 对cgroup进行性能监控
- net_prio (since Linux 3.3; CONFIG_CGROUP_NET_PRIO) 针对每个网络接口设置cgroup的访问优先级。
- hugetlb (since Linux 3.5; CONFIG_CGROUP_HUGETLB) 限制cgroup的huge pages的使用量。
- pids (since Linux 4.3; CONFIG_CGROUP_PIDS) 限制一个cgroup及其子孙cgroup中的总进程数。
# 查看当前系统支持哪些subsystem
[root@YZ-25-65-49 ~]# cat /proc/cgroups
#subsys_name hierarchy num_cgroups enabled
cpuset 5 3 1
cpu 7 78 1
cpuacct 7 78 1
memory 10 77 1
devices 3 77 1
freezer 11 3 1
net_cls 8 3 1
blkio 4 77 1
perf_event 2 3 1
hugetlb 9 3 1
pids 6 5 1
net_prio 8 3 1
使用cgroup
cgroup相关的所有操作都是基于内核中的cgroup virtual filesystem,使用cgroup很简单,挂载这个文件系统就可以了。一般情况下都是挂载到/sys/fs/cgroup目录下,当然挂载到其它任何目录都没关系。
默认systemd已经帮我们将各个subsystem和cgroup树关联并挂载好了,mount |grep cgroup查看
# xxx为任意字符串,取一个有意义的名字就可以了,当用mount命令查看的时候,xxx会显示在第一列
# 挂载一颗和所有subsystem关联的cgroup树到/sys/fs/cgroup
mount -t cgroup xxx /sys/fs/cgroup
# 挂载一颗和cpuset subsystem关联的cgroup树到/sys/fs/cgroup/cpuset
mkdir /sys/fs/cgroup/cpuset
mount -t cgroup -o cpuset xxx /sys/fs/cgroup/cpuset
# 挂载一颗与cpu和cpuacct subsystem关联的cgroup树到/sys/fs/cgroup/cpu,cpuacctmkdir /sys/fs/cgroup/cpu,cpuacct
mount -t cgroup -o cpu,cpuacct xxx /sys/fs/cgroup/cpu,cpuacct
# 挂载一棵cgroup树,但不关联任何subsystem,下面就是systemd所用到的方式mkdir /sys/fs/cgroup/systemd
mount -t cgroup -o none,name=systemd xxx /sys/fs/cgroup/systemd
- 创建并管理cgroup
# 准备需要的目录
dev@ubuntu:~$ mkdir cgroup && cd cgroup
dev@ubuntu:~/cgroup$ mkdir demo
# 由于name=demo的cgroup树不存在,所以系统会创建一颗新的cgroup树,然后挂载到demo目录
dev@ubuntu:~/cgroup$ sudo mount -t cgroup -o none,name=demo demo ./demo
# 挂载点所在目录就是这颗cgroup树的root cgroup,在root cgroup下面,系统生成了一些默认文件
dev@ubuntu:~/cgroup$ ls ./demo/
cgroup.clone_children cgroup.procs cgroup.sane_behavior notify_on_release release_agent tasks
- 创建并管理子cgroup
# 创建子cgroup很简单,新建一个目录就可以了
dev@ubuntu:~/cgroup$ cd demo
dev@ubuntu:~/cgroup/demo$ sudo mkdir cgroup1
# 在新创建的cgroup里面,系统默认也生成了一些文件,这些文件的意义和root cgroup里面的一样
dev@ubuntu:~/cgroup/demo$ ls cgroup1/
cgroup.clone_children cgroup.procs notify_on_release tasks
# 新创建的cgroup里没有任何进程和线程
dev@ubuntu:~/cgroup/demo$ wc -l cgroup1/cgroup.procs
0 cgroup1/cgroup.procs
dev@ubuntu:~/cgroup/demo$ wc -l cgroup1/tasks
0 cgroup1/tasks
# 每个cgroup都可以创建自己的子cgroup,所以我们也可以在cgroup1里面创建子cgroup
dev@ubuntu:~/cgroup/demo$ sudo mkdir cgroup1/cgroup11
dev@ubuntu:~/cgroup/demo$ ls cgroup1/cgroup11
cgroup.clone_children cgroup.procs notify_on_release tasks
# 删除cgroup也很简单,删除掉相应的目录就可以了
dev@ubuntu:~/cgroup/demo$ sudo rmdir cgroup1/
- 添加进程
dev@ubuntu:~/cgroup/demo$ sudo mkdir test
dev@ubuntu:~/cgroup/demo$ cd test
# 将当前bash加入到上面新创建的cgroup中
dev@ubuntu:~/cgroup/demo/test$ echo $$
1421
dev@ubuntu:~/cgroup/demo/test$ sudo sh -c 'echo 1421 > cgroup.procs'
- subsystem限制cgroup
# 进入目录/sys/fs/cgroup/pids/并新建一个目录,即创建了一个子cgroup
dev@dev:~$ cd /sys/fs/cgroup/pids/
dev@dev:/sys/fs/cgroup/pids$ sudo mkdir test
# 由于这是个新创建的cgroup,所以里面还没有任何进程
pids.current: 表示当前cgroup及其所有子孙cgroup中现有的总的进程数量
pids.max: 当前cgroup及其所有子孙cgroup中所允许创建的总的最大进程数量
dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current
0
# max表示没做任何限制
dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max
max
# 将pids.max设置为1,即当前cgroup只允许有一个进程
dev@dev:/sys/fs/cgroup/pids/test$ echo 1 > pids.max
# 将当前bash进程加入到该cgroup
dev@dev:/sys/fs/cgroup/pids/test$ echo $$ > cgroup.procs
dev@dev:/sys/fs/cgroup/pids/test$ ls
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: Resource temporarily unavailable
- 查看当前进程属于哪些cgroup
[root@YZ-25-65-49 ~]# cat /proc/$$/cgroup
17:name=demo:/
15:name=test:/
11:freezer:/
10:memory:/user.slice
9:hugetlb:/
8:net_prio,net_cls:/
7:cpuacct,cpu:/user.slice
6:pids:/
5:cpuset:/
4:blkio:/user.slice
3:devices:/user.slice
2:perf_event:/
1:name=systemd:/user.slice/user-511.slice/session-83167.scope