Table of Contents
(6) freezer – 暂停/恢复cgroup中的task
systemd-nspawn 在轻量级容器中运行命令或操作系统
Docker背后的内核知识——cgroups资源限制
摘要
当我们谈论Docker时,我们常常会聊到Docker的实现方式。很多开发者都会知道,Docker的本质实际上是宿主机上的一个进程,通过namespace实现了资源隔离,通过cgroup实现了资源限制,通过UnionFS实现了Copy on Write的文件操作。但是当我们再深入一步的提出,namespace和cgroup实现细节时,知道的人可能就所剩无几了。本文在docker基础研究工作中着重对内核的cgroup技术做了细致的分析和梳理,希望能对读者深入理解Docker有所帮助
正文
上一篇中,我们了解了Docker背后使用的资源隔离技术namespace,通过系统调用构建一个相对隔离的shell环境,也可以称之为一个简单的“容器”。本文我们则要开始讲解另一个强大的内核工具——cgroups。他不仅可以限制被namespace隔离起来的资源,还可以为资源设置权重、计算使用量、操控进程启停等等。在介绍完基本概念后,我们将详细讲解Docker中使用到的cgroups内容。希望通过本文,让读者对Docker有更深入的了解。
1. cgroups是什么
cgroups(Control Groups)最初叫Process Container,由Google工程师(Paul Menage和Rohit Seth)于2006年提出,后来因为Container有多重含义容易引起误解,就在2007年更名为Control Groups,并被整合进Linux内核。顾名思义就是把进程放到一个组里面统一加以控制。官方的定义如下{![引自:https://www.kernel.org/doc/Documentation/cgroups/cgroups.txt]}。
cgroups是Linux内核提供的一种机制,这种机制可以根据特定的行为,把一系列系统任务及其子任务整合(或分隔)到按资源划分等级的不同组内,从而为系统资源管理提供一个统一的框架。
通俗的来说,cgroups可以限制、记录、隔离进程组所使用的物理资源(包括:CPU、memory、IO等),为容器实现虚拟化提供了基本保证,是构建Docker等一系列虚拟化管理工具的基石。
对开发者来说,cgroups有如下四个有趣的特点:
* cgroups的API以一个伪文件系统的方式实现,即用户可以通过文件操作实现cgroups的组织管理。
* cgroups的组织管理操作单元可以细粒度到线程级别,用户态代码也可以针对系统分配的资源创建和销毁cgroups,从而实现资源再分配和管理。
* 所有资源管理的功能都以“subsystem(子系统)”的方式实现,接口统一。
* 子进程创建之初与其父进程处于同一个cgroups的控制组。
本质上来说,cgroups是内核附加在程序上的一系列钩子(hooks),通过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的。
2. cgroups的作用
实现cgroups的主要目的是为不同用户层面的资源管理,提供一个统一化的接口。从单个进程的资源控制到操作系统层面的虚拟化。Cgroups提供了以下四大功能{![参照自:http://en.wikipedia.org/wiki/Cgroups]}。
- 资源限制(Resource Limitation):cgroups可以对进程组使用的资源总额进行限制。如设定应用运行时使用内存的上限,一旦超过这个配额就发出OOM(Out of Memory)。
- 优先级分配(Prioritization):通过分配的CPU时间片数量及硬盘IO带宽大小,实际上就相当于控制了进程运行的优先级。
- 资源统计(Accounting): cgroups可以统计系统的资源使用量,如CPU使用时长、内存用量等等,这个功能非常适用于计费。
- 进程控制(Control):cgroups可以对进程组执行挂起、恢复等操作。
过去有一段时间,内核开发者甚至把namespace也作为一个cgroups的subsystem加入进来,也就是说cgroups曾经甚至还包含了资源隔离的能力。但是资源隔离会给cgroups带来许多问题,如PID在循环出现的时候cgroup却出现了命名冲突、cgroup创建后进入新的namespace导致脱离了控制等等{![详见:https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=a77aea92010acf54ad785047234418d5d68772e2]},所以在2011年就被移除了。
3. 术语表
- task(任务):cgroups的术语中,task就表示系统的一个进程。
- cgroup(控制组):cgroups 中的资源控制都以cgroup为单位实现。cgroup表示按某种资源控制标准划分而成的任务组,包含一个或多个子系统。一个任务可以加入某个cgroup,也可以从某个cgroup迁移到另外一个cgroup。
- subsystem(子系统):cgroups中的subsystem就是一个资源调度控制器(Resource Controller)。比如CPU子系统可以控制CPU时间分配,内存子系统可以限制cgroup内存使用量。
- hierarchy(层级树):hierarchy由一系列cgroup以一个树状结构排列而成,每个hierarchy通过绑定对应的subsystem进行资源调度。hierarchy中的cgroup节点可以包含零或多个子节点,子节点继承父节点的属性。整个系统可以有多个hierarchy。
4. 组织结构与基本规则
大家在namespace技术的讲解中已经了解到,传统的Unix进程管理,实际上是先启动init
进程作为根节点,再由init
节点创建子进程作为子节点,而每个子节点由可以创建新的子节点,如此往复,形成一个树状结构。而cgroups也是类似的树状结构,子节点都从父节点继承属性。
它们最大的不同在于,系统中cgroup构成的hierarchy可以允许存在多个。如果进程模型是由init
作为根节点构成的一棵树的话,那么cgroups的模型则是由多个hierarchy构成的森林。这样做的目的也很好理解,如果只有一个hierarchy,那么所有的task都要受到绑定其上的subsystem的限制,会给那些不需要这些限制的task造成麻烦。
了解了cgroups的组织结构,我们再来了解cgroup、task、subsystem以及hierarchy四者间的相互关系及其基本规则{![参照自:https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Resource_Management_Guide/sec-Relationships_Between_Subsystems_Hierarchies_Control_Groups_and_Tasks.html]}。
- 规则1: 同一个hierarchy可以附加一个或多个subsystem。如下图1,cpu和memory的subsystem附加到了一个hierarchy。
图1 同一个hierarchy可以附加一个或多个subsystem -
规则2: 一个subsystem可以附加到多个hierarchy,当且仅当这些hierarchy只有这唯一一个subsystem。如下图2,小圈中的数字表示subsystem附加的时间顺序,CPU subsystem附加到hierarchy A的同时不能再附加到hierarchy B,因为hierarchy B已经附加了memory subsystem。如果hierarchy B与hierarchy A状态相同,没有附加过memory subsystem,那么CPU subsystem同时附加到两个hierarchy是可以的。
图2 一个已经附加在某个hierarchy上的subsystem不能附加到其他含有别的subsystem的hierarchy上 -
规则3: 系统每次新建一个hierarchy时,该系统上的所有task默认构成了这个新建的hierarchy的初始化cgroup,这个cgroup也称为root cgroup。对于你创建的每个hierarchy,task只能存在于其中一个cgroup中,即一个task不能存在于同一个hierarchy的不同cgroup中,但是一个task可以存在在不同hierarchy中的多个cgroup中。如果操作时把一个task添加到同一个hierarchy中的另一个cgroup中,则会从第一个cgroup中移除。在下图3中可以看到,
httpd
进程已经加入到hierarchy A中的/cg1
而不能加入同一个hierarchy中的/cg2
,但是可以加入hierarchy B中的/cg3
。实际上不允许加入同一个hierarchy中的其他cgroup野生为了防止出现矛盾,如CPU subsystem为/cg1
分配了30%,而为/cg2
分配了50%,此时如果httpd
在这两个cgroup中,就会出现矛盾。
图3 一个task不能属于同一个hierarchy的不同cgroup -
规则4: 进程(task)在fork自身时创建的子任务(child task)默认与原task在同一个cgroup中,但是child task允许被移动到不同的cgroup中。即fork完成后,父子进程间是完全独立的。如下图4中,小圈中的数字表示task 出现的时间顺序,当
httpd
刚fork出另一个httpd
时,在同一个hierarchy中的同一个cgroup中。但是随后如果PID为4840的httpd
需要移动到其他cgroup也是可以的,因为父子任务间已经独立。总结起来就是:初始化时子任务与父任务在同一个cgroup,但是这种关系随后可以改变。
图4 刚fork出的子进程在初始状态与其父进程处于同一个cgroup
5. subsystem简介
subsystem实际上就是cgroups的资源控制系统,每种subsystem独立地控制一种资源,目前Docker使用如下八种subsystem,还有一种net_cls
subsystem在内核中已经广泛实现,但是Docker尚未使用。他们的用途分别如下。
- blkio: 这个subsystem可以为块设备设定输入/输出限制,比如物理驱动设备(包括磁盘、固态硬盘、USB等)。
- cpu: 这个subsystem使用调度程序控制task对CPU的使用。
- cpuacct: 这个subsystem自动生成cgroup中task对CPU资源使用情况的报告。
- cpuset: 这个subsystem可以为cgroup中的task分配独立的CPU(此处针对多处理器系统)和内存。
- devices 这个subsystem可以开启或关闭cgroup中task对设备的访问。
- freezer 这个subsystem可以挂起或恢复cgroup中的task。
- memory 这个subsystem可以设定cgroup中task对内存使用量的限定,并且自动生成这些task对内存资源使用情况的报告。
- perf_event 这个subsystem使用后使得cgroup中的task可以进行统一的性能测试。{![perf: Linux CPU性能探测器,详见https://perf.wiki.kernel.org/index.php/Main_Page]}
- *net_cls 这个subsystem Docker没有直接使用,它通过使用等级识别符(classid)标记网络数据包,从而允许 Linux 流量控制程序(TC:Traffic Controller)识别从具体cgroup中生成的数据包。
6. cgroups实现方式及工作原理简介
(1)cgroups实现结构讲解
cgroups的实现本质上是给系统进程挂上钩子(hooks),当task运行的过程中涉及到某个资源时就会触发钩子上所附带的subsystem进行检测,最终根据资源类别的不同使用对应的技术进行资源限制和优先级分配。那么这些钩子又是怎样附加到进程上的呢?下面我们将对照结构体的图表一步步分析,请放心,描述代码的内容并不多。
图5 cgroups相关结构体一览
Linux中管理task进程的数据结构为task_struct
(包含所有进程管理的信息),其中与cgroup相关的字段主要有两个,一个是css_set *cgroups
,表示指向css_set
(包含进程相关的cgroups信息)的指针,一个task只对应一个css_set
结构,但是一个css_set
可以被多个task使用。另一个字段是list_head cg_list
,是一个链表的头指针,这个链表包含了所有的链到同一个css_set
的task进程(在图中使用的回环箭头,均表示可以通过该字段找到所有同类结构,获得信息)。
每个css_set
结构中都包含了一个指向cgroup_subsys_state
(包含进程与一个特定子系统相关的信息)的指针数组。cgroup_subsys_state
则指向了cgroup
结构(包含一个cgroup的所有信息),通过这种方式间接的把一个进程和cgroup联系了起来,如下图6。
图6 从task结构开始找到cgroup结构
另一方面,cgroup
结构体中有一个list_head css_sets
字段,它是一个头指针,指向由cg_cgroup_link
(包含cgroup与task之间多对多关系的信息,后文还会再解释)形成的链表。由此获得的每一个cg_cgroup_link
都包含了一个指向css_set *cg
字段,指向了每一个task的css_set
。css_set
结构中则包含tasks
头指针,指向所有链到此css_set
的task进程构成的链表。至此,我们就明白如何查看在同一个cgroup中的task有哪些了,如下图7。
图7 cglink多对多双向查询
细心的读者可能已经发现,css_set
中也有指向所有cg_cgroup_link
构成链表的头指针,通过这种方式也能定位到所有的cgroup,这种方式与图1中所示的方式得到的结果是相同的。
那么为什么要使用cg_cgroup_link
结构体呢?因为task与cgroup之间是多对多的关系。熟悉数据库的读者很容易理解,在数据库中,如果两张表是多对多的关系,那么如果不加入第三张关系表,就必须为一个字段的不同添加许多行记录,导致大量冗余。通过从主表和副表各拿一个主键新建一张关系表,可以提高数据查询的灵活性和效率。
而一个task可能处于不同的cgroup,只要这些cgroup在不同的hierarchy中,并且每个hierarchy挂载的子系统不同;另一方面,一个cgroup中可以有多个task,这是显而易见的,但是这些task因为可能还存在在别的cgroup中,所以它们对应的css_set
也不尽相同,所以一个cgroup也可以对应多个·css_set
。
在系统运行之初,内核的主函数就会对root cgroups
和css_set
进行初始化,每次task进行fork/exit时,都会附加(attach)/分离(detach)对应的css_set
。
综上所述,添加cg_cgroup_link
主要是出于性能方面的考虑,一是节省了task_struct
结构体占用的内存,二是提升了进程fork()/exit()
的速度。
图8 css_set与hashtable关系
当task从一个cgroup中移动到另一个时,它会得到一个新的css_set
指针。如果所要加入的cgroup与现有的cgroup子系统相同,那么就重复使用现有的css_set
,否则就分配一个新css_set
。所有的css_set
通过一个哈希表进行存放和查询,如上图8中所示,hlist_node hlist
就指向了css_set_table
这个hash表。
同时,为了让cgroups便于用户理解和使用,也为了用精简的内核代码为cgroup提供熟悉的权限和命名空间管理,内核开发者们按照Linux 虚拟文件系统转换器(VFS:Virtual Filesystem Switch)的接口实现了一套名为cgroup
的文件系统,非常巧妙地用来表示cgroups的hierarchy概念,把各个subsystem的实现都封装到文件系统的各项操作中。有兴趣的读者可以在网上搜索并阅读VFS的相关内容,在此就不赘述了。
定义子系统的结构体是cgroup_subsys
,在图9中可以看到,cgroup_subsys
中定义了一组函数的接口,让各个子系统自己去实现,类似的思想还被用在了cgroup_subsys_state
中,cgroup_subsys_state
并没有定义控制信息,只是定义了各个子系统都需要用到的公共信息,由各个子系统各自按需去定义自己的控制信息结构体,最终在自定义的结构体中把cgroup_subsys_state
包含进去,然后内核通过container_of
(这个宏可以通过一个结构体的成员找到结构体自身)等宏定义来获取对应的结构体。
图9 cgroup子系统结构体
(2)基于cgroups实现结构的用户层体现
了解了cgroups实现的代码结构以后,再来看用户层在使用cgroups时的限制,会更加清晰。
在实际的使用过程中,你需要通过挂载(mount)cgroup
文件系统新建一个层级结构,挂载时指定要绑定的子系统,缺省情况下默认绑定系统所有子系统。把cgroup文件系统挂载(mount)上以后,你就可以像操作文件一样对cgroups的hierarchy层级进行浏览和操作管理(包括权限管理、子文件管理等等)。除了cgroup文件系统以外,内核没有为cgroups的访问和操作添加任何系统调用。
如果新建的层级结构要绑定的子系统与目前已经存在的层级结构完全相同,那么新的挂载会重用原来已经存在的那一套(指向相同的css_set)。否则如果要绑定的子系统已经被别的层级绑定,就会返回挂载失败的错误。如果一切顺利,挂载完成后层级就被激活并与相应子系统关联起来,可以开始使用了。
目前无法将一个新的子系统绑定到激活的层级上,或者从一个激活的层级中解除某个子系统的绑定。
当一个顶层的cgroup文件系统被卸载(umount)时,如果其中创建后代cgroup目录,那么就算上层的cgroup被卸载了,层级也是激活状态,其后代cgoup中的配置依旧有效。只有递归式的卸载层级中的所有cgoup,那个层级才会被真正删除。
层级激活后,/proc
目录下的每个task PID文件夹下都会新添加一个名为cgroup
的文件,列出task所在的层级,对其进行控制的子系统及对应cgroup文件系统的路径。
一个cgroup创建完成,不管绑定了何种子系统,其目录下都会生成以下几个文件,用来描述cgroup的相应信息。同样,把相应信息写入这些配置文件就可以生效,内容如下。
tasks
:这个文件中罗列了所有在该cgroup中task的PID。该文件并不保证task的PID有序,把一个task的PID写到这个文件中就意味着把这个task加入这个cgroup中。cgroup.procs
:这个文件罗列所有在该cgroup中的线程组ID。该文件并不保证线程组ID有序和无重复。写一个线程组ID到这个文件就意味着把这个组中所有的线程加到这个cgroup中。notify_on_release
:填0或1,表示是否在cgroup中最后一个task退出时通知运行release agent
,默认情况下是0,表示不运行。release_agent
:指定release agent执行脚本的文件路径(该文件在最顶层cgroup目录中存在),在这个脚本通常用于自动化umount
无用的cgroup。
除了上述几个通用的文件以外,绑定特定子系统的目录下也会有其他的文件进行子系统的参数配置。
在创建的hierarchy中创建文件夹,就类似于fork中一个后代cgroup,后代cgroup中默认继承原有cgroup中的配置属性,但是你可以根据需求对配置参数进行调整。这样就把一个大的cgroup系统分割成一个个嵌套的、可动态变化的“软分区”。
7. cgroups的使用方法简介
(1)安装cgroups工具库
本节主要针对Ubuntu14.04版本系统进行介绍,其他Linux发行版命令略有不同,原理是一样的。不安装cgroups工具库也可以使用cgroups,安装它只是为了更方便的在用户态对cgroups进行管理,同时也方便初学者理解和使用,本节对cgroups的操作和使用都基于这个工具库。
apt-get install cgroup-bin
安装的过程会自动创建/cgroup
目录,如果没有自动创建也不用担心,使用 mkdir /cgroup
手动创建即可。在这个目录下你就可以挂载各类子系统。安装完成后,你就可以使用lssubsys
(罗列所有的subsystem挂载情况)等命令。
说明:也许你在其他文章中看到的cgroups工具库教程,会在/etc目录下生成一些初始化脚本和配置文件,默认的cgroup配置文件为/etc/cgconfig.conf
,但是因为存在使LXC无法运行的bug,所以在新版本中把这个配置移除了,详见:https://bugs.launchpad.net/ubuntu/+source/libcgroup/+bug/1096771。
(2)查询cgroup及子系统挂载状态
在挂载子系统之前,可能你要先检查下目前子系统的挂载状态,如果子系统已经挂载,根据第4节中讲的规则2,你就无法把子系统挂载到新的hierarchy,此时就需要先删除相应hierarchy或卸载对应子系统后再挂载。
- 查看所有的cgroup:
lscgroup
- 查看所有支持的子系统:
lssubsys -a
- 查看所有子系统挂载的位置:
lssubsys –m
- 查看单个子系统(如memory)挂载位置:
lssubsys –m memory
(3)创建hierarchy层级并挂载子系统
在组织结构与规则一节中我们提到了hierarchy层级和subsystem子系统的关系,我们知道使用cgroup的最佳方式是:为想要管理的每个或每组资源创建单独的cgroup层级结构。而创建hierarchy并不神秘,实际上就是做一个标记,通过挂载一个tmpfs{![基于内存的临时文件系统,详见:http://en.wikipedia.org/wiki/Tmpfs]}文件系统,并给一个好的名字就可以了,系统默认挂载的cgroup就会进行如下操作。
mount -t tmpfs cgroups /sys/fs/cgroup
其中-t
即指定挂载的文件系统类型,其后的cgroups
是会出现在mount
展示的结果中用于标识,可以选择一个有用的名字命名,最后的目录则表示文件的挂载点位置。
挂载完成tmpfs
后就可以通过mkdir
命令创建相应的文件夹。
mkdir /sys/fs/cgroup/cg1
再把子系统挂载到相应层级上,挂载子系统也使用mount命令,语法如下。
mount -t cgroup -o subsystems name /cgroup/name
其中 subsystems 是使用,
(逗号)分开的子系统列表,name 是层级名称。具体我们以挂载cpu和memory的子系统为例,命令如下。
mount –t cgroup –o cpu,memory cpu_and_mem /sys/fs/cgroup/cg1
从mount
命令开始,-t
后面跟的是挂载的文件系统类型,即cgroup
文件系统。-o
后面跟要挂载的子系统种类如cpu
、memory
,用逗号隔开,其后的cpu_and_mem
不被cgroup代码的解释,但会出现在/proc/mounts里,可以使用任何有用的标识字符串。最后的参数则表示挂载点的目录位置。
说明:如果挂载时提示mount: agent already mounted or /cgroup busy
,则表示子系统已经挂载,需要先卸载原先的挂载点,通过第二条中描述的命令可以定位挂载点。
(4)卸载cgroup
目前cgroup
文件系统虽然支持重新挂载,但是官方不建议使用,重新挂载虽然可以改变绑定的子系统和release agent
,但是它要求对应的hierarchy是空的并且release_agent会被传统的fsnotify
(内核默认的文件系统通知)代替,这就导致重新挂载很难生效,未来重新挂载的功能可能会移除。你可以通过卸载,再挂载的方式处理这样的需求。
卸载cgroup非常简单,你可以通过cgdelete
命令,也可以通过rmdir
,以刚挂载的cg1为例,命令如下。
rmdir /sys/fs/cgroup/cg1
rmdir
执行成功的必要条件是cg1下层没有创建其它cgroup,cg1中没有添加任何task,并且它也没有被别的cgroup所引用。
cgdelete cpu,memory:/
使用cgdelete
命令可以递归的删除cgroup及其命令下的后代cgroup,并且如果cgroup中有task,那么task会自动移到上一层没有被删除的cgroup中,如果所有的cgroup都被删除了,那task就不被cgroups控制。但是一旦再次创建一个新的cgroup,所有进程都会被放进新的cgroup中。
(5)设置cgroups参数
设置cgroups参数非常简单,直接对之前创建的cgroup对应文件夹下的文件写入即可,举例如下。
- 设置task允许使用的cpu为0和1.
echo 0-1 > /sys/fs/cgroup/cg1/cpuset.cpus
使用cgset
命令也可以进行参数设置,对应上述允许使用0和1cpu的命令为:
cgset -r cpuset.cpus=0-1 cpu,memory:/
(6)添加task到cgroup
- 通过文件操作进行添加
echo [PID] > /path/to/cgroup/tasks
上述命令就是把进程ID打印到tasks中,如果tasks文件中已经有进程,需要使用">>"
向后添加。 -
通过
cgclassify
将进程添加到cgroupcgclassify -g subsystems:path_to_cgroup pidlist
这个命令中,subsystems
指的就是子系统(如果使用man命令查看,可能也会使用controllers表示),如果mount了多个,就是用","
隔开的子系统名字作为名称,类似cgset
命令。 -
通过
cgexec
直接在cgroup中启动并执行进程cgexec -g subsystems:path_to_cgroup command arguments
command
和arguments
就表示要在cgroup中执行的命令和参数。cgexec
常用于执行临时的任务。
(7)权限管理
与文件的权限管理类似,通过chown
就可以对cgroup文件系统进行权限管理。
chown uid:gid /path/to/cgroup
uid
和gid
分别表示所属的用户和用户组。
8. subsystem配置参数用法
(1)blkio – BLOCK IO资源控制
- 限额类
限额类是主要有两种策略,一种是基于完全公平队列调度(CFQ:Completely Fair Queuing )的按权重分配各个cgroup所能占用总体资源的百分比,好处是当资源空闲时可以充分利用,但只能用于最底层节点cgroup的配置;另一种则是设定资源使用上限,这种限额在各个层次的cgroup都可以配置,但这种限制较为生硬,并且容器之间依然会出现资源的竞争。- 按比例分配块设备IO资源
- blkio.weight:填写100-1000的一个整数值,作为相对权重比率,作为通用的设备分配比。
- blkio.weight_device: 针对特定设备的权重比,写入格式为
device_types:node_numbers weight
,空格前的参数段指定设备,weight
参数与blkio.weight
相同并覆盖原有的通用分配比。{![查看一个设备的device_types:node_numbers
可以使用:ls -l /dev/DEV
,看到的用逗号分隔的两个数字就是。有的文章也称之为major_number:minor_number
。]}
- 控制IO读写速度上限
- blkio.throttle.read_bps_device:按每秒读取块设备的数据量设定上限,格式
device_types:node_numbers bytes_per_second
。 - blkio.throttle.write_bps_device:按每秒写入块设备的数据量设定上限,格式
device_types:node_numbers bytes_per_second
。 - blkio.throttle.read_iops_device:按每秒读操作次数设定上限,格式
device_types:node_numbers operations_per_second
。 - blkio.throttle.write_iops_device:按每秒写操作次数设定上限,格式
device_types:node_numbers operations_per_second
- blkio.throttle.read_bps_device:按每秒读取块设备的数据量设定上限,格式
- 针对特定操作(read, write, sync, 或async)设定读写速度上限
- blkio.throttle.io_serviced:针对特定操作按每秒操作次数设定上限,格式
device_types:node_numbers operation operations_per_second
- blkio.throttle.io_service_bytes:针对特定操作按每秒数据量设定上限,格式
device_types:node_numbers operation bytes_per_second
- blkio.throttle.io_serviced:针对特定操作按每秒操作次数设定上限,格式
- 统计与监控
以下内容都是只读的状态报告,通过这些统计项更好地统计、监控进程的 io 情况。- blkio.reset_stats:重置统计信息,写入一个int值即可。
- blkio.time:统计cgroup对设备的访问时间,按格式
device_types:node_numbers milliseconds
读取信息即可,以下类似。 - blkio.io_serviced:统计cgroup对特定设备的IO操作(包括read、write、sync及async)次数,格式
device_types:node_numbers operation number
- blkio.sectors:统计cgroup对设备扇区访问次数,格式
device_types:node_numbers sector_count
- blkio.io_service_bytes:统计cgroup对特定设备IO操作(包括read、write、sync及async)的数据量,格式
device_types:node_numbers operation bytes
- blkio.io_queued:统计cgroup的队列中对IO操作(包括read、write、sync及async)的请求次数,格式
number operation
- blkio.io_service_time:统计cgroup对特定设备的IO操作(包括read、write、sync及async)时间(单位为ns),格式
device_types:node_numbers operation time
- blkio.io_merged:统计cgroup 将 BIOS 请求合并到IO操作(包括read、write、sync及async)请求的次数,格式
number operation
- blkio.io_wait_time:统计cgroup在各设备中各类型IO操作(包括read、write、sync及async)在队列中的等待时间(单位ns),格式
device_types:node_numbers operation time
- blkio.*_recursive:各类型的统计都有一个递归版本,Docker中使用的都是这个版本。获取的数据与非递归版本是一样的,但是包括cgroup所有层级的监控数据。
(2) cpu – CPU资源控制
CPU资源的控制也有两种策略,一种是完全公平调度 (CFS:Completely Fair Scheduler)策略,提供了限额和按比例分配两种方式进行资源控制;另一种是实时调度(Real-Time Scheduler)策略,针对实时进程按周期分配固定的运行时间。配置时间都以微秒(µs)为单位,文件名中用us
表示。
- CFS调度策略下的配置
- 设定CPU使用周期使用时间上限
- cpu.cfs_period_us:设定周期时间,必须与
cfs_quota_us
配合使用。 - cpu.cfs_quota_us :设定周期内最多可使用的时间。这里的配置指task对单个cpu的使用上限,若
cfs_quota_us
是cfs_period_us
的两倍,就表示在两个核上完全使用。数值范围为1000 – 1000,000(微秒)。 - cpu.stat:统计信息,包含
nr_periods
(表示经历了几个cfs_period_us
周期)、nr_throttled
(表示task被限制的次数)及throttled_time
(表示task被限制的总时长)。
- 按权重比例设定CPU的分配
- cpu.shares:设定一个整数(必须大于等于2)表示相对权重,最后除以权重总和算出相对比例,按比例分配CPU时间。(如cgroup A设置100,cgroup B设置300,那么cgroup A中的task运行25%的CPU时间。对于一个4核CPU的系统来说,cgroup A 中的task可以100%占有某一个CPU,这个比例是相对整体的一个值。)
- RT调度策略下的配置
实时调度策略与公平调度策略中的按周期分配时间的方法类似,也是在周期内分配一个固定的运行时间。- cpu.rt_period_us :设定周期时间。
- cpu.rt_runtime_us:设定周期中的运行时间。
(3) cpuacct – CPU资源报告
这个子系统的配置是cpu
子系统的补充,提供CPU资源用量的统计,时间单位都是纳秒。
1. cpuacct.usage:统计cgroup中所有task的cpu使用时长
2. cpuacct.stat:统计cgroup中所有task的用户态和内核态分别使用cpu的时长
3. cpuacct.usage_percpu:统计cgroup中所有task使用每个cpu的时长
(4)cpuset – CPU绑定
为task分配独立CPU资源的子系统,参数较多,这里只选讲两个必须配置的参数,同时Docker中目前也只用到这两个。
1. cpuset.cpus:在这个文件中填写cgroup可使用的CPU编号,如0-2,16
代表 0、1、2和16这4个CPU。
2. cpuset.mems:与CPU类似,表示cgroup可使用的memory node
,格式同上
(5) device – 限制task对device的使用
- **设备黑/白名单过滤 **
- devices.allow:允许名单,语法
type device_types:node_numbers access type
;type
有三种类型:b(块设备)、c(字符设备)、a(全部设备);access
也有三种方式:r(读)、w(写)、m(创建)。 - devices.deny:禁止名单,语法格式同上。
- devices.allow:允许名单,语法
- 统计报告
- devices.list:报告为这个 cgroup 中的task设定访问控制的设备
(6) freezer – 暂停/恢复cgroup中的task
只有一个属性,表示进程的状态,把task放到freezer所在的cgroup,再把state改为FROZEN,就可以暂停进程。不允许在cgroup处于FROZEN状态时加入进程。
* **freezer.state **,包括如下三种状态:
– FROZEN 停止
– FREEZING 正在停止,这个是只读状态,不能写入这个值。
– THAWED 恢复
(7) memory – 内存资源管理
- 限额类
- memory.limit_in_bytes:强制限制最大内存使用量,单位有
k
、m
、g
三种,填-1
则代表无限制。 - memory.soft_limit_in_bytes:软限制,只有比强制限制设置的值小时才有意义。填写格式同上。当整体内存紧张的情况下,task获取的内存就被限制在软限制额度之内,以保证不会有太多进程因内存挨饿。可以看到,加入了内存的资源限制并不代表没有资源竞争。
- memory.memsw.limit_in_bytes:设定最大内存与swap区内存之和的用量限制。填写格式同上。
- memory.limit_in_bytes:强制限制最大内存使用量,单位有
- 报警与自动控制
- memory.oom_control:改参数填0或1,
0
表示开启,当cgroup中的进程使用资源超过界限时立即杀死进程,1
表示不启用。默认情况下,包含memory子系统的cgroup都启用。当oom_control
不启用时,实际使用内存超过界限时进程会被暂停直到有空闲的内存资源。
- memory.oom_control:改参数填0或1,
- 统计与监控类
- memory.usage_in_bytes:报告该 cgroup中进程使用的当前总内存用量(以字节为单位)
- memory.max_usage_in_bytes:报告该 cgroup 中进程使用的最大内存用量
- memory.failcnt:报告内存达到在
memory.limit_in_bytes
设定的限制值的次数 - memory.stat:包含大量的内存统计数据。
- cache:页缓存,包括 tmpfs(shmem),单位为字节。
- rss:匿名和 swap 缓存,不包括 tmpfs(shmem),单位为字节。
- mapped_file:memory-mapped 映射的文件大小,包括 tmpfs(shmem),单位为字节
- pgpgin:存入内存中的页数
- pgpgout:从内存中读出的页数
- swap:swap 用量,单位为字节
- active_anon:在活跃的最近最少使用(least-recently-used,LRU)列表中的匿名和 swap 缓存,包括 tmpfs(shmem),单位为字节
- inactive_anon:不活跃的 LRU 列表中的匿名和 swap 缓存,包括 tmpfs(shmem),单位为字节
- active_file:活跃 LRU 列表中的 file-backed 内存,以字节为单位
- inactive_file:不活跃 LRU 列表中的 file-backed 内存,以字节为单位
- unevictable:无法再生的内存,以字节为单位
- hierarchical_memory_limit:包含 memory cgroup 的层级的内存限制,单位为字节
- hierarchical_memsw_limit:包含 memory cgroup 的层级的内存加 swap 限制,单位为字节
8. 总结
本文由浅入深的讲解了cgroups的方方面面,从cgroups是什么,到cgroups该怎么用,最后对大量的cgroup子系统配置参数进行了梳理。可以看到,内核对cgroups的支持已经较为完善,但是依旧有许多工作需要完善。如网络方面目前是通过TC(Traffic Controller)来控制,未来需要统一整合;资源限制并没有解决资源竞争,在各自限制之内的进程依旧存在资源竞争,优先级调度方面依旧有很大的改进空间。希望通过本文帮助大家了解cgroups,让更多人参与到社区的贡献中。
参考资料
- https://sysadmincasts.com/episodes/14-introduction-to-linux-control-groups-cgroups
- https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Resource_Management_Guide/index.html
- http://www.cnblogs.com/lisperl/archive/2013/01/14/2860353.html
- https://www.kernel.org/doc/Documentation/cgroups
- http://www.sel.zju.edu.cn/?p=573
libvirt虚拟化API
libvirt项目:
- 是用于管理虚拟化平台的工具包
- 可从C,Python,Perl,Java等访问
- 根据开源许可证获得许可
- 支撑KVM, QEMU,Xen的, Virtuozzo的, VMware ESX的, LXC, BHyve和 更
- 针对Linux,FreeBSD,Windows和OS-X
- 被许多应用程序使用
最近/即将发布的版本更改
Linux 容器工具 LXC
LXC 项目由一个 Linux 内核补丁和一些 userspace 工具组成。这些 userspace 工具使用由补丁增加的内核新特性,提供一套简化的工具来维护容器。
容器可以提供轻量级的虚拟化,以便隔离进程和资源,而且不需要提供指令解释机制以及全虚拟化的其他复杂性。本文循序渐进地介绍容器工具 Linux Containers(LXC)。
容器有效地将由单个操作系统管理的资源划分到孤立的组中,以更好地在孤立的组之间平衡有冲突的资源使用需求。与虚拟化相比,这样既不需要指令级模 拟,也不需要即时编译。容器可以在核心 CPU 本地运行指令,而不需要任何专门的解释机制。此外,也避免了准虚拟化(paravirtualization)和系统调用替换中的复杂性。
通 过提供一种创建和进入容器的方式,操作系统让应用程序就像在独立的机器上运行一样,但又能共享很多底层的资源。例如,可以有效地共享公共文件(比如 glibc)的页缓存,因为所有容器都使用相同的内核,而且所有容器还常常共享相同的 libc 库(取决于容器配置)。这种共享常常可以扩展到目录中其他不需要写入内容的文件。
容器在提供隔离的同时,还通过共享这些资源节省开销,这意味着容器比真正的虚拟化的开销要小得多。
容 器技术早就出现。例如,Solaris Zones 和 BSD jails 就是非 Linux 操作系统上的容器。用于 Linux 的容器技术也有丰富的遗产,例如 Linux-Vserver、OpenVZ 和 FreeVPS。虽然这些技术都已经成熟,但是这些解决方案还没有将它们的容器支持集成到主流 Linux 内核。
systemd-nspawn 在轻量级容器中运行命令或操作系统
版权声明
本文译者是一位开源理念的坚定支持者,所以本文虽然不是软件,但是遵照开源的精神发布。
- 无担保:本文译者不保证译文内容准确无误,亦不承担任何由于使用此文档所导致的损失。
- 自由使用:任何人都可以自由的阅读/链接/打印此文档,无需任何附加条件。
- 名誉权:任何人都可以自由的转载/引用/再创作此文档,但必须保留译者署名并注明出处。
其他作品
本文译者十分愿意与他人分享劳动成果,如果你对我的其他翻译作品或者技术文章有兴趣,可以在如下位置查看现有的作品集:
联系方式
由于译者水平有限,因此不能保证译文内容准确无误。如果你发现了译文中的错误(哪怕是错别字也好),请来信指出,任何提高译文质量的建议我都将虚心接纳。
- Email(QQ):70171448在QQ邮箱
名称
systemd-nspawn — 在轻量级容器中运行命令或操作系统
大纲
systemd-nspawn
[OPTIONS...] [COMMAND
[ARGS...] ]
systemd-nspawn
--boot [OPTIONS...] [ARGS...]
描述
systemd-nspawn 可用于在一个轻量级的名字空间容器中运行一条命令、甚至一个操作系统。 它与 chroot(1) 很相似, 但是功能更为强大:它能够完全的虚拟化文件系统层次结构、进程树、各种进程间通信(IPC)子系统、 主机名与域名。
systemd-nspawn 能够 通过 --directory=
选项运行在任何包含操作系统的目录树上。通过使用 --machine=
选项, 可以自动在特定的位置(特别是保存容器镜像的默认目录 /var/lib/machines
) 搜索操作系统目录树。
与 chroot(1) 相比, systemd-nspawn 的强大之处还体现在它能够在容器内启动一个完整的Linux操作系统。
systemd-nspawn 会对容器内部的进程作如下限制:(1)仅允许以只读方式访问例如 /sys
, /proc/sys
, /sys/fs/selinux
这样的内核接口。 (2)禁止修改主机的网络接口以及系统时钟。(3)禁止创建设备节点。 (4)禁止重启主机操作系统。 (5)禁止加载内核模块。
可以使用例如 dnf(8), debootstrap(8), pacman(8) 这样的工具为 systemd-nspawn 容器安装一个操作系统目录树。 详见后文的"例子"小节以了解更多有关如何使用这些命令的示例。
出于安全考虑, systemd-nspawn 会在启动容器之前,首先检查容器中是否存在 /usr/lib/os-release
或 /etc/os-release
文件(参见 os-release(5))。 因此对于那些不包含 os-release
文件的容器镜像来说, 有必要手动添加 os-release
文件。
systemd-nspawn 既可以在交互式命令行上直接调用,也可以作为系统服务在后台运行。 当作为后台服务运行时,每一个容器实例就是一个以容器自己的名字命名的服务实例。 为了简化容器实例的创建,提供了一个默认的 systemd-nspawn@.service
模版单元, 它以容器的名字作为单元实例的名字。注意,当 systemd-nspawn 被模版单元调用时, 命令行选项的默认值与从交互式命令行调用时的默认值是不一样的。最重要的差别在于:模版单元会默认使用 --boot
选项, 而交互式命令行默认并不使用它。 其他的差别在后文对每一个命令行选项的详细解释中会有所说明。
可以使用 machinectl(1) 工具来管理容器。 它提供了许多方便的命令来基于 systemd-nspawn@.service
模版 以系统服务的方式运行容器。
对于每一个容器,都可以存在一个同名的 .nspawn
配置文件, 用于针对个别容器做特别的设置(详见 systemd.nspawn(5) 手册)。 配置文件中的设置可以覆盖 systemd-nspawn@.service
模版的默认设置, 从而可以避免直接对模版进行修改。
注意, systemd-nspawn 会挂载专属于容器内部私有的例如 /dev
, /run
之类的虚拟文件系统。 它们在容器之外不可见,并且随着容器的退出而自动消亡。
注意,即使在同一个目录上运行两个 systemd-nspawn 容器,这两个容器内的进程也是互相不可见的。 两个容器的 PID 名字空间是完全互相隔离的,除了共享相同的底层文件系统之外, 两个容器之间几乎不再共享其他对象(除了几个极个别的运行时对象)。 machinectl(1) 的 login 或 shell 命令可以 在一个正在运行的容器中打开一个额外的登录会话。
systemd-nspawn 遵守 Container Interface 规范。
由 systemd-nspawn 启动的容器将会被注册到 systemd-machined.service(8) 服务中(如果它正在运行的话),并且被该服务持续跟踪,此外,该服务还提供了与这些容器交互的编程接口。
选项
如果使用了 --boot
选项,那么命令行参数(ARGS)将被原封不动的传递给 init 程序。 否则,将创建一个容器并在其中启动 COMMAND
程序, 同时,命令行参数(ARGS)将被原封不动的传递给此程序。 如果既没有使用 --boot
选项, 也没有设置命令行参数(ARGS), 那么将创建一个容器并在其中启动一个 shell 。
可以使用的命令行选项(OPTIONS)如下:
-D
, --directory=
容器的 根文件系统目录。
如果 --directory=
与 --image=
都没有设置, 那么将会搜索与容器名称(也就是 --machine=
选项的值)同名的目录, 并将其用作容器的根目录。参见 machinectl(1) 手册的"文件与目录"小节,以了解具体的搜索路径。
如果 --directory=
, --image=
, --machine=
都没有设置,那么将会把当前目录用作容器的根目录。 此选项不可与 --image=
同时使用。
--template=
容器根目录的模版。必须是一个目录(或 "btrfs
" 子卷)。 如果指定了此选项并且容器的根目录(--directory=
)尚不存在, 那么将会首先创建一个空的根目录(或 "btrfs
" 快照), 然后将模版目录中的内容原封不动的复制一份填充进去。 如果模版是一个 "btrfs
" 子卷的根, 那么只需创建一个COW(copy-on-write)快照,即可瞬间完成容器根目录的填充。 如果模版不是一个 "btrfs
" 子卷的根(甚至不是 "btrfs
" 文件系统), 那么必须使用低效的复制操作(如果文件系统支持的话,也可能实际上是通过 copy-on-write 操作), 这样就有可能需要花费大量的时间才能完成容器根目录的填充。 此选项不可与 --image=
或 --ephemeral
同时使用。
注意, 模版中 "hostname", "machine ID" 之类的系统标识设置, 将会被原封不动的复制到容器根目录去。
-x
, --ephemeral
以无痕模式运行容器。使用此选项之后,容器将会运行在文件系统的临时快照之上, 当容器结束运行时,这个临时快照将会被立即删除,因此不会留下任何痕迹。 此选项不可与 --template=
同时使用。
注意,使用此选项启动的容器, 原有的 "hostname", "machine ID" 之类的系统标识设置, 将会原封不动的保持不变。
-i
, --image=
容器的镜像。 参数必须是一个普通文件或者块设备节点。 指定的镜像(普通文件或块设备)将被挂载为容器的根文件系统。 用作镜像的文件或块设备必须满足如下条件:
-
仅包含一个 MBR 分区表, 其中仅含有一个 0x83 类型的分区(Linux), 并被标记为引导分区(bootable)。
-
仅包含一个 GPT 分区表, 其中仅含有一个 0fc63daf-8483-4772-8e79-3d69d8477de4 类型的分区(Linux数据)。
-
仅包含一个 GPT 分区表, 其中仅含有一个根分区(将被挂载为容器的根文件系统)。 可选的,还可以含有一个HOME分区(将被挂载到 /home 目录)、 一个服务器数据分区(将被挂载到 /srv 目录)。 所有这些分区的类型标记(GUID)必须遵守 Discoverable Partitions Specification 规范。
-
不包含任何分区表,整个文件或块设备本身就是一个单纯的文件系统(直接挂载为容器的根文件系统)。
对于GPT镜像,如果存在ESP(EFI System Partition)分区,并且根文件系统上的 /efi
目录存在且为空, 那么ESP分区将会被自动挂载到 /efi
目录。若 /efi
目录不存在或不为空, 并且根文件系统上的 /boot
目录存在且为空,那么ESP分区将会被自动挂载到 /boot
目录。
LUKS加密分区将会被自动解密。如果使用 --root-hash=
给出了GPT镜像内受 dm-verity(数据完整性校验技术)保护的分区的"root hash", 那么将在挂载该分区之前自动校验分区数据的正确性。
其他无关的分区(例如交换分区)将不会被挂载。 此选项不可与 --directory=
或 --template=
同时使用。
--root-hash=
用于校验分区数据完整性(dm-verity)的"root hash"(十六进制字符串)。 如果容器镜像中包含基于 dm-verity 技术的分区数据完整性元数据,此选项的值将用于校验分区数据的正确性。 例如在使用 SHA256 算法的情况下,"root hash"一般是一个256位的二进制数(表现为一个64字符的十六进制字符串)。 如果没有使用此选项,但是容器的镜像文件含有 "user.verity.roothash
" 扩展属性(参见 xattr(7)), 那么将从此扩展属性中提取分区的"root hash"(十六进制字符串),并将其用于校验分区数据的正确性。 如果容器的镜像文件不含此扩展属性,但是在镜像文件的所在目录中存在文件名相同且后缀名为 .roothash
的文件, 那么将会从此文件中读取分区的"root hash"(十六进制字符串), 并将其用于校验分区数据的正确性。
-a
, --as-pid2
以 PID=2 运行指定的程序。 如果既没有使用此选项,也没有使用 --boot
选项,那么将以 PID=1 运行指定的程序(一般是 init 或 shell)。 注意,在UNIX系统上,PID=1 的进程(init)必须满足一些特殊的要求, 例如,它必须能够收集所有孤儿进程,还必须要实现与 sysvinit 兼容的信号处理器(特别是在收到 SIGINT 信号时重启、 收到 SIGTERM 信号时重新执行、收到 SIGHUP 信号时重新加载配置、等等)。使用了 --as-pid2
选项之后, 将会以 PID=1 运行一个极度简化的 init 进程,同时以 PID=2 运行指定的程序(不需要满足 init 进程的特殊要求)。 这个极度简化的 init 进程,仅能够满足UNIX系统对 init 进程的最低要求(收集孤儿进程以及处理 sysvinit 信号)。 强烈建议使用此选项在容器中运行绝大多数普通程序。 换句话说,除非运行的是能够满足UNIX系统 PID=1 进程的特殊要求的 init 或者 shell 程序, 否则应该明确使用此选项。 此选项不可与 --boot
同时使用。
-b
, --boot
自动搜索 init 程序并以 PID=1 运行它(而不是 shell 或用户指定的程序)。 使用此选项之后,命令行上的参数(ARGS)将会被原封不动的传递给 init 程序。 此选项不可与 --as-pid2
同时使用。
下面的表格解释了 不同调用模式之间的差异:
表 1. 调用模式
选项 | 解释 |
---|---|
--as-pid2 与 --boot 都没有使用 | 在容器中以 PID=1 运行 COMMAND 进程,并将 ARGS 原封不动的作为命令行参数传递给 COMMAND 进程。 |
仅使用了 --as-pid2 | 首先在容器中以 PID=1 运行一个极度简化的 init 进程,然后以 PID=2 运行 COMMAND 进程,并将 ARGS 原封不动的作为命令行参数传递给 COMMAND 进程。 |
仅使用了 --boot | 自动搜索 init 程序并在容器中以 PID=1 运行它,同时将 ARGS 原封不动的作为命令行参数传递给此 init 程序。 |
注意,在使用 systemd-nspawn@.service
模版的情况下, 将默认以 --boot
模式运行 systemd-nspawn 。
--chdir=
在容器内启动进程之前,首先将工作目录切换到此选项指定的目录。 必须设为一个以容器内的文件系统名字空间为基准的绝对路径。
--pivot-root=
在容器内将根目录切换为此选项指定的目录,同时将容器内原来的根目录卸载或挂载到其他目录。 如果将此选项设为一个单独的路径,那么表示将容器的根目录切换为此选项指定的目录,同时卸载原来的根目录。 如果将此选项设为冒号分隔的两个路径("新目录:旧目录"), 那么表示将容器的根目录切换为"新目录", 同时将容器内原来的根目录挂载到"旧目录"。 注意,所有的路径都必须是以容器内的文件系统名字空间为基准的绝对路径。
此选项仅可用于包含多个不同启动目录的容器(例如包含多个 OSTree 部署的容器)。 此选项模拟了 initrd(initial RAM disk) 的功能:选取特定的目录作为根目录, 并在完成根目录的切换之后,再启动 PID=1 的 init 进程。
-u
, --user=
进入容器之后, 将进程的用户身份切换为此选项指定的用户(必须是在容器内确实存在的用户)。 注意,此选项仅能用于预防某些粗心大意的操作可能造成的破坏, 它并非是一个特别坚固的安全特性, 可能无法抵挡某些精心设计的破坏。
-M
, --machine=
设置容器的名称。 此名称可以用于 在运行时引用该容器(例如 machinectl(1) 之类的工具)。 此外,该名称还被用作容器的初始主机名(hostname)(容器启动之后可以修改)。 如果未指定此选项,那么将使用容器根目录路径的末尾部分, 同时, 如果使用了 --ephemeral
模式的话, 还可能会再加上一个随机字符串后缀。 如果容器的根目录就是主机的根目录, 那么容器的初始主机名将使用主机的主机名。
--hostname=
设置容器的初始主机名, 默认使用 --machine=
的值。 容器名(--machine=
)用于从外部标识容器, 而主机名(--hostname=
)则用于从内部标识容器自身。 为了避免不必要的混淆,明智的做法是将两者始终保持一致。 所以应该尽量避免使用此选项,仅使用 --machine=
即可。 注意,无论容器的初始主机名是 --hostname=
还是 --machine=
, 容器内的进程都可以在运行中对主机名进行修改。
--uuid=
设置容器的UUID("machine ID")。 容器的初始化系统将会使用此处的设置填充 /etc/machine-id
文件(如果确实不存在)。 注意,仅在容器中的 /etc/machine-id
确实不存在的情况下, 此选项才有意义。
-S
, --slice=
将此容器添加到指定的 slice 单元中,而不是默认的 machine.slice
单元。 仅当此容器运行在自己的 scope 单元内时(也就是未使用 --keep-unit
选项), 此选项才有意义。
--property=
为容器所属的 scope 单元设置一个单元属性 仅当此容器运行在自己的 scope 单元内时(也就是未使用 --keep-unit
选项),此选项才有意义。 属性的赋值语法与 systemctl set-property 命令完全相同。 此选项主要用于修改容器的资源控制(例如内存限制)。
--private-users=
控制容器的用户名字空间。启用之后,容器将拥有自己私有的用户与组(UID与GID), 容器内的私有 UID/GID(从 root 的 UID=0,GID=0 开始向上递增) 将会被映射到宿主系统上一段未使用的 UID/GID 范围(通常是高于 65536 的范围)。 此选项可以接受如下几种设置:
-
设为一个或两个正整数(冒号分隔),表示开启用户名字空间。 第一个数字表示宿主系统上分配给容器的 UID/GID 起点,第二个数字表示宿主系统上分配给容器的 UID/GID 数量。 如果省略第二个数字,那么表示分配 65536 个。
-
使用了此选项但是未设置选项值,或者设为布尔值 yes ,表示开启用户名字空间。 在这种情况下,为容器分配的 UID/GID 数量是固定的 65536 个, 而 UID/GID 范围的起点则是容器根目录自身的 UID/GID 。 要使用此设置,必须确保:(1)在启动容器之前先准备好完整的容器目录树内容; (2)目录树中的所有文件与目录的 UID/GID 都没有超出允许的范围; (3)所有 ACL 中涉及的 UID/GID 都没有超出允许的范围; (4)容器根目录的 UID/GID 必须是 65536 的整数倍。
-
未使用此选项,或者设为布尔值 no ,表示彻底关闭用户名字空间。
-
设为特殊值 "
pick
" ,表示开启用户名字空间。 在这种情况下,为容器分配的 UID/GID 数量是固定的 65536 个, 而 UID/GID 范围的起点则按如下规则确定: 如果容器根目录自身的 UID/GID 既未被宿主系统使用也未被其他容器使用, 那么 UID/GID 范围的起点就是容器根目录自身的 UID/GID (这与上文的"yes"类似), 否则(也就是已被宿主或其他容器占用), 将会自动在宿主系统 UID/GID 的 524288-1878982656 范围内随机选择一个未被占用的值作为起点(必须是 65536 的整数倍)。 此设置隐含的设置了下文的--private-users-chown
选项, 从而可以确保容器中的文件和目录的 UID/GID 不会超出自动选择的范围。 此设置让用户名字空间的行为变得完全自动化。 在这种情况下,第一次启动一个先前从未使用过的容器镜像,可能会导致为该容器重新分配 UID/GID 范围, 从而导致非常耗时的调整 UID/GID 的操作。 不过以后再次启动此容器就很轻松了(除非又遇到需要重新分配 UID/GID 范围的情况)。
建议为每个容器都分配 65536 个 UID/GID ,以完整覆盖常用的16位 UID/GID 范围。 出于安全考虑,切不可为不同的容器分配重叠的 UID/GID 范围。 应该将32位 UID/GID 的高16位用作容器标识符、低16位用作容器内的用户标识符。 事实上, --private-users=pick
就隐含了这个规则。
当开启了用户名字空间之后,分配给每个容器的 GID 范围将始终保持与 UID 范围完全相同。
在绝大多数场合, --private-users=pick
都是首选的设置。 因为它增强了容器的安全性并且在绝大多数场合都能自动完成必要的操作。
注意,被选中的 UID/GID 范围并不会被记录在 /etc/passwd
或 /etc/group
文件中(事实上根本不会记录在任何地方)。 这个范围仅在启动容器的当时,根据容器根目录的 UID/GID 进行推算。
注意,当启用了用户名字空间之后, 文件系统将会遵循容器与宿主之间的 UID/GID 映射规则。 这就意味着在容器与宿主之间来回复制文件的时候, 需要根据映射规则自动修改文件的 UID/GID 。
--private-users-chown
调整容器内所有文件与目录的 UID/GID 与 ACL 以确保遵循容器与宿主之间的 UID/GID 映射规则(见上文)。 注意, 使用此选项有可能会导致巨大的性能损失。
--private-users=pick
隐含的设置了此选项。 如果关闭了用户名字空间,那么此选项将被忽略(不起任何作用)。
-U
如果内核支持用户名字空间,那么此选项等价于 --private-users=pick --private-users-chown
,否则等价于 --private-users=no
注意,在使用 systemd-nspawn@.service
模版的情况下, -U
是默认设置。
注意,如果想要在文件系统上撤销 --private-users-chown
(或 -U
) 造成的影响,可以通过将容器的 UID/GIU 起点重置为"0"来实现:
systemd-nspawn … --private-users=0 --private-users-chown
--private-network
将容器的网络从宿主的网络断开。 这样,在容器中,除了 loopback 设备、 --network-interface=
指定的设备、 --network-veth
配置的设备, 其他所有网络接口都将变为不可见。 使用此选项之后, CAP_NET_ADMIN capability 将被添加到容器现有的 capabilities 集合中。 当然,你也可以明确的使用 --drop-capability=
去掉它。 如果没有明确设置此选项(但可能被其他选项隐含的设置了), 那么该容器将能够完全访问宿主机的全部网络。
--network-namespace-path=
接受一个内核网络名字空间的文件路径, 表示将该容器运行在指定的网络名字空间中。 路径应该指向一个 (可能是绑定挂载的)网络名字空间文件(位于 /proc/$PID/ns/net
)。 一个典型的用法是指向 /run/netns/
目录下一个由 ip-netns(8) 创建的网络名字空间文件(例如 --network-namespace-path=/run/netns/foo
)。 注意,此选项不能与其他网络选项(例如 --private-network
或 --network-interface=
)一起使用。
--network-interface=
为容器分配指定的网络接口。 这将会从宿主系统删除指定的网络接口, 并将其转移到容器中。 当容器终止之后,此接口将会被交还给宿主系统。 注意, --network-interface=
隐含的设置了 --private-network
选项。 可以多次使用此选项 以分配多个网络接口。
--network-macvlan=
为指定的以太网接口创建一个 "macvlan
" 接口, 并将其添加到容器中。 所谓 "macvlan
" 接口, 是指在一个现有的物理以太网接口上添加一个新的MAC地址而创建的虚拟接口。 添加到容器内的这个 "macvlan
" 接口, 其名称将由宿主系统内对应的以太网接口名称再加上 "mv-
" 前缀组成。 注意,--network-macvlan=
隐含的设置了 --private-network
选项。 可以多次使用此选项以添加多个 "macvlan
" 接口。
--network-ipvlan=
为指定的以太网接口创建一个 "ipvlan
" 接口, 并将其添加到容器中。 所谓 "ipvlan
" 接口, 是指在一个现有的物理以太网接口上添加一个新的IP地址而创建的虚拟接口。 添加到容器内的这个 "ipvlan
" 接口, 其名称将由宿主系统内对应的以太网接口名称再加上 "iv-
" 前缀组成。 注意,--network-ipvlan=
隐含的设置了 --private-network
选项。 可以多次使用此选项以添加多个 "ipvlan
" 接口。
-n
, --network-veth
在容器与宿主之间创建一个虚拟以太网连接("veth
")。 宿主端看到的以太网连接的名称就是容器的名称(也就是 --machine=
的值)再加上 "ve-
" 前缀。 容器端看到的以太网连接的名称则是 "host0
" 。注意, --network-veth
隐含的设置了 --private-network
选项。
注意, systemd-networkd.service(8) 默认包含 /usr/lib/systemd/network/80-container-ve.network
, 此文件匹配所有通过该选项创建的虚拟以太网连接的宿主端接口, 此文件不但为这些接口启用了 DHCP 功能,而且还为这些接口设置了通向宿主机外部网络的路由(从而可以连通外网)。 该服务还默认包含 /usr/lib/systemd/network/80-container-host0.network
, 此文件匹配所有通过该选项创建的虚拟以太网连接的容器端接口,并且为这些接口启用了 DHCP 功能。 如果在宿主与容器内同时运行了 systemd-networkd
服务, 那么无须额外的配置,即可自动实现在容器与宿主之间进行 IP 通信, 并且可以连接到外部网络。
注意, --network-veth
是使用 systemd-nspawn@.service
模版时的默认选项。
--network-veth-extra=
在容器与宿主之间添加一个额外的虚拟以太网连接。 接受一对冒号分隔的网络接口名称("宿主网络接口名称:容器网络接口名称")。 如果省略了后一个名称, 那么表示使用同一个名称来命名宿主和容器的网络接口。 此选项与 --network-veth
没有任何关系。 此选项不但可以多次使用,而且还可以明确的指定网络连接的名称。 注意, --network-bridge=
对于 --network-veth-extra=
创建的网络连接没有任何作用。
--network-bridge=
把 --network-veth
创建的虚拟以太网连接("veth
")的宿主端添加到此选项指定的以太网桥上。 接受一个有效的网桥设备的网络接口名称。 注意,--network-bridge=
隐含的设置了 --network-veth
选项。 如果使用了此选项,那么太网连接的宿主端名称将使用 "vb-
" 前缀(而不是 "ve-
" 前缀)。
--network-zone=
在容器中创建一个虚拟以太网连接("veth
"),并将其添加到一个自动管理的以太网桥上。 以太网桥的接口名称就是此选项的值再加上 "vz-
" 前缀。 此网桥接口将在当第一个配置了此名称的容器启动时自动启动, 并在最后一个配置了此名称的容器退出时自动移除。 因此,使用此选项配置的网桥接口,只会在至少有一个引用了此网桥的容器处于运行状态时才会存在。 此选项与 --network-bridge=
很相似, 不同之处在于此选项所配置的网桥会被自动创建与删除(无须人为干预)。
使用此选项,可以方便的将一组相关的本地容器,添加到基于虚拟以太网的同一个广播域(也就是同一子网)之中。 这样的广播域就被称为"区域"(zone)。每一个容器都只能是某个 zone 的一部分,而每个 zone 则可以包含多个容器。 zone 的名称(也就是 --network-zone=
的值)可以自由选择, 但是必须确保加上 "vz-
" 前缀之后,可以构成一个合法的网络接口名称。 使用了相同 --network-zone=
值的多个运行中的容器, 将会自动加入到同一个 zone 当中。
注意, systemd-networkd.service(8) 默认包含 /usr/lib/systemd/network/80-container-vz.network
, 此文件匹配所有通过该选项创建的网桥接口,此文件不但为这些接口启用了 DHCP 功能, 而且还为这些接口设置了通向宿主机外部网络的路由(从而可以连通外网)。 因此,在绝大多数情况下,通过使用 --network-zone=
选项,无须额外的配置, 即可自动实现将多个本地容器加入同一个广播域(也就是同一子网)、并与宿主之间互相连通, 而且可以通过宿主机连接到外部网络。
-p
, --port=
如果为容器开启了私有网络, 那么可以使用此选项在宿主的一个IP端口与容器的一个IP端口之间建立映射。 选项值必须满足"[协议:]宿主端口[:容器端口]"格式。 其中的"协议"必须是 "tcp
" 或 "udp
" 、 "宿主端口"与"容器端口"都必须是一个 1-65535 之间的某个端口号。 如果省略"协议:"部分, 那么等价于设为"tcp:"; 如果省略":容器端口"部分, 那么等价于使用与"宿主端口"相同的端口号。 注意,仅在容器确实使用了私有网络的情况下,才可以使用此选项。 也就是说仅在确实使用了例如 --network-veth
, --network-zone=
, --network-bridge=
这些选项的时候,才可以使用此选项。
-Z
, --selinux-context=
用于容器内进程的 SELinux 安全上下文标签。
-L
, --selinux-apifs-context=
用于容器内 虚拟内核文件系统中的文件的 SELinux 安全上下文标签。
--capability=
给容器额外赋予指定的 capabilities 。 接受一个逗号分隔的 capabilities(7) 列表。容器默认拥有的 capabilities 集合包括: CAP_AUDIT_CONTROL, CAP_AUDIT_WRITE, CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_DAC_READ_SEARCH, CAP_FOWNER, CAP_FSETID, CAP_IPC_OWNER, CAP_KILL, CAP_LEASE, CAP_LINUX_IMMUTABLE, CAP_MKNOD, CAP_NET_BIND_SERVICE, CAP_NET_BROADCAST, CAP_NET_RAW, CAP_SETFCAP, CAP_SETGID, CAP_SETPCAP, CAP_SETUID, CAP_SYS_ADMIN, CAP_SYS_BOOT, CAP_SYS_CHROOT, CAP_SYS_NICE, CAP_SYS_PTRACE, CAP_SYS_RESOURCE, CAP_SYS_TTY_CONFIG 。 此外,如果使用了 --private-network
选项,那么将会自动包含 CAP_NET_ADMIN 。 特殊值 "all
" 表示赋予全部的 capabilities 。
--drop-capability=
从容器中 删除指定的 capabilities 。 这将导致容器以少于默认 capabilities 集合(见上文)的方式 运行。
--no-new-privileges=
接受一个布尔值。为容器中的进程设置 PR_SET_NO_NEW_PRIVS
标记的值。 默认值为 no 。设为 yes 表示禁止容器中的进程获取新特权, 也就是,文件的 "setuid" 位以及文件系统 capabilities 将会失效。 详见 prctl(2) 手册。
--system-call-filter=
设置容器的系统调用过滤器。接受一个空格分隔的列表, 列表中的每一项都是一个系统调用名称或系统调用组名称(组名称带有 "@
" 前缀,可以使用 systemd-analyze(1) 的 syscall-filter 命令列出所有系统调用组名称)。 如果列表前面带有 "~
" 前缀,那么表示黑名单列表(禁止容器使用列表中的系统调用), 否则表示白名单列表(允许容器使用列表中的系统调用)。 如果多次使用此选项,那么表示将多个列表组合在一起。 如果某个系统调用既在白名单中又在黑名单中,那么以黑名单为准(也就是黑名单的优先级更高)。 注意,systemd-nspawn 始终隐含着一个默认的系统调用白名单, 此选项仅仅是在这个隐含的默认白名单基础上利用白名单添加或利用黑名单删除某些系统调用。 注意,容器的系统调用过滤器还隐含的受到 --capabilities= 命令行选项的影响。
--rlimit=
为容器设置特定的 POSIX 资源限制。 可以接受两种格式: (1) "
" (2) "LIMIT
=SOFT
:HARD
" 。其中, LIMIT
=VALUE
LIMIT
是一个资源限制类型(例如 RLIMIT_NOFILE
或 RLIMIT_NICE
)、 SOFT
与 HARD
分别是软限制与硬限制的数值。 如果使用(2)格式,那么 VALUE
将同时表示软限制与硬限制的数值。 特殊值 "infinity
" 表示取消该资源类型的限制。 可以多次使用此选项,从而对多种类型的资源进行限制。 如果多次对同一种资源类型设置限制,那么以最后一个设置为准。 有关资源限制的更多详情,参见 setrlimit(2) 手册。 默认情况下,容器中 init 进程(PID=1) 的资源限制将被设置为与 Linux 内核最初传递给宿主 init 进程的值相同。 注意,针对单个用户设置的资源限制(特别是 RLIMIT_NPROC
),仍然会作用于该用户的容器。这就意味着,除非使用 --private-users=
开启了用户名字空间, 否则,针对同一用户设置的任何资源限制,都将作用于该用户的所有本地容器与宿主进程。 因此,必须特别注意这种限制,因为它们可能由不可信任的代码设置。例如: "--rlimit=RLIMIT_NOFILE=8192:16384
"
--oom-score-adjust=
设置该容器的 OOM ("Out Of Memory") 计分调整值(也就是 /proc/self/oom_score_adj
的值)。 该值会影响因为内存不足而需要杀死进程时,杀死该容器的优先级。详见 proc(5) 手册。 取值范围是 -1000…1000 之间的一个整数(数值越大越容易被优先杀死)。
--cpu-affinity=
设置该容器的CPU关联性。 接受一个逗号分隔的CPU编号与CPU范围("编号下限-编号上限")的列表。详见 sched_setaffinity(2) 手册。
--kill-signal=
当 systemd-nspawn 自身收到 SIGTERM
信号时,要给容器内的 init 进程(PID=1)发送什么信号(以触发容器的有序关闭)。 如果使用了 --boot
选项, 那么默认为 SIGRTMIN+3
信号(对于与 systemd 兼容的 init 来说,表示正常有序的关闭)。 如果没有使用 --boot
选项,那么默认为 SIGKILL
信号(粗暴的强制关闭)。 更多有关可用信号的解释,可参见 signal(7) 手册。
--link-journal=
通过软连接或绑定挂载控制容器内的日志对宿主系统的可见性。 若开启,则表示允许从宿主系统查看容器内的日志文件。 注意,宿主系统的日志在容器内是永远不可见的。 可将此选项设为 "no
", "host
", "try-host
", "guest
", "try-guest
", "auto
" 之一。 "no
" 表示不作任何连接(不可见)。 "host
" 表示将容器的日志文件直接存储在宿主系统的 /var/log/journal/
目录中, 也就是,将宿主系统上对应的日志目录绑定挂载到容器内对应的日志目录上。 "machine-id
guest
" 表示将容器的日志文件直接存储在容器自身的 /var/log/journal/
目录中, 同时,在宿主系统的相同路径上创建一个软连接,并将其指向容器内对应的日志目录。 "machine-id
try-host
"/"try-guest
" 与 "host
"/"guest
" 类似, 不同之处在于:当宿主系统没有启用持久日志时(/var/log/journal
不存在或不可写),不会导致容器启动失败。 默认值 "auto
" 表示如果宿主系统的 /var/log/journal
目录下与该容器对应的日志目录确实存在, 那么就将其绑定挂载到容器内对应的日志目录上, 否则不作任何连接(相当于设为 "no
")。 实际上,只要成功使用 "guest
" 或 "host
" 启动过一次容器, 并且后来又使用 "auto
" 再次启动该容器, 那么就会在宿主系统的日志目录中创建该容器专用的持久日志目录。
注意,在使用 systemd-nspawn@.service
模版的情况下, --link-journal=try-guest
是默认设置。
-j
等价于 --link-journal=try-guest
--resolv-conf=
如何处理容器内的 /etc/resolv.conf
文件(也就是如何处理宿主系统与容器之间的DNS同步)。 可设为 "off
", "copy-host
", "copy-static
", "bind-host
", "bind-static
", "delete
", "auto
" 之一。 "off
" 表示保持容器内的 /etc/resolv.conf
文件原封不动, 就好像此文件是容器镜像内的固有文件一样(既不修改它、也不使用绑定挂载覆盖它)。 "copy-host
" 表示把宿主系统的 /etc/resolv.conf
直接复制到容器中。 "bind-host
" 表示把宿主系统的 /etc/resolv.conf
绑定挂载到容器中。 "copy-static
"/"bind-static
" 表示把 systemd-resolved.service(8) 提供的静态 resolv.conf
文件直接复制/绑定挂载到容器中。 "delete
" 表示删除容器内的 /etc/resolv.conf
文件(若存在)。 "auto
" 表示:(1)如果开启了私有网络(--private-network
),那么设为 "off
" ; (2)否则如果 systemd-resolved.service
可用,那么设为 "copy-static
"(可写镜像) 或 "bind-static
"(只读镜像); (3)否则设为 "copy-host
"(可写镜像) 或 "copy-static
"(只读镜像)。 如果容器有可能自己修改 /etc/resolv.conf
文件,那么应该使用 "copy-*
" 以确保与宿主系统分离(从而允许在容器内修改),否则应该使用 "bind-*
" 以确保无法在容器内修改 /etc/resolv.conf
文件(因为使用了只读绑定挂载)(不过如果容器具有足够的特权, 有可能卸载掉已绑定的挂载)。注意,无论绑定挂载还是复制, 在完成一次性的早期初始化之后, 通常不会再进一步传播配置(因为该文件通常已经被复制与重命名)。 默认值为 "auto
"
--timezone=
如何处理容器内的 /etc/localtime
文件(也就是如何处理宿主系统与容器之间的本地时区同步)。 可设为 "off
", "copy
", "bind
", "symlink
", "delete
", "auto
" 之一。 "off
" 表示保持容器内的 /etc/localtime
文件原封不动, 就好像此文件是容器镜像内的固有文件一样(既不修改它、也不使用绑定挂载覆盖它)。 "copy
" 表示把宿主系统的 /etc/localtime
直接复制到容器中。 "bind
" 表示把宿主系统的 /etc/localtime
绑定挂载到容器中。 "symlink
" 表示在容器内创建 /etc/localtime
软连接, 并将其指向容器内对应于宿主时区的时区文件。 "delete
" 表示删除容器内的 /etc/localtime
文件(若存在)。 "auto
" 表示如果宿主系统的 /etc/localtime
是软连接,那么设为 "symlink
" ,否则设为 "copy
"(可写镜像) 或 "bind
"(只读镜像)。 默认值为 "auto
"
--read-only
以只读模式 挂载容器的根文件系统。
--bind=
, --bind-ro=
将宿主机上的一个文件或目录绑定挂载到容器内指定的目录上。 如果参数是一个单独的路径,那么它必须是一个目录,表示从宿主系统绑定挂载到容器内的相同路径上。 如果参数是冒号分隔的一对路径("源路径:目标路径"),那么第一个源路径表示宿主系统上的源路径(既可以是文件也可以是目录)、 第二个目标路径表示在容器内的挂载目录(必须是目录)。如果还想指定挂载选项,那么可以使用"源路径:目标路径:挂载选项"这样格式的参数。 如果在源路径前加上 "+
" 字符前缀,那么表示此源路径是容器内的源路径, 从而可以实现容器内部的绑定挂载。 如果源路径是一个空字符串,那么表示此源路径实际上是宿主系统 /var/tmp
目录下的一个临时目录,并且在容器关闭之后,此临时目录将会被自动删除。 "挂载选项"是一系列逗号分隔的挂载选项,但实际上,目前仅允许使用 rbind
与 norbind
两个选项,用于控制是否以递归的方式绑定挂载(默认是"rbind")。 如果要在路径中包含冒号,那么必须使用 "\:
" 进行转义。 可以多次使用此选项以创建多个互不相关的绑定挂载点。 使用 --bind-ro=
表示仅创建只读的绑定挂载点。
注意,当与 --private-users
一起使用时, 产生的挂载点将被 nobody
用户所拥有。 这是因为挂载点下的文件和目录的用户与组仍然位于宿主系统中,而非位于容器中, 因此在容器内就会呈现为 UID 65534 (nobody)。 推荐使用 --bind-ro=
以只读方式创建这种挂载点。
--tmpfs=
在容器内挂载一个 tmpfs 内存文件系统。 如果设为一个单独的绝对路径, 那么表示容器内的挂载点。 如果还想同时指定挂载选项, 那么可以使用"挂载路径:挂载选项"这样格式的参数。 除非在挂载选项中专门进行了设置, 否则挂载点默认被 root/root 拥有,并且默认的访问权限是 0755 。 此选项在启动无状态容器(也就是将 /var
挂载到内存),特别是与 --read-only
组合使用时,非常有用。 如果要在路径中包含冒号, 那么必须使用 "\:
" 进行转义。
--overlay=
, --overlay-ro=
将多个目录树依次组合成一个 overlay 文件系统,并将其挂载到容器中。 接受一系列冒号分隔的目录路径列表,除了最后一个路径表示容器内的挂载点之外, 前面的所有路径都表示宿主机上用于组成 overlay 文件系统的一个个目录树(从左到右依次叠加)。
如果要在路径中包含冒号, 那么必须使用 "\:
" 进行转义。
如果设置了三个或更多路径, 那么最后一个路径表示容器内的挂载点, 前面的所有路径都表示宿主机上用于组成 overlay 文件系统的一个个目录树(从左到右依次叠加)。 最左边的路径位于 overlay 文件系统最底层, 倒数第二个路径位于 overlay 文件系统最上层。 使用 --overlay-ro=
表示创建只读的 overlay 文件系统。 如果使用 --overlay=
创建了可读写的 overlay 文件系统, 那么所有对 overlay 文件系统的写入都将仅作用于最上层的目录, 也就是倒数第二个路径所在的目录。
如果仅设置了两个路径, 那么第二个路径既是宿主机上 overlay 文件系统的最上层目录, 同时又是容器内 overlay 文件系统的挂载点。 此二选项都要求必须设置至少两个路径。
如果在组成 overlay 文件系统的源路径前面加上 "+
" 字符前缀,那么表示此源路径是容器内的源路径。 如果将组成 overlay 文件系统的最上层目录的源路径设为空字符串, 那么表示此源路径实际上是宿主系统 /var/tmp
目录下的一个临时目录, 并且在容器关闭之后,此临时目录将会被自动删除。 这个特性经常用于让只读的容器目录在运行时变为可写。 例如,使用 "--overlay=+/var::/var
" 可以自动在只读的 /var
目录上叠加一个可写的临时目录。
更多关于 overlay 文件系统的介绍,可参见 overlayfs.txt 文档。 注意,overlay 文件系统与常规的文件系统在基本语义上有着很大的不同, 例如,进程在写入一个文件的时候, 可能会看到该文件的所属设备与 inode 信息在写入前后发生了变化, 或者有时候进程会看到某个文件已经过期的老旧版本。 注意,因为此选项会从最上层的目录树自动获得 "workdir=
" 目录的 overlay 文件系统挂载选项(原封不动的搬运过来), 再加上 "workdir=
" 必须与最上层的目录树位于同一个文件系统上, 所以,最上层的目录树本身其实并不是一个真正的挂载点。 还需要注意的是,"lowerdir=
" 目录的挂载选项 来自于 此选项中的路径栈(以反向顺序排列)。
-E
, NAME
=VALUE
--setenv=
NAME
=VALUE
向容器内的 init 进程传递一个环境变量(以 "NAME=VALUE
" 格式)。 此选项既可以用于覆盖一个已有环境变量的默认值,也可以用于添加一个新的环境变量。 可以多次使用此选项以添加多个环境变量。
--register=
控制是否将该容器注册到 systemd-machined.service(8) 服务中。 接受一个布尔值,默认值为 "yes
" 。 当容器中运行的是一个完整操作系统的时候(准确的说是当 PID=1 的进程是系统与服务管理器的时候),应该开启此选项。 开启此选项之后,该容器即可被 machinectl(1) 工具控制,并且被 ps(1) 工具显示出来。 如果该容器内的 PID=1 进程不是服务管理器,那么应该将此选项设为 "no
" 。
--keep-unit
不是在一个临时创建的 scope 单元中运行此容器, 而是直接在调用 systemd-nspawn 命令的 service 或 scope 单元中运行此容器。 如果同时还设置了 --register=yes
,那么该 service 或 scope 单元将被注册到 systemd-machined.service(8) 服务中。 如果从一个服务单元内部调用 systemd-nspawn 命令,那么必须使用此选项, 并且该服务单元的唯一作用必须是仅仅只运行一个单独的 systemd-nspawn 容器。 禁止在用户会话环境中使用此选项。
注意,使用了 --keep-unit
选项之后, --slice=
与 --property=
将会失效。同时使用 --keep-unit
与 --register=no
可以防止将调用 systemd-nspawn 命令的 service 或 scope 单元注册到 systemd-machined 中。
--personality=
设置 容器内 uname(2) 所报告的体系架构。目前仅支持 "x86
" 与 "x86-64
" 两个值。 此选项对于在64位宿主机上运行32位容器来说才有实际意义。 如果未设置此选项, 那么将使用与宿主机相同的值。
-q
, --quiet
静默模式。 也就是关闭状态报告。 使用此选项之后, 将仅输出容器内操作系统自身的控制台内容。
--volatile
, --volatile=
MODE
控制以何种"易失"模式启动容器。 --volatile
(等价于 --volatile=yes
) 表示以完全无状态模式(完全隐私模式)启动容器, 也就是将内存("tmpfs
")用作容器的根文件系统, 同时仅以只读模式挂载容器镜像内的 /usr
目录, 这样,将以只读模式使用容器镜像, 所有对容器内文件系统的修改都将在容器关闭后丢失。 --volatile=state
表示以常规方式挂载容器镜像内的根目录, 但是仅将 /var
挂载到内存中("tmpfs
"), 这样,容器将以用户自定义的配置启动, 但是运行过程中的状态都将在容器关闭之后丢失。 --volatile=no
是默认值, 表示以常规的读写模式挂载容器镜像内的文件系统。
此选项为容器提供了类似于宿主机 "systemd.volatile=
" 内核引导选项的功能。详见 kernel-command-line(7) 手册。
注意,要使用此选项,容器内的操作系统必须满足以下条件:(1)可以在仅挂载 /usr
的条件下启动;(2)可以自动填充 /var
目录;(3)当 "--volatile=yes
" 时, 还必须能够自动填充 /etc
目录。
--settings=
MODE
控制 systemd-nspawn 是否搜索并使用专门针对每个容器的 .nspawn
配置文件。 接受一个布尔值或者特殊值 override
或 trusted
。
默认值 yes 表示在 /etc/systemd/nspawn/
与 /run/systemd/nspawn/
目录中搜索与容器名称(来自于 --machine=
选项或者容器镜像目录/文件的名称)相同且后缀名为 .nspawn
的配置文件并应用这些配置文件。 如果没有找到对应的配置文件, 那么将会进一步在容器镜像文件的所在目录、或容器根目录的所在父目录中搜索。 如果找到了对应的配置文件,那么将会仅应用其中的非特权指令, 而所有特权指令都将被忽略。 注意,命令行上的设置比容器配置文件的优先级更高, 也就是 .nspawn
文件中的配置将会被命令行上的设置所覆盖。 这里所说的特权指令, 是指有可能造成权限提升或者要求访问主机资源(例如主机的文件或目录)的配置指令。 更多关于 .nspawn
文件的说明,参见 systemd.nspawn(5) 手册。
override
与 yes 类似, 不同之处在于,容器配置文件的优先级将会高于命令行上的设置。 也就是 .nspawn
文件中的配置将会 覆盖命令行上的设置。
trusted
与 yes 类似, 不同之处在于,将会搜索所有位于 /etc/systemd/nspawn/
目录、 /run/systemd/nspawn/
目录、 容器镜像文件的所在目录、容器根目录的所在父目录 中的 .nspawn
文件,并应用这些配置文件。 注意,这些文件中的配置依然会被命令行上的设置所覆盖。
设为 no 表示 完全忽略 .nspawn
文件。
--notify-ready=
配置容器内的 init 进程是否支持通知机制, 仅接受一个布尔值(no
与 yes
)。 设为 no
表示 systemd-nspawn 将会在启动容器内 init 进程的同时, 向 systemd 发送一条 "READY=1
" 消息。 设为 yes
表示 systemd-nspawn 将会等待容器内 init 进程主动发送 "READY=1
" 消息,然后再将此消息转发给 systemd 。 有关通知机制的详情, 参见 sd_notify(3) 手册。
-h
, --help
--version
例子
例 1. 下载 Fedora 镜像并在其中启动 shell
# machinectl pull-raw --verify=no \ https://mirrors.163.com/fedora/releases/30/Cloud/x86_64/images/Fedora-Cloud-Base-30-1.2.x86_64.raw.xz # systemd-nspawn -M Fedora-Cloud-Base-30-1.2.x86_64.raw
这将使用 machinectl(1) 下载镜像并在其中启动一个 shell
例 2. 创建一个最小化的 Fedora 系统并在容器中启动它
# dnf -y --releasever=30 --installroot=/var/lib/machines/f30 \ --disablerepo='*' --enablerepo=fedora --enablerepo=updates install \ systemd passwd dnf fedora-release vim-minimal # systemd-nspawn -bD /var/lib/machines/f30
这将在 /var/lib/machines/f30
目录中安装一个最小化的 Fedora 系统,并在名字空间容器中启动该系统。 因为此 Fedora 系统安装在标准目录 /var/lib/machines/
之中,所以还可以使用 systemd-nspawn -M f30 命令来启动。
例 3. 创建一个最小化的 Debian unstable 系统并在容器中启动一个 shell
# debootstrap unstable ~/debian-tree/ # systemd-nspawn -D ~/debian-tree/
这将在 ~/debian-tree/
目录中安装一个最小化的 Debian unstable 系统, 然后在一个容器中启动此系统中的 shell 。
debootstrap 支持 Debian, Ubuntu, Tanglu 开箱即用,所以对于这三种发行版可以使用相同的安装命令。 对于其他 Debian 系发行版,必须明确指定一个镜像,详见 debootstrap(8) 手册。
例 4. 创建一个最小化的 Arch Linux 系统并在容器中启动它
# pacstrap -c -d ~/arch-tree/ base # systemd-nspawn -bD ~/arch-tree/
这将在 ~/arch-tree/
目录中安装一个最小化的 Arch Linux 系统, 然后在一个容器中启动此系统。
例 5. 安装 OpenSUSE Tumbleweed 滚动发行版
# zypper --root=/var/lib/machines/tumbleweed ar -c \ https://download.opensuse.org/tumbleweed/repo/oss tumbleweed # zypper --root=/var/lib/machines/tumbleweed refresh # zypper --root=/var/lib/machines/tumbleweed install --no-recommends \ systemd shadow zypper openSUSE-release vim # systemd-nspawn -M tumbleweed passwd root # systemd-nspawn -M tumbleweed -b
例 6. 在容器中启动一个宿主系统的临时快照
# systemd-nspawn -D / -xb
这将在容器中运行当前宿主系统的一个临时快照,并在容器退出后立即销毁此快照。 在容器运行期间对文件系统所做的任何更改都将在容器关闭后丢失。
例 7. 运行带有 SELinux 沙盒安全上下文的容器
# chcon system_u:object_r:svirt_sandbox_file_t:s0:c0,c1 -R /srv/container # systemd-nspawn -L system_u:object_r:svirt_sandbox_file_t:s0:c0,c1 \ -Z system_u:system_r:svirt_lxc_net_t:s0:c0,c1 -D /srv/container /bin/sh
例 8. 运行含有一个 OSTree 部署的容器
# systemd-nspawn -b -i ~/image.raw \ --pivot-root=/ostree/deploy/$OS/deploy/$CHECKSUM:/sysroot \ --bind=+/sysroot/ostree/deploy/$OS/var:/var
退出状态
等于 在容器中执行的程序的退出状态
参见
systemd(1), systemd.nspawn(5), chroot(1), dnf(8), debootstrap(8), pacman(8), zypper(8), systemd.slice(5), machinectl(1), btrfs(8)
Linux Namespaces机制
...
SELinux:Linux安全子系统
...