干货分享 - Systemd 技术原理&实践(上)
优麒麟的程序员小哥在研究如何优化系统资源模块时,查阅了许许多多的资料,发现没有一篇能详细把 systemd 的优势与原理说得很清楚的中文介绍文章,于是自己动手下载了 systemd 的源码,对照资料汇总了一份有关 systemd 的详解文章,希望能对研究 systemd 的优客有所帮助。
systemd 介绍
1. systemd 的起源
关于 systemd 的起源,首先要从 Linux 的 init 程序说起。Linux 系统在启动过程中,内核完成初始化以后,由内核第一个启动的程序便是 init 程序,路径为 /sbin/init(为一个软连接,链接到真实的 init 进程),其 PID 为1,它为系统里所有进程的“祖先”,Linux 中所有的进程都由 init 进程直接或间接进行创建并运行,init 进程以守护进程的方式存在,负责组织与运行系统的相关初始化工作,让系统进入定义好的运行模式,如命令行模式或图形界面模式。
init 程序的发展,大体上可分为三个阶段:sysvinit->upstart->systemd,根据 init 进程的发展特性,可以简单理解为如下:
sysvinit:init 系统通过 shell 脚本以串行的方式启动系统服务,下一个进程必须等待上一个进程启动完成后才能开始启动,因此系统启动的过程比较慢。
upstart:在 sysvinit 的基础上,把一些没有关联的程序并行启动,以提高启动的速度,但是存在依赖关系的程序仍然为串行启动。
systemd:通过套接字激活的机制,让所有无论有无依赖关系的程序全部并行启动,并且仅按照系统启动的需要启动相应的服务,最大化提高开机启动速度。
目前优麒麟操作系统使用的就为 systemd。systemd 的意思为 system daemon,意为系统守护进程,由 Lennart Poettering 带头开发,采用更加优秀的服务框架,并且与老的 sysvinit 兼容,其设计目的就是克服 sysvinit 与 upstart 的缺点,进一步地提高启动速度。目前主流的系统中,systemd 的守护进程主要分为系统态(system)与用户态(user),可以在 ps -ef 中看到 systemd 的守护进程,如下:
PID 为1的进程/sbin/init 即是 system 态的 systemd,它为一个软链接,指向真实的 systemd 路径,在优麒麟操作系统中一般放在/lib/systemd/目录:
systemd 为进程服务集合的总称,它包含许多的进程,负责控制、管理系统的资源,其中包括 systemd-login,负责用户登录相关信息的创建、修改与删除;systemd-sleep 控制系统的休眠、睡眠状态切换等等。在优麒麟操作系统下,它们主要集中在/lib/systemd/文件目录:
每个进程的主要用途可以阅读 freedesktop systemd 手册:https://www.freedesktop.org/software/systemd/man/
目前 systemd 占据 init 程序的主导,有统一天下的趋势。
2. systemd 的主要功能
systemd 采用并行的启动方式,并提供按需启动的方式:systemd 在设计之初最关注两件事情:更少的开始,更多的并行。更少的开始,意味着在开机启动阶段,systemd 仅启动系统启动时必要的一些服务,更多其他的服务推迟启动,直到真正需要它的时候,例如优麒麟的蓝牙 bluetooth 与截图相关的服务,开机的时候系统不会用到它;优麒麟的 U 盘启动器相关的服务,可以等到接入 U 盘的时候再启动;如果系统未连接到网络,那那些需要用到网络的相关服务也可以无需启动,直到网络连通后的第一次连接再启动即可。更多的并行,意味着服务的启动不需要像 sysvinit 一样序列化启动,而是同时运行所有需要的服务,以便系统 cpu 资源利用的最大化,因此总的启动时间最小化,后面会介绍 systemd 是如何实现所有服务并行启动。
采用 cgroup 跟踪管理进程的生命周期:cgroup 为控制组,是一个层级结构,类似与文件管理系统的结构。当一个进程创建了子进程,子进程会继承父进程的 cgroup,就好比子进程创建在父进程的目录下,当子进程又创建一个子进程时,这个子进程会继承上一个子进程的 cgroup,也就相当与继承了父进程的 cgroup,好比这个子进程创建在上一个子进程的目录下,也就在父进程的目录下,通过这一机制就可以把父进程与所有的子进程关联起来并进行跟踪,当停止父进程时,可以通过查询 cgroup 找到所有关联的子进程,从而确保干净的停止所有相关服务。
启动挂载与自动挂载:在系统启动过程中,systemd 在初始化时会自身进行一些挂载,如/sys 目录与/run 目录的挂载,这些都是系统启动时至关重要的文件系统。systemd 还能实现动态挂载点的自动挂载,例如不需要经常使用的光盘、U 盘的挂载,只在这些设备接入时,systemd 启动相应的服务并对其进行临时的挂载以便访问其中的内容,当这些设备拔出时,这些挂载点将被取消以便节约资源。
事务的依赖关系管理:系统有很多的服务存在依赖关系,例如麒麟软件商店需要等待网络服务的启动,lightdm 与 systemd 交互需要等待 D-Bus 的启动,大多数服务也需要等待 syslog 的完全启动与初始化。systemd 采用 Unit(配置单元)管理这些服务的依赖关系,维护一个事务的一致性,并保证所有的相关服务不会出现相互依赖而产生死锁的情况,后面会对 Unit 进行详细介绍。
日志:systemd 自带 journalctl 命令来查看系统保存的所有日志信息,并且可以支持通过一些参数来对日志进行过滤,方便用户进行日志分析。
其他:systemd 经过几代的更新,实现的功能已经十分的多了,甚至有人觉得 systemd 管得太多了,导致一些服务都没有了存在的必要。例如 systemd 添加了许多 systemctl 的命令,可以实现系统电源的管理;systemd 还添加了看门狗机制,其他守护进程需要定期 ping systemd 进程,否则会视为失败而重启它等等。详情可以去阅读设计师的博客 http://0pointer.de/blog/projects 。
3. systemd 如何实现服务的并行
systemd 的设计理念就是希望让所有的服务并行的启动,以最大化利用硬件资源,提高启动的时间。但是我们知道服务之间存在依赖关系,客户端需要等待服务端的启动才可以建立连接,例如前面提到的,在优麒麟操作系统中,所有的服务都需要等待 syslog 服务的启动,那 systemd 是如何摆脱这同步和序列化过程的呢?
systemd 的设计师认为,对于传统的守护进程,他们真正实际等待另一个守护进程提供的是套接字的准备,需要的是一个文件系统的 socket 套接字描述符,这是它们唯一等待的,因此是否可以设法让这些套接字描述符可以更早的创建用于连接,从而不用等待整个守护进程完整的启动?答案是可以的。
在 C 语言中,一个进程启动另一个进程时,一般是执行系统调用 exec(),systemd 在调用 exec()来启动服务之前,先创建与该服务关联的监听套接字并激活,然后在 exec()启动服务期间把套接字传递给它,因此在服务还在启动的时候,套接字就已经处于可用的状态。通过这一方式,systemd 可以在第一步中为所有的服务创建套接字,然后第二步一次运行所有的服务,如果一个服务需要依赖于另一个服务,由于套接字已经准备好,服务之间可以直接进行连接并继续执行启动,如果遇到了需要同步的请求,不得不等待阻塞的情况,那阻塞的也将只会是一个服务,并且只是一个服务的一个请求,不会影响其他服务的启动,由此实现服务之间不需要再进行序列化的启动。Linux 内核提供了套接字缓冲区功能,帮助 systemd 实现了最大的并行化,还是拿 syslog 服务来说,优麒麟操作系统上大多数服务在启动初期都会先进行日志相关的初始化配置,如果同时启动 syslog 服务与各种 syslog 的客户端服务,由于 syslog 相关套接字在 systemd 执行 exec()启动 syslog 之前已经创建并准备好,客户端可以直接连接到 syslog 的套接字上,如果遇到 syslog 启动比较慢,客户端向 syslog 发送请求消息,syslog 还无法处理的情况,通过内核 socket 缓冲区的机制,请求的消息将会传到 syslog 套接字的缓冲区之中,只要缓冲区未满,客户端就不需要等待并继续往下执行;一旦 syslog 服务完全启动,它就会使所有消息出列并处理他们;当出现另一种情况,缓冲区已满,或者需要同步消息请求的情况,虽然这个时候客户端不得不阻塞等待,但是也只有一个客户端的一个请求被阻塞,并且直到服务端赶上并处理为止。
因此 systemd 先进行套接字的激活,然后开始服务的创建,使得所有的服务可以并行启动,依赖的管理也变得多余,至少可以说是次要的,因为从服务的角度看,只要套接字是激活的,另一个服务有没有启动都没有区别,这样一种方式也使得程序更加地健壮,因为不论服务可用或不可用,甚至是崩溃,套接字都处于可用的状态,不会让客户端注意到丢失连接。
4. systemd 执行单元--Unit 介绍
Unit 是 systemd 管理服务与资源的基本单元,可以简单理解为 systemd 启动后每次需要执行的服务,每次需要处理的资源,都被抽象为一个配置单元 Unit,保存在一个 Unit 文件里面。例如,当用户登录到优麒麟操作系统时,systemd 会执行 systemd-login.service 这个 Unit 文件来启动 login 服务,持续跟踪用户的会话、进程、空闲状态,为用户分配一个 slice 单元;当用户执行睡眠操作时,systemd 会执行 systemd-suspend.service 文件的 Unit,来启动 systemd-sleep 服务执行系统睡眠操作。Unit 文件可以根据其后缀名分为12种不同的类型,systemd 内部给这12种类型的 Unit 定义了不同的全局模板,因此 systemd 的执行流程为:
首先找到对应的 Unit 文件,然后根据 Unit 文件的类型匹配对应的全局模板,再然后根据这个模板解析 Unit 文件,最后执行 Unit 文件里的操作。接下来简单介绍一下12种 Unit 文件类型:
(1)service:这是最明显的单元类型,代表一个后台守护进程,可以启动、停止、重新启动、重新加载守护进程,是最常用的一类 Unit 文件。
(2)socket:这个单元在文件系统或互联网上封装了一个套接字。目前 systemd 支持流、数据报和顺序包类型的 AF_INET、AF_INET6、AF_UNIX 套接字。还支持经典的 FIFO 作为传输。每个套接字单元都有一个匹配的服务单元,相应的服务在第一个连接进入套接字时就会启动,例如:nscd.socket 在传入连接上启动 nscd.service。
(3)device:这个单元封装了 Linux 设备树中的一个设备。如果设备通过 udev 规则为此标记,它将在 systemd 中作为设备单元公开。使用 udev 设置的属性可用作配置源来设置设备单元的依赖关系。
(4)mount:这个单元封装了文件系统层次结构中的一个挂载点。systemd 监控所有挂载点,也可用于挂载或卸载挂载点。systemd 会将/etc/fstab 中的条目都转换为挂载点,并在开机时处理。
(5)automount:这个单元类型在文件系统层次结构中封装了一个自动挂载点。每个自动挂载单元都有一个匹配的挂载单元,当该自动挂载点被访问时,systemd 就会执行挂载点中定义的挂载行为。
(6)target:这种单元类型用于单元的逻辑分组:它本身并不做任何事情,它只是引用其他单元,从而可以一起控制。比如:想让系统进入图形化模式,需要运行许多服务和配置命令,这些操作都由一个个的配置单元表示,将所有这些配置单元组合为一个目标(target),就表示需要将这些配置单元全部执行一遍以便进入目标所代表的系统运行状态。
(7)snapshot:类似于 target 单元,snapshot 本身实际上不做任何事情,它们的唯一目的是引用其他单元。快照可用于保存或回滚 init 系统的所有服务和单元的状态。比如允许用户临时进入特定状态,例如“紧急外壳”,终止当前服务,并提供一种简单的方法返回之前的状态。
(8)swap:和挂载配置单元类似,交换配置单元用来管理交换分区。用户可以使用交换配置单元来定义系统中的交换分区,可以让这些交换分区在启动时被激活。
(9)timer:定时器配置单元用来定时触发用户定义的服务操作,是一种基于定时器的服务激活,这类配置单元取代了 atd、crond 等传统的定时服务。
(10)path:这类配置单元用来监控指定目录或者文件的变化,根据变化触发其他配置单元服务的运行。
(11)scope:这个单元主要表示从 systemd 外部创建的进程。
(12)slice:此单元主要用于封装管理一组进程资源占用的控制组的 slice 单元,也就是主要用于 cgroup,它通过在 cgroup 中创建一个节点实现资源的控制,一般包含 scope 与 service 单元。
下面通过蓝牙服务 bluetooth.service 介绍一下 Unit 文件的结构。
Unit 文件主要分为以下三个大的部分:
Unit 段:此部分所有 Unit 文件通用,用来定义 Unit 的元数据、配置以及与其他 Unit 的关系,Description 描述 Unit 文件信息,Documentation 表示指定服务的文档,Condition 表示服务启动的条件,有些 Unit 还包含 wants、before、after、require 字段,这些表示服务的一个依赖关系。
Install 段:此部分所有 Unit 文件通用,通常指定运行目标的 target,使得服务在系统启动时自动运行。Wantedby、RequiredBy 与 Unit 段 Wants 字段类似,表示依赖关系,Alias 字段表示启动运行时使用的别名。
service 段:服务(Service)类型的 Unit 文件特有的字段,用于定义服务的具体管理和执行动作。其中包括 Type 字段,定义进程的行为,例如使用 fork()启动,使用 dbus 启动等等;ExecStart、ExecStartPre、ExecStartPos、ExecReload、ExecStop 分别表示启动当前服务执行的命令、启动当前服务之前执行的命令、启动当前服务之后启动的命令、重启当前服务时执行的命令、停止当前服务时执行的命令。以上图为例,启动蓝牙服务所需要执行的命令为/usr/lib/bluetooth/bluetoothd。
相关字段更详细的描述可以参考 freedesktop 的 man 手册:https://www.freedesktop.org/software/systemd/man/systemd.unit.html
https://www.freedesktop.org/software/systemd/man/systemd.service.html
以优麒麟操作系统为例,Unit 文件主要的存储路径如下:
system:
/etc/systemd/system/* /run/systemd/system/* /lib/systemd/system/* |
user:
~/.config/systemd/user/* /etc/systemd/user/* $XDG_RUNTIME_DIR/systemd/user/* /run/systemd/user/* ~/.local/share/systemd/user/* /usr/lib/systemd/user/* |
考虑篇幅问题,本期先带来 systemd 的基础介绍,下期将带大家详细了解开机启动的过程中 systemd 的作用机制。