﻿# listen 内核实现 —— 从"空壳"到"服务端"的转折点

> 上一篇 [socket-kernel-internals.md](/concepts/network/socket-kernel-internals.md) 造好了 socket 对象并绑定了地址，下一篇 [accept-kernel-internals.md](/concepts/network/accept-kernel-internals.md) 从队列取连接。本篇聚焦四件套的**第三步**：`listen()` 怎么把 socket 从 `TCP_CLOSE` 推到 `TCP_LISTEN`，以及那两张让 `accept()` 得以工作的队列是怎么来的。

## 一、listen 做了什么

一句话：**`listen()` 把 socket 标记为 `TCP_LISTEN`（至此内核才开始处理 SYN），同时创建两张队列——半连接队列和全连接队列。** 没有 `listen()`，`accept()` 什么也拿不到。

| 动作 | 详情 | 位置 |
|------|------|------|
| 标记 `TCP_LISTEN` | `sk->sk_state = TCP_LISTEN`，内核此后才处理 SYN | sk 状态字段 |
| 建半连接队列 | `reqsk_queue_alloc()` 分配 hash 表，用于暂存收到 SYN 但还没完成握手的 `request_sock` | `icsk->icsk_accept_queue` |
| 建全连接队列 | 复用同一数据结构，三次握手完成后移入此处，等 `accept()` 取走 | 同上 |
| 设全连接队列上限 | `sk->sk_max_ack_backlog = min(backlog, somaxconn)` —— **会被截断** | sk 字段 |

## 二、两队列模型

```bash
                       客户端 SYN 到达
                             │
                             ▼
                    ┌─────────────────┐
                    │   半连接队列      │  ← 收到 SYN，回 SYN-ACK，等着客户端回 ACK
                    │  (SYN queue)     │     上限: tcp_max_syn_backlog
                    └────────┬────────┘
                             │ 客户端回 ACK，三次握手完成
                             ▼
                    ┌─────────────────┐
                    │   全连接队列      │  ← 握手完成，等 accept() 取走
                    │  (accept queue)  │     上限: min(somaxconn, backlog)
                    └────────┬────────┘
                             │ accept() 取走
                             ▼
                       用户进程拿到 newfd
```

> **图析**：两队列是串联的过滤器。半连接队列是"门厅"——只验证 SYN 来源是否合理（默认防 SYN flood 靠的是它的大小限制 + syncookies 旁路），通过后给客户端回 SYN-ACK 并暂存 request_sock。全连接队列是"候客室"——三次握手完成、连接已建立，就等应用 `accept()` 领走。**两个队列的上限由不同参数控制**，这是最容易配错的地方。

| 属性 | 半连接队列（SYN queue） | 全连接队列（accept queue） |
|------|----------------------|--------------------------|
| 存放内容 | 收到 SYN 并回 SYN-ACK 后，等客户端回 ACK 的 `request_sock` | 三次握手完成的 `request_sock`，其 `.sk` 已指向子 sock |
| 加入时机 | `tcp_conn_request()` 收到 SYN | `inet_csk_reqsk_queue_add()` 握手完成 |
| 移除时机 | 收到客户端 ACK → 升级移入全连接 | `reqsk_queue_remove()` → accept 取走 |
| 上限控制 | `net.ipv4.tcp_max_syn_backlog` | `min(net.core.somaxconn, listen() backlog)` |
| 满了的行为 | 丢新 SYN（或启用 syncookies 绕过） | 默认丢 ACK 让客户端重传（不直接 RST） |

> **关键区分**：`listen(fd, 128)` 的第二个参数只控制**全连接队列**，不控制半连接队列。很多人以为 backlog 调大了就能防 SYN flood——不能，半连接队列的上限由 `tcp_max_syn_backlog` 单独控制。

## 三、listen() 调用链总览

```bash
用户态:  listen(sockfd, 128)
内核态:
  __sys_listen(fd, backlog)
    │
    ├─ sockfd_lookup_light(fd)         // fd → struct socket
    │
    ├─ backlog = min(backlog, somaxconn)  // ★ 截断！somaxconn 是天花板
    │
    ├─ security check
    │
    └─ sock->ops->listen(sock, backlog)  // AF_INET → inet_listen()
              │
              ▼
       inet_listen(sk, backlog)
         │
         ├─ 状态检查：必须是 TCP_CLOSE 或 TCP_LISTEN
         │   • 首次 listen → 走 inet_csk_listen_start() 全量初始化
         │   • 已经是 TCP_LISTEN → 只更新 backlog（不改队列）
         │
         └─ inet_csk_listen_start(sk, backlog)
              │
              ├─ reqsk_queue_alloc()    分配半连接队列 hash 表
              ├─ sk->sk_max_ack_backlog = backlog
              ├─ sk->sk_state = TCP_LISTEN  ★ 转折点
              └─ sk->sk_prot->hash(sk)   注册到全局 TCP hash 表
```

> **图析**：调用链最关键的三个节点——(1) `min(backlog, somaxconn)` 在 VFS 层就把 backlog 截断了，你传 65535 进去、somaxconn=128 的话实际生效的是 128；(2) `inet_listen` 的状态检查区分首次和重复——已 LISTEN 时只改 backlog、不重建队列，保证运行时热调；(3) `inet_csk_listen_start` 是真正的"开张"——分配队列 hash 表、标记 `TCP_LISTEN`、注册到全局 hash（从此能被 SYN 包匹配到）。

**重复 listen**：如果 socket 已经是 `TCP_LISTEN`，再次调用 `listen()` **不重建队列**，只更新 `sk_max_ack_backlog`——已有的连接不受影响。这允许运行时调大 backlog 而不断开已有连接。

## 四、listen() 入口到协议层的时序

```plantuml
@startuml
skinparam shadowing false
participant "用户进程" as U #C8E6C9
participant "__sys_listen" as SYS #BBDEFB
participant "inet_listen" as INET #BBDEFB
U -> SYS : listen(sockfd, 128)
SYS -> SYS : sockfd_lookup_light(fd)\nfd → struct socket
SYS -> SYS : backlog = min(128, somaxconn)
note right: somaxconn=4096 → backlog=128\nsomaxconn=64 → backlog=64 ★
SYS -> SYS : security_socket_listen()
note right: SELinux/AppArmor 检查
SYS -> INET : sock->ops->listen()\n→ inet_listen(sk, 128)
INET -> INET : lock_sock(sk)
INET -> INET : 检查 TCP 状态
note right
  TCP_CLOSE: 首次 listen
  TCP_LISTEN: 重复 listen
  其他状态: 拒绝
end note
INET -> INET : 确定走首次初始化路径
INET -> INET : release_sock(sk)
note right of U : 接下来进入 inet_csk_listen_start\n真正做初始化
@enduml
```

> **图析**：这一阶段把"门面工作"做完——验证 fd 有效、截断 backlog 到 somaxconn 上限、通过安全模块检查、确认 TCP 状态允许 `listen()`。**其中 `min(backlog, somaxconn)` 是最容易踩的坑**：`listen(fd, 4096)` 写多大都没用，实际天花板在 `sysctl net.core.somaxconn`。如果 somaxconn 还是默认的 128，backlog 传 4096 也只能拿到 128。

## 五、inet_csk_listen_start() 执行时序——真正的开张

```plantuml
@startuml
skinparam shadowing false
participant "inet_listen" as INET #BBDEFB
participant "inet_csk_listen_start" as CSK #BBDEFB
participant "request_sock_queue" as RQ #FFF9C4
participant "全局 TCP hash 表" as HASH #FFCCBC
INET -> CSK : inet_csk_listen_start(sk, 128)
CSK -> CSK : sk->sk_state = TCP_CLOSE\nsk->sk_protocol = IPPROTO_TCP
CSK -> RQ : reqsk_queue_alloc()\n分配半连接队列 hash 表
note right
  队列大小由 tcp_max_syn_backlog\n和系统内存共同决定
end note
CSK -> CSK : sk->sk_ack_backlog = 0
CSK -> CSK : sk->sk_max_ack_backlog = 128
CSK -> CSK : sk->sk_state = TCP_LISTEN
note right: ★ 从此刻起，内核为这个 socket\n处理收到的 SYN 报文
CSK -> HASH : sk->sk_prot->hash(sk)\n注册到全局 TCP hash 表
note right
  客户端 SYN 到达后\n在此表中匹配 → 找到这个 listen socket
end note
CSK --> INET : return 0
@enduml
```

> **图析**：`inet_csk_listen_start` 执行的五步中没有一步是可以省略的。(1) `reqsk_queue_alloc` 是唯一一次分配内存——两队列共享这个 hash 表结构，上限受 `tcp_max_syn_backlog` 控制；(2) `sk_ack_backlog = 0` 清空计数器，全连接队列当前积压 = 0；(3) `sk_max_ack_backlog = 128` 设全连接队列上限；

> (4) **`sk_state = TCP_LISTEN` 是整条调用链的核心转折**——此前的 socket 对网络包来说是透明的，此后的 socket 会接收并响应 SYN；(5) `sk_prot->hash` 把 socket 注册到全局 hash 表，让后续到达的 SYN 包能匹配到这个 socket。**自此，服务端正式"开张"**。

## 六、观测与调优

| 手段 | 命令 | 看什么 |
|------|------|--------|
| 看 LISTEN socket | `ss -tlnp` | `State` 显示 `LISTEN`，`Recv-Q/Send-Q` 显示全连接队列积压/上限 |
| 调 somaxconn | `sysctl net.core.somaxconn=65535` | 全连接队列的天花板 |
| 调半连接上限 | `sysctl net.ipv4.tcp_max_syn_backlog=8192` | SYN queue 上限 |
| 查溢出计数 | `netstat -s \| grep "overflowed"` | 看到队列溢出次数 |

延伸阅读：[accept-kernel-internals.md](/concepts/network/accept-kernel-internals.md)（accept 怎么从队列取连接）、[kernel-tuning-net.md](/concepts/network/kernel-tuning-net.md)（somaxconn / tcp_max_syn_backlog 完整调优）。

## 一句话总结

**`listen()` 是三行代码里最关键的一行：它把 `sk_state` 从 `TCP_CLOSE` 推到 `TCP_LISTEN`（至此内核才开始响应 SYN），创建半连接+全连接两队列作为连接的"暂存间"，并设好全连接队列上限（被 somaxconn 截断）。没有 listen，socket+bind 造出来的只是有名字的空白卡片——listen 把它变成了真正的服务端。**

