背景
- 不太适合进入
docker
,需要直接部署在宿主机上面,拥有root
权限,操作修改系统,读取系统的一些信息等 - 保证开机启动以及异常重启
- 有一套完善的日志系统
- 支持
Before/After
的启动机制 - 支持基于
cgroup
的限制 - 以最低的系统开销实现该目的
- 方便打包为
deb
包进行安装,参考各种服务的安装,比如apt install nginx docker postgresql
等deb
包背后的逻辑与实现 - 兼容基本全部的
linux
发行版本
虽然之前使用过的supervisor
也可以满足上述大部分需求,但是打工人的世界总是需要去追求一下极致的,于是决定好好学习一下systemd
Systemd
基本介绍
- 内核加载到内存之后启动的第一个用户空间程序,
pid
是1,是所有进程的父进程,会托管所有僵尸进程,如果这个程序挂了,系统也就直接关机了 systemd
不是内核的一部分,但是却成为了Debian/Ubuntu
系和Fedora/Centos/Redhat/RockyLinux
系的系统初始化进程
Linux
启动流程
systemd
是系统启动流程中初始化阶段的主要角色,有必要介绍一下linux
系统启动流程
硬件引导启动阶段(第一阶段)
机器电源接通时,存储在主板芯片上面的固件初始化,POST(Power On Self Test)
上电自检BIOS
阶段- 初始化硬件,内存,磁盘,显卡等
- 查找启动介质(一般是硬盘),查找
MBR
或者EFI
分区的第一阶段引导程序,并且移交控制权
MBR
或者EFI
程序阶段,查找第二阶段的GRUB(GRand Unified Boot)
引导程序并安装到BootLoder
BootLoader
启动引导阶段(第二阶段)
stage1
: 执行BootLoader
的主程序(GRUB
程序),开始启动stage1.5
stage1.5
: 引导文件系统中的stage2
stage2
: 加载GRUB
核心映像grub.conf/grub.cfg
阶段- 解析
grub.conf/grub.cfg
配置文件,加载默认内核镜像和initrd
(初始磁盘镜像,对应于/boot/initrd.img
文件)镜像到内存中,当所有镜像准备好后,即跳转到内核镜像,移交系统控制权给内核,接下去进入到内核阶段 - 文件后缀名称在
Ubuntu22
上面看到的是.cfg
,可能其他系统是.conf
结尾 - 该配置文件路径是
/grub/grub.cfg
或者/boot/grub/grub.cfg
,路径具体位置取决于系统安装时候是否分割了一个/boot
分区,之前装系统的时候/boot
分区都是必须分配的
- 解析
内核引导阶段(第三阶段)
/boot/kernel and Kernel parameter
- 内核读取
/boot
下面的文件,执行加载 查看
/boot
分区的内容如下,可以看到内核保存了两个版本的镜像(img-5.15.0-53-generic
和img-5.15.0-56-generic
),其中新镜像是目前系统正常运行时候加载的镜像,每次apt
更新系统的时候如果有新版本内核释放,更新时候会保留一份老版本镜像,等下一次新镜像更新到时候才会删除,一份冗余$ ll /boot 总用量 165M -rw-r--r-- 1 root root 256K 十月 18 02:36 config-5.15.0-53-generic -rw-r--r-- 1 root root 256K 十一月 22 23:08 config-5.15.0-56-generic drwx------ 3 root root 4.0K 一月 1 1970 efi drwxr-xr-x 5 root root 4.0K 十二月 3 09:44 grub lrwxrwxrwx 1 root root 28 十二月 2 09:45 initrd.img -> initrd.img-5.15.0-56-generic -rw-r--r-- 1 root root 65M 十二月 1 09:01 initrd.img-5.15.0-53-generic -rw-r--r-- 1 root root 65M 十二月 4 09:04 initrd.img-5.15.0-56-generic lrwxrwxrwx 1 root root 28 十二月 2 09:45 initrd.img.old -> initrd.img-5.15.0-53-generic drwx------ 2 root root 16K 五月 26 2021 lost+found -rw-r--r-- 1 root root 179K 二月 7 2022 memtest86+.bin -rw-r--r-- 1 root root 181K 二月 7 2022 memtest86+.elf -rw-r--r-- 1 root root 181K 二月 7 2022 memtest86+_multiboot.bin -rw------- 1 root root 6.0M 十月 18 02:36 System.map-5.15.0-53-generic -rw------- 1 root root 6.0M 十一月 22 23:08 System.map-5.15.0-56-generic lrwxrwxrwx 1 root root 25 十二月 2 09:45 vmlinuz -> vmlinuz-5.15.0-56-generic -rw------- 1 root root 12M 十月 18 02:41 vmlinuz-5.15.0-53-generic -rw------- 1 root root 12M 十一月 23 01:07 vmlinuz-5.15.0-56-generic lrwxrwxrwx 1 root root 25 十二月 2 09:45 vmlinuz.old -> vmlinuz-5.15.0-53-generic
- 内核读取
/boot/initrd
- 引导
initrd
解压载入 - 在内存中加载内核使用的
root
文件系统,执行initrd
文件系统中的init
程序,完成加载其他驱动模块 执行
/sbin/init
进程发现
/sbin
是指向/usr/sbin
的一个软链接$ ll / |grep bin lrwxrwxrwx 1 root root 7 五月 26 2021 bin -> usr/bin lrwxrwxrwx 1 root root 8 五月 26 2021 sbin -> usr/sbin
查看
init
发现init
是指向systemd
的一个软链接$ ll /usr/sbin |grep init lrwxrwxrwx 1 root root 20 九月 10 02:47 init -> /lib/systemd/systemd -rwxr-xr-x 1 root root 13K 三月 15 2022 mkinitramfs lrwxrwxrwx 1 root root 14 九月 10 02:47 telinit -> /bin/systemctl -rwxr-xr-x 1 root root 6.8K 二月 9 2022 update-initramfs
- 引导
Systemd
初始化阶段(第四阶段)
- 内核启动第一个用户空间应用程序,系统控制权移交给
systemd
- 该阶段会加载执行级别,加载服务,启动
shell
和图形化界面 - 初始化阶段有3个类型,
SysV
初始化,UpStart
(Ubuntu
开发的用于替换SysV
的初始化程序)和Systemd
(由Redhat
工程师开发),很多老系统一般都是SysV
作为初始化程序,从centos7
(大概是2014年)以后的系统大多都是采用Systemd
,Ubuntu15
开始使用systemd
为什么各大主流linux
系统采用systemd
- 改进启动项的效率,避免频繁的进程创建,内核/用户切换,为系统的启动和管理提供一套完整的解决方案
- 利用
Dbus
进程间通讯与socket
激活机制,解决任务启动时的依赖问题,实现启动并行化 - 实现任务
daemons
的精确控制,使用内核的cgroup
机制,不依赖pid
来追踪进程,进程多次fork
之后生成的进程也不会脱离systemd
的控制 - 统一任务定义,用户不需要编写
shell
脚本,而是使用systemd
制定的unit
规则 systemd
有两大设计理念,启动更少的服务,更多地并行启动服务,最终实现用户可以快速进入系统systemd
使用C
编写,用于替换传统的脚本shell
启动,shell
中调用系统命令的操作会导致新进程的生成,比如awk, sed, grep
等都会生成新进程,产生了很大的开销,systemd
的运行开销比SysV
和UpStart
小很多,速度也更快
systemd
做了什么
解决启动项的依赖性
Socket
依赖:例如服务A
依赖服务B
的socket
,但是服务B
还未启动,所以systemd
创建了一个临时socket
用于接收服务A
的请求与数据,当B
真正起来的时候再把临时socket
缓存的数据以及描述符替换为B
的socket
D-Bus(Desktop Bus)
依赖:是一个用来在进程间通信的服务,该服务支持Bus Activation
特性,即服务A
要通过D-Bus
服务和B
通讯,但是B
没有启动,那么D-Bus
可以把B
起来,在B
启动的过程中,D-Bus
缓存数据,systemd
使用这个特性并行启动A
和B
- 文件系统依赖: 系统启动过程中,
systemd
参考了autofs
的设计思路,使得依赖文件系统的服务和文件系统本身初始化两者可以并发工作,监测到某个文件系统挂载点真正被访问到的时候才触发挂载操作
提供了一个系统和服务管理器
- 利用
Linux
的cgroups
监视进程 - 支持快照和系统恢复
- 维护挂载点和自动挂载点
- 各服务间基于依赖关系进行精密控制
- 基于
journald
的服务日志管理 - 控制基础系统配置
- 维护登陆用户列表以及系统账户
- 运行时目录和设置
- 运行容器和虚拟机
- 简单的管理网络配置
- 网络时间同步
- 日志转发
- ...
systemd
的管控范围已经远超作为系统启动项的角色,可以理解为已经大一统管理linux
各项功能了,职责和最初的sysv
和upstart
有所脱离,变得极其复杂庞大,这个也是最初redhat/centos
系列和debian/ubuntu
系列采用systemd
引起的巨大波澜
在此推荐阅读一下itwire
于2014发表的 Linus Torvalds
关于systemd
的访谈文章(Linus
保持中立意见,如果这个人没有开喷的话,说明是已经取得巨大的成功了)
https://www.itwire.com/business-it-news/open-source/65402-torvalds-says-he-has-no-strong-opinions-on-systemd
也可以阅读一下systemd
的作者Lennert
发表的关于PID 1
的再思考Rethinking PID 1
http://0pointer.de/blog/projects/systemd.html
systemd
的Unit
单元的文件类型
系统初始化需要很多操作,比如启动后台服务,挂载文件系统,配置网络等,过程中的每一步都被 Systemd
抽象为一个配置单元,即 Unit
,具体包含的类型如下
type | name | 作用 |
---|---|---|
Service unit | .service | 封装一个后台服务进程,比如sshd ,dockerd |
Target unit | .target | 将多个单元在逻辑上组合在一起。 |
Device unit | .device | 定义内核识别的设备,在 sysfs(5) 里面作为 udev(7) 设备树展示 |
Socket unit | .socket | 用于标识进程间通信用到的 socket 文件 |
Snapshot unit | .snapshot | 管理系统快照 |
Swap unit | .swap | 用于标识 swap 文件或设备 |
Mount unit | .mount | 封装一个文件系统挂载点(也向后兼容传统的 /etc/fstab 文件) |
Automount unit | .automount | 封装一个文件系统自动挂载点 |
Path unit | .path | 根据文件系统上特定对象的变化来启动其他服务。 |
Time unit | .timer | 封装一个基于时间触发的动作。取代传统的 crond 等任务计划服务 |
Slice unit | *.slice | 控制特定 CGroup 内所有进程的总体资源占用。 |
Systemd Unit
文件的位置与执行优先级
不同发行版本的路径是不同的,当三个目录下面存在同名文件的时候,优先级最高目录下的文件会被优先使用
/usr/lib/systemd/system
或/lib/systemd
: 使用包管理器安装的软件的systemd unit
的实际配置文件的存放位置,在Ubuntu22
上面/lib
路径是指向/usr/lib
的一个软链接,优先级最低/run/systemd/system
:在运行时创建的systemd unit
文件,该目录优先于已安装服务单元文件的目录/etc/systemd/system
: 优先级最高,由systemctl
命令创建的systemd unit
文件以及为扩展服务而添加的unit
文件都将启用,系统管理员安装的单元
Systemd Unit
文件的组成
主要由三块Unit
,Service
,Install
组成
具体版本或者全部字段解释执行如下命令查看
$ man systemd.unit
$ man systemd.service
$ man systemd.exec
....
或者访问地址https://www.freedesktop.org/software/systemd/man/
或者第三方中文版http://www.jinbuguo.com/systemd/systemd.index.html
常见字段解释如下
Unit
块下面的字段
Requires
: 强依赖,依赖的其它Unit
列表,列在其中的Unit
会在这个Unit
启动时的同时被启动,如果其中任意一个Unit
启动失败,这个Unit
也会启动失败Wants
: 弱依赖,与Requires
相似,但只是在当前Unit
启动时,触发启动列出的Unit
,而不考虑这些Unit
启动是否成功After
:与Requires
相似,该字段列出的所有Unit
全部启动后,才会启动当前的Unit
Before
: 与After
相反,当前Unit
应该在列出的Unit
之前启动OnFailure
:Unit
启动失败时,自动启动列出的每个Unit
Conflicts
: 与这个Unit
有冲突的模块,如果列出的Unit
已经在运行时,当前Unit
不能启动,反之亦然
Service
块下面的字段
EnvironmentFile
: 指定当前服务的环境变量定义文件,该文件内部的key=value
键值对,可以用$key
的形式,在当前.service
文件中引用ExecStart
: 定义启动进程时执行的命令ExecStartPre
: 启动当前服务之前执行的命令ExecStartPost:
启动当前服务之后执行的命令
ExecStop
: 停止服务时执行的命令ExecStopPost
: 停止服务之后执行的命令ExecReload
: 重启服务时执行的命令
Type
: 启动类型simple
: 默认值,执行ExecStart
指定的命令,启动主进程forking
: 以fork
方式从父进程创建子进程,创建后父进程会立即退出oneshot
: 一次性进程,Systemd
会等当前服务退出,再继续往下执行dbus
: 当前服务通过D-Bus
启动notify
: 当前服务启动完毕,会通知Systemd
,再继续往下执行idle
: 若有其他任务执行完毕,当前服务才会运行WorkingDirectory
: 指定服务的工作目录RootDirectory
: 指定服务进程的根目录,如果配置了这个参数,服务将无法访问指定目录以外的文件
User
: 指定运行服务的用户Group
: 指定运行服务的用户组KillMode
: 定义Systemd
如何停止服务,可选项如下control-group
: 默认值,当前控制组里面的所有子进程,都会被杀掉process
: 只杀主进程mixed
: 主进程将收到SIGTERM
信号,子进程收到SIGKILL
信号none
: 没有进程会被杀掉,只是执行服务的stop
命令
install块下面的字段
WantedBy
值为一个或多个Target
,当前Unit
激活 (enable
,即开机启动) 时符号链接会放入/usr/lib/systemd/system
目录下以<Target>.wants
后缀构成的子目录中,该参数常见的值是multi-user.target
,这样,当该target
中的任意一个Unit
启动的时候,本Unit
都会跟着一起启动,这就是配置开机自启动的关键Also
当前Unit enable/disable
时,同时enable/disable
的其他Unit
Alias
当前Unit
可用于启动的别名
systemd Unit
的配置与使用
最好的样例还是参考操作系统已经有的内容
比如现在要编写一个自启动服务,可以参考docker.service
和sshd.service
$ systemctl cat docker
$ systemctl cat sshd
如果要编写一个定时任务,可以参考apt-dayly.timer
$ systemctl cat apt-daily.timer
systemctl
使用
管理系统和服务的命令行工具
设备启动,关机等操作,可以看到设备运行操作命令是指向systemctl
的软链接
$ ll /usr/sbin |grep system
lrwxrwxrwx 1 root root 14 九月 10 02:47 halt -> /bin/systemctl
lrwxrwxrwx 1 root root 20 九月 10 02:47 init -> /lib/systemd/systemd
lrwxrwxrwx 1 root root 14 九月 10 02:47 poweroff -> /bin/systemctl
lrwxrwxrwx 1 root root 14 九月 10 02:47 reboot -> /bin/systemctl
lrwxrwxrwx 1 root root 14 九月 10 02:47 runlevel -> /bin/systemctl
lrwxrwxrwx 1 root root 14 九月 10 02:47 shutdown -> /bin/systemctl
lrwxrwxrwx 1 root root 14 九月 10 02:47 telinit -> /bin/systemctl
systemd
常用操作
动作 | 命令 | 注释 |
---|---|---|
分析系统状态 | ||
显示系统状态 | systemctl status | |
列出正在运行的单元 | systemctl or systemctl list-units | |
列出失败的单元 | systemctl --failed | |
列出已安装的单元 | systemctl list-unit-files | |
Show process status for a PID | systemctl status {pid} | cgroup slice, memory and parent |
检查单元状态 | ||
显示单元的手册页 | systemctl help {unit} | 如果单元支持 |
显示单元的状态 | systemctl status {unit} | 包括其是否在运行 |
检查单元是否配置为自动启动 | systemctl is-enabled {unit} | |
启动、重新启动和重新加载单元 | ||
立即启动单元 | systemctl start {unit} | |
立即停止单元 | systemctl stop {unit} | |
重新启动单元 | systemctl restart {unit} | |
重新加载单元及其配置 | systemctl reload {unit} | |
重新加载 systemd 配置 | systemctl daemon-reload | 扫描单元的变动 |
启用单元 | ||
开机自动启用单元 | systemctl enable {unit} | |
开机自动启用单元,并立即启动它 | systemctl enable --now {unit} | |
取消开机自动启用单元 | systemctl disable {unit} | |
重新启用 单元 | systemctl reenable {unit} | 先取消启用,再启用 |
禁用单元 | ||
禁用单元,使其无法启动 | systemctl mask {unit} | |
取消禁用单元 | systemctl unmask {unit} |
systemd
其他常用命令模块
journalctl
查询
systemd
服务的日志信息$ journalctl
loginctl
systemd
的登入管理器,可以查看目前登录到当前计算机的用户信息$ sudo loginctl SESSION UID USER SEAT TTY 2 1000 gong seat0 tty2 640 1000 gong pts/17 641 1000 gong pts/19
如果使用
ssh {用户名}@localhost
再次登录到本地计算机,使用loginctl
的时候会看到多了一个session
这个时候可以使用如下命令,指定一个
session id
的方式关闭某个用户的访问$ sudo loginctl kill-session 641
systemd-analyze
分析系统启动过程中的耗时,关闭或者调整某些服务可以优化启动速度
$ sudo systemd-analyze blame 1min 55.462s fstrim.service 39.372s fwupd-refresh.service 33.827s apt-daily.service 31.949s apt-daily-upgrade.service 16.637s systemd-networkd-wait-online.service 6.193s plymouth-quit-wait.service 5.070s docker.service 1.551s snapd.service 1.540s netfilter-persistent.service 1.318s snapd.seeded.service 1.196s vmware.service 1.129s freeradius.service 1.049s gpu-manager.service .....
systemd service
使用示例
编写一个文件/usr/lib/systemd/system/test-boot.service
如下
[Unit]
Description=test boot
# 表示在网络模块启动之后才启动本Unit
After=network.target
[Service]
# 该命令是阻塞的,每隔1秒会打印当前日期
ExecStart=/bin/sh -c "while true; do date; sleep 1; done"
# 配置查询的工作路径,该路径需要存在,否则会报错
WorkingDirectory=/tmp/
[Install]
# 该参数表示此Unit是开机启动时候关联到multi-user.target
# 当multi-user.target下面的任意一个Unit启动都会触发本Unit的启动
# 即enable状态的时候会创建一个链接到/etc/systemd/system/multi-user.target.wants/目录下面
WantedBy=multi-user.target
执行命令如下
$ sudo systemctl daemon-reload
查看当前服务状态
$ sudo systemctl status test-boot
○ test-boot.service - test boot
Loaded: loaded (/lib/systemd/system/test-boot.service; disabled; vendor preset: enabled)
Active: inactive (dead)
配置开机启动,注意观察软链接的位置是配置在
$ sudo systemctl enable test-boot
Created symlink /etc/systemd/system/multi-user.target.wants/test-boot.service → /lib/systemd/system/test-boot.service.
接下去启动服务并查看状态
$ sudo systemctl start test-boot
$ sudo systemctl status test-boot
● test-boot.service - test boot
Loaded: loaded (/lib/systemd/system/test-boot.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2023-03-16 17:32:13 CST; 20s ago
Main PID: 940646 (sh)
Tasks: 2 (limit: 38197)
Memory: 364.0K
CGroup: /system.slice/test-boot.service
├─940646 /bin/sh -c "while true; do date; sleep 1; done"
└─940825 sleep 1
三月 16 17:32:24 gong sh[940765]: 2023年 03月 16日 星期四 17:32:24 CST
三月 16 17:32:25 gong sh[940769]: 2023年 03月 16日 星期四 17:32:25 CST
三月 16 17:32:26 gong sh[940778]: 2023年 03月 16日 星期四 17:32:26 CST
三月 16 17:32:27 gong sh[940782]: 2023年 03月 16日 星期四 17:32:27 CST
三月 16 17:32:28 gong sh[940786]: 2023年 03月 16日 星期四 17:32:28 CST
三月 16 17:32:29 gong sh[940795]: 2023年 03月 16日 星期四 17:32:29 CST
三月 16 17:32:30 gong sh[940799]: 2023年 03月 16日 星期四 17:32:30 CST
三月 16 17:32:31 gong sh[940803]: 2023年 03月 16日 星期四 17:32:31 CST
三月 16 17:32:32 gong sh[940814]: 2023年 03月 16日 星期四 17:32:32 CST
最后可以重启机器校验一下是否可以开机自启动
参考阅读
itwire
于2014发表的 Linus Torvalds
关于systemd
的访谈
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。