1️⃣ 面经一 (腾讯)

  • 什么是协程(用户态线程,用户栈,用户态切换)
  • 协程和线程相比有什么优势(轻量级、切换快、开销小,用户管理,配合非阻塞 IO 更好地实现异步并发)
  • 更好地实现异步并发是怎么理解的(IO 操作时检测到需要等待缓冲时切换协程)
  • 线程也可以在等待时切换,协程的优势是什么(用户自己操作)
  • 轻量级怎么理解(线程的栈是 MB 级别的,协程的栈是 KB 级别的,线程在内核中切换,协程在用户态中切换)
  • 怎么设置非阻塞(fctnl
  • 返回 EAGAIN 是什么意思(缓冲区没准备好)
  • 互斥锁怎么实现的(访问锁时如果锁被占用就阻塞,加入阻塞队列,锁被释放时唤醒阻塞队列的中的线程进入就绪队列)
  • http协议怎么解析的(引入了其他项目的解析器)
  • http 是什么格式(消息行 消息头 消息体)
  • N:M 的协程调度器是什么(N线程处理M协程)
  • 如何调度(先来先服务,线程依次去协程队列取任务执行,如果没有任务就执行空闲协程,陷入 epoll_wait,有任务时再唤醒空闲线程去执行任务)
  • 如果所有线程都不空闲,其他协程是不是就不调度了 (不会立刻调度,要等当前占着线程的协程让出执行权)
  • 其他协程就不调度了吗
    • 当前框架是协作式 / 非抢占式调度。调度器只能在协程主动让出时切换,不能强行把正在运行的协程“抢下来”。
    • 当前框架更适合 IO 密集型,不适合 CPU 密集型
    • Go 在这方面做得更完善。Go 从 1.14 开始支持 goroutine 的异步抢占,但那需要安全点、信号和运行时配合,复杂度比我当前项目高很多
  • 堆排序时间复杂度(O(nlogn),建堆后 n 次下滤)
  • 快速排序时间复杂度(平均 O(nlogn),最坏 O(n2)

2️⃣ 面经二(腾讯)

  • 面向对象的三大特性(封装、继承、多态)
  • new/malloc 的区别(性质、原理、大小、失败)
  • 虚函数表放在哪(常量区)
  • 基类和子类的构造和析构顺序(构造先基类后子类,析构相反)
  • 讲一下 map 的实现?具体细节?(红黑树及其性质)
  • move 的实现和原理? (static_cast<std::remove_ref<T> &&>())
  • 如果不是指针呢?比如 move 的是结构体对象,有指针也有 int?(基本类型拷贝,对象进行移动)
  • 进程空间布局(代码段、data 段、bss 段、堆、文件映射区、栈)
  • 段是怎么理解的(内存段,存储代码或者数据)
  • 页表是管理什么的(虚拟内存和物理内存映射关系)
  • 进程线程协程的区别(从资源和切换上答)
  • linux 下进程和线程的调度有区别吗?(没有区别,都用 task_struct 表示,看成一个任务进行调度)
  • 栈和堆有什么区别?(管理方式、空间大小、分配释放、访问效率、生命周期)
  • 栈为什么比堆效率高?
    • 栈只需移动栈帧,堆需要找合适的内存空间
    • 栈帧中的局部变量通常在一段连续区域里,函数执行时访问集中,CPU Cache 容易命中;堆对象经常比较分散,指针跳转多,cache miss 概率更高
    • 堆容易产生额外间接访问
  • 栈内存和堆内存都要找到内存地址,怎么对比速度?
    • 地址计算成本不同
      • 栈变量的地址通常是已知规则的,比如 “SP/BP + 偏移量”,编译器很容易生成指令,定位非常直接。
      • 堆对象的地址通常先要通过分配器申请得到,再通过指针间接访问。对象位置更随机,寻址链路可能更长。
    • 缓存命中率不同
      • 栈上的数据通常连续,当前函数频繁访问,cache 更容易命中
      • 堆上的对象分布离散,尤其链表、树这类结构,容易 cache miss
  • CPU 多级缓存设置的目的是什么?(局部性原理,把近期或频繁使用的数据放到更靠近 CPU、速度更快的存储层里)
  • 那栈和堆,是不是和 CPU 多级缓存是类似的?(栈和堆是进程虚拟地址空间中的内存组织方式; L1/L2/L3 CacheCPU 的硬件存储层次)
  • 并发和并行有什么区别(多个任务切换执行/多个任务同时执行)
  • 阻塞 IO 和非阻塞 IO 区别(等待缓冲时是阻塞还是返回错误)
  • 动态链接和静态链接的区别(运行时链接库/编译时复制库)
  • epoll 的实现(将需要监听的 fd 保存在内核中的一个红黑树中,监听到事件时返回对应的 fd
  • epollselect 的区别(内核红黑树、异步回调、只返回有事件 fd
  • 边缘触发和水平触发(事件发生后只通知一次/只要有数据没读写完就再次通知)
  • 怎么判断事件有没有读写完(读写时会给定数据的真实长度,返回值是真正读写成功的数据长度)
  • 没读写完会产生什么错误码(EAGAIN
  • TCP 四次挥手
  • 客户端 timewait 过多怎么办
    • 产生原因:短连接太多,客户端经常主动关闭连接
    • 解决办法:
      • 减少短连接,替换成长连接;
      • 让服务端主动断开;
      • 扩大客户端可用端口资源 ip_local_port_range
      • 复用 timewait 对应的 socket,内核参数:net.ipv4.tcp_tw_reuse
      • 调整系统所能容纳的 timewait 数量,内核参数:net.ipv4.tcp_max_tw_buckets
  • 一个数据包从网卡到应用层的收包过程
  • 这个过程中软中断是那一部分?
  • 应用层到内核缓冲区读数据会有软中断吗
  • 对这个过程中的 reuseport 有了解吗?
    • reuseport 指的是多个 socket 可以同时 bind 到同一个 IP:Port,数据包一路到传输层后,内核根据四元组找到目标 socket;如果目标端口对应的不是单个 socket,而是一组开启了 SO_REUSEPORTsocket,那么内核会在这组 socket 之间选一个来接收这个连接或数据包。
  • reuseaddr了解过吗?
    • reuseaddr 放宽 bind() 时对本地地址的校验规则,允许更快重用本地地址/端口
    • 最常见的使用场景是:服务端重启。
  • 内核在什么情况会发送 RST(内核认为 “这个报文不属于当前连接” 或者 “本端要直接中止连接” 时)
  • 怎么判断是错误的连接(序列号和确认号)
  • HTTPS 完整的握手流程(RSA 握手)
  • 具体有哪几种算法(RSAECDHE
  • HTTP Session 复用
    • HTTPSSession 复用 其实是 TLS 会话恢复。常见两种:Session IDSession TicketSession ID 是服务端保存会话,客户端下次带 ID 来命中缓存恢复;Session Ticket 是服务端把会话状态加密后发给客户端保存,下次客户端带回来恢复,服务端更省状态,也更适合负载均衡。
  • HTTP 长连接(发送一次请求和响应后不断开)
  • QUIC 了解过吗?(解决TCP队头阻塞、更快连接,端口迁移)
  • 为什么 QUIC 连接更快?
  • 还了解 QUIC 的其他的知识吗?
  • 有性能测试吗,有对比其他服务器比如 Nginx 吗?
  • 服务器接收和响应的数据流是怎么样的,从 accept 开始(accept 是单独一个协程,有新连接就建一个新协程)
  • 为什么用协程(切换开销小,配合非阻塞实现异步)
  • 定时器用什么实现(set
  • 支持主线程调度任务减少开销是什么意思(调度器线程执行完调度任务后也用来执行任务)

3️⃣ 面经三(腾讯)

  • 项目是高性能服务器,性能有多高?
  • 所以说这个服务器可以替换 Nginx 吗?很多面试者都说自己的服务器测试结果比 Nginx 好,那为什么还是 Nginx 被使用的最多,对此有什么看法?
    • 很多自研服务器压测赢 Nginx,本质上是因为测试路径更短、功能更少、场景更理想;而 Nginx 做的是更通用、更稳、更可运维的工程权衡。
    • 我不会说它可以替代 Nginx,因为 Nginx 的优势不只是吞吐,具有 生产级入口组件 的完整能力,包括反向代理、负载均衡、缓存、限流、日志、协议支持、运维成熟度和长期稳定性。
      那给这些服务器加上对应的功能是不是就可以替代了?(理论上可以,工程上很难)
  • 高性能需要从哪些方面入手,才能写出真正的高性能框架,自由发挥
    • 先定义 “高性能” 到底是什么
    • 架构上选对模型 Reactor/Proactor
    • 让一次请求的 IO 成本尽量低(减少 syscall/减少数据拷贝)
    • 把并发需求和内存开销压下去
    • 通过数据来定位瓶颈
  • 了解 Nginx 为什么性能高吗
    • 事件驱动架构:与传统的多进程/多线程模型不同,Nginx 使用单线程通过事件循环处理大量并发连接。
    • 非阻塞 I/O 模型:当工作进程遇到 I/O 操作(如磁盘读写或网络通信)时,不会阻塞等待而是立即返回并处理其他请求
    • Master-Worker 进程模型
      • Master 进程:负责管理工作进程,不处理实际请求
      • Worker 进程:实际处理请求,彼此独立
    • Nginx 采用高度模块化的设计,核心只包含最基本的功能,其他功能通过模块实现
  • 设计高性能服务器,代码本身就要高效,应该如何写出高效的代码呢,可以举例
    • 减少不必要的内存分配和拷贝
    • 减少锁竞争,尽量让代码 “无共享”
    • 核心代码要短,少分支、少函数层层绕、少系统调用
  • 有没有分析项目中代码的瓶颈在哪,通过压测分析了吗
  • 为什么想做后台开发,对其他的技术方向有了解吗,有考虑现在比较流行的AI,大模型方向吗,后台开发其实也是写重复的逻辑,未来肯定会被AI替代,怎么看待这件事
    • 第一,我对后台这种偏系统、偏工程的工作更感兴趣。相比单纯做界面或者偏展示层的东西,我更喜欢去处理高并发、性能优化、网络通信、存储、稳定性这些问题。因为后台开发不仅是把功能写出来,更重要的是在真实场景下把服务做稳、做快、做可扩展,我觉得这类问题更有挑战性,也更适合我的兴趣。
    • 我觉得后台开发的成长路径很扎实。它会涉及操作系统、网络、数据库、缓存、消息队列、分布式这些基础能力,这些东西比较通用,也能让我把计算机基础真正用起来。我自己平时也更愿意关注这类问题,比如服务端性能、协程调度、IO 模型、系统设计这些。
    • 我的看法是:会替代一部分重复劳动,但不会简单替代后台工程师。后台开发真正有价值的部分,不是机械地写代码,而是:
      • 需求抽象和系统建模
      • 架构权衡
      • 性能、稳定性、成本之间的平衡
      • 线上问题定位
      • 这些事情不是 “生成几段代码” 就能解决的。
  • 你觉得做后台开发需要具备哪些技术和知识,平常是怎么具备这些知识点的
    • 计算机基础和编码能力
    • 后台核心组件和工程知识
    • 系统设计和问题排查能力

4️⃣ 面经四(阿里)

  • 协程库是基于什么实现的?是 C++ 自己的协程库吗?(Linux 提供 API u_context)’

  • C++ 协程的区别?

    • 性质:
      • ucontext:用户态上下文切换 API,本质是手工切线程上下文;
      • coroutine:它是语言特性,不是一个现成调度器。
    • 有/无栈
      • ucontext:每个上下文通常都要有自己独立的栈。
      • coroutine:本质是函数被改写成状态机,挂起点只有编译器知道。它不会保存整个调用栈。
  • 有栈协程还是无栈协程?(有栈,独立栈,分配在堆上)

  • 为什么要用协程?(切换快、用户管理、异步并发)

  • 协程库用在什么场景?(讲协程调度器的实现)

  • 协程的 resume 用在什么场景?(IO 就绪)

  • 怎么判断要阻塞了(fd 设置为 非阻塞、调用同步系统调用 read、通过返回值判断)

  • 异步 IO 还是同步 IO?(同步非阻塞 IO,等待缓冲不阻塞,数据从内核态拷贝到用户态仍然阻塞)

  • 是一个协程绑定一个 fd 不断地进行 IO 操作吗?

    • 不是一个协程永久绑定一个 fd

      当某个协程对某个 fd 做读/写,结果发现现在不能继续(EAGAIN)时,就把 “当前协程” 临时挂到这个 fd 的某个事件上(READ``/WRITE),然后 yield();等 epoll 发现这个 fd 就绪,再把这个协程重新调度回来。

  • 那这个逻辑和用 lambda 操作捕获 fd 和相关参数实现有什么区别?

    • lambda 自己保存状态、自己组织下一步
    • 协程: 框架保存执行位置和局部状态,事件到了直接续跑
  • 不用协程,用自己写的一个结构体去保存上下文,进行切换,与使用协程有什么区别?

    • 保存的 “上下文” 层次不一样
      • 协程可以 隐式保存执行位置和局部变量,在事件到来后从挂起点继续执行;

      • 结构体方案需要程序员 显式维护执行阶段和中间状态;

        如果这个 “结构体上下文” 里还包含独立栈、寄存器现场,并支持上下文切换,那本质上就是在自己实现协程/ fiber

  • 手动保存的上下文和协程保存的上下文有什么区别?

    手动保存的上下文,保存的是“显式挑出来的状态”;协程保存的上下文,保存的是 “让代码能从挂起点继续执行所需的运行现场”。

  • 协程上下文主要是保存什么?

    • 协程自己的独立栈;
    • 栈指针:恢复时知道从哪继续用栈;
    • 程序计数器 PC:恢复后下一条执行哪条指令;
    • 寄存器内容:通用寄存器、部分调用约定相关寄存器;
    • 线程上下文;
  • ucontext 需要用到哪些寄存器的值,每个寄存器是干什么的?

    • uc_link:当前上下文结束后切到哪个上下文
    • uc_stack:这个上下文使用的栈
    • uc_sigmask:这个上下文生效时屏蔽哪些信号
    • uc_mcontext:机器相关的寄存器现场,也就是最关键的 CPU 上下文。
  • 缓冲区一直没好,什么时候加入协程队列?

    • 等待 fd 就绪: 缓冲区没好时,当前协程不会进入 “可运行队列”,而是先挂在 fd 的事件槽里,等 fd 就绪后才重新加入调度队列
    • 设置超时定时器: 即使缓冲区一直没好,只要超时触发,协程也会被重新加入调度队列,然后恢复执行,接着发现是超时错误并返回。
  • epoll_wait 是在哪里调用,是单独的线程吗?每个空闲线程调用,(管道、事件、定时器通知)

  • 相当于是将 epoll_wait 封装成一个任务吗?为什么这样考虑?

    • 不是把 epoll_wait 封装成 “普通业务任务”,而是把它放进了 idle 协程;
    • 空闲线程阻塞在 epoll 上,不占 CPU;
  • 每个任务对应多个还是 1 个 fd?是

  • 如果整个线程池比较忙,会不会有延时的场景,就是连接数量超过线程数量的时候?(会有延时场景)

  • epoll_wait 的具体逻辑是怎么样的?(线程如果在协程队列中能获取到任务就执行,否则就执行 idle 协程,陷入 epoll_wait,等待唤醒)

  • epoll_wait 什么时候唤醒?(事件、定时器、新任务通知管道)

  • 想问的是陷入 epoll_wait 后如何切换?(不切换,就是因为没有任务,就要陷入 epoll_wait 中,而不要切换)

  • 所有线程都陷入 epoll_wait,那协程岂不是没用了?(说明没有任务要运行,有任务的时候线程就被唤醒了)

  • 只有一个线程,陷入 epoll_wait,不能切换,来了新连接,是不是无法处理了?

    • 唯一的线程进入 idle()
    • epoll_wait 里睡眠
    • 新连接到来
    • 监听 fd 变为可读
    • 内核唤醒这个线程,epoll_wait 返回
    • 框架把监听 fd 对应的协程/回调 schedule
    • 线程去执行 accept
  • epoll_wait 监听的是哪些 fd,只有管道吗?

    • 一个 IOManager 通常只有一个 m_epfd;
    • 管道读端 m_tickleFds[0];
    • 监听 socketfd;
    • 已建立连接的 socket fd;
    • 通过 addEvent(fd, READ/WRITE) 注册进去的 fd;
  • 为什么要用管道?(用于线程间的通信,通知有新任务)

  • fd 注册了事件也可以用来通知,为什么还要用管道?

    • 当前线程可能正睡在 epoll_wait, 如果不主动唤醒,它可能一直睡到 timeout;
    • 突然插入了一个更早到期的定时器,如果不叫醒它,它还会继续按原来的 5 秒睡,定时器就晚了,用管道把它唤醒,让它重新计算超时时间;
    • 如果线程都睡在 epoll_wait,通过 stopping() 退出,这时要通过管道统一唤醒;
  • http 服务器应该主要是 socket 套接字,还用到其他的 fd 了吗?

    • 监听 socket fd;
    • 已连接 socket fd
    • 管道 fd
  • 有几个 epoll fd,哪些 fd 要注册到 epoll fd 上?

    • 一个 IOManager 对应 1 个 epoll fd;
    • 管道读端 m_tickleFds[0]
    • 需要等待事件的业务 fd
  • 协程池有多少个 epoll_wait 任务?空闲线程都 epoll_wait

  • epoll_wait 任务什么时候加入到协程任务队列?(不用加入到协程任务队列,线程运行的函数中存在一个判断,如果能从协程任务队列中找到协程任务,就取出来执行,否则执行空闲协程,空闲协程中 epoll_wait()

参考来源