本文译者是一位开源理念的坚定支持者,所以本文虽然不是软件,但是遵照开源的精神发布。
本文译者十分愿意与他人分享劳动成果,如果你对我的其他翻译作品或者技术文章有兴趣,可以在如下位置查看现有的作品集:
由于译者水平有限,因此不能保证译文内容准确无误。如果你发现了译文中的错误(哪怕是错别字也好),请来信指出,任何提高译文质量的建议我都将虚心接纳。
daemon — 编写与打包系统守护进程
"守护进程"的意思是 在后台运行的服务进程, 常用于监督系统的运行或者提供某种功能。 在传统的 SysV Unix 系统上, 多个守护进程必须严格按照特定的顺序依次启动。 在"新型"的 systemd(1) 系统上, 守护进程的启动顺序非常简单且非常强大。 本手册同时涵盖了上述两种不同的启动方案, 并特别推荐了应该包含在 systemd 系统中的守护进程。
传统的SysV 守护进程 在启动的时候, 应该在初始化阶段 执行下面的步骤:
关闭除 STDIN STDOUT STDERR 之外的 所有文件描述符
重置所有信号处理器
重置所有信号掩码
清理环境变量(重置一部分,移除一部分)
调用 fork()
创建一个后台进程
在子进程中调用
setsid()
从终端脱离并创建一个独立的会话
在子进程中再一次调用 fork()
以确保守护进程永远无法获取任何终端。
第一个子进程主动退出, 只有第二个子进程(实际的守护进程)保持运行, 并且以 init(PID=1) 为父进程。
守护进程(第二个子进程)将 STDIN STDOUT STDERR 连接到 /dev/null
虚拟设备
守护进程将 umask 设为 0
守护进程将当前目录切换到根目录(/)
守护进程将自身的PID记录到例如 /run/foobar.pid
这样的文件中
守护进程丢弃自己不需要的权限 (如果可以)
守护进程通知最初的父进程:初始化工作已完成
最初的父进程自身退出
注意,这些步骤对于下文讲述的新型守护进程是不需要的, 除非为了刻意兼容传统的SysV系统。
Linux 系统上的新型守护进程更容易被监控也更容易实现。 守护进程无需实现前文所描述的复杂步骤, 即可直接在 systemd 提供的干净的上下文环境中运行:
环境变量已经被清理、信号处理器与信号掩码已经被重置、
没有遗留的文件描述符、守护进程自动在其专属的会话中执行、
标准输入(STDIN)已被连接到 /dev/null
虚拟设备(除非另有配置)、
标准输出(STDOUT)与标准错误(STDERR)已被连接到
systemd-journald.service(8)
日志服务(除非另有配置)、umask 已经被重置 … 等等
新型守护进程 只需要遵守如下要求:
收到 SIGTERM
信号后
关闭进程并确保干净的退出
收到 SIGHUP
信号后
重新加载配置文件
(若需要)
主守护进程 在退出时 应该按照 LSB recommendations for SysV init scripts 的要求返回恰当的退出码,以便于 systemd 判断服务的退出状态。
若可行,在初始化的最后一步, 通过 D-Bus 创建进程的控制接口, 并在 D-Bus 上注册一个总线名称。
提供一个
.service
单元文件,
包含如何启动/停止/维护该服务的配置。
详见
systemd.service(5)
手册。
尽可能 依赖于 systemd 的资源控制与权限剥夺功能 (CPU与内存占用/文件访问等等), 而不要自己实现它们。 详见 systemd.exec(5) 手册。
若使用了 D-Bus , 则强烈推荐使用基于 D-Bus 的启动机制。 这样做有许多好处: (1)守护进程可以按需延迟启动; (2)可以和依赖于它的进程并行启动 (提升启动速度); (3)守护进程可以在失败时被自动重启 而不丢失D-Bus总线上的请求 (详见下文)。
若守护进程通过套接字提供服务, 则强烈推荐使用 基于套接字的启动机制(详见下文)。 这样做有许多好处: (1)守护进程可以按需延迟启动; (2)可以和依赖于它的进程并行启动(提升启动速度); (3)对于无状态协议(例如 syslog, DNS), 守护进程可以在失败时被自动重启 而不丢失套接字上的请求(详见下文)
若可能, 守护进程应该通过 sd_notify(3) 接口通知 systemd "启动已完成"或"状态已更新"这样的消息。
不要使用
syslog()
记录日志,
只需简单的使用
fprintf()
向 STDERR 输出日志即可。
如果必须指明日志等级,
则可以在日志的
行首加上类似
"<4>
"
这样的前缀即可(这里表示4级"WARNING")。
详见
sd-daemon(3)
与
systemd.exec(5) 手册。
上述要求与 Apple MacOS X Daemon Requirements 类似, 但并不完全相同。
systemd 提供了
多种启动机制(见下文),
而服务单元也经常同时使用其中的几种。
例如
bluetoothd.service
可以在插入蓝牙硬件时被启动,
也可以在某进程访问其 D-Bus 接口时被启动。
又如打印服务可以在IPP端口有流量接入时被启动,
也可以在插入打印机硬件时被启动,
还可以在有文件进入打印机 spool 目录时被启动。
甚至对于必须在系统启动时无条件启动的服务,
为了尽可能并发启动,
也应该使用某些启动机制。
如果某守护进程实现了一个 D-Bus 服务或者监听一个套接字,
那么使用基于 D-Bus 或基于套接字的启动机制,
将允许该进程与其客户端同时并行启动(从而加快启动速度)。
因为所有的通信渠道都已事先建立,
并且不会丢失任何客户端请求,
同时 D-Bus 总线或者内核会将客户端请求排入队列等候,
直到完成启动。
传统的守护进程一般是在系统启动时通过SysV初始化脚本自动启动,
systemd 也支持这种启动方式。
对于 systemd 来说,如果希望确保某单元在系统启动时自动启动,
那么最佳的做法是在默认启动目标
(通常是 multi-user.target
或 graphical.target
)的
.wants/
目录中为该单元建立软链接。
参见
systemd.unit(5)
手册以了解 .wants/
目录,
参见
systemd.special(7)
手册以了解上述两个特殊的启动目标。
为了尽可能提高并行性与健壮性, 以及简化配置与开发, 对于需要监听套接字的服务, 强烈推荐使用基于套接字的启动机制。 使用此机制后, 守护进程不再需要创建和绑定套接字, 而是由 systemd 接管这个工作。 systemd 将会根据单元文件的设置, 预先创建所需的套接字, 并在第一个客户端请求接入的时候 启动该服务, 以实现服务的按需启动。 该机制的好处还在于, 预先创建好套接字之后, 所有使用此套接字通信的进程可以并行启动(包括客户端和服务端)。 此外,重启服务只会导致丢失最低限度的客户端连接, 甚至不丢失任何客户端请求 (例如对于 DNS 或 syslog 这样的无状态协议)。 因为套接字在服务重启期间 始终保持有效并且可被访问, 同时所有客户端请求 也都被排入队列等候处理。
使用此机制之后, 守护进程必须要从 systemd 接收已创建好的套接字, 而不能自己创建并绑定套接字。 关于如何使用该机制,参见 sd_listen_fds(3) 与 sd-daemon(3) 手册。 只需要小小的修改, 即可在原有启动机制的基础上 添加基于套接字的启动机制, 至于如何移植, 详见后文。
systemd 通过
.socket
单元实现该机制,详见
systemd.socket(5)
手册。
必须确保所有为支持基于套接字启动而创建的监听 socket 单元都被包含在
sockets.target
中。
建议在 socket 单元的
"[Install]
" 小节加入
WantedBy=sockets.target
设置,
以确保在启用该单元时能够自动添加上述依赖关系。
除非明确设置了 DefaultDependencies=no
,
否则会为所有 socket 单元隐含的创建必要的顺序依赖。
有关
sockets.target
的解释,详见
systemd.special(7) 手册。
如果某 socket 单元已被包含在 sockets.target
中,
那么不建议在其中再添加任何额外的依赖关系(例如
multi-user.target
之类)。
如果守护进程使用 D-Bus 与客户端通信,
那么它应该使用基于 D-Bus 的启动机制,
这样当客户端访问其 D-Bus 接口时,该服务将被自动启动。
该机制是通过 D-Bus service 文件实现的(不要与普通的单元文件混淆)。
为了确保让 D-Bus 使用 systemd 来启动与维护守护进程,
必须在这些 D-Bus service 文件中使用
SystemdService=
指明其匹配的服务单元。
例如,对于文件名为
org.freedesktop.RealtimeKit.service
的
D-Bus service 来说,
为了将其绑定到 rtkit-daemon.service
服务单元,
必须确保在该文件中设置了
SystemdService=rtkit-daemon.service
指令。
注意,必须明确设置
SystemdService=
指令,
否则当服务单元同时使用多种启动机制时,
可能会导致竞争条件的出现。
用于管理特定类型硬件的守护进程,
只应该在符合条件的硬件变为可用或者被插入时,
才需要启动。
为了达到上述目的,
可以将服务的启动/停止与硬件的插入/拔出事件绑定。
当带有 "systemd
" 标签的设备出现在 sysfs/udev 设备树中时,
systemd 将会自动为其创建对应的 device 单元。
通过向这些单元中添加对其他单元的 Wants=
依赖,
就可以实现当该 device 单元被启动(也就是硬件被插入)时,
连带启动其他单元,从而实现基于设备的启动。
这可以通过向 udev 规则库中添加 SYSTEMD_WANTS=
属性来实现,
详见 systemd.device(5) 手册。
通常,并不是将 service 单元直接添加到设备的 Wants=
依赖中,
而是通过专用的 target 单元间接添加。
例如,不是将 bluetoothd.service
添加到各种蓝牙设备的 Wants=
依赖中,
而是将 bluetoothd.service
添加到 bluetooth.target 的 Wants=
依赖中,
同时再将 bluetooth.target 添加到各种蓝牙设备的 Wants=
依赖中。
通过引入 bluetooth.target 这个抽象层,
系统管理员无需批量修改 udev 规则库,
仅通过 systemctl enable|disable … 命令
修改 bluetooth.target.wants/
目录中的软链接,
即可控制 bluetoothd.service
的使用。
对于处理 spool 文件或目录的
守护进程(例如打印服务)来说,
仅在 spool 文件或目录状态发生变化或者内容非空时,
才需要启动。
通过 .path
单元实现的、
基于路径的启动机制正好适用于这种场合,
详见
systemd.path(5) 手册。
在编写单元文件时 应当考虑下列建议:
尽可能不用
Type=forking
。
若非用不可,则必须正确设置
PIDFile=
指令。参见
systemd.service(5)
手册。
若守护进程在 D-Bus 上注册了一个名字,
则应尽可能使用 Type=dbus
类型的服务。
设置一个
易于理解的
Description=
确保
DefaultDependencies=yes
,
除非
该单元必须在系统启动的早期启动或者必须在系统关闭的末期关闭。
通常无需显式定义依赖关系。 不过,如果确实需要显式定义依赖关系, 为了确保单元文件不局限于特定的发行版,仅应该依赖于 systemd.special(7) 中列出的单元 以及自身所属软件包中提供的单元。
确保在
"[Install]
" 小节中
包含完整的启用信息(参见
systemd.unit(5) 手册)。
若希望自动启动该单元,
则应该设置 WantedBy=multi-user.target
或
WantedBy=graphical.target
若希望自动启动该单元的套接字,则应该设置
WantedBy=sockets.target
。
通常你还希望在启用该单元时,
一起启用对应的套接字单元
(假定为 foo.service
),
因此还应该设置
Also=foo.socket
当从源代码编译安装(make
install)软件包时,
其中的系统服务单元文件会被默认安装到
pkg-config systemd --variable=systemdsystemunitdir
命令返回的目录中(通常是 /usr/lib/systemd/system
);
而其中的用户服务单元文件会被默认安装到
pkg-config systemd --variable=systemduserunitdir
命令返回的目录中(通常是 /usr/lib/systemd/user
);
但并不应该使用 systemctl enable … 命令启用它们。
当从包管理器安装(rpm -i)二进制软件包时,
其中的单元文件应该同样安装到上述位置。
但不同之处在于,
还应该使用 systemctl enable … 命令启用它们,
因此安装的单元
有可能会在开机时自动启动。
使用 autoconf(1) 的软件包可以在源代码配置期间 使用包含类似如下片段的配置脚本 来确定 单元文件的安装路径:
PKG_PROG_PKG_CONFIG AC_ARG_WITH([systemdsystemunitdir], [AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd service files])],, [with_systemdsystemunitdir=auto]) AS_IF([test "x$with_systemdsystemunitdir" = "xyes" -o "x$with_systemdsystemunitdir" = "xauto"], [ def_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd) AS_IF([test "x$def_systemdsystemunitdir" = "x"], [AS_IF([test "x$with_systemdsystemunitdir" = "xyes"], [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])]) with_systemdsystemunitdir=no], [with_systemdsystemunitdir="$def_systemdsystemunitdir"])]) AS_IF([test "x$with_systemdsystemunitdir" != "xno"], [AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir])]) AM_CONDITIONAL([HAVE_SYSTEMD], [test "x$with_systemdsystemunitdir" != "xno"])
无论系统中 是否存在 systemd , 上述配置片段都允许自动安装单元文件。 (上述配置片段中 并不包含用户单元目录, 怎样修改以加入用户单元目录, 就留给读者作为练习题吧)
为了确保
make distcheck 正常工作,
建议将下面的内容添加到
automake(1)
项目顶层目录的
Makefile.am
文件中:
AM_DISTCHECK_CONFIGURE_FLAGS = \ --with-systemdsystemunitdir=$$dc_install_base/$(systemdsystemunitdir)
最后,应该使用类似下面的 automake 配置片段来安装单元文件:
if HAVE_SYSTEMD systemdsystemunit_DATA = \ foobar.socket \ foobar.service endif
在
rpm(8)
.spec
文件中,
可以使用下面的配置片段在安装/卸载时启用/禁用服务单元。
这个示例中使用了由 systemd 提供的 RPM 宏。
对于非红帽系的其他发行版,
请阅读相应的打包指南,
以详细了解其软件包管理器的相应等价替代。
在文件头部:
BuildRequires: systemd %{?systemd_requires}
下面是脚本:
%post %systemd_post foobar.service foobar.socket %preun %systemd_preun foobar.service foobar.socket %postun %systemd_postun
如果在软件包升级时需要重启服务,
应该将上面的 "%postun
" 脚本
替换为:
%postun %systemd_postun_with_restart foobar.service
注意, "%systemd_post
" 与
"%systemd_preun
"
的参数是将被安装/卸载的单元名称列表(空格分隔)。
"%systemd_postun
" 不需要参数。
"%systemd_postun_with_restart
"
的参数是将被重启的单元名称列表(空格分隔)。
若想 从只附带 SysV init 脚本的老版本软件包升级到 同时附带 SysV init 脚本与原生 systemd 服务单元的新版本软件包, 可以使用类似下面的片段:
%triggerun -- foobar < 0.47.11-1 if /sbin/chkconfig --level 5 foobar ; then /bin/systemctl --no-reload enable foobar.service foobar.socket >/dev/null 2>&1 || : fi
本例中的 0.47.11-1 是第一个包含原生服务单元的版本号。 上述片段保证了首次安装单元文件时, 当且仅当原有的 SysV init 脚本已被启用的情况下, 新的原生服务单元才会被启用, 这样就确保了版本升级不会破坏服务的启用/禁用状态。注意, chkconfig 是红帽系发行版专有的命令, 用于检查 SysV init 脚本是否被启用。 其他发行版一般也都有自己专属的 功能类似的命令。
虽然 systemd 兼容 传统的 SysV 初始化系统, 但是移植旧有的守护进程可以更好的利用 systemd 的先进特性。 建议对旧有的 SysV 守护进程做如下改进: …[省略]…