精《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #7 Cgroup、Namespace、Linux容器

时间:2022-03-11 00:16:53

HACK #7 Cgroup、Namespace、Linux容器

本节将介绍Cgroup与Namespace以及通过这两个功能实现的容器功能。
Cgroup
Cgroup(control group)是将任意进程进行分组化管理的Linux内核功能。Cgroup本身是提供将进程进行分组化管理的功能和接口的基础结构,I/O或内存的分配控制等具体的资源管理功能是通过这个功能来实现的。这些具体的资源管理功能称为Cgroup子系统或控制器。
Cgroup子系统有控制内存的Memory控制器、控制进程调度的CPU控制器等。运行中的内核可以使用的Cgroup子系统由/proc/cgroup来确认。
Cgroup提供了一个cgroup虚拟文件系统,作为进行分组管理和各子系统设置的用户接口。要使用Cgroup,必须挂载cgroup文件系统。这时通过挂载选项指定使用哪个子系统。这里指定debug这个没有实质功能的调试用子系统来挂载。

# mount -t cgroup -o debug cgroup /cgroup

注意事项:这里所说的“虚拟文件系统”,是指procfs和sysfs这种不具有物理设备的文件系统。并不是用来接纳文件系统差异的内核内部层layerVFS。
小贴士:关于cgroup文件系统的标准化挂载要点,是由开发论坛进行讨论的,但目前尚未得出结论。这里是挂载到/cgroup。
挂载后,在挂载位置下应该可以看到下列几个文件。这些是Cgroup呈现出来的特殊文件。

# ls /cgroup
cgroup.event_control debug.current_css_set debug.taskcount
cgroup.procs debug.current_css_set_cg_links notify_on_release
debug.cgroup_css_links debug.current_css_set_refcount release_agent
debug.cgroup_refcount debug.releasable tasks

文件名前缀为cgroup的以及没有前缀的文件是由Cgroup的基础结构提供的特殊文件。而前缀为debug的文件是由debug子系统提供的特殊文件。Cgroup的子系统提供的特殊文件都会像这样加上子系统的前缀。因此,根据挂载时指定的选项,即所使用的子系统不同,存在的特殊文件也不同。但是Cgroup的基础结构所提供的特殊文件则是无论指定哪种子系统都一直存在的。特殊文件分为只读文件和可读写文件。只读文件是为用户提供信息的文件。可读写的特殊文件通过写入值来更改Cgroup以及Cgroup子系统设置的文件。设置的值可以通过读入特殊文件来确认。在这些特殊文件中,最重要的是tasks特殊文件。其内容可以显示如下。

# cat /cgroup/tasks
1
2
3
4
...

虽然看上去只是一些数字的排列,但其实这些是属于这个分组的线程的线程ID(TID)。在这时,系统上运行的所有线程的TID都包含在/cgroup/tasks中。这就表示全部线程都属于这个分组。那么这里出现的“分组”又是什么呢?分组,就是体现为cgroup文件系统目录的线程的集合。由于/cgroup也是目录,因此它也表示一个分组。像这样位于挂载点最上层的目录是自动生成的分组,称为根分组。在这个阶段,只有/cgroup(即根分组)是系统上存在的唯一分组。
小贴士:英语中将通过Cgroup创建的分组称做cgroup,容易与表示结构的“Cgroup”混淆,所以这里仅称为“分组”。
下面尝试创建一个分组,也就是在/cgroup下创建子目录。其内容如下所示。

# mkdir /cgroup/test
# ls /cgroup/test
cgroup.event_control debug.current_css_set debug.taskcount
cgroup.procs debug.current_css_set_cg_links notify_on_release
debug.cgroup_css_links debug.current_css_set_refcount tasks
debug.cgroup_refcount debug.releasable

虽然是新生成的目录,但是已经有文件存在。cgroup文件系统在目录生成的同时就会在其中配置特殊文件。
/cgroup/test也和/cgroup一样有tasks。其内容如下。

# cat /cgroup/test/tasks

tasks的内容似乎是空的,这表示这个分组内一个线程也没有。可以将适当的线程添加到这个分组中。要将线程添加到分组中,可以在tasks中写入该线程的TID。这里以添加shell本身为例。

# echo 
$$

2474
# echo
$$
> /cgroup/test/tasks
# cat /cgroup/test/tasks
2474
3821

tasks的内容中包含shell的TID(也是PID,即进程ID)—2474,可以看出这个shell已经属于test分组。除此以外,这个分组内还有另一个TID为3821的线程,这是什么呢?我们再来看一下tasks的内容。

# cat /cgroup/test/tasks
2474
3822

结果居然发生了变化。事实上这个改变的部分,是显示了tasks内容的cat进程的TID。最初的cat(3821)和第二次的cat(3822)是不同的进程,TID也不同,所以结果发生了变化。但是似乎并没有将cat进程添加到test分组中。其实,属于分组的进程一旦生成子进程,其子进程就会自动属于母进程。由于cat是shell的子进程,因此前者自动属于test分组。大家应该还记得,在挂载cgroup文件系统后,系统上的所有线程是属于根分组的。也就是说,除了将明确指定为新生成分组内的进程为祖先进程以外,生成的进程都属于根分组。
这时,再显示/cgroup/tasks的内容的话,应该不会显示shell的TID(2474)。这是因为shell不属于根分组,而是属于test分组。然后,再将这个shell返回到根分组。

# echo 2474 > /cgroup/tasks

这样,shell的TID(2474)就再次属于/cgroup/tasks,而/cgroup/test/tasks就变空。如果分组中一个线程也没有,可以进行撤销。删除目录就可以撤销分组。

# rmdir /cgroup/test

表2-1是每个子系统中Cgroup都会提供的特殊文件列表。
表2-1 Cgroup提供的文件种类
精《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #7 Cgroup、Namespace、Linux容器

这里仅介绍了最基本的Cgroup使用方法,也就是分组的创建、撤销和将线程添加到分组的方法。实际使用Cgroup时,应在将线程添加到分组后,在分组内的特殊文件中设置值,来控制系统的运行。
Namespace
使用Namespace(命名空间),可以让每个进程组具有独立的PID、IPC和网络空间。
可以向clone系统调用的第3个参数flags设置划分命名空间的标志,通过执行clone系统调用可以划分命名空间。
例如,划分PID命名空间后,在新生成的PID命名空间内进程的PID是从1开始的。从新PID为1的进程fork()分叉得到的进程,被封闭到这个新的PID命名空间,与其他PID命名空间分隔开。在新创建的PID命名空间中生成的进程,其PID有可能与存在于原PID命名空间中的进程相同,但由于二者的PID命名空间划分开,就不存在相互影响。
同样,也可以用PID、网络、文件系统的挂载空间、UTS(Universal Time sharing System)为对象进行资源划分。可以在clone系统调用的第3个参数中设置资源划分的种类,如表2-2所示。
表2-2 资源划分
精《Linux内核精髓:精通Linux内核必会的75个绝技》一HACK #7 Cgroup、Namespace、Linux容器

Linux 容器
使用Cgroup和Namespace就可以实现容器。容器这个技术也称为操作系统虚拟化,是将一个内核所管理的资源划分成多个分组。
在容器中,CPU和内存资源是使用Cgroup来划分的。PID、IPC、网络等资源使用Namespace来划分。
LXC
Linux中实际安装的容器有LXC(Linux Container)。本节将以Fedora 14为例介绍LXC的使用方法。

# yum install lxc

要使用网络,还需要安装bridge-utils。

# yum install bridge-utils

在使用LXC之前,必须启用cgroup文件系统。使用下列命令挂载cgroup文件系统。

# mount -t cgroup cgroup /cgroup

另外,向/etc/fstab添加下列语句,就可以在系统启动时自动挂载cgroup文件系统。

cgroup /cgroup cgroup defaults 0 0

首先,将bash shell进程放进容器。
这里要为容器中使用的文件系统准备一个/lxc目录。

# mkdir /lxc
# cd /lxc

然后准备作为容器内的根文件系统的目录。

# mkdir rootfs
# cd rootfs

还需要准备其他必要的目录(这些目录主要使用bind mount)。

# mkdir bin dev etc lib lib64 proc sbin sys usr var

然后还要生成LXC的配置文件lxc.conf以及引用的fstab。

# vi /lxc/lxc.conf
lxc.utsname = lxc
lxc.rootfs = /lxc/rootfs
lxc.mount = /lxc/fstab

# vi /lxc/fstab
/bin /lxc/rootfs/bin none ro,bind 0 0
/sbin /lxc/rootfs/sbin none ro,bind 0 0
/lib /lxc/rootfs/lib none ro,bind 0 0
/lib64 /lxc/rootfs/lib64 none ro,bind 0 0
/etc /lxc/rootfs/etc none ro,bind 0 0
/usr /lxc/rootfs/usr none ro,bind 0 0
/dev /lxc/rootfs/dev none rw,bind 0 0
/dev/pts /lxc/rootfs/dev/pts none rw,bind 0 0
/proc /lxc/rootfs/proc proc defaults 0 0
/sys /lxc/rootfs/sys sysfs defaults 0 0

准备工作完成后,使用lxc-create命令生成名为lxc的容器。

# lxc-create -n lxc -f /lxc/lxc.conf

使用lxc-ls命令可以确认容器列表。

# lxc-ls
lxc

使用lxc-create命令在lxc容器内执行bash。

# lxc-execute -n lxc bash

小贴士:在笔者的环境下,出现了终端的按键输入不显示的情况。发生这种情况时可以执行reset命令,终端的操作就会恢复。
bash 4.1# reset(不显示在画面上)
执行ps命令,就可以发现PID是从1开始的,除lxc容器以外看不到其他进程。

bash-4.1# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.1 0.1 14688 664 pts/0 S 21:34 0:00 /usr/lib64/lxc/lxc-init -- bash
root 2 0.3 0.3 108440 1760 pts/0 S 21:34 0:00 bash
root 6 0.0 0.2 108120 1104 pts/0 R+ 21:34 0:00 ps aux

另外,生成的容器可以使用lxc-destroy命令来撤销。

# lxc-destroy -n lxc

接下来尝试启用网络,并启动sshd进程。然后,创建用来连接分配到容器的网络接口的网桥(关于网桥请参考Hack #24)。
在这个例子中将IP地址设置为192.168.20.254。

# brctl addbr br0
# ifconfig br0 192.168.20.254

然后,修改lxc.conf,添加网络设置。

# vi /lxc/lxc.conf
lxc.utsname = lxc
lxc.rootfs = /lxc/rootfs
lxc.mount = /lxc/fstab
lxc.network.type = veth
lxc.network.flags = up
lxc.network.link = br0
lxc.network.name = eth0
lxc.network.ipv4 = 192.168.20.1/24

启动sshd时需要下列目录,要事先创建。当该目录不存在时,从lxc-execute启动sshd时就会失败。

# mkdir -p rootfs/var/empty/sshd

接下来生成容器。

# lxc-execute -n lxc /usr/sbin/sshd

这时打开其他终端,确认SSH服务器是否正在运行。
首先,使用ping命令确认网络是否已连接。

# ping 192.168.20.1
PING 192.168.20.1 (192.168.20.1) 56(84) bytes of data.
64 bytes from 192.168.20.1: icmp_req=1 ttl=64 time=0.899 ms
64 bytes from 192.168.20.1: icmp_req=2 ttl=64 time=0.174 ms
^C

使用ssh命令,连接分配到容器的IP地址192.168.20.1。

# ssh 192.168.20.1
root@192.168.20.1's password:

SSH连接已建立,输入密码后就成功登录。
通过ps命令所显示的进程来确认资源是否已被容器隔离。

-bash-4.1# ps auxw
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 14688 660 pts/0 S+ 21:43 0:00 /usr/lib64/lxc/
lxc-init -- /usr/sbin/sshd
root 3 0.0 0.2 75104 1116 ? Ss 21:43 0:00 /usr/sbin/sshd
root 4 1.4 0.8 108816 4068 ? Ss 21:47 0:00 sshd: root@pts/3
root 6 1.6 0.3 108440 1904 pts/3 Ss 21:47 0:00 -bash
root 19 0.0 0.2 108124 1104 pts/3 R+ 21:47 0:00 ps auxw

-bash-4.1# ifconfig eth0
eth0 Link encap:Ethernet HWaddr 3A:A1:C2:A0:6F:1B
inet addr:192.168.20.1 Bcast:192.168.20.0 Mask:255.255.255.0
inet6 addr: fe80::38a1:c2ff:fea0:6f1b/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:280 errors:0 dropped:0 overruns:0 frame:0
TX packets:259 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:31389 (30.6 KiB) TX bytes:31373 (30.6 KiB)

-bash-4.1# exit
logout

可以在其他终端执行lxc-stop,来关闭装有sshd的容器。

# lxc-stop -n lxc

然后运行debian。
使用debootstrap创建用来启动debian的根文件系统。deboot strap使用yum来安装。

# yum install debootstrap

创建debian的根文件系统,需要准备/debian。

# mkdir /debian
# cd /debian

现在执行debootstrap,生成debian lenny的文件系统。

# debootstrap - -arch=amd64 lenny lenny

然后,准备LXC用的设置。

# vi /debian/lenny.conf
lxc.utsname = lenny
lxc.network.type = veth
lxc.network.flags = up
lxc.network.link = br0
lxc.network.name = eth0
lxc.network.ipv4 = 192.168.20.2/24
lxc.rootfs = /debian/lenny
lxc.mount = /debian/lenny.fstab

# vi /debian/lenny.fstab
devpts /debian/lenny/dev/pts devpts defaults 0 0
proc /debian/lenny/proc proc defaults 0 0
sysfs /debian/lenny/sys sysfs defaults 0 0

创建名称为lenny的容器。

# lxc-create -n lenny -f lenny.conf

所创建的容器可以通过lxc-start来启动。只到这一步的话,init虽然启动,但不能进行任何操作。这是因为刚执行debootstrap后,安装的仅是最低限度需要的数据包。这里为了让外部能够连接到容器,需要安装sshd。

# lxc-start -n lenny bash

debian环境的shell就会在容器内启动。然后使用apt-get安装openssh-server。

lenny:~# apt-get install openssh-server

另外,为了在SSH上进行登录,还需要设置root的密码。

lenny:~# passwd

在容器内启动lenny。

# lxc-start -n lenny
INIT: version 2.86 booting
Setting the system clock.
Cannot access the Hardware Clock via any known method.
Use the --debug option to see the details of our search for an access method.
Unable to set System Clock to: Fri Dec 10 22:59:45 UTC 2010 (warning).
Activating swap...done.
Setting the system clock.
Cannot access the Hardware Clock via any known method.
Use the --debug option to see the details of our search for an access method.
Unable to set System Clock to: Fri Dec 10 22:59:46 UTC 2010 (warning).
Cleaning up ifupdown....
Loading kernel modules...FATAL: Could not load /lib/modules/2.6.35.9-64.fc14.x86_64/modules.dep: No such file or directory
Checking file systems...fsck 1.41.3 (12-Oct-2008)
done.
Setting kernel variables (/etc/sysctl.conf)...done.
Mounting local filesystems...done.
Activating swapfile swap...done.
Setting up networking....
Configuring network interfaces...done.
INIT: Entering runlevel: 2
Starting enhanced syslogd: rsyslogd.
Starting OpenBSD Secure Shell server: sshd.
Starting periodic command scheduler: crond.

我们尝试从其他终端使用SSH来连接。

# ssh 192.168.20.2
root@192.168.20.2's password:
lenny:~#

在debian环境下实施关闭(shutdown)的话,容器结束。

lenny:~# shutdown -h now

按照前面所述方法使用LXC就可以简单地创建容器。
小结
本节介绍了Linux内核的资源划分功能:划分CPU、内存空间、I/O等的Cgroup,以及划分PID、IPC、网络、mount命名空间的Namespace。另外,还介绍了实际安装上述资源划分功能的容器LXC。
参考文献
man 2 clone
man 2 unshare
LXC
http://lxc.sourceforge.net/
http://www.ibm.com/developerworks/jp/linux/library/l-lxc-containers/
debootstrap
http://www.debian.org/releases/stable/i386/apds03.html.ja
—Munehiro IKEDA, Hiroshi Shimamoto