从按下电源键到屏幕上出现登录提示符,Linux 系统在这几十秒内完成了一场极其精密的“接力赛”。很多朋友平时只关注应用层的开发或者常规命令的使用,对“开机时到底发生了什么”这件事并不在意,可一旦遇到“服务器重启后某个服务自动起不来”“开机进不了系统”“启动慢得离谱”这类问题,就完全懵了。说实话,Linux 的引导过程和服务控制,是所有运维和开发都应该彻底吃透的基础课。这篇文章从 BIOS/UEFI 自检讲起,一直拆到内核初始化、systemd 接管、单元文件编写和故障排查,把我实际踩过的坑和常用的排查思路都整理出来了,适合刚入门 Linux 的同学建立完整认知,也适合有一定经验的运维朋友查漏补缺。
1. 引导过程全链路拆解:从按下电源键到登录提示符
1.1 固件自检阶段的“暗战”:BIOS 与 UEFI
很多人以为按了电源键之后系统就直接开始引导了,其实第一个干活的是主板上的固件程序。传统的主板用的是 BIOS(Basic Input Output System),它做的事情比较“笨”:自检 CPU、内存、显卡这些硬件,然后按照 CMOS 里保存的启动顺序,去找第一个可引导设备,读它的主引导记录(MBR),把控制权交出去。
现在新出的服务器和大多数 PC 都换成了 UEFI,它相比 BIOS 最大的变化是:读取的是 GPT 分区表,而不是 MBR。GPT 支持更大的磁盘容量(超过 2TB),还带了备份分区表,比 MBR 抗损坏能力强得多。而且 UEFI 模式下,引导程序存放在 EFI 系统分区(ESP,一般是/boot/efi),格式是 FAT,里面有一堆.efi文件,比如grubx64.efi、shimx64.efi,这些就是真正的引导加载程序入口。
这里有一个实操中值得注意的细节:确认自己是 UEFI 还是 BIOS 模式,可以直接执行ls /sys/firmware/efi,如果这个目录存在,说明是 UEFI 模式;如果提示 No such file or directory,那就是传统 BIOS 模式。很多云服务器的 VNC 界面里会显示初始化过程,如果你发现启动方式不对(比如 UEFI 机器装成了 BIOS 引导),后面大概率会遇到“内核能加载,但进不了系统”的灵异问题。
1.2 GRUB2 引导加载程序:内核装载的关键一环
固件阶段结束后,控制权交给 GRUB2。GRUB2 在传统的 BIOS 机器上装在 MBR 区域,在 UEFI 机器上是加载 ESP 分区里的grubx64.efi。GRUB2 的主要任务是:加载内核 (vmlinuz-*)和初始内存盘(initramfs-*),并把控制权移交给内核。
很多人对 GRUB2 觉得很难,觉得配置太复杂,其实日常运维不需要去背/boot/grub2/grub.cfg这个文件的语法,它是用grub2-mkconfig命令自动生成的,生成时会读取/etc/default/grub和/etc/grub.d/目录下的脚本。我通常会关注下面几个关键参数:
GRUB_TIMEOUT:菜单倒计时秒数,生产环境建议改成 5 秒以内,免得重启的时候卡在菜单等人。GRUB_CMDLINE_LINUX:传递给内核的启动参数,比如quiet表示静默启动减少输出,rhgb是 Red Hat 系的图形启动进度条,console=tty0用于串口,net.ifnames=0禁用网卡命名规则。GRUB_DEFAULT:默认启动的菜单项,可以是数字,也可以是saved。
改完配置之后千万别忘了执行grub2-mkconfig -o /boot/grub2/grub.cfg(Ubuntu 系用update-grub),我见过太多人改了/etc/default/grub然后直接重启,以为改过了,结果发现根本没生效。
在 GRUB 界面,你还可以按e键进入临时编辑模式,直接修改内核启动参数。比如你改错了显卡驱动导致黑屏,可以在linux那一行末尾加上nomodeset或者single,然后按Ctrl+X启动,这个临时修改只对本次引导生效,不改写配置文件,非常实用。
1.3 内核初始化和 initramfs:到底谁先谁后
内核被 GRUB2 加载进内存之后,第一件事是解压自身(现在内核镜像都是压缩过的),然后开始初始化硬件:CPU 特性检测、内存管理、中断机制、块设备驱动、文件系统驱动……但这里有个“鸡生蛋”的问题:真正的根文件系统挂在某个磁盘分区上,而要读写这个磁盘分区,内核可能需要对应的驱动,而驱动又存放在根文件系统里。怎么打破这个死循环?
答案就是 initramfs(初始内存盘)。它本质上是一个提前打包进内存的微型根文件系统,里面包含了必要的磁盘驱动、LVM 工具、dm-crypt 工具、文件系统驱动等。内核先挂载 initramfs,运行里面的 init 脚本,加载各种驱动模块,然后把真正的根文件系统挂载到/sysroot,最后用switch_root把根目录切换过去,启动真正的/sbin/init。
这个阶段如果出了问题,最明显的现象就是开机时卡在Waiting for root device或者dracut-initqueue超时。排查思路一般是:
- 确认
/etc/fstab里根分区对应的设备路径是否正确,尤其是用了 UUID 的场景,磁盘顺序变了 UUID 不会变,但设备名/dev/sda可能会变。 - 确认 initramfs 里的驱动是否完整,可以用
dracut --force重新生成 initramfs。 - 如果你用的是 LVM,确认内核启动参数里有没有
rd.lvm.lv=你的卷组/逻辑卷。
1.4 init 进程接管:systemd 成为 PID 1
根文件系统切换完成之后,内核卸载 initramfs,启动真正的第一个用户空间进程:/sbin/init。在现在的绝大多数主流发行版上,这个 init 就是 systemd,它成为 PID 1,然后开始并行地启动各类服务和目标。
这里我想多说一句,为什么 systemd 会成为现代 Linux 的“标准答案”?老派的 SysV init 是串行启动的,一个服务一个服务地按顺序跑,启动一个要等前一个结束,几十个服务排下来,开机慢得离谱。systemd 的核心理念是并行和按需启动:依赖关系不冲突的服务同时启动,有依赖的等对应服务就绪后再启动;同时引入了 socket 激活和 D-Bus 激活,意思是某些服务可以等真正被调用时才启动。这样开机速度自然快很多。
systemd 根据默认的default.target来决定引导进入什么模式,通常default.target是graphical.target(图形界面)或者multi-user.target(纯命令行多用户模式)。在排查系统问题时,经常会在 GRUB 菜单临时加systemd.unit=emergency.target或者systemd.unit=rescue.target进入救援模式。
2. systemd 服务控制的正确玩法:单元文件与日常命令
2.1 理解 unit:systemd 管理的最小资源对象
systemd 管理的所有对象都叫 unit,不止是服务。常见的有.service(后台服务进程)、.socket(监听套接字)、.target(一组 unit 的逻辑集合,类似于“运行级别”)、.timer(定时任务,可以取代 crontab 的部分场景)、.mount(挂载点)等。你可以用systemctl -t service列出所有服务 unit,用systemctl -t target查看所有 target。
一个在运维中高频踩坑的点:很多人用systemctl start xxx启动服务后,发现进程确实在跑,但重启机器后服务又没起来。这是因为start只是临时启动,不会设置开机自启。开机自启要用systemctl enable xxx,它会创建一个从/etc/systemd/system/multi-user.target.wants/到/usr/lib/systemd/system/xxx.service的软链接,表示在进入 multi-user.target 时启动这个服务。
这里我建议你把 enable 和 start 连起来用:systemctl enable --now xxx,这样一次性完成“开机自启 + 立即启动”。新版本 systemd 都支持这个参数,非常省事。
2.2 日常服务操作命令速查表
我整理一下平时最常用、也最应该刻在脑子里的 systemctl 命令组。说实话,大部分人只需要用下面这些,就足以应付绝大多数场景。
| 场景 | 命令 |
|---|---|
| 查看服务状态 | systemctl status nginx |
| 启动服务 | systemctl start nginx |
| 停止服务 | systemctl stop nginx |
| 重启服务 | systemctl restart nginx |
| 重新加载配置(不中断服务) | systemctl reload nginx |
| 查看服务是否活跃 | systemctl is-active nginx |
| 查看服务是否开机自启 | systemctl is-enabled nginx |
| 设置开机自启并立即启动 | systemctl enable --now nginx |
| 取消开机自启 | systemctl disable nginx |
| 屏蔽服务(禁止一切方式启动) | systemctl mask nginx |
| 列出所有加载的 unit | systemctl list-units |
注意reload和restart的区别。reload是让服务重新读取配置文件,不中断现有连接,适合 Nginx、sshd 这类支持平滑重载的服务;restart是干掉进程再拉起,连接会断,生产环境操作前一定要想清楚。如果服务本身不支持 reload,你执行systemctl reload会收到 “Job type reload is not applicable” 的报错。
2.3 手写一个 service 单元文件:以 Nginx 为例
很多初学者不太敢碰单元文件,觉得那是什么高深配置,其实拆开看就几个关键字段。我以部署一个自定义脚本服务为例,展示一个实用的 service 文件结构。
[Unit] Description=My Python Data Collector After=network-online.target Wants=network-online.target [Service] Type=simple User=appuser Group=appgroup WorkingDirectory=/opt/myapp Environment="PYTHONUNBUFFERED=1" ExecStart=/usr/bin/python3 /opt/myapp/collector.py --config /etc/myapp/config.yml ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=3 TimeoutStartSec=30 [Install] WantedBy=multi-user.target逐个说说关键字段的意思和里面藏着的坑。
[Unit]段的Description就是服务的描述信息,After表示这个服务应该在哪些 unit 之后启动,但不代表强依赖,它只是排序约束。Wants是弱依赖,表示“希望 network-online.target 已启动”,但如果它失败了,不影响本服务启动;如果你要强依赖,用Requires,它表示“要求的 unit 失败了,本服务也不能启动”。实际中我建议大部分场景用After+Wants组合,尽量避免Requires,因为强依赖会让故障扩散,一个网络问题可能导致一串服务起不来。
[Service]段里,Type=simple是最常见的,表示 ExecStart 启动的进程就是主进程。还有一种常见的是Type=forking,表示启动命令会 fork 出一个子进程然后父进程退出,传统 daemon 化程序(比如老版本的 nginx、mysqld)就是这种。如果 Type 写错了,systemd 会认为服务启动失败,这是新手最常踩的坑。
Environment=用来定义环境变量,注意同一个键不要重复写,多个环境变量就多写几个 Environment 行,或者用EnvironmentFile=指向一个配置文件。
ExecReload是用来定义systemctl reload动作的,比如这里给主进程发 HUP 信号让它重读配置。如果你的服务没有 reload 逻辑,可以不写这一行。
Restart=是生产环境最重要的一个参数,可取值有no、on-failure、on-abnormal、always等。我之前见过有人把Restart=always用在那种“启动失败会立刻退出”的服务上,结果 systemd 每 3 秒拉起一次进程,CPU 被打满,日志刷屏。我的建议是:常规服务用Restart=on-failure,只有明确需要常驻且退出即异常的服务才用always,并且一定要配合RestartSec设置重启间隔,防止疯狂重启。
写完文件之后,放到/etc/systemd/system/目录下,执行systemctl daemon-reload重新加载配置,然后就可以用常规的systemctl start/status命令来操作了。
2.4 target 与运行级别的对应关系
如果你之前接触过 SysV init,一定记得运行级别这回事:0 是关机,3 是命令行多用户,5 是图形界面。systemd 用 target 取代了运行级别,但它们之间的对应关系你还是要清楚:
| 运行级别 | target 名称 | 用途 |
|---|---|---|
| 0 | runlevel0.target / poweroff.target | 关机 |
| 1 | runlevel1.target / rescue.target | 单用户救援模式 |
| 3 | runlevel3.target / multi-user.target | 多用户命令行 |
| 5 | runlevel5.target / graphical.target | 图形界面 |
| 6 | runlevel6.target / reboot.target | 重启 |
查看当前默认的启动 target 用systemctl get-default,修改用systemctl set-default multi-user.target。如果你只是临时想切换到某个 target(不修改默认配置),用systemctl isolate multi-user.target。比如图形界面机器闹脾气了,你可以systemctl isolate multi-user.target临时切到命令行模式去排查问题。
3. 启动性能分析与优化:让开机时间从 90 秒降到 20 秒
3.1 排查启动耗时的利器:systemd-analyze
开头说过 systemd 的一大优势是并行启动,但有的机器开机还是很慢,那大概率是有某个服务卡住了,拖累了整个开机链路。这时候别靠肉眼盯着屏幕数秒,使用systemd-analyze工具就能定位。
先看总体的启动时间分布:
systemd-analyze time输出大致长这样:
Startup finished in 2.393s (firmware) + 4.041s (loader) + 1.243s (kernel) + 32.952s (userspace) = 40.631s graphical.target reached after 32.736s in userspace看到没?内核起来只用 1.2 秒,但 userspace 花了 32 秒,说明问题绝对出在用户空间的某个服务上。接下来按耗时排序查看:
systemd-analyze blame这条命令会列出所有服务的启动耗时,从高到低排序,一眼就能看到谁是“拖延症患者”。但这里要注意一个容易误判的点:blame显示的耗时是服务从开始到结束的耗时,并不代表它阻塞了后续服务的启动。有的服务启动慢,但它不阻碍别的服务,对总开机时间没有影响;真正该关注的是“关键链”上的服务。
systemd-analyze critical-chain这条命令会追踪从default.target回推到每个关键节点的依赖链,看得非常清楚。比如:
graphical.target @32.736s └─multi-user.target @32.736s └─mysql.service @28.291s +4.443s └─network.target @28.286s └─network.service @25.052s +3.234s这说明 mysql.service 是在等 network.service 完成之后才启动的,真正拖时间的是 network.service。找到元凶之后再决定是优化它的配置,还是调整依赖关系。
我实际工作中最喜欢用的还有一个systemd-analyze plot,它生成一个 SVG 格式的启动时间线图,眼力不够的时候看图最直观。可以配合浏览器查看,或者转换成 PNG 存档。很多知道这个技巧的运维朋友都是用它出图给团队看的,比自己解释半天效率高得多。
3.2 常见的“开机变慢”原因和优化手段
启动慢的原因翻来覆去就那么几类,我按出现频率排序给你梳理一下。
第一个是网络相关的服务。比如 NetworkManager 或者 systemd-networkd 在等待 DHCP 响应,如果网络环境里 DHCP 服务器响应慢,系统会一直等。这种我建议在/etc/systemd/system/下加一个 drop-in 配置,给网络服务设置TimeoutStartSec,或者确认是不是服务配置里写了wait-online需要等待网络完全就绪。如果你不需要“联网才继续启动”,可以考虑把After=network-online.target改成After=network.target,后者不等待网络完全就绪。
第二个是某些服务里写了不必要的sleep脚本。这个我要单独拎出来讲一下,因为太常见了。有些历史遗留脚本为了等待上一个服务,直接在 ExecStart 前面写sleep 30,这种“简单粗暴”的等法在 systemd 风格里属于反面教材。如果你要等一个依赖服务,应该通过After=和Requires=声明依赖关系,让 systemd 自己调度;如果确实是业务逻辑需要延迟,建议让程序内部处理重试,而不是在启动命令里硬 sleep。脚本起点没问题,但系统整体被拖死就得不偿失了。
第三个是频繁访问磁盘的服务错开了时间片。比如开机时同时有几个服务去做日志清洗、临时文件清理、数据库表优化,磁盘 I/O 密集且互相抢资源。这种如果系统启动时间特别敏感,可以给服务配置加Nice=或者IOSchedulingClass=,降低它们的调度优先级,把宝贵的启动阶段让给关键服务。
第四个是硬件探测等待。比如 USB 设备、SCSI 设备探测慢,这个通常在 dmesg 里能看到线索。如果确认是某个驱动模块的问题,可以考虑把它加入 initramfs 或者在 modprobe 配置里禁用。
3.3 禁用和屏蔽不重要服务的安全姿势
“到底哪些服务能禁?”这个问题没有标准答案,因为每台机器的角色不一样。但有一条原则我始终强调:先确认服务是干什么的,再决定禁不禁。推荐用systemctl list-unit-files --state=enabled列出所有开机自启的 unit,逐个审视。
有些服务名字一看就能判断用不上,比如avahi-daemon.service(主要用于局域网设备发现,服务器上用不上)、postfix.service(邮件传输代理,云服务器没有邮件收发需求时可以禁用)、bluetooth.service(服务器没有蓝牙硬件,直接屏蔽)。但像chronyd、systemd-journald、NetworkManager这类,就不要脑子一热就给禁了。
如果你确定某个服务不仅不需要,还总担心它被某种方式意外启动,可以在禁用的基础上再 mask 一下:
systemctl disable bluetooth.service systemctl mask bluetooth.servicemask 的原理是把这个 unit 软链接到/dev/null,这样任何显式或隐式的启动请求都会失败。我在安全加固的时候喜欢用 mask 处理那些“虽然禁用了但总是莫名被拉起”的服务,效果非常彻底。
4. 典型故障场景与排查思路:从 GRUB 失联到服务崩溃
4.1 开机卡在 GRUB 菜单或者直接进 emergency mode
我个人的经验里,运维人碰到的“最慌”的场景就是重启之后机器死活进不了系统。比如你在 GRUB 菜单里选择内核后,屏上刷过一堆启动日志,然后卡在[FAILED] Failed to mount /data或者直接掉进 emergency mode。这种十有八九是/etc/fstab里写了一个挂载项,而那个设备启动时找不到。
排查思路是这样的:先进入 emergency mode(临时在 GRUB 的 linux 行加systemd.unit=emergency.target),然后用cat /etc/fstab检查每个挂载项,尤其关注用设备路径(如/dev/sdb1)而不是 UUID 的条目。确认哪一行有问题之后:
- 如果是临时设备导致的问题,把这行注释掉再重启,进系统之后再想办法解决设备识别的问题。
- 如果是磁盘分区本身有问题,先用
fsck -y /dev/xxx修复文件系统。 - 如果只是顺序问题,比如根分区依赖 LVM 而 initramfs 里没有对应驱动,重新生成 initramfs(
dracut --force)通常能解决。
这里我还想说一个细节:总是建议先在能进紧急模式的时候就把/etc/fstab备份一下,加注释也比直接删强。我以前图省事直接删过某行挂载项,后来忘了原来的挂载参数,重新补的时候还要翻一堆历史命令,浪费时间。
4.2 服务启动失败:journalctl 是唯一的“真相”
服务起不来的原因千奇百怪,但我排查时永远遵循同一个流程:先看状态,再看日志,最后推理。执行:
systemctl status nginx输出里会有最近几条日志,比如最常见的ExecStart=/usr/sbin/nginx failed: No such file or directory,或者Permission denied。如果你的服务和网上的教程一模一样,却起不来,大概率是路径拼写错误、文件权限不对、或者配置文件语法有错误。
这时候用journalctl看完整日志:
journalctl -u nginx -x --no-pager-u nginx指定 unit,-x为日志条目补充说明,--no-pager让输出直接打满屏幕方便滚动翻看。
还有一类比较隐蔽的问题:服务启动脚本里用了相对路径。比如 ExecStart 里写./start.sh,而启动时的工作目录和你预期的不一致,最终报No such file or directory。这就是我在单元文件示例里特意写WorkingDirectory=/opt/myapp的原因。ExecStart 里的可执行文件路径必须是绝对路径,脚本里的相对路径依赖工作目录时必须显式声明。
4.3 磁盘空间满了,服务悄悄全部崩掉
磁盘满引发的故障往往表现得很诡异。比如 MySQL 突然写入失败,Nginx 请求全部 500,CRON 任务没有日志输出——所有人都以为程序出 bug 了,其实只是/或者某个数据盘撑满了。
排查方法非常直接:
df -h查看各分区的使用率。如果发现 100%,接着用du -sh /var/log/*、du -sh /tmp/*这类命令定位大目录。日志文件是头号嫌疑人,比如/var/log/journal目录如果无限增长,是因为 journald 的日志没有设置上限,这个问题我在生产环境踩过一次,之后规范的做法是编辑/etc/systemd/journald.conf,配置:
SystemMaxUse=1G MaxRetentionSec=7day另外记住一条血泪教训:磁盘满的时候不要直接删正在写入的日志文件,就算 rm 了,文件句柄还被进程占着,空间不会释放。正确姿势是: > /var/log/xxx.log用这个命令把文件内容清空而不是删除文件,或者找到占用句柄的进程重启它,也可以用lsof +L1找到那些“已被删除但被进程占用”的文件。
4.4 网卡命名和管理服务之间的“相爱相杀”
机房里的机器经常有秩序问题:重启之后网卡名从eth0变成了ens192,或者多网卡的机器网卡顺序变了。传统网络配置(/etc/sysconfig/network-scripts/ifcfg-eth0)里如果写死了名字,系统起来之后网卡默默无闻,网络不通,服务自然全部失败。
这种问题你在日志里看到的往往是一堆服务因为网络不可用而报错。解决方式有两种流派:
- 用
net.ifnames=0 biosdevname=0内核参数关闭可预测命名规则,让网卡回归eth0/eth1这种传统命名,适合老脚本多、迁移成本高的环境。 - 改用 NetworkManager 或者 systemd-networkd 的现代化配置,用 MAC 地址或者硬件路径来稳定匹配网卡,从机制上防止名字漂移。
我个人在云环境里更推荐后者,timeouts 少一些、管理更统一。但如果你接手的是古老裸金属服务器的存量环境,第一种方法反而更省事。具体场景具体分析。
5. 进阶技巧与生产环境心得:少走弯路的经验之谈
5.1 慎用 Requires,多用 Wants 和 After
这一条真的太重要了,得多说几句。很多人在写服务单元文件时,看到“依赖”两个字就下意识写Requires,觉得这样才严密。但Requires的语义是很重的:如果依赖服务失败,本服务也会被停止或者启动失败。这会导致故障链式传播。
举个例子,你的 web 应用依赖数据库:
After=mysql.service Requires=mysql.service如果 MySQL 因为你做维护而手动停掉了,systemd 会把 web 应用也停掉。这在某些自动恢复场景下是好事,但如果你只是临时停一下 MySQL 做数据修复,web 应用全被带崩就非常被动。
更稳妥的写法是:
After=mysql.service Wants=mysql.service这是“尽量先启动 MySQL,但 MySQL 挂了不影响 web 应用启动”,之后应用本身通过连接重试来应对数据库暂时不可用的情况。我的原则很简单:业务能容忍的依赖,用 Wants;业务强耦合、数据库挂了应用活着也没意义的场景,才考虑 Requires。
5.2 Restart 配置不当导致的“疯狂重启”事故
前面提到了 Restart=always 的坑,这里我再展开讲一次。有一次新同事部署一个批量处理任务,把Restart=always写在了一个“跑完就退出”的服务上。结果任务正常执行完,退出码为 0,systemd 一看服务结束了,马上又拉起一个,任务重新跑一遍,然后又结束,又拉起……整个就是一个没完没了的死循环,不仅浪费资源,而且数据可能被重复处理,造成业务事故。
这个场景最无奈,因为服务根本不是“异常退出”,而是“正常结束”,但 Restart=always 的策略要求“无论怎样都重新拉起”。标准解法是结合进程的退出行为选对 Restart 策略:
- 一次性任务:
Restart=no,执行完就完事。 - 常驻服务但可以被信号优雅停止:
Restart=on-failure,只有非零退出码才重启。 - 进程可能因 OOM 被杀、或信号异常终止,但希望它自动恢复:
Restart=on-abnormal。 - 只有 Kubernetes 这类自己管控探活的场景,才用
Restart=always,否则一个意外退出都能让 systemd 无限重启。
还有一个细节:Restart同样适用于systemctl restart这种手动操作,但它不适用于用户通过 stop 命令主动停止。如果发现某服务被主动 stop 之后又“自己”起来了,先检查是不是 Restart 策略配错了,再检查是不是被另一个 service 的ExecStartPost或者 watchdog 机制拉起来的。
5.3 用 systemd 的定时器优雅替代 crontab
这一节算是我“强推”给团队的新习惯——系统定时任务优先用 systemd timer 而不是 crontab。理由非常实在:
- crontab 的日志散落各处,和 systemd 日志体系割裂;timer 的触发记录直接进 journald,排查起来统一方便。
- timer 天然带“上次触发时间和下次触发时间”状态,你可以用
systemctl list-timers看到所有定时任务的执行计划,如果上一次执行出了问题,会直接 Failed 状态提醒你。 - timer 可以配置
Persistent=true,意思是机器停机期间错过的任务,下次开机后自动补跑。这个对备份类任务尤其重要,crontab 完全没有这个能力。
举个例子,一个经典的备份任务:
# /etc/systemd/system/backup.timer [Unit] Description=Daily backup timer [Timer] OnCalendar=*-*-* 02:00:00 Persistent=true [Install] WantedBy=timers.target对应的 service 文件就写你的备份脚本调用。启用:
systemctl enable --now backup.timer我实际用下来最大感受是:巡检的时候只要看一个systemctl list-timers,所有定时任务一目了然,比挨个机器去crontab -l舒服太多。
5.4 查看 unit 之间的依赖关系图,别靠瞎猜
有时排查问题需要快速理清服务之间的依赖关系,与其猜来猜去,不如直接用 systemd 自带的工具看。
systemctl list-dependencies nginx这个命令会列出 nginx.service 依赖的所有 unit,包括它需要哪些 target 先就绪。往上一层可以看systemd-analyze dot nginx,它会生成 dot 格式的依赖图,配合 Graphviz 可以转成一张漂亮的依赖关系图。
我还经常用systemctl list-dependencies --reverse(或者新版里的systemctl list-dependencies --reverse参数)查看“有哪些服务依赖 nginx”,有时候一个服务明明没在开机自启列表里,却总是在重启后出现,说明它是被别的服务以依赖形式拉起来的,顺着反向依赖链一查一个准。
6. 结语:把这套逻辑内化成肌肉记忆
这篇文章从引导链路一路聊到 systemd 的服务编排、启动优化和故障排查,内容很多,但核心就一条主线:Linux 启动就是一场控制权接力赛,systemd 是接棒后的总调度,服务控制的一切问题都能从“依赖关系”和“运行状态”两个维度找到答案。
我自己在实际工作中体会最深的一点是:遇到引导或者服务故障,最忌讳的是乱试,最有效的永远是先把systemctl status和journalctl -u的日志看完,再动手改配置。多数“灵异事件”最后都水落石出为权限、路径、Restart 策略或者 fstab 里的一个小笔误。
最后再分享一个小技巧:如果你要给一批机器做服务控制的统一巡检,别一台台 SSH 上去敲命令,直接用systemctl list-units --failed配合systemctl --failed(新版简写)就能看到所有失败的 unit。把这条命令写进巡检脚本里,每天早上跑一遍,把异常服务列表推给值班群,比盯监控大屏省心得多。引导过程和服务控制决定了系统的第一口“呼吸”,把这两块吃透,很多东西就都通了。