1. 从按下电源键到登录界面:Linux引导过程到底做了什么
很多刚接触Linux运维的朋友,第一次被问到"Linux系统启动过程是怎样的"时,往往只能说出"开机→进系统"这个笼统的流程。但真正处理过系统无法启动、开机服务异常、内核崩溃这类问题之后,你会发现,引导过程里每一步都藏着排障的关键线索。
这篇文章就围绕Linux引导过程与服务控制这条主线,把从按下电源键到出现登录提示符之间发生的事情完整拆开来讲,同时把systemd服务管理的常用操作、实战场景和故障排查一并梳理清楚。适合刚入门Linux的初学者,也适合已经工作一段时间、想系统补全这块知识体系的运维朋友。内容不需要你提前掌握太多底层知识,只要跟着节奏走,每个环节我都会解释清楚"为什么是这样"。
我先把核心结论放在前面:Linux的引导过程可以分成固件初始化→引导加载程序→内核初始化→init进程→系统服务启动这五个阶段,而现代Linux发行版里,服务控制这件事几乎全部由systemd接管。整篇文章就是沿着这条主线展开的。
2. 引导过程的五个阶段:BIOS/UEFI、GRUB2、内核、initramfs与systemd
2.1 第一阶段:固件初始化(BIOS vs UEFI)
按下电源键之后,第一个执行的并不是Linux内核,而是主板上的固件程序。传统的主板用的是BIOS,新一些的主板则用UEFI取代了BIOS。
BIOS的工作逻辑很直接:做加电自检(POST),检测CPU、内存、硬盘这些基础硬件是否正常,然后按照你设定的启动顺序,去第一个可启动设备里找引导记录。它找的是硬盘主引导记录(MBR),MBR位于磁盘的第一个扇区,大小只有512字节,里面存放着一段很小的引导程序——GRUB2的第一阶段就装在这里。
UEFI和BIOS最大的区别在于,它不再依赖MBR那512字节的空间,而是直接读取硬盘上独立划分的EFI系统分区(ESP),这个分区通常格式化为FAT32,里面存放着.efi格式的引导文件。UEFI固件会根据NVRAM中的启动项记录,直接加载GRUB2的efi文件,跳过了传统BIOS逐级寻找MBR的过程。
注意:在老旧的BIOS+MBR组合下,如果GRUB2第一阶段损坏,你会看到屏幕上出现"GRUB Loading"之后卡住,或者直接报"Missing GRUB"之类的错误。而UEFI+GPT组合下,常见的故障是启动项丢失,开机直接进入固件设置界面。这两种故障的处理思路完全不同,后面我会专门展开。
2.2 第二阶段:GRUB2引导加载程序
GRUB2是绝大多数Linux发行版默认的引导加载程序,它的核心作用是把内核加载到内存里,然后把控制权交给内核。但GRUB2实际做的事情比这多得多,它还能让你选择启动哪个内核版本、给内核传递启动参数、进入救援模式或内存测试模式。
GRUB2的配置文件在/boot/grub2/grub.cfg(CentOS/RHEL系)或/boot/grub/grub.cfg(Debian/Ubuntu系),但这里有个重点:这个文件不建议手动编辑。因为它是通过grub2-mkconfig命令根据/etc/default/grub和/etc/grub.d/目录下的脚本自动生成的。手动改grub.cfg,下次重新生成配置时你的修改就会被覆盖。
GRUB2的工作流程可以简单概括为:加载grub.cfg配置→显示菜单(或直接按默认项执行)→加载vmlinuz内核文件和initramfs镜像→把控制权交给内核。
这里有个容易忽略的点:GRUB2加载内核时,/boot分区不一定已经被挂载,所以GRUB2自身需要能够识别文件系统,这也是为什么GRUB2比老式GRUB复杂得多——它内置了ext4、xfs、btrfs等文件系统的读取驱动。
2.3 第三阶段:内核初始化与initramfs
内核被加载到内存之后,第一件事不是去挂载你的根文件系统,而是先做CPU、内存、中断控制器这些最基础硬件的初始化。但这里存在一个"先有鸡还是先有蛋"的问题:要挂载根文件系统,需要对应的文件系统驱动和磁盘控制器驱动,而这些驱动本身就在根文件系统里。
解决这个问题的方案就是initramfs(initial RAM filesystem),也就是你在/boot目录下看到的initramfs-xxx.img文件。initramfs是一个小型的临时根文件系统,里面打包了最基本的驱动和初始化工具,比如磁盘控制器驱动、文件系统驱动、LVM工具、dm-crypt工具等。
内核启动时会先把initramfs加载到内存里并解压,然后执行里面的init脚本,这个脚本负责的事情包括:加载必要的内核模块、组装根文件系统(如果根分区在LVM卷或加密分区上,这里就要做激活操作)、把真正的根文件系统挂载到/sysroot目录,最后用switch_root切换过去,释放initramfs占用的内存。
我遇到过不少运维朋友对initramfs的理解比较模糊,觉得它只是"一堆驱动打包在一起"。实际上initramfs里包含的是一整套完整的用户空间初始化环境,你甚至可以把它解压出来,用chroot进去执行命令,很多系统修复操作就是在这一步完成的。
2.4 第四阶段:根文件系统挂载与init进程启动
当initramfs里的init脚本完成了根文件系统的挂载,并且switch_root切换成功之后,内核会正式启动根文件系统上的第一个用户空间进程。在传统的System V init体系里,这个进程是/sbin/init;在现代systemd体系里,这个进程同样是/sbin/init,但它是systemd的符号链接——也就是说,pid为1的进程实际上就是systemd。
这里有个很多新手容易混淆的概念:内核和init进程的分界线。内核初始化完毕的标志是能够启动用户空间程序,而init进程启动之后,才算进入了真正的"用户空间初始化"阶段。内核态和用户态的分界点,就在switch_root之后、pid 1进程启动的那一刻。
systemd作为pid 1进程,会读取/etc/systemd/system/和/usr/lib/systemd/system/目录下的unit文件,建立起整个系统的服务依赖树,然后按照依赖关系依次启动各个服务。从这一刻开始,系统的"引导过程"算是走完了硬件和内核部分,进入了"服务控制"的领域。
2.5 第五阶段:systemd并行启动服务与会话初始化
传统SysV init是按脚本顺序一个一个启动服务的,一个服务卡住,后面全部排队等待。systemd最大的改进就是引入并行启动机制:它根据unit文件中的依赖关系构建一张依赖图,没有依赖关系的服务可以同时启动,有依赖关系的服务则按照依赖顺序等待。同样的启动任务,SysV可能需要一两分钟,systemd往往十几秒就能完成。
但这并不意味着systemd是无脑并行。unit文件里通过After=和Requires=这些指令定义了启动顺序。有个容易踩坑的点:After=只控制顺序,不控制依赖;Requires=才表示"我必须要这个服务活着";而Wants=是弱依赖,目标服务启动失败不会影响当前服务继续启动。很多服务启动异常的根源,就是unit文件里这几种依赖关系被搞混了。
会话初始化这一层,getty服务会为每个虚拟终端启动登录进程,显示登录提示符。如果是图形界面环境,display manager服务(如gdm、sddm)会在此时启动,最终呈现登录界面。
3. systemd服务控制:unit类型、target与systemctl实战
3.1 systemd的核心概念:unit、service与target
systemd把系统中的一切可管理对象都抽象成了unit,可以分为service(服务)、socket(套接字)、device(设备)、mount(挂载点)、timer(定时器)、target(聚合目标)等类型。其中service是最常见的,我们平时说的"启动Nginx""停止MySQL",操作的就是service类型的unit。
target则是一个逻辑分组,用来表示"系统当前处于什么状态"。你可以把它理解为运行级别的现代替代品:SysV时代的运行级别3对应多用户文本模式,运行级别5对应图形模式,而systemd里对应的是multi-user.target和graphical.target。不过target不只是简单的运行级别映射,它还能聚合一组服务,比如network.target表示"所有网络服务都准备好了"。
用systemctl list-units --type=target可以看到当前系统里有哪些target可用。systemctl get-default显示默认启动target,systemctl set-default multi-user.target可以把系统设置为默认无图形界面启动。很多服务器为了省内存,都会把默认target设置为multi-user.target,有需要的时候再手动启动图形界面。
3.2 systemctl命令的完整操作清单
服务控制最常用的工具就是systemctl,我按使用场景整理了一份命令清单,建议你直接收藏:
启动与停止类:
systemctl start nginx:立即启动服务systemctl stop nginx:立即停止服务systemctl restart nginx:重启服务(先stop再start)systemctl reload nginx:重新加载配置,不中断服务。这个命令非常实用,比如修改了Nginx配置想让它生效,用reload而不需要restart,避免连接中断
开机自启类:
systemctl enable nginx:设置开机自启systemctl disable nginx:取消开机自启systemctl enable --now nginx:设置开机自启的同时立即启动服务,这条命令是我日常用得最多的,一步到位systemctl is-enabled nginx:查看服务是否设置了开机自启
查看状态类:
systemctl status nginx:查看服务详细状态,包括主进程PID、占用内存、最近日志systemctl is-active nginx:只看服务是否在运行,脚本里做判断时用这个命令很合适,返回值0表示运行中systemctl list-units --type=service --state=running:列出所有运行中的服务systemctl list-unit-files --type=service:列出所有服务unit文件及其是否启用状态
屏蔽与恢复类:
systemctl mask nginx:彻底屏蔽服务,即使手动start也会提示Failed to start。mask会在/etc/systemd/system/下创建一个指向/dev/null的符号链接,相当于把这个unit彻底禁用systemctl unmask nginx:解除屏蔽
这里插一个实际场景:有时候你只是想临时停掉某个服务,不想改它的开机自启配置,用systemctl stop就好。但如果你想让某个服务"怎么都无法启动",比如一个安全合规要求必须禁掉的服务,就应该用systemctl mask而不是仅仅disable。disable只是不开机自启,手动启动还是能起来的。
3.3 使用journalctl查看服务日志
和systemd配套的日志系统是journald,它把服务和内核的日志统一收集起来,用journalctl命令查询。这个命令对排查问题极其重要:
journalctl -u nginx:查看nginx服务的日志journalctl -u nginx -f:实时跟踪日志输出,调试问题时非常有用journalctl -u nginx --since "10 minutes ago":只看最近10分钟的日志journalctl -u nginx --since today:只看今天的日志journalctl -p err -b:查看本次启动以来所有错误级别的日志,系统出问题时第一步就该这样查
有个关键点:journald的日志默认是保存在内存和/var/log/journal/目录里的。如果/var/log/journal/不存在,系统重启后日志就容易丢。建议你确保这个目录存在,并且可以用journalctl --vacuum-size=200M来限制日志占用的磁盘空间,防止日志文件无限增长把磁盘占满。
4. 手写一个systemd服务文件:从0到1的完整实操
4.1 场景需求与unit文件结构
理论说再多,不如自己动手做一个服务。我下面用一个实际需求来演示:我们有一个Python写的Web服务脚本/opt/myapp/app.py,需要让它以守护进程方式运行,开机自启,崩溃后能被自动拉起来。
首先,在/etc/systemd/system/myapp.service创建一个unit文件,内容如下:
[Unit] Description=My Python Web Application After=network.target [Service] Type=simple User=appuser Group=appgroup WorkingDirectory=/opt/myapp ExecStart=/usr/bin/python3 /opt/myapp/app.py Restart=on-failure RestartSec=3 Environment="APP_ENV=production" [Install] WantedBy=multi-user.target拆开来看各个配置项的含义:
[Unit]部分,Description是服务的描述信息,After=network.target表示在网络服务就绪之后再启动本服务。如果不加这行,服务可能在网络接口尚未配置完成时启动,Python程序里如果依赖网络连接就会失败。
[Service]部分,Type=simple表示ExecStart启动的进程就是服务主进程。这里要注意的是,如果你想让systemd帮你管理子进程,就需要用Type=forking并且在unit文件里指定PIDFile=,但对绝大多数应用场景和脚本,用Type=simple最简单可靠。
User=和Group=指定服务运行的用户和组,这是安全基线里很重要的一环。用root运行一切服务是运维大忌,应该为每个服务创建专用系统用户。WorkingDirectory指定工作目录,很多Python应用依赖相对路径找配置文件,这个不设置就容易报文件找不到。
Restart=on-failure的含义是:仅当服务以非零退出码退出时才自动重启。配合RestartSec=3,表示失败后等3秒再拉起。如果你的服务是主动停止(比如systemctl stop),它不会触发重启。如果你希望无论什么原因退出都重启,可以把Restart=always。
Environment=用于设置环境变量,多个环境变量就写多行。
[Install]部分,WantedBy=multi-user.target是核心配置,它声明了这个服务被安装到哪个target下。WantedBy的意思就是"当进入multi-user.target时,启动这个服务"。执行systemctl enable的时候,实际做的事情就是在/etc/systemd/system/multi-user.target.wants/目录下创建一个指向unit文件的符号链接。
4.2 配置生效、启动与自检
写完unit文件后,先执行systemctl daemon-reload让systemd重新读取unit文件。这一步经常被新手跳过,结果修改了配置之后执行start,发现systemd还在用旧配置,怎么排查都不对劲。
之后的完整验证流程:
# 1. 重新加载unit配置 systemctl daemon-reload # 2. 设置开机自启并立即启动 systemctl enable --now myapp # 3. 查看服务运行状态 systemctl status myapp # 4. 测试服务是否真正工作 curl http://127.0.0.1:8000 # 5. 查看实时日志 journalctl -u myapp -f如果启动失败,最有效的排查命令组合是:
systemctl status myapp journalctl -u myapp --since "5 minutes ago"前者能看到服务有没有启动成功、退出码是多少,后者能看到具体的报错信息。大部分情况下,日志里已经把问题根源写得清清楚楚了。
4.3 修改配置后的正确姿势
服务运行一段时间后需要调整参数,比如改环境变量或者改启动命令。修改完unit文件之后,记得一定要重新执行systemctl daemon-reload,然后再systemctl restart myapp。
有个细节:只改unit文件里的Environment变量,执行restart之后新配置一定会生效吗?答案是,必须先daemon-reload再restart,顺序反了的话restart依然用的是旧配置。这个坑我踩过一次,改完配置直接restart,结果服务起来还是老环境变量,当时还在想是不是systemd缓存了,后来才反应过来是忘了先reload。
5. 服务控制的实战场景:开机自启、超时控制与资源限制
5.1 开机自启的完整链路解析
前面提到enable实际是创建软链接,这里展开讲一下完整的链路:
/usr/lib/systemd/system/myapp.service是软件包安装时自带的unit文件,属于系统整体配置。/etc/systemd/system/myapp.service是管理员自定义的unit文件,优先级更高。当你在/etc/systemd/system/下创建unit文件时,它会覆盖/usr/lib/systemd/system/下的同名文件。
执行systemctl enable myapp时,systemd会在/etc/systemd/system/multi-user.target.wants/目录下创建符号链接,指向myapp.service文件。这样在启动过程中,systemd在加载multi-user.target时,就会顺带加载这个目录下的所有符号链接指向的unit。
如果你想禁用一个开机自启服务,执行systemctl disable myapp,systemd会删除这个符号链接。配置文件本身还在,只是系统启动时不会去加载它了。
5.2 服务启动超时的处理技巧
有些服务启动很慢,比如依赖数据库初始化的应用,在启动阶段可能耗时超过systemd默认的90秒超时限制。此时systemd会杀掉启动中的服务并报告错误。
调整方法是在unit文件的[Service]部分增加超时配置:
TimeoutStartSec=180 TimeoutStopSec=30TimeoutStartSec控制启动超时,TimeoutStopSec控制停止超时。尤其是停止超时,我遇到过一些应用在收到stop信号后需要较长时间做优雅退出,如果限制太短,systemd会强制kill进程,可能导致数据未落盘或配置未保存。
还有一个更细致的参数是TimeoutStopSec可以写成TimeoutStopSec=infinity,表示永不超时,但一般不建议这么干,服务卡死的时候你连停都停不掉,很难收场。
5.3 通过systemd限制服务CPU和内存
systemd本身就能做简单的资源限制,不需要依赖cgroup工具手动操作。比如要给服务限制最大内存:
[Service] MemoryMax=500M MemoryHigh=400M CPUQuota=50%MemoryMax是硬限制,超过这个值进程会被OOM killer杀掉;MemoryHigh是软限制,超过后内核会积极回收内存,但不一定杀进程。CPUQuota=50%表示最多使用一个CPU核心的50%。
这个功能在做多租户隔离或者防止某个服务占光系统资源时很实用。比如线上服务器同时跑了好几个Java应用,其中一个出现了内存泄漏,如果没有限制,它会逐步吃光系统内存导致全部服务不可用;加了MemoryMax之后,即使有泄漏,到了上限就会被限制或杀掉,其他服务不受影响。
6. 从引导到服务控制的故障排查实录
6.1 系统启动卡住的定位方法
先看两个最常见的启动故障表现:
现象一:开机后卡在黑屏光标闪烁。这种情况通常是GRUB2已经加载了内核,但内核找不到根文件系统导致panic。可以按Ctrl+Alt+F2切换到其他虚拟终端看日志,或者在GRUB2菜单里按e编辑启动项,在内核启动参数最后加上rd.break进入dracut shell,手动检查根分区设备是否可用。
现象二:开机直接进入紧急模式(Emergency Mode),提示Failed to mount某些挂载点。这通常是因为/etc/fstab里的挂载配置有问题,比如磁盘的UUID写错了,或者挂载的磁盘没有插好。此时系统会进入紧急模式,让你输入root密码进行修复。修复方法是用journalctl -xb查看具体报错,然后检查fstab里的UUID,用blkid重新获取正确的UUID并修改fstab。
6.2 服务启动失败的五步排查流程
当systemctl start一个服务报错时,我的排查顺序是固定的:
第一步,systemctl status 服务名,看服务当前状态、主进程PID、退出码,以及最后几行日志。这一步多半能判断是不是根本性问题,比如二进制文件不存在、端口被占用。
第二步,journalctl -u 服务名 --since "10 minutes ago",把完整日志拉出来看。服务启动失败的具体报错一般都藏在这里。
第三步,手动执行启动命令。比如服务启动脚本是/usr/bin/python3 /opt/myapp/app.py,那就手动在终端跑一下,看前台能不能起来、报什么错。这一步能区分是程序本身的问题,还是systemd环境导致的问题。
第四步,检查权限和上下文。很多服务以专用用户运行,但日志目录、socket文件、数据目录的所有权没给到位,导致启动时报Permission denied。另外还要检查SELinux,在RHEL/CentOS系列上,SELinux拦截服务是非常常见的问题,看日志如果出现AVC denied,就需要用ausearch -m avc查看具体被拒的操作,或者临时用setenforce 0验证是不是SELinux导致的。
第五步,如果以上都没有头绪,看/var/log/messages或/var/log/syslog,以及应用自身的日志文件。有些应用把日志写到自己的目录,不经过journald,所以系统日志里看不到完整信息。
6.3 实战案例:Nginx启动失败排查全过程
我拿一个真实的案例来完整走一遍排查流程。某台服务器上执行systemctl start nginx报错:
Job for nginx.service failed because the control process exited with error code. See "systemctl status nginx.service" and "journalctl -xe" for details.执行systemctl status nginx,看到的关键信息是:
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)这个错误很直白:80端口被占用了。继续查谁占用了80端口:
ss -lntp | grep :80发现是一个旧的nginx进程还在跑,是之前测试时手动启动的,没走systemd。这种情况比较尴尬:systemd要启动nginx,但端口已经被非systemd管理的进程占用了。
处理方法是先停掉旧的nginx进程,再启动systemd服务:
pkill -f 'nginx: master' systemctl start nginx这个案例说明一个问题:排查的核心是先读懂报错信息,再按图索骥找根源。Address already in use是最常见的端口冲突问题,也是最容易解决的。更麻烦的是那种日志只说"exit code 1"但没给出细节的情况,这时就需要把排查重心放到程序日志和手动执行上。
7. 常用排查命令速查与经验心得
7.1 命令速查表
我整理了一份平时运维中最高频使用的命令组合,贴在服务器终端旁边或者放在笔记里都会很方便:
| 使用场景 | 命令 |
|---|---|
| 查看系统本次启动时间 | uptime -s |
| 查看上次启动时间 | who -b |
| 查看本次启动的日志 | journalctl -b |
| 查看上次启动的日志 | journalctl -b -1 |
| 查看启动耗时最长的服务 | systemd-analyze blame |
| 查看启动关键路径 | systemd-analyze critical-chain |
| 查看所有失败的服务 | systemctl list-units --state=failed |
| 查看服务依赖关系 | systemctl list-dependencies sshd |
| 查看开机自启列表 | systemctl list-unit-files --state=enabled |
| 查看内核启动参数 | cat /proc/cmdline |
| 查看内存启动早期日志 | dmesg | grep -i error |
其中systemd-analyze blame是我非常推荐的一个命令,系统启动慢的时候,它能直接告诉你每个服务各花了多少时间,一眼就能定位到启动瓶颈。很多"开机要两分钟"的问题,查完发现是一个网络挂载服务在傻等超时,优化思路立刻就有了。
7.2 我再分享几个实操中的体会
第一点,尽量用systemd管理一切服务,包括你自己编译安装的软件。很多朋友习惯了用nohup或者写个start.sh脚本启动应用,这当然也能跑,但带来的问题是你失去了systemd提供的守护能力:进程死了没人拉起来、开机不会自动启动、日志分散在各处。花十分钟写个unit文件,长期收益非常大。
第二点,修改系统和服务的任何配置前,先备份。我在生产环境操作时,习惯先把要改的配置文件复制一份带时间戳的备份,比如cp /etc/fstab /etc/fstab.bak.20250601。一旦操作失误,能快速回滚。这在排查fstab问题时尤其重要——fstab写错了,系统重启直接进紧急模式,没有备份的话还得现场算UUID,压力很大。
第三点,排查问题要有耐心,先读日志再动手。这是我在多次踩坑之后总结的最大教训。刚入行时,遇到服务起不来,我第一反应是反复重启、各种乱试,结果有时候碰巧好了但根本不知道原因是什么。后来养成了"先看状态、再看日志、再动手"的习惯,排障效率高了很多。很多你以为的"疑难杂症",其实就是日志里第一行错误已经写明的事情。
8. 结尾:一个小技巧,让排障效率再提升一档
最后再分享一个我个人的小习惯。每次拿到一台新服务器,我会先把journald的持久化存储打开,命令很简单:
mkdir -p /var/log/journal systemd-tmpfiles --create --prefix /var/log/journal这个操作的目的是让journal日志在重启后依然保留。默认情况下,如果/var/log/journal目录不存在,journal日志存放在内存文件系统里,重启即消失。一旦系统出了问题需要重启排查,之前的内存日志全部丢失,等于最关键的证据没了。
还有一个小习惯是配置终端里的/etc/hosts文件。我总会在/etc/hosts里给服务器自身加一条解析记录,很多服务的启动过程会反查主机名,解析不通会导致启动失败或超时。这个坑看起来特别低级,但实际发生的频率比我预想的高得多。
Linux引导过程和服务控制这块知识,看起来是基础,但真的是整个系统管理的基石。引导过程搞清楚了,系统起不来不慌;服务控制搞清楚了,服务异常也不会手忙脚乱。把这套逻辑理顺,很多问题都不是问题了。