平常工作经常涉及systemd,之前掌握更多的是使用方法。源码走读了很久,准备系统的整理下,供其他人参考和交流。systemd提供的不仅仅是一个简单的启动管理系统,而是一个高度集成、功能丰富的操作系统基础架构解决方案,一套协同管理Linux系统各个方面的大规模软件套件,旨在简化系统配置、提高系统启动速度和整体性能。由于本人知识有限,梳理会有所侧重,其他未能覆盖到的地方可以参考其他文章。unti以service为主,源码梳理主路线:main->service ExecStart。
linux启动时序在介绍systemd之前,有必要介绍下系统启动从上电到systemd的流程,有助于把握systemd在整个系统中的位置。
- 上电后启动ROMCode,做开机自检(POST)。
- 选择启动设备,从磁盘加载引导程序bootloader到内存。
- bootloader加载kernel镜像到内存,执行各个内核模块初始化和driver初始化,如mm_init、sched_init、init_IRQ等。
- kernel完成初始化进入用户空间,执行第一个用户空间init程序。
- 对于使用systemd的系统,内核不再寻找传统的/sbin/init,而是查找配置为init的程序,通常指向/usr/lib/systemd/systemd。
用户空间进程众多,systemd充当管理者的角色按照一定的时序高效并行启动各个进程。systemd用单元(unit)的概念来描述待管理的结构,每个单元都是系统的一个组件,具有特定的配置和行为,根据要实现的功能或作用划分为:service、mount、device、timer、socket、scope、target等[1]。
需要简单介绍下target单元类型,以便后面介绍sytemd分段启动做铺垫。它代表了系统的runlevel或者一组相关service的目标状态。target本身并不执行任何服务,而是作为一个容器,定义了一组需要同时激活的service集合。每个target unit文件(通常以.target为扩展名)包含所依赖的其他service和target单元。当系统切换到某个target时,systemd会确保所有该target依赖的service都被加载并启动成功,从而实现特定级别的系统功能。在Linux系统中常见的几个target units包括:
- sysinit.target:系统初始化阶段,完成系统底层服务及设备挂载等操作;
- basic.target:包含了基本系统服务,是许多其他target的基础。
- multi-user.target:代表多用户命令行模式,不包含图形界面。
- graphical.target:代表带有图形桌面环境的多用户模式。
default.target是一个特殊的unit,systemd在启动系统时始终使用default.target作为起点。default.target默认情况下会软连接到multi-user.target(服务器系统)或graphical.target(GUI)其中之一。可以通过下面命令查看和修改默认值:
root@localhost:~# ls -l /usr/lib/systemd/system/default.target
lrwxrwxrwx 1 root root 16 Jan 5 14:27 /usr/lib/systemd/system/default.target -> graphical.target
// 查看default.target
root@localhost:~# systemctl get-default
graphical.target
// 修改default.target
root@localhost:~# systemctl set-default multi-user.target
Created symlink /etc/systemd/system/default.target → /usr/lib/systemd/system/multi-user.target.
systemd分层启动
参考 systemd bootup
上图是参考systemd bootup,进行了缩减以便清晰说明systemd分层启动的流程。
systemd以default.target为启动开始点,default.target软连接到graphical.target(以GUI系统为例),因为graphical.target配置了after=multi-user.target和Requires=multi-user.target,会先等待multi-user.target启动完成,依次类推到sysinit.target,最终会以sysinit.target以及依赖项为最早启动项,而graphical.target为最终达到的目标状态。
// graphical.target
[Unit]
Requires=multi-user.target
Wants=display-manager.service
After=multi-user.target rescue.service rescue.target display-manager.service
// multi-user.target
[Unit]
Requires=basic.target
After=basic.target rescue.service rescue.target
//basic-user.target
[Unit]
Requires=sysinit.target
Wants=sockets.target timers.target paths.target slices.target
After=sysinit.target sockets.target paths.target slices.target tmp.mount
//sysinit.target
[Unit]
Wants=local-fs.target swap.target
After=local-fs.target swap.target emergency.service emergency.target
Requires、Wants和After的区别
从上面给出的配置中可以看到使用了三个关键字,用来定义和管理unit之间的依赖关系,根据不同的需求选择不同的关键字,为了达到更好的效果有时候也需要组合使用:
- Requires: 侧重于业务之间的强依赖。如A.service Requires B.service,systemd会保证A启动前B处于启动状态(start)或者已经启动完成状态(running),所以如果A启动时发现B未启动,systemd会拉起B,这样A和B就同时启动了。如果B启动失败或者运行中出现异常退出,那么A同样也会退出,体现了2者的强绑定。场景举例:web服务器依赖于mysql数据库才能正常工作,依赖关系必须使用Requires。
- wants: 同Requires,区别在于依赖关系弱依赖,即被依赖者(B)不存在或者异常退出,依赖者(A)不受影响,依然正常运行。场景举例:系统大多service都需要收集log用于定位,弱依赖log service,即使log service未能启动,其他service也能正常运行。
- after: 侧重于启动顺序,解决的是时间上的依赖问题,而非功能上的依赖。如A.service After B.service,A必须等待B启动完成,B启动完成表示B处于active状态或者启动完成后正常退出。
启动完成的概念有必要解释下,即服务有start状态转为active状态。对于Type=notify类型的service来说开发者可以根据功能流程通过sd_notify(0, "READY=1")通知systemd已经启动完成。
用下面2个实验说明下Requires和After的区别:
- 实验1: A After B.service
- 实验2: A Requires B.service
// A.c
#include <stdio.h>
#include <unistd.h>
int main() {
while (1) {}
return 0;
}
// /etc/systemd/system/A.service
[Unit]
After=B.service //实验1
# Requires=B.service //实验2
[Service]
ExecStart=/home/xxx/Desktop/A
[Install]
WantedBy=multi-user.target
// B.c
#include <stdio.h>
#include <unistd.h>
#include <systemd/sd-daemon.h>
#include <sys/wait.h>
int main() {
// sleep 200ms后通知systemd ready
usleep(200000);
sd_notify(0, "READY=1");
while (1) {}
return 0;
}
// /etc/systemd/system/B.service
[Unit]
Description=B
[Service]
Type=notify
ExecStart=/home/xxx/Desktop/B
[Install]
WantedBy=multi-user.target
实验2结果:A Requires B,A和B同时启动
实验1结果:A After B,A在B sleep 200ms后启动
最常见的场景是当A.service需要在启动前确保B.service已经运行,在A.service中同时配置"Requires=B"和"After=B"。
注解[1]:
systemd Unit | Description |
.automount | 用于实现启动时按需(即插即用)并行挂载文件系统单元。当访问特定的挂载点时,对应的文件系统会自动挂载。 |
.device | 定义在/dev/目录下暴露给系统管理员的硬件和虚拟设备。并非所有设备都有unit文件;通常,硬盘、网络设备等块设备会有相应的unit文件。 |
.mount | 定义Linux文件系统结构中的一个挂载点,用于管理文件系统的挂载操作。 |
.scope | .scope单元用于定义和管理一组系统进程集合。此类单元不由unit文件配置,而是通过程序方式创建。主要用于组织和管理服务工作者进程资源。 |
.service | .service unit文件定义由systemd管理的进程,包括cron定时任务服务、CUPS打印系统、iptables防火墙规则、逻辑卷管理服务(如LVM)、NetworkManager网络管理服务以及其他更多服务。 |
.slice | .slice单元定义了一个“切片”,它是系统资源的一个概念性划分,与一组进程相关联。可以将所有系统资源视为一个整体,而“切片”则代表从这个整体中划分出来的一部分资源。 |
.socket | .socket单元定义了进程间通信套接字,如网络套接字。当有连接请求到达时,systemd可以根据此单元来激活对应的服务。 |
.swap | .swap单元定义交换设备或交换文件,用于管理系统中的虚拟内存空间。 |
.target | .target单元定义了一组unit文件集合,它们表示启动同步点、运行级别和服务组。目标单元确定为了成功启动必须处于活动状态的服务和其他单元。 |
.timer | .timer单元定义了可以在指定时间触发程序执行的计时器 |