
内存与文件的统一写状态机1. 它是什么原理是什么输出队列中的元素统一表示为using OutputSegment std::variantMemorySegment, FileSegment;MemorySegment保存用户态缓冲区、offset、remaining。FileSegment保存UniqueFd、文件偏移、剩余长度。队列只允许队尾追加、队首发送。状态机的核心规则是只处理队首后面的 segment 不能越过队首。内存段调用send文件段调用sendfile。成功发送n字节就推进 offset减少 remaining。EINTR原地重试同一个 segment。EAGAIN保留当前 offset 和 remaining打开EPOLLOUT等待下一次可写事件。remaining 变成 0segment 退役、出队、归还内存或文件 FD 资源。队列为空关闭EPOLLOUT。因此Memory → File → Memory仍然严格按顺序输出。需要注意项目中“统一”的是队列模型、顺序、状态推进、错误处理和生命周期发送系统调用本身仍然不同。2. 项目里怎么用为什么用静态文件请求的路径是FileHandler → openat2 打开文件 → fstat 获取固定长度 → HttpResponse::SetFile → ResponseBatch(Header File) → Reservation 统一准入 → OutputQueue → send / sendfile动态响应的 body 会构造成MemorySegment静态文件则构造成FileSegment。使用它主要有四个原因保证 HTTP pipeline 顺序响应头、文件正文、下一个动态响应不能乱序。减少内存占用文件内容不整体读入用户态缓冲直接使用sendfile。统一处理慢客户端文件和内存都能通过短写、EAGAIN、EPOLLOUT续传。统一资源回收UniqueFd在文件发送完成、连接关闭或异常时自动关闭 FD。项目还特意把两个概念分开计数flow_backlog_bytes内存和文件都计入用于 HIGH/LOW 背压。accounted_output_payload_bytes只计算用户态内存用于内存预算。文件虽然不占用户态 payload但仍然占文件 FD 和队列 segment所以还有单连接 FD 数和 segment 数上限。pipeline 请求与背压解除后的异步续处理1. 它是什么原理是什么它是什么原理是什么这里包含三个概念HTTP pipeline流水线请求客户端在同一条 HTTP/1.1 连接上不必等前一个响应回来就连续发送多个请求。服务器需要正确区分请求边界并按请求顺序返回响应。背压客户端读取响应较慢服务器输出队列不断积压就暂时减少上游输入和处理避免继续产生大量响应。解除背压后的异步续处理输出积压降下来后把“继续处理剩余输入”投递到事件循环稍后执行。注意Keep-Alive 表示连接可以复用pipeline 进一步允许前一个响应还没收到就发送后续请求。它也不意味着服务器必须并行处理这些请求。最关键的是区分两个位置内核 socket 接收缓冲区 → 用户态 _in_buffer → HTTP 解析与业务处理关闭EPOLLIN只能停止继续从 socket 读取已经进入_in_buffer的请求仍然存在。所以暂停时必须同时停止解析恢复时也必须考虑这部分输入。2. 我的项目里怎么用为什么用我的服务器支持 HTTP pipelineOnmessage()会循环解析输入缓冲区里的完整请求。为了避免慢客户端导致响应持续积压我用输出队列的待发送字节数控制背压同时控制 socket 读取和 HTTP 解析。积压下降后恢复读事件并异步续处理已经缓冲的请求。具体流程如下。① 正常情况下连续处理完整请求。HttpServer::Onmessage()在while (buf-ReadAbleSize())中解析请求只有请求完整后才进入路由、生成响应再重置上下文处理下一个请求。响应通过ResponseBatch提交到统一 FIFO 输出队列保持响应顺序。② 输出积压达到 HIGH同时暂停读取和解析。项目当前设置条件行为积压达到或超过4 MiB进入背压关闭读事件停止继续解析已处于背压积压仍大于1 MiB保持暂停积压降到1 MiB 或以下解除背压恢复读取安排输入续处理这里统计的是_flow_backlog_bytes即输出队列里所有 segment 的剩余发送量文件待发送部分也计入。暂停时设置_backpressured true关闭读事件HTTP 解析循环通过CanProcessInput()检查这个状态在当前响应触发 HIGH 后停止处理后续请求。③ 写出数据后检查是否可以恢复。HandleWrite()随实际发送量扣减积压随后调用ResumeReadInLoop()。满足 LOW 条件后清除背压状态。在没有输入越限的情况下恢复读事件。如果输入缓冲区还有数据通过QueueInLoop()安排续处理任务。举个例子一次读入请求 A、B ↓ 处理 A产生大响应输出积压达到 HIGH ↓ 停止解析B 留在用户态缓冲区 ↓ 客户端逐渐读取 A 的响应输出积压下降到 LOW ↓ 恢复读事件并投递续处理任务 ↓ 任务执行继续解析 B这里“异步”指延后到同一个 owner EventLoop 的任务阶段执行不是另开线程。如果继续产生大响应也可以再次进入背压。为什么这样做既控制慢客户端造成的积压又保证已经收到的请求能够继续推进同时避免写回调直接嵌套调用 HTTP 解析和业务处理。EventLoop 与连接状态的线程归属1. 它是什么原理是什么EventLoop本质是一个事件分发循环用epoll_wait等待 socket、eventfd、timerfd的事件。Poller返回活跃的Channel。EventLoop 在所属线程中依次调用 Channel 的读、写、关闭、错误回调。再执行跨线程投递到任务队列中的任务。项目中的主循环是epoll_wait - Channel::HandEvent() - RunAllTask()每个 EventLoop 在创建时记录线程 ID并通过AssertInLoop()强制要求Channel/Poller/Timer/Connection状态只能由该线程操作。跨线程任务通过互斥锁保护任务队列eventfd唤醒阻塞中的epoll_waitowner 线程取出任务并执行。因此它类似一个“单线程 Actor”外部线程可以发消息但不能直接修改 Actor 内部状态。2. 项目里怎么用为什么用连接如何分配线程TcpServer有一个 BaseLoop负责监听 socket接收新连接管理连接表处理全局停止流程。同时可以创建多个 Worker EventLoop。新连接到来时通过线程池轮询选择一个 workerEventLoop *owner_loop _pool.NextLoop(); conn.reset(new Connection(owner_loop, ...));所以一个 Connection 从创建开始就固定绑定一个_loopEventLoop *_loop;连接的以下状态全部归 owner loop 管理_statu_channel_socekt_in_buffer_output_queue定时器背压状态请求 deadline关闭状态跨线程如何操作连接例如业务线程调用conn-Send(...) conn-Shutdown() conn-EnableInactiveRelease(...)这些接口不会直接改连接而是进入 [DispatchToOwner() (line 2343)](source/server.hpp:2343)如果当前已经是 owner 线程直接执行如果是其他线程封装成任务并QueueInLoop()owner loop 再执行真正的SendInLoop()、ShutdownInLoop()等逻辑。Send()还会先复制数据保证调用方随后修改或释放原始缓冲区不会影响异步发送。但是SendResponseBatch()明确要求在 owner loop 调用因为它要保证整批响应在 owner loop 中一次性提交到 FIFO。关闭为什么也必须回 owner loop连接关闭不是简单close(fd)还要完成从 epoll 删除 Channel关闭 socket取消 timer清空输出队列归还内存和 fd 预算调用关闭回调通知 BaseLoop 从连接表删除。项目还特别把Release()延迟到下一轮任务执行_loop-QueueInLoop([self]() { self-ReleaseInLoop(); });因为当前epoll_wait返回的 active Channel 快照中可能还保存着这个连接的Channel*。如果当前回调里立即析构连接后面的事件分发可能访问悬空指针。为什么使用这种模型主要原因有三个减少锁竞争连接内部状态不需要每个字段都加锁状态机由 owner loop 串行推进。保证操作顺序读、写、关闭、超时、发送任务都在同一条队列中线性化输出队列天然保持 FIFO。避免生命周期竞态关闭、移除 epoll、关闭 fd、取消定时器都在同一个线程完成降低 use-after-free 和重复 teardown 风险。跨线程任务、shared_ptr 与连接所有权1. 它是什么原理是什么跨线程任务是把“不能在当前线程直接执行的操作”封装成闭包放进目标线程的任务队列由目标线程执行。项目中的EventLoop::QueueInLoop()用 mutex 保护任务队列把任务放入_tasks写eventfd唤醒可能阻塞在epoll_wait的线程目标 EventLoop 被唤醒后执行任务。RunInLoop()如果发现当前已经是 owner 线程就直接执行否则进入队列。shared_ptr使用引用计数管理对象生命周期。任务闭包按值捕获shared_ptr后即使外部的最后一个引用释放任务执行前对象也不会析构。这里要注意shared_ptr的引用计数管理是线程安全的Connection内部字段并不会因此自动线程安全项目通过 owner loop 保证连接状态只在一个线程修改weak_ptr只观察对象不延长生命周期适合 Timer 回调。连接所有权可以分成三层TcpServer的连接表持有连接的强引用Connection自己持有 socket、Channel、输入缓冲和输出队列排队任务临时持有shared_ptrConnection保证任务执行时对象有效。2. 项目里怎么用为什么这样用连接继承了std::enable_shared_from_thisConnection并定义了using PtrConnection std::shared_ptrConnection;连接建立后服务器先配置回调和资源预算再把连接放进连接表最后异步执行EstablishedInLoop()。Established()、Send()、Shutdown()、EnableInactiveRelease()等公共接口都可以被非 owner 线程调用但它们最终都会经过DispatchToOwner()PtrConnection self shared_from_this(); loop-QueueInLoop([self, command]() { command(self); });例如Send()有两个生命周期保护先把调用方传入的数据复制到std::string再把shared_ptrConnection和数据一起放进任务。所以调用方即使马上复用或释放原始缓冲区也不会影响异步发送。关闭连接时Release()不直接释放而是继续投递一个强持有连接的任务PtrConnection self shared_from_this(); _loop-QueueInLoop([self]() { self-ReleaseInLoop(); });ReleaseInLoop()在 owner loop 中完成设置DISCONNECTED关闭跨线程投递闸门从 Poller 移除 Channel关闭 socket取消 Timer清空输出队列归还内存和文件描述符配额执行 close callback。服务器停止时也不会直接从 BaseLoop 操作所有连接而是按 owner loop 分组给每个 Worker 投递强引用快照等待所有 Worker 完成清理后再停止线程。这样做的原因是避免多个线程同时修改同一个连接避免 Poller、Channel、Timer 被错误线程操作避免排队任务访问已经析构的对象避免停止过程中连接仍被 Worker 使用。延迟回收与幂等关闭1. 它是什么原理是什么“延迟回收”解决的是事件循环中的生命周期问题。项目的Poller::Poll()会先生成一个active Channel*快照然后统一执行回调最后才执行RunAllTask()。如果在某个回调里直接析构Connection后续快照中仍可能存在该连接的Channel*就会出现悬空指针和 use-after-free。项目采用两阶段关闭收到 EOF / 错误 / 超时 / Shutdown ↓ CONNECTED → DISCONNECTING ↓ 立即停止读事件、关闭新的命令投递 ↓ Release() 投递到 EventLoop 任务队列 ↓ 当前 active 事件分发结束 ↓ ReleaseInLoop() 真正 teardownRelease()会捕获shared_ptrConnection保证延迟任务执行前对象还活着ReleaseInLoop()才真正执行从 Poller 移除 Channel关闭 socket取消请求 deadline 和 inactive timer清空输出队列关闭 FileSegment 的 fd归还内存预算、文件 fd 计数和连接指标最后触发 close callback。“幂等关闭”指的是关闭请求可以来自多个路径但最终 teardown 只能执行一次。项目通过以下状态保证幂等CONNECTING → CONNECTED → DISCONNECTING → DISCONNECTEDShutdownInLoop()遇到DISCONNECTING或DISCONNECTED直接返回ReleaseInLoop()遇到DISCONNECTED也直接返回。因此 EOF、超时、写错误、业务主动关闭、服务停止同时发生时只有第一次关闭真正生效。2. 项目里怎么用为什么用连接级别HTTP 请求处理完且响应需要关闭时调用conn-Shutdown()。HTTP 错误响应先写入输出队列再调用Shutdown()。对端 EOF 走优雅关闭允许已经排队的响应继续发送。读错误、写错误、请求 deadline 超时走硬关闭直接丢弃未发送输出并延迟 teardown。inactive timer 和 request deadline 都只捕获weak_ptr避免 timer 反过来延长 Connection 生命周期。跨线程调用Shutdown()、Send()时先通过DispatchToOwner()投递到连接所属的 EventLoop。服务级别TcpServer::StopNow()也采用同样思想用生命周期状态Running → Stopping → Stopped抢占停止权停止 Acceptor禁止新连接进入按 owner EventLoop 对连接做快照把每个 worker 的连接 teardown 投递回各自线程用TeardownBarrier等待所有 worker 完成清空连接表停止 worker最后退出 BaseLoop。这样做的原因有三个Poller、TimerWheel、Channel、socket 都要求在 owner loop 操作当前事件快照中保存的是裸Channel*外部线程可能在连接关闭期间继续调用Send()或Shutdown()。所以项目的核心原则是状态变化先同步生效资源回收延迟到安全点所有真实 I/O 和 Poller 操作只在 owner loop 执行。如果不延迟释放只采用智能指针那么状态就不能及时修改时间轮的绝对超时时间是为了防止固定长度的时间轮返回回来之后判断不出新旧时间事件请求的绝对时间是由链接内变量控制请求行和header是一个阶段body一个阶段每个阶段有每个阶段超时时间这样防止一个链接不断发送短信息不到一个请求但始终占着链接。停机屏障与断连 / 超时 / 停机竞态1. 它是什么原理是什么这个项目里“停机屏障、断连、超时、停机竞态”可以统一理解为先阻止新的工作进入再让已有工作在正确的 owner loop 中完成清理最后回收线程和对象。这里的“停机屏障”不是 CPU 内存屏障而是一个 teardown barrier类似CountDownLatchCONNECTED - DISCONNECTING - DISCONNECTEDCONNECTED正常收发。DISCONNECTING停止接收新请求必要时继续冲刷已经提交的响应。DISCONNECTEDChannel、socket、timer、输出队列都已清理。连接的所有状态只由所属的 EventLoop 修改。跨线程调用Send()、Shutdown()时只是把任务投递到 owner loop。服务器停机时将生命周期改为Stopping不再接受新连接停止 Acceptor对ConnectionMap做强引用快照并按 owner loop 分组每个 worker 在自己的 loop 中执行所有连接的ReleaseInLoop()worker 完成后调用barrier-Done()BaseLoop 等待所有 worker 完成清空连接表停止并 Join worker设置为Stopped退出 BaseLoop。超时分两类空闲超时没有请求处理时启用I/O 活动会刷新请求超时Header 和 Body 分别使用从阶段开始计算的绝对 deadline收到零碎字节不会无限续命。时间轮负责大致调度真正关闭前还会用steady_clock再确认一次绝对时间避免提前关闭。2. 项目里怎么用为什么这样用连接断开有三条主要路径对端 EOF走优雅关闭停止读事件但把已经进入OutputQueue的响应发送完读写错误、请求 deadline 到期停止读并排队释放挂起输出直接丢弃HTTP 错误或业务主动关闭先生成错误响应并提交再调用Shutdown()。ShutdownInLoop()会清除请求 deadline把连接改为DISCONNECTING移除读事件禁止新输入如果有待发送数据打开EPOLLOUT输出队列清空后进入ReleaseInLoop()。关闭跨线程投递闸门状态改为DISCONNECTED从 Poller 移除 Channel关闭 socket取消 timer清空输出队列归还内存、文件 FD、segment 配额最后执行 close callback。这样设计的原因是epoll的 active 列表中保存的是Channel*不能在回调中直接析构对象每个连接的 Channel、Timer 和队列都必须由 owner loop 操作停机时必须确保 worker 不再访问 ConnectionStart()返回才是安全析构服务器的完成点外部即使还持有shared_ptrConnectionteardown 也会立即关闭 fd 和释放资源而不是等 shared_ptr 析构。DispatchToOwner()还使用_dispatch_mutex保护“检查闸门并入队”这一小段逻辑。teardown 先关闭闸门再允许 worker 被 Join避免外部线程向已经销毁的 EventLoop 投递任务。故障注入 / 真实 socket 回归 / ASan·UBSan / TSan1️⃣ 是什么 / 原理- 故障注入主动制造异常EINTR/EAGAIN/EMFILE、文件截短、慢消费者专测错误路径——网络服务器 80% 的 bug 在这里。- 真实 socket 测试用 loopback TCP/socketpair 走真实内核栈测试。EAGAIN 时机、partial write、RST、EPOLLRDHUP 顺序这些内核行为 mock 不出来。- 回归验证分层 gatemake step34同一套测试在多种构建下重复执行失败立刻定位到哪个安全层。- ASan编译插桩 影子内存 redzone/quarantine查越界、UAF、double-free、泄漏LSan。- UBSan查未定义行为有符号溢出、错误对齐、非法枚举转换等。- TSan基于 happens-before 关系追踪锁/原子操作报数据竞争。- 分工ASan/UBSan 查「内存/语义错误」单线程也能查TSan 查「并发访问模式」互补不互替。2️⃣ 项目里怎么用 / 为什么构建矩阵test/makefilemake step34 一键全跑Gate 配置 跑什么step34-plain 无插桩 全部功能回归step34-asan -O1 -g3 -fsanitizeaddress,undefinedhalt_on_error1 与 plain 完全相同的功能集step34-tsan -fsanitizethread 精选并发敏感子集LoopThread、owner-loop 释放、时间轮、StopNow、跨线程 Send/Shutdown、全局 CAS 预算、并发 sendfile、metricsstep34-release -O2 -DNDEBUG 真实起服务 curl 验证动态/静态响应防“只在 Debug 正确”典型故障注入全部真实存在- EINTR socketpair() 建的一对本地互连 fdreceiver 端等价于服务端读端peer 不写就等于“客户端不发包”recv() 阻塞。辅助线程向主线程发送信号打断 Socket::Recv() 把 errnoEINTR 翻译成 RecvStatus::Interrupted 返回测试再用 EXPECT 断言这个 status 和 error_codeEINTR 是否成立——成立就证明封装层正确识别了“被信号打断”且当普通错误处理、也没死循环重试。- EMFILE 先获取下一个可用fd用 setrlimit 压低到这个fd 强制 fd 耗尽监听套接字返回fd耗尽的状态然后验证无 busy loop1.2秒内重试的次数大约是五次不是无限进程实际消耗cpu时间不会是1.2秒、listenfd 不关、250ms 冷却后恢复解除fd限制后能正常获取fd。- 文件中途截短传输中 ftruncate验证 sendfile 提前 EOF 判硬错关闭、且尾部排队的动态数据绝不越过文件错发线上行为字节/EOF由 peer 验证服务端内部状态队列/FD/不变量由测试以进程内白盒方式直接验证不依赖客户端“知道”什么。- 慢消费者/背压调小 SO_SNDBUF 对端不读 → 强制 EAGAIN、partial write触发 HIGH/LOW 水位backpressure_test.cc:129。ASan内存错误越界、Double-Free、栈/全局/堆溢出、内存泄漏ASan 是怎么发现越界和 UAF 的核心是影子内存每 8 字节应用内存对应 1 字节影子字节映射公式为shadow(A) ShadowBase (A 3)。影子字节取值含义0 表示这 8 字节全部可访问1~7 表示前 N 字节有效用于处理非 8 对齐的尾部负值表示中毒。编译器为每一次 load/store 插入形如__asan_load4(addr)的调用运行时先查影子字节发现中毒立即报错free 之后内存不会立即被复用进入中毒状态TSan 判定 data race 的条件是什么四个条件同时成立才报① 同一内存位置② 来自不同线程③ 至少一个是写④ 这两次访问之间没有 happens-before 关系。注意第四条——结果碰巧对了也照样报因为它看的是逻辑顺序而非实际值。sqglobe.com1Q2什么是 happens-before哪些操作会建立 HB 边HB 是并发操作之间的偏序关系由同步原语建立。TSan 能识别的 HB 边包括mutex 的 unlock→lock、条件变量的 wait/notify、线程 create/join、正确的 acquire/release 原子操作含 fence。UBSan 检测哪些 UB典型包括有符号整数溢出INT_MAX 1、越界移位1 31对 int 是 UB、空指针解引用通过null检查项、未对齐访问、数组下标越界静态可确定时、INT_MIN / -1、浮点转换溢出、无效枚举值等。llvm.org1Q2UBSan 和 ASan 的本质区别UBSan 处理的是编译器在生成代码时假设永远不发生的 UB检查逻辑在编译期就能确定并插入条件判断ASan 处理的是运行时才能确定的内存访问合法性需要影子内存这种运行时数据结构支撑。所以 UBSan 开销极小而 ASan 开销大。