daemon 中文手册
名称
daemon — 编写与打包系统守护进程
描述
"守护进程"的意思是在后台运行的服务进程,常用于监督系统的运行或者提供某种功能。在传统的 SysV Unix 系统上,多个守护进程必须严格按照特定的顺序依次启动。在"新型"的systemd(1) 系统上,守护进程的启动顺序非常简单且非常强大。本手册同时涵盖了上述两种不同的启动方案,并特别推荐了应该包含在 systemd 系统中的守护进程。
传统的SysV守护进程
传统的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 接口时,该服务将被自动启动。该机制是通过 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) 手册。
基于定时器的启动
对于周期性的操作(例如垃圾文件清理或者网络对时),可以通过基于定时器的启动机制来实现。这种机制通过 .timer
单元实现,详见systemd.timer(5) 手册。
其他启动方式
在其他操作系统上还存在着其他的启动机制,不过这些机制都可以被前述的各种机制的组合替代。因此在这里不再赘述。
与 Systemd 整合
编写 systemd 单元文件
在编写单元文件时应当考虑下列建议:
尽可能不用
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
安装 service 单元文件
当从源代码编译安装(makeinstall)软件包时,其中的系统服务单元文件会被默认安装到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 守护进程做如下改进:…[省略]…
放置守护进程的数据
建议遵守file-hierarchy(7)所建议的通用准则。
参见
systemd(1),sd-daemon(3),sd_listen_fds(3),sd_notify(3),daemon(3),systemd.service(5),file-hierarchy(7)