【Linux】内核怎么管理侦听socket

内核怎么管理侦听 socket

Linux 内核管理监听 socket 的核心是:用全局哈希表(listening_hash)组织所有监听 socket,配合两套队列(SYN 队列和 accept 队列)完成连接建立和交接

先读这条最重要的结论 :无论是否使用 SO_REUSEPORT三次握手始终由内核(软中断上下文)独立完成,应用进程从不参与握手 。两种情况的唯一区别是:SYN 到达时候选侦听 socket 有几个、内核要不要"多选一"。进程从头到尾只负责一件事------调用 accept() 取走已经建立好的连接。


一、监听 socket 的内核表示

一个监听 socket 在内核中涉及三层结构:

结构 作用
struct socket 用户态可见的 socket 抽象,对应一个文件描述符
struct sock 内核传输控制块,保存协议状态(TCP 具体化为 struct tcp_sock
struct inet_connection_sock 内嵌于 tcp_sock,增加连接管理字段(accept 队列、keepalive 等)

listen() 被调用后,struct sock 的状态变为 TCP_LISTEN,并挂入全局监听哈希表。


二、全局监听哈希表:listening_hash

内核维护一个全局数组(32 个桶):

c 复制代码
struct inet_listen_hashbucket listening_hash[INET_LHTABLE_SIZE];

每个桶是一条链表,哈希值由本地端口计算:

c 复制代码
static inline unsigned int inet_lhashfn(struct net *net, const unsigned short num)
{
    return num & (INET_LHTABLE_SIZE - 1);
}

所有处于 TCP_LISTEN 状态的 socket 都在这里。 这是内核管理监听 socket 的主索引,也是收包路径定位侦听 socket 的入口。


三、监听 socket 的生命周期

1. 创建:listen() 系统调用

c 复制代码
int listen(int sockfd, int backlog);

内核执行:

  1. 如果 socket 尚未 bind,先自动绑定(autobind)分配一个临时端口------不是简单检查报错。
  2. 调用 inet_csk_listen_start()
    • sk_state 设为 TCP_LISTEN
    • 初始化 accept 队列(icsk_accept_queue);
    • 调用 inet_hash() 把 socket 插入 listening_hash
  3. 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_overflow=1 时直接回 RST

五、连接建立的完整流程

复制代码
客户端 SYN
    │
    ▼
内核收到 SYN(软中断上下文)
    │
    ▼
__inet_lookup_listener() 在 listening_hash 中查找监听 socket
    │
    ▼
创建 request sock,放入 SYN 队列          ← 内核完成,进程不参与
    │
    ▼
内核回复 SYN+ACK                          ← 内核完成,进程不参与
    │
    ▼
客户端 ACK(第三次握手)
    │
    ▼
内核创建 full sock,放入 accept 队列       ← 内核完成,进程不参与
    │
    ▼
唤醒等待在 accept()/epoll_wait 的进程      ← 进程此刻才被唤醒
    │
    ▼
应用 accept() 取出连接,返回新 fd          ← 进程唯一参与的一步

注意 :握手三步全部发生在进程被唤醒之前。即使应用进程被暂停(如 SIGSTOP),内核照样完成握手、填满 accept 队列------只是没人来取。


六、监听 socket 的查找机制

当 SYN 包到达时,内核调用 __inet_lookup_listener() 查找监听 socket。真实机制是按端口定位桶 + 按地址打分

  1. 目的端口 哈希定位 listening_hash 的桶------端口必须精确匹配,这是进桶的前提;
  2. 遍历桶内链表,按地址打分(score) :绑定 IP 与包的目的 IP 精确相等 得分高于绑定 0.0.0.0(通配),最高分者胜出;
  3. 同时检查: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_REUSEPORT SO_REUSEPORT
同端口监听 socket 数量 1 个 N 个
SYN 到达时内核的动作 直接命中唯一 socket,没得选 按四元组哈希多选一
握手由谁完成 内核 内核(在被选中的 socket 上执行)
连接进哪个 accept 队列 唯一 socket 的队列 被选中 socket 的队列
谁能 accept 持有该 socket 的进程(们) 只有被选中 socket 对应的进程

7.2 场景一:单进程监听(非重用端口)

最常见的情况。进程 listen() 后,内核创建唯一一个 监听 socket 挂到 listening_hash

  1. SYN 到达,内核查表直接命中唯一监听 socket;
  2. 内核创建 request sock 入 SYN 队列,回 SYN+ACK,等第三次 ACK------全程应用不参与
  3. 握手完成,内核创建 full sock 放入该 socket 的 accept 队列;
  4. 内核唤醒等待进程,进程 accept() 取走连接。

不存在"选择哪个进程"的问题,因为只有一个监听 socket。

7.3 场景二:多进程共享同一个监听 fd(fork 模式,仍非重用端口)

父进程 listen()fork 出多个子进程,子进程继承同一个监听 fd:

  • 仍然只有一个监听 socket ,挂在 listening_hash
  • 多个进程都阻塞在同一个 socket 的 accept() 上;
  • SYN 到达、握手完成、连接入队,全部由内核完成;
  • 连接入队后,内核通过互斥等待队列(exclusive wait)只唤醒一个等待进程(大致 FIFO),避免惊群;
  • 哪个进程先被唤醒并完成 accept(),连接就归谁------进程之间靠 accept 队列的互斥机制竞争,保证一个连接只被一个进程取走。

注意 :这不是 SO_REUSEPORT------监听 socket 始终只有一个,内核没有"多选一"的动作,竞争发生在用户态的 accept 上。

7.4 场景三:SO_REUSEPORT(多个独立监听 socket)

多个进程各自创建自己的监听 socket,都绑定同一端口并设置 SO_REUSEPORT

c 复制代码
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on));

此时 listening_hash 同一端口下挂了 N 个监听 socket。SYN 到达时:

  1. 内核立即用四元组哈希多选一 :提取 {源IP, 源端口, 目的IP, 目的端口} 计算哈希,对共享该端口的监听 socket 数量取模,选定一个;
  2. 选择发生在 SYN 阶段,即握手开始之前。从这一刻起,这个半开连接(SYN_RCVD)就与被选中的监听 socket 绑定;
  3. 后续握手仍由内核完成 :SYN+ACK 的发送、等待第三次 ACK、创建 full sock,全部是内核在被选中的 socket 上执行的------不是被选中的进程在做
  4. 握手完成后,full sock 进入被选中 socket 的 accept 队列 ,只有该 socket 对应的进程能 accept() 到它。

两个精确化的点:

  • 一致性保证的粒度是四元组,不是"客户端" :同四元组的包必然命中同一 socket;但同一客户端的不同连接使用不同源端口,四元组不同,可能被分散到不同进程
  • 代价 :被选中的进程如果崩溃,它名下未完成的半开连接和 accept 队列中已建立的连接会被内核直接丢弃,其他存活进程无法接管 (因为连接只挂在它的 socket 上)。这也是内核 4.19 引入 BPF_PROG_TYPE_SK_REUSEPORT 的原因------允许用自定义 eBPF 程序替换默认哈希,实现进程重启时的平滑迁移等灵活调度。

7.5 常见误区纠正

❌ 错误说法 ✅ 正确说法
"SO_REUSEPORT 下,被选中的进程独立完成三次握手" 握手永远由内核完成;SO_REUSEPORT 只决定连接进哪个 socket 的队列,进程只负责 accept
"内核在 SYN 阶段定位到进程" 内核定位的是监听 socket;进程只是 socket 的持有者,非重用端口时内核根本不涉及"选进程"
"同一客户端的连接总是打到同一进程" 哈希依据是四元组;同一客户端的不同连接(源端口不同)可能分散到不同进程
"非重用端口时内核先建连接再通知进程,重用端口时进程自己建连接" 两种情况下握手都由内核完成,区别仅在于候选 socket 是一个还是多个

八、关键内核参数

参数 作用
net.core.somaxconn accept 队列全局上限
net.ipv4.tcp_max_syn_backlog SYN 队列上限
net.ipv4.tcp_syncookies SYN Flood 防护
net.ipv4.tcp_abort_on_overflow accept 队列满时是否发 RST(0=丢 ACK,1=回 RST)

九、总结

维度 机制
组织 全局 listening_hash,按本地端口哈希
生命周期 listen() 插入(未绑定则先 autobind),close() 移除
连接建立 SYN 队列存半连接,accept 队列存全连接,握手全程由内核完成
查找 按端口定位桶 + 按地址打分,IP 精确匹配优先于通配
复用 SO_REUSEPORT 支持多监听 socket 共享端口,SYN 阶段按四元组哈希多选一
防护 SYN 队列限半连接,accept 队列限待取连接,syncookies 兜底

一句话 :Linux 内核用 listening_hash 管理所有监听 socket,用 SYN 队列和 accept 队列管理连接建立过程;收到 SYN 时按端口查表定位监听 socket(SO_REUSEPORT 时按四元组哈希多选一),由内核独立完成三次握手 ,然后把连接放入对应 socket 的 accept 队列,唤醒应用来 accept()------进程从不参与握手,只负责取连接。

相关推荐
本人手速666+1 小时前
企业微信 API 接入层如何降低业务系统复杂度
运维·网络协议·微信·自动化·ipad
j7~1 小时前
【Linux网络】三十七.TCP 协议通讯流程
linux·网络编程·三次握手·四次挥手·tcp 协议通讯流程
顶点多余1 小时前
9.15 面试问题总结(一面挂)
linux·面试·职场和发展
顺风尿一寸2 小时前
从 ls -U 到 Tomcat libs加载jar包:ext4 目录哈希序如何影响 Java 文件排序顺序
linux·jvm
qeen872 小时前
【Linux】操作系统之进程介绍(一)
linux·笔记·学习·进程
邪修king2 小时前
Re:Linux系统篇(二十五):文件系统(一):磁盘硬件底层原理:从物理结构到 CHS/LBA 寻址,搞懂硬盘数据的定位逻辑
java·linux·运维·gpt
小则又沐风a2 小时前
TCP协议讲解-----了解TCP可靠性的基石
linux
刚入门的大一新生2 小时前
Linux-简单设计libc库
linux·c++
科小墨3 小时前
【Linux 开发者系列 · 第 1 节】NPM 完全指南:它是什么、为什么重要、在 Linux 上怎么用
linux