代码改变世界

进程都杀了,为什么 `netstat` 还能看到端口?

2026-09-09 07:09  AlfredZhao  阅读(211)  评论(0)    收藏  举报

在运维和开发过程中,你可能也遇到过类似的“诡异”现象:某个服务占用端口出了问题,你找到它的 PID,执行了 kill -9 <PID> 强制杀掉进程。但当再次用 netstatss 检查时,却发现该端口依然留在列表中:

$ 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 连接

在解答这个问题之前,首先需要厘清操作系统中的两种网络状态:

  1. 监听端口(LISTEN):应用程序通过绑定并监听某个端口,随时准备接收新的连接请求。
  2. 已建立的连接(ESTABLISHED):客户端与服务端完成“三次握手”后建立的通信通道。

当你执行 kill -9 杀掉进程时,应用程序绑定的 LISTEN 监听端口已经被立刻释放了。这也是为什么在上面的 netstat 输出中,你再也找不到 [已脱敏IP]:8000 LISTEN 这一行。

然而,那些已经建立好的 TCP 连接,其生命周期并不会随着应用程序的死亡而立刻画上句号。

flowchart LR subgraph 进程存活时 A[应用程序进程] -->|bind + listen| B[LISTEN 监听端口] A -->|accept| C[ESTABLISHED 已建立连接] end subgraph kill -9 之后 D[应用程序进程已死] -.->|立即释放| E[LISTEN 监听端口消失] D -.->|内核接管 Socket 句柄| F[ESTABLISHED 连接转为 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 强制杀死时,发生了以下过程:

  1. 内核接管:应用程序死亡后,操作系统内核会自动接过该进程拥有的所有 TCP Socket 句柄。
  2. 发送 FIN 包:内核主动向网络对端发送一个带有 FIN 标志位的数据包,告诉对方:“我这边要关闭连接了”。
  3. 卡在 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(地址已被占用)的错误。实际上不会,原因如下:

  1. 端口“监听权”已经释放:报“端口被占用”错误的常见原因是有另一个进程正在 LISTEN 状态下占用该端口;此外,若未开启 SO_REUSEADDR,处于 TIME_WAIT 等状态的残余连接也可能导致绑定失败。杀掉原进程后,LISTEN 句柄已被立即释放,残留的 FIN_WAIT1 只是连接在做收尾清理,并不占据端口的“监听权”。
  2. SO_REUSEADDR 机制保障:多数现代 Web 框架(如 FastAPI、Uvicorn、Spring Boot、Nginx 等)在创建端口监听时,默认会开启系统级的 SO_REUSEADDR 选项,但具体行为需以所用框架和版本为准。这个选项告诉操作系统:“只要没有其他进程在 LISTEN,即便该端口上还有处于挥手/清理状态(如 FIN_WAIT1TIME_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 现象 A[kill -9 杀掉进程] --> B[netstat 仍显示端口连接] B --> C[PID 显示为 -] B --> D[状态为 FIN_WAIT1] end subgraph 原因 E[LISTEN 监听端口已立即释放] --> 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一起成长~