按下电源键,随着风扇转动的声音,显示器上开启的图标亮起。之后,只需要静静等待几秒钟,登录界面显示,输入密码,即可愉快的玩耍了。
这是我们大概每天都做的事情。那么中间到底发生了什么?
简单地说,从BIOS或者UEFI开始读取硬盘。接下来,进入bootloader(LILO或者GRUB),bootloader开始载入内核,内核初始化完毕后,紧接着进入用户空间的初始化。
用户空间的启动的第一个进程即pid=1,就是从一个叫init的程序开始的,这也是本文的主角
- 1. Systemd简介与使用
- 1.1. 用户空间的启动顺序
- 1.2. SystemV
- 1.2.1. 运行级别
- 1.3. Systemd
- 1.3.1. systemd启动步骤
- 1.3.2. 单元和单元类型
- 1.3.2.1. 单元类型
- 1.3.3. systemd相关指令
- 1.3.3.1. 电源管理
- 1.3.3.2. 分析系统状态
- 1.3.3.3. 单元的管理
- 1.3.3.4. 单元的依赖列表
- 1.3.3.5. 其他
- 1.4. systemd配置
- 1.4.1. Service文件
- 1.4.1.1. [Unit]
- 1.4.1.2. [Service]
- 1.4.1.3. [Install]
- 1.4.2. target文件
- 1.4.1. Service文件
- 1.5. systemd日志服务
- 1.6. 在systemd中添加单元
- 1.6.1. 写一个小栗子
- 1.7. systemd 的按需和资源并行启动
- 1.8. 参考
1. Systemd简介与使用
1.1. 用户空间的启动顺序
用户的空间的大致启动顺序如下:
- init
- 基础底层服务,如udevd(设备管理器),syslogd(日志管理)
- 网络配置
- 中高层服务,如cron(定时器)
- 登录提示符(getty)、GUI、MySQL(如果设置开机启动的话)
init是内核启动的第一个用户空间进程,主要负责启动、终止系统中的基础服务进程。
Linux下,init主要有三个实现版本:
- System V,传统的init
- Upstart,Ubuntu后期针对sys-v的一个改进实现版本
- systemd,是一套中央化系统及设置管理程序(init),包括有守护进程、程序库以及应用软件,兼容sys-v。现代大部分桌面版都使用此实现。也是本文主要介绍的一个...emmmm...框架。是的,systemd更像一个服务管理框架。
1.2. SystemV
先说说传统的SystemV,他其实就是利用一系列脚本来启动服务,之后的事就撒手不管了。
SystemV init依赖一个特定的启动顺序每次只能启动执行一个启动任务。
这些都是通过一个核心配置文件tab(/etc/init
)和一组启动脚本以及符号链接集执行的,本质上为系统提供了合理的启动顺序,
支持不同的运行级别。
他的好处是依赖关系简单,任务之间泾渭分明的一个一个启动,即使某个基础服务出了错也便于排查。但也正因为如此,他的启动性能很不好。
服务无法并行启动不说,而且只能按照预先规定的顺序启动服务。如果你安装了新的硬件或者新服务,他不提供及时支持的标准方法。
图1
我们把用户空间init的服务分别叫做Job A
、Job B
...图1可以看到,在SysV init之下,服务必须一个接一个的顺序启动,前面的服务初始化完毕,后面才可以开始。因此,启动时间就是所有服务启动时间之和。
他的改进版Upstart
在此基础上就做了优化——互不相关的服务可以并行启动,这样启动总时间就等于时间消耗最大的一组服务,而不是所有服务之和。systemd在并行启动上采取了比Upstart
更加激进的方案
图2
图2是systemd的并行启动方式,他让配置所有的服务同时启动。如果Job Aing
依赖Job B
怎么办呢?首先两个Job
是同时启动的,A如果先启动,就向B发送请求服务,B会先将请求缓存起来,等到B初始化完毕之后,再处理缓存的请求。
相比SysV init,这也带来了不确定性,即你不知道此时到底哪些服务起了,哪些没起,全依赖系统管理
1.2.1. 运行级别
运行级别的概念最早应该也是来自于SysV init.
简单地说,运行级别定义了系统的特定状态,这种状态可以看成一系列服务状态的集合。
不同的发行版有不同的运行级别,但比较公认的如下:
- 0,关机
- 1,单用户模式(修复模式),如果你的系统凉凉了,这将是你的救命稻草
- 6,重启
以我个人的deepin15.7
为例,如图
其中runlevel2/3/4都属于同一个运行等级(multi-user),而系统的默认的运行等级为5——graphical。我们平时所用的桌面环境就是这个等级了。其实,现代大部分采用systemd的发行版都和这个大同小异。
我们使用systemctl cat graphical.target
打开graphical.target
文件,可以看到下面内容:
[Unit]
Description=Graphical Interface
Documentation=man:systemd.special(7)
Requires=multi-user.target
Wants=display-manager.service
Conflicts=rescue.service rescue.target
After=multi-user.target rescue.service rescue.target display-manager.service
AllowIsolate=yes
其中的Requires=multi-user.target
表示,如果想启动graphical.target
(即运行等级5)就必须先启动multi-user.target
(运行等级3).由此可见,在systemd中,运行等级5就是在等级3基础上,同时启动一个display-manager
服务。display-manage
顾名思义,肯定是和图像显示有关的咯。
如果你对.target
文件,和他的定义语法很迷惑,没有关系,后面还会详细解释。我举这个例子,只是想让你了解systemd是兼容systemV的运行等级概念的。所以,你关于SystemV的认识也是可以继续沿用的
1.3. Systemd
在Linux中以d
结尾的,表示这是一个守护程序,systemd就是这个系统的守护程序
相比于之前的版本,systemd最关键的特性是:
- 延迟启动某些服务和系统功能,等到需要他们的时候才开启
- 完全并行启动
systemd 架构图
1.3.1. systemd启动步骤
systemd的特性复杂,下面给出大致的启动步骤,使我们有个总体观:
- systemd加载配置信息
- 判定启动目标,一般是default.target
- 判定启动目标的依赖关系
- 激活依赖服务,启动目标
- 响应系统消息,激活其他组件
1.3.2. 单元和单元类型
systemd不光负责处理进程和服务,同时还能挂载文件系统、监控网络套接字等等。在systemd中
所有服务和功能都被抽象成一个个单元(Unit),根据功能不同,单元类型也不同。systemd正是通过配置这些单元
来开关、管理服务的。
1.3.2.1. 单元类型
比较常用的几种:
- 服务单元,传统的守护进程(XXX.service文件表示)
- 挂载单元,控制文件系统挂载(XXX.mount文件表示)
- 目标单元,将服务单元、挂载单元等单元组织在一起的单元,一般对应Sys-V的运行等级(XXX.target文件表示)
上面的尤其是服务单元我们会经常打交道,而且必要时也可以自定义服务单元等。比如我们的蓝牙功能就抽象成blueteeth.service
,管理磁盘的udev系统对应systemd-udevd.service
文件。如果你安装了mysql,
还可以找到一个mysql.service
文件。
使用deepin15.7的过程中,遇到过一个bug,就是在系统长期休眠之后再重启,蓝牙模块莫名其妙的关闭了,进入[设置]面板也
无法找到蓝牙配置选项了。这时执行systemctl restart blueteeth.service
重启蓝牙模块,大概率就会修复了
除了以上几种,还有其他类型,比如
socket单元(.socket
)、系统设备单元(.device
)、交换单元(.swap
)、路径单元(.path
)、定时单元(.time
),
不一而足
1.3.3. systemd相关指令
1.3.3.1. 电源管理
主要涉及开关、系统重启等,如果你是当前唯一用户的话则不需要提权,否则需要root密码
systemctl reboot #重启
systemctl poweroff #关机
systemctl suspend #待机
systemctl hibernate #休眠
systemctl rescue #进入单用户模式
1.3.3.2. 分析系统状态
主要是查看系统中纳入systemd管理的服务的状态
systemctl status #系统状态
systemctl list-units #所有激活单元列表
systemctl --failed #运行失败单元列表
# 列出所有配置文件
$ systemctl list-unit-files
# 列出指定类型的配置文件
$ systemctl list-unit-files --type=service
1.3.3.3. 单元的管理
使用systemd操作单元的激活与关闭
systemctl start <unit> #立即激活单元
systemctl stop <unit> #立即关闭单元
sudo systemctl kill <unit> #前面的stop不好使了,就强行杀死这个单元
systemctl restart <unit> #重启单元
systemctl status <unit> #单元状态,这是和好用的指令,能够看到服务单元的几乎所有信息
systemctl is-enabled <unit> #单元是否配置自动启动
systemctl enable <unit># 配置自动启动单元
systemctl disable <unit>#关闭单元自动启动
systemctl help <unit>#单元帮助手册,一般是服务单元
systemctl daemon-reload <unit>#扫描单元配置文件变动,重新载入
systemctl mask <unit> #禁用单元
systemctl unmask <unit>#取消禁用
下面是我本人计算机上mysql的状态信息:
systemctl status mysql.service
● mysql.service - MySQL Community Server
Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: enabled)
Active: active (running)
Process: 2666 ExecStart=/usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid (code=exited, status=0/SUCCESS)
Process: 2602 ExecStartPre=/usr/share/mysql/mysql-systemd-start pre (code=exited, status=0/SUCCESS)
Main PID: 2668 (mysqld)
Tasks: 27 (limit: 4915)
Memory: 218.1M
CGroup: /system.slice/mysql.service
└─2668 /usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid
- loaded,单元配置文件地址
- active:激活状态
- process:开启服务时执行的指令
- main Pid:主进程ID
- memory:占用内存
- CGroup:systemd通过CGroup控制进程,这里展示该服务的所有子进程
1.3.3.4. 单元的依赖列表
systemctl list-depandencies <xxx.service> #列出xxx.service的依赖单元
在systemd中的单元的依赖关系
1.3.3.5. 其他
一些杂七杂八的指令
systemd-analyze #系统启动时间统计
systemd-analyze blame #查看所有服务启动时间列表,blame就能看出,这是要等一个背锅位
localectl #本地化信息
timedatectl #时区信息
loginctl list-user #列出当前登录用户
systemd的指令非常丰富,可以通过查询文档获取全部指令
1.4. systemd配置
systemd的配置文件主要分布在两个地方:
系统单元目录(全局配置,我的是/lib/systemd/system
)和系统配置目录(局部配置,我的是/etc/systemd/system
)
你可以通过下面的指令查询配置目录:
pkg-config systemd --variable=systemdsystemunitdir #单元目录
pkg-config systemd --variable=systemdsystemconfdir #配置目录
其实配置目录的很多文件都是指向单元目录的软链接。
单元配置文件就像一个蓝图,定义了一个单元的依赖关系、启动顺序、开启关闭指令或者挂载点等,
systemd就是读取这些信息来管理单元的。
1.4.1. Service文件
在systemd中一个.service
就是一个服务类型的配置单元,同时也代表了一个服务功能。
我们使用sysctemctl cat ssh.service
来查看ssh.service文件内容,该文件就在/lib/systemd/system
下.
注:这个Service只有在你安装openssh-server之后才会有.
[Unit]
Description=OpenBSD Secure Shell server
After=network.target auditd.service
ConditionPathExists=!/etc/ssh/sshd_not_to_be_run
[Service]
EnvironmentFile=-/etc/default/ssh
ExecStartPre=/usr/sbin/sshd -t
ExecStart=/usr/sbin/sshd -D $SSHD_OPTS
ExecReload=/usr/sbin/sshd -t
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
RestartPreventExitStatus=255
Type=notify
RuntimeDirectory=sshd
RuntimeDirectoryMode=0755
[Install]
WantedBy=multi-user.target
Alias=sshd.service
可以看到service文件分为Unit
/Service
/Install
三个区块,我们分开解释
1.4.1.1. [Unit]
主要描述启动顺序与依赖关系
[Unit]
Description=OpenBSD Secure Shell server
After=network.target auditd.service
ConditionPathExists=!/etc/ssh/sshd_not_to_be_run
Description,一段描述Service的信息
After,表示ssh.service
在network.target auditd.service
单元之后启动。另外还有一个属性Before
,
表示当前单元在列出的单元之前启动。比如Before=bar.service
,说明当前单元在bar.service
之前启动。After
、Before
定义了单元之间启动的顺序
ConditionPathExists,表示在后面的路径存在时返回true,这里使用了!
非运算符,应该是取反的意思。
同样还有其他几个路径判断条件——ConditionPathIsDirectory
、ConditionFileNotEmpty
,顾名思义,他们的
意义应该不难猜吧。这些条件必须返回为true
,否则该单元不会运行
1.4.1.2. [Service]
这个区块定义如何启动当前服务
[Service]
EnvironmentFile=-/etc/default/ssh
ExecStartPre=/usr/sbin/sshd -t
ExecStart=/usr/sbin/sshd -D $SSHD_OPTS
ExecReload=/usr/sbin/sshd -t
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
RestartPreventExitStatus=255
Type=notify
EnvironmentFile,指定当前服务环境参数文件,内部使用键值对定义,可以使用$key
读取值,比如后面的$SSHD_OPTS
ExecStartPre,定义启动服务前执行的指令
ExecStart,定义启动程序执行的指令
ExecReload,表示重启服务时执行的命令。其他的诸如ExecStop等等,望文生义即可
KillMode,定义 Systemd 如何停止 sshd 服务,process表示当kill sshd服务的时候,仅杀死主进程,子进程还是留着的。
其他的kill模式还有:
- control-group(默认值):当前控制组里面的所有子进程,都会被杀掉
- mixed:主进程将收到 SIGTERM 信号,子进程收到 SIGKILL 信号
- none:没有进程会被杀掉,只是执行服务的 stop 命令
Restart字段,定义了 sshd 退出后,Systemd 的重启方式。on-failure,表示任何意外的失败,就将重启sshd。
另外还有其他重启模式定义:
- no(默认值):退出后不会重启
- on-success:只有正常退出时(退出状态码为0),才会重启
- on-abnormal:只有被信号终止和超时,才会重启
- on-abort:只有在收到没有捕捉到的信号终止时,才会重启
- on-watchdog:超时退出,才会重启
- always:不管是什么退出原因,总是重启
最后一个比较重要的是Type字段,定义启动类型。notify,表示启动结束后会发出通知信号,然后 Systemd 再启动其他服务。
其他的类型如下:
- simple(默认值):ExecStart字段启动的进程为主进程
- forking:ExecStart字段将以fork()方式启动,此时父进程将会退出,子进程将成为主进程
- oneshot:类似于simple,但只执行一次,Systemd 会等它执行完,才启动其他服务
- dbus:类似于simple,但会等待 D-Bus 信号后启动
1.4.1.3. [Install]
定义如何安装这个配置文件,即怎样做到开机启动
WantedBy字段:表示该服务所在的Target。
Target的含义是服务组,表示一组服务。WantedBy=multi-user.target指的是,sshd 所在的 Target 是multi-user.target。
systemctl enable sshd.service
其实就是将sshd服务的链接放在multi-user.target.wants
目录下。
同时multi-user.target
是系统的默认target,在启动该target的时候,他下面的服务都会开机启动。
这也就是只要挂上multi-user.target
就能开机启动的原因
1.4.2. target文件
执行systemctl cat multi-user.target
,可得:
[Unit]
Description=Multi-User System
Documentation=man:systemd.special(7)
Requires=basic.target
Conflicts=rescue.service rescue.target
After=basic.target rescue.service rescue.target
AllowIsolate=yes
target文件只是组织一批服务,因此他没有[service]、[mount]等定义启动或者挂载的区块
Requires,表示强依赖关系,即必须要求basic.target
启动,否则multi-user启动失败。
其他的依赖关系如下:
- Wants,只用于激活依赖,没有强依赖关系,该服务没起来也不影响当前服务
- Conflicts,冲突关系,有我没他,否则不能运行
- Requisite,前置依赖,当前单元激活前,必须激活它,否则失败,属于强依赖
Wants是比较重要的依赖关系,他不会将启动错误扩散给其他单元。systemd文档鼓励我们多用Wants关系
AllowIsolate,表示允许使用systemctl isolate命令切换到multi-user.target
1.5. systemd日志服务
systemd 自带日志服务 journald,该日志服务的设计初衷是克服现有的 syslog 服务的缺点。
- syslog 不安全,消息的内容无法验证
- 数据没有严格的格式,非常随意
Systemd Journal 用二进制格式保存所有日志信息,用户使用 journalctl 命令来查看日志信息。无需自己编写复杂脆弱的字符串分析处理程序。
常见的指令如下:
# 查看所有日志(默认情况下 ,只保存本次启动的日志)
$ sudo journalctl
# 查看内核日志(不显示应用日志)
$ sudo journalctl -k
# 查看系统本次启动的日志
$ sudo journalctl -b
$ sudo journalctl -b -0
# 查看上一次启动的日志(需更改设置)
$ sudo journalctl -b -1
# 查看指定时间的日志
$ sudo journalctl --since="2012-10-30 18:17:16"
$ sudo journalctl --since "20 min ago"
$ sudo journalctl --since yesterday
$ sudo journalctl --since "2015-01-10" --until "2015-01-11 03:00"
$ sudo journalctl --since 09:00 --until "1 hour ago"
# 显示尾部的最新10行日志
$ sudo journalctl -n
# 显示尾部指定行数的日志
$ sudo journalctl -n 20
# 实时滚动显示最新日志
$ sudo journalctl -f
# 查看指定服务的日志
$ sudo journalctl /usr/lib/systemd/systemd
# 查看指定进程的日志
$ sudo journalctl _PID=1
# 查看某个路径的脚本的日志
$ sudo journalctl /usr/bin/bash
# 查看指定用户的日志
$ sudo journalctl _UID=33 --since today
# 查看某个 Unit 的日志
$ sudo journalctl -u nginx.service
$ sudo journalctl -u nginx.service --since today
# 实时滚动显示某个 Unit 的最新日志
$ sudo journalctl -u nginx.service -f
# 合并显示多个 Unit 的日志
$ journalctl -u nginx.service -u php-fpm.service --since today
# 查看指定优先级(及其以上级别)的日志,共有8级
# 0: emerg
# 1: alert
# 2: crit
# 3: err
# 4: warning
# 5: notice
# 6: info
# 7: debug
$ sudo journalctl -p err -b
# 日志默认分页输出,--no-pager 改为正常的标准输出
$ sudo journalctl --no-pager
# 以 JSON 格式(单行)输出
$ sudo journalctl -b -u nginx.service -o json
# 以 JSON 格式(多行)输出,可读性更好
$ sudo journalctl -b -u nginx.serviceqq
-o json-pretty
# 显示日志占据的硬盘空间
$ sudo journalctl --disk-usage
# 指定日志文件占据的最大空间
$ sudo journalctl --vacuum-size=1G
# 指定日志文件保存多久
$ sudo journalctl --vacuum-time=1years
1.6. 在systemd中添加单元
关于自定义单元的首要一点建议:不要更改/lib/systemd/system
(系统单元目录),他由系统维护。
我们一般在/etc/systemd/system
下自定义启动单元。
1.6.1. 写一个小栗子
- 创建一个名为
test1.target
的单元
[Unit]
Description=test 1
- 创建
test2.target
,依赖与test1
[Unit]
Description=test 2
Wants=test1.target
- 激活test2.target,test1作为依赖也会被激活
systemctl start test2.target
- 验证两个是否都被激活
systemctl status test1.target test2.target
注:如果单元内包含[Install]模块,需要在start前enable他.systemctl enable <unit>
- 删除单元
systemctl stop <unit> #首先停止单元
systemctl disable <unit> #如果有[Install]模块,则删除连接符号
#最后删除单元文件即可
1.7. systemd 的按需和资源并行启动
这是一个很复杂的概念,最好单独讨论