☰
systemd时代守护进程管理:systemctl、单元文件与日志排障实战
2026/9/26 4:57:08 网站建设 项目流程

1. 这一章在讲什么:守护进程在 systemd 时代的管理思路

RH124 作为红帽的系统管理入门课程,前面几章在讲文件系统、用户权限、网络配置,到了第 7 章突然切换到"控制服务和守护进程",很多人一开始会觉得有点跳。但实际上这一章才是从"会用 Linux"迈向"会管 Linux"的分水岭。守护进程(daemon)这个词,有 UNIX 经验的老兵都不陌生,它的特点就是脱离终端、常驻后台、默默干活——比如你服务器上的 sshd、httpd、chronyd,全是守护进程。问题是,这些进程怎么启动、怎么随开机自动拉起、怎么在崩溃后自动恢复、怎么查到它到底干没干活,不同的时代有不同的答案。

在 RHEL 7 之后,这个答案统一成了 systemd。RH124 作为红帽官方教材,很自然地把 systemd 和 systemctl 作为这一章的主线,取代了老教材里那些 init 脚本和 chkconfig 的内容。学完这一章,你应该能回答几个问题:怎么查看一个服务的运行状态?怎么让服务开机自启?怎么临时停掉一个服务而不影响下次开机?系统从启动到字符界面和图形界面,到底切换了什么 target?服务跑挂了,去哪里找日志?

说实话,这些操作如果只靠死记命令,过两天就忘。我在实际带新人和维护服务器的过程中,发现真正能拉开差距的,是对"单元文件"(unit file)的理解。systemd 所有行为都是围绕单元文件来定义的,systemctl 只是操作这些单元的外壳工具。所以这篇记录我不会只给你列命令,会把命令背后的设计逻辑、单元文件长什么样、排障时该看哪儿,都尽量讲清楚,学完你再去操作,心里会踏实得多。

适用读者:准备考 RHCSA/RHCE 的学员,或者刚从"命令行玩家"转变成"系统管理员"的运维新人。如果你已经能熟练管理 systemd,这篇还可以帮你查漏补缺,尤其后面第 5 部分的排障实录,都是我在真实环境里踩过的坑。

2. systemctl 命令:管理服务的日常操作全解

2.1 查看服务状态:别只看 running 三个字

任何人第一次接触 systemctl,几乎都是从这行命令开始的:

systemctl status sshd

输出长这样:

● sshd.service - OpenSSH server daemon Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; vendor preset: enabled) Active: active (running) since Fri 2024-11-01 09:12:33 CST; 3 days ago Docs: man:sshd(8) man:sshd_config(5) Main PID: 1234 (sshd) Tasks: 26 Memory: 14.6M CGroup: /system.slice/sshd.service

很多初学者只看 Active 那一行,见到 active (running) 就觉得一切正常。但实际上值得关注的信息非常多。Loaded 那一行里的 enabled 表示开机自启已启用,如果这里显示 disabled,说明服务虽然现在跑着,但重启机器后它就不会自动起来了。Main PID 告诉你主进程号,配合ps -p 1234可以快速确认进程归属。CGroup 行则显示这个服务把所有子进程都纳入了自己的控制组,这其实是 systemd 对进程隔离的一种管理方式。

查看服务状态还有几个高频命令,我习惯称它们为"状态三连":

systemctl is-active sshd systemctl is-enabled sshd systemctl is-failed sshd

这三个命令输出非常简洁,分别只回答:现在是不是活的?开机会不会自启?有没有进入失败状态?非常适合在脚本里做条件判断。我写监控脚本时常用systemctl is-active做健康检查,而不是用ps aux | grep去碰运气,因为后者经常被同名进程干扰。

要补充一个查看全体服务的视角:

systemctl list-units --type=service --state=running systemctl list-unit-files --state=enabled

第一条展示当前正在运行的 service 单元,第二条展示哪些单元被配置成了开机自启。第二条在生产环境里尤其有用,排查"怎么这台机器一开机就启动了一堆奇怪进程"时,基本靠它定位。

2.2 启停重载:start、stop、restart、reload 的区别

这组命令平时太常见了,但很多人并不清楚 restart 和 reload 的差异,甚至不知道有 reload 这个选项。

systemctl start httpd systemctl stop httpd systemctl restart httpd systemctl reload httpd

start 和 stop 不用说,就是启动和停止。restart 是彻底停止进程再重新拉起,适合配置改动较大、或者进程内部状态已经乱掉的情况。reload 则是向进程发送 SIGHUP 信号,让进程在不中断服务的情况下重新读取配置文件。对 httpd 和 nginx 这类支持平滑重载的服务来说,日常改完配置后应该用 reload 而不是 restart。

为什么我强调这一点?因为 restart 会断开现有的 TCP 连接,线上环境如果有正在进行的请求,一次 restart 就直接把它们掐断了。而 reload 能保证服务进程的监听端口不关闭,连接不断,只是应用新配置。2018 年我见过一位同事在生产库房服务器上改完 nginx 配置,习惯性敲了 restart,结果秒级中断导致一批正在上传的数据失败,场面一度很尴尬。所以说,分清这俩不仅是考试考点,更是生产事故的避坑点。

如果你不确定某个服务是否支持 reload,可以看服务的单元文件里有没有定义ExecReload,或者在 man 文档里查。大多数现代服务都有,保守起见,也可以用验证配置的方式先确认配置没问题:

httpd -t nginx -t

确认 syntax ok 之后再 reload,更稳妥。

2.3 enable 与 disable:控制开机自启的底层逻辑

enable 和 disable 是这一章里必须扎实掌握的一对命令。很多人以为是"启动"和"取消启动",实际它的准确含义是"设置开机自启"和"取消开机自启"。所以常见操作是:

systemctl enable httpd systemctl disable httpd

前者会在 /etc/systemd/system 下生成一个指向 /usr/lib/systemd/system/httpd.service 的软链接(具体放在 multi-user.target.wants 目录里),后者则把这个软链接删掉。理解到这层,很多疑问就迎刃而解了:enable 一个服务后并不会让它立刻启动,它只是告诉 systemd "下次开机进入 multi-user.target 时,你要把这个单元带上"。如果现场就要它马上跑起来,得再执行 systemctl start,或者直接用一条命令:

systemctl enable --now httpd

--now 表示 enable 的同时立即启动,省得敲两次命令。反过来,禁用一个服务最常用的做法:

systemctl disable --now httpd

disable 还有一个很多人不知道的兄弟命令——preset。RH124 课程里会提到,它的作用是按照发行版预设的策略批量设置 enable/disable。红帽官方预设文件在 /usr/lib/systemd/system-preset/ 目录下。手动管理少量服务时不太用得上,但在批量部署、初始化镜像时非常高效。我做自动化装机后的初始化脚本时,就经常用systemctl preset来把一堆服务恢复到发行版默认的启停状态,减少人为判断。

2.4 mask 与 unmask:彻底屏蔽一个服务的“硬手段”

在学习启停时,有个场景 enable 和 disable 都搞不定:A 服务被 B 服务依赖,你停掉 A,B 一启动又把 A 拉起来了,或者你在安全加固时想彻底禁用一个服务,禁止任何人(包括依赖它的其他单元)拉起它。这时候需要的就是 mask。

systemctl mask httpd systemctl unmask httpd

mask 的原理比 disable 更彻底,它把 httpd 的单元文件符号链接到 /dev/null,systemd 在解析这个单元时发现目标是空设备,就会直接认为该服务不存在、不可启动。disable 只是移除了"开机自启"的软链接,但 start 仍然可以手动拉起来。mask 则连手动启动都会失败,报错信息类似 "Unit httpd.service is masked"。

什么场景下适合 mask?比如服务器上装了 postfix 但又不想让它运行、不想让它开机自启、还想防止别人(或某个依赖它的应用)不小心把它拉起来,直接 mask 一劳永逸。在 RHEL 7 以上,systemctl mask在考试里也会出现,要能分辨它和 disable 的强弱关系。

3. 单元文件解密:从看懂到写自己的守护进程配置

3.1 单元文件的位置和优先级:改哪里才对?

systemd 里的每个服务、每个 target、每个 socket,都是一段文本配置,叫单元文件(unit file)。以 sshd 为例,它的配置文件路径是 /usr/lib/systemd/system/sshd.service。这个目录是发行版自带的单元文件的存放地,一般不建议直接改——因为软件包更新时可能会覆盖。管理员自建或修改的单元文件,应该放在 /etc/systemd/system/ 下。如果你把同名文件放在这个目录,它的优先级会高于 /usr/lib/systemd/system 下的版本。

除了这两个位置,还有一个临时目录 /run/systemd/system,它的优先级介于两者之间,主要用于运行时临时修改,系统重启后即失效。三个位置的优先级从高到低依次是:

  • /etc/systemd/system
  • /run/systemd/system
  • /usr/lib/systemd/system

这个设计跟 PATH 环境变量的覆盖思想很像:基础版本由软件包提供,定制版本由管理员维护,临时版本用于运行时调整。理解优先级之后,一个常见的困惑就解决了:为什么我改了 /usr/lib/systemd/system/httpd.service 里的配置却不生效?很可能因为 /etc/systemd/system 下已经有一份同名文件把它覆盖了。

在 RH124 考试里,经常会考到"如何让系统使用管理员自定义的单元文件而不是默认的"这类场景,答案就是放到 /etc/systemd/system 下,优先级保证系统会优先加载它。

3.2 单元文件结构:Unit 段、Service 段、Install 段各管什么

打开一个典型的服务单元文件看看结构:

[Unit] Description=OpenSSH server daemon Documentation=man:sshd(8) man:sshd_config(5) Wants=sshd-keygen.target After=network.target sshd-keygen.target [Service] Type=notify EnvironmentFile=-/etc/crypto-policies/back-ends/opensshserver.config ExecStart=/usr/sbin/sshd -D $OPTIONS ExecReload=/bin/kill -HUP $MAINPID KillMode=process Restart=on-failure RestartSec=42s [Install] WantedBy=multi-user.target

[Unit] 段是所有单元共用的,描述这个单元的元信息。Description 就是你在systemctl status里看到的第一行描述。After 和 Wants 控制依赖关系:After 表示本单元要在 network.target 之后启动,但不强制要求它必须启动;Wants 是"想要"依赖,如果被依赖的单元存在就启动它,失败也不影响本单元。比 Wants 更强的是 Requires,它表示硬依赖,被依赖单元启动失败,本单元也会被标记为失败。还有 Conflicts,用于声明互斥关系,比如 A 和 B 配置了 Conflict,启动 A 会自动停掉 B。

[Service] 段是服务单元的灵魂。Type 字段定义了 systemd 如何判断服务启动成功,常见的有 simple、forking、oneshot、notify。这一块值得展开讲一下,因为选错 Type 会导致服务状态判断错位,问题很隐蔽。

  • simple:ExecStart 指定的进程启动就算服务成功了,适用于前台运行的进程。
  • forking:父进程启动后 fork 出子进程,父进程退出,systemd 认为服务成功。常见于传统 double-fork 式守护进程,比如早期的 sshd、nginx。需要配合 PIDFile 指定 pid 文件,systemd 靠它定位主进程。
  • oneshot:执行完 ExecStart 配置的命令就算完成,常用于一次性任务(比如初始化、挂载)。
  • notify:进程启动完成后主动通知 systemd"我准备好了",需要进程自身支持 sd_notify 协议。

KillMode 控制停止服务时如何杀死进程。默认值是 control-group,意思是把这个服务控制组里的所有进程全部杀掉。但有些服务 fork 出去的子进程不希望被连带杀死,可以用 process 模式只杀主进程。Restart 字段定义服务崩溃退出后的重启策略,常用值有 no、on-failure、on-abnormal、always。生产服务器上的关键服务,很多配置是 Restart=on-failure,配合 RestartSec 设定重启前的等待秒数,能有效避免"崩溃后疯狂重启"的抖动。

[Install] 段只跟 enable/disable 有关。WantedBy=multi-user.target 表示在 enable 时,生成一个软链接放入 /etc/systemd/system/multi-user.target.wants/ 目录。目标 target 允许"想要"本服务,于是开机进入 multi-user.target 时,本服务就被带上。如果单元文件里没有 [Install] 段,执行 enable 时会报错提示 "Unit file has no Install section",这意味着它不适合被 enable。

3.3 实操:自己编写一个守护进程服务单元

只看不写很难理解单元文件的要点,这里带大家实际写一个例子。假设我在 /usr/local/bin/ 下放了一个脚本 mydaemon.sh,它会在后台循环收集系统负载信息,写入 /var/log/mydaemon.log:

#!/bin/bash while true; do echo "$(date) $(uptime)" >> /var/log/mydaemon.log sleep 60 done

我给它加上可执行权限后,写一个单元文件 /etc/systemd/system/mydaemon.service:

[Unit] Description=My custom daemon for load logging After=network.target [Service] Type=simple ExecStart=/usr/local/bin/mydaemon.sh Restart=on-failure RestartSec=10s [Install] WantedBy=multi-user.target

写完先做三件事:重新加载配置、启用开机自启、启动服务。

systemctl daemon-reload systemctl enable --now mydaemon systemctl status mydaemon

这里有个非常重要的习惯我强调无数次:改完单元文件必须先执行 systemctl daemon-reload,否则 systemd 不会感知到你新增或修改了单元文件。初次接触这个机制的人,90% 的"我明明写了配置却不起作用"问题,都出在忘了这一步。本质原因很简单:systemd 为了性能,会缓存已经加载的单元配置,不会每次命令实时去重新解析文件。

再做一个 forking 类型的练习。假设我有一个名为 myserver 的程序,启动后它会 fork 一个子进程常驻后台,同时把主进程 PID 写到 /var/run/myserver.pid。单元文件可以这么写:

[Service] Type=forking PIDFile=/var/run/myserver.pid ExecStart=/usr/local/bin/myserver ExecStop=/bin/kill -TERM $MAINPID Restart=always

注意 ExecStop 里的 $MAINPID 变量,它自动替换为主进程的 PID,配合 KillMode=process 可以做到只杀主进程不杀子进程(当然如果子进程也需要回收,就得用默认的 control-group 模式)。这一段的配置逻辑,RHCSA 考试里经常结合实际问题考,尤其 Type 类型的选择,可以说是单元文件中价值最高的一门学问。

4. target:现代“运行级别”如何控制开机状态

4.1 从 runlevel 到 target:一张映射表让你看懂系统状态

在老 Linux 时代,系统启动到什么状态用运行级别 runlevel 描述,从 0 到 6 一共七个级别,0 是关机,6 是重启,3 是字符界面多用户,5 是图形界面。当时管理开机状态靠的是 /etc/inittab 和 rc.local。systemd 时代,这套概念被 target 替代了。target 不是一个数字,而是一组单元的集合,相当于把多个服务编排在一起,用一个目标名字来引用。

RHEL 7 专门保留了 runlevel 和 target 的映射关系,这不是历史包袱,而是为了迁移方便。映射表如下:

runleveltarget 单元含义
0poweroff.target关机
1rescue.target单用户救援模式
2multi-user.target多用户字符界面(无图形)
3multi-user.target多用户字符界面(无图形)
4multi-user.target多用户字符界面(无图形)
5graphical.target多用户图形界面
6reboot.target重启

可以看到 runlevel 2、3、4 都映射到同一个 multi-user.target,这正是 systemd 简化模型的关键思路:不再为细微差别制定不同的级别,而是做组合。

查看和切换默认 target 用的是这两条命令:

systemctl get-default systemctl set-default graphical.target

服务器上通常保持 multi-user.target 就够了,省掉图形界面占用的内存和 CPU。切换之后不会立即生效,需要重启系统才会进入新的默认 target。这和传统的 init 3 命令不一样,set-default 只是改默认配置,不改变当前运行目标。

4.2 临时切换 target:isolate 的用途与危险

当前运行状态用什么 target,也能临时切换,命令是 isolate:

systemctl isolate multi-user.target systemctl isolate graphical.target

这条命令会把当前系统切换到指定 target,即停止不需要的单元、启动需要的单元。对服务器管理员来说,最常见的场景是:图形环境桌面上误开了桌面环境,想让机器回到纯字符模式省资源,直接systemctl isolate multi-user.target,桌面相关的服务会被停掉。

危险也要说清楚。isolate 是一个"破坏性"操作,它会停掉当前 target 不需要的单元。如果你在远程 SSH 连接时执行systemctl isolate poweroff.target,机器会直接关机,这比啥都狠。我亲眼见过有人误操作把远程服务器关机的事情。所以这条命令敲之前,务必确认你 isolate 到一个对的 target 上。

救援模式也有专门入口:

systemctl rescue systemctl emergency

rescue 相当于传统的单用户模式,会挂载所有文件系统但只启动最基础的服务,适合修复 root 密码、修复 fstab 错误这类操作。emergency 更激进,只挂载根文件系统为只读,主要用于修复比较严重的启动故障。这两者都是排障场景的主力技能,在 RHCSA 里考"忘记 root 密码怎么进单用户重置"时,就是靠它们来完成的。

4.3 理解 target 的依赖关系:为什么一个 target 能带动一堆服务

target 之所以能把一堆服务"编排"起来,靠的就是 [Unit] 段里的 Wants、Requires 和 After。拿 graphical.target 来说,它在自己的单元文件里定义:Requires=multi-user.target,意思是图形界面必须先有多用户字符界面作为底座。multi-user.target 又通过 .wants 软链接关联了一堆 enable 过的服务。

这就解释了为什么systemctl enable一个服务时,默认的 WantedBy=multi-user.target 能让它在开机时启动:因为开机过程中 systemd 在加载 multi-user.target 时,会把 multi-user.target.wants 目录里所有软链接指向的单元一并启动。你可以亲手验证一下:

ls -l /etc/systemd/system/multi-user.target.wants/

里面会有各种服务文件的软链接,正是 enable 命令生成的。理解这个机制后,遇到"为什么服务开机就是没起来"的问题,可以先到对应 target 的 .wants 目录里看看有没有软链接,比在配置文件里翻半天快得多。

5. journald 日志系统与真实排障实录

5.1 journalctl:从服务日志里查出问题真相

第 7 章还有一个绕不开的重要内容,就是系统日志。systemd 时代统一使用 journald 收集日志,配套的查看命令是 journalctl。我平时排查服务问题,第一步永远是先看日志再猜原因,而不是漫无目的地重启服务。

journalctl 的常用姿势:

journalctl -u httpd

只看 httpd 这个单元的日志。如果服务正在出问题,加上 -f 实时跟踪输出:

journalctl -u httpd -f

按时间过滤也是高频需求,比如昨天上午十点到十一点之间的日志:

journalctl -u httpd --since "2024-11-01 10:00:00" --until "2024-11-01 11:00:00"

支持相对时间,--since "-1 hour"表示最近一小时。按日志级别过滤也很实用:

journalctl -p err

只看 error 及其更高级别的消息。把多个条件组合起来用,基本能覆盖大部分排查场景:

journalctl -u sshd -p err --since "-30 min"

这里有一个关键知识点:journald 日志默认是存在内存里的(/run/log/journal),系统重启后内存日志就没了。想让日志持久化到磁盘,需要创建 /var/log/journal 目录(或修改 /etc/systemd/journald.conf 中的 Storage= 参数),然后重启 systemd-journald。我在生产环境中基本上都会第一时间配置持久化,否则机器一重启,崩溃前的日志就人间蒸发了,排障无从谈起。配置持久化的方法并不复杂:

mkdir -p /var/log/journal systemctl restart systemd-journald

重启后 journald 会自动开始向 /var/log/journal 写日志。至于日志占用磁盘空间的问题,可以在 /etc/systemd/journald.conf 里设置 SystemMaxUse=500M 之类的上限,避免日志把 /var 分区塞满。

5.2 实战排障:五类常见故障的识别与处理

单元文件改完不生效的问题前面说过,这里不再重复。下面整理几个真实环境里常见的服务排障实录,每个都值得重点记一下。

故障一:服务启动失败,status 显示 failed

现象:

Active: failed (Result: exit-code)

排查流程:先看日志

journalctl -u myservice -n 50

关键看有没有权限拒绝、路径不存在、端口占用之类的关键字。比如 nginx 启动失败,日志里报 "bind() to 0.0.0.0:80 failed (98: Address already in use)",那就是端口被占用了。定位占用进程用:

ss -lntp | grep :80

看到 PID 后,要么 kill 掉冲突进程,要么改 nginx 的监听端口。

故障二:明明 start 成功了,但几秒后进程消失

大概率是进程自行退出后,systemd 因为 Restart 策略把它拉了起来。看状态时会发现服务反复启动又失败。排查思路:手动在前台运行 ExecStart 对应的程序,看输出报什么错。很多守护进程在前台会直接打印错误到 stderr,日志里反而不一定有。

故障三:服务 kill 不掉,停了又起来

如果 Restart=always 被配置在单元文件里,stop 服务时 systemd 会先执行停止动作,但因为重启策略是 always,它会立刻把服务重新拉起来。这种时候正确的处理顺序是:

systemctl stop myservice systemctl disable myservice

如果 disable 还不行(比如它有依赖方),就上 mask:

systemctl mask myservice

mask 之后连依赖方都无法拉起它。这个场景在管理自研服务时特别常见,因为研发同学往往喜欢在单元文件里写 Restart=always。

故障四:socket 激活的服务,监听在 socket 上但 service 没跑

RHEL 7 之后,有些服务(比如 sshd)可以被 socket 激活。系统只监听 socket,有连接进来才真正启动 sshd.service。所以你看systemctl status sshd.socket可能是 active (listening),而 sshd.service 反而是 inactive。这不是故障,是设计行为。如果希望 sshd 一直常驻,需要屏蔽 socket 激活并直接启动 service:

systemctl stop sshd.socket systemctl mask sshd.socket systemctl start sshd.service

故障五:服务状态显示 active (exited),但它还要继续干活

这种常见于 Type=oneshot 的服务,执行完就正常退出。只要没有报错,active (exited) 就是"正常完成任务"的状态。很多新手看到 exited 以为服务挂了,其实不是。判断标准很简单:看是否为预期的 oneshot 型单元,以及日志里有没有 error。

5.3 systemd-analyze:排查启动性能的辅助工具

这一节内容教科书里着墨不多,但实际管理中很实用。systemd 这套体系自带一个性能分析工具 systemd-analyze,能告诉你系统启动花了多少时间、哪些服务最耗时。

systemd-analyze time systemd-analyze blame

time 显示固件、内核、用户空间的启动耗时,blame 按启动耗时从高到低排序所有单元。接手一台响应很慢的服务器时,先用 blame 看看有没有某个服务拖了后腿,比如启动耗时长到离谱的 NetworkManager-wait-online 之类,再决定要不要禁用或调优。这在当前的云服务器场景里特别常用,很多云主机的启动慢就是被 wait-online 类服务耽误的。

6. 这部分内容还想再多说几句的经验之谈

这一章学到这里,命令你基本都会了,但如果只停留在"会命令"层面是不够的。我个人的体会是,systemd 最值得花时间理解的是它的"声明式"设计哲学。老系统里管理服务靠的是脚本的顺序执行,服务之间的依赖关系都埋在脚本里,很难看清全貌。systemd 把一切抽象成单元、target、依赖、socket 这些声明式的配置,然后由 init 进程统一调度。你写的不是"怎么启动",而是"这个服务该在什么条件下启动、依赖什么、失败了怎么办",剩下的交给系统去执行。这种思路转变,才是第 7 章真正的价值。

还有一个小技巧分享给即将考试的读者:RHCSA 机考环境里,系统会大量使用 systemd。考试时遇到服务启动失败,千万不要反复 restart,先去看日志。系统故意给你设的故障点,八成都在日志里给了明示,只是很多人一紧张就只顾敲 restart。冷静下来,用 journalctl 查一眼,比盲目操作高效十倍。

最后分享一个我怎么快速上手陌生服务器的小流程:接手机器后,我会先跑systemctl list-units --type=service --state=running看当前跑着什么,再跑systemctl list-unit-files --state=enabled看哪些开机自启,然后systemctl get-default确认默认 target。三分钟就能大致摸清这台服务器的服务布局。这套流程在排查"这台机器怎么多了这么多进程"的困惑时,几乎每次都能定位到问题源头,你也可以试试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询