第59课:编写 systemd Unit 文件
所属模块:进程管理
难度:高级
标签:systemd, unit-files
上一课回顾:systemctl 服务管理
下一课预告:journalctl 日志查看
1. 学习目标
通过本课程的学习,你将能够:
- 理解 systemd Unit 文件的基本结构和语法。
- 编写一个用于管理自定义应用程序(如脚本、二进制程序)的
.serviceUnit 文件。 - 掌握关键的
Service段落指令,以控制进程的启动、重启行为和环境。 - 配置服务的依赖关系、启动顺序和安装信息。
- 验证、启用并测试自定义的 systemd 服务。
2. 核心概念
在上一课中,我们学习了如何使用 systemctl 命令来管理(启动、停止、查看状态)由 systemd 提供的服务。但这些服务是如何被 systemd 识别和管理的呢?答案就是 Unit 文件。
你可以将 Unit 文件想象成一份给 systemd 的 “说明书”。它以纯文本形式定义了一个服务(或一个挂载点、设备、套接字等)应如何运行。systemd 在启动时或被要求时,会读取这些说明书,并严格按照指令来管理对应的进程。
一个典型的 .service Unit 文件主要包含三个部分:
[Unit]段落:提供通用的描述信息,如服务名称、描述,并定义与其他 Unit 的依赖和顺序关系(例如,必须在网络就绪后才能启动)。[Service]段落:核心部分,定义服务如何运行。包括启动命令、进程类型、重启策略、工作目录、环境变量等。[Install]段落:定义当服务被 “启用”(enable)时应如何链接。通常指定服务在哪个“运行级别”或“目标”下启动,最常见的是WantedBy=multi-user.target。
3. 代码示例
让我们通过一个具体的例子来学习。假设我们有一个简单的 Python HTTP 服务器脚本,我们希望将它作为一个系统服务来管理。
第一步:创建应用程序脚本
创建一个简单的 Python 脚本,用于提供一个 Web 服务页面。
文件路径:/usr/local/bin/my-simple-web.py
#!/usr/bin/env python3
"""
一个简单的Python HTTP服务器,用于演示。
"""
import http.server
import socketserver
import os
import signal
import sys
# 设置工作目录和端口
PORT = 8000
# 为了简单,我们让脚本在 /tmp 目录下运行并生成文件
os.chdir("/tmp")
# 写入一个简单的HTML文件作为首页
with open("index.html", "w") as f:
f.write("<html><body><h1>Hello from my systemd service!</h1></body></html>")
# 定义处理器
Handler = http.server.SimpleHTTPRequestHandler
# 创建并启动服务器
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print(f"Serving on port {PORT}", flush=True)
# 优雅地处理停止信号
def signal_handler(sig, frame):
print("Shutting down server...", flush=True)
httpd.shutdown()
sys.exit(0)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
httpd.serve_forever()
第二步:创建 systemd Unit 文件
这是本课的重点。我们为上述 Python 服务创建 Unit 文件。
文件路径:/etc/systemd/system/my-simple-web.service
# /etc/systemd/system/my-simple-web.service
# 描述:一个用于演示的简单Python HTTP服务
[Unit]
Description=My Simple Python HTTP Server
# 确保网络已经就绪后才启动我们的服务
After=network.target
# 声明我们的服务依赖网络(可选,但更明确)
Requires=network.target
[Service]
# 指定服务类型:`simple` 表示主进程就是由 ExecStart 启动的进程
Type=simple
# 执行启动命令的完整路径。我们使用 python3 解释器来运行我们的脚本。
# 注意:`/usr/bin/env python3` 路径在 Unit 文件中可能不可靠,最好用 `which python3` 查找绝对路径。
ExecStart=/usr/bin/python3 /usr/local/bin/my-simple-web.py
# 设置工作目录。脚本中 os.chdir("/tmp") 会生效,但这里设置可以覆盖或确认。
WorkingDirectory=/tmp
# 定义重启策略:当进程异常退出(非正常信号如 SIGTERM, SIGINT)时,总是尝试重启。
Restart=on-failure
# 定义重启前的等待时间(秒)。
RestartSec=5
# 定义服务启动前等待的超时时间。
TimeoutStartSec=30
# 定义服务停止的超时时间。
TimeoutStopSec=10
# 定义标准输出和错误输出的日志标识符,便于用 journalctl 按名字过滤。
SyslogIdentifier=my-simple-web
# 定义进程运行的用户和组(安全考虑,避免以 root 运行不必要的服务)。
# User=nobody
# Group=nogroup
[Install]
# 定义当服务被 `systemctl enable` 时,它希望被哪个目标(运行级别)依赖。
# `multi-user.target` 类似于传统的运行级别 3,是一个非图形的多用户命令行环境。
WantedBy=multi-user.target
第三步:验证、启用和启动服务
# 1. 重新加载 systemd 配置,让它知道我们添加了新文件
sudo systemctl daemon-reload
# 2. 验证 Unit 文件语法是否正确(虽然不是必须的,但推荐)
systemd-analyze verify /etc/systemd/system/my-simple-web.service
# 3. 启动服务
sudo systemctl start my-simple-web.service
# 4. 查看服务状态
sudo systemctl status my-simple-web.service
# 5. 查看服务的实时日志
sudo journalctl -u my-simple-web.service -f
# 6. 测试服务是否正常工作
curl http://localhost:8000
# 应输出:<html><body><h1>Hello from my systemd service!</h1></body></html>
# 7. (可选) 设置服务开机自启
sudo systemctl enable my-simple-web.service
4. 实践练习
练习1:修改现有服务配置
- 修改我们创建的
my-simple-web.service文件,将监听端口从 8000 改为 9000。 - 同时,将运行用户改为
nobody(取消[Service]段中User和Group行的注释)。 - 重新加载配置并重启服务。
- 使用
curl http://localhost:9000测试,并用systemctl status查看运行用户是否已变更。
练习2:为另一个脚本创建服务
- 创建一个新的Bash脚本
/usr/local/bin/heartbeat.sh,内容如下:#!/bin/bash while true; do echo "Heartbeat at $(date)" >> /tmp/heartbeat.log sleep 10 done - 为这个脚本编写一个对应的 Unit 文件
/etc/systemd/system/heartbeat.service。 - 要求:
- 描述为 “Heartbeat Logger Service”。
- 服务类型为
simple。 - 确保脚本有执行权限(
chmod +x)。 - 设置当服务失败时,在 15 秒后重启。
- 将标准输出也重定向到系统日志(默认行为)。
- 启用并启动该服务,然后检查
/tmp/heartbeat.log文件是否每 10 秒追加一条记录。
练习3:理解依赖与顺序
- 思考并回答:如果我们把
my-simple-web.service中的After=network.target注释掉,服务可能会在什么情况下启动失败?为什么?
5. 常见错误
-
路径错误:
ExecStart中的命令或脚本路径不是绝对路径,导致 systemd 找不到可执行文件。- 工作目录 (
WorkingDirectory) 不存在,导致应用程序启动失败。 - 解决:始终使用
which <command>或realpath <script>获取绝对路径。
-
权限不足:
- Unit 文件本身权限不对(应由 root 拥有,权限为 644)。
ExecStart指向的脚本或程序没有可执行权限 (chmod +x)。- 以非 root 用户(如
nobody)运行时,该用户无权访问脚本、工作目录或需要的资源(如端口 <1024)。 - 解决:检查文件权限和所有权。使用
sudo -u nobody <command>提前测试。
-
依赖与顺序混淆:
- 混淆
Requires和After。Requires是强依赖,如果依赖的 Unit 启动失败,本 Unit 也会失败。After只是声明启动顺序,但不强求依赖存在。 - 解决:通常
Requires和After一起使用。对于可选的弱依赖,可使用Wants和After。
- 混淆
-
服务类型 (
Type) 设置错误:- 最常见的错误是将
forking类型的程序(如传统守护进程,自己 fork 到后台)设置为Type=simple,导致 systemd 认为进程已退出而不断重启。 - 解决:确定你的程序是前台运行(用
simple或exec)还是后台运行(用forking)。
- 最常见的错误是将
-
忘记
daemon-reload:- 修改了 Unit 文件后,没有执行
sudo systemctl daemon-reload,导致更改未被 systemd 识别。 - 解决:每次编辑 Unit 文件后,务必执行此命令。
- 修改了 Unit 文件后,没有执行
6. 小结
本节课我们深入学习了如何通过编写 Unit 文件来将自定义应用程序纳入 systemd 的管理体系。关键要点回顾:
- Unit 文件是核心:它是 systemd 理解和管理服务的配置蓝图。
- 三大段落:
[Unit]定义描述和依赖关系,[Service]定义运行细节,[Install]定义安装链接。 - 关键指令:
ExecStart:启动命令。Type:服务进程类型(simple,forking,oneshot等)。Restart:重启策略(on-failure,always)。After/Requires/Wants:控制启动顺序和依赖。
- 工作流程:编写文件 →
daemon-reload→start/enable→status/journalctl查看。 - 安全与健壮性:考虑使用非特权用户运行服务、设置合理的超时和重启策略。
掌握 Unit 文件的编写,意味着你能够将几乎任何应用程序或脚本转化为易于管理、可监控、具备自愈能力的系统服务,这是 Linux 系统管理员和 DevOps 工程师的核心技能之一。