1. 自我介绍
  2. 各个指标的定义是什么,怎么采集的
  3. 为什么用postmsg,原子ack
  4. 流畅性监控这里,怎么区分的消息队列积压,还是说这里没做处理
  5. 野指针、悬空指针、内存泄漏
  6. 详细讲讲tls 快照这里
  7. 为什么要用跨进程锁
  8. 跨进程锁和进程间的有什么区别
  9. std::mutex 和 atomic 有什么区别
  10. 进程和线程区别
  11. 那什么时候用多线程,什么时候用多进程
  12. Linux 命令说一些
  13. 跑shell 命令都要注意什么
  14. RAII
  15. 用过哪些智能指针
  16. socket编程 listen accept
  17. 讲讲IO多路复用
  18. 多agent 协作,确定性路由,为什么不让llm来决定

手撕:实现一个线程安全的定长队列,长度大于定长push 阻塞,队列为空pop 阻塞


百度基础安全部后端二面 · 参考答案

根据本轮面试题、简历中的腾讯搜狗输入法 C++ 客户端实习 / 科大讯飞 C++ 开发 / 《杀戮尖塔 2》Game Agent 项目整理。
使用方式:先记住每题的第一句话,再按追问展开;不要把没有实际做过的 API、阈值和实现细节说成自己的经历。

本轮答题主线

  • 项目主线:资源监控 → UI 卡顿探针 → 抓栈定位跨进程锁等待 → 超时锁与 TLS 快照优化。
  • 基础主线:C++ 生命周期与并发 → Linux 进程 / 线程 / Socket / epoll。
  • 安全主线:执行 Shell 命令时要强调参数注入、权限、超时、资源限制、审计和 TOCTOU。
  • Agent 主线:LLM 负责不确定的分析,状态机 / 策略 / 权限负责约束控制流;确定性路由优先保证可验证和可回放。

1. 自我介绍

参考回答(约 1 分钟):

面试官好,我叫江岸伟,是安徽大学计算机技术硕士在读,本科是电子信息工程。

目前我在腾讯搜狗输入法 PC 客户端做 C++ 开发实习,主要负责稳定性和性能方向。第一块是资源占用监控,覆盖 5 个核心进程,采集 CPU、内存、磁盘空间和磁盘 I/O,做聚合上报,并据此定位过升级流程中的旧版 IME 文件残留问题;第二块是 UI 卡顿检测,我设计了 Watchdog 线程,每 200ms 通过 PostMessage 探测 UI 线程,由 UI 线程更新原子 ACK 时间戳,再按响应延迟分级,最终通过抓栈定位到跨进程互斥锁无限等待;第三块是流畅性优化,在保持 ABI 兼容的前提下新增超时锁接口,并用焦点驱动的 TLS 配置快照减少热路径加锁,按键路径加锁次数下降了 50%。

之前我在科大讯飞智能汽车事业部做过 APA 自动泊车相关 C++ 开发,涉及车位角点融合、状态机和 GJK 碰撞检测。项目上还做过基于 LangGraph 和 MCP 的《杀戮尖塔 2》自主决策 Game Agent,包含战斗、卡组和路线等职责拆分,以及状态校验和策略记忆。

我的基础方向是 C/C++、C++17、STL、RAII、多线程 / 异步编程和 Linux 系统编程,熟悉 Socket、epoll、TCP/IP、HTTP;同时对 Agent、MCP、RAG 和 AI Coding 工具链有实践。虽然经历偏客户端和系统工程,但我比较关注并发、性能、可靠性和安全边界,这些能力也希望迁移到基础安全部的后端场景中。

**回答重点:**不要逐条背简历,围绕“问题—方案—结果—迁移到岗位”讲;被追问时优先讲真实案例,不主动编造工具名或实现细节。


2. 各个指标的定义是什么,怎么采集的

参考回答:

我会先把“指标定义”和“采集方式”分开讲,避免把不同口径混在一起。监控链路覆盖 5 个核心进程,采集线程按策略读取系统计数器,计算增量、打上进程和版本等维度,再做聚合与上报。

  • CPU 使用率:一段时间内进程消耗的 CPU 时间增量,除以墙上时间增量;如果展示的是整机归一化百分比,还要明确是否再除以逻辑 CPU 数。用两次快照的进程用户态 + 内核态 CPU 时间差计算,而不是在业务线程里持续计时。
  • 内存占用:要先明确口径。Working Set 更接近当前驻留物理内存,Private Bytes / Commit 更接近进程私有的提交量,二者不能混称为同一个“内存”。线上监控要固定一种口径,必要时同时采集并分别命名。
  • 磁盘空间:通常是目标卷的总空间、可用空间和使用率;它反映的是容量余量,不等于进程的磁盘 I/O。
  • 磁盘 I/O:读取 / 写入字节数在采样周期内的增量,除以周期得到吞吐;如果系统接口提供,还可以补充 IOPS、等待时间等指标。

所以采集流程是:低频快照 → 用增量换算速率 → 本地按进程 / 版本聚合 → 批量上报。采集放在独立线程,不进入 UI 和按键热路径;采样周期、指标开关和上报策略由云控调节。具体 Windows API 名称我会以实际代码为准,不会把没有使用过的 API 说成项目事实。

常见追问:

  • CPU 公式中的时间单位必须一致;展示口径要说明是否按所有逻辑核归一化。
  • 指标要带时间戳、进程标识、客户端版本和采样周期,否则多个进程或版本的数据无法比较。
  • 证明监控开销可接受:做开关监控的 A/B 基准,对比按键延迟、CPU 基线、内存和 I/O;同时上报监控线程自身耗时,并保留总开关。

3. 为什么用 PostMessage,为什么用原子 ACK

参考回答:

Watchdog 的目标是探测 UI 线程是否还能回到消息循环,所以我需要的是异步探针。PostMessage 把消息放入目标线程队列后立即返回,Watchdog 不会因为 UI 正在卡顿而一起阻塞;如果用 SendMessage,发送线程要等 UI 处理完成,反而可能把 Watchdog 也挂住。

UI 线程收到探针后更新一个原子 ACK 时间戳。这个共享状态只有很小的读写,不值得用互斥锁;原子变量能避免数据竞争,并保证 Watchdog 读到的是一个完整值。需要更严格地计算单次探针延迟时,我会同时携带单调递增的 sequence,记录 sent / received 的时间,而不只看“最近一次 ACK”。

PostMessage 不是可靠队列:需要检查发送失败,防止探针无限堆积,并控制未完成探针数量。这个机制用于判断 UI liveness,不把它当作业务消息传输。

**一句话对比:**PostMessage 是异步投递,SendMessage 是同步等待;原子 ACK 是无锁地发布一个小型状态,不代表所有业务数据都适合用 atomic。


4. 流畅性监控如何区分消息队列积压、长任务和锁等待

参考回答:

这里要区分“发现 UI 没响应”和“证明消息队列积压”。我当时的主要链路是 PostMessage + ACK + 卡顿时抓栈,最终确认的是 UI 线程卡在跨进程锁上;并没有把消息队列长度做成一个足够完整的一等指标,所以我不会说仅凭 ACK 就能精确区分所有情况。

如果完善这条链路,我会给探针加 sequence,并记录几个阶段:Watchdog 发出、UI 收到、处理开始、处理结束、ACK 发布,同时维护 sent_count - ack_count。再把消息循环心跳、探针处理耗时和必要的队列状态作为辅助证据:

  • 消息队列积压:已发 sequence 和已确认 sequence 的差距持续扩大;UI 恢复后会短时间集中处理,ACK 追上,说明消息循环曾经没有及时消费。
  • 长任务:消息已经出队并进入处理函数,但处理开始到结束耗时很长;队列可能不一定持续增长,调用栈通常显示在计算、文件扫描或同步调用中。
  • 锁等待:处理函数已经开始,但线程长期睡眠在 mutex / 临界区 / 跨进程同步对象上;CPU 往往不高,抓栈能看到等待点。
  • 死循环或忙等:ACK 不更新、CPU 占用高,调用栈会停在循环或重复计算路径。

GetQueueStatus、PeekMessage 一类接口最多作为辅助,不能代替阶段埋点和抓栈,因为读取队列本身有竞态,也未必覆盖所有消息来源。最终定性要结合 ACK、处理阶段、CPU / I/O 和调用栈。

**被问“当时有没有做处理”时:**可以直接说“当时优先解决的是根因锁等待,通过抓栈确认后加超时锁和降级;消息积压监控是可以继续补强的方向”。


5. 野指针、悬空指针、内存泄漏

参考回答:

  • 野指针通常指未初始化、被破坏或指向无效地址的指针,问题是它从一开始就没有可靠指向。
  • 悬空指针原来指向过有效对象,但对象已经释放或离开作用域,指针还被继续使用,典型结果是 use-after-free。
  • 内存泄漏是资源已经分配,但程序丢失了最后的可达引用,导致无法释放;它不一定立即崩溃,但会让占用持续增长。

预防上,先明确所有权:默认用 RAII 和 unique_ptr,确实需要共享时才用 shared_ptr,观察者使用引用、裸指针或 weak_ptr 并明确其不拥有对象;初始化指针、避免返回局部对象的引用、避免从一个裸指针构造多个 shared_ptr。排查时结合复现、dump / 调用栈、ASan / UBSan、Valgrind 或平台调试工具,以及静态检查和资源增长对比。

智能指针只能解决一部分生命周期问题:shared_ptr::get() 暴露出去的裸指针仍可能悬空,循环引用仍可能泄漏,因此关键还是所有权和生命周期设计。


6. 详细讲讲 TLS 快照

参考回答:

这里的 TLS 是 Thread Local Storage,不是 HTTPS 里的 TLS。原来的问题是输入法按键热路径频繁读取 UserEnv / 配置,需要获取跨线程或跨进程同步锁;锁竞争会把一个本应很短的读取变成不可控的等待。

我的思路是把“配置更新”和“热路径读取”分开:焦点切换或配置变更时,在有同步保护的上下文中读取权威状态,构造一份完整的、带版本号的配置快照;然后通过 UI 线程自己的焦点事件 / 消息,在 UI 线程内更新它的 thread_local 副本。按键热路径只读取当前线程的副本,不再为每次读取获取锁;快照的异步落盘交给后台任务,避免 UI 线程被磁盘 I/O 阻塞。

这里有两个重要的正确性点:第一,不能让别的线程直接写 UI 线程的 TLS,更新动作要发生在拥有该 TLS 的线程,或采用明确的不可变快照发布方案;第二,TLS 是优化后的本地视图,不是全局真相,要通过版本号 / 焦点标识 / 变更通知处理过期情况,更新失败时保留受控的加锁回退路径。

这个方案用空间和更新复杂度换读取路径的低延迟,实际效果是按键路径加锁次数下降 50%。这个数字需要结合埋点或性能对比说明“优化前后同一场景”,不能只凭感觉估计。

追问抓手:

  • 快照应先完整构造,再发布;不要在读线程看到一半更新一半的可变结构。
  • 线程局部副本不适合跨线程共享,也不能替代进程间同步。
  • 如果配置变更频繁,焦点事件不足以覆盖所有更新,需要额外的变更通知或版本校验。

7. 为什么要用跨进程锁

参考回答:

因为参与访问的是不同进程共同拥有的资源或协议状态,例如输入法和宿主 / 辅助进程之间共享的对象。如果只用进程内 mutex,它只对同一地址空间里的线程有效,另一个进程即使使用了“同名变量”也不会被它阻塞,无法保护跨进程临界区。

跨进程锁可以把对共享内存、命名对象、文件或 IPC 协议状态的访问串行化。但它应该只保护很短的内存操作,不能在锁内做磁盘 I/O、网络调用或等待另一个不受控的进程;在 UI 热路径上必须使用有界等待、try-lock 或降级路径。还要统一加锁顺序、处理持有者崩溃 / 锁失效、设置安全的对象权限。

我们当时的问题不是“用了锁”本身,而是 UI 热路径上的跨进程锁没有超时,异常情况下会无限等待。后续通过新增超时接口保持旧 ABI,并让超时走降级,降低了卡死风险。

**补充:**如果共享状态可以改成单一进程持有、其他进程通过消息 / RPC 请求,就优先考虑所有权转移,往往比长期共享锁更容易维护。


8. 跨进程锁和进程间锁有什么区别

参考回答:

在通常语境里,“跨进程锁”和“进程间锁”是同一类概念,都是协调不同进程之间的并发访问;前者强调作用域跨越进程,后者强调参与者是多个进程,没有本质区别。

真正需要区分的是实现方式:Windows 可以使用命名的内核互斥体等对象;Linux 可以使用文件锁、命名 IPC 对象,或者放在共享内存中并配置为 PTHREAD_PROCESS_SHARED 的同步对象。它们在命名 / 生命周期、权限、持有者崩溃后的行为和超时能力上不同。

而 std::mutex 是进程内同步原语,不能直接拿来协调两个独立地址空间的线程。若面试官这里想问的是“进程内锁和进程间锁”,我会按这个维度展开,而不会把两个同义词硬说成不同东西。


9. std::mutex 和 atomic 有什么区别

参考回答:

std::mutex 保护的是一段临界区和一组不变量:加锁后可以安全地读写多个字段、容器或复杂对象,竞争时线程会阻塞或被唤醒。std::atomic 保证某个原子对象的单次读写、交换、递增或 CAS 不被并发操作打断,适合计数器、状态位、sequence 和 ACK 时间戳。

atomic 不是 mutex 的全面替代:例如“判断队列非空再弹出”涉及多个操作和容器结构,不能只给一个 size 用 atomic 就保证整体正确;多字段的一致性通常需要 mutex,或重新设计成 CAS 状态机。C++ 的 atomic 也不保证一定是硬件 lock-free,要用 is_lock_free 判断。

我在 Watchdog 里用 atomic 保存 ACK 时间戳,是因为共享数据只有一个小状态;配置表、队列或需要保持不变量的结构仍应使用 mutex。跨进程场景也不能因为看到 atomic 就默认可用,通常要使用平台提供的进程间同步原语和共享内存方案。

可继续展开:memory_order_relaxed 适合只做计数;发布快照时可用 release / acquire 建立可见性,但内存序必须和数据发布设计一起验证。


10. 进程和线程的区别

参考回答:

进程是资源分配和隔离的基本单位,拥有独立的虚拟地址空间、页表和资源句柄视图;线程是调度的基本单位,同一进程的线程共享代码段、数据、堆和大部分进程资源,但各自有栈、寄存器上下文、调度状态和 TLS。

进程之间隔离强,一个进程崩溃或内存破坏通常不会直接覆盖另一个进程,但创建、切换和 IPC 成本更高;线程共享内存,通信便宜、切换较轻,但需要同步,数据竞争或一个线程的非法访问可能影响整个进程。进程间通信要用管道、Socket、共享内存等,线程间则可用 mutex、condition variable、atomic 等。


11. 什么时候用多线程,什么时候用多进程

参考回答:

我会根据共享程度、故障隔离和通信成本选择,而不是简单按 CPU 核数选择。

  • 多线程:任务属于同一个服务 / 客户端,需要频繁共享数据;例如 UI 线程配合采集、网络或计算 worker,或者把阻塞 I/O 从主线程移走。它内存成本较低、通信快,但必须明确所有权、锁顺序和退出协议。
  • 多进程:需要故障隔离、权限隔离、独立重启、限制资源,或第三方 / 不可信模块不能和主进程共享地址空间;也适合组件生命周期完全独立的场景。代价是 IPC、序列化、内存占用和部署复杂度。

在输入法场景中,UI 和紧密协作的工作通常放在线程内;宿主交互或已有的辅助组件由于进程边界和隔离要求,通过跨进程机制协作。CPU 密集任务可以用多线程利用多核,但若更重要的是崩溃隔离和安全边界,优先拆成进程。


12. 说一些 Linux 命令

参考回答:

我一般按排查目标使用,而不是只背命令名:

目标常用命令关注点
文件 / 磁盘pwd、ls -lah、find、stat、du -sh、df -hdu 看目录实际占用,df 看文件系统剩余空间
文本 / 日志grep / rg、head、tail -f、awk、sed、sort、uniq、wc先缩小范围,再统计和定位
进程 / 资源ps、pgrep、top / htop、free -h、vmstat、pidstat、lsof看进程、线程、CPU、内存、打开的 fd
网络ip addr、ip route、ss -lntp、curl -v、ping、nc地址路由、监听端口、HTTP 和 TCP 连通性
调试strace、gdb、coredumpctl、dmesg / journalctl系统调用、core、内核和服务日志
权限 / 服务id、chmod、chown、sudo、systemctl当前身份、权限和服务状态

例如遇到服务端口问题,我会先 ss -lntp 看是否监听,再用 curl -v 或 nc 验证协议,最后用日志或 strace 缩小问题;遇到进程异常,则结合 ps、top、lsof 和 core / gdb。


13. 跑 Shell 命令都要注意什么

参考回答:

Shell 命令本身就是一个高风险边界,尤其是服务端代替用户执行命令时。我会从以下方面控制:

  1. 防命令注入:不要把用户输入直接拼进 system() 或 sh -c 字符串;优先使用 execve / posix_spawn 这类 argv 接口,命令和参数使用 allowlist。必须经过 shell 时才做严格转义,并用 -- 结束选项解析。
  2. 防误删和路径攻击:删除前校验路径是否为空、是否为预期目录、是否发生规范化 / 符号链接跳转;不要直接执行 rm -rf "$user_path"。临时文件使用安全创建接口和最小权限,注意 check-then-use 的 TOCTOU 竞态。
  3. 权限最小化:不要默认 root / sudo,固定可信的可执行文件路径,控制 cwd、PATH 和继承环境变量;敏感 token 不放在命令行参数、日志或错误信息里。
  4. 生命周期和资源:设置超时、退出码检查、stdout / stderr 大小上限;读取管道时避免只读一侧导致死锁,超时后按进程组终止并回收子进程;设置 close-on-exec 和必要的 fd / CPU / 内存限制。
  5. 审计与可回放:记录调用者、允许的命令类型、开始结束时间、退出码和脱敏参数;生产环境先 dry-run 或在沙箱中执行,避免无审查的 curl | bash。

如果只是本地排查,我也会先确认 pwd、目标路径和 git status,再执行有副作用的命令;涉及删除、权限和批量修改时先备份或使用可逆方案。

**和基础安全岗位的联系:**命令执行不是“会不会写 Shell”这么简单,核心是把输入、权限、文件系统、资源和审计边界都控制住。


14. RAII

参考回答:

RAII 是 Resource Acquisition Is Initialization:对象构造时获取资源,析构时释放资源,利用 C++ 的确定性析构把资源生命周期绑定到对象生命周期。资源不只包括堆内存,也包括锁、文件、Socket、线程句柄、事务和临时状态。

例如 lock_guard 构造时加锁,离开作用域时自动解锁;unique_ptr 析构时释放堆对象。这样即使函数提前 return 或发生 C++ 异常,栈展开也会执行局部对象析构,减少泄漏和忘记解锁。RAII 不是只利用栈:堆上的对象也可以由栈上或成员中的智能指针管理。

使用 RAII 时要明确拷贝 / 移动语义和所有权,优先遵循 rule of zero;对 C API 句柄可以配自定义 deleter。它不能保证进程在 SIGKILL、崩溃、abort 或 _exit 时完成清理,所以进程级故障还需要 dump、恢复和外部监管。


15. 用过哪些智能指针

参考回答:

  • std::unique_ptr:独占所有权,不能拷贝但可以移动,适合作为默认选择,开销小。
  • std::shared_ptr:多个对象共享所有权,通过控制块引用计数管理生命周期;只有确实存在共享所有权时使用,拷贝会带来计数和控制块开销。
  • std::weak_ptr:不拥有对象,用 lock() 临时获得 shared_ptr,常用于观察者关系和打破循环引用。

创建时优先 make_unique / make_shared,不要从同一个裸指针分别构造两个 shared_ptr,否则会 double delete;需要在成员函数中生成指向自身的共享指针时,要正确使用 enable_shared_from_this,不能随手写 shared_ptr(this)。

线程安全要分开看:不同 shared_ptr 实例对同一控制块的引用计数通常是安全的,但指向的业务对象不因此变成线程安全;对象并发读写仍要用 mutex 或其他同步机制。文件句柄等非内存资源也可以用 unique_ptr + 自定义 deleter 管理。


16. Socket 编程中的 listen 和 accept

参考回答:

TCP 服务端的基本流程是 socket → setsockopt → bind → listen → accept → recv/send → close。

  • socket 创建监听套接字;必要时设置地址复用和 close-on-exec。
  • bind 把本地 IP 和端口绑定到套接字。
  • listen 把主动连接套接字变为被动监听套接字,并设置待处理连接队列的上限。TCP 的握手排队和已经建立但尚未被应用取走的连接队列在内核中有不同阶段,不能简单把 backlog 理解成“能同时服务的连接数”。
  • accept 从已建立连接队列中取出一个连接,返回一个新的已连接 fd;原来的监听 fd 继续负责接收新连接。

非阻塞场景下我会使用 accept4(..., SOCK_NONBLOCK | SOCK_CLOEXEC),循环 accept 直到 EAGAIN / EWOULDBLOCK,处理 EINTR 和错误;连接 fd 要设置读写超时或由事件循环管理,限制连接数和输入大小。客户端则通常是 socket → connect → send/recv。

容易被问的点:listen 不负责读取业务数据;accept 不会复用监听 fd;一个客户端连接对应一个新的连接 fd。绑定 0.0.0.0 前要考虑暴露面、认证和访问控制。


17. 讲讲 I/O 多路复用

参考回答:

I/O 多路复用让一个线程等待多个 fd 的就绪事件,避免每个连接一个线程。select 和 poll 每次都要把集合交给内核并线性检查;epoll 通过内核维护的注册集合和就绪事件机制,epoll_wait 重点返回就绪 fd,适合大量连接。

Linux 服务通常是:把监听 fd 和连接 fd 设为非阻塞,用 epoll_create1 创建实例,epoll_ctl 注册 / 修改 / 删除事件,epoll_wait 获取事件后分发到 accept、读、写处理函数。ET(边缘触发)下必须循环读到 EAGAIN,写入也要处理部分写,只在发送缓冲区有数据时关注 EPOLLOUT;LT(水平触发)更容易写对。工作线程模型还要考虑 EPOLLONESHOT、连接生命周期和惊群。

epoll 是就绪通知,不是自动完成 I/O:收到可读事件后仍要自己 read/recv,不能假设一次读完;收到事件也要处理 ERR/HUP。这套模型和我熟悉的 Linux Socket、并发及性能问题是相通的。

**追问对比:**epoll 解决的是“等待很多 fd 的事件”,不是把所有业务逻辑变成异步;io_uring 则更接近提交 / 完成队列式的异步 I/O,模型不同。


18. 多 Agent 协作为什么采用确定性路由,不让 LLM 决定

参考回答:

我的项目把战斗决策、卡组构建和路线规划拆成不同职责的 Agent / 工作流节点,每个节点有自己的上下文、工具和输入输出,通过结构化消息传递状态。确定性路由不是说系统里没有 LLM,而是把“当前状态允许走哪条控制流”交给状态机和规则,把具体分析、策略建议交给 LLM。

主要原因有四点:

  1. 安全和权限:路由决定可调用哪些工具、会不会产生外部副作用,不能完全交给自然语言输出;用 allowlist 和状态校验可以避免越权调用。
  2. 可靠和可控:控制流需要满足状态不变量、超时、重试和幂等,规则比模型输出稳定;错误路由要能回退和降级。
  3. 测试和审计:确定性路由可以按状态和事件写单测,日志可回放,出现错误时能定位是路由、工具还是模型决策。
  4. 成本和延迟:如果每一步都让大模型判断“现在该做什么”,会增加 token、延迟和不必要的不确定性;实时战斗路径尤其需要短链路。

更实际的方案是混合式:LLM 可以提出候选动作或规划意图,但输出必须是 schema 约束的 enum / JSON,再由策略层校验当前状态、权限和工具参数;不合法就拒绝、重试或走规则兜底。真正互不依赖的路线规划和复盘任务才适合并行。

我也会诚实说明项目形态:这里的“多 Agent”重点是职责、上下文和工具隔离,并不等于多个模型无条件互相对话。拆分的价值在于边界清晰、可观测、可测试,而不是为了凑一个名词。


手撕:线程安全的定长阻塞队列

**先讲思路:**一个 mutex 保护队列和关闭状态;not_full 负责生产者在满时等待,not_empty 负责消费者在空时等待。用 condition_variable::wait(lock, predicate) 处理虚假唤醒;状态改变后再通知。close() 是可选增强,用于让永久阻塞的生产者 / 消费者能够退出。

#include <condition_variable>
#include <cstddef>
#include <deque>
#include <mutex>
#include <stdexcept>
#include <utility>
 
template <class T>
class BoundedQueue {
public:
    explicit BoundedQueue(std::size_t capacity) : capacity_(capacity) {
        if (capacity_ == 0) {
            throw std::invalid_argument("capacity must be positive");
        }
    }
 
    // 队列满时阻塞;close 后不再接受新元素
    void push(T value) {
        std::unique_lock<std::mutex> lock(mutex_);
        not_full_.wait(lock, [this] {
            return closed_ || queue_.size() < capacity_;
        });
 
        if (closed_) {
            throw std::runtime_error("queue is closed");
        }
        queue_.push_back(std::move(value));
 
        lock.unlock();
        not_empty_.notify_one();
    }
 
    // 队列空时阻塞;返回 false 表示队列已关闭且已经取空
    bool pop(T& value) {
        std::unique_lock<std::mutex> lock(mutex_);
        not_empty_.wait(lock, [this] {
            return closed_ || !queue_.empty();
        });
 
        if (queue_.empty()) {
            return false;
        }
        value = std::move(queue_.front());
        queue_.pop_front();
 
        lock.unlock();
        not_full_.notify_one();
        return true;
    }
 
    void close() {
        {
            std::lock_guard<std::mutex> lock(mutex_);
            closed_ = true;
        }
        not_empty_.notify_all();
        not_full_.notify_all();
    }
 
    std::size_t size() const {
        std::lock_guard<std::mutex> lock(mutex_);
        return queue_.size();
    }
 
private:
    const std::size_t capacity_;
    mutable std::mutex mutex_;
    std::condition_variable not_empty_;
    std::condition_variable not_full_;
    std::deque<T> queue_;
    bool closed_ = false;
};

写完主动说明:

  • push / pop 的队列操作是 O(1),额外空间 O(capacity)。
  • wait 会在睡眠时释放 mutex,醒来后重新竞争;必须使用 predicate / while,不能只判断一次。
  • notify_one 足够唤醒一个对端;关闭时用 notify_all 唤醒所有等待者。
  • close 不是题目硬性要求,但可以解决析构或服务退出时的永久阻塞;如果题目只要求基本版,可以先去掉 closed_。
  • 生产环境还可以增加 try_push、超时 push/pop、取消令牌和统计等待时间。

面试前最后自查

  • 能说清实际指标的系统 API / 数据结构、采样周期和百分比口径,不把推测说成项目事实。

  • 能解释 TLS 副本由哪个线程更新,以及快照过期 / 配置变更如何处理。

  • 能说清项目实际使用的跨进程同步对象、超时返回后的降级路径和 ABI 兼容做法。

  • 能不看代码写出有 predicate 的 bounded blocking queue。

  • 能从 listen 讲到 accept4、非阻塞、EAGAIN,再讲到 epoll 的 ET / LT。

  • 遇到 Shell 执行题,先讲注入、权限、超时、fd、资源限制、审计,再讲命令本身。