最少需要三个参数

#!/sbin/openrc-run

command=
command_args=
pidfile=

supervise-daemon 负责管理 pid 文件创建,日志重定向,修改执行用户,当前工作目录等

参考资料
https://github.com/fenprace/openrc-docs-zh-hans/blob/master/website/docs/OpenRC%20服务脚本编写指南.md

本文是《OpenRC Service Script Writing Guide》一文的中文翻译。

:::info

:::

本文档的目标读者,是为了自己的项目或维护的软件包,而编写 OpenRC 服务脚本的开发者或打包者们。本文包含了各种建议、意见、提示、技巧、原则、警告和注意事项等。

本文旨在指出一些在实际编写 OpenRC 服务脚本中常见的错误,提供替代方案,以防止这些错误发生。每一个你应该做的要点,或不该做的错事,都有对应的章节。我们不考虑那些旁门左道,并假设你使用 start-stop-daemon 来管理一个典型的,长期运行的 UNIX 进程。

服务脚本格式

服务脚本都是 shell 脚本。出于可移植性考量,OpenRC 只使用标准的 POSIX sh 子集。默认的解释器(可以于构建 OpenRC 时调整)是 /bin/sh,所以使用 mksh 之类的解释器不会有问题。

OpenRC 已经于 busybox sh、ash、dash、bash、mksh、zsh 和其他的 shell 环境下测试。由于 busybox sh 使用内置功能替换了一些指令,但不提供等价的功能,使用 busybox sh 会带来一些困难。

服务脚本的解释器是 #!/sbin/openrc-run。不使用这个解释器会破坏依赖,OpenRC 不支持这种做法(换句话说,如果你坚持使用 #!/bin/sh,后果自负)。

depend 函数声明了服务脚本的依赖。所有脚本必须包含 start/stop/status 函数。OpenRC 提供了默认的 start/stop/status 函数,除非你有非常正当的原因,你都应该使用默认的 start/stop/status 函数。

可以简单地添加额外函数:

extra_commands="checkconfig"
checkconfig() {
  doSomething
}

这段代码导出了 checkconfig 函数,执行 /etc/init.d/someservice checkconfig 就会运行这个函数。

extra_commands 里定义的函数永远都可以使用,而在 extra_started_commands 里定义函数只能在服务启动后使用,在 extra_stopped_commands 里定义的函数只能在服务停止时使用。可以用这种方式实现优雅的重载服务或类似功能。

编写 restart 函数不会起作用,这是 OpenRC 的一个设计决策,因为重启服务可能会涉及依赖项的重启(比如 network -> apache)。restart 被映射为 stop() + start()(再加上处理依赖)。如果服务需要在重启时,做一些与正常启动或停止服务所不同的操作,应该测试 $RC_CMD 变量,比如:

[ "$RC_CMD" = restart ] && do_something

depend 函数

这个函数声明了服务脚本的依赖,这将决定服务脚本启动的顺序。

depend() {
  need net
  use dns logger netmount
  want coolservice
}

need 声明了强依赖——net 总是需要在这个服务之前启动。

use 是一个软依赖——如果 dns、logger 或 netmount 在当前运行级别,在启动这个服务之前,先启动它们;但如果它们不在当前运行级别,就不管它们。

want 介于 need 和 use 之间——如果系统里安装了 coolservice,不管它是否在当前运行级别中,都尝试启动它,但我们不在乎它是否成功启动了。

before 声明本服务需要在一个服务之前启动。

after 声明本服务需要在另一个服务之后启动,但不创建依赖(所以服务停止是独立的)。

provide 允许多个服务提供同一个服务类型,比如:所有 cron 守护程序都定义了 provide cron,所以启动其中的一个就可以满足 cron 依赖。

keyword 允许服务在特定平台下改变行为,比如 keyword -lxc 可以让一个服务在 lxc 容器里无效化。这对于键盘映射或模块加载一类的服务很有用,因为这类服务要么是平台特定的,要么在容器或虚拟机等坏境下不可用。

默认函数

所有服务脚本都需要包含以下函数:

start()
stop()
status()

OpenRC 提供了以上函数的默认实现,在 lib/rc/sh/openrc-run.sh——这样就可以写出十分精简的服务脚本。你可以在每个服务脚本中,按需求覆盖这些函数。

这些默认函数要求服务脚本中,必须有以下变量:

command=
command_args=
pidfile=

所以“最小”的服务脚本可以在 6 行内写下。

不要编写你自己的 start/stop 函数

基于你提供的信息,OpenRC 有能力启动或停止大多数守护进程。一个合格的守护进程应该可以默默地在后台运行,并创建自己的 PID 文件。对于这样的守护进程,你可能只需要提供这些变量给 OpenRC:

  • command
  • command_args
  • pidfile

这些信息足够让 OpenRC 独立启动和停止守护进程了。下面这个例子来自 OpenNTPD 的服务脚本:

command="/usr/sbin/ntpd"

# 这个特别的 RC_SVCNAME 变量包含了该服务的名称。
pidfile="/run/${RC_SVCNAME}.pid"
command_args="-p ${pidfile}"

如果守护进程默认在前台运行,但提供了使它后台运行并创建 PID 文件的选项,那么你还需要:

  • command_args_background

这个变量应该包含让你的守护进程后台运行并创建 PID 文件的选项,下面的片段来自 NRPE 的服务脚本:

command="/usr/bin/nrpe"
command_args="--config=/etc/nagios/nrpe.cfg"
command_args_background="--daemon"
pidfile="/run/${RC_SVCNAME}.pid"

因为 NRPE 默认以 root 身份运行,不需要额外的授权就可以写入 /run/nrpe.pid。OpenRC 会以合适的参数启动和停止进程,甚至会在启动进程时传入 --daemon 选项让 NRPE 进入后台运行(NPRE 知道怎么创建自己的 PID 文件)。

但要是你的守护进程不是那么合格呢?要是它不会在后台运行,或者不会创建 PID 文件呢?如果它两者都做不到,就用:

  • command_background=true

这会额外传递一个 --make-pidfile 选项给 start-stop-daemon,让 start-stop-daemon 为你创建 $pidfile(而不是把创建 PID 文件的任务交给守护进程)。

如果你的守护进程无法改变自身的用户或用户组,你可以告诉 start-stop-daemon 以非特权用户启动进程:

  • command_user="user:group"

最后,如果你的守护进程可以启动到后台,但是无法创建 PID 文件,那你唯一的选择就是使用:

  • procname

OpenRC 会利用 procname,通过比较运行中进程的名字,来查找你的守护进程,这样做并不是很可靠。不过你的守护进程本来就不应该只后台运行,而不创建 PID 文件。下面的例子是 CA NetConsole Daemon 服务脚本的一部分:

command="/usr/sbin/cancd"
command_args="-p ${CANCD_PORT}
              -l ${CANCD_LOG_DIR}
              -o ${CANCD_LOG_FORMAT}"
command_user="cancd"

# cancd 将自身守护进程化,但不创建 PID 文件,也不提供在前台运行的选项
# 所以在终止进程时,我们能做的,最多就是尝试以进程名称查找进程来终止它
procname="cancd"

回顾一下:

  1. 如果进程在后台运行,并创建自己的 PID 文件,用 pidfile
  2. 如果进程不在后台运行(或者提供了让它在前台运行的选项),并且不创建 PID 文件,那就用 command_background=truepidfile
  3. 如果进程在后台运行,但是不创建 PID 文件,用 procname 代替 pidfile,但如果你的进程提供了让它在前台运行的选项,那你应该让它在前台运行(然后按照上一点的方法处理它)。
  4. 最后一种情况,尽管这样做没什么意义,你的进程不在后台运行但创建了 PID 文件。你应该禁用或无效化该进程的 PID 文件(或者把 PID 文件写入一个无用的路径),然后用 command_background=true

重载守护进程的配置

许多守护进程都会响应信号来重载配置。假设你的进程接收到 SIGHUP 就重载配置,可以增加一个“reload”命令,来让你的服务脚本具有重载配置功能。首先,在服务脚本里声明这个命令。

extra_started_commands="reload"

我们用 extra_started_commands 而不是 extra_commands,因为“reload”只在进程运行时(也就是启动后)有效。现在可以用 start-stop-daemon 发送信号到对应的进程(假设你已经在服务脚本里定义了 pidfile):

reload() {
  ebegin "Reloading ${RC_SVCNAME}"
  start-stop-daemon --signal HUP --pidfile "${pidfile}"
  eend $?
}

不要在配置损坏时重启 / 重载

这是一个十分常见的情景:用户启动了守护进程,修改配置文件,然后再尝试重启进程。如果修改后的配置文件有错误,会导致进程停止后不能再次启动(由于配置文件错误)。编写一个检查配置文件的函数,配合 start_prestop_pre 钩子,就可以预防这种情况发生。

checkconfig() {
  # 检查配置文件
}

start_pre() {
  # 如果不是重启,在启动进程前检查配置文件的有效性(相比盲目的启动进程,
  # 这样可以生成更好的错误信息)
  #
  # 反之,如果这 *是* 一次重启,那么 stop_pre 函数会确保配置文件可用,
  # 我们没必要再检查一遍。
  if [ "${RC_CMD}" != "restart" ] ; then
    checkconfig || return $?
  fi
}

stop_pre() {
  # 如果是重启,在停止进程前检查配置文件的有效性。
  if [ "${RC_CMD}" = "restart" ] ; then
      checkconfig || return $?
  fi
}

要防止 重载 损坏的配置文件,很简单:

reload() {
  checkconfig || return $?
  ebegin "Reloading ${RC_SVCNAME}"
  start-stop-daemon --signal HUP --pidfile "${pidfile}"
  eend $?
}

只有 root 用户可以写入 PID 文件

只能有 root 一个用户可以写入 PID 文件,也就是说,PID 文件所在的文件夹拥有者必须是 _root_。在 Linux 中,这个文件夹一般是 /run,在其他操作系统中一般是 /var/run。

一些守护进程以非特权用户运行,然后(以非特权用户)在 /var/run/foo/foo.pid 之类的路径下创建自己的 PID 文件。这使得非特权用户能够杀死 root 进程,因为服务停止时,_root_ 一般会向 PID 文件(该状况下,这个文件由非特权用户控制)的内容发送一个 SIGTERM 信号。该问题的预兆之一,是用 checkpath 来设置 PID 文件所在目录的所有权,比如:

# 别这么做 别这么做 别这么做 别这么做
start_pre() {
  # 确保 pidfile 所在的目录可以被 foo 用户 / 用户组写入
  checkpath --directory --mode 0700 --owner foo:foo "/var/run/foo"
}
# 别这么做 别这么做 别这么做 别这么做

如果 foo 用户拥有 /var/run/foo,那么它可以随心所欲地修改 /var/run/foo/foo.pid 文件。即使 root 拥有 PID 文件,_foo_ 用户还是可以删除 root 所拥有的 PID 文件,再创建一个属于 foo 的文件取而代之。要避免安全问题,PID 文件必须由 root 创建,且必须在 root 拥有的文件夹中。如果你的守护进程负责 fork 到后台并创建 PID 文件,PID 文件却由非特权运行时用户所拥有,那么有可能是上游的问题。

只要作为 root 创建了 PID 文件(在放弃特权前),就可以直接把它写入 root 拥有的目录中去。比如,_foo_ 守护进程想要写入 /var/run/foo.pid,不需要 checkpath。注意:技术上,只要 root 拥有 PID 文件和 PID 文件所在的目录,完全可以使用类似于 /var/run/foo/foo.pid 的目录结构。

理想情况下(参考把服务脚本放到上游),上游应该集成你的服务脚本,合适的 PID 文件目录应该由构建系统,例如:

pidfile="@piddir@/${RC_SVCNAME}.pid"

正面例子之一是这个 Nagios 核心服务脚本,在构建时指定了 PID 文件的完整路径。

不要让用户控制 PID 文件的位置

允许末端用户通过 conf.d 变量控制 PID 文件的位置是一个常见错误,原因有以下几点:

  1. PID 文件由用户控制时,你需要确保 PID 文件的父目录存在并且可以写入,为你的服务脚本增加了不必要的代码。
  2. 如果 PID 文件的路径在服务运行时改变了,你会发现你没法停止进程。
  3. 包含 PID 文件的目录最好由上游构建系统决定(参考把服务脚本放到上游)。Linux 当下流行的趋势把 PID 文件放在 /run,尽管其他操作系统依然使用 /var/run。最好是在 ./configure 脚本中配置你 PID 文件的路径。
  4. 其实反正也没人在乎 PID 文件的位置。

因为 OpenRC 服务名称不能重名:

pidfile="/var/run/${RC_SVCNAME}.pid"

这样就可以保证你的 PID 文件不会重名。

(致打包者)把服务脚本放到上游

上游是分发 OpenRC 服务脚本的理想位置。与 systemd 服务类似,一个配置良好的 OpenRC 服务脚本应该是发行版不可知的,且最好配置在上游。为何?有两个原因:第一,在上游意味着对该服务脚本的所有修改与改进,都有一个权威的来源。第二,每个服务脚本都有一些路径依赖传递进构建系统的参数,比如:

command=/usr/bin/foo

在基于 autotools 的构建系统中,其实应该写成:

command=@bindir@/foo

这样就可以根据用户传入的 --bindir,改变 command 变量的值。如果你在自己发行版的仓库中配置服务脚本,你就得自己保持 command 路径与软件包同步,这可不好玩。

小心“need net”依赖

关于“need net”依赖,你需要知道两件事:

  1. 回环(loopback)接口不满足“need net”,所以你必须开启 其他的 接口。
  2. 取决于 rc.confrc_depend_strict 的值,要满足“need net” ,可以是开启 任何一个 非回环接口,也可以是开启 所有 非回环接口。

第一点说明对于只需要绑定 0.0.0.0 的守护进程来说,设定“need net”是错误的;第二点说明对于依赖某个特定接口(比如 WAN 接口)的守护进程来说,设定“need net”也是不对的。我们讨论两种最常见的,需要使用“need net”的场景:需要访问网络资源的客户端,和提供网络资源的服务端。

网络客户端

网络客户端通常需要开启 WAN 接口。你很可能会想让你的服务脚本依赖 WAN 接口;但是在这之前,请先问自己一个问题:如果 WAN 接口不可用,会发生什么坏事?换句话说,如果管理员要禁用 WAN,是否应该停止你的服务?这些问题通常的答案都是:“不”,在这种情况下,你应该将“net”依赖完全抛诸脑后。

假设有一个联网更新病毒特征的服务,要正常工作,它需要连接互联网。然而,服务本身并不依赖 WAN 接口。如果 WAN 接口开启,那一切正常;反之如果 WAN 接口关闭,那么最坏的事情也不过是纪录一条“服务器不可用”的警告,病毒特征更新服务本身不会崩溃。而且,更重要的是——即使管理员关闭了 WAN 接口,你也肯定不想终止你的病毒特征更新服务。

网络服务端

相较于客户端,服务端通常更好处理。大多是服务端守护进程都默认监听 0.0.0.0(也就是所有地址),也因此仅提供回环接口就可以工作。在 OpenRC 中,回环服务位于 boot 运行级别中,因此大多数服务端进程不需要额外的网络依赖。

例外之一是那些在 WAN 不可用时,会产生负面效果的守护进程。比如,只要你的监控的主机无法访问,Nagios 服务器进程就会生成“天要塌了”警报。所以这种情况你的服务脚本应该要求某个接口(通常是 WAN)可用。“need”依赖是比较恰当的选择,因为你会想要在关闭网络前,先停止 Nagios 程序。

如果你的守护进程可以只监听某个特定的接口,请参考依赖特定接口一节。

依赖特定接口

即使你要依赖一个特定的接口,要程序化的识别、选择接口通常也很困难。例如,假设你的 sshd 守护进程监听 192.168.1.100(而不是正常的 0.0.0.0),那么你将面临两个问题:

  1. 解析 sshd_config,找出被监听的 IP;然后
  2. 确定 192.168.1.100 的接口对应的网络服务名称。

在服务脚本里解析配置文件一般都不是好主意,但是第二个问题更加困难。与之相反,最可靠的(也是最懒惰的)方法是让用户在修改 sshd_config 时指定依赖。在服务配置文件中增加:

# 根据你配置文件中的“bind”设置指定网络服务。比如你的绑定了 127.0.0.1,
# 那这里应该设置成对应回环接口的“loopback”
rc_need="loopback"

对于绑定 0.0.0.0的守护进程来说,这样的默认配置十分合理,同时还允许用户按需指定接口,比如rc_need="net.wan"。让用户负责在修改进程配置文件时,选择合适的服务依赖。

标签: none

添加新评论