背景

  • 不太适合进入docker,需要直接部署在宿主机上面,拥有root权限,操作修改系统,读取系统的一些信息等
  • 保证开机启动以及异常重启
  • 有一套完善的日志系统
  • 支持Before/After的启动机制
  • 支持基于cgroup的限制
  • 以最低的系统开销实现该目的
  • 方便打包为deb包进行安装,参考各种服务的安装,比如apt install nginx docker postgresqldeb包背后的逻辑与实现
  • 兼容基本全部的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-genericimg-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年)以后的系统大多都是采用SystemdUbuntu15开始使用systemd

为什么各大主流linux系统采用systemd

  • 改进启动项的效率,避免频繁的进程创建,内核/用户切换,为系统的启动和管理提供一套完整的解决方案
  • 利用 Dbus 进程间通讯与 socket 激活机制,解决任务启动时的依赖问题,实现启动并行化
  • 实现任务daemons的精确控制,使用内核的 cgroup 机制,不依赖 pid 来追踪进程,进程多次fork 之后生成的进程也不会脱离 systemd 的控制
  • 统一任务定义,用户不需要编写shell脚本,而是使用 systemd 制定的 unit 规则
  • systemd有两大设计理念,启动更少的服务,更多地并行启动服务,最终实现用户可以快速进入系统
  • systemd使用C编写,用于替换传统的脚本shell启动,shell中调用系统命令的操作会导致新进程的生成,比如awk, sed, grep等都会生成新进程,产生了很大的开销,systemd的运行开销比SysVUpStart小很多,速度也更快

systemd做了什么

解决启动项的依赖性

  • Socket依赖:例如服务A依赖服务Bsocket,但是服务B还未启动,所以systemd创建了一个临时socket用于接收服务A的请求与数据,当B真正起来的时候再把临时socket缓存的数据以及描述符替换为Bsocket
  • D-Bus(Desktop Bus)依赖:是一个用来在进程间通信的服务,该服务支持Bus Activation特性,即服务A要通过 D-Bus 服务和B通讯,但是B没有启动,那么 D-Bus 可以把B起来,在B启动的过程中,D-Bus 缓存数据,systemd使用这个特性并行启动AB
  • 文件系统依赖: 系统启动过程中,systemd 参考了 autofs 的设计思路,使得依赖文件系统的服务和文件系统本身初始化两者可以并发工作,监测到某个文件系统挂载点真正被访问到的时候才触发挂载操作

提供了一个系统和服务管理器

  • 利用 Linuxcgroups监视进程
  • 支持快照和系统恢复
  • 维护挂载点和自动挂载点
  • 各服务间基于依赖关系进行精密控制
  • 基于journald的服务日志管理
  • 控制基础系统配置
  • 维护登陆用户列表以及系统账户
  • 运行时目录和设置
  • 运行容器和虚拟机
  • 简单的管理网络配置
  • 网络时间同步
  • 日志转发
  • ...

systemd的管控范围已经远超作为系统启动项的角色,可以理解为已经大一统管理linux各项功能了,职责和最初的sysvupstart有所脱离,变得极其复杂庞大,这个也是最初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

systemdUnit单元的文件类型

系统初始化需要很多操作,比如启动后台服务,挂载文件系统,配置网络等,过程中的每一步都被 Systemd 抽象为一个配置单元,即 Unit,具体包含的类型如下

typename作用
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文件的组成

主要由三块UnitServiceInstall组成

具体版本或者全部字段解释执行如下命令查看

$ 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 之前启动
  • OnFailureUnit 启动失败时,自动启动列出的每个 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.servicesshd.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 PIDsystemctl 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

最后可以重启机器校验一下是否可以开机自启动

参考阅读

Linux 系统启动流程

Linux 的小伙伴 systemd 详解

linuxinit程序发展历史

Linux PID 1Systemd

Docker基础技术:Linux CGroup

archlinuxsystemd wiki文档

itwire于2014发表的 Linus Torvalds关于systemd的访谈

systemd的作者Lennert发表的关于PID 1的再思考

systemdunit配置


龚正阳
29 声望5 粉丝

粗犷型程序员