1. 从一个真实的调试现场说起做QNX系统开发的朋友大概率都经历过这样的场景设备跑着跑着内存悄悄涨上去了系统响应越来越慢最后要么某个进程被内核干掉要么整机重启。你打开IDE连上目标板想看看内存到底被谁吃了结果发现手里能用的工具就那么几个pidin、showmem、hogs翻来覆去地看数据是有一堆但总感觉隔着一层纱看不清全貌。我最近在做一个车载域控制器的项目系统跑的是QNX 7.1上面有十几个进程包括几个常驻的服务进程和一堆动态加载的模块。项目跑到第三个月的时候测试同事反馈说系统连续运行48小时之后某个服务进程的响应时间从平均2ms涨到了15ms而且内存占用曲线一直在缓慢爬坡。这个问题很典型典型的慢泄漏不是那种一下子把内存吃光的急性病而是温水煮青蛙式的慢性病。排查这种问题光看进程级别的内存统计是不够的。你需要深入到进程的地址空间里去看看每一个内存映射区域到底是谁、为什么、什么时候映射进来的。这就是mappings这个工具的价值所在。它不像pidin那样只给你一个总数而是把进程地址空间里每一段映射的起始地址、大小、权限、偏移量、所属对象全部列出来。你可以把它理解成Linux下的/proc/[pid]/maps但QNX的mappings在信息粒度和使用方式上有自己的特点。这篇文章我会从实际排查经验出发把mappings这个工具彻底拆开讲清楚。包括它输出的每一列到底是什么意思、怎么用它来定位内存泄漏、怎么结合pmap和其他工具做交叉验证、以及在实际项目中我踩过的那些坑。不管你是刚接触QNX的新手还是已经用了几年但没深挖过内存分析的老手应该都能从里面找到有用的东西。2. QNX内存管理的基本盘2.1 进程地址空间的整体布局在深入mappings之前有必要先把QNX的进程地址空间模型理清楚。QNX是微内核架构这一点和Linux有本质区别。Linux的内核和用户进程共享同一个地址空间布局内核态和用户态通过页表权限位来隔离。QNX不一样它的微内核只负责最基础的调度、IPC、中断处理文件系统、网络协议栈、设备驱动这些全部跑在用户态的独立进程里。这意味着什么呢意味着QNX的进程地址空间里除了常规的代码段、数据段、堆、栈之外还会有大量通过mmap映射进来的共享内存区域。这些共享内存区域可能是用来做进程间通信的可能是用来映射设备寄存器的也可能是用来加载动态库的。mappings工具的核心价值就是把这些区域全部可视化出来。一个典型的QNX进程地址空间大概长这样最上面是栈往下是内存映射区域再往下是堆最下面是代码段和数据段。中间会穿插一些内核相关的映射比如qnx_slog、qnx_secpol这些。每个区域都有自己的权限属性比如r-x表示可读可执行rw-表示可读可写rwx表示可读可写可执行。2.2 为什么需要专门的内存分析工具有人可能会问pidin不是已经能看到进程的内存占用了吗为什么还要用mappings这个问题问得好。pidin给出的内存数据是聚合后的结果比如pidin mem会告诉你某个进程用了多少代码段、多少数据段、多少堆、多少栈。但它不会告诉你这些内存具体映射在哪些地址上也不会告诉你每一段映射的来源是什么。举个例子假设你发现某个进程的堆内存一直在涨你用pidin mem看到堆的大小从1MB涨到了10MB。但你知道这10MB具体是怎么分配的吗是malloc分配的还是mmap映射的是匿名映射还是文件映射这些问题pidin回答不了但mappings可以。再比如你怀疑某个共享库被重复加载了导致内存浪费。pidin只会告诉你这个进程加载了哪些库但不会告诉你每个库映射在哪个地址、映射了多大。mappings会把每一个映射区域都列出来你一眼就能看出有没有重复映射。2.3 mappings工具在工具链中的位置QNX的工具链里和内存分析相关的工具有好几个各自有各自的定位。pidin是进程信息查看器showmem是系统级内存统计hogs是资源占用排序pmap是进程内存映射查看器mappings也是进程内存映射查看器。那pmap和mappings有什么区别呢从我实际使用的经验来看pmap更偏向于快速查看输出格式比较紧凑适合在终端里快速扫一眼。mappings的输出更详细每一列的信息更完整适合做深入分析。另外mappings支持更多的过滤选项比如你可以只看某个特定类型的映射或者只看某个地址范围。在实际排查内存泄漏的时候我通常会用mappings来做详细分析用pmap来做快速验证。还有一个工具叫meminfo这个是系统级别的内存信息包括物理内存总量、空闲内存、缓存内存等等。它和mappings不在一个层面上meminfo看的是全局mappings看的是局部。排查内存问题的时候通常需要两者结合使用。3. mappings输出字段的逐行拆解3.1 输出格式总览mappings的基本用法很简单在终端里输入mappings [pid]就行。如果不带pid参数它会列出系统里所有进程的映射信息。输出大概长这样pid: 12345 start end size offset flags object 0x100000 0x101000 0x1000 0x0 r-x /proc/boot/procnto 0x101000 0x102000 0x1000 0x0 rw- /proc/boot/procnto 0x200000 0x210000 0x10000 0x0 rw- [anon] 0x300000 0x320000 0x20000 0x0 r-x /usr/lib/libc.so.4 ...每一列的含义如下列名含义备注start映射区域的起始虚拟地址十六进制表示end映射区域的结束虚拟地址不包含在区域内size映射区域的大小end - startoffset在映射对象中的偏移量文件映射时有效flags权限标志r/w/x/s等object映射对象的名称文件路径或[anon]3.2 start和end地址范围怎么看start和end是虚拟地址不是物理地址。这一点很重要因为虚拟地址是每个进程独立的不同进程的同一个虚拟地址可能映射到不同的物理内存。在QNX里虚拟地址的分配策略和Linux不太一样QNX更倾向于把映射区域放在固定的地址范围内这样便于调试和性能优化。看地址范围的时候有几个经验性的判断。第一如果两个映射区域的地址范围有重叠那肯定有问题说明地址空间管理出了bug。第二如果某个映射区域的起始地址不是页对齐的通常是4KB对齐那也可能有问题。第三如果映射区域的地址范围特别大比如几百MB甚至上GB那就要警惕了可能是某个进程申请了过大的内存。3.3 size大小计算与异常判断size这一列是end减去start的结果单位是字节。在QNX里映射区域的大小通常是页大小的整数倍。QNX 7.1的默认页大小是4KB但也可以配置成其他值比如16KB或64KB。如果你看到某个映射区域的大小不是页大小的整数倍那说明这个映射区域可能被截断了或者工具本身的计算有问题。判断size是否异常需要结合进程的实际行为。比如一个简单的命令行工具它的代码段通常只有几十KB到几百KB。如果它的代码段映射大小超过了1MB那就要查一查是不是链接了不必要的库或者编译选项有问题。再比如一个服务进程的堆内存如果持续增长且不回落那基本可以确定有内存泄漏。3.4 offset文件映射的偏移量offset这一列只在文件映射时有意义表示映射区域在文件中的起始偏移量。对于匿名映射[anon]这一列通常是0。对于共享库映射offset通常也是0因为整个库文件都被映射进来了。但对于某些特殊场景比如只映射文件的一部分offset就会是非零值。在实际排查中offset这一列用得不多但在某些特定场景下很有用。比如你怀疑某个文件被部分映射了导致数据不一致这时候看offset就能确认。再比如你在做性能优化想确认某个大文件是不是被完整映射了offset和size结合起来看就能判断。3.5 flags权限标志的深层含义flags这一列是权限标志常见的值有r-x可读可执行通常是代码段rw-可读可写通常是数据段或堆rwx可读可写可执行这个要特别小心通常是JIT编译器的代码页r--只读通常是只读数据段--x只可执行比较少见除了基本的读写执行权限QNX的mappings还会显示一些额外的标志位比如s共享映射表示这个映射区域可以被多个进程共享p私有映射表示这个映射区域是进程私有的写时复制n匿名映射表示这个映射区域不对应任何文件权限标志的异常组合是排查安全问题的重要线索。比如如果一个数据段被标记为rwx那说明这个区域既可以写又可以执行这通常是缓冲区溢出攻击的温床。再比如如果一个共享库被标记为rw-那说明这个库可以被修改这通常是不正常的因为共享库应该是只读的。3.6 object映射对象的识别object这一列是映射对象的名称可能是文件路径也可能是[anon]表示匿名映射。在QNX里映射对象可以是可执行文件本身比如/proc/boot/procnto共享库比如/usr/lib/libc.so.4设备文件比如/dev/io-display共享内存对象比如/dev/shmem/xxx匿名映射显示为[anon]识别映射对象是排查内存问题的关键。如果你看到某个进程映射了一个不该映射的文件那可能是配置错误。如果你看到大量匿名映射那可能是堆内存分配过多。如果你看到某个共享库被多个进程重复映射那可能是动态库加载策略有问题。4. 用mappings定位内存泄漏的实战方法4.1 内存泄漏的三种典型模式在QNX系统里内存泄漏通常表现为三种模式第一种是堆内存泄漏。进程通过malloc或new分配内存但忘记释放导致堆内存持续增长。这种泄漏在mappings里表现为[anon]映射区域的数量和大小持续增加。第二种是文件映射泄漏。进程通过mmap映射文件但忘记munmap导致文件映射区域持续增加。这种泄漏在mappings里表现为文件映射区域的数量持续增加但每个区域的大小可能不变。第三种是共享内存泄漏。进程创建了共享内存对象但忘记删除导致共享内存对象持续累积。这种泄漏在mappings里表现为/dev/shmem/下的对象数量持续增加。4.2 快照对比法两次采样找差异定位内存泄漏最直接的方法就是快照对比。具体操作是在系统稳定运行的时候用mappings抓一次快照保存到文件里。然后让系统运行一段时间比如几个小时或者几天再抓一次快照。最后用diff工具对比两次快照找出新增的映射区域。# 第一次快照 mappings 12345 /tmp/mappings_before.txt # 等待一段时间 # 第二次快照 mappings 12345 /tmp/mappings_after.txt # 对比差异 diff /tmp/mappings_before.txt /tmp/mappings_after.txt对比的时候要关注几个点新增的映射区域是什么类型是匿名映射还是文件映射大小是多少地址范围在哪里如果新增的都是匿名映射那基本可以确定是堆内存泄漏。如果新增的是文件映射那就要看是哪个文件被映射了。4.3 地址空间增长趋势分析除了快照对比还可以做趋势分析。具体做法是每隔一段时间比如每10分钟抓一次mappings快照然后统计每个进程的映射区域总大小和总数量画成趋势图。如果某个进程的映射区域总大小或总数量呈上升趋势那就有泄漏嫌疑。# 统计某个进程的映射区域总大小 mappings 12345 | awk NR1 {sum strtonum($3)} END {print sum} # 统计某个进程的映射区域总数量 mappings 12345 | wc -l趋势分析的好处是可以提前发现潜在问题不用等到系统出故障了才去排查。在实际项目中我通常会在测试环境里跑一个自动化脚本每隔一段时间抓一次快照然后自动分析趋势。一旦发现异常增长就自动触发告警。4.4 结合pmap做交叉验证mappings和pmap虽然功能有重叠但输出格式和侧重点不同。在实际排查中我通常会两个工具一起用互相验证。比如mappings显示某个进程有一个很大的匿名映射区域但pmap显示这个区域的大小不一样那就要查一查是不是工具本身的问题或者是不是映射区域在两次采样之间发生了变化。# 用pmap查看进程内存映射 pmap 12345 # 用mappings查看进程内存映射 mappings 12345交叉验证的另一个好处是可以发现一些隐藏的问题。比如mappings可能会把某些内核映射区域过滤掉而pmap会显示出来。反过来pmap可能会把某些共享内存区域合并显示而mappings会分开显示。两者结合才能看到完整的画面。5. 常见问题与排查技巧实录5.1 mappings输出为空或不全有时候你会遇到mappings输出为空的情况或者输出明显不全。这种情况通常有几个原因第一权限不够。mappings需要读取进程的地址空间信息如果当前用户没有足够的权限就会输出为空。解决办法是用root用户或者有足够权限的用户来执行。第二进程已经退出。如果你指定的pid对应的进程已经退出了mappings自然就输出为空。解决办法是先确认进程还在运行。第三工具本身的问题。某些版本的QNX里mappings可能存在bug导致输出不全。解决办法是升级到最新版本或者换用pmap。5.2 地址空间碎片化严重地址空间碎片化是QNX系统里比较常见的问题尤其是在长时间运行的进程里。碎片化的表现是映射区域数量很多但每个区域都很小导致地址空间利用率低。在mappings的输出里你会看到大量的小块映射区域地址不连续。碎片化的危害是可能导致大块内存分配失败即使系统总内存还很多。解决办法是优化内存分配策略尽量使用大块分配避免频繁的小块分配和释放。另外可以考虑使用内存池技术减少碎片化。5.3 共享库重复映射共享库重复映射是另一个常见问题。表现是同一个共享库被同一个进程映射了多次导致内存浪费。在mappings的输出里你会看到同一个库文件的路径出现了多次每次的地址范围不同。重复映射的原因通常是动态库加载器的问题或者程序里显式调用了dlopen但没有调用dlclose。解决办法是检查动态库加载逻辑确保每个库只被加载一次。另外可以用LD_DEBUG环境变量来跟踪动态库的加载过程。5.4 匿名映射过多匿名映射过多通常意味着堆内存分配过多。在mappings的输出里你会看到大量的[anon]映射区域。这些区域可能是malloc分配的也可能是mmap匿名映射的。匿名映射过多的危害是内存占用高而且不容易追踪。解决办法是使用内存分析工具比如mtrace或valgrind的QNX版本来追踪内存分配。另外可以考虑使用自定义的内存分配器记录每次分配的调用栈。5.5 常见问题速查表问题现象可能原因排查方法解决办法mappings输出为空权限不够检查当前用户权限用root用户执行mappings输出为空进程已退出检查进程状态确认进程还在运行映射区域数量多但都很小地址空间碎片化统计映射区域大小分布优化内存分配策略同一个库出现多次共享库重复映射检查dlopen/dlclose调用确保库只加载一次大量[anon]映射堆内存分配过多用mtrace追踪分配优化内存使用映射区域大小不是页大小整数倍映射被截断检查mmap调用参数确保映射大小页对齐6. 工具选型与性能优化建议6.1 mappings vs pmap vs pidin mem这三个工具各有优劣选择哪个取决于你的具体需求。mappings适合做详细分析输出信息最全但速度相对较慢。pmap适合做快速查看输出紧凑速度快。pidin mem适合做进程级别的内存统计不涉及地址空间细节。在实际项目中我通常会这样组合使用先用pidin mem做全局扫描找出内存占用异常的进程。然后用pmap做快速验证确认异常进程的映射情况。最后用mappings做详细分析定位具体的泄漏点。6.2 自动化监控脚本的编写手动抓快照效率太低尤其是在需要长时间监控的场景下。我通常会写一个自动化脚本定期抓取mappings快照然后自动分析。脚本的核心逻辑是#!/bin/bash # 自动化内存监控脚本 PID$1 INTERVAL600 # 10分钟 LOG_DIR/tmp/mem_monitor mkdir -p $LOG_DIR while true; do TIMESTAMP$(date %Y%m%d_%H%M%S) mappings $PID $LOG_DIR/mappings_$TIMESTAMP.txt # 统计映射区域总大小 TOTAL_SIZE$(mappings $PID | awk NR1 {sum strtonum($3)} END {print sum}) echo $TIMESTAMP $TOTAL_SIZE $LOG_DIR/size_trend.txt sleep $INTERVAL done这个脚本会每10分钟抓一次快照并记录映射区域总大小的变化趋势。你可以根据实际需求调整间隔时间和日志目录。6.3 性能开销与采样频率的权衡mappings本身对系统性能的影响很小因为它只是读取内核里的数据结构不做任何计算密集型操作。但在高频采样的情况下累积的开销也不容忽视。比如如果你每秒钟抓一次快照那mappings进程本身就会占用一定的CPU和内存。采样频率的选择需要权衡。如果内存泄漏很慢比如每天只涨几MB那每小时采样一次就够了。如果内存泄漏很快比如每分钟涨几MB那可能需要每分钟采样一次。在实际项目中我通常从每10分钟采样一次开始根据实际情况调整。6.4 结合系统日志做关联分析mappings的输出是静态的只能告诉你当前时刻的映射情况。要理解映射变化的原因还需要结合系统日志。QNX的系统日志slog里会记录进程的启动、退出、内存分配失败等事件。把这些事件和mappings的快照关联起来就能更准确地定位问题。# 查看系统日志 sloginfo # 查看特定进程的日志 sloginfo -p 12345关联分析的一个典型场景是你发现某个进程的映射区域突然增加了很多同时系统日志里出现了内存分配失败的记录。那基本可以确定这个进程在尝试分配大块内存但系统内存不足导致分配失败。解决办法是优化内存使用或者增加系统内存。7. 个人实操体会mappings这个工具我用了大概有五年了。从最开始只会看个大概到后来能通过输出快速定位问题中间踩了不少坑。最大的体会是不要孤立地看mappings的输出一定要结合进程的行为、系统的状态、日志的记录来综合分析。另外mappings的输出格式在不同版本的QNX里可能会有细微差别。比如QNX 6.5和QNX 7.1的mappings输出就不完全一样。在跨版本迁移的时候要注意这些差异避免因为格式变化导致分析错误。最后分享一个小技巧如果你觉得mappings的输出太长看起来费劲可以用grep过滤。比如只看匿名映射mappings 12345 | grep anon或者只看某个特定大小的映射区域mappings 12345 | awk $3 0x100000这些过滤技巧可以大大提高分析效率。在实际排查中我通常会先用过滤命令缩小范围然后再仔细看具体的映射区域。
企业数字化 ERP 产品动态
相关推荐
SPI隔离通信实战:MAX14431数字隔离器与PIC18F4620硬件设计 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:03:59
工业级隔离SPI设计:MAX14431与MKV46F256选型调试全解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:03:59
MAX14431BAEE+与PIC18F4620隔离SPI通信链路设计 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:03:59
MPU-60X0六轴芯片九轴输出:量程、DMP、FIFO与总线踩坑 简介:《MPU-6000 / MPU-6050 产品说明书》中文翻译版是一份面向嵌入式硬件工程师、飞控与机器人开发者的传感器手册,针对英文原版阅读门槛高、寄存器与引脚细节难懂的问题,只翻译了实际开发中用得上的部分。内容覆盖芯片整体架构、DMP 数字运… · 2026/11/3 3:54:38
逆向拆解能力:从结果反推决策链,把别人经验变成自己起点 不知道从什么时候开始,我判断一个人有没有“手感”,只用一把尺子:给他一个好结果,看他能不能倒推出它怎么来的。这说的就是 reverse-skill,逆向拆解的能力。看到一篇爆款文章,普通人会想“写得好”… · 2026/11/3 3:54:38
hidetoolz 3.0:绿色工具集整理与打包分发实践 1. 先想清楚:一个自用工具箱为什么要做到 3.0我这几年有个习惯,电脑里始终放着一个自建的工具目录,名字换过几次,最早叫tools,后来改成toolbox,现在固定叫hidetoolz,版本号一路推到 3.0。说白了… · 2026/11/3 3:54:38
消费品牌如何构建系统性烙印体系提升认知度 1. 为什么消费品牌需要系统性烙印体系?在消费品市场高度饱和的今天,消费者每天接触的品牌信息超过5000条。我服务过的一个新锐茶饮品牌,首月营销投放费用高达300万,但消费者调研显示品牌记忆度仅为12%。这个残酷的现实让我意识到&… · 2026/11/3 3:54:38
以太网供电POE实战指南:从选型到部署的避坑心得 以太网供电这个话题,我最早接触是在一个安防监控项目里。当时甲方要求所有摄像头“只走一根线”,我第一反应是这怎么可能——又要供电又要传数据,难不成还得在弱电箱里塞一堆插座?后来被现场逼着把POE这套东西从头啃了一遍&#x… · 2026/11/3 3:54:38
Level-2全推行情Python SDK部署避坑:多市场订阅与版本对齐 简介:《同花顺level-2全推行情Python API操作手册》面向高频交易与量化开发者,尤其适合做涨停板、盘口异动类策略的研究与工程人员,用来解决Level-2行情如何借助Python稳定接入、按字段准确解析的落地问题。资料是单个PDF文档,压缩… · 2026/11/3 3:53:38
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18