Linux引导过程与systemd服务控制:从开机到业务就绪的必修课
你有没有遇到过这种场景一台服务器重启之后某个业务服务就是起不来SSH能登进去但敲systemctl status看到的只有一堆看不太懂的报错或者更头疼的服务器卡在引导界面连系统都进不去只能傻盯着屏幕干着急我见过太多同事在这两个环节栽跟头其实引导过程与服务控制这两块恰恰是Linux系统里最值得搞懂、也最容易被忽略的基础功。这篇文章不打算从头讲操作系统原理而是从实际运维和日常使用的角度出发把“按下电源键”到“服务全部就绪”这条链路完整拆开再聚焦到systemd服务控制的具体操作。无论你是刚接触Linux的初学者还是被某次故障折磨过、想系统补课的后端开发者这篇文章都能帮你把这两块知识串成一条线以后遇到问题至少知道往哪个方向查。1. 先理清思路引导过程与服务控制到底覆盖了什么1.1 为什么这两个话题要放在一起聊很多人会把“引导过程”和“服务控制”当成两件不相干的事但实际上它们是同一条链路的上下游。引导过程解决的是“操作系统怎么从磁盘上被加载起来”的问题服务控制解决的是“系统起来了之后软件服务按照什么规则被拉起和运行”的问题。两者之间靠一个关键角色衔接——init进程也就是现代Linux里的systemd。理解了这个关系你再去看故障就会清晰很多如果引导阶段出了问题通常表现为开机黑屏、卡在Logo、进入紧急模式如果服务控制出了问题通常表现为系统能正常登录但业务进程没起来、端口没监听、日志疯狂刷错误。两类问题的排查思路完全不一样但都需要你对整条链路的运转方式有整体认知。1.2 一条完整的时间线从按下电源键到服务就绪我习惯把整条链路分成五个阶段每个阶段都有明确的任务和标志性的输出固件阶段CPU上电后先执行固件代码做硬件自检然后按照启动顺序去寻找可引导设备。Bootloader阶段引导加载程序被加载到内存它负责找到内核文件并将其读入内存。内核初始化阶段内核自解压后完成硬件驱动加载、根文件系统挂载等操作最后启动init进程。systemd初始化阶段systemd作为1号进程接管系统并行或按依赖顺序启动各种系统服务和目标。用户态服务就绪登录管理器、网络服务、业务进程陆续启动完毕系统达到可用状态。这五个阶段对应到实际系统上可以用命令直接观察。比如运行systemd-analyze你会看到类似“Kernel userspace initrd”的时间统计这就是内核阶段和用户态阶段的耗时分布。后面我会专门讲怎么用这些工具定位开机慢的问题。2. 引导过程的核心环节拆解2.1 固件阶段BIOS与UEFI到底在忙什么固件阶段是整个引导链路的第一环但很多人对它的理解停留在“就是开机Logo那个画面”。实际上固件做了两件关键的事情自检和引导设备选择。传统BIOS的自检流程比较冗长会逐项检查内存、键盘、磁盘控制器等硬件。而现代UEFI固件的自检更快并且支持更多现代特性比如GPT分区表、安全启动、更大的引导分区等。判断你的机器用的是哪种模式最简单的办法是查看系统里有没有/sys/firmware/efi这个目录。有这个目录说明是UEFI模式没有则大概率是传统BIOS。在实际操作中固件阶段出问题的概率不高但有两个常见场景值得留意一是启动顺序被改动导致找不到引导设备二是开启了安全启动却用了不支持的内核模块。前者进BIOS调整启动顺序即可后者要么关闭Secure Boot要么给内核模块签名。我之前在一台新服务器上装完系统重启后直接黑屏排查了好久才发现是安全启动把第三方驱动给拦了。2.2 Bootloader阶段GRUB2的任务与配置思路固件阶段结束后引导权交给Bootloader。Linux发行版里最常见的是GRUB2它的核心任务有两个一是把内核镜像和initrd初始RAM磁盘加载到内存二是把启动参数传给内核。GRUB2的配置文件通常位于/boot/grub2/grub.cfg但实际操作中你基本不需要手改这个文件而是通过/etc/default/grub和/etc/grub.d/目录下的脚本生成。改完配置后运行grub2-mkconfig -o /boot/grub2/grub.cfgRHEL系或update-grubDebian/Ubuntu系即可生效。这里有个细节值得注意grub.cfg里会记录内核启动参数比如我们经常看到的quiet参数表示减少开机输出rhgb表示显示图形化开机动画。如果遇到内核参数需要调整比如临时修改分辨率、禁用某个驱动可以在GRUB菜单界面按e键进入编辑模式修改后按CtrlX启动。这种方式只对本次启动生效适合做临时测试。2.3 内核初始化阶段从内核自解压到systemd接管Bootloader把内核和initrd加载到内存后内核开始自解压并初始化核心子系统。这个阶段做的事情很多CPU和内存管理初始化、总线探测、块设备驱动加载、根文件系统挂载等。initrd在这个阶段扮演的角色很关键。因为内核本身不知道根文件系统在哪个设备上也不知道设备驱动什么时候才能加载完毕所以先用initrd这个临时根文件系统把必要的驱动和工具跑起来然后通过其中的脚本找到真正的根设备并挂载。挂载成功后再切换到真正的根文件系统执行/sbin/init。这个阶段如果出错最常见的表现是卡在内核日志的最后几行比如“Cannot open root device”或者“Kernel panic - not syncing”。这时候需要检查启动参数里的root配置是否正确常见写法是root/dev/mapper/vg-root或者rootUUIDxxx。UUID的方式更可靠因为设备名可能变化而UUID相对稳定。我处理过的一次引导失败就是因为在两块磁盘间挪动系统后设备名从sda变成了sdbroot参数指向错误改回UUID后恢复正常。内核初始化完成后会执行作为init路径的第一号进程也就是systemd。从此进入用户态引导的“内核阶段”结束接下来就是服务控制的天下。3. systemd时代下的服务控制实操3.1 systemctl基础操作与单元文件结构在现代Linux发行版里service命令基本已经退出历史舞台取而代之的是systemctl。日常运维里最常用的一套操作是systemctl start/stop/restart/reload/status以及enable/disable来控制开机自启。这里值得单独说的是restart和reload有本质区别。restart会完全停掉进程再重新拉起业务会中断reload通常在进程不中断的情况下重新加载配置文件适用于Nginx这类支持平滑重载的服务。能用reload解决的就别用restart这是生产环境的基本素养。systemd把服务的定义放在单元文件Unit File里路径分三个层级/usr/lib/systemd/system/是软件包自带的/etc/systemd/system/是管理员自定义或覆盖的/run/systemd/system/是运行时生成的重启即消失。优先级上后两个路径会覆盖前一个所以如果你想修改某个服务的行为正确的做法不是在原文件上改而是在/etc/systemd/system/下放一个override文件。单元文件分为多个段落最核心的是[Unit]、[Service]和[Install]三块。[Unit]段描述服务的基本信息和依赖关系[Service]段定义实际的启动命令、运行方式和重启策略[Install]段则告诉systemd如何把这个服务挂接到开机启动序列上。简单来说[Unit]是“它什么时候能被启动”[Service]是“它具体怎么跑”[Install]是“要不要开机拉起来”。3.2 Target与依赖如何设计服务的启动顺序早期SysVinit用运行级别runlevel来管理启动状态systemd引入了Target概念来替代。Target本质上是一组服务的集合比如multi-user.target对应传统的运行级别3graphical.target对应带图形界面的运行级别5。你可以通过systemctl get-default查看当前默认Target用systemctl set-default切换。服务之间的依赖关系是设计单元文件时最容易出错的地方。systemd里有两对容易混淆的关键字Requires和WantsAfter和Before。Requires是硬依赖被依赖的服务启动失败时依赖方也会被停止Wants是软依赖被依赖的服务失败时不影响依赖方正常启动。After和Before只定义顺序不定义依赖关系意思是“我要在你之后启动”或“我要在你之前启动”。实际项目中最常见的配置组合是WantsAfter表示“我希望你在之前启动而且如果你挂了也别连累我”。我见过一个很有意思的踩坑案例某团队写了一个应用服务的单元文件想在Redis启动后再启动应用于是只写了Afterredis.service结果应用经常抢跑。原因就是After只定义了顺序但如果本机没有redis.service这个单元文件或者它没有被启动After是管不住的。正确做法是加上Wantsredis.service让systemd在启动应用前主动去拉Redis依赖。3.3 实战手写一个自定义服务单元光看概念容易迷糊手写一个单元文件走一遍流程很多疑问就自然消除了。假设我们有一个Python脚本/home/user/app.py希望它作为常驻服务运行崩溃后自动重启并且开机自启。首先创建单元文件/etc/systemd/system/myapp.service内容如下[Unit] DescriptionMy Python Application Service Afternetwork.target [Service] Typesimple Useruser WorkingDirectory/home/user ExecStart/usr/bin/python3 /home/user/app.py Restarton-failure RestartSec5 EnvironmentPYTHONUNBUFFERED1 [Install] WantedBymulti-user.target这里有几个容易被新手忽略的点。Typesimple表示ExecStart启动的进程就是主服务进程systemd不额外fork如果脚本里有daemonize的写法就需要用Typeforking并指定PIDFile。Useruser表示以普通用户身份运行生产环境千万别用root跑业务服务。Restarton-failure配合RestartSec5表示只有异常退出时才自动重启并且等5秒再拉起避免崩溃后疯狂重启打满CPU。文件创建后依次执行sudo systemctl daemon-reload sudo systemctl enable myapp.service sudo systemctl start myapp.servicedaemon-reload是必须的因为systemd会缓存单元文件新文件或改动后不reload是不会生效的。enable命令本质上是创建了一个软链接到multi-user.target.wants目录下如果想取消开机启动执行disable即可。整个流程走下来你对“启动、自启、依赖”这三个概念的理解会扎实很多。4. 引导与服务控制的故障排查实录4.1 引导失败常见场景与修复流程引导失败是所有Linux故障里最让人头疼的一类因为系统都进不去很多工具用不上。但按照链路去排查绝大多数问题能定位到Bootloader、内核参数、根文件系统这三处。最常见的场景是开机卡在“Give root password for maintenance”或类似提示这通常意味着根文件系统挂载失败。此时输入root密码进入维护模式先查看/etc/fstab里有没有错误的挂载项。需要注意fstab里的UUID和实际磁盘上的UUID可能不匹配用blkid命令查看真实UUID然后修正fstab。如果是文件系统损坏导致的挂载失败可以在维护模式先卸载对应分区再用fsck修复。另一个高发场景是GRUB引导菜单恢复。比如系统迁移或双系统安装后GRUB被覆盖开机直接进入grub命令行。这种情况需要手动加载内核启动系统后重新安装GRUB。做法是先通过GRUB命令行找到boot分区和根分区手动设置root和prefix加载normal模块进入菜单然后进系统执行grub2-mkconfig和grub2-install。这里分享一个实用技巧如果你不确定改了什么导致引导异常可以在GRUB菜单界面按e进入编辑把内核参数里的quiet和rhgb删掉改成verbose模式启动。这样内核和systemd会输出详细的启动日志屏幕上能看到最后一条正常执行的操作是什么据此判断卡在哪里。4.2 服务起不来的典型原因和排查手法服务起不来是日常运维里最频繁的故障类型但大多数情况下排查方法都是固定套路。第一步永远是systemctl status服务名查看Active状态和进程信息。第二步是journalctl -u服务名查看日志如果日志太多可以加-n 50只显示最后50行。我总结过几类最高频的失败原因按出现频率排序如下配置错误比如配置文件里端口被占用、路径写错、权限不足这类问题通常在日志里能看到具体报错。依赖未就绪数据库、中间件、网络设备还没启动好服务抢先启动后连接失败。这类问题要么加After和Wants依赖要么在服务脚本里加等待重试逻辑。权限和SELinux问题进程以普通用户启动但需要访问受限资源或者SELinux策略阻止了访问。排查时建议先查看日志里有没有AVC denial记录使用ausearch -m avc命令查询。环境变量缺失手动跑脚本正常systemd启动就失败很可能是systemd环境的PATH、LD_LIBRARY_PATH等环境变量和你的shell环境不一致。解决方案是在单元文件里显式设置Environment。关于日志这块journald日志系统默认不会持久化存储服务器重启后历史日志就丢了。如果希望日志持久化把/etc/systemd/journald.conf里的Storageauto改成Storagepersistent重启journald服务即可。生产环境强烈建议开启否则排障时连前一次开机失败的原因都找不到。4.3 性能优化视角如何缩短开机时间引导过程和服务控制了解清楚后还有一个很实用的场景就是优化开机速度。systemd提供了两个特别好用的命令systemd-analyze和systemd-analyze blame。执行systemd-analyze会显示内核启动耗时、用户态启动耗时和总耗时。如果用户态耗时占了大部分说明瓶颈在服务启动环节。接着执行systemd-analyze blame系统会列出每个服务启动耗时从高到低排序你一眼就能看到哪个服务最磨蹭。我看过一台测试机开机花了3分钟用blame一查发现有个服务在等待某台不存在的网络存储超时。把那个服务的超时时间调短并且把它的依赖从Requires改成Wants后开机时间直接缩到50秒以内。当然优化时要注意不要为了快而大幅改动依赖关系否则可能引入服务启动顺序问题。更稳妥的做法是先禁用确实不需要的服务再逐个优化剩余服务的启动耗时。systemd-analyze critical-chain命令还能显示当前Target下的关键启动链路逐层展示服务之间的依赖关系。实际使用中我通常先看critical-chain确定主链路再用blame看具体耗时两者配合能快速定位到拖慢启动速度的“元凶”。比如数据库服务本身要3秒但它依赖的网络服务要20秒才能就绪那优化重点其实是网络服务而非数据库。4.4 忘了root密码、进了紧急模式怎么办还有一种情况虽然不算日常高频但真遇到时最让人手足无措那就是忘了root密码、系统进不去。这种问题本质上也属于引导控制范畴因为进入维护模式或者修改内核启动参数就能解决。现代发行版里进入单用户模式的方式是在GRUB菜单按e编辑启动参数在linux行末尾添加rd.breakRHEL系或singleDebian系然后启动进入一个特殊的shell环境。rd.break会进入一个早期RAM盘环境此时根文件系统以只读方式挂载在/sysroot目录下。执行以下命令序列即可重设root密码mount -o remount,rw /sysroot chroot /sysroot passwd root touch /.autorelabel exit reboot这里的touch /.autorelabel是SELinux系统需要的作用是让下次开机时自动重置安全上下文。虽然忘了root密码这种问题主要发生在刚接手服务器的新同学身上但掌握这套方法至少能避免遇到问题时只能重装系统的尴尬。5. 一些实践经验总结做了这么多年Linux系统维护我最大的感受是引导过程和服务控制的知识从表面上看是“背命令、记参数”但真正遇到故障时它考察的是你对整条启动链路的理解程度。比如服务起不来你只知道看systemctl status是不够的还要能通过日志推断是网络没就绪、配置写错、还是SELinux拦截。引导进不去你也需要在GRUB命令行里能手动引导一次系统否则就只能眼睁睁看着屏幕发呆。按照我个人的习惯每接手一台新机器会先做三件事确认系统用的UEFI还是BIOS模式、记录默认Target和关键服务的启动依赖、开启journald日志持久化。这些小操作平时看不见价值但系统真出问题时它们往往能帮你省下几个小时排查时间。最后再分享一个实用小技巧在编写或修改单元文件后可以先用systemd-analyze verify来检查语法它能帮你揪出很多低级错误。另外systemctl list-dependencies --after服务名可以查看该服务后面的依赖链systemctl list-dependencies --before查看它之前的依赖链。这个命令用来梳理服务启动顺序特别直观强烈建议试一试。

相关新闻

Spring Boot + JWT实现登录与接口权限控制:核心代码与避坑指南

Spring Boot + JWT实现登录与接口权限控制:核心代码与避坑指南

做Java后端开发的朋友,肯定绕不开Spring Boot接口权限控制这件事。凡是系统需要登录,就离不开登录接口和JWT令牌这套组合。很多教程只给代码,不讲取舍和坑,真上了生产环境就出各种奇怪问题。这篇文章不聊空泛的理论,直…

2026/10/12 1:02:21 阅读更多 →
内存碎片治理实战:从原理量化到分配器与对象池优化

内存碎片治理实战:从原理量化到分配器与对象池优化

凌晨两点被监控电话叫醒,大概率不是 CPU 打满,而是内存又出问题了。RSS 曲线像爬坡一样一天比一天高,重启进程后立刻回落,可到了业务高峰期还是时不时报bad_alloc,或者 Redis 的INFO memory里mem_fragmentation_ratio飙…

2026/10/12 1:02:23 阅读更多 →
Sdcb Chats 1.10:本地复刻ChatGPT高级数据分析与PPT生成

Sdcb Chats 1.10:本地复刻ChatGPT高级数据分析与PPT生成

很多人可能都有过这种体验:手里攥着一份几百行的Excel,想快速算个汇总、找找异常值,或者被临时通知要拼一份像样的PPT,结果大半时间都耗在操作用户界面和调格式上面了。ChatGPT那个“Advanced Data Analysis”确实好用&#xff0c…

2026/10/11 17:47:57 阅读更多 →

最新新闻

代码级对抗攻击:AST扰动如何绕过SAST与CI门禁

代码级对抗攻击:AST扰动如何绕过SAST与CI门禁

1. 这不是“黑客炫技”,而是代码层攻防的日常切片“Code-Level Adversarial Attacks 相关工作”——看到这个标题,很多人第一反应是:又一个AI安全论文里的抽象概念?其实不然。它背后是一群人在真实代码世界里反复拆解、注入、绕过…

2026/10/12 5:44:22 阅读更多 →
自然数立方与连续奇数之和:推导证明与Python验证

自然数立方与连续奇数之和:推导证明与Python验证

任何一个自然数 m,它的立方都可以写成 m 个连续奇数之和。这句话我第一次读到时,第一反应是:真的假的?当时正好在翻等差数列求和公式,索性拿纸笔列了一串奇数:1、3、5、7、9、11、13、15、17、19、21……然…

2026/10/12 5:44:22 阅读更多 →
最新Nessus2026.10.8版本主机漏洞扫描/探测工具Windows/Linux

最新Nessus2026.10.8版本主机漏洞扫描/探测工具Windows/Linux

前言 Nessus号称是世界上最流行的扫描程序,全世界有超过75000个组织在使用它。该工具提供完整的电脑扫描服务,并随时更新其数据库。Nessus不同于传统的扫描软件,Nessus可同时在本机或远端上遥控,进行系统的分析扫描。对应渗透测试…

2026/10/12 5:44:22 阅读更多 →
psutil详解:用Python搞定CPU、内存与进程监控

psutil详解:用Python搞定CPU、内存与进程监控

前阵子公司一台线上服务CPU突然飙到100%,我用系统自带的任务管理器看了半天,除了一个pid能锁定,剩下的信息全靠猜。后来发现这个进程是哪个服务的、占了多少内存、网络连了哪里,全是黑盒。那次之后我花了两天把psutil完整过了一遍…

2026/10/12 5:44:22 阅读更多 →
编程循环控制核心:break与continue用法详解与多语言实战对比

编程循环控制核心:break与continue用法详解与多语言实战对比

做开发这几年,几乎每个项目里都会碰上循环控制的问题。for 循环本身很简单,真正让新手挠头、让老手翻车的,往往是循环体里的那两个关键字:break 和 continue。很多新手写循环,要么不敢用 break 导致白白跑完全部数据&a…

2026/10/12 5:44:22 阅读更多 →
mediamtx v1.21.2发布:UDP、JWT、RTSP、RTMP、HLS、WebRTC全面修复,稳定性与安全性再提升

mediamtx v1.21.2发布:UDP、JWT、RTSP、RTMP、HLS、WebRTC全面修复,稳定性与安全性再提升

2026年10月10日,mediamtx 发布 v1.21.2 最新版本。本次更新以“修复与改进”为主,覆盖通用逻辑、API、Media-Over-QUIC、RTSP、RTMP、HLS、WebRTC 以及依赖库升级等多个方向。 v1.21.2 没有引入新的功能模块,而是集中处理实际运行中可能出现的…

2026/10/12 5:43:21 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →