Skip to content

Latest commit

 

History

History
879 lines (660 loc) · 48.3 KB

File metadata and controls

879 lines (660 loc) · 48.3 KB

十四、从 BusyBox Runit 开始

在上一章中,我们研究了经典的 System Vinit和最先进的systemd程序。 我们还谈到了 BusyBox 的最小init程序。 现在,我们来看看 BusyBox 对runit程序的实现。 BusyBoxrunit在系统 Vinit的简单性和systemd的灵活性之间取得了合理的平衡。 出于这个原因,runit的完整版在流行的现代 Linux 发行版(如 void)中使用。 虽然systemd可能在云中占据主导地位,但对于许多嵌入式 Linux 系统来说, 通常是矫枉过正。 BusyBoxrunit提供了服务监控和专用服务日志记录等高级功能,没有systemd的复杂性和开销 。

在本章中,我将向您展示如何将系统划分为单独的 BusyBoxrunit服务,每个服务都有自己的目录和run脚本。 接下来,我们将了解如何使用check脚本强制某些服务等待其他服务启动。 然后,我们将向服务添加专用日志记录,并了解如何配置日志轮换。 最后,我们以一个服务通过写入命名管道向另一个服务发送信号的示例结束。 与 System Vinit不同,BusyBoxrunit服务是并行启动的,而不是顺序启动,这可以显著加快启动速度。 您对init计划的选择会对您的产品的行为和用户体验产生明显的影响。

在本章中,我们将介绍以下主题:

  • 获取 BusyBoxrunit
  • 创建服务目录和文件
  • 服务监管
  • 取决于其他服务
  • 专用服务日志记录
  • 发信号通知服务

技术要求

要按照示例操作,请确保您具备以下条件:

  • 一种基于 Linux 的主机系统
  • 适用于 Linux 的蚀刻器
  • 一种 microSD 卡读卡器和卡
  • USB 转 TTL 3.3V 串行电缆
  • 覆盆子派 4
  • 一种 5V 3A USB-C 电源

您应该已经为第 6 章选择构建系统安装了 Buildroot 的 2020.02.9 LTS 版本。 如果没有,请参考Buildroot 用户手册(https://buildroot.org/downloads/manual/manual.html)的系统要求部分,然后再按照第 6 章中的说明在您的 LINUX 主机上安装 Buildroot。

本章的所有代码都可以在本书的 GitHub 存储库的Chapter14文件夹中找到:https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition

获取 BusyBox Runit

要准备本章的系统,我们需要执行以下操作:

  1. 导航到为第 6 章选择生成系统

    $ cd buildroot

    克隆 Buildroot 的目录

  2. Check to see if runit is provided by BusyBox:

    $ grep Runit package/busybox/busybox.config
    # Runit Utilities

    在撰写本文时,BusyBoxrunit仍然是 Buildroot 2020.02.9LTS 版本中的一个可用选项。 如果在以后的版本中再也找不到 BusyBoxrunit,请恢复到该标记。

  3. Undo any changes and delete any untracked files or directories:

    $ make clean
    $ git checkout .
    $ git clean –-force -d

    请注意,git clean --force将删除 Nova U-Boot 修补程序以及我们在前面练习中添加到 Buildroot 的任何其他文件。

  4. 创建名为busybox-runit的新分支以捕获您的工作:

    $ git checkout -b busybox-runit
  5. 将 BusyBoxrunit添加到树莓 PI 4 的默认配置:

    $ cd configs
    $ cp raspberrypi4_64_defconfig rpi4_runit_defconfig
    $ cd ..
    $ cp package/busybox/busybox.config \ 
    board/raspberrypi/busybox-runit.config
    $ make rpi4_runit_defconfig
    $ make menuconfig
  6. From the main menu, drill down into the Toolchain | Toolchain type submenu and select External toolchain:

    Figure 14.1 – Selecting External toolchain

    图 14.1-选择外部工具链

  7. Back out one level and drill down into the Toolchain submenu. Select the Linaro AArch64 toolchain, then back out another level to return to the main menu:

    Figure 14.2 – Selecting the Linaro AArch64 toolchain

    图 14.2-选择 Linaro AArch64 工具链

  8. BusyBox 应该已经被选为init系统,但是您可以通过导航到System Configuration|Init System子菜单并观察到选择了BusyBox而不是systemVsystemd来确认这一点。 从Init System子菜单返回到主菜单。

  9. From the main menu, drill down into the Target packages | BusyBox configuration file to use? text field under BusyBox:

    Figure 14.3 – Selecting a BusyBox configuration file to use

    图 14.3-选择要使用的 BusyBox 配置文件

  10. Replace the package/busybox/busybox.config string value in that text field with board/raspberrypi/busybox-runit.config:

![Figure 14.4 – BusyBox configuration file to use?](img/B11566_14_04.jpg)

图 14.4-要使用的 BusyBox 配置文件?
  1. 如果询问是否保存新配置,请退出menuconfig的并选择。 默认情况下,Buildroot 将新配置保存到名为.config的文件中。
  2. 使用 BusyBox 配置的新位置更新configs/rpi4_runit_defconfig
```sh
$ make savedefconfig
```
  1. 现在让我们开始为runit
```sh
$ make busybox-menuconfig
```

配置 BusyBox
  1. Once inside busybox-menuconfig, you should notice a submenu called Runit Utilities. Drill down into that submenu and select all of the options on that menu page. The chpst, setuidgid, envuidgid, envdir, and softlimit utilities are command-line tools that are frequently referenced by service run scripts, so it is best to include them all. The svc and svok utilities are holdovers from daemontools so you can choose to opt out of them if you feel so inclined:
![Figure 14.5 – Runit Utilities](img/B11566_14_05.jpg)

图 14.5-Runit 实用程序
  1. Runit Utilities子菜单中,向下钻取到服务的默认目录文本字段。
  2. Enter /etc/sv in the Default directory for services text field:
![Figure 14.6 – Default directory for services](img/B11566_14_06.jpg)

图 14.6-服务的默认目录
  1. 当询问是否保存新配置时,退出busybox-menuconfig并选择。 与menuconfig选项类似,busybox-menuconfig只将新的 BusyBox 配置保存到输出目录中的.config文件。 在 Buildroot 的 2020.02.9 LTS 版本中,默认情况下,BusyBox 输出目录为output/build/busybox-1.31.1
  2. 将您的更改保存到board/raspberrypi/busybox-runit.config
```sh
$ make busybox-update-config
```
  1. BusyBox includes an inittab file for its init program in Buildroot's package/busybox directory. This configuration file instructs BusyBox init to start user space by mounting various filesystems and linking file descriptors to stdin, stdout, and stderr device nodes. In order for BusyBox init to transfer control to BusyBox runit, we need to replace the following lines in package/busybox/inittab:
```sh
# now run any rc scripts
::sysinit:/etc/init.d/rcS
```

这些行需要替换为它们的 BusyBox`runit`等效行:

```sh
# now switch over to runit
null::respawn:runsvdir /etc/sv
```
  1. We also need to remove the following lines from BusyBox's inittab:
```sh
# Stuff to do before rebooting
::shutdown:/etc/init.d/rcK
```

删除的`::shutdown`命令不需要替换行,因为 BusyBox`runit`会在重新启动之前自动终止其监控的进程。

现在您有了新的configs/rpi4_runit_defconfigboard/raspberrypi/busybox-runit.config文件以及修改后的package/busybox/inittab文件,您可以使用这些文件在 Raspberry PI 4 的自定义 Linux 映像上启用 BusyBoxrunit。将这三个文件提交到 Git,这样您的工作就不会丢失。

要构建自定义映像,请使用以下命令:

$ make rpi4_runit_defconfig
$ make

构建完成后,会将可引导映像写入outpimg/sdcard.img文件。 使用 Etcher 将此图像闪存到 microSD 卡上,将其插入 Raspberry PI 4,然后通电。 系统除了启动之外不会做太多事情,因为/etc/sv中还没有服务可供runsvdir启动。

要玩 BusyBoxrunit,请将串行电缆连接到您的 Raspberry PI 4 上,然后 以root身份登录,不需要密码。 我们没有向此映像添加connman,因此输入 /sbin/ifup -a以打开以太网接口:

# /sbin/ifup -a
[  187.076662] bcmgenet: Skipping UMAC reset
[  187.151919] bcmgenet fd580000.genet: configuring instance for external RGMII (no delay)
udhcpc: started, v1.31.1
udhcpc: sending discover
[  188.191465] bcmgenet fd580000.genet eth0: Link is Down
udhcpc: sending discover
[  192.287490] bcmgenet fd580000.genet eth0: Link is Up - 1Gbps/Full - flow control rx/tx
udhcpc: sending discover
udhcpc: sending select for 192.168.1.130
udhcpc: lease of 192.168.1.130 obtained, lease time 86400
deleting routers
adding dns 192.168.1.254

我们将在下一节中研究runit服务目录的结构和布局。

创建服务目录和文件

runit是重新实施daemontools过程监督工具包。 它是由 Gerrit Pape 创建的,作为 System Vinit和其他 Unixinit方案的替代品。 在撰写本文时,runit上最好的两个信息来源是 Pape 的网站(http://smarden.org/runit/)和 void Linux 的在线文档。

BusyBox 的runit实现与标准runit的主要区别在于 自文档化。 例如,sv --help没有提到sv实用程序的startcheck选项,实际上 BusyBox 的实现支持这些选项。 BusyBoxrunit的源代码可以在 BusyBox 的output/build/busybox-1.31.1/runit目录中找到。 您还可以在https://git.busybox.net/busybox/tree/runit在线浏览 BusyBoxrunit源代码的最新版本。 如果 BusyBox 的runit实现中有任何错误或功能缺失,您可以通过修补 Buildroot 的busybox包来修复或添加它们。

Arch Linux 发行版支持同时使用 BusyBoxrunitsystemd进行简单的进程监控。 你可以在 Arch Linux Wiki 上阅读更多关于如何做到这一点的内容。 BusyBox 默认为init,并且没有记录将 BusyBoxinit替换为runit的步骤。 出于这些原因,我不会将 BusyBoxinit替换为runit,而是将向您展示如何使用 BusyBoxrunit向 BusyBoxinit添加服务监管。

\t0 抯服务布局目录

以下是runit上的 void Linux 发行版原始文档(现已弃用)中的引语:

服务目录只需要一个文件,即名为run的可执行文件,该文件预计会exec前台的一个进程。

除了必需的run脚本外,runit服务目录还可以包含finish脚本、check脚本和conf文件。 finish脚本在服务关闭或进程停止时运行。 run脚本创建一个conf文件,以便在run内部使用任何环境变量之前对其进行设置。

与 BusyBoxinit``/etc/init.d目录一样,/etc/sv目录通常是存储runit服务的位置。 以下是一个简单的嵌入式 Linux 系统的 BusyBoxinit脚本列表:

$ ls output/target/etc/init.d
S01syslogd  S02sysctl   S21haveged  S45connman  S50sshd  rcS
S02klogd    S20urandom  S30dbus     S49ntp      rcK

Buildroot 将这些 BusyBoxinit脚本作为各种守护进程的包的一部分提供。 对于 BusyBoxrunit,我们必须自己生成这些启动脚本。

下面是同一系统的 BusyBoxrunit服务列表:

$ ls -D output/target/etc/sv
bluetoothd  dbus   haveged  ntpd  syslogd
connmand    dcron  klogd    sshd  watchdog

每个 BusyBoxrunit服务都有自己的目录,其中包含一个可执行的run脚本。 也在目标映像上的 BusyBoxinit脚本将不会在启动时运行,因为我们从inittab中删除了::sysinit:/etc/init.d/rcS。 与init脚本不同,要使用runitrun脚本需要在前台运行,而不是在后台运行。

Void Linux 发行版是runit服务文件的宝库。 以下是sshd的 voidrun脚本:

#!/bin/sh
# Will generate host keys if they don't already exist
ssh-keygen -A >/dev/null 2>&1
[ -r conf ] && . ./conf
exec /usr/sbin/sshd -D $OPTS 2>&1

runsvdir实用程序启动并监控在 /etc/sv目录下定义的服务集合。 因此,需要将sshdrun脚本安装为 /etc/sv/sshd/run,以便runsvdir可以在启动时找到它。 它还必须是可执行的,否则 BusyBoxrunit将无法启动它。

/etc/sv/sshd/run的内容与 Buildroot 的 /etc/init.d/S50sshd的摘录进行对比:

start() {
      # Create any missing keys
      /usr/bin/ssh-keygen -A
      printf "Starting sshd: "
      /usr/sbin/sshd
      touch /var/lock/sshd
      echo "OK"
}

sshd默认在后台运行。 -D选项强制sshd在前台运行。 runit希望您在run脚本中以exec作为前台命令的前缀。 exec命令替换当前进程中的当前程序。 最终结果是,从/etc/sv/sshd开始的./run进程在没有分叉的情况下变成了/usr/sbin/sshd -D进程:

# ps aux | grep "[s]shd"
  201 root     runsv sshd
  209 root     /usr/sbin/sshd -D

请注意,sshd``run脚本为$OPTS环境变量提供了一个conf文件。 如果/etc/sv/sshd内不存在conf文件,则$OPTS未定义且为空,在这种情况下正好可以。 与大多数runit服务一样,在系统关闭或重新启动之前,sshd不需要finish脚本来释放任何资源。

服务配置

Buildroot 在其包中包含的init脚本是 BusyBoxinit脚本。 这些init脚本需要移植到 BusyBoxrunit,并安装到output/target/etc/sv下的不同目录。 与单独修补每个包相比,我发现将所有服务文件捆绑在 Buildroot 树外部的rootfs覆盖包或伞包中更容易。 Buildroot 通过BR2_EXTERNAL``make变量启用树外定制,该变量指向包含定制的目录。

将 Buildroot 放入br2-external树的最常见方法是将其作为子模块嵌入到 Git 存储库的顶层:

$ cat .gitmodules 
[submodule "buildroot"]
     path = buildroot
     url = git://git.buildroot.net/buildroot
     ignore = dirty
     branch = 15a05e6d5a875759d217d61b3c7b31ec87ea4eb5

将 Buildroot 作为子模块嵌入可以简化对您添加到 Buildroot 的任何包或应用到 Buildroot 的补丁的维护。 该子模块被固定在一个标记上,以便在故意升级 Buildroot 之前,任何树外定制都保持稳定。 请注意,上面buildroot子模块的提交散列被固定到该 Buildroot LTS 版本的2020.02.9标记上:

$ cd buildroot
$ git show --summary
commit 15a05e6d5a875759d217d61b3c7b31ec87ea4eb5 (HEAD -> busybox-runit, tag: 2020.02.9)
Author: Peter Korsgaard <[email protected]>
Date:   Sun Dec 27 17:55:12 2020 +0100
    Update for 2020.02.9

    Signed-off-by: Peter Korsgaard <[email protected]>

buildroot是父BR2_EXTERNAL目录的子目录时,要运行make,我们需要传递一些额外的参数:

$ make -C $(pwd)/buildroot BR2_EXTERNAL=$(pwd) O=$(pwd)/output

以下是 Buildroot 为br2-external树推荐的目录结构:

+-- board/
|   +-- <company>/
|       +-- <boardname>/
|           +-- linux.config
|           +-- busybox.config
|           +-- <other configuration files>
|           +-- post_build.sh
|           +-- post_image.sh
|           +-- rootfs_overlay/
|           |   +-- etc/
|           |   +-- <some file>
|           +-- patches/
|               +-- foo/
|               |   +-- <some patch>
|               +-- libbar/
|                   +-- <some other patches>
+-- configs/
|   +-- <boardname>_defconfig
+-- package/
|   +-- <company>/
|       +-- package1/
|       |    +-- Config.in
|       |    +-- package1.mk
|       +-- package2/
|           +-- Config.in
|           +-- package2.mk
+-- Config.in
+-- external.mk
+-- external.desc

请注意,您在上一节中创建的自定义rpi4_runit_defconfigbusybox-runit.config 文件将插入到此树中。 根据 Buildroot 的指导方针,这两个配置应该是特定于电路板的文件。 <boardname>_defconfig以正在配置映像的电路板的名称作为前缀。 busybox.config放在相应的board/<company>/<boardname>目录中。 还要注意,定制 BusyBoxinittab所在的rootfs_overlay/etc目录也是特定于主板的。

由于 BusyBoxrunit的所有服务配置文件都驻留在/etc/sv中,因此将它们全部提交到特定于电路板的rootfs覆盖似乎是合理的。 根据我的经验,这个解决方案很快就会变得过于僵化。 通常需要为同一电路板配置多个映像。 例如,消费设备可以具有单独的开发、生产和制造图像。 每个镜像都包含不同的服务,因此不同镜像之间的配置需要不同。 出于这些原因,服务配置最好在软件包级别进行,而不是在主板级别。 我使用树外伞包 (每种图像类型一个包)来配置 BusyBoxrunit的服务。

在顶层,br2-external树必须包含external.descexternal.mkConfig.in文件。 external.desc文件包含一些描述br2-external树的基本元数据:

$ cat external.desc
name: ACME
desc: Acme's external Buildroot tree

Buildroot 将BR2_EXTERNAL_<name>_PATH变量设置为br2-external树的绝对路径,以便变量可以在 Kconfig 和 Make 文件中引用。 desc字段是作为BR2_EXTERNAL_<name>_DESC变量提供的可选描述。 根据本external.desc<name>替换为ACMEexternal.mk文件通常只包含引用在external.desc中定义的BR2_EXTERNAL_<name>_PATH变量的一行:

$ cat external.mk 
include $(sort $(wildcard $(BR2_EXTERNAL_ACME_PATH)/package/acme/*/*.mk))

include行告诉 Buildroot 在哪里搜索外部包.mk文件。 外部软件包的相应Config.in文件的位置在br2-external树的顶级Config.in文件中定义:

$ cat Config.in 
source "$BR2_EXTERNAL_ACME_PATH/package/acme/development/Config.in"
source "$BR2_EXTERNAL_ACME_PATH/package/acme/manufacturing/Config.in"
source "$BR2_EXTERNAL_ACME_PATH/package/acme/production/Config.in"

Buildroot 读取br2-external树的Config.in文件,并将其中包含的包配方添加到顶级配置菜单。 让我们用developmentmanufacturingproduction伞包填充 Buildroot 的br2-external树结构的其余部分:

├── configs
│   ├── supergizmo_development_defconfig
│   ├── supergizmo_manufacturing_defconfig
│   └── supergizmo_production_defconfig
└── package
    └── acme
        ├── development
        │   ├── Config.in
        │   ├── development.mk
        │   ├── haveged_run
        │   ├── inittab
        │   ├── ntpd.etc.conf
        │   ├── sshd_config
        │   ├── sshd_run
        │   └── user-tables.txt
        ├── manufacturing
        │   ├── apply-squash-update
        │   ├── Config.in
        │   ├── haveged_run
        │   ├── inittab
        │   ├── manufacturing.mk
        │   ├── mfg-profile
        │   ├── sshd_config
        │   ├── sshd_run
        │   ├── test-button
        │   ├── test-fan
        │   ├── test-gps
        │   ├── test-led
        │   └── user-tables.txt
        └── production
            ├── Config.in
            ├── dcron-root
            ├── download-apply-update
            ├── inittab
            ├── ntpd.etc.conf
            ├── ota.acme.systems.crt
            ├── production.mk
            └── user-tables.txt

如果将此目录树与 Buildroot 的前一个目录树进行比较,可以看到<boardname>已替换为supergizmo<company>已替换为acme。 您可以将伞包看作是覆盖在一些常见基本图像之上的图像。 这样,所有三个映像都可以共享相同的 U-Boot、内核和驱动程序,因此它们的更改仅适用于用户空间。

考虑需要在设备的开发映像中包含哪些包才能使其有效。 至少,开发人员希望能够ssh进入设备,使用sudo执行命令,并使用vim编辑板载文件。 此外,他们希望能够使用stracegdbperf等工具跟踪、调试和分析他们的程序。 出于安全原因,这些软件都不属于设备的生产映像。

Config.in对于development伞形软件包,选择仅应部署到内部开发人员的试生产硬件的软件包:

$ cat package/acme/development/Config.in 
config BR2_PACKAGE_DEVELOPMENT
     bool "development"
     select BR2_PACKAGE_HAVEGED
     select BR2_PACKAGE_OPENSSH
     select BR2_PACKAGE_SUDO
     select BR2_PACKAGE_TMUX
     select BR2_PACKAGE_VIM
     select BR2_PACKAGE_STRACE
     select BR2_PACKAGE_LINUX_TOOLS_PERF
     select BR2_PACKAGE_GDB
     select BR2_PACKAGE_GDB_SERVER
     select BR2_PACKAGE_GDB_DEBUGGER
     select BR2_PACKAGE_GDB_TUI
     help
       The development image overlay for Acme's SuperGizmo.

在软件包构建过程的安装步骤期间,不同的服务脚本和配置文件将写入output/target目录。 以下是package/acme/development/development.mk的相关摘录:

define DEVELOPMENT_INSTALL_TARGET_CMDS
     $(INSTALL) -D -m 0644 $(@D)/inittab $(TARGET_DIR)/etc/inittab
     $(INSTALL) -D -m 0755 $(@D)/haveged_run $(TARGET_DIR)/etc/sv/haveged/run
     $(INSTALL) -D -m 0755 $(@D)/sshd_run $(TARGET_DIR)/etc/sv/sshd/run
     $(INSTALL) -D -m 0644 $(@D)/sshd_config $(TARGET_DIR)/etc/ssh/sshd_config
endef

Buildroot<package>.mk文件包含<package>_BUILD_CMDS<package>_INSTALL_TARGET_CMDS节。 这个伞形软件包被命名为development,因此它的安装宏被定义为DEVELOPMENT_INSTALL_TARGET_CMDS。 前缀<package>需要与包的Config.in文件的config BR2_<package>行中的<package>后缀匹配,否则宏名称将导致包生成错误。

haveged/runsshd/run脚本安装到目标系统上的/etc/sv目录。 启动runsvdir所需的自定义inittab安装到目标上的/etc。 除非这些文件安装在具有预期权限的正确位置,否则 BusyBoxrunit无法启动havegedsshd服务。

haveged是一个软件随机数生成器,旨在缓解 Linux/dev/random设备中的低熵条件。 低熵条件可能会阻止sshd启动,因为 SSH 协议严重依赖随机数。 一些较新的 SoC 可能还没有对其硬件随机数生成器提供内核支持。 如果不在这些系统上运行havegedsshd可能需要几分钟时间才能在引导后开始接受连接。

在 BusyBoxrunit下运行haveged非常简单:

$ cat package/acme/development/haveged_run 
#!/bin/sh
exec /usr/sbin/haveged -w 1024 -r 0 -F

productionmanufacturing伞包将不同的包和服务集合叠加到图像上。 production映像包括用于下载和应用软件更新的工具。 manufacturing映像包括工厂技术人员用于配置和测试硬件的工具。 BusyBoxrunit也非常适合这两种用例。

Config.in对于,production伞式软件包选择定期无线软件更新所需的软件包:

$ cat package/acme/production/Config.in 
config BR2_PACKAGE_PRODUCTION
     bool "production"
     select BR2_PACKAGE_DCRON
     select BR2_PACKAGE_LIBCURL
     select BR2_PACKAGE_LIBCURL_CURL
     select BR2_PACKAGE_LIBCURL_VERBOSE
     select BR2_PACKAGE_JQ
     help
       The production image overlay for Acme's SuperGizmo.

在开发和制造环境中,强制 OTA 更新通常是不可取的,因此这些软件包被排除在这些映像之外。 production映像包括一个download-apply-update脚本,该脚本使用curl向 OTA 服务器查询最新可用的软件更新。 板载还包括公共 SSL 证书,因此curl可以验证 OTA 服务器的真实性。 dcron守护进程被配置为每 10 到 20 分钟运行一次download-apply-update,并带有一些噪音,以避免蜂拥而至的人群。 如果有较新的更新可用,则脚本将下载映像,验证它,并在重新引导之前将其应用于 microSD 卡。 以下是package/acme/production/production.mk的相关摘录:

define PRODUCTION_INSTALL_TARGET_CMDS
     $(INSTALL) -D -m 0644 $(@D)/inittab $(TARGET_DIR)/etc/inittab
     $(INSTALL) -D -m 0644 $(@D)/dcron-root $(TARGET_DIR)/etc/cron.d/root
     $(INSTALL) -D -m 0775 $(@D)/download-apply-update $(TARGET_DIR)/usr/sbin/download-apply-update
     $(INSTALL) -D -m 0644 $(@D)/ota.acme.com.crt $(TARGET_DIR)/etc/ssl/certs/ota.acme.com.crt
     $(INSTALL) -D -m 0644 $(@D)/ntpd.etc.conf $(TARGET_DIR)/etc/ntp.conf
endef

production映像cd构建到br2-external树的根,并发出以下命令:

$ make clean
$ make supergizmo_production_defconfig
$ make

为 Acme SuperGizmo 构建developmentmanufacturing映像的步骤仅在选择defconfig时有所不同。 除了最后一行之外,这三种定义配置几乎相同,根据所选图像的不同,该行可以是BR2_PACKAGE_DEVELOPMENT=yBR2_PACKAGE_PRODUCTION=yBR2_PACKAGE_MANUFACTURING=y。 这三个伞包是相互排斥的,因此不要选择多个伞包包含在同一图像中,否则可能会遇到意想不到的结果。

服务监督

一旦我们在/etc/sv下使用run脚本创建了服务目录,并确保 BusyBoxinit启动runsvdir,BusyBoxrunit将处理所有剩余的工作。 这包括启动、停止、监视和重新启动其控制下的所有服务。 runsvdir实用程序为每个服务目录启动runsv进程,如果终止则重新启动runsv进程。 因为run脚本在前台运行各自的后台进程,所以runsv希望run阻塞,以便在run退出时,runsv会自动重新启动它。

系统启动期间需要服务自动重新启动,因为run脚本可能会崩溃。 在 BusyBoxrunit中尤其如此,在 BusyBoxrunit中,服务几乎同时启动,而不是一个接一个地启动。 例如,当依赖服务或基本系统资源(如 GPIO 或设备驱动程序)尚不可用时,服务可能无法启动。 在下一节中,我将向您展示如何表达服务之间的依赖关系,以便您的系统启动序列保持确定性。

下面是在我们的简单嵌入式 Linux 系统上运行的runsv进程:

# ps aux | grep "[r]unsv"
  177 root     runsvdir /etc/sv
  179 root     runsv ntpd
  180 root     runsv haveged
  181 root     runsv syslogd
  182 root     runsv dcron
  185 root     runsv dbus
  187 root     runsv bluetoothd
  192 root     runsv watchdog
  195 root     runsv connmand
  199 root     runsv sshd
  202 root     runsv klogd

请注意,inittab中的runsvdir /etc/sv命令直到 PID 177 才会执行。 PID 为 1 的进程是/sbin/init,它只是一个指向/bin/busybox的符号链接。 PID 2 到 176(未显示)都是内核线程和系统服务,因此它们的命令在由ps显示时会出现在方括号内。 方括号表示进程没有与之关联的实际命令行。 由于connmandbluetoothd都依赖于 D-BUS 启动,因此在 D-BUS 启动和运行之前,runsv可能已多次重新启动任一服务:

# pstree -a
init
  |-getty -L 115200 ttyS0
  |-hciattach /dev/ttyAMA0 bcm43xx 921600 flow - 60:81:f9:b0:8a:02
  |-runsvdir /etc/sv
  |   |-runsv ntpd
  |   |   `-ntpd -u ntp -c /etc/ntp.conf -U 60 -g -n
  |   |       `-{ntpd}
  |   |-runsv haveged
  |   |   `-haveged -w 1024 -r 0 -F
  |   |-runsv syslogd
  |   |   `-syslogd -n -O /var/data/log/messages -b 99 -s 1000
  |   |-runsv dcron
  |   |-runsv dbus
  |   |   `-dbus-daemon --system --nofork --nopidfile --syslog-only
  |   |-runsv bluetoothd
  |   |   `-bluetoothd -E --noplugin=* -n
  |   |-runsv watchdog
  |   |   `-watchdog -T 10 -F /dev/watchdog
  |   |-runsv connmand
  |   |   `-connmand -n
  |   |-runsv sshd
  |   |   `-sshd -D
  |   `-runsv klogd
  |       `-klogd -n
  `-wpa_supplicant -u

有些服务需要连接到互联网才能启动。 由于 DHCP 的异步特性,这可能会使服务启动延迟数秒。 由于connmand管理该系统上的所有网络接口,因此这些服务又依赖于connmand。 如果设备的 IP 地址因网络间切换或续订 DHCP 租约而更改,则可能需要重新启动许多相同的服务。 幸运的是,BusyBoxrunit提供了一种从命令行轻松重启服务的方法。

控制服务

BusyBoxrunit提供了用于管理和检查服务的sv命令行工具:

# sv --help
BusyBox v1.31.1 () multi-call binary.
Usage: sv [-v] [-w SEC] CMD SERVICE_DIR...
Control services monitored by runsv supervisor.
Commands (only first character is enough):
status: query service status
up: if service isn't running, start it. If service stops, restart it
once: like 'up', but if service stops, don't restart it
down: send TERM and CONT signals. If ./run exits, start ./finish
     if it exists. After it stops, don't restart service
exit: send TERM and CONT signals to service and log service. If they exit,
     runsv exits too
pause, cont, hup, alarm, interrupt, quit, 1, 2, term, kill: send
STOP, CONT, HUP, ALRM, INT, QUIT, USR1, USR2, TERM, KILL signal to service

sv的帮助消息解释了uponcedownexit命令的作用。 它还说明了pauseconthupalarminterruptquit12termkill命令如何直接映射到 POSIX 信号。 请注意,每个命令的第一个字符足以调用它。

让我们使用ntpd作为目标服务来试验各种sv命令。 您的状态时间与我的不同,具体取决于您在两个命令之间等待的时间:

  1. Restart the ntpd service:

    # sv t /etc/sv/ntpd
    # sv s /etc/sv/ntpd
    run: /etc/sv/ntpd: (pid 1669) 6s

    sv t命令重新启动服务,sv s命令获取其状态。 tterm的缩写,因此sv t在重新启动服务之前发送服务TERM信号。 状态消息显示ntpd在重启后已经运行了 6 秒。

  2. Now let's see what happens to the status when we use sv d to stop the ntpd service:

    # sv d /etc/sv/ntpd
    # sv s /etc/sv/ntpd
    down: /etc/sv/ntpd: 7s, normally up

    这一次,状态消息显示ntpd自停止以来已停机 7 秒。

  3. Start the ntpd service back up:

    # sv u /etc/sv/ntpd
    # sv s /etc/sv/ntpd
    run: /etc/sv/ntpd: (pid 2756) 5s

    STATUS 消息现在显示ntpd自启动以来已经运行了 5 秒。 请注意,PID 比以前更高,因为系统自ntpd重新启动以来已经运行了一段时间。

  4. Do a one-off start of ntpd:

    # sv o /etc/sv/ntpd
    # sv s /etc/sv/ntpd
    run: /etc/sv/ntpd: (pid 3795) 3s, want down

    sv o命令类似于sv u,不同之处在于目标服务停止后不会再次重新启动。 您可以通过使用sv k /etc/sv/ntpdntpd服务发送KILL信号并观察到ntpd服务关闭并保持关闭来确认这一点。

下面是我们介绍的sv命令的详细形式:

# sv term /etc/sv/ntpd
# sv status /etc/sv/ntpd
# sv down /etc/sv/ntpd
# sv up /etc/sv/ntpd
# sv once /etc/sv/ntpd

如果服务需要条件错误或信号处理,您可以在finish脚本中定义该逻辑。 Servicefinish脚本是可选的,只要run退出就会执行。 finish脚本有两个参数:$1(来自run的退出代码)和$2(由waitpid系统调用确定的退出状态的最低有效字节)。 当run正常退出时,run的退出代码为 0,当run异常退出时,退出代码为-1。 当run正常退出时,状态字节为 0,当run被信号终止时,状态字节为 0。 如果runsv无法启动run,则退出代码为 1,状态字节为 0。

检测到 IP 地址更改的服务可以通过向sv t发出命令来重新启动网络服务。 这与ifplugd执行的操作类似,不同之处在于ifplugd在以太网链路状态而不是 IP 地址更改时触发。 这样的服务可以像 shell 脚本一样简单,它由持续轮询所有网络接口的单个while循环组成。 您还可以从runfinish脚本发出sv命令,作为服务之间通信的一种方式。 在下一节中,我将向您展示如何做到这一点。

取决于其他服务

我提到了像connmandbluetoothd这样的一些服务需要 D-Bus。 D-BUS 是支持发布-订阅进程间通信的消息系统总线。 D-BUS 的 Buildroot 软件包提供了一个系统dbus-daemon和一个参考libdbus库。 libdbus库实现了低级 D-Bus C API,但是其他语言(如 Python)存在到libdbus的高级绑定。 一些语言还提供了完全不依赖于libdbus的 D-BUS 协议的替代实现。 D-BUS 服务(如connmandbluetoothd)期望系统dbus-daemon在它们可以启动之前已经在运行。

启动依赖项

官方的runit文档建议使用sv start来表示对runit控制下的其他服务的依赖关系。 为确保 D-BUS 在connmand开始之前可用,您应该相应地定义您的/etc/sv/connmand/run

#!/bin/sh
/bin/sv start /etc/sv/dbus > /dev/null || exit 1
exec /usr/sbin/connmand -n

sv start /etc/sv/dbus如果系统dbus-daemon尚未运行,则尝试启动系统dbus-daemonsv start命令与sv up类似,不同之处在于它将等待-w参数或SVWAIT环境变量指定的秒数来启动服务。 未定义-w参数或SVWAIT环境变量时,默认的最长等待时间为 7 秒。 如果服务已经启动,则返回退出代码 0 表示成功。 退出代码 1 表示故障,导致/etc/sv/connmand/run在没有启动connmand的情况下提前退出。 监视connmandrunsv进程将继续尝试启动该服务,直到最终成功。

下面是我们对应的/etc/sv/dbus/run,它是我从 void 派生的:

#!/bin/sh
[ ! -d /var/run/dbus ] && /bin/install -m755 -g 22 -o 22 -d /var/run/dbus
[ -d /tmp/dbus ] || /bin/mkdir -p /tmp/dbus
exec /bin/dbus-daemon --system --nofork --nopidfile --syslog-only

与 Buildroot 的/etc/init.d/S30dbus中的以下摘录形成对比:

# Create needed directories.
[ -d /var/run/dbus ] || mkdir -p /var/run/dbus
[ -d /var/lock/subsys ] || mkdir -p /var/lock/subsys
[ -d /tmp/dbus ] || mkdir -p /tmp/dbus
RETVAL = 0
start() {
    printf "Starting system message bus: "
    dbus-uuidgen --ensure
    dbus-daemon --system
    RETVAL=$?
    echo "done"
    [ $RETVAL -eq 0 ] && touch /var/lock/subsys/dbus-daemon
}
stop() {
    printf "Stopping system message bus: "
    ## we don't want to kill all the per-user $processname, we want
    ## to use the pid file *only*; because we use the fake nonexistent 
    ## program name "$servicename" that should be safe-ish
    killall dbus-daemon
    RETVAL=$?
    echo "done"
    if [ $RETVAL -eq 0 ]; then
        rm -f /var/lock/subsys/dbus-daemon
        rm -f /var/run/messagebus.pid
    fi
}

请注意,Buildroot 版本的 D-BUS 服务脚本要复杂得多。 因为runit在前台运行dbus-daemon,所以不需要lockpid文件以及与这些文件相关联的所有仪式。 您可能会认为前面的stop()函数是一个好的finish脚本,除了在runit的情况下,没有要删除的dbus-daemon或者要删除的pidlock文件。 服务finish脚本在runit中是可选的,因此它们应该仅保留用于有意义的工作。

自定义启动依赖项

如果/etc/sv/dbus目录中存在checksv会运行此脚本来检查服务是否可用。 如果check以 0 退出,则认为服务可用。 check机制使您能够表达除正在运行的进程之外的服务可用的附加后置条件。 例如,仅仅因为connmand已经启动并不意味着一定已经建立了到互联网的连接。 check脚本确保一个服务在其他服务可以启动之前完成其预期的操作。

要验证 Wi-Fi 是否打开,您可以定义以下check

#!/bin/sh
WIFI_STATE=$(cat /sys/class/net/wlan0/operstate)
"$WIFI_STATE" = "up" || exit 1
exit 0

通过将前面的脚本安装到/etc/sv/connmand/check,您需要 Wi-Fi 才能启动connmand服务。 这样,当您发出sv start /etc/sv/connmand时,该命令仅在 Wi-Fi 接口打开时返回退出代码 0,即使connmand正在运行。

您可以使用sv check命令在不启动服务的情况下执行check脚本。 与sv start类似,如果服务目录中存在checksv会运行此脚本来确定服务是否可用。 如果check以 0 退出,则认为服务可用。 sv将等待最多 7 秒,直到check返回退出代码 0。 与sv start不同,如果check返回非零退出代码,sv不会尝试启动服务。

把这一切放在一起

我们已经看到了sv startcheck机制如何使我们能够表达服务之间的启动依赖关系。 将这些功能与finish脚本相结合使我们能够构建进程监督树。 例如,充当父进程的服务可以在停止时调用sv down来关闭其依赖子服务。 我认为,正是这种高级定制让 BusyBoxrunit变得如此强大。 您只需使用简单、定义良好的 shell 脚本就可以将系统调整为您想要的行为方式。 要了解有关监督树的更多信息,我推荐有关 Erlang 容错的文献。

专用服务日志记录

专用服务记录器只记录来自单个守护进程的输出。 专用日志记录很好,因为不同服务的诊断数据分布在单独的日志文件中。 集中式系统记录器(如syslogd)生成的单一日志文件通常很难清理。 这两种形式的日志记录都有各自的用途:专用日志记录在可读性方面表现出色,而集中式日志记录提供了上下文。 您的服务可以每个都有自己的专用记录器,并且仍然写入syslog,因此您可以两者都不牺牲。

它是如何工作的?

因为服务run脚本在前台运行,所以向服务添加专用记录器只涉及将标准输出从服务的run重定向到日志文件。 通过在目标服务目录中创建一个log子目录,并在其中包含另一个run脚本,可以启用专用服务日志记录。 这个额外的run用于服务的记录器,而不是服务本身。 当该log目录存在时,打开从服务目录中的run进程的输出到log目录中的run进程的输入的管道。

以下是sshd的可能服务目录布局:

# tree etc/sv/sshd
etc/sv/sshd
|-- finish
|-- log
|   `-- run
`-- run

更准确地说,当 BusyBoxrunit``runsv进程遇到此服务目录布局时,它除了在必要时启动sshd/runsshd/finish之外,还会执行几项操作:

  1. 创建管道
  2. 将标准出站从runfinish重定向到管道
  3. 切换到log目录
  4. 开始log/run
  5. log/run的标准输入重定向为从管道读取

runsv启动和监控sshd/log/run,就像它启动和监控sshd/run一样。 一旦为sshd添加了一个记录器,您会注意到sv d /etc/sv/sshd只停止sshd。 要停止记录器,除非将该命令添加到/etc/sv/sshd/finish脚本,否则必须输入sv d /etc/sv/sshd/log

向服务添加专用日志记录

BusyBoxrunit提供了一个svlogd日志记录守护进程,以在您的log/run脚本中使用:

# svlogd --help
BusyBox v1.31.1 () multi-call binary.
Usage: svlogd [-tttv] [-r C] [-R CHARS] [-l MATCHLEN] [-b BUFLEN] DIR...
Read log data from stdin and write to rotated log files in DIRs
-r C       Replace non-printable characters with C
-R CHARS   Also replace CHARS with C (default _)
-t         Timestamp with @tai64n
-tt        Timestamp with yyyy-mm-dd_hh:mm:ss.sssss
-ttt       Timestamp with yyyy-mm-ddThh:mm:ss.sssss
-v         Verbose

请注意,svlogd需要一个或多个DIR输出目录路径作为参数。

要向现有 BusyBoxrunit服务添加专用日志记录,请执行以下操作:

  1. 在服务目录内创建log子目录。
  2. log子目录中创建一个run脚本。
  3. 使该run脚本可执行。
  4. 使用execrun内部运行svlogd

以下是 void 的/etc/sv/sshd/log/run脚本:

#!/bin/sh
[ -d /var/log/sshd ] || mkdir -p /var/log/sshd
exec chpst -u root:adm svlogd -t /var/log/sshd

由于svlogd会将sshd日志文件写入/var/log/sshd,因此如果该目录尚不存在,我们首先需要创建该目录。 要使sshd日志文件持久存在,您可能需要修改inittab,以便在启动runsvdir之前,在引导时将/var挂载到可写闪存分区。 execchpst -u root:adm部分确保svlogdroot用户和adm组特权和权限运行。

-t选项为写入日志文件的每一行添加一个 TAI64N 格式的时间戳前缀。 虽然 TAI64N 时间戳是精确的,但它们不是最易读的。 svlogd提供的其他时间戳选项是-tt-ttt。 一些守护进程编写自己的时间戳以进行标准输出。 要避免编写带有令人困惑的双时间戳的行,只需从log/run``svlogd命令中省略-t或其任何变体即可。

您可能会想要向klogdsyslogd服务添加专用记录器。 抵制住这种诱惑。 klogdsyslogd是系统范围的日志记录守护进程, 它们都非常擅长它们的工作。 除非 记录器出现故障并且您需要对其进行调试,否则记录该记录器的操作实际上是没有意义的。 如果您开发的服务同时记录到stdoutsyslog,请确保从syslog消息文本中排除时间戳。 syslog协议包括一个timestamp字段,用于嵌入时间戳。

每个专用记录器都在其自己的单独进程中运行。 在设计嵌入式系统时,需要考虑支持这些额外的记录器进程所需的额外开销。 如果您打算使用 BusyBoxrunit在资源受限的系统上监控大量服务,请选择要向哪些服务添加专用日志记录,否则响应能力可能会受到影响。

原木轮换

svlogd使用默认的 10 个日志文件自动旋转日志文件,每个日志文件的大小不超过 100 万字节。 回想一下,这些轮换的日志文件被写出到一个或多个DIR输出目录中,路径作为参数传递到svlogd。 当然,这些轮换设置是可配置的,但在我开始之前,让我解释一下日志轮换是如何工作的。

让我们假设svlogd不知何故知道名为NUMSIZE的两个值。 NUM是要保留的日志文件数。 SIZE是日志文件的最大大小。 svlogd将日志消息追加到名为current的日志文件中。 当current的大小达到SIZE字节时,则svlogd旋转current

要旋转current文件,svlogd将执行以下操作:

  1. 关闭current日志文件。
  2. current设为只读。
  3. current重命名为@<timestamp>.s
  4. 创建新的current日志文件并开始写入。
  5. 统计除current之外的现有日志文件数。
  6. 如果count等于或超过NUM,则删除最旧的日志文件。

<timestamp>用于重命名要轮换的current日志文件是文件轮换时的时间戳,而不是创建时的时间戳。

现在观察这里对SIZENUMPATTERN的描述:

# svlogd --help 
BusyBox v1.31.1 () multi-call binary.
[Usage not shown]
DIR/config file modifies behavior:
sSIZE - when to rotate logs (default 1000000, 0 disables)
nNUM - number of files to retain
!PROG - process rotated log with PROG
+,-PATTERN - (de)select line for logging
E,ePATTERN - (de)select line for stderr

这些设置从DIR/config文件(如果存在)中读取。 请注意,SIZE为 0 将禁用日志轮换,并且不是默认设置。 下面是一个DIR/config文件,它使svlogd最多保留 100 个日志文件,每个文件的大小最大为 9999,999 字节,总共大约有 1 GB 的循环日志写入到一个输出目录中:

s9999999
n100

如果将多个DIR输出目录传递给svlogd,则svlogd会记录所有这些目录。 为什么要将相同的消息记录到多个目录? 答案是您将相同的消息记录到多个目录。 由于每个输出目录都有自己的config文件,因此您可以使用模式匹配来选择要将哪些消息记录到哪个输出目录。

假设长度为NPATTERN,如果DIR/config中的一行以+-Ee开头,则svlogd会相应地将每个日志消息的前N字符与PATTERN进行匹配。 +-前缀适用于currentEe前缀适用于标准误差。 +PATTERN选择并-PATTERN过滤出要记录到current的匹配行。 EPATTERN选择并ePATTERN过滤出匹配的行,以提醒标准错误。

发信号通知服务

在前面的启动依赖关系部分中,我展示了如何使用sv命令行工具控制服务。 稍后,我演示了如何在runfinish脚本中使用sv startsv down命令在服务之间通信。 您可能已经猜到,当执行sv命令时,runsv正在向它监控的run进程发送 POSIX 信号。 但您可能不知道的是,sv工具通过命名管道控制其目标runsv进程。 命名管道supervise/control和可选的log/supervise/control被打开,以便其他进程可以向runsv发送命令。 使用sv命令发送服务信号很容易,但如果您愿意,可以完全绕过sv,直接将控制字符写入control管道。

没有专用日志记录的服务的运行时目录布局如下所示:

# tree /etc/sv/syslogd
/etc/sv/syslogd
|-- run
`-- supervise
    |-- control
    |-- lock
    |-- ok
    |-- pid
    |-- stat
    `-- status

/etc/sv/syslogd下的control文件是服务的命名管道。 pidstat文件包含服务的实时 PID 和状态值(rundown)。 supervise子目录及其所有内容由runsv syslogd在系统启动时创建和填充。 如果服务包含专用记录器,runsv也会为其生成一个supervise子目录。

以下控制字符(tduo)直接映射到我们已经遇到的简短形式的sv命令(termdownuponce):

  • t``term:在重新启动服务之前向进程发送TERM信号。
  • d``down:向进程发送TERM信号,后跟CONT信号,并且不重新启动它。
  • u``up:启动服务,如果进程退出,则重新启动它。
  • o``once:尝试启动服务的时间最长为 7 秒,之后不会重新启动。
  • 1:向进程发送USR1信号。
  • 2:向进程发送USR2信号。

控制字符12特别重要,因为它们对应于用户定义的信号。 如何响应USR1USR2信号由接收端的服务决定。 如果您是负责扩展服务的开发人员,则可以通过实现信号处理程序来实现。 两个用户定义的信号可能看起来不太好用,但是如果您将这些不同的事件与写入配置文件的更新结合起来,您可以实现很多功能。 用户定义的信号还有一个额外的优点,即不需要像STOPTERMKILL信号那样停止或终止正在运行的进程。

摘要

这一章深入探讨了一个鲜为人知的init系统,我觉得这个系统在很大程度上被低估了。 与systemd类似,BusyBoxrunit可以在引导和运行时强制服务之间的复杂依赖关系。 它只是以一种简单得多的方式做到了,我认为它比systemd更像 Unix。 此外,在启动时间方面,没有什么比 BusyBoxrunit更好的了。 如果您已经使用 Buildroot 作为构建系统,那么我强烈建议您考虑将 BusyBoxrunit用于您设备的init系统。

在我们的探险中,我们覆盖了很多地方。 首先,您了解了如何将 BusyBoxrunit安装到您的设备上,并使用 Buildroot 启动它。 然后,我向您展示了如何使用树外伞包以不同的方式组装和配置服务。 接下来,在深入研究服务依赖关系和表达它们的方式之前,我们先对实时流程监督树进行了实验。 之后,我向您展示了如何添加专用记录器和配置服务的日志轮换。 最后,我描述了服务如何写入现有的命名管道,以此作为彼此发送信号的一种方式。

在下一章中,我将把注意力转向 Linux 系统的电源管理,目的是展示如何降低能耗。 如果您正在设计使用电池供电的设备,这将特别有用。

进一步阅读

以下是本章中提到的各种资源: