前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >容器的生命周期

容器的生命周期

原创
作者头像
mariolu
发布2021-07-31 00:46:30
1.4K0
发布2021-07-31 00:46:30
举报

一、容器是什么

实际上根本没有容器这样的东西。容器由两个 Linux 原语组成:

  1. 命名空间
  2. 控制组 (cgroups)

在研究容器是什么之前,了解如何在 Linux 中创建和管理新进程很重要。

叉

在上图中,父进程可以被认为是一个活动的shell会话,子进程可以被认为是在shell中运行的任何命令,例如:ls、pwd。现在,当运行新命令时,会创建一个新进程。这是由父进程通过调用函数来完成的fork。当它创建一个新的独立进程时,它将子进程的进程 ID (PID) 返回给调用该函数的父进程fork。在适当的时候,父母和孩子都可以继续执行他们的任务并终止。子PID对于父进程跟踪新创建的进程很重要。

二、命名空间

让我们继续了解Linux有哪些命名空间。

命名空间是一种隔离原语,可以帮助我们隔离各种类型的资源。在 Linux 中,目前可以对七种不同类型的资源执行此操作。它们是,没有特定的顺序:

  • Network namespace
  • Mount
  • UTS或者Hostname namespace
  • Process ID or PID namespace
  • Inter process communication or IPC namespace
  • cgroup namespace
  • User namespace

默认情况下,这些命名空间中都已经存在于系统中。

有关进程的所有信息都包含在 下procfs,通常安装在/proc. 运行echo $$会给当前正在运行的进程的PID:

代码语言:javascript
复制
$ echo $$
448884

查看/proc/<PID>/ns,将看到该进程使用的命名空间列表。例如:

代码语言:javascript
复制
$ 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 会话中,我们将执行以下操作:

代码语言:javascript
复制
# unshare -n

可以查看/proc/<PID>/ns目录以验证我们确实创建了一个新的命名空间:

代码语言:javascript
复制
# ls -l /proc/$$/ns/net
lrwxrwxrwx 1 root root 0 Feb 23 18:46 /proc/447612/ns/net -> 'net:[4026533490]'

命名空间 ID 与我们上面看到的主机网络命名空间不同。ip link在此之后运行命令只显示回环接口:

代码语言:javascript
复制
# 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,通常认为理所当然的也不会起作用:

代码语言:javascript
复制
# ping 127.0.0.1
ping: connect: Network is unreachable

但是为什么会发生上述情况呢?

起初创建了一个新的网络命名空间,这个行为隔离了默认命名空间中已有的网络资源。在这个新的命名空间中,唯一可用的loopback接口。然而,它还没有分配给它的 IP 地址:

代码语言:javascript
复制
# 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. 运行以下命令可以解决这个问题:

代码语言:javascript
复制
# ip address add dev lo local 127.0.0.1/8
# ip link set lo up

首先,为该接口分配了 IP 地址127.0.0.1,并将接口的状态设置为UP,从而使其可用于侦听传入的网络数据包。现在ping将按预期工作:

代码语言:javascript
复制
# 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 中的变量设置为易于识别的变量:

代码语言:javascript
复制
# export PS1="[netns: CHILD]# "
[netns: CHILD]#

还生成一个具有 root 访问权限的新终端,以便在其中运行的 shell 属于主机网络命名空间。将再次设置PS1变量以帮助轻松识别主机命名空间:

代码语言:javascript
复制
# export PS1="[netns: HOST]# "
[netns: HOST]#

ip link在此界面上运行命令将显示系统中当前安装的网络接口。例如:

代码语言:javascript
复制
[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

要列出系统中的所有网络命名空间,我们可以运行:

代码语言:javascript
复制
[netns: HOST]# ip netns list

,这将产生一个空的输出。那么这是否意味着该命令不起作用或者我们在那里做错了什么,即使之前创建了一个新的网络命名空间?这两个问题的答案都是否定的。由于在 UNIX中一切都是文件,因此该ip命令在目录中查找网络名称空间/var/run/netns。目前该目录是空的。因此,我们将首先创建一个空文件,然后再次尝试运行该命令:

代码语言:javascript
复制
[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 中执行以下命令来完成:

代码语言:javascript
复制
[netns: CHILD]# mount -o bind /proc/$$/ns/net /var/run/netns/child
[netns: CHILD]# ip netns list
child

这次列出网络命名空间的命令可以正常工作,没有任何错误。这意味着已经将命名空间与 ID 关联4026533490到文件 /var/run/netns/child,并且命名空间现在是持久的。

现在需要找到一种方法让主机和子网络命名空间相互通信。为此,将在主机网络命名空间中创建一对虚拟以太网设备:

代码语言:javascript
复制
[netns: HOST]# ip link add veth0 type veth peer name veth1

在此命令中,创建了一个名为,veth0而的虚拟以太网设备。而该对设备的另一端称为veth1

代码语言:javascript
复制
[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像以前一样显示地址:

代码语言:javascript
复制
[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 设备的位置:

代码语言:javascript
复制
[netns: HOST]# ip link set veth1 netns child

这里我们指示将veth1网络设备分配给命名空间childip link这个命名空间中不会显示veth1装置:

代码语言:javascript
复制
[netns: HOST]# ip link | grep veth
36: veth0@if35: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000

而另一方面,veth1现在出现在子网络命名空间中:

代码语言:javascript
复制
[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:

代码语言:javascript
复制
[netns: HOST]# ip address add dev veth0 local 10.16.8.1/24
[netns: HOST]# ip link set veth0 up

可以使用以下命令验证命令的结果:

代码语言:javascript
复制
[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

子命名空间也是如此:

代码语言:javascript
复制
[netns: CHILD]# ip address add dev veth1 local 10.16.8.2/24
[netns: CHILD]# ip link set veth1 up
代码语言:javascript
复制
[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通:

代码语言:javascript
复制
[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
代码语言:javascript
复制
[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目录下。让我们来看看内容:

代码语言:javascript
复制
# 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路径下新建一个目录(名称由我们决定)。所以让我们这样做:

代码语言:javascript
复制
# mkdir /sys/fs/cgroup/memory/child
代码语言:javascript
复制
# 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

操作系统为每个新目录创建了一大堆文件。让我们看看其中一个文件:

代码语言:javascript
复制
# cat /sys/fs/cgroup/memory/demo/memory.limit_in_bytes
9223372036854771712

此文件中的值指示进程可以使用的最大内存(如果它是此 cgroup 的一部分)。让我们将此值设置为一个小得多的数字,例如 4MB,但以字节为单位:

代码语言:javascript
复制
# echo 4000000 > /sys/fs/cgroup/memory/demo/memory.limit_in_bytes

让我们看看这个文件:

代码语言:javascript
复制
# cat /sys/fs/cgroup/memory/demo/memory.limit_in_bytes
3997696

这不是我们写入文件的确切内容,但它大约为 3.99 MB。我的猜测是这与由操作系统管理的内存对齐有关。

现在在新的主机名命名空间中启动一个新进程:

代码语言:javascript
复制
# unshare -u

这将启动一个新的 shell 进程。尝试运行一个命令,wget我知道它需要超过 4MB 的内存才能运行:

代码语言:javascript
复制
# 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文件:

代码语言:javascript
复制
# echo $$ > /sys/fs/cgroup/memory/demo/cgroup.procs

让我们看看这个文件的内容:

代码语言:javascript
复制
# cat /sys/fs/cgroup/memory/demo/cgroup.procs
468401
468464

这里似乎有两个条目。第一个条目是我们写入文件的 shell 进程的 PID。另一个是cat我们运行的进程的PID 。这是因为默认情况下,所有子进程都与父进程属于同一个 cgroup。一旦进程终止,PID 会自动从文件中删除。如果我们再次运行相同的命令,我们仍然会找到两个条目,但第二个会有所不同:

代码语言:javascript
复制
# cat /sys/fs/cgroup/memory/demo/cgroup.procs
468401
468464

现在再次尝试运行wget命令:

代码语言:javascript
复制
# wget wikipedia.org
URL transformed to HTTPS due to an HSTS policy
--2020-02-24 12:44:26--  https://wikipedia.org/
Killed

该进程立即被杀死,因为它试图使用比当前允许的 cgroup 更多的内存。

四、总结

因此,namespacescgroups以隔离和控制资源的使用和形成普遍称为容器。:

  1. 功能:它限制了 root 权限的使用。有时需要运行需要提升权限才能做一件事的进程,但以 root 身份运行它是一种安全风险,因为这样该进程几乎可以对系统执行任何操作。为了限制这种情况,功能提供了一种分配特殊权限的方法,而无需向进程授予系统范围的 root 权限。一个例子是,如果需要一个程序能够管理网络接口和相关操作,可以授予该程序能力CAP_NET_ADMIN
  2. Seccomp:它限制了系统调用的使用。为了进一步降低安全性,可以使用它们来阻止可能造成额外伤害的系统调用。例如,阻塞kill系统调用将阻止进程能够终止或向其他进程发送信号。

所以在namespaces允许我们隔离资源类型的同时,cgroups帮助我们控制一个进程的资源使用。并capabilities通过将操作分解为不同类型的功能来限制 root 权限的使用。最后seccomp有助于阻止进程调用不需要的系统调用。这些概念组合在一起形成了一个容器,这是一种比同时担心所有这些更好的抽象。

五、最后一点

fork这篇文章前面的图表有点不完整。这是一个更完整的图表:

表单等待pid
表单等待pid

如前所述fork,将子进程的 PID 返回给父进程,并使用此 PID 来“等待”子进程完成执行。这是由waitpid系统调用完成的。这对于避免僵尸进程很重要,这被称为收割。一旦子进程终止,父进程就有责任确保为子进程分配的所有资源都被清理干净。简而言之,是容器运行时或容器引擎的工作。它产生新的容器或子进程,并确保在容器终止后清理资源。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、容器是什么
  • 二、命名空间
  • 三、群组
  • 四、总结
  • 五、最后一点
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档