原文链接:
Understanding Systemd Units and Unit Files


导论
目前,越来越多的Linux发行版正在采用或者准备采用systemd初始化系统,而这一强大的管理套件可以对你的服务器进行多方面的管理,譬如服务管理、挂载设备管理、系统状态管理。 在systemd里面,所谓的unit是指任何系统可以操作并管理的资源,这也是systemd处理的主要对象。这些资源对象被定义在一些配置文件里面,而这些配置文件被称为unit files。在本文中,我们将为您介绍systemd所能处理的各种不同的unit单元。我们也会涵盖介绍一些常用的针对unit files的指令,用以体现这些资源是如何在系统里面进行处理。

Systemd Unit能够带给你什么东西?

Unitssystemd所能够管理的对象,他们是一些对于系统资源的标准化表示,可以被systemd套件的守护进程所管理,同时也可以被提供的命令工具所操作。

Units在一定程度上和init系统里面的服务(services)或者作业(jobs)很相似,但是Units有着更加宽广的定义,可以是抽象服务,网络资源,设备,文件系统挂载,以及隔离态的资源池。

这种Units的观点认为,其余那些由单一统一服务定义的init系统可以将这些服务拆分为多个功能单元,这种由功能(function)组织的单元,可以让您轻松地开启功能,停止功能,或扩展功能,而不用修改整个单元的行为。

Units容易执行的一些设置 如下所示:

  • 基于套接字(socket-based)的触发激活: 与服务相关的套接字,从守护进程中剥离出来,目的是为了更好的分开来进行处理。这么做有很多好处:比如我们可以对服务延迟启动,直到其相关的套接字可用为止。比如这么做可以允许系统在服务启动过程之前先创建好套接字,从而使得能够并行启动该服务的相关服务。

  • 基于总线(bus-based)的触发激活Units也可以被D-Bus提供的总线接口激活。当一个与某个单元相关的总线被发布的时候,该单元便可以开始工作。

  • 基于路径(path-based)的触发激活:一个单元可以依据某个确定的文件系统路径的活动状态以及可用性而开始工作。相当于集成了Linux的inotify功能。

  • 隐式依赖映射:大部分单元的依赖树可以有systemd自己构建。但是您仍然可以自己添加依赖,调整信息。不过大部分的任务已经不用您个人自己完成了。

  • 实例与模板:单元文件的模板可以用来给同一个单元创建多个实例,这可以使得彼此相差不多或者本身是同系的单元能够提供相同的通用×××

  • 简单的安全加固Units可以通过添加简单的指令从而执行一些相当不错的安全设置。例如您可以对特定的文件系统设置只读或者无法访问,可以限制内核功能,可以非配私有的/tmp目录以及,可以设置网络访问控制。

  • 插件与片段:通过使用片段(snippets)覆盖系统的单元文件,可以轻易地扩展Units功能。

Systemd 单元文件从哪里获取?

定义systemd如何处理“单元”的文件可以在多个不同的路径下面,而每一个路径都有不同的特性和含义。

系统拷贝的单元文件一般都保存在/lib/systemd/system目录底下,一般的软件安装都会将单元文件安装到这个目录底下。

该路径下的单元文件能够在会话里面依据需求开启或者停止,这些单元文件又被成为泛用或普通的单元文件,一般由上游的项目维护者编写,其目的是让systemd系统都能够运行相关的单元文件。建议您不要修改这个目录里面的单元文件。您应该覆盖这些文件,必要的话使用另一个可以取代该目录的目录,将编写的单元文件放置进去。

如果您要修改单元文件的原有功能,最好在/etc/systemd/system目录下面。位于此目录下面的单元文件比其他任何文件系统位置下的单元文件的优先级都要高。如果您需要修改系统拷贝的单元文件,最安全灵活的做法是将一个同名的单元文件放到这个目录下面。

如果您只是需要覆盖某些系统单元文件的部分指令(即系统单元文件的一部分内容),您可以在一个子目录下面使用片段。这么做的话便会对系统拷贝的单元文件的指令进行追加或者修改,让您只针对您需要的指令部分,而不需要修改或覆盖整个单元文件。

正确的做法是创建一个单元文件名字加上”.d”的子目录,例如某个单元文件叫做example.service,则需要创建一个名为example.service.d的目录,在这个目录里面,以.conf结尾的文件可以用于覆盖或者扩展对应的系统单元文件的属性。

还存在一个运行时单元定义的目录:/run/systemd/system。在该目录下的单元文件的优先级介于/etc/systemd/system/lib/systemd/system之间。

systemd进程在上述目录里面动态创建运行时单元文件,该目录用于针对会话级别的系统单元行为的修改。所有在该目录下面做的改变都将会在系统重启之后消失。

Units的类型

systemd按单元所描述的资源类型,将诸多单元进行分类。最简单判断资源类型的方式便是查看单元文件的后缀名。下面列出了systemd可识别的单元类型:

  • .service:一个service单元文件描述了如何管理服务器上面的一个服务或者应用。单元文件里面将会包含如何开始或者停止该服务,在何种情况下该服务会自动运行,以及和相关软件的依赖关系及顺序信息。

  • .socket:一个socket单元文件描述了一个网络或IPC套接字,或者一个消息队列缓存,systemd用其所基于套接字的触发激活。当所定义的activity对于该socket可见的情况下,他们一般会有一个与之相关的.service单元文件会被启动。

  • .device:描述针对udev或者sysfs文件系统而言必要的systemd管理设备。并非所有的设备都会有.device文件。在下面一些场景下,.device单元是必要的:调整设备、挂载设备、访问设备。

  • .mount:定义了被systemd管理的挂载点。命名方法是将”/”换为”-“,并在结尾添加.mount。比如/dev/hugepage/挂载点,名称为dev-hugepage.mount

  • automount:配置.mount定义的挂载点为自动挂载点,前提是必须要有与之对应的.mount单元文件。

  • .swap:定义交换空间。该文件的名字必须反应和交换空间相关的设备或者路径

  • .target:为启动或者改变状态提供一个同步点,也可以用作将系统带到一个新的状态。其余的单元通过设定和某个target的关系,和该target的操作绑定到一起。

  • .path:定义了可以被基于路径的触发激活所使用的路径。默认情况下,当路径到了指定的状态(切换到了指定的路径),一个同名的.service文件将会启用。这里是采用inotify去监控路径的改变。

  • ,timer:定义了可以由systemd管理的计时器,和cron计划任务类似。当计时器到时,一个匹配的单元将会启动。

  • .snapshot:被systemctl snapshot命令所创建。可以让您在系统发生改变之后重新构建系统(回滚)。该快照只针对会话有效,用于回滚临时状态。

  • .slice:与Linux的CGroup相关。其名称反应了其在cgroup树的层次。默认情况下,单元依据其类型的不同被放置在一定的slice里面。

  • .scope:Scope单元由systemd接收到总线接口的信息而自动生成。这些Scope单元用于管理由外部创建的系统进程。

如您所见,systemd所管理的单元的种类有很多。很多不同类型的单元协同工作进而添加新的功能。例如一些单元用于触发其他单元,并且提供激活功能。

基于功能及管理目的,这里我们将主要关注.service单元。

Directive1=default_value

如果片段里面提供了下面的内容,则上述的Directive1的赋值将会被抹除:

Directive1=

一般而言,systemd可以允许灵活的配置,例如多种布尔赋值形式都是可以接受的(1, yes, on, true) 或者 (0, no off)。时间格式可以被智能地解析:用一个不含单位的秒,加上系统支持的任意一种时间格式。

[Unit]区段的指令集

大多数单元文件的第一个区段为[Unit]区段。该区段用于定义该单元的元数据,并且用于配置和其他单元的依赖关系

虽然说区段的顺序前后并不影响systemd对于单元文件的解析,但是[Unit]该区段也会被放到第一的位置,主要是因为该区段提供了该单元的总览。在[Unit]区段中,存在着下述常用的指令:

  • Descriptions=:该指令用于描述单元的名称及基本功能。很多种类的systemd工具都将其作为返回值,因此写一些简短有启示性的内容,是一种良好的作风。

  • Documentation=:该指令提供文档URI列表的位置,既可以是内部的man帮助文档,也可以是网络可访问的URL。systemctl status命令可以显示出该内容。

  • Requires=:该指令列出了该单元所依赖的重要的其他单元,如果该单元被激活,所列出的那些依赖单元理应也是被激活的状态。若其依赖单元有未被激活的状态,则其本身不会被激活成功。这些单元和当前单元是并行开启的,并不是先后的关系

  • Wants=:该指令与之前的Requires相似,但是宽松一些。systemd将会试图在启动单元的时候,将Wants列表里面的依赖单元也启动,如果依赖单元未找到或者启动失败,并不会影响原单元。推荐用该指令去配置依赖关系。同样,也是并行开启。

  • BindsTo=:该指令也和Requires相似,但是当依赖单元终止运行的时候,原单元也会终止

  • Before=:该指令后面列出的单元直到原单元被启动完成之前,不会完成启动。例如D.service里面添加了Before=A.service B.service C.service,则如果D和(A,B,C)同时被激活,A,B,C这三个单元会等待D激活完成之后再进行激活过程,换句话说,D排在A,B,C的前面。注意,在这里并没有暗示依赖关系。

  • After=:该指令和上面的Before用法相反。如果D.service里面添加了After=A.service B.service C.service,则如果D和(A,B,C)同时被激活,D会等待A,B,C激活完成之后,再完成自己的激活,换句话说,A,B,C排在D的前面。注意,在这里并没有暗示依赖关系。

  • Conflicts=:列出不能够和当前单元一同激活的单元,如果启动原单元,则Conflicts中列出的单元会被停止

  • Condition…=:有很多以Condition开头的指令,目的是为了让管理员在启动单元之前测试Condition中给出的条件是否满足。该选项可以用于提供很对某些系统的条件检测机制。如果条件不满足,则该单元本身会被“友好地”跳过,而不会执行(gracefully skipped)。

  • Assert…=:与上述Condition…类似,该指令检测运行环境的各个方面,从而决定该单元是否应该被激活,与Condition…的友好跳过不同,如果某一检测失败,则会导致该单元失败(failure)

利用上述指令,以及一些实用的关于单元及其依赖的信息,一个操作系统便可以搭建起来了。

[Install]区段的指令集

在单元文件的另一侧(结尾处),最后一个区段一般都是[Install]区段。这一区段是可选的,是定义其enable或者disable行为的区段。Enabling一个单元,意味着将其标记为开机自动启动。

因为如此,只有能够开机启动的单元会有这个区段。在该区段的指令控制了当该单元设定为开机启动的时候将会发生什么

  • WantedBy=:该指令是设定开机启动的一种最普遍的方法。该指令允许您设定依赖关系,就如同Wants=在[Unit]区段里面一样。不同点在于,WantedBy指令被辅助的单元所包含,确保了主单元的相对干净。当含有该指令的单元被设定为enable的时候,一个新的目录会在/etc/systemd/system下面创建。例如WantedBy=multi-user.target,则在/etc/systemd/system下面会创建一个名为multi-user.target.wants的目录。在其中,会创建一个指向当前单元的符号链接,用以创建依赖性(即创建一个依赖池,放到了.want目录下面,形成了所有multi-user.target的依赖项),如果将该单元进行disable操作,则会从.want里面移除符号链接,从而从依赖池里面剔除掉。

  • RequiredBy=:该指令和WantedBy十分相似,不过是强依赖

  • Alias=:使得一个单元可以用别名来进行启动

  • Also=:允许多个单元作为一个集合进行enable或者disable。一般会将所列出的单元进行统一的安装或者卸载

特定单元区段指令集

针对device,target,snapshotscope单元而言,并不存在特定的单元指令,而下面所提到的单元是拥有自己特定的指令集的区段。

[Service]区段

[Service]区段用于提供服务可用的配置。

在[Service]区段中,一个重要的基本设定是TYPE=。该指令对整个服务按照进程进行归类,并将一些行为作为守护进程。由于其告知systemd如何正确的管理服务并且查到服务的状态,因此其重要性不言而喻。Type指令可以是下述内容的其中一种:

  • simple:设定ExecStart的进程作为主进程。作为Busname=Type=缺省情况下的默认值。任何与该进程的通信,需要在该单元外部建立相关的通信单元。例如如果需要用套接字进行通信,则需要创建套接字单元。

  • forking:ExecStart的主服务创建子进程,之后主服务退出(经典的Unix守护进程的做法)。之后会通知systemd子进程仍然在运行。

  • oneshot:该种类型通知systemd,ExecStart执行的命令(或进程)是短生命周期的,因此会等待该进程退出之后再执行。该行为是Type=ExecStart=都缺省的时候的默认值。多用于一次性任务(比如,修改配置文件,ip命令修改网络配置,…..),而且此种类型通常需要设定RemainAfterExit=选项

  • dbus:设置了ExecStart=BusName=时的默认值。该单元会在D-Bus上面获取由BusName指定的名称,在获取名称之后,systemd才会处理下一个单元。

  • notify:启动进程需要通过sd_notify接口向systemd发送一个消息通知。而systemd在收到该消息通知之前,会阻塞处理下一个单元的操作。

  • idle:该进程会延迟到其他作业都调度完毕之后再执行,这样可以避免控制台上面的状态信息与shell脚本的输出混杂在一起。

一些附加的指令在特定的服务类型下或许有用,例如:

  • RemainAfterExit=:结合oneshot使用。决定进程全部退出之后,是否依然将该服务看做是活动状态(active status),默认值为no

  • PIDFile=:如果服务类型被标记为”forking“,该指令用于设定其PID文件的路径,用于监控。

  • BusName=:如果服务类型被标为”bus“,该指令用于设定与此服务通信所使用的D-Bus名称。

  • NotifyAccess=:如果服务类型被标记为”notify“,设置通过sd_notify访问服务状态通知socket的模式,可以设定为none, main, all之一。none是默认值,表示不更新任何守护进程的状态;main表示只接受主进程的状态更新消息。all表示接受该服务cgroup内所有进程状态的更新消息。

如下指令定义了如何管理服务
注:所谓的”服务进程”是指ExecStartPre, ExecStartPost, ExecStop, ExecStopPost, ExecReload中设置的进程。

  • ExecStart=:定义了启动服务的绝对路径指令及传入参数。在除了oneshot服务类型的情况下,ExecStart一般只会在一个单元文件里面存在一行。命令行必须以一个绝对路径表示的可执行文件开始,并且其后的那些参数将依次作为”argv[1] argv[2] …”传递给被执行的进程。 如果在绝对路径前加上可选的 “@” 前缀,那么其后的那些参数将依次作为”argv[0] argv[1] argv[2] …”传递给被执行的进程。 如果在绝对路径前加上可选的 “-” 前缀,那么即使该进程以失败状态(例如非零的返回值或者出现异常)退出,也会被视为成功退出。 如果在绝对路径前加上可选的 “+” 前缀,那么进程将拥有完全的权限(超级用户的特权)。 可以同时使用 “-“, “@”, “+” 前缀, 且顺序任意。

  • ExecStartPre=:在执行主进程ExecStart之前执行的内容。可以有多行,上述内容提到的”-“号对其同样有用,功能与上相同。

  • ExecStartPost=:在执行主进程ExecStart之后执行的内容。可以有多行,上述内容提到的”-“号对其同样有用,功能与上相同。

  • ExecReload=:是一个可选指令,用于设置当前服务被要求重载的时候所执行的命令行,语法规则与ExecStart相同。

  • ExecStop=:设置停止服务的命令,如果没有给出,该服务的所有进程都将被强行杀掉

  • ExecStopPost=:在ExecStop之后执行

  • RestartSec=:设置在重启服务之前暂停多长时间。默认值为100毫秒

  • Restart=:设定在何种情况条件下,服务进行重启该选项的值可以取no, always, on-success, on-failure, on-abnormal, on-watchdog, on-abort之一

  • TimeoutSec=:设定最大启动时长和最大停止时长。亦可以单独地用TimeoutStartSec=TimeoutStopSec=来设定。

[Socket]区段

Socket单元十分普遍,主要由于很多的服务基于socket激活,从而提供更好的并行以及灵活性。每一个socket单元都需要有一个与之对应的服务单元。当socket接收到activity的时候,其服务单元会被激活。

对于设定一个实际的套接字单元而言,如下的指令是常用的:

  • ListenStream=:定义流套接字地址,TCP连接使用。

  • ListenDatagram:定义数据报套接字地址,UDP连接使用。

  • ListenSequentialPacket=:定义Unix套接字地址。

  • ListenFIFO:定义消息队列缓存。

一些其他套接字单元的特性可以通过这些附加的指令进行控制:

  • Accept=:决定该服务的其余实例是否由于连接的创建而新建。如果设定为false,一个服务实例将会处理所有的连接。

  • SocketUser=:定义Unix套接字的所有者,默认为root用户。

  • SocketGroup=:定义Unix套接字的所属组,默认为root组。如果只定义了SocketUser,空缺SocketGroup,则systemd会选择一个合适的组作为其所属组。

  • SocketMode=:设定FIFO实体的权限。

  • Service=:如果本应该与套接字单元所对应的服务单元的名称与套接字单元的名称不同,则可以通过该项来定义服务单元的名称。

[Mount]区段

Mount单元将挂载点的管理交予systemd。
其命名规范如下:如果想管理/sys/kernel/config目录,需要创建sys-kernel-config.mount单元文件。
常用的指令如下所示:

  • What=:被挂载资源的绝对路径

  • Where=:挂载点的绝对路径,如果不采用传统的文件系统路径的话,则需要和对应的.service单元的名字相同。

  • Type=:挂载点的文件系统类型.

  • Options=:挂载点的option参数,用逗号进行参数之间的分割。

  • SloppyOptions= :当有未识别的挂载参数时,是否决定挂载操作成功与失败的布尔值。

  • DirectoryMode=:若挂载点的父目录需要被创建,则定义父目录的权限。

  • TimeoutSec=:超时时间。

[Automount]区段

定义自动挂载。仅有两个指令:

  • Where=:同上

  • DirectoryMode=:同上

[Swap]区段

用于设置交换空间,其命名规范和[Mount]区段的命名规范相似。
主要的指令如下所示:

  • What=:交换空间的绝对路径。

  • Priority=:swap空间的优先级数字编号。

  • Options=:任意在/etc/fstab里面对于swap空间的参数都可以写在这里,用逗号分割。

  • TimeoutSec=:超时时间。

[Path]区段

定义为systemd所监控的文件系统路径。需要存在一个与之对应的单元,当特定的监控路径发生状态变化的时候(通过inotify事件),所对应的单元会被激活。

[Path]区段可以包含如下指令:

  • PathExists=:用于检查路径是否存在。如果存在,则相关的单元会被激活。

  • PathExistsGlob=:功效和上面的相同,不过支持Glob通配符及扩展表达式。

  • PathChanged=:用于观察路径的变化。当所观察的文件(一切皆文件)关闭的时候,触发相关单元的激活操作。

  • PathModified=:有着和上面一样的功能,再附加对于文件的写操作的观测。

  • DirectoryNotEmpty=:当此目录不再是空目录的时候,触发相应的单元激活。

  • Unit=:设定所需要激活的特定单元的名称。如果没有指定,则会搜寻与该Path文件同名的.service单元文件,作为其关联的单元文件。

  • MakeDirectory=:在对路径进行观察之前,是否需要建立路径目录。

  • DirectoryMode=:如果上面的功能被开启,则会设定所创建的路径目录的权限。

[Timer]区段

类似于定义计划任务,可以替代部分cronat的功能。同样需要有一个与之关联的单元,当计时器到时的时候,触发该单元。含有的指令如下所示:

  • OnActiveSec=:在.timer单元激活之后多少秒之后执行关联的单元,例如OnActiveSec=50,意味着在.timer激活之后50秒,会激活相应的关联单元。

  • OnBootSec=:在系统启动之后多少秒执行关联的单元。

  • OnStartupSec=:在systemd启动之后多少秒执行关联的单元。

  • OnUnitActiveSec=:根据最后一次相关的单元激活的时间设置计时器。

  • OnUnitInactiveSec=:根据最后一次相关的单元设定为Inactive的时间设置计时器。

  • OnCalendar=:设置相关单元的绝对激活时间,参照systemd.time语法格式。

  • AccuracySec=:设置计时器的精度。默认为一分钟之内。

  • Unit=:设置计时器到时之后所激活的相关联的单元。如果不设定的话,systemd会寻找与.timer同名的.service单元进行激活。

  • Persistent=:如果设定为true,如果存在.timer文件非激活时期本应该激活的关联单元,则当.timer激活的时候,会立刻激活这个与timer相关的单元。

  • WakeSystem=:设定唤醒系统的时间。

[Slice]区段

该区段实际上并不存在.slice特定的配置。不过该区段包含了很多关于资源管理的指令集。
一些常用的存在于其他单元文件的[slice]区段,可以从systemd.resource-control的man手册里面查询到。

从单元文件模板创建实例单元

之前我们提到了从模板创建单元文件的点子。本节将详细讲述。

单元文件的模板其实与普通的单元文件无异。不过模板提供了修改的灵活性,即通过让特定的配置段去动态地收集运行时的信息。

模板与实例单元的名称

模板单元文件在单元名称和类型后缀的中间添加了一个@符号,如下所示:

example@.service

从该模板创建的单元文件的名称和上面的文件相比,添加了唯一表示符,添加在@符号和类型后缀之间,如下所示,instance1即为标识符:

example@instance1.service

实例单元文件一般是模板的软链接,而且拥有唯一的标识符,可以回溯至唯一的模板文件。当systemd管理实例单元的时候,会首先找给定名称的单元,如果找不到的话,则会回溯到一个与之相关的模板单元。

模板占位符

模板单元文件的强大之处在于依据操作系统环境动态地替换信息。通过将特定的内容改为占位符,达到上述功能。下面列出一些常用的占位符,这些占位符在生成的实例单元里面会被动态地替换为相关的信息:

  • %n:插入单元文件的全名。

  • %N:保留单元文件中的转义字符。

  • %p:引用单元文件的前缀,即从开头到@符号之间的内容。

  • %P:引用单元文件的前缀,并保留转义字符。

  • %i:引用单元文件的标识符。该选项是最常用的。例如端口号可以作为服务的标识符,而且模板可以利用这个占位符对端口进行详细设定。

  • %I:引用单元文件的标识符,并保留转义字符。

  • %f:引用非转义的实例名称或者前缀,并在开头添加”/” 符号。

  • %c:引用单元的cgroup,移除了标准层次结构的/sys/fs/cgroup/systemd/。

  • %u:引用运行实例单元的用户。

  • %U:引用运行实例单元的UID号。

  • %H:引用运行实例单元的主机名。

  • %%:用于插入百分比。

利用上述占位符,systemd将会把该模板所创建的实例单元的对应位置解析为正确的运行时的值。

总结

使用systemd做管理的时候,了解单元与单元文件可以使得管理变得轻松。与很多其他的init系统不同,你不需要为了了解init启动文件或者服务文件而精通一门脚本语言。单元文件利用了尽可能简单声明性质的语法,使得你能够轻易了解其目的、功能及激活动作。

将激活的逻辑打散成不同的单元,不仅优化了systemd并行处理的能力,同时让配置变得简单,从而让你可以在不重构其整体连接性质的状态下,对个别的单元进行修改与重启。掌握这些能力能够使你在管理系统的时候更加灵活,给予你更多的力量。