使用Type=forking时,要求ExecStart启动的命令自身就是以daemon模式运行的。
而以daemon模式运行的进程都有一个特性:总是会有一个瞬间退出的中间父进程,例如,nginx命令默认以daemon模式运行,所以可直接将其配置为forking类型:
Type=simple是一种最常见的通过systemd服务系统运行用户自定义命令的类型,也是省略Type指令时的默认类型。
例如,nginx命令默认以daemon模式运行,所以可直接将其配置为forking类型:
注意上面status报告的信息中,ExecStart启动的nginx的进程PID=7912,且该进程的状态是已退出,退出状态码为0,这个进程是daemon类进程创建过程中瞬间退出的中间父进程。在forking类型中,该进程称为初始化进程。同时还有一行Main PID: 7913 (nginx),这是systemd真正监控的nginx服务主进程,其PID=7913,是PID=7912进程的子进程。
Type=forking类型代表什么呢?要解释清楚该type,需从进程创建开始说起。
因为systemd service启动的服务进程都是systemd的子进程,所以,在服务进程启动时,总是由pid=1的systemd进程fork()一个子进程(子systemd进程),再在此进程分支中通过systemd.exec配置该子进程的环境,最后使用exec()去调用ExecStart指定的服务启动命令。Exec()调用程序时会替换当前进程,所以启动后的服务进程将会替代子systemd进程,于是服务进程自身成为pid=1 systemd进程的子进程。
对于Type=forking来说,pid=1的systemd进程fork出来的子进程正是瞬间退出的中间父进程,且systemd会在中间父进程退出后就认为服务启动成功,此时systemd可以立即去启动后续需要启动的服务。
如果Type=forking服务中的启动命令是一个前台 命令会如何呢?比如将sleep配置为forking模式,将nginx daemon off配置为forking模式等。
答案是systemd会一直等待中间ExecStart启动的进程作为中间父进程退出,在等待过程中个,systemctl start会一直卡住,直到等待超时而失败,在此阶段中,systemctl status将会查看到服务处于activating状态。
回到forking类型的服务。由于daemon类的进程会有一个瞬间退出的中间父进程(如上面的PID=7913的nginx进程),systemd是如何知道哪个进程是应该被监控的服务主进程(Main PID)呢?
答案是靠猜。没错,systemd真的就是靠猜的。当设置Type=forking时,有一个GuessMainPID指令其默认值为Yes,它表示systemd会通过一些算法去猜测Main PID。当systemd的猜测无法确定哪个是主进程时,后果是严重的:systemd将不可靠。因为systemd无法正确探测服务是否真的失败,当systemd误认为服务失败时,如果本服务器配置了自动重启(配置了Restart指令),重启服务器时可能会和当前正在运行但是systemd误认为失败的服务冲突(比如出现端口已被占用问题)。
多数情况下的猜测过程很简单,systemd只需去找目前存活的属于本服务的leader进程即可。但有些服务(少数)情况可能比较复杂,在多进程之间做简单的猜测并非总是可靠。
好在,Type=forking时的systemd提供了PIDFile指令(Type=forking通常都会结合PIDFile指令),systemd会从PIDFile指令所指定的PID文件中获取服务的主进程PID。例如,编写一个nginx的服务配置文件:
GuessMainPID=Takes a boolean value that specifies whether systemd should try to guess the main PID of a service if it cannot be determined reliably. This option is ignored unless Type=forking is set and PIDFile= is unset because for the other types or with an explicitly configured PID file, the main PID is always known. The guessing algorithm might come to incorrect conclusions if a daemon consists of more than one process. If the main PID cannot be determined, failure detection and automatic restarting of a service will not work reliably. Defaults to yes.
该参数只有在启动类型为forking,且没有指定PIDFile参数时才有效。
关于PIDFile,有必要去了解一些注意事项,否则它们可能就会成为你的坑。
首先,PIDFile只适合在Type=forking模式下使用,其它时候没必要使用,因为其它类型的Service主进程的PID都是确定的。systemd推荐PIDFile指定的PID文件在/run目录下,所以,可能需要修改服务程序的配置文件,将其PID文件路径修改为/run目录之下,当然这并非必须。
但有一点必须注意,PIDFile指令的值要和服务程序的PID文件路径保持一致。例如nginx的相关配置:
其次,systemd会在中间父进程退出后立即读取这个PID文件,读取成功后就认为该服务已经启动成功。但是,systemd读取PIDFile的时候,服务主进程可能还未将PID写入到PID文件中,这时systemd将出现问题。所以,对于服务程序的开发人员来说,应尽早将主进程写入到PID文件中,比如可以在中间父进程fork完之后立即写入PID文件,然后再退出,而不是在fork出来的服务主进程内部由主进程负责写入。
上面的nginx服务配置文件是某个nginx版本yum包提供的,但却是有问题的,我曾经踩过这个坑,网上甚至将其报告为一个Bug。
上面的nginx.service文件可以正常启动服务,但无法systemctl reload,只要reload就报错,而且报错时提示kill命令语法错误。kill语法错误显然是因为没有获取到$MAINPID变量的值,而这正是因为systemd在nginx写入PID文件之前先去读取了PID文件,因为没有读取到内容,所以$MAINPID变量为空值。
解决办法是使用ExecStartPost=/usr/bin/sleep 0.1,让systemd在初始化进程(即中间父进程)退出之后耽搁0.1秒再继续向下执行,即推迟了systemd读取PID的过程,保证能让systemd从PID文件中读取到值。
最后,systemd只会读PIDFile文件而不会写,也不会创建它。但是,在停止服务的时候,systemd会尝试删除PID文件。因为服务进程可能会异常终止,导致已终止的服务进程的PID文件仍然保留着,所以在使用PIDFile指令时,通常还会使用ExecStartPre指令来删除可能已经存在的PID文件。正如上面给出的nginx配置文件一样。
Type=simple类型的服务只适合那些在shell下运行在前台的命令。也就是说,当一个命令本身会以daemon模式运行时,将不能使用simple,而应该使用Type=forking。比如ls命令、sleep命令、非daemon模式运行的nginx进程以及那些以前台调试模式运行的进程,在理论上都可以定义为simple类型的服务。
例如,编写一个/usr/lib/systemd/system/test.service运行sleep进程:
使用daemon-reload重载并启动该服务进程:
10秒内,sleep进程以daemon模式运行在后台,就像一个服务进程一样。10秒之后,sleep退出,于是systemd将该进程从监控队列中踢出。再次查看进程的状态将是inactive:
再来分析上面的服务配置文件中的指令。
ExecStart指令指定启动本服务时执行的命令,即启动一个本该前台运行的sleep进程作为服务进程在后台运行。
需注意,systemd service的命令行中必须使用绝对路径,且只能编写单条命令(Type=oneshot时除外),如果要命令续行,可在尾部使用反斜线符号\等。
此外,命令行中支持部分类似Shell的特殊符号,但不支持重定向> >> << <、管道|、后台符号&,具体可参考man systemd.service中command line段落的解释说明。
对于Type=simple来说,systemd系统在fork出子systemd进程后就认为服务已经启动完成了,所以systemd可以紧跟着启动排在该服务之后启动的服务。它的伪代码模型大概是这样的:
例如,先后连续启动两个Type=simple的服务,进程流程图大概如下:
换句话说,当Type=simple时,systemd只在乎fork阶段是否成功,只要fork子进程成功,这个子进程就受systemd监管,systemd就认为该Unit已经启动。
因为子进程已成功被systemd监控,无论子进程是否启动成功,在子进程退出时,systemd都会将其从监控队列中踢掉,同时杀掉所有附属进程(默认行为是如此,杀进程的方式由systemd.kill中的KillMode指令控制)。所以,查看服务的状态将是inactive(dead)。
例如,下面的配置种,睡眠1秒后,该服务的状态将变为inactive(dead)。
这没什么疑问。但考虑一下,如果simple类型下ExecStart启动的命令本身就是以daemon模式运行的呢?其结果是systemd默认会立刻杀掉所有属于服务的进程。
原因也很简单,daemon类进程总是会有一个瞬间退出的中间父进程,而在simple类型下,systemd所fork出来的子进程正是这个中间父进程,所以systemd会立即发现这个中间父进程的退出,于是杀掉其它所有服务进程。
例如,以运行bash -c '(sleep 3000 &)'的simple类型的服务,被systemd监控的bash进程会在启动sleep后立即退出,于是systemd会立即杀掉属于该服务的sleep进程。
再例如,nginx命令默认是以daemon模式运行的,simple类型下直接使用nginx命令启动服务,systemd会立刻杀掉所有nginx,即nginx无法启动成功。
simple:在fork出子systemd进程后,systemd就认为该服务启动成功了
exec:在fork出子systemd进程且子systemd进程exec()调用ExecStart命令成功后,systemd认为该服务启动成功
oneshot:在ExecStart命令执行完成退出后,systemd才认为该服务启动成功
因为服务进程退出后systemd才继续工作,所以在未配置RemainAfterExit 指令时,oneshot类型的服务永远无法出现active状态,它直接从启动状态到activating到deactivating再到dead状态
当结合RemainAfterExit指令时,在服务进程退出后,systemd会继续监控该Unit,所以服务的状态为active(exited),通过这个状态可以让用户知道,该服务曾经已经运行成功,而不是从未运行过
通常来说,对于那些执行单次但无需长久运行的进程来说,可以采用type=oneshot,比如启动iptables,挂载文件系统的操作、关机或重启的服务等