百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

systemd service之:服务配置文件编写(2)

off999 2025-03-19 15:41 41 浏览 0 评论

接下来会通过示例来描述不同Service Type值的应用场景。在此之前,强烈建议先阅读前后台进程父子关系和daemon类进程来搞懂进程之间的关系和Daemon类进程的特性。

systemd service:Type=forking

当使用systemd去管理一个长久运行的服务进程时,最常用的Type是forking类型。

使用Type=forking时,要求ExecStart启动的命令自身就是以daemon模式运行的。而以daemon模式运行的进程都有一个特性:总是会有一个瞬间退出的中间父进程,如果不了解这点特性,请看前后台进程父子关系和daemon类进程。

例如,nginx命令默认以daemon模式运行,所以可直接将其配置为forking类型:

BASH

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ cat test.service 
[Unit]
Description = Test

[Service]
Type = forking
ExecStart = /usr/sbin/nginx

$ systemctl daemon-reload
$ systemctl start test
$ systemctl status test
● test.service - Test
   Loaded: loaded
   Active: active (running)
  Process: 7912 ExecStart=/usr/sbin/nginx (code=exited, status=0/SUCCESS)
 Main PID: 7913 (nginx)
    Tasks: 5
   Memory: 4.6M
   CGroup: /system.slice/test.service
           ├─7913 nginx: master process /usr/sbin/nginx
           ├─7914 nginx: worker process
           ├─7915 nginx: worker process
           ├─7916 nginx: worker process
           └─7917 nginx: worker process

注意上面status报告的信息中,ExecStart启动的nginx的进程PID=7912,且该进程的状态是已退出,退出状态码为0,这个进程是daemon类进程创建过程中瞬间退出的中间父进程。在forking类型中,该进程称为初始化进程。同时还有一行Main PID: 7913 (nginx),这是systemd真正监控的nginx服务主进程,其PID=7913,是PID=7912进程的子进程。

Type=forking类型代表什么呢?要解释清楚该type,需从进程创建开始说起。

对于Type=forking来说,pid=1的systemd进程fork出来的子进程正是瞬间退出的中间父进程,且systemd会在中间父进程退出后就认为服务启动成功,此时systemd可以立即去启动后续需要启动的服务。

如果Type=forking服务中的启动命令是一个前台命令会如何呢?比如将sleep配置为forking模式,将nginx daemon off配置为forking模式等。

答案是systemd会一直等待中间ExecStart启动的进程作为中间父进程退出,在等待过程中,systemctl start会一直卡住,直到等待超时而失败,在此阶段中,systemctl status将会查看到服务处于activating状态。

BASH

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cat test.service 
[Unit]
Description = Test

[Service]
Type = forking
ExecStart = /usr/sbin/nginx -g 'daemon off;'

$ systemctl daemon-reload
$ systemctl start test    # 卡住
$ systemctl status test   # 另一个窗口查看
● test.service - Test
   Loaded: loaded
   Active: activating (start)
  Control: 9227 (nginx)
    Tasks: 1
   Memory: 2.0M
   CGroup: /system.slice/test.service
           └─9227 /usr/sbin/nginx -g daemon off;

回到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的服务配置文件:

BASH

1
2
3
4
5
6
7
8
9
10
$ cat test.service 
[Unit]
Description = Test

[Service]
Type = forking
PIDFile = /run/nginx.pid
ExecStartPre = /usr/bin/rm -f /run/nginx.pid
ExecStart = /usr/sbin/nginx
ExecStartPost = /usr/bin/sleep 0.1

Type=forking时PIDFile指令的坑

关于PIDFile,有必要去了解一些注意事项,否则它们可能就会成为你的坑。

首先,PIDFile只适合在Type=forking模式下使用,其它时候没必要使用,因为其它类型的Service主进程的PID都是确定的。systemd推荐PIDFile指定的PID文件在/run目录下,所以,可能需要修改服务程序的配置文件,将其PID文件路径修改为/run目录之下,当然这并非必须。

但有一点必须注意,PIDFile指令的值要和服务程序的PID文件路径保持一致

例如nginx的相关配置:

BASH

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ grep -i 'pid' /etc/nginx/nginx.conf    
pid /run/nginx.pid;

$ cat /usr/lib/systemd/system/nginx.service
[Unit]
Description=The nginx HTTP and reverse proxy server
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/run/nginx.pid    # 路径和nginx.conf中保持一致
ExecStartPre=/usr/bin/rm -f /run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID
KillSignal=SIGQUIT
TimeoutStopSec=5
KillMode=process
PrivateTmp=true

[Install]
WantedBy=multi-user.target

其次,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配置文件一样。

systemd service:Type=simple

Type=simple是一种最常见的通过systemd服务系统运行用户自定义命令的类型,也是省略Type指令时的默认类型。

Type=simple类型的服务只适合那些在shell下运行在前台的命令。也就是说,当一个命令本身会以daemon模式运行时,将不能使用simple,而应该使用Type=forking。比如ls命令、sleep命令、非daemon模式运行的nginx进程以及那些以前台调试模式运行的进程,在理论上都可以定义为simple类型的服务。至于为何有如此规则,稍后会解释的明明白白。

例如,编写一个
/usr/lib/systemd/system/test.service运行sleep进程:

PLAINTEXT

1
2
3
4
5
6
[Unit]
Description = test

[Service]
Type = simple
ExecStart = /usr/bin/sleep 10  # 命令必须使用绝对路径

使用daemon-reload重载并启动该服务进程:

BASH

1
2
3
4
5
6
7
8
9
10
11
$ systemctl daemon-reload
$ systemctl start test
$ systemctl status test
● test.service - Test
   Loaded: loaded
   Active: active (running)
 Main PID: 6902 (sleep)
    Tasks: 1
   Memory: 96.0K
   CGroup: /system.slice/test.service
           └─6902 /usr/bin/sleep 10

10秒内,sleep进程以daemon模式运行在后台,就像一个服务进程一样。10秒之后,sleep退出,于是systemd将该进程从监控队列中踢出。再次查看进程的状态将是inactive:

BASH

1
2
3
4
$ systemctl status test
● test.service - Test
   Loaded: loaded
   Active: inactive (dead)

再来分析上面的服务配置文件中的指令。

ExecStart指令指定启动本服务时执行的命令,即启动一个本该前台运行的sleep进程作为服务进程在后台运行。

需注意,systemd service的命令行中必须使用绝对路径,且只能编写单条命令(Type=oneshot时除外),如果要命令续行,可在尾部使用反斜线符号\等。

此外,命令行中支持部分类似Shell的特殊符号,但不支持重定向> >> << <、管道|、后台符号&,具体可参考man systemd.service中command line段落的解释说明。

对于Type=simple来说,systemd系统在fork出子systemd进程后就认为服务已经启动完成了,所以systemd可以紧跟着启动排在该服务之后启动的服务。它的伪代码模型大概是这样的:

PLAINTEXT

1
2
3
4
5
6
7
8
9
10
11
# pid = 1: systemd

# start service1 with Type=simple
pid=fork()
if(pid=0){
  # Child Process: sub systemd process
  exec()
}

# start other services after service1
...

例如,先后连续启动两个Type=simple的服务,进程流程图大概如下:

换句话说,当Type=simple时,systemd只在乎fork阶段是否成功,只要fork子进程成功,这个子进程就受systemd监管,systemd就认为该Unit已经启动

因为子进程已成功被systemd监控,无论子进程是否启动成功,在子进程退出时,systemd都会将其从监控队列中踢掉,同时杀掉所有附属进程(默认行为是如此,杀进程的方式由systemd.kill中的KillMode指令控制)。所以,查看服务的状态将是inactive(dead)

例如,下面的配置种,睡眠1秒后,该服务的状态将变为inactive(dead)

PLAINTEXT

1
2
[Service]
ExecStart = /usr/bin/sleep 1

这没什么疑问。但考虑一下,如果simple类型下ExecStart启动的命令本身就是以daemon模式运行的呢?其结果是systemd默认会立刻杀掉所有属于服务的进程

原因也很简单,daemon类进程总是会有一个瞬间退出的中间父进程,而在simple类型下,systemd所fork出来的子进程正是这个中间父进程,所以systemd会立即发现这个中间父进程的退出,于是杀掉其它所有服务进程。

例如,以运行bash -c '(sleep 3000 &)'的simple类型的服务,被systemd监控的bash进程会在启动sleep后立即退出,于是systemd会立即杀掉属于该服务的sleep进程。

BASH

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cat test.service    
[Unit]
Description = Test

[Service]
ExecStart = bash -c '( sleep 3000 & )'

$ systemctl daemon-reload
$ systemctl start test
$ systemctl status test
● test.service - Test
   Loaded: loaded
   Active: inactive (dead)

再例如,nginx命令默认是以daemon模式运行的,simple类型下直接使用nginx命令启动服务,systemd会立刻杀掉所有nginx,即nginx无法启动成功。

BASH

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cat test.service    
[Unit]
Description = Test

[Service]
ExecStart = /usr/sbin/nginx

$ systemctl daemon-reload
$ systemctl start test
$ systemctl status test
● test.service - Test
   Loaded: loaded
   Active: inactive (dead)

但如果将nginx进程以非daemon模式运行,simple类型的nginx服务将正常启动:

BASH

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ cat test.service 
[Unit]
Description = Test

[Service]
ExecStart = /usr/sbin/nginx -g 'daemon off;'

$ systemctl daemon-reload
$ systemctl start test
$ systemctl status test
● test.service - Test
   Loaded: loaded
   Active: active (running)
 Main PID: 7607 (nginx)
    Tasks: 5
   Memory: 4.6M
   CGroup: /system.slice/test.service
           ├─7607 nginx: master process /usr/sbin/nginx -g daemon off;
           ├─7608 nginx: worker process
           ├─7609 nginx: worker process
           ├─7610 nginx: worker process
           └─7611 nginx: worker process

Systemd Service:其它Type类型

除了simple和forking类型,还有exec、oneshot、idle、notify和dbus类型,这里不考虑notify和dbus,剩下的exec、oneshot和idle都类似于simple类型。

  • 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,挂载文件系统的操作、关机或重启的服务等
  • idle:无需考虑这种类型

模板型服务配置文件

systemd service支持简单的模板型Unit配置文件,在Unit配置文件中可以使用%n %N %p %i...等特殊符号进行占位,在systemd读取配置文件时会将这些特殊符号解析并替换成对应的值。

这些特殊符号的含义可参见man systemd.unit。通常情况下只会使用到%i%I,其它特殊符号用到的机会较少。

使用%i %I这两个特殊符号时,要求Unit的文件名以@为后缀,即文件名格式为Service_Name@.service。当使用systemctl管理这类服务时,@符号后面的字符会传递到Unit模板文件中的%i%I

例如,执行下面这些命令时,会使用abc替换service_name@.service文件中的%i%I

BASH

1
2
3
4
5
systemctl start service_name@abc
systemctl status service_name@abc
systemctl stop service_name@abc
systemctl enable service_name@abc
systemctl disable service_name@abc

有时候这是很实用的。比如有些程序即是服务端程序又是客户端程序,区分客户端和服务端的方式是使用不同配置文件。

假设用户想在一个机器上同时运行xyz程序的服务端和客户端,可编写如下Unit服务配置文件:

现在用户可以在/etc/server目录下同时提供服务程序的服务端配置文件和客户端配置文件。

PLAINTEXT

1
2
/etc/server/server.conf
/etc/server/client.conf

如果要管理该主机上的服务端:

BASH

1
2
3
4
5
systemctl start xyz@server
systemctl status xyz@server
systemctl stop xyz@server
systemctl enable xyz@server
systemctl disable xyz@server

如果要管理该主机上的客户端:

BASH

1
2
3
4
5
systemctl start xyz@client
systemctl status xyz@client
systemctl stop xyz@client
systemctl enable xyz@client
systemctl disable xyz@client

使用target组合多个服务

有些时候,一个大型服务可能由多个小服务组成。

比如c服务由a.service和b.service组成,因为组合了两个服务,所以c服务可以定义为c.target。

a.service内容:

PLAINTEXT

1
2
3
4
5
6
7
[Unit]
Description = a.service
PartOf = c.target
Before = c.target

[Install]
ExecStart = /path/to/a.cmd

b.service内容:

PLAINTEXT

1
2
3
4
5
6
7
[Unit]
Description = b.service
PartOf = c.target
Before = c.target

[Install]
ExecStart = /path/to/b.cmd

c.target内容:

PLAINTEXT

1
2
3
4
[Unit]
Description = c.service, consists of a.service and b.service
After = a.service b.service
Wants = a.service b.service

c中配置Wants表示a和b先启动,但启动失败不会影响c的启动。如果要求c.target和a.service、b.service的启动状态一致,可将Wants替换成Requires或BindsTo指令。

PartOf指令表明a.service和b.service是c.target的一部分,停止或重启c.target的同时,也会停止或重启a和b。再加上c.target中配置了Wants指令(也可以改成Requires或BindsTo),使得启动c的时候,a和b也已经启动完成。

但是要注意,PartOf是单向的,停止和重启a或b的时候不会影响c。

文章作者: 骏马金龙

文章链接:
https://www.junmajinlong.com/linux/systemd/service_2/

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 骏马金龙!

相关推荐

Python钩子函数实现事件驱动系统(created钩子函数)

钩子函数(HookFunction)是现代软件开发中一个重要的设计模式,它允许开发者在特定事件发生时自动执行预定义的代码。在Python生态系统中,钩子函数广泛应用于框架开发、插件系统、事件处理和中...

Python函数(python函数题库及答案)

定义和基本内容def函数名(传入参数):函数体return返回值注意:参数、返回值如果不需要,可以省略。函数必须先定义后使用。参数之间使用逗号进行分割,传入的时候,按照顺序传入...

Python技能:Pathlib面向对象操作路径,比os.path更现代!

在Python编程中,文件和目录的操作是日常中不可或缺的一部分。虽然,这么久以来,钢铁老豆也还是习惯性地使用os、shutil模块的函数式API,这两个模块虽然功能强大,但在某些情况下还是显得笨重,不...

使用Python实现智能物流系统优化与路径规划

阅读文章前辛苦您点下“关注”,方便讨论和分享,为了回馈您的支持,我将每日更新优质内容。在现代物流系统中,优化运输路径和提高配送效率是至关重要的。本文将介绍如何使用Python实现智能物流系统的优化与路...

Python if 语句的系统化学习路径(python里的if语句案例)

以下是针对Pythonif语句的系统化学习路径,从零基础到灵活应用分为4个阶段,包含具体练习项目和避坑指南:一、基础认知阶段(1-2天)目标:理解条件判断的逻辑本质核心语法结构if条件:...

[Python] FastAPI基础:Path路径参数用法解析与实例

查询query参数(上一篇)路径path参数(本篇)请求体body参数(下一篇)请求头header参数本篇项目目录结构:1.路径参数路径参数是URL地址的一部分,是必填的。路径参...

Python小案例55- os模块执行文件路径

在Python中,我们可以使用os模块来执行文件路径操作。os模块提供了许多函数,用于处理文件和目录路径。获取当前工作目录(CurrentWorkingDirectory,CWD):使用os....

python:os.path - 常用路径操作模块

应该是所有程序都需要用到的路径操作,不废话,直接开始以下是常用总结,当你想做路径相关时,首先应该想到的是这个模块,并知道这个模块有哪些主要功能,获取、分割、拼接、判断、获取文件属性。1、路径获取2、路...

原来如此:Python居然有6种模块路径搜索方式

点赞、收藏、加关注,下次找我不迷路当我们使用import语句导入模块时,Python是怎么找到这些模块的呢?今天我就带大家深入了解Python的6种模块路径搜索方式。一、Python模块...

每天10分钟,python进阶(25)(python进阶视频)

首先明确学习目标,今天的目标是继续python中实例开发项目--飞机大战今天任务进行面向对象版的飞机大战开发--游戏代码整编目标:完善整串代码,提供完整游戏代码历时25天,首先要看成品,坚持才有收获i...

python 打地鼠小游戏(打地鼠python程序设计说明)

给大家分享一段AI自动生成的代码(在这个游戏中,玩家需要在有限时间内打中尽可能多的出现在地图上的地鼠),由于我现在用的这个电脑没有安装sublime或pycharm等工具,所以还没有测试,有兴趣的朋友...

python线程之十:线程 threading 最终总结

小伙伴们,到今天threading模块彻底讲完。现在全面总结threading模块1、threading模块有自己的方法详细点击【threading模块的方法】threading模块:较低级...

Python信号处理实战:使用signal模块响应系统事件

信号是操作系统用来通知进程发生了某个事件的一种异步通信方式。在Python中,标准库的signal模块提供了处理这些系统信号的机制。信号通常由外部事件触发,例如用户按下Ctrl+C、子进程终止或系统资...

Python多线程:让程序 “多线作战” 的秘密武器

一、什么是多线程?在日常生活中,我们可以一边听音乐一边浏览新闻,这就是“多任务处理”。在Python编程里,多线程同样允许程序同时执行多个任务,从而提升程序的执行效率和响应速度。不过,Python...

用python写游戏之200行代码写个数字华容道

今天来分析一个益智游戏,数字华容道。当初对这个游戏颇有印象还是在最强大脑节目上面,何猷君以几十秒就完成了这个游戏。前几天写2048的时候,又想起了这个游戏,想着来研究一下。游戏玩法用尽量少的步数,尽量...

取消回复欢迎 发表评论: