
内核怎么管理侦听 socketLinux 内核管理监听 socket 的核心是用全局哈希表listening_hash组织所有监听 socket配合两套队列SYN 队列和 accept 队列完成连接建立和交接。先读这条最重要的结论无论是否使用SO_REUSEPORT三次握手始终由内核软中断上下文独立完成应用进程从不参与握手。两种情况的唯一区别是SYN 到达时候选侦听 socket 有几个、内核要不要多选一。进程从头到尾只负责一件事——调用accept()取走已经建立好的连接。一、监听 socket 的内核表示一个监听 socket 在内核中涉及三层结构结构作用struct socket用户态可见的 socket 抽象对应一个文件描述符struct sock内核传输控制块保存协议状态TCP 具体化为struct tcp_sockstruct inet_connection_sock内嵌于 tcp_sock增加连接管理字段accept 队列、keepalive 等当listen()被调用后struct sock的状态变为TCP_LISTEN并挂入全局监听哈希表。二、全局监听哈希表listening_hash内核维护一个全局数组32 个桶structinet_listen_hashbucketlistening_hash[INET_LHTABLE_SIZE];每个桶是一条链表哈希值由本地端口计算staticinlineunsignedintinet_lhashfn(structnet*net,constunsignedshortnum){returnnum(INET_LHTABLE_SIZE-1);}所有处于TCP_LISTEN状态的 socket 都在这里。这是内核管理监听 socket 的主索引也是收包路径定位侦听 socket 的入口。三、监听 socket 的生命周期1. 创建listen()系统调用intlisten(intsockfd,intbacklog);内核执行如果 socket 尚未 bind先自动绑定autobind分配一个临时端口——不是简单检查报错。调用inet_csk_listen_start()把sk_state设为TCP_LISTEN初始化 accept 队列icsk_accept_queue调用inet_hash()把 socket 插入listening_hash。accept 队列上限取min(backlog, net.core.somaxconn)。2. 运行等待连接监听 socket 不主动做任何事——没有线程轮询、没有定时器只是挂在哈希表里等待 SYN 包。内核收到 SYN 后通过listening_hash找到它然后由内核完成握手。3. 关闭close()或shutdown()调用inet_csk_listen_stop()从listening_hash中移除清空 SYN 队列和 accept 队列释放所有未完成的连接状态变为TCP_CLOSE。四、两套队列SYN 队列和 accept 队列每个监听 socket 内部维护两个队列这是内核管理连接建立的核心。1. SYN 队列半连接队列存放收到 SYN、但三次握手未完成的request sock迷你 sock只含握手所需字段。数据结构内核 4.4 及以前是request_sock_queue中的listen_opt半开哈希表4.4 起 request_sock 以自身四元组直接插入全局 ehash旧结构已移除很多老文章仍写 syn_table已过时。大小受net.ipv4.tcp_max_syn_backlog约束。作用限制半连接数量抵御 SYN Flood。2. accept 队列全连接队列存放三次握手完成、等待应用accept()的full sock。数据结构icsk_accept_queue中的rskq_accept_head链表FIFO。大小由min(listen() 的 backlog, net.core.somaxconn)决定。作用缓冲已建立的连接等待应用取走。3. 队列满了会怎样队列满了之后SYN 队列默认丢弃新 SYN启用tcp_syncookies后不存 request_sock靠 cookie 继续服务accept 队列默认丢弃第三次握手的 ACK客户端超时重传tcp_abort_on_overflow1时直接回 RST五、连接建立的完整流程客户端 SYN │ ▼ 内核收到 SYN软中断上下文 │ ▼ __inet_lookup_listener() 在 listening_hash 中查找监听 socket │ ▼ 创建 request sock放入 SYN 队列 ← 内核完成进程不参与 │ ▼ 内核回复 SYNACK ← 内核完成进程不参与 │ ▼ 客户端 ACK第三次握手 │ ▼ 内核创建 full sock放入 accept 队列 ← 内核完成进程不参与 │ ▼ 唤醒等待在 accept()/epoll_wait 的进程 ← 进程此刻才被唤醒 │ ▼ 应用 accept() 取出连接返回新 fd ← 进程唯一参与的一步注意握手三步全部发生在进程被唤醒之前。即使应用进程被暂停如 SIGSTOP内核照样完成握手、填满 accept 队列——只是没人来取。六、监听 socket 的查找机制当 SYN 包到达时内核调用__inet_lookup_listener()查找监听 socket。真实机制是按端口定位桶 按地址打分用目的端口哈希定位listening_hash的桶——端口必须精确匹配这是进桶的前提遍历桶内链表按地址打分score绑定 IP 与包的目的 IP精确相等得分高于绑定0.0.0.0通配最高分者胜出同时检查SO_BINDTODEVICE绑定的网卡、网络命名空间、SO_REUSEPORT分组。⚠️ 常见误传“查找优先级包含’端口通配’”。监听 socket 的查找不存在端口通配——端口永远精确匹配桶就是按端口定位的通配 vs 精确只存在于 IP 这一个维度。多监听 socket 场景场景说明不同端口各自落在不同哈希桶互不干扰同一端口不同 IP如 192.168.1.1:80 与 10.0.0.1:80同桶共存查找时精确 IP 匹配打分更高优先命中SO_REUSEPORT多进程共享端口同桶挂多个监听 socket按四元组哈希多选一见第七章七、核心问题谁负责建立 TCP 连接重用端口 vs 非重用端口这是最容易产生困惑的地方先给统一结论再分场景拆解。7.1 统一结论无论是否SO_REUSEPORT三次握手都由内核独立完成应用进程不参与。需要精确化的说法是内核定位的是监听 socket不是进程。内核不关心 socket 属于哪个进程它只负责查表定位 socket → 完成握手 → 把连接放进该 socket 的 accept 队列 → 唤醒等待者。两种情况的唯一区别非 SO_REUSEPORTSO_REUSEPORT同端口监听 socket 数量1 个N 个SYN 到达时内核的动作直接命中唯一 socket没得选按四元组哈希多选一握手由谁完成内核内核在被选中的 socket 上执行连接进哪个 accept 队列唯一 socket 的队列被选中 socket 的队列谁能 accept持有该 socket 的进程们只有被选中 socket 对应的进程7.2 场景一单进程监听非重用端口最常见的情况。进程listen()后内核创建唯一一个监听 socket 【这个端口只有一个socket和它对应】挂到listening_hashSYN 到达内核查表直接命中唯一监听 socket内核创建 request sock 入 SYN 队列回 SYNACK等第三次 ACK——全程应用不参与握手完成内核创建 full sock 放入该 socket 的 accept 队列内核唤醒等待进程【只有一个进程】进程accept()取走连接。不存在选择哪个进程的问题因为只有一个监听 socket。7.3 场景二多进程共享同一个监听 fdfork 模式仍非重用端口父进程listen()后fork出多个子进程子进程继承同一个监听 fd【但是每次子进程的fd都是指向同一个socket内核对象】仍然只有一个监听 socket挂在listening_hash多个进程都阻塞在同一个 socket 的accept()上SYN 到达、握手完成、连接入队全部由内核完成连接入队后内核通过互斥等待队列exclusive wait只唤醒一个等待进程大致 FIFO避免惊群哪个进程先被唤醒并完成accept()连接就归谁——进程之间靠 accept 队列的互斥机制竞争保证一个连接只被一个进程取走。注意这不是SO_REUSEPORT——监听 socket 始终只有一个内核没有多选一的动作竞争发生在用户态的 accept 上。7.3.1 在全连接队列中有了 链接时内核到底是怎么通知的通知谁 那个谁是在哪里为什么能通知到等待队列sk_wqstruct socket_wait_queuestruct sock里有等待队列头 sk_wq。每个阻塞在accept()的 进程会把自己的 ** 进程描述符task_struct** 包装成一个wait_queue_entry等待项挂载到sk_wq这个等待队列上。多个 task_struct全部挂在同一个 sock 的 sk_wq 队列添加等待项的时候设置 WQ_FLAG_EXCLUSIVE排他等待标记。WQ_FLAG_EXCLUSIVE 含义唤醒的时候只唤醒 1 个等待项而不是全部用来解决惊群三次握手完成连接进入全连接队列时内核通知流程 客户端 ACK 到达内核三次握手完成新连接 sock 放入 listener 的sk_acceptq全连接队列。内核判断sk_acceptq 从空 → 现在有连接了触发唤醒逻辑运行wake_up_interruptible(sk-sk_wq);wake_up遍历sk_wq里挂着的等待项 遇到带WQ_FLAG_EXCLUSIVE标记的等待项只唤醒这1个进程然后直接break停止遍历不会唤醒剩下所有 worker → 这就是Linux内核层面消除 accept 惊群的核心。 ⚠️ 老内核2.6之前没有 exclusive 标记wake_up 会唤醒队列上全部 worker所有 worker 被唤醒去抢 accept 队列但只有1个能拿到连接其余 worker 发现队列空重新休眠就是经典accept 惊群。7.4 场景三SO_REUSEPORT多个独立监听 socket多个进程各自创建自己的监听 socket【内核中有多个socket对象但是绑定的是同一个端口】都绑定同一端口并设置SO_REUSEPORTsetsockopt(fd,SOL_SOCKET,SO_REUSEPORT,on,sizeof(on));此时listening_hash同一端口下挂了N 个监听 socket。SYN 到达时内核立即用四元组哈希多选一提取{源IP, 源端口, 目的IP, 目的端口}计算哈希对共享该端口的监听 socket 数量取模选定一个选择发生在 SYN 阶段即握手开始之前。从这一刻起这个半开连接SYN_RCVD就与被选中的监听 socket 绑定后续握手仍由内核完成SYNACK 的发送、等待第三次 ACK、创建 full sock全部是内核在被选中的 socket 上执行的——不是被选中的进程在做握手完成后full sock 进入被选中 socket 的 accept 队列只有该 socket 对应的进程能accept()到它【这个socket 的 accept等待队列中只有一个进程】。两个精确化的点一致性保证的粒度是四元组不是客户端同四元组的包必然命中同一 socket但同一客户端的不同连接使用不同源端口四元组不同可能被分散到不同进程。代价被选中的进程如果崩溃它名下未完成的半开连接和 accept 队列中已建立的连接会被内核直接丢弃其他存活进程无法接管因为连接只挂在它的 socket 上。这也是内核 4.19 引入BPF_PROG_TYPE_SK_REUSEPORT的原因——允许用自定义 eBPF 程序替换默认哈希实现进程重启时的平滑迁移等灵活调度。7.4.1 SO_REUSEPORT 负载均衡与均匀分发原理核心一句话Linux 默认 SO_REUSEPORT 分发是基于四元组 hash 取模天然偏向会话粘性不是轮询。均匀性依赖源端口、源 IP 足够离散内核 4.14 之后有优化。默认分发算法Linux 原生无 eBPFSYN包到达内核拿到四元组(sip,sport,dip,dport)hash_valhash(sip,sport,dip,dport)listener_idxhash_val%NN当前该dip:dport下注册的SO_REUSEPORT监听 socket 数量。 然后选中第 listener_idx 对应的 listener sock后续整个握手、半连接队列、全连接队列都归属这个 sock。 特点 同一个四元组 → hash 固定永远分到同一个 listener。 同一个客户端同一个源端口连服务端多次重连一定会落到同一个 worker。 分发均匀性取决于源端口/源IP的离散程度 ✅ 大量客户端源端口随机、分散hash 分布很均匀各个 worker 连接数接近。 ❌ 少数客户端短连接、源端口复用范围很小hash 值集中连接大量打到同一个 worker负载不均衡。 误区不是 “来了一个新连接就轮询下一个进程”。轮询是连接到达顺序调度SO_REUSEPORT默认是哈希分片。7.4.2 怎么做到真正均匀分发两种方案方案 1eBPF SK_REUSEPORT4.19你前面提到的BPF_PROG_TYPE_SK_REUSEPORT内核在 SYN 包到达时不再使用默认四元组 hash执行你写的 eBPF 程序自己选定 listener。你可以自己实现轮询分发根据各个 worker 当前连接数动态选负载最低的进程真正动态负载均衡自定义路由策略约束依然必须保证同一个四元组的所有包选中同一个 listener否则 TCP 流断裂。eBPF 不能破坏这个前提。方案 2依赖客户端侧源端口充分随机最简单业务上大量不同客户端、源端口随机默认 hash 的分布就足够均匀。典型场景互联网高并发 web 服务eBPF 程序本身是在内核态执行但它不是原生内核代码由用户态加载、校验再注入内核7.5 常见误区纠正❌ 错误说法✅ 正确说法“SO_REUSEPORT 下被选中的进程独立完成三次握手”握手永远由内核完成SO_REUSEPORT 只决定连接进哪个 socket 的队列进程只负责 accept“内核在 SYN 阶段定位到进程”内核定位的是监听 socket进程只是 socket 的持有者非重用端口时内核根本不涉及选进程“同一客户端的连接总是打到同一进程”哈希依据是四元组同一客户端的不同连接源端口不同可能分散到不同进程“非重用端口时内核先建连接再通知进程重用端口时进程自己建连接”两种情况下握手都由内核完成区别仅在于候选 socket 是一个还是多个八、关键内核参数参数作用net.core.somaxconnaccept 队列全局上限net.ipv4.tcp_max_syn_backlogSYN 队列上限net.ipv4.tcp_syncookiesSYN Flood 防护net.ipv4.tcp_abort_on_overflowaccept 队列满时是否发 RST0丢 ACK1回 RST九、总结维度机制组织全局listening_hash按本地端口哈希生命周期listen()插入未绑定则先 autobindclose()移除连接建立SYN 队列存半连接accept 队列存全连接握手全程由内核完成查找按端口定位桶 按地址打分IP 精确匹配优先于通配复用SO_REUSEPORT支持多监听 socket 共享端口SYN 阶段按四元组哈希多选一防护SYN 队列限半连接accept 队列限待取连接syncookies 兜底一句话Linux 内核用listening_hash管理所有监听 socket用 SYN 队列和 accept 队列管理连接建立过程收到 SYN 时按端口查表定位监听 socketSO_REUSEPORT 时按四元组哈希多选一由内核独立完成三次握手然后把连接放入对应 socket 的 accept 队列唤醒应用来accept()——进程从不参与握手只负责取连接。