实际上根本没有容器这样的东西。容器由两个 Linux 原语组成:
在研究容器是什么之前,了解如何在 Linux 中创建和管理新进程很重要。
在上图中,父进程可以被认为是一个活动的shell会话,子进程可以被认为是在shell中运行的任何命令,例如:ls、pwd。现在,当运行新命令时,会创建一个新进程。这是由父进程通过调用函数来完成的fork
。当它创建一个新的独立进程时,它将子进程的进程 ID (PID) 返回给调用该函数的父进程fork
。在适当的时候,父母和孩子都可以继续执行他们的任务并终止。子PID对于父进程跟踪新创建的进程很重要。
让我们继续了解Linux有哪些命名空间。
命名空间是一种隔离原语,可以帮助我们隔离各种类型的资源。在 Linux 中,目前可以对七种不同类型的资源执行此操作。它们是,没有特定的顺序:
默认情况下,这些命名空间中都已经存在于系统中。
有关进程的所有信息都包含在 下procfs
,通常安装在/proc
. 运行echo $$
会给当前正在运行的进程的PID:
$ echo $$
448884
查看/proc/<PID>/ns,
将看到该进程使用的命名空间列表。例如:
$ ls /proc/448884/ns -lh
total 0
lrwxrwxrwx 1 root root 0 Feb 23 19:00 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Feb 23 19:00 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Feb 23 19:00 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 Feb 23 19:00 net -> 'net:[4026532008]'
lrwxrwxrwx 1 root root 0 Feb 23 19:00 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Feb 23 19:00 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Feb 23 19:00 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Feb 23 19:00 uts -> 'uts:[4026531838]'
对于每个命名空间,都有一个文件,它是指向命名空间 ID 的符号链接。所以对于网络命名空间,上例中命名空间的ID是net:[4026532008]
。4026532008
就是inode号。对于同一命名空间中的两个进程,这个数字是相同的。
在 Linux 上,要创建新的命名空间,可以使用系统调用unshare
. 为了创建一个新的网络命名空间,需要添加标志-n
。因此,在具有 root 权限的 shell 会话中,我们将执行以下操作:
# unshare -n
可以查看/proc/<PID>/ns
目录以验证我们确实创建了一个新的命名空间:
# ls -l /proc/$$/ns/net
lrwxrwxrwx 1 root root 0 Feb 23 18:46 /proc/447612/ns/net -> 'net:[4026533490]'
命名空间 ID 与我们上面看到的主机网络命名空间不同。ip link
在此之后运行命令只显示回环接口:
# ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
如果有任何网络接口,如WIFI卡或以太网端口,它们根本不会出现。事实上,如果尝试运行ping 127.0.0.1
,通常认为理所当然的也不会起作用:
# ping 127.0.0.1
ping: connect: Network is unreachable
但是为什么会发生上述情况呢?
起初创建了一个新的网络命名空间,这个行为隔离了默认命名空间中已有的网络资源。在这个新的命名空间中,唯一可用的loopback
接口。然而,它还没有分配给它的 IP 地址:
# ip address
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
这说明该接口目前不仅没有IP地址,而且state
还设置为DOWN
. 运行以下命令可以解决这个问题:
# ip address add dev lo local 127.0.0.1/8
# ip link set lo up
首先,为该接口分配了 IP 地址127.0.0.1
,并将接口的状态设置为UP
,从而使其可用于侦听传入的网络数据包。现在ping
将按预期工作:
# ping 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.020 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.060 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.071 ms
为了理解隔离的概念,将继续尝试让这个新的网络接口(我们称之为 CHILD)与主机网络命名空间对话,反之亦然。
为了帮助理解,将PS1
这个 shell 中的变量设置为易于识别的变量:
# export PS1="[netns: CHILD]# "
[netns: CHILD]#
还生成一个具有 root 访问权限的新终端,以便在其中运行的 shell 属于主机网络命名空间。将再次设置PS1
变量以帮助轻松识别主机命名空间:
# export PS1="[netns: HOST]# "
[netns: HOST]#
ip link
在此界面上运行命令将显示系统中当前安装的网络接口。例如:
[netns: HOST]# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3: enp0s31f6: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN mode DEFAULT group default qlen 1000
link/ether 0e:94:18:de:da:b3 brd ff:ff:ff:ff:ff:ff
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
link/ether 02:42:ad:0f:83:cc brd ff:ff:ff:ff:ff:ff
11: wlp61s0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DORMANT group default qlen 1000
link/ether fa:3d:a9:90:95:5d brd ff:ff:ff:ff:ff:ff
要列出系统中的所有网络命名空间,我们可以运行:
[netns: HOST]# ip netns list
,这将产生一个空的输出。那么这是否意味着该命令不起作用或者我们在那里做错了什么,即使之前创建了一个新的网络命名空间?这两个问题的答案都是否定的。由于在 UNIX中一切都是文件,因此该ip
命令在目录中查找网络名称空间/var/run/netns
。目前该目录是空的。因此,我们将首先创建一个空文件,然后再次尝试运行该命令:
[netns: HOST]# touch /var/run/netns/child
[netns: HOST]# ip netns list
Error: Peer netns reference is invalid.
Error: Peer netns reference is invalid.
child
确实child
在列表中看到了命名空间,但也看到了一个错误。这是因为还没有将运行新命名空间的 shell 映射到这个文件。为此,我们将挂载/proc/<PID>/ns/net
文件绑定到我们上面创建的新文件。这可以通过在运行子网络命名空间的 shell 中执行以下命令来完成:
[netns: CHILD]# mount -o bind /proc/$$/ns/net /var/run/netns/child
[netns: CHILD]# ip netns list
child
这次列出网络命名空间的命令可以正常工作,没有任何错误。这意味着已经将命名空间与 ID 关联4026533490
到文件 /var/run/netns/child
,并且命名空间现在是持久的。
现在需要找到一种方法让主机和子网络命名空间相互通信。为此,将在主机网络命名空间中创建一对虚拟以太网设备:
[netns: HOST]# ip link add veth0 type veth peer name veth1
在此命令中,创建了一个名为,veth0
而的虚拟以太网设备。而该对设备的另一端称为veth1
。
[netns: HOST]# ip link | grep veth
35: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
36: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
目前,这两个设备都存在于主机命名空间中。如果ip link
在子网络命名空间中运行,它只会loopback
像以前一样显示地址:
[netns: CHILD]# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
那么可以做些什么来让 veth 设备之一出现在子命名空间中呢?为此,我们将在主机网络命名空间中运行以下命令,因为这是当前存在 veth 设备的位置:
[netns: HOST]# ip link set veth1 netns child
这里我们指示将veth1
网络设备分配给命名空间child
。ip link
这个命名空间中不会显示veth1
装置:
[netns: HOST]# ip link | grep veth
36: veth0@if35: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
而另一方面,veth1
现在出现在子网络命名空间中:
[netns: CHILD]# ip link | grep veth
35: veth1@if36: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
在让它们相互通信之前,还有两个步骤,即为每个veth
设备分配一个 IP 地址并将状态设置为 up:
[netns: HOST]# ip address add dev veth0 local 10.16.8.1/24
[netns: HOST]# ip link set veth0 up
可以使用以下命令验证命令的结果:
[netns: HOST]# ip address | grep veth -A 5
36: veth0@if35: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state LOWERLAYERDOWN group default qlen 1000
link/ether 32:c7:79:c7:e2:e0 brd ff:ff:ff:ff:ff:ff link-netns child
inet 10.16.8.1/24 scope global veth0
valid_lft forever preferred_lft forever
子命名空间也是如此:
[netns: CHILD]# ip address add dev veth1 local 10.16.8.2/24
[netns: CHILD]# ip link set veth1 up
[netns: CHILD]# ip address | grep veth -A 5
35: veth1@if36: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 5a:62:dd:40:a6:f1 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.16.8.2/24 scope global veth1
valid_lft forever preferred_lft forever
inet6 fe80::5862:ddff:fe40:a6f1/64 scope link
valid_lft forever preferred_lft forever
最后,应该能够互相ping通:
[netns: HOST]# ping 10.16.8.2
PING 10.16.8.2 (10.16.8.2) 56(84) bytes of data.
64 bytes from 10.16.8.2: icmp_seq=1 ttl=64 time=0.086 ms
64 bytes from 10.16.8.2: icmp_seq=2 ttl=64 time=0.099 ms
64 bytes from 10.16.8.2: icmp_seq=3 ttl=64 time=0.100 ms
[netns: CHILD]# ping 10.16.8.1
PING 10.16.8.1 (10.16.8.1) 56(84) bytes of data.
64 bytes from 10.16.8.1: icmp_seq=1 ttl=64 time=0.057 ms
64 bytes from 10.16.8.1: icmp_seq=2 ttl=64 time=0.090 ms
64 bytes from 10.16.8.1: icmp_seq=3 ttl=64 time=0.118 ms
接下来是 cgroups。它控制进程可以消耗的资源量。最好的例子是 CPU 和内存。这样做的最佳用例是避免进程意外使用所有可用的 CPU 或内存并阻止整个系统执行任何其他操作。cgroup 位于该/sys/fs/cgroup
目录下。让我们来看看内容:
# ls /sys/fs/cgroup/ -lh
total 0
dr-xr-xr-x 5 root root 0 Feb 17 01:05 blkio
lrwxrwxrwx 1 root root 11 Feb 17 01:05 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 Feb 17 01:05 cpuacct -> cpu,cpuacct
dr-xr-xr-x 5 root root 0 Feb 17 01:05 cpu,cpuacct
dr-xr-xr-x 2 root root 0 Feb 17 01:05 cpuset
dr-xr-xr-x 5 root root 0 Feb 17 01:05 devices
dr-xr-xr-x 2 root root 0 Feb 17 01:05 freezer
dr-xr-xr-x 2 root root 0 Feb 17 01:05 hugetlb
dr-xr-xr-x 9 root root 0 Feb 20 00:24 memory
lrwxrwxrwx 1 root root 16 Feb 17 01:05 net_cls -> net_cls,net_prio
dr-xr-xr-x 2 root root 0 Feb 17 01:05 net_cls,net_prio
lrwxrwxrwx 1 root root 16 Feb 17 01:05 net_prio -> net_cls,net_prio
dr-xr-xr-x 2 root root 0 Feb 17 01:05 perf_event
dr-xr-xr-x 5 root root 0 Feb 17 01:05 pids
dr-xr-xr-x 2 root root 0 Feb 17 01:05 rdma
dr-xr-xr-x 5 root root 0 Feb 17 01:05 systemd
dr-xr-xr-x 5 root root 0 Feb 17 01:06 unified
每个目录都是一个可以控制使用的资源。要创建新的cgroup
,我们需要在这些资源之一中创建一个新目录。例如,如果我们打算新建cgroup
一个控制内存使用的新目录,我们会在/sys/fs/cgroups/memory
路径下新建一个目录(名称由我们决定)。所以让我们这样做:
# mkdir /sys/fs/cgroup/memory/child
# ls -lh /sys/fs/cgroup/memory/demo/
total 0
-rw-r--r-- 1 root root 0 Feb 24 12:29 cgroup.clone_children
--w--w--w- 1 root root 0 Feb 24 12:29 cgroup.event_control
-rw-r--r-- 1 root root 0 Feb 24 12:29 cgroup.procs
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.failcnt
--w------- 1 root root 0 Feb 24 12:29 memory.force_empty
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.failcnt
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.limit_in_bytes
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.max_usage_in_bytes
-r--r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.slabinfo
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.tcp.failcnt
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.tcp.limit_in_bytes
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.tcp.max_usage_in_bytes
-r--r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.tcp.usage_in_bytes
-r--r--r-- 1 root root 0 Feb 24 12:29 memory.kmem.usage_in_bytes
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.limit_in_bytes
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.max_usage_in_bytes
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.memsw.failcnt
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.memsw.limit_in_bytes
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.memsw.max_usage_in_bytes
-r--r--r-- 1 root root 0 Feb 24 12:29 memory.memsw.usage_in_bytes
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.move_charge_at_immigrate
-r--r--r-- 1 root root 0 Feb 24 12:29 memory.numa_stat
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.oom_control
---------- 1 root root 0 Feb 24 12:29 memory.pressure_level
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.soft_limit_in_bytes
-r--r--r-- 1 root root 0 Feb 24 12:29 memory.stat
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.swappiness
-r--r--r-- 1 root root 0 Feb 24 12:29 memory.usage_in_bytes
-rw-r--r-- 1 root root 0 Feb 24 12:29 memory.use_hierarchy
-rw-r--r-- 1 root root 0 Feb 24 12:29 notify_on_release
-rw-r--r-- 1 root root 0 Feb 24 12:29 tasks
操作系统为每个新目录创建了一大堆文件。让我们看看其中一个文件:
# cat /sys/fs/cgroup/memory/demo/memory.limit_in_bytes
9223372036854771712
此文件中的值指示进程可以使用的最大内存(如果它是此 cgroup 的一部分)。让我们将此值设置为一个小得多的数字,例如 4MB,但以字节为单位:
# echo 4000000 > /sys/fs/cgroup/memory/demo/memory.limit_in_bytes
让我们看看这个文件:
# cat /sys/fs/cgroup/memory/demo/memory.limit_in_bytes
3997696
这不是我们写入文件的确切内容,但它大约为 3.99 MB。我的猜测是这与由操作系统管理的内存对齐有关。
现在在新的主机名命名空间中启动一个新进程:
# unshare -u
这将启动一个新的 shell 进程。尝试运行一个命令,wget
我知道它需要超过 4MB 的内存才能运行:
# wget wikipedia.org
URL transformed to HTTPS due to an HSTS policy
--2020-02-24 12:36:58-- https://wikipedia.org/
Loaded CA certificate '/etc/ssl/certs/ca-certificates.crt'
Resolving wikipedia.org (wikipedia.org)... 103.102.166.224, 2001:df2:e500:ed1a::1
Connecting to wikipedia.org (wikipedia.org)|103.102.166.224|:443... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: https://www.wikipedia.org/ [following]
--2020-02-24 12:36:58-- https://www.wikipedia.org/
Resolving www.wikipedia.org (www.wikipedia.org)... 103.102.166.224, 2001:df2:e500:ed1a::1
Connecting to www.wikipedia.org (www.wikipedia.org)|103.102.166.224|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 76776 (75K) [text/html]
Saving to: ‘index.html’
index.html 100%[============================================>] 74.98K 362KB/s in 0.2s
2020-02-24 12:36:59 (362 KB/s) - ‘index.html’ saved [76776/76776]
现在我们注意到该命令有效。这是因为这个进程是默认 cgroup 的一部分。要使其成为新 cgroup 的一部分,需要将此进程的 PID 写入cgroup.procs
文件:
# echo $$ > /sys/fs/cgroup/memory/demo/cgroup.procs
让我们看看这个文件的内容:
# cat /sys/fs/cgroup/memory/demo/cgroup.procs
468401
468464
这里似乎有两个条目。第一个条目是我们写入文件的 shell 进程的 PID。另一个是cat
我们运行的进程的PID 。这是因为默认情况下,所有子进程都与父进程属于同一个 cgroup。一旦进程终止,PID 会自动从文件中删除。如果我们再次运行相同的命令,我们仍然会找到两个条目,但第二个会有所不同:
# cat /sys/fs/cgroup/memory/demo/cgroup.procs
468401
468464
现在再次尝试运行wget
命令:
# wget wikipedia.org
URL transformed to HTTPS due to an HSTS policy
--2020-02-24 12:44:26-- https://wikipedia.org/
Killed
该进程立即被杀死,因为它试图使用比当前允许的 cgroup 更多的内存。
因此,namespaces
和cgroups
以隔离和控制资源的使用和形成普遍称为容器。:
CAP_NET_ADMIN
。kill
系统调用将阻止进程能够终止或向其他进程发送信号。所以在namespaces
允许我们隔离资源类型的同时,cgroups
帮助我们控制一个进程的资源使用量。并capabilities
通过将操作分解为不同类型的功能来限制 root 权限的使用。最后seccomp
有助于阻止进程调用不需要的系统调用。这些概念组合在一起形成了一个容器,这是一种比同时担心所有这些更好的抽象。
fork
这篇文章前面的图表有点不完整。这是一个更完整的图表:
如前所述fork
,将子进程的 PID 返回给父进程,并使用此 PID 来“等待”子进程完成执行。这是由waitpid
系统调用完成的。这对于避免僵尸进程很重要,这被称为收割。一旦子进程终止,父进程就有责任确保为子进程分配的所有资源都被清理干净。简而言之,这是容器运行时或容器引擎的工作。它产生新的容器或子进程,并确保在容器终止后清理资源。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。