有一次周五晚上,我维护的一台服务器重启之后业务端口一直不通,SSH能连上,但服务就是起不来。我登录进去查了一圈,发现某个核心进程显示 failed 状态,日志里只有一行 "ExecStart path not found"。后来才发现,是部署脚本更新了安装目录,但服务控制单元文件里的路径还指着旧位置。那次排查让我把 Linux 引导过程和服务控制从底到顶重新过了一遍,也让我意识到很多运维问题其实都出在"系统是怎么起来的"和"服务是怎么被拉起来的"这两个基本环节上。这篇文章就把这两块内容完整梳理出来,从按下电源到服务就绪,每个环节的原理、命令和踩坑都会讲到。
这篇内容适合两类人:刚接触 Linux 系统管理、想系统搞懂引导和 systemd 工作原理的新手;以及已经会用 systemctl 但遇到故障总是靠搜答案、想建立完整排查思路的运维。读完之后,你会清楚知道系统启动的每个阶段在干什么、unit 和 target 是什么关系、enable 和 start 到底有什么区别、服务起不来时该按什么顺序查。
1. 从按下电源到登录提示符:引导链路的完整时序
很多人把"重启就好"当成万能解法,但重启从来不是一个瞬间动作。按下电源键之后,系统其实走了一条很长的链路:固件初始化、引导装载程序、内核启动、根文件系统挂载、第一个用户态进程接管、最后把各项服务拉起来。这段链路里任何一环出问题,表现都不一样。搞清楚链路顺序,定位故障时就不会瞎猜。
1.1 固件阶段:BIOS 与 UEFI 是怎么找到启动设备的
传统 BIOS 的流程是:CPU 上电复位后,固件先做自检(POST),然后按 CMOS 里保存的启动顺序逐一遍历设备,比如光驱、U盘、硬盘。它在每个设备上找主引导记录(MBR),检查末尾是不是 0x55AA 标志,如果找到了就把 MBR 里的引导代码加载进内存执行。
UEFI 的流程不太一样。UEFI 固件不再依赖"扫描设备找 MBR",而是直接读取 NVRAM 里保存的启动项(BootOrder),从 GPT 分区的 ESP(EFI System Partition)里加载 .efi 引导文件。如果主板开启了 Secure Boot,还会先校验引导文件的签名,签名不合法就直接拒绝启动。
我在实际维护中遇到的服务器,新机型基本都是 UEFI,老虚拟机还有不少是传统 BIOS。两者最大的差别在引导文件放哪、怎么被找到,但从运维角度看,你不需要在里面选边站。只要知道"固件按什么顺序找引导设备"这一条,就足够支撑后面的排查了。很多"开机黑屏无响应"的机器,问题恰恰是启动顺序被改掉了,或者 ESP 分区里的引导文件损坏,本质上和操作系统没关系。
1.2 GRUB2 阶段:选择内核并把控制权交给 vmlinuz
固件执行完引导代码之后,接下来登场的是引导装载程序。现在主流发行版基本都是 GRUB2。GRUB2 读取 /boot/grub2/grub.cfg(不同发行版路径略有差异),把内核菜单显示出来,默认情况下几秒后自动选择默认内核,加载 vmlinuz 内核镜像和 initramfs 内存文件系统镜像,然后跳转到内核入口。
这里有个高频误区:有人习惯直接手改 grub.cfg。实际不该这么做。grub.cfg 是由 /etc/default/grub 和 /etc/grub.d/ 下的脚本生成的,手改之后一旦执行 grub2-mkconfig 就会被覆盖。正确的姿势是改 /etc/default/grub(比如调整超时时间、修改默认内核参数),然后执行 grub2-mkconfig -o /boot/grub2/grub.cfg 重新生成。
initramfs 这层也值得多说两句。内核在引导初期不可能内置所有驱动,尤其当根文件系统放在 LVM、磁盘加密或者特殊 RAID 控制器后面时,内核自己根本找不到根设备。initramfs 的作用就是把必要的存储驱动、文件系统工具和脚本打包成一个临时根文件系统,让内核先加载它,再由里面的脚本识别真正的根设备、加载驱动、挂载根文件系统,最后 switch_root 把控制权切换到真实系统。理解了这一层,你就能明白为什么内核和 initramfs 版本不匹配时会出现 "Unable to mount root fs" 这类报错。
1.3 内核初始化与 PID 1 交接:引导过程如何过渡到服务控制
内核完成架构初始化、挂载好根文件系统之后,会执行第一个用户态进程。在现代 systemd 发行版上,这个进程就是 /usr/lib/systemd/systemd。PID 1 在 Linux 里非常特殊:它不能被 kill,是所有用户态进程的祖先,同时负责收养孤儿进程、管理挂载点和 cgroup。
systemd 启动后,会读取 default.target 以及它的依赖链,把开机需要启动的 target 和 service 全部拉起。这个阶段就是"引导过程"和"服务控制"真正交汇的地方:引导过程的终点,是 systemd 接管并开始服务控制。
想直观感受这一整条链路花在哪些环节,可以用两个命令:
systemd-analyze # 查看固件、内核、用户空间的耗时 systemd-analyze blame # 按耗时排序显示各个服务启动耗时实测下来,绝大多数开机慢的问题,都能在 blame 的输出里一眼看到是哪个服务在拖后腿。后面会专门讲到怎么用这些信息定位启动慢的根因。
2. systemd 的底层逻辑:unit、target 与依赖关系
服务控制表面上就是 systemctl start、stop 这些命令,但如果你不理解 systemd 背后的抽象逻辑,很多"奇怪现象"根本解释不了。比如:为什么 enable 之后开机才自启?为什么两个服务明明没配置依赖关系,却会互相等待?这些答案都在 unit 和 target 的设计里。
2.1 从串行启动到并行启动:systemd 要解决什么问题
传统 SysV init 的方式是把启动动作拆成一堆编号脚本,S01 开头、S02 开头……按数字顺序一个一个执行。这种设计简单,但有两个硬伤:一是串行启动,后面脚本再快也得排在前面脚本后面,开机速度永远被最慢的那条链路拖住;二是脚本之间没有统一的依赖描述,谁依赖谁全靠脚本里自己写。
systemd 的思路是把启动对象抽象成 unit,每个 unit 显式声明自己的依赖关系。systemd 会根据这些关系构建一张依赖图,能并行的全部并行启动,只有存在依赖关系的部分才需要等待。打个比方:传统 init 是食堂排队打饭,一个人打完下一个才能打;systemd 是自助餐,多个窗口同时供应,只要保证"醋溜土豆丝在米饭之前就位"(After),其他菜各炒各的,互不耽误。
2.2 unit 家族:不只是 .service 一种
systemd 里 unit 有很多类型,日常打交道最多的是下面这些:
- .service:封装一个守护进程或应用,最常用。
- .target:逻辑分组,表示一组 unit 的集合状态,类似传统运行级别。
- .socket:监听某个套接字,有连接进来时才触发对应的 .service 启动,这叫按需启动。
- .timer:定时任务,实际执行体是一个 .service,用于替代传统的 cron。
- .mount 和 .automount:封装挂载点,可以实现访问目录时才挂载。
写一个干净的 service unit,通常长这样:
[Unit] Description=Demo Web Service After=network.target Wants=network-online.target [Service] Type=simple User=webuser Group=webgroup WorkingDirectory=/opt/demo EnvironmentFile=/etc/demo/demo.env ExecStart=/usr/bin/demo_server --config /etc/demo/demo.conf Restart=on-failure RestartSec=3 LimitNOFILE=65535 [Install] WantedBy=multi-user.target[Unit] 段描述这个服务的元信息和依赖关系。[Service] 段描述进程怎么跑、以什么身份跑、失败怎么办。[Install] 段描述 enable 时应该被挂到哪个 target 下面。这里每个字段都不是随便写的,比如 User 和 WorkingDirectory 不设置时,服务默认以 root 身份在 / 目录下运行,很多程序会把日志写到你意想不到的位置。
Type 字段是 service unit 里最容易选错的地方。Type=simple 表示 ExecStart 里那个进程本身就是主进程,systemd 认为它一运行服务就算启动;Type=forking 表示主进程会 fork 出子进程后自己退出,systemd 需要靠 PIDFile 或者子进程的存活情况来判断状态;Type=oneshot 表示这是个一次性任务,命令执行完服务就退出;Type=notify 表示进程启动完成后会主动通过 sd_notify 通知 systemd。选错 Type 最直接的后果是 systemd 对服务存活状态的判断不准确,明明进程活着,systemctl status 可能显示 dead。
2.3 target 与运行级别的对应关系
target 类似传统运行级别,但比运行级别更灵活。表面对应关系是这样的:
| 传统运行级别 | 对应的 target | 说明 |
|---|---|---|
| 0 | poweroff.target | 关机 |
| 1 | rescue.target | 单用户模式,用于维护 |
| 3 | multi-user.target | 多用户命令行模式 |
| 5 | graphical.target | 图形界面模式 |
| 6 | reboot.target | 重启 |
查看和切换默认 target 用:
systemctl get-default systemctl set-default multi-user.targetset-default 的本质是建立符号链接,把 /etc/systemd/system/default.target 指向 multi-user.target。Install 段的 WantedBy=multi-user.target 决定了 enable 时这个服务会在 /etc/systemd/system/multi-user.target.wants/ 目录下生成一个软链接。这也是为什么 enable 和 start 是两个完全不同的动作的原因:enable 只是在 target 的 wants 目录里"报名",让开机时被拉起;start 是立刻执行。
依赖关系要区分两个维度:Requires/Wants 表示"要不要启动",After/Before 表示"谁先谁后"。很多人把 After 当成"依赖"来用,其实 After 只控制顺序,不保证对方会被启动;想让某个目标一定先起来,还得配 Requires 或者 Wants。顺序依赖和硬依赖是两回事,理解这一点,排查"服务明明配了 After 却还是起不来"的问题时就不会犯迷糊。
3. systemctl 交互背后的符号链接逻辑:从启动服务到开机自启
systemctl 的日常命令其实不多,但每个命令背后的动作如果不清楚,很容易出现"我 disable 了服务,为什么它还在跑"这种啼笑皆非的情况。真实运维里,这种误解出现的频率远比你想象得高。
3.1 start、enable、daemon-reload 三者的关系
systemctl start 是立即启动一个 service,修改的只是当前运行状态,不影响开机。systemctl enable 是在 /etc/systemd/system/ .wants/ 里创建带绝对路径的符号链接,实际上是在声明"开机时请拉起我"。
修改 unit 文件之后必须执行 systemctl daemon-reload,这一步很多人会漏掉。systemd 在启动时把 unit 文件解析成内部状态,不是每次执行 systemctl 都重新读一遍文件。如果改了 ExecStart 不重载,systemd 内部还是旧定义,start 也是用旧参数启动。新增 unit 文件放到 /etc/systemd/system/ 下之后不执行 daemon-reload,直接 start 会报 "Unit not found",这个错我见过太多次。
判断服务当前状态有几个命令,各有用处:
systemctl is-active demo.service # 只看是否 active systemctl is-enabled demo.service # 只看是否开机自启 systemctl is-failed demo.service # 只看是否处于 failed 状态实际排查时,建议先 is-enabled 再 is-active,两个维度分开看。一个服务可能在跑,但不是开机自启;也可能开机自启了,但这次启动失败。
3.2 stop、disable、mask 表面相似,底层完全不同
这三个命令的差异,用一张表就能说清楚:
| 命令 | 对当前状态的影响 | 对开机自启的影响 | 底层实现 |
|---|---|---|---|
| stop | 立即停止进程 | 不影响 | 向主进程发 SIGTERM |
| disable | 不影响当前状态 | 取消开机自启 | 删除 wants 目录里的软链接 |
| mask | 立即屏蔽,无法手动启动 | 完全禁止启动 | 把 unit 软链接到 /dev/null |
最容易被误解的是 disable:它只取消开机自启,如果服务当前正在运行,它还会继续跑。有人把 disable 当 stop 用,结果机器重启之后服务又起来了,还以为是 bug。mask 更彻底,把 unit 指向 /dev/null 之后,systemctl start 都会失败,因为 systemd 发现这个 unit 被屏蔽了。在不需要删除软件包但又不想要某个服务被拉起来的情况下,mask 比 disable 更合适。
3.3 服务单元文件实践:几个真实踩过的高频坑
写 service unit 的时候,有几个坑是新手和老手都容易碰到的。
第一个,ExecStart 不支持 shell 语法。unit 文件里不能直接写管道、重定向和复杂引号,比如 ExecStart=/bin/sh -c "cmd | tee /tmp/log" 这种写法需要小心处理。更干净的方案是把命令写成一个独立脚本,让 ExecStart 只填脚本路径。这样既避免了转义问题,后续调试也能直接手动执行脚本验证。
第二个,路径尽量用绝对路径。systemd 启动服务时不会继承你交互 shell 里的 PATH,有些程序依赖相对路径找配置或日志目录,结果就是行为异常。
第三个,EnvironmentFile 的格式。这个文件里写 KEY=value,每行一个,不需要 export 前缀。如果某个值带特殊字符,尽量用引号包起来。配置文件写错时服务启动可能直接失败,日志里却只有一行 "Failed to parse environment"。
第四个,WorkingDirectory 不设置时默认是 /。很多应用日志路径用的是相对路径,遇到这种情况会写不进日志,或者写到完全想不到的位置。给 web 类进程配置 WorkingDirectory 是基本操作。
日志查看是排障核心。常用组合:
journalctl -u demo.service -b # 只看本次开机以来的日志 journalctl -u demo.service -f # 跟踪实时输出 journalctl -u demo.service --since "10 min ago" journalctl -u demo.service -p err # 只看 error 级别很多服务起不来的问题,日志里已经把原因写得明明白白,只是大家习惯先看 systemctl status 的第一行状态,忽略了下面的日志内容。
4. 引导异常与服务故障的现场排查路线
故障发生的时候,最忌讳的是盲目重启和反复试错。引导和服务故障其实都可以按链路逆推,你只需要知道自己现在卡在哪一环。
4.1 起不来:从固件阶段到 systemd 阶段的故障现象分级
机器起不来,先别看代码,直接看现象:
- 黑屏无任何输出、风扇转:大概率在固件阶段,检查硬件、启动顺序、引导设备是否有盘。
- 停在 GRUB 提示符或 grub rescue:引导装载程序阶段出问题,grub.cfg 丢失或者核心文件损坏。
- 内核解压后报 panic、或者挂载根文件系统失败:内核参数不对、initramfs 不匹配、根设备驱动没加载。
- 系统日志在滚动刷屏但进不了登录界面:systemd 阶段的问题,某个 target 下的服务不断失败导致卡住。
GRUB 丢失或者 grub.cfg 损坏的修复思路是:用安装介质引导进入紧急模式,chroot 到原系统,重新生成配置文件,必要时重新安装引导装载程序到磁盘。命令大致是:
chroot /mnt/sysimage grub2-mkconfig -o /boot/grub2/grub.cfg grub2-install /dev/sda内核级别的引导参数排障,可以在 GRUB 菜单界面按 e 编辑当前启动项,在 linux 开头那一行末尾追加参数。比如追加 systemd.unit=rescue.target 可以直接进入单用户维护模式,追加 rd.break 可以在 initramfs 完成挂载前中断,进入一个紧急 shell。rd.break 方式还常用于忘记 root 密码的场景,在紧急 shell 里重新挂载根文件系统后可以直接改密码。需要说明的是,这类操作只应发生在我方拥有物理访问权限、带外管理权限且明确负责维护的设备上,切勿在未经授权的环境中尝试。
4.2 服务起不来:先看状态,再看日志,最后手动执行
服务起不来是最常见的故障。我习惯的排查顺序是固定的三步。
第一步,看状态:
systemctl status demo.service重点关注这行:Loaded 后面的 enable/disable 信息、Active 后面的 failed、以及 Process 段的 exit code。
第二步,看日志:
journalctl -u demo.service -b日志里常见的有几类信息:ExecStart 路径找不到、权限拒绝、配置文件格式错误、端口被占用。前两类占了我遇到的七成问题。
第三步,手动执行 ExecStart 里的原始命令。这一步极其有效,因为它能立刻区分是程序本身的问题还是 systemd 管理方式的问题。如果手动跑没问题,但 systemctl start 失败,那基本可以确定问题出在 unit 文件的配置,比如 User 没配、WorkingDirectory 不对、环境变量没加载。
4.3 启动慢的定位:systemd-analyze 的两个高频用法
开机慢和某个服务启动慢,定位工具都一样。systemd-analyze blame 能按耗时排序列出所有服务的启动时间,一眼就能抓到耗时大户。systemd-analyze critical-chain 则显示一条关键依赖链,链上每个节点的等待时间都标得很清楚。
实际案例里,最常见的是某个服务在等待网络。很多 unit 把 After=network.target 写上了,但 network.target 只表示网络服务开始执行,不表示网络已经配置完成。服务一旦需要真实网络,应该考虑 After=network-online.target,并配套 Wants=network-online.target。处理过一次之后,你就知道"服务起不来/启动慢"很多时候不是程序的锅,而是启动时机不对。
| 现象 | 常见原因 | 排查命令 |
|---|---|---|
| 开机黑屏 | 固件阶段、启动顺序 | 检查固件设置、引导设备 |
| grub rescue | 引导文件损坏 | 安装介质 chroot 修复 |
| root fs 挂载失败 | initramfs 不匹配 | 检查内核版本、磁盘驱动 |
| 服务 failed | ExecStart 路径、权限、端口 | status + journalctl |
| 开机很慢 | 某个 service 等待 | systemd-analyze blame |
5. 把链路串起来:一次服务 failed 排查的完整过程
这一节用一个集成场景把前面所有内容串一遍。假设你维护的一台服务器在重启后,业务端口没起来,SSH 正常。
登录后的第一件事不是重启服务,而是看整体状态:
systemctl list-units --failed输出里通常会列出 failed 的 service。假设是 webapp.service,接着看它的详细状态:
systemctl status webapp.service大概率显示 Active: failed (Result: exit-code),Process 段写着主进程退出码。然后拉日志:
journalctl -u webapp.service -b常见结果有几种:目录不存在、权限不足、端口被占用、配置文件解析失败。根据日志指向修复,比如调整 ExecStart、修改权限、或改监听端口。
修复之后执行 daemon-reload、重启服务。如果这个服务还涉及开机自启,用 is-enabled 确认一下。我在实践中发现,很多服务之前被人执行过 disable,本来以为只是"暂时停止",结果重启后服务压根没被拉起。这个细节,恰恰就是"启停状态"和"自启状态"分离设计带来的常见陷阱。
处理完服务,如果这台机器开机明显变慢,再用 systemd-analyze critical-chain 拉一下关键路径。很多时候你会发现,慢的不是业务服务本身,而是某个边缘服务在等待超时,但它堵住了整条链。
我在实际操作中比较习惯一条经验:诊断服务起不来时,先手动执行一次 ExecStart 里的原始命令,往往比翻日志还快。程序本身都不跑,systemd 管理方式再正确也是白搭。诊断引导慢则优先看 critical-chain 而不是凭感觉猜。
最后再分享一个小技巧:在 service unit 里统一加上 Restart=on-failure 和 RestartSec=3,这个组合能把大量"偶发崩溃后服务永久挂死"的问题自动消化掉。配合 LimitNOFILE 调整文件描述符上限,很多高并发场景下的"莫名其妙 failed"其实都能从根上避免。
引导过程和服务控制这两块内容,单独拆开学都只是知识点,放在一起看才是完整链路。硬件初始化、引导装载程序、内核、initramfs、systemd、target、service,每一层都在为下一层铺路。真正把链路建立起来之后,你会发现排障不再需要到处找答案,照着链路往下走就行。