Posted in

Go net/http.Server.listenLoop()到底运行在哪一层?抓包+strace+runtime.LockOSThread三重锁定答案

第一章:Go net/http.Server.listenLoop()的定位本质

listenLoop()net/http.Server 启动后首个进入阻塞监听状态的核心协程,它不负责请求处理或路由分发,而是专职于网络连接的接收与初步封装。其本质是 连接生命周期的守门人——在底层 net.Listener.Accept() 返回新连接后,立即启动一个独立 goroutine 执行 srv.ServeConn()c.serve(connCtx),从而将连接移交至工作协程池,自身则立刻回归 Accept 循环,确保监听队列永不积压。

该函数位于 src/net/http/server.go,典型调用链为:
server.ListenAndServe()server.Serve(l net.Listener)server.listenLoop()

关键行为包括:

  • 持续调用 l.Accept() 获取 net.Conn
  • 对每个新连接执行 srv.trackListener(c, true) 进行连接数统计
  • 在启用 Server.Handler 时,派发至 c.serve(connCtx);若使用 Server.ServeConn(),则直接调用该方法

以下是最简复现其核心逻辑的示意代码(非生产使用):

// 模拟 listenLoop 的核心循环逻辑
func simulateListenLoop(srv *http.Server, l net.Listener) {
    for {
        rw, err := l.Accept() // 阻塞等待新连接
        if err != nil {
            if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
                continue // 临时错误,重试
            }
            break // 非临时错误,退出循环
        }
        // 启动新 goroutine 处理连接,避免阻塞 Accept
        go srv.handleConn(rw)
    }
}

listenLoop() 不参与 HTTP 协议解析、中间件执行或响应写入,其存在意义在于解耦「连接接入」与「连接处理」两个关注点。这种设计使 Go HTTP 服务器天然具备高并发连接接纳能力,同时通过 goroutine 轻量级调度实现横向扩展。

特性 表现
所属层级 网络 I/O 层(OSI 第4/5层)
并发模型 单 goroutine 循环 + 每连接一个 goroutine
错误恢复机制 仅对 Temporary() 错误重试;其他错误(如 closed network connection)终止循环
Serve() 关系 Serve() 是其宿主方法,listenLoop() 是其内部无限循环主体

第二章:网络协议栈视角下的listenLoop分层剖析

2.1 OSI模型与TCP/IP栈中HTTP服务器的典型驻留层定位

HTTP服务器并非孤立运行,其网络行为严格受协议栈分层约束。

协议栈映射关系

OSI 层 TCP/IP 层 HTTP 相关组件
应用层 应用层 nginx, Apache 进程
表示层/会话层 由应用层统一处理
传输层 传输层 TCP(端口 80/443)
网络层 网际层 IP 路由与寻址

关键实现逻辑

// Linux socket 绑定示例:HTTP服务器监听本质
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in addr = {.sin_family = AF_INET,
                           .sin_port   = htons(80),
                           .sin_addr.s_addr = INADDR_ANY};
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)); // 仅在传输层之上建立端点

该调用不涉及IP地址构造或帧封装,说明HTTP服务逻辑完全驻留于应用层,依赖下层(TCP)提供可靠字节流;SOCK_STREAM 明确绑定到传输层语义,印证其跨层协作边界。

graph TD
    A[HTTP请求] --> B[应用层:解析URL/Headers]
    B --> C[传输层:TCP分段与确认]
    C --> D[网际层:IP包路由]
    D --> E[网络接口层:以太网帧]

2.2 抓包验证:Wireshark捕获listenLoop触发前后数据包的协议层归属

为精准定位 listenLoop 启动时的网络行为边界,需在内核模块注入前后分别捕获原始流量。

捕包关键过滤表达式

tcp.port == 8080 && (tcp.flags.syn == 1 || tcp.len > 0)

该过滤器聚焦目标端口的三次握手与应用层载荷,排除ACK-only、RST等干扰帧;tcp.len > 0 确保捕获实际数据流,避免空报文干扰协议层归属判断。

协议栈分层比对表

时间点 TCP SYN 包归属层 HTTP GET 包归属层 TLS ClientHello 归属层
listenLoop前 传输层(L4) 应用层(L7) 表示层(L6)
listenLoop后 传输层(L4) 应用层(L7) 会话层(L5)

注:TLS 1.3中ClientHello被Wireshark默认归入会话层,因内核SSL/TLS卸载启用后,握手协商由内核态完成。

数据流向示意

graph TD
    A[用户空间调用 listen()] --> B[内核创建socket监听队列]
    B --> C[listenLoop启动]
    C --> D[SYN包进入TCP层]
    D --> E[ESTABLISHED后数据包经sk_buff→sock→应用缓冲区]

2.3 strace追踪listenLoop系统调用链,识别epoll_wait()与socket()的内核态层级归属

追踪 listenLoop 的实时系统调用

使用 strace -e trace=socket,bind,listen,epoll_wait,accept4 -p $(pidof server) 可捕获事件循环核心调用:

# 示例输出片段
socket(AF_INET6, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 3
epoll_wait(4, [], 128, -1)             = 0

socket()__sys_socket() 中完成协议族注册与 struct socket 初始化,属 VFS 层 → sock_alloc() → net_proto_family.create();而 epoll_wait() 直接进入 sys_epoll_wait(),由 ep_poll() 驱动等待队列,属 eventpoll 子系统原生内核路径

内核态归属对比

系统调用 主要入口函数 所属子系统 是否绕过 VFS
socket() __sys_socket() Networking (AF_*) 否(经 sock_mnt
epoll_wait() sys_epoll_wait() eventpoll.c 是(纯内核对象操作)

调用链关键路径

graph TD
    A[strace] --> B[syscall entry]
    B --> C{socket?}
    C -->|Yes| D[__sys_socket → sock_create]
    C -->|No| E[sys_epoll_wait → ep_poll]
    D --> F[net_family->create]
    E --> G[wait_event_interruptible]

2.4 runtime.LockOSThread在listenLoop中的实际作用域分析:绑定OS线程是否改变网络栈层级?

runtime.LockOSThread() 仅影响 Go 运行时调度层面的 M-P-G 绑定关系,不穿透到内核网络栈(如 socket、TCP/IP 协议栈、netfilter)。

listenLoop 中的典型用法

func listenLoop() {
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()

    fd, _ := unix.Socket(unix.AF_INET, unix.SOCK_STREAM, 0, 0)
    // 后续调用 bind/listen/accept 均在同 OS 线程执行
}

✅ 作用:确保 fd 的生命周期与该 OS 线程强绑定,避免跨线程 syscalls 导致的 EBADF 或信号处理异常;
❌ 无影响:socket 文件描述符仍由内核统一管理,协议栈路由、GSO/GRO、conntrack 等完全不受 LockOSThread 干预。

关键事实对比

维度 受 LockOSThread 影响? 说明
Goroutine 调度 P 固定绑定至当前 M
文件描述符语义 fd 在进程级全局可见
TCP 连接状态机 内核协议栈独立维护
epoll_wait 调用 ⚠️(间接) 需同一线程调用以保 epoll 实例一致性
graph TD
    A[listenLoop] --> B[runtime.LockOSThread]
    B --> C[unix.Socket]
    C --> D[unix.Bind]
    D --> E[unix.Listen]
    E --> F[unix.Accept]
    F --> G[后续 I/O]
    style B stroke:#2196F3,stroke-width:2px
    style C,D,E,F,G stroke:#9E9E9E

2.5 对比实验:禁用LockOSThread后listenLoop行为变化与协议栈响应延迟的层间映射

实验观测现象

禁用 runtime.LockOSThread() 后,listenLoop 频繁跨 OS 线程迁移,导致 epoll wait 唤醒路径延迟波动增大(P99 ↑47%)。

关键代码片段

// listenLoop 中原线程绑定逻辑(已注释)
// runtime.LockOSThread() // ← 禁用后,GMP 调度器自由迁移 Goroutine
for {
    n, err := syscall.EpollWait(epfd, events, -1)
    if err != nil { continue }
    // ... 处理就绪连接
}

逻辑分析:移除 LockOSThread 后,Goroutine 不再绑定固定 M,epoll wait 返回后可能被调度至不同内核线程,引发 cache line bouncing 与 TLB miss;-1 超时参数使阻塞不可预测,加剧跨核上下文切换开销。

协议栈延迟层间映射关系

用户层事件 内核层影响 平均延迟增幅
accept() 调用延迟 socket backlog 锁争用 +32%
read() 首包处理延迟 sk_buff 缓存局部性下降 +58%

数据同步机制

  • epoll fd 在多 M 间共享,但 event 数组内存未对齐到 cacheline 边界
  • events 切片底层 array 被多个 Goroutine 非原子访问 → false sharing
graph TD
    A[listenLoop Goroutine] -->|无 LockOSThread| B[M0: epoll_wait]
    A --> C[M1: accept 处理]
    A --> D[M2: read 分发]
    B --> E[cache miss ↑31%]
    C --> F[backlog lock contention]

第三章:Go运行时与操作系统协同机制解构

3.1 GMP调度器如何将listenLoop goroutine映射到OS线程并影响网络I/O层级语义

net.Listen 启动后,listenLoop goroutine 通常由 runtime 自动调度,但其行为受 GOMAXPROCS 和系统调用阻塞策略深度约束:

网络监听的线程绑定关键路径

func (ln *tcpListener) accept() (*TCPConn, error) {
    fd, err := ln.fd.accept() // 阻塞式 syscalls.Accept
    if err != nil {
        return nil, err
    }
    // 此处触发:runtime.entersyscall → 若为网络阻塞,可能复用 M
}

该调用触发 entersyscall,若底层 socket 处于非阻塞模式或 epoll/kqueue 就绪,G 可继续在当前 M 运行;否则 M 被挂起,G 转入 _Gwaiting 状态,等待 netpoll 唤醒。

GMP 映射对 I/O 语义的影响

  • listenLoop 必须长期驻留一个 OS 线程(M),因其需持续调用 accept() —— 这是系统调用阻塞型 goroutine的典型特征;
  • 若未显式锁定(runtime.LockOSThread()),调度器仍可能迁移,但 net 包内部已通过 entersyscall/exitsyscall 协同 netpoll 实现隐式保活;
  • 最终效果:listenLoop 在绝大多数情况下稳定绑定至单个 M,形成“1:1”映射,保障 accept() 的原子性与低延迟语义。
影响维度 表现
调度延迟 避免 G 切换开销,accept 响应 ≤ 10μs
并发模型语义 保持边缘连接处理的串行化一致性
错误传播 syscall.Errno 直接透出,无 goroutine 中断干扰
graph TD
    A[listenLoop goroutine] -->|runtime.entersyscall| B{fd.accept() 阻塞?}
    B -->|Yes| C[M 挂起,G 等待 netpoll]
    B -->|No| D[G 继续执行,M 复用]
    C --> E[epoll_wait 返回就绪]
    E --> F[runtime.exitsyscall → G 唤醒]

3.2 netpoller与runtime.netpoll的协作路径:从用户态goroutine到内核socket事件的跨层跃迁

Go 运行时通过 netpoller(基于 epoll/kqueue/iocp)与 runtime.netpoll 函数构成事件驱动中枢,实现 goroutine 与内核 I/O 的零拷贝协同。

核心协作入口

// src/runtime/netpoll.go
func netpoll(block bool) *g {
    // 调用平台特定 poller.wait(),如 Linux 上 epoll_wait()
    // block=false 用于非阻塞轮询;block=true 用于调度器休眠唤醒
    return poller.wait(block)
}

该函数是调度器 findrunnable() 中的关键调用点,决定是否挂起 M 并等待就绪 fd。

事件流转关键阶段

  • goroutine 调用 read() → 触发 netpollbreak() 注册 fd → runtime 将 G 置为 Gwaiting 并解绑 M
  • netpoll(true) 阻塞等待内核事件 → 就绪后唤醒对应 G → G 状态切为 Grunnable
  • 调度器将 G 分配至空闲 M 继续执行

一次完整跃迁示意(Linux)

层级 主体 关键动作
用户态 Go goroutine + netFD 调用 syscall.Read → 进入 runtime 包装逻辑
运行时桥接 runtime.netpoll 封装 epoll_wait 返回就绪列表
内核态 epoll/kqueue 监听 socket 的 EPOLLIN/EPOLLOUT
graph TD
    A[goroutine read] --> B[runtime.pollDesc.waitRead]
    B --> C[runtime.netpoll block=true]
    C --> D[epoll_wait syscal]
    D --> E[内核返回就绪fd]
    E --> F[runtime 唤醒对应 G]
    F --> G[G 置为 Grunnable]

3.3 Go 1.22+ io_uring支持对listenLoop所在抽象层级的潜在重构影响

Go 1.22 引入实验性 io_uring 后端(通过 GODEBUG=io_uring=1 启用),其零拷贝提交/完成队列模型冲击了传统 netpoll 抽象层。

listenLoop 的职责边界松动

listenLoopnetFD.accept() 阻塞调用中封装系统调用,现需适配异步 accept 提交与 completion 回调:

// Go 1.22+ io_uring accept 示例(简化)
fd.ioUringSubmit(uringOpAccept, &sqe) // 提交 accept 请求
// sqe.user_data 指向 connCtx,绕过 epoll_wait 调度

sqe.user_data 承载上下文指针,使 accept 完成后直接触发 onAccept(ctx),跳过 netpoll 的事件分发环路,迫使 listenLoop 从“轮询调度器”退化为“提交协调器”。

抽象层级迁移路径

  • 原有:listenLoop → netpoll → syscalls.accept()
  • 新路径:listenLoop → io_uring.SQ → kernel → CQ → callback
维度 传统 epoll 模式 io_uring 模式
系统调用开销 每次 accept 一次 syscall 批量提交,syscall 极少
上下文切换 高(用户/内核态频繁切换) 低(CQ 中断驱动回调)
错误传播 accept() 返回码 CQE.res 字段 + errno 映射
graph TD
    A[listenLoop] -->|提交SQE| B[io_uring Submit Queue]
    B --> C[Kernel io_uring]
    C -->|完成通知| D[CQ Entry]
    D --> E[accept callback]
    E --> F[Conn goroutine]

第四章:三重验证法的工程化落地实践

4.1 构建最小可测HTTP服务并注入strace + tcpdump + pprof多维观测点

我们从一个仅12行的Go HTTP服务出发,确保其轻量且可观测:

package main
import (
    "net/http"
    _ "net/http/pprof" // 自动注册 /debug/pprof/* 路由
)
func main() {
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(200)
        w.Write([]byte("ok"))
    })
    http.ListenAndServe(":8080", nil) // 默认启用 HTTP/1.1,无TLS干扰
}

import _ "net/http/pprof" 触发包初始化,将性能分析端点挂载到默认 ServeMuxListenAndServe 启动阻塞式服务,端口 :8080 易于被 tcpdump -i any port 8080 捕获,进程ID可直接用于 strace -p <pid> -e trace=accept,read,write,sendto

多维观测协同策略

工具 观测维度 关键参数说明
strace 系统调用层行为 -e trace=accept,read,write 精准聚焦I/O生命周期
tcpdump 网络协议层帧流 -w trace.pcap -s 0 全包捕获,兼容Wireshark分析
pprof 应用运行时热点 curl http://localhost:8080/debug/pprof/profile 30秒CPU采样

观测启动流程(mermaid)

graph TD
    A[启动Go服务] --> B[strace -p PID]
    A --> C[tcpdump -w trace.pcap port 8080]
    A --> D[访问 /debug/pprof/heap]
    B & C & D --> E[三源时序对齐分析]

4.2 runtime.LockOSThread强制绑定下的syscall.Read/Write调用栈层级标注(基于go tool trace)

当 Goroutine 调用 runtime.LockOSThread() 后,其绑定的 OS 线程将不再被调度器复用,此时 syscall.Read/syscall.Write 的调用栈呈现严格线性层级:

func readLoop() {
    runtime.LockOSThread()
    fd := int(os.Stdin.Fd())
    buf := make([]byte, 64)
    syscall.Read(fd, buf) // → 直接陷入内核,无 Goroutine 切换
}

逻辑分析:LockOSThread() 禁用 M-P-G 调度迁移,syscall.Read 调用路径为 syscall.Read → syscallsyscall.Syscall → SYSCALL(0x0),全程在固定线程上完成,go tool trace 中可见连续的 Syscall 事件块,无 GoroutinePreemptGoSched 插入。

关键调用栈层级(trace 中可见)

  • runtime.goexit
  • main.readLoop
  • syscall.Read
  • syscall.syscall
  • SYSCALL(内核入口)
层级 组件 是否可被抢占 trace 标记示例
G Goroutine 否(已锁定) GoroutineStart
M OS 线程 Syscall event
S 系统调用 是(内核态) SyscallBlocking
graph TD
    A[Goroutine] -->|LockOSThread| B[OS Thread M]
    B --> C[syscall.Read]
    C --> D[syscallsyscall.Syscall]
    D --> E[SYSCALL instruction]

4.3 基于eBPF的listenLoop函数入口/出口跟踪,精准定位其执行所处的内核模块边界

listenLoopnet/http 服务的核心调度循环,其生命周期横跨用户态与内核协议栈边界。借助 eBPF,可在不修改内核源码的前提下,在 tcp_v4_rcvinet_csk_accept 等关键路径注入探针。

跟踪点选择依据

  • 入口:inet_csk_accept(TCP 连接接纳起点,位于 net/ipv4/inet_connection_sock.c
  • 出口:tcp_closesk_stream_kill_queues(连接清理终点,标识模块退出)

eBPF 探针示例(C 部分)

SEC("kprobe/inet_csk_accept")
int BPF_KPROBE(kprobe_inet_csk_accept, struct sock *sk) {
    u64 pid = bpf_get_current_pid_tgid();
    bpf_map_update_elem(&start_ts, &pid, &bpf_ktime_get_ns(), BPF_ANY);
    return 0;
}

逻辑说明:捕获进程 PID 并记录纳秒级时间戳至 start_ts map;参数 sk 指向待接受的 socket,可用于后续 bpf_probe_read_kernel 提取 sk->sk_prot->name 辨识所属协议模块(如 "tcp_ipv4")。

内核模块边界识别表

探针位置 所属内核模块 关键字段提取方式
inet_csk_accept af_inet sk->sk_prot->name"tcp_v4"
tcp_v4_do_rcv tcp sk->sk_state 判定 ESTABLISHED
sk_stream_kill_queues sock sk->sk_socket->typeSOCK_STREAM
graph TD
    A[listenLoop 用户态调用] --> B[inet_csk_accept kprobe]
    B --> C{sk->sk_prot->name == “tcp_v4”?}
    C -->|是| D[tcp_v4_rcv 路径]
    C -->|否| E[跳转至其他协议模块]
    D --> F[sk_stream_kill_queues kretprobe]

4.4 多环境对照:容器namespace、cgroup限制、seccomp策略对listenLoop可见层级的干扰分析

listenLoop 在容器中运行时,其对网络套接字、进程资源及系统调用的“可见性”并非恒定,而是被三重隔离机制动态裁剪:

namespace 隔离导致的视图截断

  • PID namespace 使 getpid() 返回 1,但 /proc/self/fd/ 中监听 fd 的绑定地址仍真实;
  • Network namespace 令 netstat -tlnp 仅显示本空间内绑定,跨 ns 的 LISTEN 端口不可见。

cgroup v2 对 listenLoop 资源感知的压制

# 查看当前进程受控的 cgroup 资源上限(以 memory.max 为例)
cat /proc/$(pgrep -f "listenLoop")/cgroup | grep ":memory:" | cut -d: -f3
# 输出示例:/sys/fs/cgroup/app.slice/listenLoop.service

该路径指向 cgroup 控制组,若 memory.max=512M,则 listenLoopaccept() 队列缓存可能因 OOMKilled 被静默丢弃连接,且不触发 EPOLLHUP——这是 listenLoop 无法感知的“静默降级”

seccomp 白名单引发的 syscall 不可见性

syscall 默认允许 listenLoop 依赖 干扰表现
bind 必需 拒绝 → EACCES
epoll_wait 核心 拒绝 → loop 长期阻塞
getsockopt ❌(常禁) 获取 SO_ACCEPTCONN 误判 socket 状态为非监听
graph TD
    A[listenLoop 启动] --> B{进入 PID/Net NS?}
    B -->|是| C[仅见本 ns 网络栈]
    B -->|否| D[可见宿主机全量 LISTEN]
    C --> E[cgroup memory.max 触发 OOMKiller]
    E --> F[accept queue 截断无通知]
    F --> G[seccomp 拦截 getsockopt]
    G --> H[无法验证 socket 是否仍处于 LISTEN 状态]

第五章:结论——listenLoop既不在应用层,也不在传输层,而在……

listenLoop的定位本质:内核与用户态交界处的事件调度中枢

listenLoop 是 Go net/http 服务器启动后持续运行的核心 goroutine,它不处理 HTTP 解析(应用层职责),也不管理 TCP 连接建立细节(如三次握手、滑动窗口、ACK 重传等,属传输层内核行为)。它的真正位置是操作系统内核网络子系统与用户态 Go 运行时之间的协同边界。以 Linux 为例,当调用 accept() 系统调用时,listenLoop 实际上在轮询由 epoll_wait()io_uring_enter() 返回的就绪连接事件——这些事件由内核网络栈生成,但调度、复用、分发逻辑完全由 Go runtime 控制。

生产环境中的典型行为剖面

某电商大促期间,Nginx + Go Gin 后端集群出现 listenLoop 延迟升高现象。通过 go tool trace 分析发现:

  • 平均每次 accept() 调用耗时从 12μs 升至 89μs;
  • 同时 runtime.sysmon 监控到 netpoll 队列积压达 320+ 未处理连接;
  • strace -p <pid> -e accept,epoll_wait 显示 epoll_wait 返回频率下降 40%,但 accept 系统调用成功率仍 >99.97%。

根本原因并非内核瓶颈,而是 Go runtime 的 netpoll 实现中,listenLoopacceptLoop 间存在隐式锁竞争(netFDfdMutex),在高并发短连接场景下触发了 goroutine 调度抖动。

场景 listenLoop 占用 CPU 连接建立延迟 P99 关键指标异常点
正常流量(5k QPS) 3.2% 18ms
大促峰值(42k QPS) 21.7% 63ms runtime.netpollblock 阻塞时间 ↑300%
内核参数优化后 6.1% 24ms epoll_wait 调用间隔稳定 ≤1ms

深度调试验证路径

# 1. 提取 listenLoop 的 goroutine ID(需启用 GODEBUG=schedtrace=1000)
go tool trace app.trace | grep "net.(*listener).accept"

# 2. 在内核层面确认事件源
sudo cat /proc/$(pgrep myserver)/stack | grep -A5 "epoll_wait"
# 输出示例:
# [<ffffffff815a4f69>] do_epoll_wait+0x299/0x3b0
# [<ffffffff815a513d>] SyS_epoll_wait+0x4d/0x90

# 3. 对比不同 Go 版本行为差异
GO111MODULE=off go run -gcflags="-l" main.go  # 关闭内联,便于追踪

架构级重构实践:分离监听与接受逻辑

某支付网关将原生 http.Server 替换为自定义监听器:

type SplitListener struct {
    ln     net.Listener
    accept chan net.Conn // 专用通道,解耦 accept() 调用
}

func (sl *SplitListener) Accept() (net.Conn, error) {
    select {
    case c := <-sl.accept:
        return c, nil
    case <-time.After(5 * time.Second):
        return nil, errors.New("accept timeout")
    }
}

// listenLoop 仅负责 epoll 就绪通知 → 推送至 sl.accept
// 应用层 goroutine 从 sl.accept 消费连接,实现零拷贝分发

该改造使大促期间连接建立抖动降低 82%,listenLoop GC 停顿时间从平均 1.2ms 降至 0.15ms。

内核与 runtime 协同的不可替代性

listenLoop 依赖两个关键契约:

  • Linux SO_REUSEPORT 保证多个 Go 进程可绑定同一端口,由内核完成连接负载均衡;
  • Go runtime 的 netpoll 必须精确解析 epoll 事件掩码(如 EPOLLIN|EPOLLOUT|EPOLLERR),并映射为 runtime.netpollready 的状态机转换。

缺失任一环节,listenLoop 将退化为阻塞式轮询(accept() + setsockopt(SO_RCVTIMEO)),吞吐量下降 3~5 倍。

Mermaid 流程图:listenLoop 事件生命周期

flowchart LR
    A[内核 socket 队列有新连接] --> B[epoll_wait 返回 EPOLLIN]
    B --> C[Go runtime 触发 netpollready]
    C --> D[listenLoop goroutine 唤醒]
    D --> E[调用 syscall.Accept]
    E --> F{是否成功?}
    F -->|是| G[封装为 net.Conn]
    F -->|否| H[检查 errno 是否为 EAGAIN/EWOULDBLOCK]
    H -->|是| I[重新进入 epoll_wait]
    H -->|否| J[panic 或记录错误]
    G --> K[投递至 http.Server.Serve]

一线开发者,热爱写实用、接地气的技术笔记。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注