系列 · Linux · 第 6 篇

Linux(六):系统服务管理

一份关于 systemd 的可用心智模型:PID 1、unit 与 target、服务生命周期、自定义 unit 文件的写法、journalctl 过滤、用 timer 替代 cron,以及一套有纪律的排障流程。

在 Linux 系统中,“服务”指的是长时间运行的后台进程,负责同步时钟、监听 SSH 连接、处理 HTTP 请求或在凌晨三点执行备份任务;这些服务通常不由用户手动启动,而是由一个管理者来完成一系列工作:开机时启动、崩溃时重启、收集日志、理清依赖关系并在关机时有序关闭。在现代 Linux 发行版中,这个管理者就是 systemd

Linux(六):系统服务管理 — 章节概览图

这篇文章,正是我初入生产环境服务器时最需要的实用指南。我们将从基础讲起:先说明为何需要专门的服务管理器,再逐步构建 unit 和 target 的核心概念模型。接着,我们会梳理日常工作中真正用得上的 systemctl 命令,逐行解析一份典型的 unit 文件内容。最后,我们还会扩展到相关的周边工具和技巧:如何用 journalctl 查看日志、如何用 .timer 单元替代传统的 cron 任务,以及一套详细的排查步骤,帮助你解决“服务无法启动”的问题。


为什么需要服务管理器#

systemd 出现之前的问题#

Linux 内核本身的功能有限:它能加载驱动、挂载根文件系统,然后启动一个程序(PID 1)。除此之外的事情,就全靠其他工具来完成了。在 systemd 出现之前,这个任务主要由 SysV init 承担:这是一个简单的程序,会从 /etc/init.d/ 目录读取脚本,并按字母顺序依次执行。每个脚本都是一段手写的 shell 脚本,负责将守护进程放到后台运行、写入 PID 文件,还得假装知道服务是否真的启动成功。

这套机制虽然能用,但随着时间推移暴露出三个结构性问题:

  1. 串行启动太慢: 在现代服务器上,按顺序跑 80 个 init 脚本会让多核 CPU 大部分时间都在等待 I/O 操作完成。
  2. 依赖关系埋在脚本里: 如果 nginx 需要网络先启动,那只能祈祷字母顺序帮忙(比如 S20network 排在 S80nginx 前面),或者在脚本里硬塞几个 sleep 命令。听起来有多不靠谱,实际就有多脆弱。
  3. 缺乏统一的状态管理: 每个守护进程自己决定如何进入后台、 PID 文件存哪里、日志怎么写、崩溃后怎么重启。 init 系统除了看脚本退出码是不是 0,完全没办法判断像 sshd 这样的服务到底有没有正常运行。

systemd 的出现彻底改变了这一局面:作为统一的监管者,它不仅能区分“启动命令已返回”和“服务真正就绪”,还能尽可能并行执行任务,并将所有事件记录到结构化的日志中。

一张图看懂 systemd 的工作模型#

systemd 架构:PID 1、unit 与 target

从上到下分为三层:

  • 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.servicenginx.service
.socket监听套接字,首次连接时启动对应服务sshd.socketdocker.socket
.target一组 unit 的命名集合,作为同步点multi-user.targetnetwork-online.target
.timer定时触发另一个 unit (替代 cron)logrotate.timerapt-daily.timer
.mount文件系统挂载点,由 /etc/fstab 或 unit 文件定义home.mountvar-log.mount
.path监视文件或目录,发生变化时激活某个 unitsystemd-tmpfiles-clean.path
.slicecgroup 容器,用于对一组 unit 统一施加资源限制system.sliceuser.slice

本文中提到的“服务”通常指 .service 类型的 unit。其他类型的 unit 共享相同的生命周期和管理命令,因此一旦理解了 service,剩下的类型也就触类旁通了。

systemctl:日常会用的那些命令#

systemctl 是你与 systemd 交互的主要入口。对于常用子命令,建议熟练到形成肌肉记忆——在繁忙的服务器上,你每天可能需要输入它们几十次。

启动、停止、重启与重新加载#

1
2
3
4
5
6
sudo systemctl start  nginx       # 立即启动服务,但不会在重启后保留状态
sudo systemctl stop   nginx       # 停止服务
sudo systemctl restart nginx      # 先停止再启动(会导致连接中断)
sudo systemctl reload  nginx      # 请求守护进程重新加载配置文件
                                  # (仅当 unit 文件中定义了 ExecReload 时有效)
sudo systemctl status  nginx      # 查看服务当前状态及最近 10 行日志

在生产环境中,restartreload 的区别非常重要。restart 总是有效,但它会直接中断服务正在进行的操作;而 reload 则更加优雅——例如, nginx 的 reload 会启动新的 worker 进程处理新配置,同时让旧的 worker 完成现有连接的处理。不过,reload 的实现依赖于服务作者的支持。通过 systemctl cat nginx.service 可以检查是否定义了 ExecReload=

开机自启管理#

1
2
3
4
5
6
sudo systemctl enable  nginx      # 设置服务在下次开机时自动启动(创建一个 .wants/ 软链接)
sudo systemctl disable nginx      # 移除开机自启的软链接
sudo systemctl is-enabled nginx   # 输出:enabled / disabled / static / masked

sudo systemctl enable  --now nginx   # 一步完成:设置开机自启并立即启动
sudo systemctl disable --now nginx   # 一步完成:取消开机自启并立即停止

enablestart 是两个独立的操作。通常情况下,刚安装的软件包会被启动,但不会设置为开机自启——这意味着它会在下次重启后消失。--now 参数将这两个操作合并,避免你遗漏其中之一。

还有一个更“强硬”的命令:mask。它的作用比 disable 更彻底,会将服务指向 /dev/null,从而完全阻止其运行——即使误操作或被其他服务依赖也无法启动。当你希望彻底禁用某个服务时可以使用(例如 sudo systemctl mask firewalld),并通过 unmask 恢复。

查看与排查#

1
2
3
4
5
6
7
8
systemctl list-units --type=service --state=running   # 当前正在运行的服务
systemctl list-units --type=service --all             # systemd 已知的所有服务
systemctl list-units --type=service --state=failed    # 排查问题的起点
systemctl list-unit-files --type=service              # 磁盘上的所有服务文件,无论是否启用

systemctl cat   sshd.service                          # 查看服务的完整 unit 文件(包括所有 drop-in 配置)
systemctl show  sshd.service                          # 显示 systemd 跟踪的所有属性
systemctl list-dependencies sshd.service              # 展示 After/Requires/Wants 的依赖关系树

有两个命令特别值得关注,但常常被忽视。systemctl cat 是查看 unit 文件的正确方式——它会将 /usr/lib/systemd/system/ 下的基础文件与 /etc/systemd/system/<unit>.d/ 中的 drop-in 配置合并显示,这正是 systemd 实际看到的内容。而 systemctl list-dependencies 则能将服务的启动顺序图可视化,当某个服务因上游依赖未启动而无法运行时,这个命令非常有用。

服务的生命周期#

当你启动一个服务后,它就会进入一个小型的状态机中运行。通过 systemctl status 查看服务状态时,输出的每一行都对应着状态机中的某个具体状态。

服务生命周期:systemctl status 报告的状态

  • inactive —— 服务单元已定义,但没有任何进程在运行。
  • activating —— 正在执行 ExecStart= 指定的启动命令。如果是 Type=simple 类型,这个状态几乎是瞬间完成的;而对于 Type=notify 类型,服务会停留在这个状态,直到它调用 sd_notify(READY=1) 通知 systemd 已准备好。
  • 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)——因为 systemd 本身已经内置了强大的监管功能。

写一个属于你自己的服务#

从“我写了个脚本”到“它能在重启和崩溃后依然正常运行”,只需要一个 unit 文件就能搞定。假设你有一个 HTTP 服务程序放在 /usr/local/bin/myapp,现在你想让它像一个真正的系统服务一样工作。

最简化的 unit 文件#

创建 /etc/systemd/system/myapp.service 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[Unit]
Description=My Custom Application
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/myapp --port 8080
Restart=on-failure
RestartSec=5
User=myapp
Group=myapp

[Install]
WantedBy=multi-user.target

然后让 systemd 感知、启动并设置开机自启:

1
2
3
sudo systemctl daemon-reload          # 重新加载磁盘上的 unit 文件
sudo systemctl enable --now myapp     # 立即启动,并在每次开机时自动运行
sudo systemctl status myapp

搞定!现在这个服务会在崩溃后自动重启,重启系统后也能恢复运行,以非 root 用户身份执行,日志输出到 journal 中,你可以用 journalctl -u myapp 查看日志。

unit 文件的结构解析#

unit 文件结构详解

unit 文件分为三个部分,每一部分回答不同的问题。

[Unit]——这是什么?依赖关系如何?#

1
2
3
4
5
6
7
[Unit]
Description=My Custom Application       # 一句简单易懂的描述
Documentation=https://example.com/docs   # 会显示在 `systemctl status` 中
After=network-online.target              # 启动顺序:在网络服务之后启动
Wants=network-online.target              # 弱依赖:如果网络服务挂了,我不受影响
Requires=postgresql.service              # 强依赖:如果 PostgreSQL 停止,我也停止
ConditionPathExists=/etc/myapp.conf      # 如果配置文件不存在,则跳过激活

这里需要重点理解的是顺序依赖的区别。After=Before= 只定义时间顺序,不会主动拉起其他服务;Wants=Requires= 则定义谁必须在运行,但不涉及顺序。大多数情况下,你需要同时设置两者,比如 Wants=network-online.targetAfter=network-online.target。否则, systemd 可能会在某个从未被请求启动的目标之后启动你的服务。

[Service]——如何实际运行进程?#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[Service]
Type=simple                                    # 类型,详见下表
ExecStart=/usr/local/bin/myapp --port 8080     # 必须是绝对路径
ExecReload=/bin/kill -HUP $MAINPID             # 可选,支持 `systemctl reload`
ExecStop=/usr/local/bin/myapp --shutdown       # 可选,默认发送 SIGTERM
Restart=on-failure                             # no | on-failure | on-abnormal | always
RestartSec=5                                   # 重启前等待时间,避免崩溃循环
User=myapp                                     # 网络服务切勿以 root 身份运行
Group=myapp
WorkingDirectory=/var/lib/myapp
Environment=LOG_LEVEL=info
EnvironmentFile=/etc/myapp/myapp.env           # 每行一个 KEY=VALUE
# 使用 cgroup 限制资源
MemoryMax=512M
CPUQuota=50%
TasksMax=4096
# 高性价比的安全加固
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true

Type= 是一个值得单独说明的选项,以下是它的含义:

类型systemd 认为服务“已启动”的时机适用场景
simple(默认)ExecStart= 执行后立即视为启动前台进程——现代守护进程、脚本、容器中的应用
exec二进制文件被 exec() 后立即视为启动类似 simple,但就绪判定更严格
forking父进程退出、子进程仍在运行时视为启动传统 double-fork 守护进程
oneshotExecStart= 返回 0 时视为启动,允许无进程运行初始化任务、挂载辅助、幂等脚本,常配 RemainAfterExit=yes
notify服务调用 sd_notify(READY=1) 时视为启动数据库、调度器等需要精确就绪信号的服务

[Install]——何时启用服务?#

1
2
3
[Install]
WantedBy=multi-user.target
# Alias=myapp-alt.service   (可选的别名)

[Install] 段仅在执行 systemctl enabledisable 时被读取。它告诉 systemd:“启用该服务时,请将它加入哪个目标”。对于 99% 的服务器服务来说,这个目标通常是 multi-user.target。如果没有 [Install] 段, unit 文件会被视为“静态”,enable 操作将拒绝执行。

正确修改 unit 文件的方式#

发行版提供的 unit 文件通常位于 /usr/lib/systemd/system/不要直接修改这些文件。正确的做法是使用 drop-in 配置覆盖:

1
sudo systemctl edit nginx.service

这会在 /etc/systemd/system/nginx.service.d/override.conf 中打开一个空文件。你在这里添加的内容会叠加到原始 unit 文件之上。如果需要完全替换原始文件(很少见),可以使用 systemctl edit --full。修改完成后,执行以下命令:

1
2
sudo systemctl daemon-reload          # 重新加载 unit 文件改动
sudo systemctl restart nginx          # 将改动应用到正在运行的进程

注意,daemon-reload 只是重新读取 unit 文件,不会重启任何服务。忘记执行这一步,是导致“改了配置却没生效”的最常见原因。

journalctl:日志已经在那里了#

systemd 内置了一个叫 journald 的日志守护进程,它会捕获每个服务的标准输出(stdout)和标准错误(stderr),同时还能记录所有写入 syslog 的内容。完全不用额外配置——只要你的服务往标准输出打印信息, journald 就会自动帮你记录下来。

journalctl 过滤器:一个日志系统,多种筛选方式

常用的过滤命令#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
journalctl -u nginx                        # 查看某个服务的日志
journalctl -u nginx -f                     # 实时跟踪新日志(类似 tail -f)
journalctl -u nginx -n 100                 # 显示最近 100 行日志
journalctl -u nginx --since "10 min ago"   # 支持人类可读的时间表达
journalctl -u nginx --since "2025-02-01" --until "2025-02-02"
journalctl -u nginx -p err                 # 只显示错误及更高级别的日志
journalctl -u nginx -o json-pretty         # 输出完整的结构化日志
journalctl -b                              # 仅查看当前启动周期的日志
journalctl -b -1                           # 查看上一次启动周期的日志
journalctl -k                              # 仅查看内核日志(相当于 dmesg)
journalctl _PID=1234                       # 查看指定进程 ID 的日志
journalctl _UID=1000 _COMM=python3         # 组合任意字段进行筛选

在排查问题时,有两个命令特别关键。journalctl -u <svc> -p err -b(“当前启动周期中某服务的错误日志”)通常是我排查故障时的第一选择。而 journalctl -b -1 则是回溯意外重启前发生了什么的关键工具——否则那些日志可能早就被清除了。

日志优先级沿用了 syslog 的标准,从 0 (最严重)到 7 (调试信息)依次为:emergalertcriterrwarningnoticeinfodebug。使用 -p err 时,表示“错误级别及以上”,即优先级 0 到 3 的日志。

让日志持久化保存#

很多 Linux 发行版默认将日志存储在 /run/log/journal/ 目录下,这是一个基于内存的临时文件系统(tmpfs),重启后就会被清空。如果你需要保留重启前的日志,可以将其改为持久化存储:

1
2
3
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

如果担心日志占用过多磁盘空间,可以通过编辑 /etc/systemd/journald.conf 文件来限制(例如设置 SystemMaxUse=2GMaxRetentionSec=30day 等),然后重新加载 systemd-journald 服务。

日志清理操作#

1
2
3
sudo journalctl --disk-usage                 # 查看日志占用的磁盘空间
sudo journalctl --vacuum-time=7d             # 保留最近 7 天的日志
sudo journalctl --vacuum-size=1G             # 限制日志总大小为 1 GiB

Timer:现代化的 cron#

虽然 cron 在所有 Linux 系统上依然可以正常使用,但在使用 systemd 的系统中,.timer 单元通常是更优的选择。它不仅能够与其他服务共享日志(journal),还能以标准单元的形式运行,从而支持重启策略、资源限制和依赖管理。更重要的是,即使错过了某个执行时间点,也可以通过 Persistent=true 配置在下次启动时补跑。

一个完整的定时任务由两部分组成:一个 .timer 文件负责定义触发时间,另一个 .service 文件则负责实际执行任务。

以下是 /etc/systemd/system/backup.service 的配置示例:

1
2
3
4
5
6
7
[Unit]
Description=Nightly database backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup

对应的 /etc/systemd/system/backup.timer 配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[Unit]
Description=Run database backup nightly

[Timer]
OnCalendar=*-*-* 02:30:00       # 每天凌晨 02:30 触发
RandomizedDelaySec=10min        # 在 10 分钟内随机延迟,避免多台机器同时执行
Persistent=true                 # 如果错过执行时间,启动后会立即补跑(适用于笔记本或虚拟机)
Unit=backup.service             # 指定触发的服务(默认与 timer 同名)

[Install]
WantedBy=timers.target

启用定时任务时,需要激活的是 timer 而非 service:

1
2
3
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers --all     # 查看每个 timer 的下次触发时间

OnCalendar= 的语法非常灵活,支持多种时间表达方式,例如:Mon..Fri 09:00(工作日每天 9 点)、hourly(每小时一次)、weekly(每周一次)、*-*-1 04:00:00(每月 1 日凌晨 4 点)等。如果不确定某个表达式的具体含义,可以使用以下命令解析:

1
systemd-analyze calendar 'Mon..Fri 09:00'

相比传统的 cronsystemd 的定时任务有以下几个优势:

  1. 执行失败时,错误信息会直接显示在 systemctl --failed 和 journal 日志中,并与服务的日志整合在一起,方便排查问题。
  2. 定时任务继承了 [Service] 段中的所有安全加固和资源限制配置,提升了任务的稳定性和安全性。
  3. Persistent=true 解决了设备休眠或关机导致任务丢失的问题。例如,cron 中设定的“凌晨 3 点执行”任务,如果设备当时处于休眠状态,任务将永远无法执行;而 systemd 的 timer 则会在设备重新启动后自动补跑。

这些特性使得 systemd 的定时任务机制更加适合现代复杂的系统环境。

启动时间线#

了解从按下电源到登录提示符之间的大致过程,可以帮助我们分析“为什么这次启动花了 90 秒”这样的问题。

启动时间线:固件 → 引导加载 → 内核 → systemd → multi-user.target

  1. 固件(BIOS/UEFI) 执行开机自检(POST),并选择一个启动设备。
  2. 引导加载程序(通常是 GRUB)将内核和 initramfs 加载到内存,并将控制权交给内核。
  3. 内核 初始化硬件设备,以只读方式挂载根文件系统,然后启动 /sbin/init——这是一个指向 systemd 的符号链接。从这一刻起, PID 1 (systemd)接管了系统的控制权。
  4. systemd 解析单元文件(unit files),并根据依赖关系图逐步启动服务,能够并行处理的部分会同时进行。
  5. 目标(Target) 按顺序激活:首先是 sysinit.target(完成早期挂载、交换分区启用、 udev 初始化等任务),接着是 basic.target(准备好套接字、定时器和路径触发器),最后是 multi-user.target(所有已启用的服务都已启动,登录提示符准备就绪)。在桌面环境中,还会进一步进入 graphical.target

以下是三个非常实用的诊断命令:

1
2
3
4
systemd-analyze                                  # 显示总启动时间,分为内核和用户空间两部分
systemd-analyze blame                            # 按启动耗时对服务进行排序
systemd-analyze critical-chain                   # 显示最长的依赖链(即决定启动时间的关键路径)
systemd-analyze critical-chain nginx.service     # 显示通往某个特定单元(如 nginx.service)的依赖链

单独使用 blame 命令可能会产生误导——即使某个服务启动耗时 5 秒,但如果没有任何其他服务依赖它,它并不会拖慢整体启动速度。真正影响启动时间的是 critical-chain 中显示的关键路径上的服务。优化这些服务才能有效缩短启动时间。

几个常见服务的 60 秒手册#

这一节列出了你日常工作中最常接触到的几个服务,并提供了简明的操作指南。

时间同步#

时间不同步迟早会引发各种问题——无论是 TLS 握手失败、日志关联混乱,还是 Kerberos 认证异常,甚至是分布式数据库的复制中断。在使用 systemd 的系统上,最简单的解决方案是直接启用内置的时间同步工具:

1
2
3
4
sudo timedatectl set-ntp true
sudo timedatectl set-timezone Asia/Shanghai
timedatectl                                # 查看当前状态,确认 "System clock synchronized: yes"
timedatectl list-timezones | grep Shanghai

timedatectl 启用的是 systemd-timesyncd,这是一个轻量级的 SNTP 客户端,足以满足普通客户端和大多数服务器的需求。如果你需要更高精度的时间同步(例如高精度集群或对外提供时间服务),可以安装 chrony 并禁用 timesyncd

千万不要用 ntpdate。它会瞬间调整时钟,可能导致依赖时间单调递增的程序(如 cron、 journald 日志顺序、数据复制)出问题。真正的 NTP 客户端会平滑地调整时钟,避免这种风险。

防火墙(firewalld)#

1
2
3
sudo systemctl enable --now firewalld
sudo firewall-cmd --get-default-zone
sudo firewall-cmd --list-all                       # 查看当前开放的规则

添加规则并使其持久化:

1
2
3
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload                         # 不执行 reload,--permanent 规则不会生效

如果不加 --permanent,规则只会在下次 reload 前生效;如果加了 --permanent 但不执行 --reload,规则虽然会持久化,但当前不会生效。实际操作中,通常两者都需要。

Zone (如 publicworkhomeinternaldmztrusteddropblock)是 firewalld 的一种机制,用于根据网络接口所属的网络类型应用不同的规则集。在单网卡的服务器上,通常所有规则都放在 public 区域,并在此区域开放所需的端口。

SSH 安全加固#

Debian 和 Ubuntu 系统的 /etc/ssh/sshd_config 默认配置相对保守,但在其他发行版上可能并非如此。以下是一些关键的安全优化建议:

1
2
3
4
5
6
7
8
Port 22                          # 修改默认端口需权衡运维成本
PermitRootLogin no               # 禁止 root 登录,先用普通用户登录再 sudo
PasswordAuthentication no        # 仅允许密钥认证,彻底杜绝暴力破解
PubkeyAuthentication yes
PermitEmptyPasswords no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2

在重新加载配置之前,务必验证配置文件的正确性,否则可能导致无法远程登录:

1
2
sudo sshd -t                            # 检查配置文件语法
sudo systemctl reload sshd              # 应用配置,且不会中断现有会话

如果 SSH 端口暴露在公网上,并且需要 IP 级别的速率限制,可以额外部署 fail2ban;否则,仅启用密钥认证已经足够让暴力破解变得毫无意义。

Cron (当 systemd timer 不适用时)#

如果你使用的系统尚未完全采用 systemd,或者需要维护现有的 cron 任务,可以参考以下示例:

1
2
3
4
# 分  时   日   月   星期   命令
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 环境变量几乎为空。此外,务必同时重定向标准输出和错误输出(例如 >> /var/log/myjob.log 2>&1),否则任务失败时你可能无从排查原因。

排障流程:服务起不来该怎么办#

当服务无法启动时,按照以下步骤逐一排查。大多数情况下,问题的答案往往就在前两步中。

1. 查看 systemctl 提供的信息

1
sudo systemctl status myapp.service

重点关注以下几个字段:Active:(状态及持续时间)、Main PID:(如果为 0,说明进程已经退出)、Process: 行(实际执行的命令及其退出码),以及底部显示的最近 10 行日志。 80% 的问题都可以在这里找到线索。

2. 查看本次启动的日志

1
sudo journalctl -u myapp.service -b -p err -xe

-x 参数会附上 systemd 的解释信息,-e 则直接跳转到日志末尾。如果服务配置了 Restart=on-failure,可以往前翻阅之前的尝试记录——这些失败的原因可能各不相同。

3. 校验配置文件是否正确

在怀疑服务本身之前,先检查配置文件是否有问题。大多数守护进程都提供了配置校验工具:

1
2
3
4
5
sudo nginx -t
sudo apachectl configtest
sudo sshd -t
sudo named-checkconf
sudo postfix check

如果是自定义服务,可以手动以目标用户身份运行 ExecStart= 中指定的命令:

1
sudo -u myapp /usr/local/bin/myapp --port 8080

4. 检查端口冲突

1
2
sudo ss -lntp | grep 8080
sudo lsof -i :8080

一种常见的情况是:服务之前崩溃后被重启,但新进程无法绑定端口,因为旧进程仍然占用着它,或者被其他完全无关的进程占用了。

5. 检查权限和路径问题

1
2
ls -ld /var/lib/myapp /var/log/myapp
namei -l /var/lib/myapp/data.db        # 展示路径中每一层目录的权限和所有者

如果服务以 myapp 用户运行,但其工作目录被 root 用户拥有且权限为 700,服务很可能会因权限不足而无法启动。使用 namei -l 可以清晰地看到路径上的权限链条。

6. 考虑安全机制的影响

在 RHEL 系发行版中,即使权限看起来没问题, SELinux 也可能阻止服务运行;在 Ubuntu 上, AppArmor 也有类似的作用。

1
2
3
4
5
6
7
# RHEL/CentOS/Rocky
sudo ausearch -m avc -ts recent          # 审计日志中的 AVC 拒绝记录
sudo setenforce 0                        # 临时切换到宽容模式(仅用于测试!)

# Ubuntu/Debian
sudo journalctl -k | grep -i apparmor
sudo aa-status

如果切换到宽容模式后问题消失,正确的解决方法是编写合适的 SELinux 策略或 AppArmor 配置文件,而不是长期关闭强制模式。

7. 检查依赖关系

1
2
sudo systemctl list-dependencies myapp.service
sudo systemctl --failed

如果服务依赖的某个单元本身处于失败状态,问题可能会向上游传递。优先修复这些依赖项。

后续阅读#

到目前为止,你应该已经掌握了一个清晰的工作模型: PID 1 负责管理各个单元(unit),这些单元会在状态机中流转,而 unit 文件则定义了它们的监管契约。journald 记录了所有的运行日志,而 systemctljournalctl 则是我们观察系统行为的两个窗口。接下来,你可以从以下几个方向继续深入:

  • man systemd.serviceman systemd.unit 是权威的参考资料——虽然内容简短且密集,但非常值得通读一遍。
  • man systemd.exec 详细列出了 [Service] 配置段中所有可用的沙箱和限制选项。这些选项大多可以免费提升系统的安全性。
  • freedesktop.org/wiki/Software/systemd/ 提供了官方文档,以及 Lennart Poettering 撰写的经典系列文章“systemd for Administrators”,非常值得一读。
  • systemd-analyze security <unit> 可以评估每个正在运行的服务的沙箱化程度,并指出哪些配置项可以进一步收紧安全策略。

本系列的后续文章将探讨 软件包管理(如何安装那些最终会被封装为服务的守护进程)以及 进程与资源管理(如何监控这些服务的实际运行情况)。本文中建立的核心概念——服务作为受监管的单元,具有明确的依赖关系和结构化的日志记录——将在后续内容中贯穿始终。

本系列

Linux 9 篇

  1. 01 Linux(一):使用基础
  2. 02 Linux(二):文件权限 —— rwx、chmod、chown 与超越它们的机制
  3. 03 Linux(三):磁盘管理
  4. 04 Linux(四):软件包管理
  5. 05 Linux(五):用户管理
  6. 06 Linux(六):系统服务管理 当前
  7. 07 Linux(七):进程与资源管理 —— 从 top 到 cgroups
  8. 08 Linux(八):文件操作深入解析
  9. 09 Linux(九):Vim 编辑器精要

读有所得?

GitHub 关注我 → 新文周更

GitHub