面灵AI→

绿盟 C++开发一面:core 分析、内存泄漏与增量渲染

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

  1. 请做一下自我介绍
  2. 你在开发中遇到过最难定位的一类问题是什么?最后是如何缩小范围的?
  3. C++ 程序发生崩溃时,你会如何分析一份 core 文件?
  4. 如果一个 C++ 服务运行数小时后内存持续上涨,你会怎样判断是否存在泄漏?
  5. Linux 下虚拟内存和物理内存之间是如何建立映射的?
  6. Linux 中怎样区分匿名内存、文件映射和线程栈占用?
  7. 如何设计一个支持超时、取消和重试的异步任务框架?
  8. 多个智能体并行处理同一任务时,如何避免重复工作和结果冲突?
  9. 大模型上下文长度不够时,如何实现上下文压缩而不明显损失关键信息?
  10. MCP 类工具调用中,如何防止工具描述过长、参数错误和权限越界?
  11. 如何设计 Markdown 文档的增量解析和实时渲染?
  12. Markdown 实时预览出现光标跳动和滚动位置丢失,通常如何处理?
  13. 数据库查询已经建立索引,但性能仍然不稳定,你会检查哪些因素?
  14. 面对多个等值条件和范围条件组合的查询,复合索引应该如何设计?
  15. 数据库出现大量锁等待时,你会怎样定位和缓解?

《参考解析》

core 文件分析:先对齐版本,再让栈说话。 第一步是确认可执行文件、依赖的动态库和 core 三者来自同一次构建——版本对不上,栈回溯出来的符号和行号会指到错误位置,后面全白干。然后 gdb ./app core.xxx,依次看:崩溃线程的调用栈(bt full 连局部变量一起打)、所有线程的栈(thread apply all bt,很多崩溃是别的线程把内存改坏了)、寄存器和出错的指令、以及当前帧的参数和 list 出的源码行。一个关键判断:崩溃点常常只是最终表现位置,不是根因位置——野指针写坏内存、栈溢出、数据竞争这三种情况尤其如此。所以还要结合最近代码变更、日志时间线,并在能复现时上 AddressSanitizer(野指针/越界)或 ThreadSanitizer(数据竞争)来定位真正的第一现场。

内存持续上涨不等于内存泄漏。 进程 RSS 上涨可能是堆泄漏、线程栈增长、大量 mmap、页缓存,也可能是分配器把释放的内存留在自己的 free list 里没归还操作系统——最后这种是「看起来泄漏」而非泄漏。判定步骤:先看 /proc/<pid>/smaps_rollup 和 pmap -x <pid> 拆清 RSS 的构成;再把请求量、对象数量、连接数和内存曲线对起来看,如果是随请求量线性上涨且不回落,泄漏的嫌疑就大;然后看某类对象是否只增不减。工具上,能离线复现就用 valgrind --tool=massif 或 LeakSanitizer,线上则用 allocation profiling 或自己在关键模块打分配/释放计数。另外要区分「每次请求泄漏一点」和「启动时预热分配、之后平稳」——压测时主动制造流量再观察斜率,比盯着一个绝对值可靠。

虚拟内存与物理内存的映射,以及怎么拆解 RSS。 进程访问的是虚拟地址,真正取数时由页表把虚拟页映射到物理页,TLB 缓存最近的转换结果;缺页异常发生时内核可能新分配物理页(匿名页)、从文件读入(文件映射)、或者执行写时复制。特别注意:mmap 或 malloc 成功只代表虚拟地址空间建好了,物理页往往是首次访问时才分配,所以大量首次触碰会带来明显的缺页延迟。区分内存构成看 /proc/<pid>/smaps:匿名页来自堆和匿名 mmap,文件映射来自动态库和映射文件,线程栈是单独的栈映射。分析时必须同时看 Rss / Pss / Private_Dirty / Shared_Clean——多进程共享同一份动态库时,把各自的 RSS 相加会严重高估实际成本,Pss 才是按共享比例分摊后的口径。

异步任务框架的超时、取消与重试。 核心是把任务做成显式状态机(等待/运行/成功/失败/超时/取消),而不是靠调用方自觉。超时尤其容易做错:调用方停止等待不代表后台任务停了,所以必须配取消令牌(一个原子的 cancelled_ 标志,任务在可中断点主动检查退出),或者用可中断的 IO 与条件变量。重试要区分可重试错误(网络抖动、限流)和不可重试错误(参数非法、权限不足),并配指数退避加随机抖动、设最大次数和总时长上限——否则下游故障恢复的瞬间会被重试洪峰再打一次。最后,每个任务要带唯一的请求标识并做幂等,否则重试会造成重复提交;结果回调要能处理「任务已被取消但结果晚到」的情况。

多智能体并行同一任务时怎么不打架。 要点是「先拆边界,再发租约,最后做版本校验」。子任务要有唯一标识和明确的所有者;调度层维护任务状态、租约时间和完成版本,智能体领取任务时用原子操作或带条件的更新(比如 UPDATE ... WHERE status='pending',靠数据库唯一约束而不是先查后写)保证同一任务不会被两个执行者同时领走。结果提交要携带任务版本,旧版本的结果不能覆盖新版本。对允许重复执行的任务,用幂等键消除重复副作用;对不允许的,就用租约超时加重新派发。语义上还要明确定义「冲突时谁说了算」,否则多个智能体给出不同结论时无法收敛。

上下文压缩:别只截断最早的对话。 简单的滑动窗口截断会丢关键约束,正确做法是按信息类型分类处理:把历史拆成事实、已定决策、硬约束、未完成事项和工具结果,对已经解决的过程做摘要,与当前任务直接相关的原文保留、低相关的中间推理才压缩。压缩产物要带来源和时间戳,否则摘要逐轮传播会出现事实漂移(越传越走样)。结构化内容(代码、配置、错误日志)优先保留关键片段和行号而不是自然语言概括——概括会丢掉复现所需的信息。工程上还可配合「长工具输出外置到文件,上下文里只留句柄和摘要」这一招,往往比压缩本身更有效。

MCP 工具调用的三个风险面。 描述过长:工具描述应该只暴露当前任务所需的能力,并按需分批下发,避免把全部工具一次性塞进系统提示挤占预算。参数错误:参数要有明确的 schema,并且服务端必须二次校验——模型生成的参数是不可信输入,不能因为「是模型给的」就放行。权限越界:文件路径、命令、网络地址、资源标识都要做白名单或范围限制,删除/发布/写生产这类高风险操作要额外确认或走独立审批。执行后还要留审计:调用者、参数摘要、耗时、结果和错误,既便于排查也是追责依据。

Markdown 增量解析与光标稳定。 编辑器不能在每次按键后重解析整篇文档,应该按块或语法节点维护结构,只重解析受影响的区间、复用未变化的渲染节点。要专门处理未闭合语法(输到一半的代码块、列表、引用、表格),否则输入过程中预览会反复跳动。安全上不能让渲染层直接拼接不可信 HTML,至少要转义并过滤协议,防止链接或内联 HTML 注入脚本。光标与滚动丢失的根因通常是「编辑区与预览区互相直接驱动滚动」和「整棵树被重建」:正确做法是维护源代码位置与预览节点的映射,按语法节点定位;光标位置要用逻辑偏移或行列信息保存,绝不能依赖已经被替换掉的 DOM 节点。

索引在、性能却飘忽,以及锁等待排查。 索引存在不代表优化器一定选它——返回数据比例过高时全表扫描反而更快,统计信息过期会导致代价估算错误,函数包裹索引列、隐式类型转换、前置 % 模糊匹配都会让索引失效;参数不同还可能生成不同执行计划,所以线上要记录完整 SQL 模板加参数分布,不能只看平均耗时。复合索引的设计原则是等值列在前、范围列和排序列在后,但必须用真实执行计划验证,同时权衡索引数量带来的写入成本。锁等待则先画等待链:哪个事务持锁、哪个在等、锁住了哪些行或表、事务已经跑了多久;根因常见于长事务、缺索引导致的大范围锁定、多张表访问顺序不一致。缓解手段是缩短事务范围、补索引减少扫描行数、统一加锁顺序,以及给事务和锁等待设超时监控。

Markdown 增量解析里的边界校验。 顺带说一个容易被忽略的细节:接受外部变更(如协同编辑的增量 patch)时,offset 和 removed 必须做范围校验再执行替换,否则越界的 patch 会直接抛异常或损坏缓冲区——这类校验应该放在入口而不是靠调用方自觉。