协程服务器常问问题
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/L3Cache是CPU的硬件存储层次) - 并发和并行有什么区别(多个任务切换执行/多个任务同时执行)
- 阻塞
IO和非阻塞IO区别(等待缓冲时是阻塞还是返回错误) - 动态链接和静态链接的区别(运行时链接库/编译时复制库)
epoll的实现(将需要监听的fd保存在内核中的一个红黑树中,监听到事件时返回对应的fd)epoll与select的区别(内核红黑树、异步回调、只返回有事件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_REUSEPORT的socket,那么内核会在这组socket之间选一个来接收这个连接或数据包。
reuseaddr了解过吗?reuseaddr放宽bind()时对本地地址的校验规则,允许更快重用本地地址/端口- 最常见的使用场景是:服务端重启。
- 内核在什么情况会发送
RST(内核认为 “这个报文不属于当前连接” 或者 “本端要直接中止连接” 时) - 怎么判断是错误的连接(序列号和确认号)
HTTPS完整的握手流程(RSA握手)- 具体有哪几种算法(
RSA和ECDHE) HTTP Session复用HTTPS的Session 复用其实是TLS会话恢复。常见两种:Session ID和Session Ticket。Session 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提供APIu_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]; - 监听
socket的fd; - 已建立连接的
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_waitepoll_wait任务什么时候加入到协程任务队列?(不用加入到协程任务队列,线程运行的函数中存在一个判断,如果能从协程任务队列中找到协程任务,就取出来执行,否则执行空闲协程,空闲协程中epoll_wait())
参考来源
- 作者:_陈顺
- 链接:https://www.nowcoder.com/discuss/620388024718774272?sourceSSR=users
- 来源:牛客网
