Systemd 使用简介

2015-09-17 linux software

现在一般新发行的版本会采用新的 init 进程,也就是 systemd ,其中启动过程可以通过 man bootup 查看。

在此,简单介绍一下 systemd 。

简介

内核加载完成之后会启动用户态的 init 进程,完成剩余的系统初始化,在 Linux 历史上,包含几种启动方案,目前为止,大部分都采用的是 systemd 进行系统启停、服务管理等。

以 Ubuntu 为例,大致分成了三阶段:A) Ubuntu 6.10 之前采用 Sysvinit;B) Ubuntu 14.10 以及之前版本使用 Upstart 同时还存留着 Sysvinit 版本;C) 从 Ubuntu 15.04 开始使用 Systemd ,不过仍然允许使用 Upstart 。

而 CentOS 从 7 开始使用 systemd ,可以通过 rpm -qif `which init` 命令查看 init 来自于那个包。

其中 Upstart 类似于中间过渡版本,而且只在部分发行版本上使用,这里就不做过多介绍,仅对 SysvInit 做简单介绍,主要关注 Systemd 的使用。

SysvInit

老的 Linux 发行版本 (例如 MCCTAMU 等) 都是采用的 SysvInit 机制,定义了 0-6 总共七个系统层级。

SysvInit 全称为 System V style Init programs,最初由 Miquel van Smoorenburg 开发,用来控制系统的启动和停止,现在由 Savannah 维护。从 1992.02 在 Minix 系统上使用,后来在 1992.07 迁移到 Linux 系统上,并一直在维护,详细可以查看 ChangeLog

支撑 SysvInit 功能的大部分是 Bash 脚本,也就是所谓的 LSB(Linux Standard Base) Init Script ,一般在 /etc/init.d 目录下,通常在开始部分包含 LSB headers ,一般以 ### BEGIN INIT INFO 开始,以 ### END INIT INFO ,包含了对脚本信息的描述,会通过 insserv 命令计算启动顺序。几乎每个服务都对应了脚本,各个脚本提供了 start、stop、restart、reload、status 类似的命令用来控制服务,甚至对返回码也有规定。

启动时,会在 /etc/inittab 中读取包含 initdefault 的默认启动层级,并根据配置的脚本启动,一般包括了设置内核参数、设定系统时钟、设置主机名、启动 swap 分区、挂载文件系统等等,可以通过 chkconfig 命令管理服务。

Systemd

虽然越来越多的发行版本开始使用 systemd ,但开始也是最具争议的一个项目,因为其不只替换了 init,而且还包括了一整套的系统任务,违背了 Linux 中的单一职责原则。

作为最新的系统和服务管理器,其设计思路借鉴了 Mac 的启动程序 Launchd,兼容 SysV 和 LSB 的启动脚本。事实上其作用远不仅是启动系统,它还接管了系统服务的启动、结束、状态查询和日志归档等职责,并支持定时任务和通过特定事件,如插入特定 USB 设备和特定端口数据触发的任务。

其有以下特性:支持并行化任务、同时采用 socket 和 D-BUS 总线式激活服务、按需启动相应的守护进程、利用 Linux 的 cgroup 监控进程、支持快照和系统恢复、维护挂载点和自动挂载点,各服务间基于依赖关系进行精密控制。

可以通过 pstree 查看启动的进程树,接下来查看一下 systemd 的特性。

资源隔离

systemd 是基于 cgroup 来实现的,当前服务层级可以通过 systemd-cgls 命令查看,通过 slice scope service 划分。

  • service 会作为一个或一组进程单元,这样进程可以作为整体进行启停。
  • scope 可以由任意进程启动,会在运行时会注册到 PID 1 进程。
  • slice 仅作为 service 和 scope 的中间层,并不会真正的包含进程。

实际上,可以将 service 和 scope 作为同一级别,用来管理服务,只是使用场景不同;而 slice 则主要是为了划分层级,并不会真正的管理服务进程。

其中 service 作为主要的服务管理配置方式,可以详细查看 服务管理 中的介绍,这里主要介绍 scope 和 slice 。

slice

如上所述,systemd 是通过 slice 对 cgroup 划分层级,在启动时,默认会包含如下几个 slice (详细可以查看 /usr/lib/systemd/system 目录下对应的配置文件) :

  • -.slice 作为所有 slice 的根 Slice ,类似于根目录。
  • system.slice 系统服务的默认存放位置。
  • user.slice 用户会话的默认存放位置。
  • machine.slice 虚拟机和容器的默认存放位置。

另外,需要注意 slice 的命名,会通过 - 分割父子 slice 名称,例如通过 parent-name.slice 可以知道其父 slice 是 parent.slice ,而当前 slice 还可以包含子 slice ,如 parent-name-child.slice

而且默认是不会创建 slice 的,只有在对应 slice 下有服务启动时创建相关的 slice 配置。

scope VS. service

service 和 slice 一般是通过配置文件 scope 并不是通过配置文件生成的,而是通过 systemd API 调用生成,当然,也包括了通过 systemd-run --scope 命令行方式调用。与 service 区别是,service 通常由 systemd 来 fork 进程,而 scope 可以有进程执行 fork ,常见的如 ssh、x11 等。

最常见的就是 /run/systemd/system/session-*.scope 中的文件,这是由 systemd-logind 服务在用户登录时针对用户所创建的,可以通过 loginctl list-sessions 查看当前会话。

使用示例

假设想对 docker 服务进行资源限制,可以创建一个 docker.slice 用来限制总体的资源,内容如下。

[Unit]
Description=Docker Slice
Before=slices.target

[Slice]
MemoryAccounting=true
MemoryLimit=2048M

CPUAccounting=true
CPUQuota=25%
TasksMax=4096

再修改相关的服务配置,在 [Service] 段中增加 Slice=docker.slice 配置项,并通过 systemctl daemon-reload 命令重新加载,然后通过 systemctl start docker 启动。

执行 systemd-cgls 命令查看层级时会看到如下内容。

├─docker.slice
 └─docker.service
   ├─1298 /usr/bin/dockerd --add-runtime oci=/usr/sbin/docker-runc
   └─1383 docker-containerd --config /var/run/docker/containerd/containerd.toml --log-level warn

如果要手动执行命令,也可以通过 systemd-run --uid=UID --gid=GID -t --slice=docker.slice /path/your/command 方式运行进程。

Systemd

Systemd 使用 target 来处理引导和服务管理过程,这些 systemd 里的 target 文件被用于分组不同的引导单元以及启动同步进程。

如果 A 要求 B 在 A 之前运行,则在 [Unit] 段中添加 Requires=B 和 After=B,如果依赖关系是可选的,可添加 Wants=B 和 After=B;注意 Wants= 和 Requires= 并不意味着 After=,即如果 After= 选项没有设置,这两个单元将被并行启动。

执行顺序

执行的第一个目标是 /etc/systemd/system/default.target,对于桌面版本,通常会指向 /usr/lib/systemd/system/graphical.target,该文件为文本,可以查看其实际会依次依赖于 multi-user.target => basic.target => sysinit.target

另外,local-fs.target 单元并不会启动用户相关的服务,它只处理与文件系统相关的底层核心服务,会根据 /etc/fstab/etc/inittab 来执行相关操作。

sysinit.target 会启动重要的系统服务例如系统挂载,内存交换空间和设备,内核补充选项等等。basic.target 会启动普通服务特别是图形管理服务,它通过 /etc/systemd/system/basic.target.wants 目录来决定哪些服务会被启动。

在这个阶段,会启动 multi-user.target 而这个 target 将自己的子单元放在目录 /etc/systemd/system/multi-user.target.wants 里。这个 target 为多用户支持设定系统环境,非 root 用户会在这个阶段的引导过程中启用。防火墙相关的服务也会在这个阶段启动。

登陆是通过 systemd-logind.service 进行,可以通过 systemctl help systemd-logind.service 查看帮助,通常是针对 XWindow,而终端登陆则通过 /usr/lib/systemd/system/getty@.service 执行。

依赖关系可以查看 System bootup process

启动优化

sysvinit 只能一次一个串行地启动进程,而 Systemd 则并行地启动系统服务进程,并且最初仅启动确实被依赖的那些服务,极大地减少了系统引导的时间。

任何启动项,只要是在系统启动时有被执行到,不论启动成功还是失败,systemd 都能够记录下他们的状态,可以通过 systemctl 查看当前的服务。

详细信息可以通过 systemctl status systemd-logind.service 查看;启动的结构树可以通过 systemd-cgls 查看。

# systemd-analyze                         ← 查看系统引导用时
# systemd-analyze time                    ← 同上
# systemd-analyze blame                   ← 查看初始化任务所消耗的时间
# systemd-analyze plot > systemd.svg      ← 将启动过程输出为svg图
# systemd-cgtop                           ← 查看资源的消耗状态

常用 systemctl 命令

通过 systemctl 命令可以管理整个系统。

----- 查看、设置、取消开机启动
# systemctl is-enabled nginx
# systemctl enable nginx
# systemctl disable nginx

----- 启动、停止、kill、重启、查看服务状态
# systemctl start nginx
# systemctl stop nginx
# systemctl kill nginx
# systemctl restart nginx
$ systemctl status nginx

----- 修改service之后重新加载
# systemctl daemon-reload

----- 杀死一个服务的所有进程,传递信号到指定服务的所有进程
# systemctl kill crond.service
----- 指定信号类型,如下两者相同,所有fork的进程都会被杀死
# systemctl kill -s SIGKILL crond.service
# systemctl kill -s 9 crond.service
----- 发送指定信号到服务的主进程
# systemctl kill -s HUP --kill-who=main crond.service

----- 其它操作
# systemctl                               ← 列出正在运行的单元
# systemctl list-units                    ← 同上
# systemctl list-units --type service     ← 同上,只是以service为单位
# systemctl --failed                      ← 查看失败的任务
# systemctl list-unit-files               ← 所有已经安装的任务
# systemctl list-dependencies nginx       ← 查看特定服务的依赖关系

所有可用的单元文件存放在 /usr/lib/systemd/system/etc/systemd/system 目录中,一个单元配置文件可以为,系统服务(.service) 、挂载点(.mount)、sockets(.sockets)、系统设备、交换分区/文件、启动目标(target)、文件系统路径、由 systemd 管理的计时器,详见 man 5 systemd.unit

通过enable 设置为开机启动时,相当于在 /etc/systemd/system/default.target 符号链接指向的目标对应目录下添加指向 nginx 的符号链接。

设置启动级别

在 sysVinit 的 runlevels 大多是以数字分级的,而在 CentOS 7 中可以通过 systemd 修改运行的等级,常用的命令如下。

----- 列出当前系统支持的所有等级
# systemctl list-units --type=target

----- 查看当前等级,并设置当前以及默认的等级
# systemctl get-default
# systemctl isolate graphical.target
# systemctl set-default graphical.target

----- 查看所有已经启动的服务,包括启动失败的
$ systemctl list-units --type=service

实际上在通过 isolate 设置启动方式的时候是将 /etc/systemd/system/default.target 指向对应的启动等级。

日志管理

journald 是 systemd 独有的日志系统,替换了 sysVinit 中的 syslog 守护进程,通过命令 journalctl 读取日志。

Systemd 统一管理所有 Unit 的启动日志,好处是,只需要 journalctl 一个命令,就可以查看所有日志 (内核日志和应用日志) 的内容;配置文件为 /etc/systemd/journald.conf 。

----- 查看某个Unit的日志
# journalctl -u nginx.service
# journalctl -u nginx.service --since today

----- 实时滚动显示某个Unit的最新日志
# journalctl -u nginx.service -f

# 合并显示多个 Unit 的日志
$ journalctl -u nginx.service -u php-fpm.service --since today

更多内容可以查看 How To Use Journalctl to View and Manipulate Systemd Logs

电源管理

systemctl 命令也可以用来关机、重启、挂起、休眠。

# systemctl poweroff
# systemctl reboot
# systemctl suspend
# systemctl hibernate

时区设置

systemd 提供了一个 timedatectl 命令行,可用于配置时区信息。

----- 查看当前所支持的时区信息
$ timedatectl list-timezones
----- 选择上述中的时区,然后设置
# timedatectl set-timezone zone
----- 查看当前时区设置的状态
# timedatectl status

管理目标

服务 systemctl 脚本存放在 /usr/lib/systemd/ 目录下,有系统 (system) 和用户 (user) 之分,前者开机后无需登录即可运行,后者则需要在用户登录后才能运行程序。

常见的服务如 nginx 等存放在 /usr/lib/systemd/system 目录下;下面以 nginx 为例,编写脚本时可以直接参考 nginx 的编写方法。

[Unit]
Description=The nginx HTTP and reverse proxy server
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/run/nginx.pid
# Nginx will fail to start if /run/nginx.pid already exists but has the wrong
# SELinux context. This might happen when running `nginx -t` from the cmdline.
# https://bugzilla.redhat.com/show_bug.cgi?id=1268621
ExecStartPre=/usr/bin/rm -f /run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=process
KillSignal=SIGQUIT
TimeoutStopSec=5
PrivateTmp=true

[Install]
WantedBy=multi-user.target

一个服务以 .service 结尾,一般会分为 3 部分:[Unit][Service][Install]

服务脚本按照上面编写完成后,以 754 的权限保存在 /usr/lib/systemd/system 目录下,这时就可以利用 systemctl 进行配置了。

Unit

设置描述、帮助文档、启动顺序以及服务的启动依赖条件等,如下是 SSHD 服务的。

Description=OpenSSH server daemon
Documentation=man:sshd(8) man:sshd_config(5)
After=network.target sshd-keygen.service
Wants=sshd-keygen.service

AfterBefore 分别设置在哪些服务之后或者之前启动,用于配置各个服务的启动顺序,注意,这里是启动顺序而非依赖关系。

通过 WantsRequires 设置弱依赖和强依赖关系,前者表示依赖的服务启停不会影响当前服务,或者表示如果依赖退出,那么该服务同时退出。

Service

用于配置如何启动服务;注意,命令应该使用绝对路径。

User=nginx
Group=nginx
PIDFile=/run/nginx.pid

Type=forking                               # 定义启动类型
#  simple : 默认值,通过ExecStart字段启动进程
#  notify : 类似于simple,服务启动结束后会发出通知信号,然后Systemd再启动其他服务

EnvironmentFile=-/etc/sysconfig/nginx      # 依赖环境,可以指定多个
EnvironmentFile=-/etc/default/nginx

ExecStartPre=/usr/bin/rm -f /run/nginx.pid # 启动服务之前执行的命令
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx                  # 启动时,多个会被最后一个覆盖
ExecStartPost=                             # 启动服务之后执行的命令
ExecReload=/bin/kill -s HUP $MAINPID       # 重启服务时执行的命令
ExecStop=                                  # 停止服务时执行的命令
ExecStopPost=                              # 停止服务之后执行的命令
TimeoutStopSec=5                           # 设置停止超时时间

KillMode=process                           # 重启行为配置,详见如下介绍
#  control-group : 默认值,当前控制组里面的所有子进程,都会被杀掉
#  process       : 只杀主进程,信号可以通过如下方式定义
#  mixed         : 主进程将收到SIGTERM信号,子进程收到SIGKILL信号
#  none          : 没有进程会被杀掉,只是执行服务的stop命令
KillSignal=SIGQUIT

Restart=on-failure                         # 意外失败后重启方式,正常停止不重启
#  no          : 默认值,退出后不重启
#  on-success  : 只有正常退出时(退出状态码为0),才会重启
#  on-failure  : 非正常退出时 (退出状态码非0),包括被信号终止和超时,才会重启
#  on-abnormal : 只有被信号终止和超时,才会重启
#  on-abort    : 只有在收到没有捕捉到的信号终止时,才会重启
#  on-watchdog : 超时退出,才会重启
#  always      : 不管是什么退出原因,总是重启
RestartSec=10                              # 重启服务之前,需要等待的秒数,默认100ms

PrivateTmp=True                            # 给服务分配独立的临时空间

在所有的服务配置之前,都可以加上一个连词号 (-),表示 “抑制错误”,也即即使发生错误也不影响其他命令的执行。

Install

在通过 enable 设置为开机启动时,添加到那个 target 里面,也即定义如何安装这个配置文件,即怎样做到开机启动。

资源隔离

相关内容可以查看 CGroup

其它

systemd 带来了一整套与操作系统交互的新途径,如可以用 hostnamectl 获得机器的 hostname 和其它有用的独特信息。

# hostnamectl                              // 查看机器信息

除了 restart 命令,也可以使用 try-start 选项,它只会在服务已经在运行中的时候重启服务。

自动登陆

在此查看下如何自动登陆,首先创建一个新的类似于 getty@.service 的服务。

# cp /lib/systemd/system/getty@.service /etc/systemd/system/autologin@.service
# ln -s /etc/systemd/system/autologin@.service /etc/systemd/system/getty.target.wants/getty@tty8.service
# vi /etc/systemd/system/getty.target.wants/getty@tty8.service
...
ExecStart=-/sbin/mingetty --autologin USERNAME %I
Restart=no
...
Alias=getty.target.wants/getty@tty8.service

最后重新加载守护进程,运行服务:

# systemctl daemon-reload
# systemctl start getty@tty8.service

需要注意的是,如果你退出了 tty8 的会话,你需要等到下次重新启动才能使用,除非你给 Restart 的值是 'always',这样可以使用 systemctl 手动开启,不过,出于安全考虑,强烈建议不要那么做。

对比

SystemD VS. SysVinit

杂项

如果重启过于频繁会报 “uagent.service start request repeated too quickly, refusing to start.”

参考

systemd System and Service Manager,system daemon 官方网站。