对 “服务” 的认识
我们利用计算机完成各类任务,需要借助计算机系统所提供的各种服务。这些服务核心内容由计算机系统的软件所提供,有周期性的服务、也有非周期性的服务,以完成各种任务目标。对计算机系统所提供的各种服务进行高效的管控,是操作系统的核心任务之一。为此,要对服务有清晰的认识。
- 进程是服务的载体。每一个进程的诞生,都需要一个父进程将其触发。从生命周期视角看,一个计算机系统从启动到运行,其中软件体系的运动演化过程,就是由一个最初始的进程开始,触发出一系列的子进程,继而由子进程触发更多的下级子进程,再由下级子进程触发更多的下下级子进程,…,如此一个动态的 “递归” 的演化过程。进程的衍生关系从宏观视角看,就是最初始进程作为 “根” ,各级子进程逐级向下延伸的树形结构。
-
操作系统是连接系统硬件和应用软件的纽带,为应用软件的运行提供了软件基础设施环境。系统管理上,当前主流操作系统均采用 “隔离式” 的软件架构,将最为关键、敏感的 “对内” 管理任务,如硬件的管理、内存的管理,封装起来,作为操作系统的内核,并放到一个 “封闭” 的(内存)小区域中,由操作系统自己管理,即在操作系统的内核态,主要进行系统“对内”的支撑;将更为普通、一般性的任务内容,放到 “开放” 的区域,用户参与管理,即操作系统的用户态。计算机系统对外的服务,主要由用户态的各种服务进程提供。
进程的衍生规则决定了每一个进程代码的执行,都由一个父进程将其触发。为用户态各种服务进程的顺利启动运行,同样需要有一个用户态的根进程(根服务),作为用户态的初始节点,完成整个用户态进程树的构建。各操作系统都结合各自特点有各自的根进程设计。
-
系统的服务有各种类型,有常态化的,也有非常态化的,都需要在系统的内存中运行并完成服务。对于常态化服务,需要常驻在系统内存中等待,等待着对服务的请求以便完成服务;对于非常态化服务,需要服务时,才将服务触发并调入内存,但非常态化服务的触发,也需要借助一个常态化的父进程,等待服务请求并递归触发。所以,无论哪种服务类型,本质上都是由系统的常态化服务来触发的。系统服务的核心是那些基础的常态化服务,这也是系统所有后续衍生服务的基础。操作系统启动,最重要的任务就是创建并加载那些基础性的常态化服务,使计算机系统进入“服务就绪”的状态。
常态化服务是一种典型的 “值守” 状态,英文中使用 daemon 来描述。Linux 系统中各种后台服务,也都使用 “服务名 + d ”来命名。
-
各种服务有各自的任务目标。每个任务所处的层级不同,任务目标不同。一个复杂的任务,往往由多个服务协同完成;每一个服务为了完成自身任务,往往需要借助其他的服务的帮助。因此,各个服务之间不是孤立存在的,是有各种关联和依赖关系的。对服务进行管理,也需要对这种关联依赖关系进行有效管理。
服务依托于进程,进程之间的关系特征(衍生父子化、协作依赖化)也为高效的服务管理指出了方向。
Linux 服务管理的设计
systemd 设计的缘起
作为主流操作系统,Linux 也有其特色的服务及服务管理设计机制。
历史上,Linux 一直采用 init 进程管理其各种服务。即,采用 init 进程作为用户态第一个进程,是其他所有进程的父进程、爷进程,启动并管理其他子(孙)进程。init 可以很好的完成各种服务的管理,但随着应用的深入,其劣势也逐步显现出来。
- 启动时间长。
init 进程采用串行启动的方式,只有前一个进程启动完,才会启动下一个进程。从服务任务的角度,很多进程完全可以并行启动。串行启动效率相对较低。
-
启动脚本复杂。
init 进程只是执行启动脚本,不管其他事情。脚本需要自己处理各种情况,这往往使得脚本变得很长。
为克服 init 的问题,一套新的服务启动及管理方案诞生了 — systemd。
systemd 最初由 redhat 的工程师 Lennart Poettering 等人于 2010 年开发。其有针对性的解决了 init 进行服务管理中存在的问题,同时也引入了更多更强大的功能。虽然在发展的过程中备受争议,但随着时间的推移,各个主流的 Linux distribution 基本上最终都采用的 systemd 的服务管理机制。
是否引入 systemd 进行启动进程和服务管理机制,在各个 Linux 发行版社区都发生过巨大的争论,包括 Lennart 本人在社区也备受指责,甚至谩骂。很多人认为 systemd 的设计包含了太多的内容,过于复杂,比如除了服务管理,还包含了日志功能,systemd 看起来像一个“大一统”的工具,似乎接管了“所有的”系统管理任务。而这对于操作系统的基础是一个非常重要的改变,这种复杂性违背了 Unix/Linux 的基本设计哲学,即 KISS (Keep it simple, stupid) 原则。风浪过后,systemd 还是凭借其优势,如系统快速引导启动、更轻松的服务间依赖管理等,进入了主流的殿堂。
采用 systemd 方案作为服务管理机制。systemd 是 Linux 系统用户态启动的第一个进程,其 PID 为 1,用户态的其他所有的进程,都由它递归启动,都是它的子进程。可以说,systemd 是真正的用户态“一哥”。
systemd 管理对象 — “服务” 的基本设计
systemd 是为管理系统的服务而设计,每个系统服务是其管理的基本对象。这些服务可以是系统预设的,也可以是通过软件或人为添加的。无论哪种服务,对服务的管理都涉及两个基本问题:
- 一个服务如何定义(界定)
- 服务如何分类
服务的定义
systemd 将其所管理的每一个 daemon 服务,都定义为一个 “unit”,这也是 systemd 管理的基本单元。每一个 daemon 服务(unit)由一个与之对应的配置文件来定义,这个文件称之为 “unit 文件”。unit 文件使用一种声明式语言(declarative language)进行编写,对该文件所定义的服务进行描述,以此来代替传统 init 方案中所使用的 shell 脚本。
服务的分类
每个服务单元都有不同的服务功能,systemd 将服务单元分为 12 种不同的类型。
类型 | 作用 |
---|---|
Service Unit | 系统服务 |
Socket Unit | 进程间通信的 socket |
Device Unit | 硬件设备 |
Timer Unit | 定时器 |
Mount Unit | 文件系统的挂载点 |
Automount Unit | 自动挂载点 |
Path Unit | 文件或路径 |
Scope Unit | 不是由 Systemd 启动的外部进程 |
Slice Unit | 进程组 |
Snapshot Unit | Systemd 快照,可以切回某个快照 |
Swap Unit | swap 文件 |
Target Unit | 多个 Unit 构成的一个组 |
如前所述,init 因其串行启动方式,导致服务启动效率比较低,为解决这个问题,systemd 设计了一种灵活的服务组合模式 — Target unit,来实现并发启动。Target unit 是一种特殊的 unit,虽然也是一个服务单元,使用一个特定配置文件来定义,但是,其本质上是一组 unit 的集合。这个集合(Target unit)可以包含各种类型的 unit,如 Service、Socket、Path,也可以包含其他 Target,这些 unit 集合可以针对某个共同的目标(场景)“并行”启动。
服务的内部结构设计 – Unit 文件结构
unit 文件用于定义一个具体的服务单元,其文件内容包含一个 systemd 的服务单元所需的所有要素。每个服务的 unit 文件是服务真正的 “总控台”,决定了一个服务在其生命周期内的行为方式。
unit 文件采用分段式 “声明” 的方式(declarative language)加以定义。不同的服务单元类型,其定义所需的基本要素有所区别,其配置文件通过文件名后缀加以区分。
例如,ssh 服务的 unit 文件内容如下,
一个典型的 Service unit 配置文件(.service 文件)包含三大部分:[Unit]
段、[Service]
段、[Install]
段,分别对应着服务的外部关系、内部结构以及触发条件。
(1) [ Unit ] 段 – 定义宏观的 “服务的外在”
[Unit]
段定义服务的外部表征,即本服务与其他服务的关联关系(启动顺序、依赖与冲突),服务的整体信息(整体描述、文档地址)等相关的信息。
— 服务间的关系定义
- 服务间启动顺序定义
- After — this unit after。本服务启动前所需要的服务列表。
- Before — this unit before。本服务启动后所需要的服务列表。
各个服务之间是有启动的先后顺序要求的,这种要求由 After 和 Before 选项来定义。
-
服务间依赖关系定义
- Requires — this unit requires。本服务启动前必须的刚需服务列表。强依赖
- Wants — this unit wants。本服务期望同步启动的服务列表。弱依赖
本服务运行所依赖的其他服务的集合由 Requires 和 Wants 选项来定义。Requires 选项表示 “强依赖” 关系,即如果 Requires 中的服务启动失败或异常退出,则本服务也必须退出;Wants 选项表示 “弱依赖” 关系,即如果 Wants 中的服务启动失败或停止运行,不影响本服务的继续执行。
Requires/Wants 只表明依赖关系,与启动顺序无关,默认情况下,所有依赖服务没有启动顺序的差别,都是同时启动的(systemd 服务管理下的并发特点);而 After/Before 只关注启动顺序,不涉及依赖关系。两类选项都是各自独立配置的。
两类选项也可同时使用,为特定的依赖服务指明启动顺序,尤其是
Wants=
和After=
的组合,是比较常见一种组合模式:用Wants=
表明对某种服务的依赖,用After=
表明该依赖服务在本服务之前启动。一个服务单元的依赖服务的集合可以用 Requires 和 Wants 选项的取值,即一系列由空格分隔的服务来定义。此外,也可以使用 Linux 的系统配置碎片化管理手段来定义。对于一个服务的 Require 列表,在服务自身的 unit 文件所在目录中增加名为
unit_filename.requires/
的子目录,新的子目录中,放入 Require 所要求的服务 unit 文件;同样的,对于一个服务的 Wants 列表,可以在服务的 unit 文件所在目录中增加名为unit_filename.wants/
的子目录;新的子目录中,放入 Wants 所要求的服务 unit 文件。- 在实际应用中,Requires/Wants 选项和系统碎片化配置手段往往结合使用,来定义服务的依赖关系。一个服务正常运行,其所依赖的服务非常多,Requires/Wants 选项和系统碎片化配置没有定义依赖服务的全集,他们定义了一个服务的 “特定” 依赖集合,通用依赖服务没有特别指出。
- 系统碎片化配置方式为定义依赖服务提供了一个非常灵活的方式,通过增加/删除 unit 文件的方式即可完成依赖服务的增减。这一特点在 target unit 中应用很多,尤其是通过在
/etc/systemd/system/some_target.requires/
和/etc/systemd/system/some_target.wants/
中添加服务 unit 文件软链接的方式,在系统启动模式相关的 target unit 中增加某些启动服务项。
- 服务间冲突关系定义
- Conflicts — this unit conflict with。与本服务有冲突的服务列表,冲突服务间只能启动一个
— 服务的宏观整体信息
- Description — 对服务的整体介绍说明
- Documentation — 服务的文档地址
(2) [ Service ] 段 – 定义微观的 “服务的内在”
不同类型的服务单元,有不同的微观内在定义方式(参数体系)。如,Service 类型用 [Service]
段来定义;Socket 类型用 [Socket]
段定义;Timer 类型用 [Timer]
段定义;Path 类型用 [Path]
段定义等等。不同类型服务的参数体系也不尽相同,但无论哪种类型的参数体系,都是用于定义服务微观内在功能。
Service 类型服务是最常见、最通用的服务单元类型,以 Service 类型服务为例,阐述服务内在的定义。
[Service]
段定义服务的内在行为,即定义服务自身启动、关闭等生命周期行为相关的信息。使用这个段可以对服务本身指令的执行方式(包括启动、重启等)进行精确的定义。
[Service]
段的参数设计采用了框架性设计思想,结合服务的生命周期,搭建了一套伴随服务生命周期的参数体系框架。设计上,[Service]
段在服务的生命周期流程的各个阶段都设置了 “切片”,即安排了对应的参数,如服务启动前、服务启动时、服务启动后、服务重启、服务中止等阶段,都有相关选项参数对服务在本阶段的运行方式进行定制化配置,以此达到对服务内在行为的精细化管理。
[Service]
段参数众多,其中,Exec-
前缀的参数是核心参数,表明服务生命周期中所执行的各种指令。结合服务生命周期,列举一些主要参数如下,
— 服务自身启动相关的参数
- ExecStart — 当前服务启动所执行的指令 (或脚本),最重要的参数
- Type — 当前服务启动的方式(怎样启动)
- Type = simple:默认值。执行 ExecStart 指定的命令,启动后常驻内存
- Type = forking:以 fork 方式从父进程创建子进程,创建后父进程终止运行
- Type = oneshot:顾名思义,一次性进程,指令工作完毕后结束,不会常驻内存
- Type = dbus:通过 D-Bus 启动当前服务
- Type = notify:当前服务启动完毕,会给 Systemd 发送通知,再继续往下执行
- Type = idle:其他任务执行都完毕,当前服务才会运行。执行优先级最低的服务
- EnvironmentFile — 当前服务的环境配置文件
- ExecStartPre — 服务启动之前,执行的指令
- ExecStartPost — 服务启动之后,执行的指令
systemd 在服务管理中也引入了容错设置,在服务启动阶段,所有启动设置的参数值之前,可以加上一个连词号 (-),即表明 “启动错误可容忍”:发生错误的时候,不影响其他指令的执行。
如 ssh.service 中,
EnvironmentFile= - /etc/default/sshd
(该文件为 openssl server 的默认设置),连词号 (-) 表示即使/etc/default/sshd
文件不存在,也不会抛出错误。
— 服务停止相关的参数
- ExecStop — 服务停止时所执行的指令,
systemctl stop
时执行 - KillMode — 当前服务停止的方式(怎样停止)
- KillMode = none:没有进程会被杀掉,只是执行服务的 stop 命令
- KillMode = process:只终止主进程,即 ExecStart 中的指令
- KillMode = control-group:终止主进程,同时终止其产生的 control-group 中所有的子进程
- ExecStopPost — 服务停止之后,执行的指令
— 服务自身重启相关的参数
- ExecReload — 重启当前服务所执行的命令,
systemctl reload
时执行 - Restart — 当前服务重启的条件
- Restart = no:服务退出后不会重启
- Restart = always:服务无论什么原因退出,总是重启
- Restart = on-success:服务只有正常退出时,才会重启
- Restart = on-failure:服务只有非正常退出时,才会重启
- Restart = on-abnormal:服务只有被信号终止和超时,才会重启
- Restart = on-abort:服务只有在收到没有捕捉到的信号终止时,才会重启
(3) [ Install ] 段 – 定义服务的 “触发者”
[Install]
段定义服务隶属哪个 target 分组,指明了服务的触发条件。
- WantedBy — this unit wanted by “some” target。本服务是附挂在哪一个 target 场景中
- Also — this unit also with some unit。本服务的伴随服务,与本服务一同被 install/deinstall
- Alias — 服务启动使用的别名
Unit 文件参数体系的全面信息,可以参考 unit 的 manpage 信息。
Unit 文件存储架构的设计
unit 文件的存储位置上也采用分等级结构存储。系统主要从三个位置装载 unit 文件。
存储位置 | 说明 | 读取优先级 |
---|---|---|
/ etc / systemd / system / | 本地化服务配置文件存储位置。定制化设定的服务 unit 文件一般放在这里。 | 高 |
/ run / systemd / system / | 系统运行过程中所产生的 unit 文件存储位置。 | 中 |
/ usr / lib / systemd / system / | 每个服务最主要、最核心的 unit 文件存放位置。系统默认安装服务的 unit 文件一般存放在这里。(Ubuntu 中的目录为 /lib/systemd/system/ ) |
低 |
除了单独的 unit 文件,systemd 也采用了系统配置碎片化管理手段来为服务内容的增强提供了更为灵活的手段。存储位置上,在单独 unit 文件的目录内,利用 unit_filename.d/
、unit_filename.wants/
、unit_filename.requires/
等子目录实现服务配置的碎片化增强。
当某个服务的 unit 文件存在于上述多个目录时,systemd 只装载优先级最高的 unit 文件,及相关系统碎片化配置。
特殊服务类 — 服务集封装单元 Target
系统上的服务众多,一个大的任务目标往往需要多个服务相互配合完成,不同的任务目标往往也对应着不同的服务集合,其中,某些重要的服务往往会被多个不同的任务所需要,为方便服务管理和复用,systemd 在服务管理中也引入的封装思想,以相对一个具体服务更为宏观的 “大” 任务目标为导向,将完成共同 “大” 目标的多个服务 “打包” 封装成一个整体 — 即 target 单元,在更为宏观的层面,对特定的服务集合进行管理。
target 的树形结构
target 是为实现一个共同的任务目标所依赖的服务的集合,本质是一个集合概念。因此,target 的应用方式也和集合一样,除单独使用,还可以:
- 和其他 service 组合
- 和其他 target 组合
- 和其他 service、target 混合组合
在使用上述三种组合的基础上,还可以进行递归组合,以此实现更大的任务目标。任何一个 target,都可以通过递归拆解,最终形成一颗 “依赖进程” 树。
上例可见,sysinit.target 由一系列基础服务(service/mount/automount)和 cryptsetup.target、local-fs.target、swap.target 共同构成,其中 3 个 target 可进一步拆解为基础服务的组合(service/mount/swap)。
target 的集合特性,可以方便在其中加入或删除基础服务,本质上使 target 成为特定 “宏观” 服务能力的接口:即 target 对外总的任务目标(效果)是确定的,但具体实现可通过内部基础服务集合灵活的调配。
target 的 unit 文件
target 也是通过 systemd 管理的一种服务单元,也需要 Unit 文件来定义其内部结构。
因特殊的集合特性,target 的 unit 文件在通用规范的基础上,也有自身的特点。
— 没有 [service]
段
target 一个集合,不是一个具体服务,只需配置宏观接口 – [Unit]
段即可,无需配置具体服务指令段
— Isolate 属性
Isolate 是一种非常特殊的服务单元启动方式 — “隔离式启动/排他式启动/独占式启动”,即一个服务单元启动自身及其相关依赖,同时停止其他所有服务单元。在 unit 文件的 [unit]
段中,两个参数 IgnoreOnIsolate=
和 AllowIsolate=
与 Isolate 属性有关。
作为一种非常 “霸道” 的服务单元启动方式。这种 “排他” 启动方式具有相当的危险性,启动一个新服务单元,可能导致当前正在使用的某些重要服务单元未做准备即被停止,造成损失与不便。因此,默认情况下,Isolate 方式是被关闭使用的,即默认取值 AllowIsolate=false
。而 target 服务单元聚焦于 “大” 任务目标,一些 “大” 任务目标天然具有 “排他性” 基因,如不同系统运行等级的进入,在这种情况下,就非常适合使用 Isolate 启动方式。所以,实践中 target 服务单元经常开启 Isolate 属性,即明确设置 AllowIsolate=yes
。
— target 内的服务集合来源
target 包含了完成任务目标 “所依赖” 的服务集合,其基础服务主要来源以下几个途径:
- unit 文件中
Requires
和Wants
参数设置 - unit 文件存储路径下的
*.target.requires/
子目录 - unit 文件存储路径下的
*.target.wants/
子目录
Linux 系统中的典型 target
为了方便 “大” 任务的管理与执行,Linux 系统预设了很多 target 服务单元。详细的服务单元名称及其任务目标可以参见 systemd.special 文档。
— 所有服务单元的底座 – 默认依赖 “ sysinit target ”
系统中的服务都有赖于系统初始化完成才有意义,可以说,系统的初始化是系统服务的基础和底座。为此,可以将系统服务单元宏观上分为两大类,
- 系统初始化所需的服务单元
- 依赖于系统初始化的服务单元
Linux 专门将系统初始化过程中的所需的服务单元 “打包” 为一个整体,即 sysinit.target 。而系统中的绝大多数(几乎所有)服务单元,都是依赖系统初始化完成才能执行,因此,sysinit.target 是其他(所有依赖系统初始化)服务单元的基础。
由于 sysinit.target 的独特定位,在设计上,为照顾大多数服务的需求,所有的服务单元,都默认自动添加了对 sysinit.target 的依赖。即在 unit 文件的 [Unit] 段
中,自动隐含添加了 Requires=sysinit.target
和 After=sysinit.target
两个参数。这两个参数也被视为 “默认依赖”,参数设计上,用 DefaultDependencies
对应这一情况,即默认情况下,DefaultDependencies=yes
。
对于那些系统初始化过程所需服务单元,即在 sysinit.target 内部的服务单元,这些服务单元本身无需、也不能存在 sysinit.target 的默认依赖(形成 “循环式依赖”),则必须显示声明没有默认依赖的情况,即在 unit 文件中必须明确指出 DefaultDependencies=no
参数。这类服务单元的依赖不能自动给出,必须手动写入。
— 系统运行等级 target
众多预设 target 单元中,最为重要的是与系统运行等级相对应的 target 服务单元。
传统运行等级 | Linux 系统对应 target 单元 |
---|---|
Runlevel 0 | runlevel0.target –> poweroff.target |
Runlevel 1 | runlevel1.target –> rescue.target |
Runlevel 2 | runlevel2.target –> multi-user.target |
Runlevel 3 | runlevel3.target –> multi-user.target |
Runlevel 4 | runlevel4.target –> multi-user.target |
Runlevel 5 | runlevel5.target –> graphical.target |
Runlevel 6 | runlevel6.target –> reboot.target |
可见,target 与系统运行等级也非一一对应的
与传统 init 进程的启动方式相比,使用 target 启动方式带来了文件存储结构上新变化,
设置变化点 | init 设置位置 | target 设置位置 |
---|---|---|
RunLevel 设置 | /etc/inittab |
/etc/systemd/system/default.target ,通常符号链接到 graphical.target 或 multi-user.target |
启动脚本位置 | /etc/init.d ,符号链接到不同的 RunLevel 目录 (如 /etc/rc3.d 、/etc/rc5.d 等) |
/lib/systemd/system 和 /etc/systemd/system |
配置文件位置 | init 配置 /etc/inittab ;服务配置 /etc/sysconfig |
/lib/systemd 和 /etc/systemd |
进一步深入与系统启动相关的 target 内部依赖树可参见 bootup 文档。
特殊服务类 — Timer
系统运行过程中,有一类常见的需求,是在特定的时间点完成特定的任务,这些任务可能是一次性的,也可能是周期性的,这类任务可以统称为定时任务。
Linux 系统中,以往通常使用 crond / atd 来设置定时任务;作为新一代的服务大管家 systemd,自然也提供了对定时任务的支持。
定时任务管理的总体架构设计
systemd 体系中,为了更为灵活、精确的管理每一个定时任务,采用模块化的思想,将每一个定时任务中最关键的两个要素 — “目标任务执行功能” 与 “计时功能” 进行了解耦并进行模块封装,分别用两个不同的服务单元来对应描述这两个关键要素。用最常规的 service 服务单元来封装 “目标任务”,用一个特殊的定时器服务单元 — timer 来封装计时的功能;同时,设计一种特定接口(timer 文件中的 unit=
参数),让 timer 单元模块和 service 单元模块建立映射关系,使得每一个 timer 单元对应唯一的 service 单元。系统运行时,通过定时器 timer 计时并触发与其对应的 service 单元的执行。
在宏观上,对所有的定时任务中的定时器单元 timer 进行统一的管理,即使用一个特定的 target 集合 — timers.target,来封装所有的定时器任务。
Timer 类型设计
Linux 系统中,有两种常见的时间描述方式:Wall time 和 Monotonic time。
时间描述方式 | 说明 |
---|---|
Wall time (Real time) | “墙上挂钟所指示的时间”,指的是一般意义上现实的时间。 |
Monotonic time | “单调时间”,指的是从某个点开始后(如系统启动以后)流逝的时间。 |
Wall time 描述的时间非常具体,与现实关系密切,取值涉及到具体的日历信息;而 Monotonic time 描述的时间比较抽象,不涉及日历信息,取值给出一个抽象的时间段。
两种时间描述方式各具特色,也都有广泛的应用。两种方式最大的区别在于:Monotonic time 一定是单调递增的,Wall time 则不一定;换句话说,Monotonic time 方式具有不可逆性,而 Wall time 方式则没有。系统中的 Wall time 指代现实中的具体时间,由于某些原因,Wall time 会被用户人为或系统(如与网络中某个节点时间同步校准)修改,而造成计算机时间被回拨。因此,在应用上,虽然 Wall time 容易与现实建立紧密联系,如可以规定特定的日期和时间执行特定的任务,易理解、应用方便,但由于其不可逆性的缺失,也会引入一些时间回拨而导致的问题。比如,使用 Wall time 规定软件试用期,时间到期后,可通过回调系统时间,欺骗试用期判断机制。
Timer 服务单元也对应 Linux 系统的两种时间描述方式,提供相应的 timer 类型:Realtime timer 和 Monotonic timer。
Timer 的 unit 文件
与其他服务单元一样,timer 也有自己对应的 unit 定义文件。文件结构上采用三个分段的声明,包括 [Unit]
段、[Install]
段,以及 timer 单元独有的 [Timer]
段。
— [ Unit ] 段的默认依赖
选项参数与其他服务单元没有区别,但 timer 服务单元因其独有的运行特征,除了在 [Uint]
段中用户明确指出的依赖关系外,还包含一些默认依赖。
- timer 单元需要系统初始化完成后开始执行,因此与systemd 管理的其他大多数服务单元一样,默认依赖 sysinit.target。即,
Requires=sysinit.target
和After=sysinit.target
。 - timer 单元设计上封装在 timers.target 中,默认
Before=timers.target
。 - timer 单元需要在系统运行时执行、在系统关机前安全停止,因此
Conflict=shutdown.target
,且Before=shutdown.target
。 - 如果 timer 设置中包含
OnCalendar=
参数,则 timer 单元默认应该在系统(日历)时钟校正后再开始执行,即After=time-sync.target
。
— [ Install ] 段
timer 单元设计上封装在 timers.target 中,则 [WantedBy]
选项取值为 timers.target。
— [ Timer ] 段
[Timer]
段是 timer 单元最核心的部分,timer 单元专属参数都在本段定义。
典型参数如下,
参数类别 | 参数名称 | 参数含义 |
---|---|---|
单调定时器 (On Type Sec) |
OnBootSec | 相对机器启动的时间 |
OnStartupSec | 相对服务管理器(systemd)首次启动的时间 | |
OnActiveSec | 相对 timer 单元自身启动的时间 | |
OnUnitActiveSec | 相对 timer 单元对应的 service 单元最近一次启动的时间 | |
OnUnitInactiveSec | 相对 timer 单元对应的 service 单元最近一次停止的时间 | |
日历定时器 | OnCalendar | 指定 timer 触发的日历时间 |
service 服务单元 | Unit | 指定 timer 对应的 service 单元
默认情况下,service 单元和 timer 单元 (文件名) 同名;如果要触发的 service 单元与 timer 单元不同名,则必须用该参数在 timer 中明确指出对应的 service 单元的名称. service 单元由 timer 单元触发,因此 service 单元的 unit 文件的 |
定时器精度 | AccuracySec | 指定 timer 的精度
默认为 1 min,为更高精度,可设为 1 us |
定时器启动优化 | RandomizedDelaySec | 用一个随机选取的时间段,延后 timer 的启动时间
取值为 0 到指定的时间值 避免多个 timer 同一时间点启动造成资源争抢,而导致系统性能下降 |
定时器的特殊触发条件 (均为布尔型参数) |
OnClockChange | 系统时钟(CLOCK_REALTIME)跳转到单调时钟(CLOCK_MONOTONIC)时触发 |
OnTimezoneChange | 本地系统时区变更时触发 | |
Persistent | 固执型触发 (错过情况下的补救措施)。
如果因为 timer 的 inactive,而错过了对应 service 的触发时间,则当 timer 被激活时,无论 service 的下一次既定触发时间是否到达,service 单元都会被(固执地)立刻触发。 本参数只对配置了 |
本参数对由于系统关机而错过的服务的补充执行十分有用 |
[Timer]
段的参数在应用中也十分灵活,
- 单调定时器参数和日历定时器参数可以在不同 timer 中单独使用,也可以在同一个 timer 中组合使用
- 单调定时器参数和日历定时器参数在一个 timer 中组合使用时,如果有一个参数的取值为空,则 timer 被重置(reset),即该参数之前设置的所有定时器参数值均为无效值,该参数之后的定时器参数取值为有效值
OnBootSec=
和OnStartupSec=
先天具有 “固执” 属性,即 timer 激活时,参数设置的时间点已过,则立刻触发对应的 service 单元
Timer 与 service 的关系分析
— 隐含顺序依赖
timer 来触发 service 的启动,timer 是 “触发器”,是服务单元链条的起点,因此原则上,timer 单元需在 service 单元之前启动,即 timer 单元对于 service 单元隐含存在一个 Before=
的参数关系。
— 映射关系
每个 timer 都对应唯一的一个 service,但一个 service 却可能存在多个 timer 对应。
— 实际启动顺序
一个 timer 对应的 service 可能由该 timer 启动,也可能由其他原因启动,因此,系统实际运行中,timer 单元与其对应的 service 单元的启动顺序可能是多样的:
- timer 启动时,service 未启动
- timer 按既定时间点,正常启动 service。这是最为理想的状态
- timer 未按既定时间点,非正常启动 service。这种启动是特殊触发条件,比如 timer 配置了某些参数(如
Persistent
、OnBootSec
、OnStartupSec
),service 被 “固执性” 启动
- timer 启动时,service 已启动
- timer 运行至既定时间点时,service 已经因其他原因被启动,目前处在运行中的状态。此时,service 无需重启,会继续保持运行状态。在这种情况下,timer 不会触发产生新的 service 实例
systemd 的时间描述语言
对时间的精准描述是 timer 正确运行的必要条件,systemd 体系也提供了灵活的方法对时间和日期进行描述。
针对时间点 (时间戳,timestamp)、时间段 (time span)、日历事件 (calendar event) 这三种时间形式,systemd 都提供了支持。
— time span
time span 指一段时间的持续期,用空格分隔的 “时间值+时间单位” 组合。
典型示例,
2h 30min
2 h
2hours
48hr
1y 12month
55s500ms
300ms20s 5day
— timestamp
timestamp 指一个特定的、唯一的时间点。
典型示例,
当前日期:2012-11-23
当前时间:18:15:22
当前时区:UTC+8
timestamp 描述 → 对应实际时间
Fri 2012-11-23 11:12:13 → Fri 2012-11-23 11:12:13
2012-11-23 11:12:13 → Fri 2012-11-23 11:12:13
2012-11-23 11:12:13 UTC → Fri 2012-11-23 19:12:13
2012-11-23 → Fri 2012-11-23 00:00:00
12-11-23 → Fri 2012-11-23 00:00:00
11:12:13 → Fri 2012-11-23 11:12:13
11:12 → Fri 2012-11-23 11:12:00
now → Fri 2012-11-23 18:15:22
today → Fri 2012-11-23 00:00:00
today UTC → Fri 2012-11-23 16:00:00
yesterday → Fri 2012-11-22 00:00:00
tomorrow → Fri 2012-11-24 00:00:00
tomorrow Pacific/Auckland → Thu 2012-11-23 19:00:00
+3h30min → Fri 2012-11-23 21:45:22
-5s → Fri 2012-11-23 18:15:17
11min ago → Fri 2012-11-23 18:04:22
@1395716396 → Tue 2014-03-25 03:59:56
- @ 符号表示以秒(s)为单位,相对 UNIX time epoch (1970-1-1 00:00) 的时间
— calender event
日历事件是一个带有日历信息的表达式,可用于标示一个或多个日历时间点。
其基本格式为,
DayofWeek Year-Month-Day Hour:Minute:Second
日历事件的表达式也支持通配符,典型通配符含义如下,
- *(星号):指代任意取值
- ,(逗号):分隔可能的候选取值
- ..(连续句点):连续取值区间
典型示例,
minutely → *-*-* *:*:00
hourly → *-*-* *:00:00
daily → *-*-* 00:00:00
weekly → Mon *-*-* 00:00:00
monthly → *-*-01 00:00:00
quarterly → *-01,04,07,10-01 00:00:00
yearly → *-01-01 00:00:00
annually → *-01-01 00:00:00
semiannually → *-01,07-01 00:00:00
Sat,Thu,Mon..Wed,Sat..Sun → Mon..Thu,Sat,Sun *-*-* 00:00:00
Mon,Sun 12-*-* 2,1:23 → Mon,Sun 2012-*-* 01,02:23:00
Wed *-1 → Wed *-*-01 00:00:00
Wed..Wed,Wed *-1 → Wed *-*-01 00:00:00
Wed, 17:48 → Wed *-*-* 17:48:00
Wed..Sat,Tue 12-10-15 1:2:3 → Tue..Sat 2012-10-15 01:02:03
*-*-7 0:0:0 → *-*-07 00:00:00
10-15 → *-10-15 00:00:00
monday *-12-* 17:00 → Mon *-12-* 17:00:00
Mon,Fri *-*-3,1,2 *:30:45 → Mon,Fri *-*-01,02,03 *:30:45
12,14,13,12:20,10,30 → *-*-* 12,13,14:10,20,30:00
12..14:10,20,30 → *-*-* 12..14:10,20,30:00
mon,fri *-1/2-1,3 *:30:45 → Mon,Fri *-01/2-01,03 *:30:45
03-05 08:05:40 → *-03-05 08:05:40
08:05:40 → *-*-* 08:05:40
05:40 → *-*-* 05:40:00
Sat,Sun 12-05 08:05:40 → Sat,Sun *-12-05 08:05:40
Sat,Sun 08:05:40 → Sat,Sun *-*-* 08:05:40
2003-03-05 05:40 → 2003-03-05 05:40:00
05:40:23.4200004/3.1700005 → *-*-* 05:40:23.420000/3.170001
2003-02..04-05 → 2003-02..04-05 00:00:00
2003-03-05 05:40 UTC → 2003-03-05 05:40:00 UTC
2003-03-05 → 2003-03-05 00:00:00
03-05 → *-03-05 00:00:00
daily → *-*-* 00:00:00
daily UTC → *-*-* 00:00:00 UTC
weekly Pacific/Auckland → Mon *-*-* 00:00:00 Pacific/Auckland
*:2/3 → *-*-* *:02/3:00
— 时间表达式验证工具
提供灵活的时间表述方式同时,systemd 也提供了时间表达式的验证工具 — systemd-analyze [timestamp || timespan || calendar]
,对时间表达式进行规范化、测试并验证设置表达式是否达到了预期。
优化设计 — 同类服务的 Unit 文件的复用机制
系统运行过程中,经常会遇到同时需要大量同类服务的场景。在这种情况下,大量的使用者需要相同的服务,而且出于效率的考虑,每个使用者都有独立的服务与之对应。这些服务的使用者虽然不同,但每个服务都是使用相同的程序逻辑,本身没有本质区别,只是针对不同的使用者的服务配置参数不同,服务的数量会随着服务使用者的变化而同步的增减。
例如,面向终端 tty1 – tty6 所提供的终端支持服务,需要为每一个 tty 都提供一个独立的支持服务,这些服务之间没有本质上的区别,除了基本的编号有所区别外,其他服务参数基本一致。针对这种场景下的服务需求,简单直接的解决方案是,为每个 tty 都配置单独的 unit 文件。这种方式无疑费时费力,也不利于相关服务能力的横向扩展。
针对上述需求,systemd 专门设计了一套 unit 文件的 “参数化” 机制:将 unit 文件视为一个 “函数”,文件的内容就是函数主体,文件名就是函数名,文件的服务对象是函数的输入参数。函数的主体,用带有输入参数的指令来编写。实际应用中,通过对文件名中输入参数的不同赋值,就可以实例化出多个逻辑结构一致、服务对象不同的 unit 文件,用以支持对应的服务进程。
Unit 服务复用模式基本实现如下:
1) unit “参数化” 文件命名要求
2) unit “参数化” 文件内容编写要求
- 服务执行的指令语句中引入输入参数,如
ExecStart=/usr/sbin/vsftpd /etc/vsftpd/%i.conf
在实际运行时,不直接使用 “参数化” 文件名,而使用带有输入参数取值的 unit 文件。unit 文件名中参数取值(实例名称)会带入到具体 unit 文件内指令执行(ExecStart)的参数中,进而形成不同的指令执行结果。
通过 “参数化” 的设计机制,实现了 unit 服务文件的多实例化。只用一套 unit 文件,无需对文件的内容进行任何修改,仅仅通过文件名中的参数置换,即可创建多个不同的 unit 文件,生成不同的服务。
Linux 服务管理的实现 – systemd 体系工具
基于服务管理的设计思路,systemd 提供了一些列工具协助完成服务管理的目标。其中,最重要的就是 systemctl 工具。systemctl 功能完备,对服务管理的各个方面提供了全面的支持。
服务的生命周期基本管理
systemctl 可以对服务的启动、停止、重启、重载等一些生命周期活动进行管控。其基本格式如下,
systemctl [ enable || disable || start || stop || restart || reload || daemon-reload ] service_unit_file
指令名称 | 含义 |
---|---|
start | 服务单元立刻启动 |
stop | 服务单元立刻停止 |
restart | “冷更新”。服务单元立刻重启 |
reload | “热更新”。在服务单元持续运行状态下,重新加载服务专属的配置文件(常常放在 /etc/systemd 目录中的 .conf 文件) |
daemon-reload | “热更新”。在服务单元持续运行状态下,重新加载服务单元的 unit 文件 |
enable | 服务单元随开机自动启动 |
disable | 服务单元不随开机自动启动 |
服务的状态查看
单服务 – 查看服务的运行状态 (status)
systemctl 支持服务单元当前运行状态相关信息显示功能。指令格式如下,
systemctl status service_unit_file
Active:
参数表明服务单元当前活跃状态。活跃状态分为宏观状态(高维状态)和微观状态(低维状态)两级。常见状态信息有,- active (running):服务单元正在运行中
- active (exit):服务单元仅执行一次,且正常结束,目前没有相关进程在系统中运行
- active (waiting):服务单元处在运行状态,但需要等待其他事件才能继续
- inactive:服务单元没有运行
Loaded:
参数表明服务单元的开机预设状态,常见信息有,- enable:服务单元开启自动运行
- disable:服务单元开机不自动运行
- static:服务单元不能自己启动,必须由其他服务单元来触发
- mask:服务单元被 “完全遮蔽”,无论如何无法被启动,即服务被强制注销
— 关于开机预设状态 — static
那些设计上仅由 timer 单元启动的 service 服务,开机预设状态即为 static 状态;可由 timer 单元启动,也可由其他因素启动的 service 的开机预设状态则不是 static 状态。
如图中示例,frtrim.service
仅由 frtrim.timer
启动,其 Loaded 状态为 static;而 anacron.service
不仅可以由 anacron.timer
触发,还可以在 multi-user.target
过程中被触发,因此,其 Loaded 状态不是 static。
Unit 文件结构上,一个 service 如果仅仅由 timer 启动,则其 unit 文件没有 [Install]
段,因为 service 的触发者非常明确,无需在 [Install]
段进行设置,如上例中 fstrim.service
。一个 service 不仅由 timer 触发,还由其他因素触发,则其他触发因素一定需在 [Install]
段指明,如上例中 anacron.service
。
单服务 – 查看服务的 unit 文件配置 (cat)
可以直接通过 cat
查看 unit 文本文件,也可以利用 systemctl 查看,指令格式为,
systemctl cat service_unit_name
使用 systemctl cat
优势在于无需指明服务 unit 文件的具体路径即可查看其配置文件内容。
单服务 – 查看服务的全属性信息 (show)
systemd 体系中每类服务单元都设计了多个属性对其进行描述。创建服务单元时,如果对每个属性都进行设置,任务繁重,systemd 为每种属性都安排了默认的设置。当创建一个具体的服务单元时,本质上是在系统默认的属性设置基础上,利用 unit 文件对服务单元属性集合中的一部分进行针对性设置,即针对那些最能体现该服务单元特色、最为关键的一部分属性进行设置,从而完成对具体服务单元的定制。
(计算机中典型的框架型设计:即通过预设的框架完成所有默认配置或流程,用户的定制化文件用来在默认框架设置的基础上,进行配置或流程的微调,如此来提高配置效率)
服务单元的属性非常多,主要类别有:
- 常见 [Unit] Section Options
- Conditions and Asserts
- 常见 [Install] Section Options
- 常见 [Service] Section Options
- 常见 [Socket] 特定 Options
- Paths 相关 Options
- User/Group Identity 相关 Options
- Capabilities 相关 Options
- Security 相关 Options
- Mandatory Access Control 相关 Options
- Process Properties 相关 Options
- Scheduling 相关 Options
- Sandboxing 相关 Options
- System Call Filtering 相关 Options
- Environment 相关 Options
- Logging and Standard Input/Output 相关 Options
- Credentials 相关 Options
- System V Compatibility 相关 Options
systemctl show service_unit_name
可以查看 service 的所有非空值属性信息;其中,包含服务单元 unit 文件中 “所有涉及服务单元自身的设定”,即包含了 [Unit]
段和 [Service/Timer/Path...]
段的所有设置,但不包含 [Install]
段的设置。而且,除了显示 [Unit]
段明确设置的依赖关系和启动顺序外,该指令也会将该服务单元的所有隐含的依赖关系和启动顺序全部显示出来。
指令加上 --all
参数可以无差别查看一个 service 的所有属性信息(包括空值属性和非空值属性)。
指令中如没有服务单元参数,即 systemctl show
,则显示服务管理器的属性。
单服务 – 查看服务的依赖关系 (list-depedencies)
systemd 体系提供了专门的服务单元依赖关系查询工具,并同时支持正向查询和反向查询。指令格式如下,
systemctl list-depedencies [service_unit_file] [- – reverse]
list-depedencies
会显示一个服务单元最完整的依赖树。
加上 --reverse
参数可进行反向依赖查询,即查找依赖本服务单元的上级服务单元。
如指令中没有给出服务单元参数,即 systemctl list-depedencies
,则显示 default.target
的依赖树。
多服务 – 全局查看系统中的服务单元信息
systemctl 也提供了一系列全局查看的功能,从宏观整体视角获得服务单元信息。
— 查看所有服务单元 – 当前活跃状态 (active)
指令格式如下,
systemctl [ list-units ] [ – -type=TYPE ] [ – – all ]
list-units
列出系统中已启动的所有全类型服务单元的当前活跃信息,包括宏观状态和微观状态。list-units
可以省略,即指令 systemctl
与 systemctl list-units
等价。
--type=
和 --all
是结果集合筛选开关。--type=
筛选出特定服务单元类型;--all
则为无差别列出系统中所有全类型服务单元当前活跃信息,无论服务单元是否已启动。
— 查看所有服务单元 – 开机预设状态 (loaded)
指令格式如下,
systemctl list-unit-files [ – -type=TYPE ]
list-unit-files
列出系统中所有全类型服务单元的开机预设状态信息,包括用户设置的开机预设状态和厂商的开机预设状态。
--type=
是结果集合筛选开关。--type=
筛选出特定服务单元类型。
— 查看特定类服务单元 – 专有信息
指令格式如下,
systemctl [ list-sockets || list-timers ] [- – all]
list-sockets
和 lsit-timers
列出系统中已启动的所有 socket 单元和 timer 单元专有信息。list-sockets
列出监听的端口和文件信息;list-timers
列出最近一次和下一次触发的时间信息。
--all
是结果集合筛选开关,无差别列出系统中所有该类型服务单元的专有信息,无论服务单元是否已启动。
特殊服务单元 target 的常用管理工具
为方便系统的管理,systemd 专门封装了几个与系统运行等级相关的 target(参见 “Linux 系统中的典型 target –> 系统运行等级 target”)。除了上述通用的服务单元管理工具外,systemctl 也提供了针对这些 target 专门管理功能。
— 查看/设置系统默认状态 target
systemd 为其管理服务的系统设置了默认的系统状态 target — default.target
。default.target
不是一个实际存在的 target,而是设计为一个抽象接口,供各种指令调用。具体实现上,其可根据需求灵活软链接到各个具体的 target 上,尤其是系统运行等级 target。
systemctl get-default
systemctl set-default destination_target
— 系统运行状态切换 – isolate
systemctl isolate destination_target
isolate
指令仅支持那些具有 “排他性” 的状态 target 之间的切换,即 target 的 unit 文件中设置了 AllowIsolate=yes
的选项。该指令主要用于系统运行等级 target 之间的切换。
系统状态快速切换
除了 isolate 指令,systemctl 提供了系统状态的快速切换指令。
指令 | 含义 |
---|---|
systemctl poweroff | 系统关机 |
systemctl reboot | 系统重启 |
systemctl suspend | 系统挂起(暂停),数据内存中 |
systemctl hibernate | 系统休眠,数据硬盘中 |
systemctl rescue | 救援模式 |
systemctl emergency | 紧急模式 |
服务调试与测试工具 — systemd-analyze
任何完备的工具体系,除了基本的功能工具外,都会提供调试测试工具,提高应用效率。systemd 体系也不例外,提供了专门用于系统服务管理的分析与调试工具 — systemd-analyze。
Reference
- 《鸟哥的私房菜》,4th
- systemd – freedesktop
- http://0pointer.de/blog/projects/systemd.html
- https://www.linuxvoice.com/linux-101-get-the-most-out-of-systemd/
- systemd – wikipedia
- http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-part-two.html
- manpage – systemd.unit
- manpage – systemctl
- man page – bootup
- https://serverfault.com/questions/812584/in-systemd-whats-the-difference-between-after-and-requires
- https://cloud.tencent.com/developer/article/1516125
- manpage – systemd-analyze
- Timer