Linux 系统服务管理
一份关于 systemd 的可用心智模型:PID 1、unit 与 target、服务生命周期、自定义 unit 文件的写法、journalctl 过滤、用 timer 替代 cron,以及一套有纪律的排障流程。
Linux 上所谓的"服务",本质上是一种"在那里待命"的长驻进程:负责对时、监听 SSH、处理 HTTP 请求、凌晨三点跑一次备份。这些进程几乎从来不会由人手工启动——必须有人在开机时拉起它们、在崩溃时把它们拉回来、抓住它们的日志、决定谁依赖谁、关机时按顺序优雅停掉。在所有现代发行版上,承担这个角色的就是 systemd。
这篇文章是我希望自己第一次被丢到生产服务器面前时就有人塞给我的那种"可用心智模型"。我们先从"为什么需要一个独立的服务管理器"讲起,把 unit / target 的模型搭起来,把日常真正会用的 systemctl 命令过一遍,再把一份 unit 文件逐行拆开看,最后覆盖与之相邻的几块阵地:用 journalctl 查日志、用 .timer 单元替代 cron,以及一套"服务起不来该怎么办"的步骤化排查清单。
1. 为什么需要服务管理器
systemd 之前的世界
裸的 Linux 内核能做的事情很有限:加载驱动、挂载根文件系统、然后启动唯一的一个程序(PID 1)。除此之外的一切都是别人的问题。历史上承担"别人"这个角色的是 SysV init:一个小程序,从 /etc/init.d/ 读取脚本、按字典序一个一个跑过去。每个脚本都是一段手写的 shell,自己负责把守护进程 fork 到后台、写 PID 文件、并假装知道服务到底起没起来。
这套办法能跑,但它累积了三个结构性问题:
- 串行启动很慢。 在一台现代服务器上挨个跑 80 个 init 脚本,多核 CPU 大半时间在等 I/O。
- 依赖关系藏在脚本自己里。 nginx 需要先有网络?那��就指望字典序帮你忙(
S20network排在S80nginx前面),或者在脚本里撒几个sleep。听起来有多脆弱,实际就有多脆弱。 - 没有共享的状态概念。 每个守护进程都自己决定怎么进入后台、PID 写哪里、日志写哪里、崩了怎么重启。init 系统除了"脚本退出码是不是 0"以外,没有任何办法知道"sshd 现在到底活着没"。
systemd 把这一切替换成一个统一的监管者:它能区分"启动命令返回了"和"服务真的就绪了",能并行尽可能多的事情,并把发生过的一切都写进结构化的日志。
一张图看懂 systemd 模型

从上往下三层:
- PID 1——
systemd本身,由内核作为第一个用户态进程启动。系统运行期间它永不退出。 - Unit——系统上每一个可被管理的对象都是一个 unit:一个守护进程(
.service)、一个等连接的套接字(.socket)、一个定时任务(.timer)、一个挂载点(.mount),等等。systemd 读取 unit 文件、追踪每个 unit 的状态、并在状态之间驱动它。 - Target——把若干 unit 组合在一起的"同步点"。
multi-user.target是经典的"机器起来了、对外提供服务"状态;graphical.target在它之上再叠一层桌面。Target 取代了 SysV 的runlevel概念,但更灵活——任何 unit 都可以把任何 target 拉进来。
具体来说,当你敲 systemctl restart nginx 时,你是在请求 PID 1 把 nginx.service 这个 unit 走一遍状态机:停掉当前进程组、等它退、再次执行 ExecStart、监视就绪、刷新缓存的状态。本文后面所有命令,都是在这个主题上的变奏。
值得记住的几种 unit 类型
| Unit 类型 | 管理什么 | 例子 |
|---|---|---|
.service | 长驻进程(90% 的场景) | sshd.service、nginx.service |
.socket | 监听套接字,首次连接时再拉起对应服务 | sshd.socket、docker.socket |
.target | 一组 unit 的命名集合,作为同步点 | multi-user.target、network-online.target |
.timer | 触发另一个 unit 的定时器(cron 的替代) | logrotate.timer、apt-daily.timer |
.mount | 一个挂载点,由 /etc/fstab 或 unit 文件派生 | home.mount、var-log.mount |
.path | 监视文件或目录,发生变化时激活某个 unit | systemd-tmpfiles-clean.path |
.slice | 一个 cgroup 容器,用来对一组 unit 统一施加资源限制 | system.slice、user.slice |
本文里说"服务"基本上就是指 .service unit。其他类型共享同一套生命周期和同一套管理命令,所以理解了 service,剩下的都是顺势的事。
2. systemctl:日常会用的那些命令
systemctl 是你的唯一入口。把常用子命令记到肌肉里是值得的——在一台繁忙的服务器上,你一天会敲它几十遍。
启停、重启、reload
| |
restart 和 reload 的区别在生产环境里很要紧。restart 永远管用,但会把服务正在做的事情拦腰打断;reload 优雅得多——nginx 的 reload 会拉起新的 worker、让老 worker 把现有连接处理完——但这事只有服务作者能实现。systemctl cat nginx.service 可以告诉你它有没有定义 ExecReload=。
开机自启的开关
| |
enable 和 start 是两件独立的事。一个刚装好的包通常会被启动,但不会被设成自启——它会跑到下次重启然后消失。--now 标志把这两件事合并,避免你忘掉其中之一。
还有个微妙的命令:mask——比 disable 更狠。它会把 unit 指向 /dev/null,结果是你不可能"不小心"启动它,也不可能被别的 unit 作为依赖拉起来。当你确定要把一个服务彻底"隔离"时使用(sudo systemctl mask firewalld);用 unmask 撤回。
列举与查看
| |
其中两个被严重低估。systemctl cat 是正确查看 unit 文件的方式——它会把 /usr/lib/systemd/system/ 下的基础文件和 /etc/systemd/system/<unit>.d/ 下的 drop-in 覆盖拼起来,这正是 systemd 自己看到的。systemctl list-dependencies 则把启动顺序图可视化出来,当某个服务因为某个上游没起而起不来时,这个命令是救命的。
3. 服务的生命周期
服务一旦启动,就开始在一个小型状态机里活动。systemctl status 输出里的每一行状态,都是这个状态机里的一个标签。

- inactive——unit 存在,但没有对应的进程在跑。
- activating——
ExecStart=正在执行。Type=simple时这一步几乎一闪而过;Type=notify则会停在这里直到服务调用sd_notify(READY=1)。 - active——服务在跑。对大多数服务来说意味着主进程活着;对
oneshot单元来说意味着命令成功返回。 - deactivating——
ExecStop=正在执行,或者 systemd 在发送信号(先 SIGTERM,超过TimeoutStopSec后 SIGKILL)。 - failed——服务非零退出、被信号杀死,或者 watchdog 触发。停在这个状态直到你重启或重置(
systemctl reset-failed)。
最关键的一条转换是图里那条虚线回路:failed → activating。配上 Restart=on-failure(或者 Restart=always),systemd 会把 failed 视为暂态,等 RestartSec 秒后再次执行 ExecStart=。这就是为什么你在 systemd 之上不再需要 Monit 之类的东西——监管者本身已经在那里了。
4. 写一个属于你自己的服务
从"我有个脚本"到"它能扛得住重启和崩溃",中间只隔着一个 unit 文件。假设你在 /usr/local/bin/myapp 放了一个 HTTP 服务程序,想让它表现得像一个真正的服务。
一个最小可用的 unit 文件
新建 /etc/systemd/system/myapp.service:
| |
接着让 systemd 注意到它、启动它、并设为自启:
| |
完了。这个服务现在崩溃后自动重启、重启后能回来、以非 root 用户运行,stdout/stderr 都进了 journal,可以用 journalctl -u myapp 查询。
unit 文件的解剖

文件分成三段,每一段回答一个不同的问题。
[Unit]——这是什么、谁依赖谁
| |
这里要理解的关键对子是顺序和依赖。After= 和 Before= 只声明什么时候——不会把别的东西拉起来。Wants= 和 Requires= 只声明谁还得在跑——不规定先后。绝大多数情况你两者都需要:既 Wants=network-online.target,也 After=network-online.target,否则 systemd 完全可能"在一个根本没被请求、也就根本没启动的 target 之后"启动你的服务。
[Service]——具体怎么把进程跑起来
| |
Type= 这个旋钮值得单列一张表:
| Type | systemd 何时认为服务"已启动" | 适用场景 |
|---|---|---|
simple(默认) | ExecStart= 一执行立刻算 | 前台进程——大多数现代守护进程、脚本、容器里跑的东西 |
exec | 二进制被 exec() 后立刻算 | 类似 simple,就绪判定稍严格 |
forking | 父进程退出、留下子进程时算 | 老式 double-fork 的守护进程 |
oneshot | ExecStart= 返回 0 时算。可以 active 而没有任何进程 | 初始化任务、挂载辅助、幂等脚本,常配 RemainAfterExit=yes |
notify | 服务自己调用 sd_notify(READY=1) 时算 | 任何想精确就绪信号的服务——数据库、调度器、所有依赖它的下游服务 |
[Install]——什么时候启用
| |
[Install] 段只会被 systemctl enable / disable 读取。它告诉 systemd:“启用我时,请把我塞到这个 target 里”——服务端 99% 的情况都是 multi-user.target。没有 [Install] 段的 unit 是 “static” 的,enable 会直接拒绝执行。
修改 unit 的正确方式
发行版包提供的 unit 文件放在 /usr/lib/systemd/system/。别去改它们。 用 drop-in 覆盖:
| |
它会打开一个空文件,路径是 /etc/systemd/system/nginx.service.d/override.conf。你写在这里的内容会叠在厂商 unit 之上。如果想完整替换厂商 unit(少见),用 systemctl edit --full。任何修改之后:
| |
daemon-reload 只是重新读 unit 文件,并不会重启任何东西。忘了它,是"我改了文件却没生效"的头号原因。
5. journalctl:日志已经在那里了
systemd 自带 journald 这个日志守护进程,它会捕获每个服务的 stdout/stderr,外加所有写到 syslog 的内容。不用额外配置——服务往 stdout 一打,journal 就记下来了。

你需要的过滤器
| |
其中两条在事故响应时是顶梁柱。journalctl -u <svc> -p err -b(“本次开机里这个 unit 的错误日志”)通常是我排查失败时第一条命令。journalctl -b -1 用来回看一次"莫名其妙"的重启之前发生了什么——那些消息否则就消失了。
优先级用的是 syslog 的那套,0(最严重)到 7(debug):emerg、alert、crit、err、warning、notice、info、debug。-p err 意思是"err 级及以上",即 0 到 3。
让 journal 跨重启留存
很多发行版默认把 journal 写在 /run/log/journal/——一个 tmpfs,重启就清空。在你需要"重启前"的日志之前,这都没问题。让它持久化:
| |
也可以编辑 /etc/systemd/journald.conf 来限制磁盘占用(SystemMaxUse=2G、MaxRetentionSec=30day 等),改完 reload systemd-journald。
清理
| |
6. Timer:现代化的 cron
cron 在每台 Linux 上都还能用。但在一台 systemd 系统上,.timer unit 通常是更好的选择——它和别的东西共用同一个 journal、以正经 unit 的身份跑(也就有重启策略、资源限制、依赖关系),还能扛过错过的执行(Persistent=true)。
一个 timer 总是成对出现:一个 .timer 按时点火,一个 .service 真正干活。
/etc/systemd/system/backup.service:
| |
/etc/systemd/system/backup.timer:
| |
启用的是 timer,不是 service:
| |
OnCalendar= 的语法很丰富——Mon..Fri 09:00、hourly、weekly、*-*-1 04:00:00 表示"每月 1 号 04:00"等等。systemd-analyze calendar 'Mon..Fri 09:00' 会告诉你某个表达式具体会解析到哪些时刻。
相比 cron,赢面有三个:失败会出现在 systemctl --failed 和 journal 里、和服务自己的日志躺在一起;任务继承 [Service] 段的所有加固/限制旋钮;Persistent=true 解决了笔记本问题(cron 那个"应该 03:00 跑"但笔记本那时正在睡的任务,结果就是再也不会跑了)。
7. 启动时间线
大致知道从按下电源到登录提示符之间发生了什么,“为什么这次启动用了 90 秒"才会变得可分析。

- 固件(BIOS/UEFI) 跑 POST,挑一个引导设备。
- 引导加载器(一般是 GRUB)把内核和
initramfs加载到内存,把控制权交给内核。 - 内核 初始化硬件、以只读方式挂上根文件系统、然后启动
/sbin/init——它是指向 systemd 的软链接。从这里开始,PID 1 接管。 - systemd 解析 unit 文件,开始走依赖图,能并行处就并行。
- Target 按顺序激活:
sysinit.target(早期挂载、swap、udev)→basic.target(socket、timer、path 已就位)→multi-user.target(所有 enable 的服务都活着,可以登录了)。桌面系统再往上叠一个graphical.target。
这里有三条诊断命令值回票价:
| |
只看 blame 是有误导性的——一个服务花 5 秒启动,如果没人在等它,它根本不会拖慢整个启动。critical-chain 才告诉你哪个慢服务真在关键路径上。优先优化那个。
8. 几个常见服务的 60 秒手册
这一节是你最常碰的那几个服务的速查卡。
时间同步
时钟漂移迟早会击穿一切——TLS 握手、日志关联、Kerberos、分布式数据库复制。在一台 systemd 系统上最简单的答案是内建客户端:
| |
timedatectl 启用的是 systemd-timesyncd,一个轻量的 SNTP 客户端,对客户端和大多数服务器都够用。如果你需要正经的 NTP 实现(高精度集群、对外提供时间服务),装 chrony 并禁用 timesyncd。
避免使用 ntpdate。它会把时钟瞬间跳到目标值,会把任何"假定时间只前进"的东西打坏(cron、journald 顺序、复制)。真正的 NTP 客户端是平滑修正的。
防火墙(firewalld)
| |
不带 --permanent,规则只活到下次 reload;带了 --permanent 但不 --reload,规则会活到下次 reload,但现在不生效。实际操作里你两个都要带。
Zone(public、work、home、internal、dmz、trusted、drop、block)是 firewalld 用来表达"这个网卡接的是哪种网络、就套哪套规则"的方式。在单网口的服务器上,你通常把所有东西留在 public,然后在那里开端口。
SSH 加固
/etc/ssh/sshd_config 的默认值在 Debian/Ubuntu 上是保守的,在别的发行版上就未必。高价值的几条改动:
Port 22 # 改默认端口要权衡运维成本
PermitRootLogin no # 用普通用户登录,再 sudo
PasswordAuthentication no # 只用密钥——把"暴力破解"这一整类问题消除掉
PubkeyAuthentication yes
PermitEmptyPasswords no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
reload 之前永远先校验——一个写坏的配置文件足以把你锁在远程机器外面:
| |
如果 SSH 端口暴露在公网且你想要 IP 级速率限制,再叠一层 fail2ban;否则光是密钥认证就足以让暴力破解毫无意义。
Cron(在 timer 不适用时)
如果你在一台不完全是 systemd 的系统上,或者要维护已经存在的 cron 任务:
# 分 时 日 月 星期 命令
30 1 * * * /usr/local/bin/backup.sh
*/10 * * * * /usr/local/bin/health-check.sh
0 3 * * 0 /usr/local/bin/weekly-cleanup.sh
用 crontab -e 编辑当前用户的任务、crontab -l 列出。命令里永远写绝对路径——cron 跑命令时 PATH 几乎是空的。stdout/stderr 都重定向出来(>> /var/log/myjob.log 2>&1),否则任务什么时候坏了都没人知道。
9. 排障流程:服务起不来该怎么办
服务起不来的时候,按顺序走下面这张清单。大部分情况你在前两步就能找到答案。
1. 先看 systemctl 跟你说了什么。
| |
重点看 Active:(状态以及持续多久)、Main PID:(如果是 0,说明进程没了)、Process: 行(实际跑了什么命令、退码是多少)、以及底部复述的最后 10 行日志。八成时候答案就在那里。
2. 看本次开机的完整日志。
| |
-x 会附上 systemd 的目录解释,-e 跳到末尾。如果服务被 Restart=on-failure 多次拉起,往前翻——那几次失败的原因经常不一样。
3. 在怪罪服务之前,先校验配置。
大多数守护进程都自带 config check:
| |
如果是自定义服务,以目标用户的身份手动跑一下 ExecStart= 命令:
| |
4. 找端口冲突。
| |
一个常见的失败模式:服务先前崩了一次,被拉起,新的进程 bind 不上,因为旧进程不知道为什么还在占着那个端口(或者完全无关的另一个进程占了)。
5. 找权限和路径问题。
| |
如果服务以 myapp 用户跑,但工作目录被 root 拥有且 mode 700,它会以一种看起来很神秘的方式起不来。namei -l 把"搜索权限"的链条可视化出来。
6. 别忘了安全层。
RHEL 系上 SELinux 可能在权限看起来都对的情况下挡住服务;Ubuntu 上 AppArmor 同理。
| |
如果切到 permissive 问题就消失,正确的做法是写一份合适的 SELinux 策略或 AppArmor profile,而不是把强制模式一直关着。
7. 沿着依赖图走一遍。
| |
如果你这个 unit Requires= 的某个东西自己就是 failed,症状会上溯到那里。先修那个。
10. 后续阅读
到这里你应该有了一个可用的心智模型:PID 1 监管 unit、unit 在状态机里活动、unit 文件描述了这份监管契约、journald 把发生的一切记录下来,而 systemctl 和 journalctl 是你向里看的两扇窗。从这里继续:
man systemd.service和man systemd.unit是权威参考——短、密、值得读一遍。man systemd.exec列出了[Service]里所有可用的沙箱/限制旋钮。其中大多数都是免费的安全加固。- freedesktop.org/wiki/Software/systemd/ 是官方文档以及 Lennart Poettering 那套优秀的"systemd for Administrators"系列。
systemd-analyze security <unit>会按沙箱姿态给每个跑着的服务打分,并告诉你拧哪几个旋钮可以收紧它。
本系列后面两篇会讲 软件包管理(去装那些之后会被你做成服务的守护进程)和 进程与资源管理(看那些服务起来之后到底在干什么)。本文里建立的心智模型——服务作为受监管的 unit、有显式依赖、有结构化日志——会一路贯穿过去。