面灵AI→

百度网络研发一面 epoll 与 TCP 连环追问

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

《面试题目》

  1. reactor 框架是什么?
  2. 有了 epoll,为什么还要封装 reactor?
  3. epoll 与 poll 的区别在哪里?
  4. 两个进程用 socket 通信怎么做?
  5. RDMA 远程读怎么实现?
  6. QUIC 相比于 HTTP 的优势?
  7. UDP、TCP 头分别是什么?
  8. TCP 状态机?
  9. TIME_WAIT 做什么?
  10. 四次挥手可以三次吗?
  11. TLS 握手过程?
  12. 系统调用?
  13. 进程与线程之间通信的方式?
  14. 死锁条件?
  15. 智能指针相关:weak_ptr 为什么能知道 shared_ptr 是否析构?
  16. strlen 和 sizeof 的区别?
  17. volatile 的作用?
  18. 虚函数?
  19. malloc 与 mmap?
  20. 算法:合并 k 个有序链表。

《参考解析》

有了 epoll 为什么还要封装 Reactor:epoll 只解决「哪些 fd 就绪」这一件事,剩下的事它一概不管:就绪之后该读多少、读到的半包怎么拼、业务处理耗时会不会阻塞整个事件循环、连接的生命周期谁管理、定时任务和超时连接怎么回收、多线程下 Reactor 怎么分流。Reactor 就是把「事件分发 + 回调注册 + 连接/缓冲区管理 + 线程模型」这套套路封装成框架,让业务只写 handler。常见形态是主从 Reactor:mainReactor 只 accept,subReactor 各自跑一个 epoll 事件循环处理读写,每个连接绑定固定线程避免共享缓冲区加锁;CPU 密集的业务再甩给独立线程池,别阻塞事件循环。

epoll 与 poll 的区别:poll 每次调用都要把整个 fd 数组从用户态拷贝到内核态,返回后还要线性扫描全部 fd 找就绪项,复杂度 O(n);epoll 用 epoll_ctl 一次性把 fd 注册进内核的红黑树,epoll_wait 只返回就绪链表里的项,复杂度 O(就绪数),且无需重复拷贝。epoll 还有两种触发模式:水平触发(LT)只要缓冲区有数据就一直通知,编程简单;边缘触发(ET)只在状态变化时通知一次,必须一次读到 EAGAIN,且 fd 必须设为非阻塞,否则会漏事件或阻塞。另外 epoll 的 fd 上限受 /proc/sys/fs/epoll/max_user_watches 和文件描述符上限约束,不是一个固定常数。

TIME_WAIT 与挥手能否三次:TIME_WAIT 存在两个理由:一是保证最后一个 ACK 能到达对端,如果这个 ACK 丢了,对端会重发 FIN,主动关闭方还在 TIME_WAIT 就能重发 ACK,否则会回 RST 让对端异常;二是让本次连接的旧报文在网络中彻底消亡,避免被同一四元组的新连接误收。等待时长是 2MSL(Linux 上通常固定 60 秒)。四次挥手能否变三次,取决于对端是否还有数据要发:如果被动方收到 FIN 后没有待发数据,可以把 ACK 和 FIN 合并成一次发送,实际就是三次;但 TCP 是全双工的,被动方可能还需要继续发数据,所以协议上必须允许四次。另外延迟确认、捎带确认也会影响实际报文数量,面试时要说明「合并是被允许的优化,不是协议规定」。

weak_ptr 为什么知道 shared_ptr 是否析构:shared_ptr 内部有两个指针:一个指向对象,一个指向控制块,控制块里存强引用计数、弱引用计数和原始指针。weak_ptr 只增加弱引用计数、不增加强引用计数,所以不影响对象生命周期;对象析构时强计数归零、对象被 delete,但控制块本身要等弱计数也归零才释放。weak_ptr::expired() 或 lock() 就是去读控制块里的强计数是否为 0,因此能准确知道对象是否还活着。使用要点:多线程下 expired() 再 lock() 之间有竞态,正确写法是直接 auto sp = wp.lock(); if (sp) {...}。

malloc 与 mmap:malloc 是 C 库函数,mmap 是系统调用,前者最终可能通过 brk 或 mmap 向内核要内存。小块内存(小于 MMAP_THRESHOLD,默认 128KB)走 brk 堆区并由 malloc 自己维护空闲链表,释放后不还给内核、留着复用,所以会出现「free 了但 RSS 不降」;大块内存直接 mmap 一块匿名映射,free 时 munmap 还给内核。多线程下 glibc 会为每个线程分配独立的 arena(每 arena 用一把锁),线程数多时 arena 和锁竞争会带来内存与性能问题,高并发服务常换成 tcmalloc/jemalloc。回答时把「库函数 vs 系统调用」「分配阈值」「内存归还行为」三点讲出来就完整了。

合并 k 个有序链表:三种解法按场景选。一是顺序两两合并,时间 O(k²n),k 小时够用;二是分治两两合并,时间 O(kn log k),空间 O(log k),实现简单且稳定;三是小顶堆(优先队列)维护每个链表当前头节点,每次取最小并推进该链表,时间 O(kn log k),空间 O(k),还能扩展到「合并 k 个有序数据流」的场景。C++ 里用 priority_queue 配自定义比较器,注意堆里存节点指针和所属链表索引;手写时要处理空链表、比较器写成 greater 语义(std::greater 对指针不是想要的效果,需要自定义 operator())。