在运维和开发过程中,你可能也遇到过类似的"诡异"现象:某个服务占用端口出了问题,你找到它的 PID,执行了 kill -9 <PID> 强制杀掉进程。但当再次用 netstat 或 ss 检查时,却发现该端口依然留在列表中:
bash
$ netstat -anop | grep 8000
tcp 0 1 [已脱敏IP]:8000 [已脱敏IP]:23331 FIN_WAIT1 - on (0.15/1/0)
tcp 0 1 [已脱敏IP]:8000 [已脱敏IP]:24535 FIN_WAIT1 - on (0.18/1/0)
进程明明已经死了(PID 变成了 -),为什么端口连接还在?这个 FIN_WAIT1 到底是什么?在这些 FIN_WAIT1 连接消失之前,其他程序还能使用这个端口吗?本文将为你揭开 TCP 连接生命周期与操作系统内核接管机制的幕后真相。
01 | 关键概念:LISTEN 端口 vs ESTABLISHED 连接
在解答这个问题之前,首先需要厘清操作系统中的两种网络状态:
- 监听端口(LISTEN):应用程序通过绑定并监听某个端口,随时准备接收新的连接请求。
- 已建立的连接(ESTABLISHED):客户端与服务端完成"三次握手"后建立的通信通道。
当你执行 kill -9 杀掉进程时,应用程序绑定的 LISTEN 监听端口已经被立刻释放了 。这也是为什么在上面的 netstat 输出中,你再也找不到 [已脱敏IP]:8000 LISTEN 这一行。
然而,那些已经建立好的 TCP 连接,其生命周期并不会随着应用程序的死亡而立刻画上句号。
flowchart LR subgraph 进程存活时 A应用程序进程 -->|bind + listen| BLISTEN 监听端口 A -->|accept| CESTABLISHED 已建立连接 end subgraph kill -9 之后 D应用程序进程已死 -.->|立即释放| ELISTEN 监听端口消失 D -.->|内核接管 Socket 句柄| FESTABLISHED 连接转为 FIN_WAIT1 end B --> E C --> F
02 | 什么是 FIN_WAIT1?
全称 :Finish Wait 1(终止等待 1 状态),标准写法为 FIN_WAIT_1。
TCP 是一种面向连接的、可靠的传输层协议。为了保证数据传输的完整性,不管服务是正常退出还是崩溃被杀,TCP 必须通过"四次挥手"来优雅地关闭连接。
- 原理 :当一方(这里是你的宿主机内核)决定主动关闭连接时,会向对端发送一个 FIN 数据包,表示"我没有数据要继续发送了,请求断开连接"。
- 定义 :发出
FIN包之后、收到对方回应的 ACK 确认包之前,这个 TCP 连接在本地操作系统内部所处的状态就被称为FIN_WAIT1。
stateDiagram-v2 \* --> ESTABLISHED: 三次握手完成 ESTABLISHED --> FIN_WAIT1: 主动发送 FIN 包 FIN_WAIT1 --> FIN_WAIT2: 收到对端 ACK 确认 FIN_WAIT1 --> FIN_WAIT1: 未收到 ACK,指数退避重传 FIN_WAIT2 --> TIME_WAIT: 收到对端 FIN 包 TIME_WAIT --> \*: 等待 2MSL 后关闭
03 | 幕后主使:Linux 内核接管了断开流程
当进程被 kill -9 强制杀死时,发生了以下过程:
- 内核接管:应用程序死亡后,操作系统内核会自动接过该进程拥有的所有 TCP Socket 句柄。
- 发送 FIN 包 :内核主动向网络对端发送一个带有
FIN标志位的数据包,告诉对方:"我这边要关闭连接了"。 - 卡在
FIN_WAIT1的原因 :如果对端是公网上的恶意扫描器、发生了网络丢包、或者对端主机宕机,对方就不会正常回应ACK。Linux 内核会启动指数退避重传机制(如 netstat 中的on (0.15/1/0)),在重传超时之前,连接会一直处于FIN_WAIT1状态。
sequenceDiagram participant App as 应用程序 (PID) participant Kernel as Linux 内核 participant Client as 外部客户端 App->>Kernel: 进程被 kill -9 杀死 Kernel->>Kernel: 接管所有 TCP Socket 句柄 Kernel->>Client: 发送 FIN 包(主动关闭连接) alt 客户端正常响应 Client-->>Kernel: 回应 ACK 确认包 Kernel->>Kernel: 连接进入 FIN_WAIT2,继续完成四次挥手 else 客户端无响应(丢包/宕机/恶意扫描器) Client--xKernel: 无 ACK 回应 Kernel->>Kernel: 指数退避重传 FIN 包<br/>netstat 显示 on (0.15/1/0) Note over Kernel: 连接暂时卡在 FIN_WAIT1<br/>直到重传超时后强制销毁 end
04 | FIN_WAIT1 没消失,其他程序能用这个端口吗?
结论:在开启 SO_REUSEADDR 的情况下,通常可以正常绑定,不会报错!
很多人的误区在于把"连接状态"和"监听端口"混为一谈了,担心启动新程序会报 Address already in use(地址已被占用)的错误。实际上不会,原因如下:
- 端口"监听权"已经释放 :报"端口被占用"错误的常见原因是有另一个进程正在
LISTEN状态下占用该端口;此外,若未开启SO_REUSEADDR,处于TIME_WAIT等状态的残余连接也可能导致绑定失败。杀掉原进程后,LISTEN句柄已被立即释放,残留的FIN_WAIT1只是连接在做收尾清理,并不占据端口的"监听权"。 SO_REUSEADDR机制保障 :多数现代 Web 框架(如 FastAPI、Uvicorn、Spring Boot、Nginx 等)在创建端口监听时,默认会开启系统级的SO_REUSEADDR选项,但具体行为需以所用框架和版本为准。这个选项告诉操作系统:"只要没有其他进程在 LISTEN,即便该端口上还有处于挥手/清理状态(如FIN_WAIT1、TIME_WAIT)的残余连接,也允许新程序直接绑定并监听该端口。"
flowchart TD A新程序尝试绑定端口 8000 --> B{是否有进程在 LISTEN?} B -->|是| C报错 Address already in use B -->|否| D{端口上是否存在残余连接?<br/>FIN_WAIT1 / TIME_WAIT} D -->|是, 但已开启 SO_REUSEADDR| E允许绑定,新程序正常启动 D -->|否| F允许绑定,新程序正常启动 C --> G需要先处理占用端口的进程 E --> H✅ 新服务正常运行 F --> H
05 | 总结
| 疑问 / 现象 | 幕后真相 |
|---|---|
为什么 PID 变成了 -? |
进程已死,连接当前由 Linux 内核接管维护。 |
FIN_WAIT1 是什么? |
Finish Wait 1(标准写法 FIN_WAIT_1),内核已发出断开请求(FIN),正在等待对端回应 ACK 的中间状态。 |
| 会阻塞新程序启动吗? | 通常不会。 LISTEN 状态已释放,在开启 SO_REUSEADDR 的情况下,新程序可以立刻绑定该端口。 |
| 需要手动处理吗? | 通常不需要。内核重传超时后会自动强制销毁,一般不会永久占用资源。 |
flowchart LR subgraph 现象 Akill -9 杀掉进程 --> Bnetstat 仍显示端口连接 B --> CPID 显示为 - B --> D状态为 FIN_WAIT1 end subgraph 原因 ELISTEN 监听端口已立即释放 --> F新程序可正常绑定端口 G内核接管已建立的 TCP 连接 --> H主动发送 FIN 包等待 ACK H --> I对端无响应时卡在 FIN_WAIT1 end subgraph 结果 J不会报 Address already in use K内核重传超时后自动清理 L无需手动干预 end A --> E A --> G B --> I F --> J I --> K K --> L
下次再遇到杀掉进程后 netstat 里残留 FIN_WAIT1 连接的情况,不用惊慌,直接启动你的新服务即可,这正是 Linux 内核网络协议栈在默默履行其可靠传输职责的表现。
关注我,和AI一起成长~