ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux开机启动优化:用systemd找出关键路径,精准提速

Linux开机启动优化:用systemd找出关键路径,精准提速

后台隔三差五就有人问:我的电脑开机怎么越来越慢?能不能照着那些大厂公开的优化文档来一套?手里正好有一份头部厂商公布的启动优化建议文档,我又在几台实机上反复测过几轮,把整个开机启动时间的优化思路完整整理出来。这套方法适用于所有基于systemd的现代Linux发行版,不同桌面环境、不同硬件条件下都能落地。这篇文章不会只丢命令,我会把每一步背后的原理、排查方法、以及我踩过的坑都讲清楚,保证你看完能直接上手,也能理解为什么要这样做。

先说一句:开机启动时间优化的关键,从来不是盲目禁用服务、疯狂改内核参数。如果你只是想把开机时间压到10秒以内而乱砍服务,后面系统跑着跑着出问题,反而让你更难受。真正的优化思路分四层:硬件层、内核层、systemd服务层、用户会话层。今天这篇主要围绕应用最广的systemd体系展开,因为绝大多数主流发行版都在用这套机制,而且系统自带的分析工具已经足够强大,不需要额外装一堆东西。

1. 先搞清楚开机时间到底耗在哪

1.1 一次开机背后的完整链路

你要优化开机速度,第一步不是凭感觉删服务,而是先弄清楚机器从按下电源键到进入桌面,时间都消耗在哪些环节。我的习惯是把整个启动过程拆成四个阶段来看:

  • 固件初始化阶段(BIOS/UEFI自检):主板固件检测内存、硬盘、外设,这个阶段和操作系统无关,但影响非常大。有些主板默认跑自检要十几秒甚至几十秒。
  • 引导加载器阶段(GRUB等):加载内核镜像和initramfs(初始内存文件系统),这一步主要是读取磁盘文件,磁盘越快时间越短。
  • 内核初始化阶段:内核解压自身、初始化硬件驱动、挂载根文件系统。如果你有大量复杂的硬件驱动或模块,这里耗时会明显增加。
  • systemd用户态启动阶段:内核交给systemd之后,它会并行启动各种服务、挂载点、socket、timer等单元,直到所有依赖满足、图形界面起来,你看到登录界面或桌面。这个阶段是优化空间最大、也是这次讨论的重点。

在systemd的世界里,理解"并行"很关键。systemd设计之初就强调并行启动:没有依赖关系的服务会同时启动,而不是像SysV init那样一个个排队。所以开机慢通常不是某一个服务启动本身慢,而是某个"关键路径"上的服务必须等前面几个服务完成后才能开始,整条链串行的时间被拉长了。你可以把启动过程想象成做一顿饭:炖汤的同时可以洗菜切菜,但炒菜必须等食材切好,这个"必须等"的环节就是瓶颈。

1.2 三条最常用的分析命令

systemd自带的分析工具体系非常完善,我最常用的三条命令分别是systemd-analyze time、systemd-analyze blame和systemd-analyze critical-chain。先把这三条命令的输入输出搞清楚,后面的事就顺了。

systemd-analyze time

这条命令会输出固件时间、加载器时间、内核时间、用户态时间,以及总计时间。我随便贴一个精简输出:

Startup finished in 3.124s (firmware) + 1.241s (loader) + 2.313s (kernel) + 12.756s (userspace) = 19.435s graphical.target reached after 12.761s in userspace

看到这个输出先别慌:总计19.435秒里,固件花了3秒多,内核花了2秒多,用户态花了12.7秒。也就是说,如果把用户态这12.7秒压下来,开机时间能明显缩短。

systemd-analyze blame

blame会列出每个服务启动耗费的时间,从高到低排序。注意这里的单位默认是毫秒,看的时候别只看数字大小,还要看启动顺序。一个服务耗时长,可能是它自己慢,也可能是它在等别的服务。

systemd-analyze critical-chain

这是我个人最推荐的一条命令。它会把从系统启动到graphical.target的关键路径完整列出来,带缩进展示依赖关系,每一行标注了服务名和启动耗时,还会标注服务是活动的还是正在等待。比如:

graphical.target @12.761s └─multi-user.target @12.761s └─network.service @9.517s +3.244s └─network-pre.target @9.514s └─firewalld.service @9.510s +0.004s └─dbus.service @9.505s └─basic.target @3.824s

这表示网络服务从第9.5秒开始启动,花了3.2秒,成为关键路径上最耗时的环节。整个链条里,会阻塞界面出现的"关键节点"一目了然。

2. 服务治理:最容易见效的优化方向

2.1 先给服务"过筛子",别急着砍

拿到blame和critical-chain的输出后,很多人第一反应是把耗时长的大户禁用掉。我的建议是:先别动手。你要先理清楚这个服务到底是干什么的、被什么东西依赖、禁用了会影响哪块功能。

我常用的筛法分三步:

  1. 把systemd-analyze blame里耗时超过1000毫秒的服务全部列出来。
  2. 逐个查看服务状态和描述:systemctl status 服务名。
  3. 看它的依赖关系:systemctl list-dependencies 服务名。
  4. 再查一下它是不是被其他服务依赖:systemctl list-dependencies 服务名 --reverse。

这套流程走完,基本能判断哪些服务是"开机真正需要的"、哪些是"可延迟的"、哪些是"彻底可去掉的"。我见过不少新人直接禁用某个服务,结果桌面环境登录后网络图标不显示,找半天原因才发现是依赖链条上被砍掉了一环。

另外要注意,有些服务虽然启动耗时长,但它是故意、或者说不得不排在前面的。例如systemd-journal-flush.service负责把日志从内存刷到磁盘,如果之前日志积累很多,它自然会慢。你禁用它肯定不行,但你可以通过调整日志大小、设置定期清理来减少它每次开机写入的数据量。这就是"治理"而不是"禁用"。

2.2 disable和mask的区别,别再搞混了

禁用服务的命令很简单:

sudo systemctl disable 服务名 sudo systemctl mask 服务名

但这两者的差别非常大。

  • disable:取消服务的开机自启,但服务本身仍然可以被其他服务或手动启动。它只是移除symlink,不会把服务"杀"掉。
  • mask:把服务真正屏蔽掉,通过symlink指向/dev/null,即使有依赖需要拉起它,systemd也不会启动,手动start也直接失败。

mask属于杀手锏,用得很少。绝大多数情况下,disable就够了。因为很多服务你只是想让它别开机启动,并不是说系统里完全不需要它。比如打印机服务cups.service,你用不到打印机,就可以disable掉,等哪天需要用的时候再手动systemctl start cups即可,完全不影响功能。但如果你把重要的系统组件给mask了,后面排查问题会非常痛苦,因为日志里往往不会直接告诉你"服务被mask了",看起来就像服务无法正常启动。

2.3 给服务设置合理的超时时间

有一个很常见的开机慢场景:某个服务因为等待网络、等待设备,在启动时卡了很久,最后依赖超时才被systemd强制结束。你看到的不是服务花了很多时间,而是整个启动流程硬生生卡住了几十秒。

systemd对服务启动有默认的超时时间,不同的服务类型默认值不一样,通常是90秒或120秒。比如systemd-networkd-wait-online.service,它要等待网络完全就绪才会结束。在没有网络的机器上,它可能白白等满90秒甚至更久,然后整个启动流程才继续往下走。这个服务是我见过最多的开机慢元凶之一。

解决办法:修改服务的超时设置,或者干脆把这个等待服务disable掉。

修改超时时间的正确做法不是直接去改/lib/systemd/system/下的原生unit文件,而是创建一个override配置。例如我想给某个服务设置30秒超时:

sudo systemctl edit 服务名

在弹出的编辑窗口里,如果默认是空文件,就添加:

[Unit] TimeoutStartSec=30

保存并退出后,systemd会自动生成override文件,比直接改源文件干净得多,也不会因为包管理器更新而被覆盖。把超时从90秒降到30秒,如果服务本身能正常起来,完全没影响;如果它因为环境问题起不来,最多30秒就会被强制结束,开机不再被拖死。

除了单个服务的超时,还可以关注systemd.conf里的DefaultTimeoutStartSec。这个全局默认值也值得检查。有经验的维护者常把默认超时调到10到30秒之间,避免某个异常服务让整个开机流程无谓地等待几分钟。但注意,改全局默认值对所有服务生效,如果你知道某个服务确实需要很长时间才能启动(比如依赖硬件初始化),就要单独给它设置更大的超时值。

3. 内核与硬件层面的启动加速

3.1 内核启动参数别乱调,但值得看懂

systemd-analyze time里"kernel"字段展示的是内核初始化耗时。这一阶段的优化手段比服务层少,但是有效果。最常见的调整就是通过GRUB给内核传启动参数。

先看当前的内核命令行参数:

cat /proc/cmdline

常见和启动时间相关的参数包括:

  • quiet:关闭内核启动日志输出。它的主要作用是减少屏幕打印带来的开销,不过现代环境下效果有限,更主要是为了界面干净。
  • splash:显示启动动画,减少用户等待焦虑,但它不是性能优化,反而会稍微增加一点负载。
  • nmi_watchdog=0:禁用NMI看门狗,在部分硬件上可以明显缩短开机时间。代价是失去部分硬件死锁检测能力。
  • modprobe.blacklist=模块名:黑名单驱动模块。如果你知道某个模块和你的硬件不兼容或完全用不到,启动时还在加载,可以用这个参数禁用它。
  • rdblacklist:针对initramfs阶段的黑名单,控制早期用户空间里加载的模块。

我自己的经验:除非明确知道某条参数对你机器的意义,否则不要批量加参数。尤其是nmi_watchdog=0这类,虽然社区里流传它能加速,但具体效果因主板而异,而且会降低某些底层问题捕获能力。你要做的是:先看dmesg日志里有没有哪些驱动加载明显报错或反复重试,再针对性地处理。

修改内核参数的位置在/etc/default/grub文件里的GRUB_CMDLINE_LINUX_DEFAULT行。改完后需要重新生成GRUB配置:

sudo update-grub # Debian/Ubuntu系 sudo grub2-mkconfig -o /boot/grub2/grub.cfg # RHEL/Fedora系

请务必确认生成的grub.cfg内容正确,否则下次开机可能进不了系统。我建议改完之后先重启一次,验证机器能正常进入系统,再进行下一步优化,不要一次把所有参数全部改完。

3.2 initramfs和存储介质的影响

内核初始化阶段有一个隐藏的优化点:initramfs。这个初始内存文件系统的体积和内容,直接关系到内核启动时解压和加载的速度。

在Arch系的发行版上,可以用lsinitcpio查看,Debian系上可以看/etc/initramfs-tools目录。如果你发现initramfs里塞了一堆根本用不到的模块,比如你用的是NVMe硬盘,但里面还放了一堆老旧SATA控制器的驱动,那就应该重新生成一个精简的initramfs。在Debian系上重新生成:

sudo update-initramfs -u

在Arch系上直接重新生成所有预设:

sudo mkinitcpio -P

还有一个和硬件强相关的点:存储介质。机械硬盘和SSD的启动时间差距是肉眼能看出来的,NVMe又比SATA SSD快不少。如果你的机器还在用机械硬盘或者古董SATA盘,优化的上限就在那里。要么换硬件,要么接受现状。对于机械硬盘用户,还有一个额外选项:检查Swap是否放在慢速分区上。Swap在开机阶段如果被访问,慢速盘上的Swap会让启动过程雪上加霜,必要时可以把swapfile从慢速盘移到SSD上。

4. 图形界面与登录会话的启动优化

4.1 显示管理器与桌面自启动项

到了用户会话阶段,时间的消耗点开始从systemd服务转移到桌面环境本身。显示管理器(Display Manager,如SDDM、GDM、LightDM等)接管后,登录界面出现的速度取决于显示管理器服务和它依赖的图形栈。

这里有一个容易让新手绕晕的点:systemd-analyze blame里可能显示显示管理器的耗时不大,但你在屏幕前等待的时间依然很长。为什么?因为blame统计的是服务进程启动时间,不包含登录后桌面环境初始化的耗时。你从输入密码到看到桌面图标完全加载,中间还有一段"会话启动"的过程,这段时间统计的是用户会话里的systemd用户实例,而不是系统级systemd。

登录后的慢启动,最常见的原因是桌面环境的自启动项。无论是KDE、GNOME、Xfce还是其它桌面,都会读取自启动目录下的desktop文件。很多软件安装时,或用户自己添加时,根本不考虑是否需要开机启动,统统丢进自启动项里。

Linux下检查自启动项的几处位置:

  • ~/.config/autostart/(用户级,最常需要清理)
  • /etc/xdg/autostart/(系统级)
  • 桌面环境自带的"会话/启动"设置面板

我的习惯是:把~/.config/autostart/里的.desktop文件全部列出来,逐个确认是否真的需要开机启动。比如输入法框架、云端同步客户端、截图工具、聊天软件等,大多数情况下完全可以用到的时候再手动开启,不需要跟系统一起启动。要禁用某个自启动项目,不需要删除文件,在desktop文件里加一行Hidden=true即可,方便以后恢复。

4.2 用户级systemd管理

现代桌面环境一般还开启了用户级systemd,也就是说用户登录后会有一个systemd用户实例,管理用户级别的服务。这个实例也会拉取很多单元。你可以通过:

systemctl --user list-unit-files --state=enabled

查看有哪些用户级服务是随登录启用的。有些应用会在这里注册后台服务,只要不涉及核心功能,同样可以disable掉一些不必要的。但用户级服务的问题排查比系统级麻烦一些,因为有些桌面组件依赖用户服务,禁用前同样要看依赖关系。

登录会话优化还有个细节:如果你用的是GNOME,可能要注意Wayland会话和X11会话的资源消耗差异;如果你用KDE,则要留意KDE的活动管理器、索引服务这类后台进程。这些不属于systemd的范畴,但它们占用的CPU和磁盘I/O,会让你的开机启动连带着变慢。判断方法很简单:登录到桌面后,打开一个终端跑top或systemd-cgtop,看看刚登录完那几分钟内CPU占用高的进程是什么,再逐一处理。

5. 实测案例:一台旧笔记本从1分50秒到28秒

5.1 优化前的基线

为了讲清楚完整的优化流程,我用一台2015年左右的旧笔记本做了几轮实测。这台笔记本用的是SATA接口的机械硬盘,8GB内存,预装的是某个KDE桌面发行版。优化前测得的开机时间大约是1分50秒(110秒左右),用户能直观感受到的等待集中在输完密码到桌面完全就绪这一段。

我先跑了三条分析命令,结果如下:

  • systemd-analyze blame前几名里有NetworkManager-wait-online.service、systemd-journal-flush.service和apparmor.service。
  • critical-chain里明显的瓶颈是NetworkManager-wait-online.service,它等了约90秒的超时后才结束,整个图形目标的到达时间被无限拉长。
  • 内核字段显示大约3秒,固件字段大约5秒,不算离谱。

这里有个关键认知:90秒的等待,不是NetworkManager本身慢,而是它配置了"等待网络就绪"的依赖,在没有正常网络接通的环境下,它只能等满超时时间才放弃。这是很多机器开机慢的经典原因。

5.2 按顺序执行的优化步骤

我按以下顺序做了调整,每一步都重启验证,避免一轮改太多导致问题说不清楚:

第一步:禁用NetworkManager-wait-online.service。这个服务本身就是可选的,很多桌面不需要它把网络状态全部就绪再进桌面。禁用它之后,NetworkManager本身照常工作,只是不再阻塞启动流程。

sudo systemctl disable NetworkManager-wait-online.service

第二步:处理systemd-journal-flush.service。它负责把内存日志刷到磁盘。我看了一下,这台机器的日志积累很大,而且磁盘又是慢速机械盘,刷起来自然久。我先限制了日志大小:

sudo journalctl --vacuum-size=50M

这会把旧日志清理到50MB以内。以后想限制日志增长,可以编辑/etc/systemd/journald.conf,设置SystemMaxUse=50M。这一步对这台旧机器效果很明显,开机时的写盘量减少,flush服务耗时直接降下来。

第三步:给apparmor.service一个合理评估。这台机器的AppArmor确实在运行,而且加载模块耗时有点高。我不打算禁用安全模块,但可以观察它的具体耗时。实测发现,它的耗时主要来自机械盘的读取,日志压缩后整体启动时间已经大幅缩短,所以这一步实际上不用动它也下降了不少。

第四步:清理KDE自启动项。这台机器装了一堆聊天工具、云同步客户端、下载器等自启动项目。我把不常用的几个在~/.config/autostart/里加了Hidden=true,让桌面登录后不再立即拉起它们。这一步对"输密码后等待桌面可用"的感受影响极大。

第五步:检查内核参数。我加了一条nmi_watchdog=0,并重新生成了GRUB配置。这台机器实测大约省了0.5到1秒的内核启动时间,不算惊艳,但反正无副作用。

5.3 优化后效果对比

几轮调整落完后,我再跑systemd-analyze time,结果是:

Startup finished in 4.876s (firmware) + 1.190s (loader) + 2.687s (kernel) + 14.582s (userspace) = 23.335s graphical.target reached after 14.578s in userspace

从110秒直接落到23秒多。从按下电源键到看到一个可用的桌面,实测在28秒左右。没有动硬件,没有卸载软件,纯粹是通过辨识真正瓶颈、治理等待逻辑、压缩启动时读写量、清理桌面自启动项就做到了这一步。

这个案例说明:一台机器开机慢,真正的问题往往不是"服务太多",而是某个"等待环节"拖死了整个关键路径。找到关键路径,解决它,比什么优化都强。

如果你也想在优化后向量化地验证效果,可以自己做个简单的基准脚本:记录电源键按下时间或GRUB出现前的固定信号,然后到桌面出现某个特征进程的那一瞬间计时。我个人习惯是重启三次取平均值,因为机械盘每次的冷启动速度会有波动,一次结果不具备参考性。

6. 常见问题与踩坑记录

6.1 服务显示超时,但实际服务本身没问题

这是最经典的现象:开机卡在某一步,最终也进了系统,但时间被拖了很久。多数情况不是服务本身崩溃,而是它依赖的某个条件没有得到满足,比如网络不通、某个设备没就绪、某个外部挂载点找不到。排查思路是要看服务的运行日志:

journalctl -u 服务名 -b

这条命令查看该服务本次启动产生的所有日志。如果日志里有明显的网络超时或设备找不到,就顺着依赖项去解决,禁用服务通常是下策。比如你有一个/etc/fstab里配置的自动挂载分区,如果U盘没插着或文件系统有异常,挂载服务卡住超时,整个开机都要等它。正确的处理是先修复fstab配置或用nofail选项让该分区挂载失败时不阻塞启动。

6.2 systemd-analyze显示很快,但体感很慢

有一种情况特别迷惑:systemd-analyze time显示系统级启动只要15秒,但你在屏幕前等了一个世纪。这是因为系统级systemd任务已经完成,graphical.target也触达了,但显示管理器拉起桌面会话后,桌面环境的用户级初始化还在继续。你登录后到桌面的这段时间,不在systemd-analyze的统计范围里。

这种情况的排查重点放在:桌面环境自启动项、用户级systemd服务、是否有高占用进程(比如文件索引器、备份工具)在登录瞬间抢CPU和磁盘。我用过的判断方法:登录后在终端跑systemd-analyze --user time,查看用户级systemd统计;再配合top命令观察登录瞬间的资源占用,基本能锁定问题。

6.3 禁用了服务,下次更新又跑出来了

有时候你disable了某个服务,但系统包管理器升级后它又被重新启用。这不是你操作错误,而是包管理器在升级时会重新执行unit文件的enable操作。解决办法:不要只disable,而是写一个override掩码文件,或者使用apt-mark hold(Debian系)之类的包锁机制,同时找到是哪个包在重置它。通常可以通过systemctl status看到unit文件最后修改时间,对照包管理器日志定位源头。

6.4 优化过度导致功能异常

这个话题我想单独拿出来说,因为看到太多人把开机时间当成唯一指标,把系统搞出各种暗病。比如禁用systemd-udev-settle.service,确实能减少等待设备枚举的时间,但如果你的机器有某些需要动态设备规则的应用,可能就会在启动初期的正常运行中出问题。再比如有人禁用systemd-logind想省时间,结果外接设备插拔无响应、用户会话管理异常。优化启动时间的正确目标,是在不损失必需功能和稳定性的前提下,消除不必要的等待和重复工作,而不是牺牲底线。

6.5 老机器换新内核后反而变慢

有一种情况是硬件较旧,换了新内核后启动变慢。原因可能是新内核包含更多驱动模块、安全性增强机制,或者新固件驱动不兼容导致超时重试。如果确认是内核版本带来的问题,可以先看是否有静态驱动或固件配置更新可用,也可以尝试用旧版本内核启动对比。这是在发行版内核包多版本并存时最快的定位方法。解决问题后,保留一个稳定的旧内核作为备用启动选项,是务实的选择。

写在最后

这篇文章从分析工具、服务治理、内核参数、桌面会话到实测案例和踩坑,基本把开机启动时间优化里最常见的路径都过了一遍。我个人在多次优化不同机器之后,最大的感触是:千万不要靠感觉和零散教程去乱改配置,真正可靠的做法永远是先量化、再定位、最后最小化修改。一台机器开机慢,大概率不是因为你少禁用了某个服务,而是因为某个等待环节被忽略了。systemd-analyze critical-chain这种工具就是在帮你直接画出那条"关键路径",把时间花在能看到结果的地方,你优化起来会轻松很多。

另外再说一个实操小技巧:每次优化前,把当前的systemd-analyze输出存到一个文件里,作为基线。优化过程中每次重启后都归档一份记录,最后好对比,也方便后续回溯哪一步改动产生了效果。这种"改动前留基线"的习惯,跟做配置变更管理一个道理,看起来简单,但在你排查问题时能帮你省下好几个小时。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进