应用调用 sendall() 返回时,数据可能仍在本机发送队列中;抓包显示一个大于 MTU 的 TCP 对象时,线上也未必出现过同样大小的帧;监控里的网卡计数增长,则不一定只来自当前进程。三种观察都是真的,但它们观察的是 Linux 网络路径上的不同位置。

第 03 篇已经说明抓包点决定证据边界,第 19 篇区分了发送端、接收端与路径上的速度限制,第 29 篇讨论了应用怎样等待 socket 就绪。本篇把这些观察点放回一台 Linux 主机:应用通过 socket 提交或接收字节,内核协议栈维护连接和队列,设备收发路径可能经过 NAPI、软中断与卸载。每个观察点只能证明其附近发生了什么。

socket 是用户态与协议栈之间的接口

Linux socket(7) 把 socket 描述为用户进程与内核协议栈之间的统一接口。文件描述符只是应用持有的引用;TCP 连接状态、发送和接收缓冲、重传及拥塞控制由内核维护。一次系统调用返回,不等于整条端到端路径完成。

以一次 TCP 发送为例,可以分出四个不同事件:

  1. send() 或 sendall() 把字节交给本机内核,或者因错误而失败;
  2. 本机 TCP 按序号发送并依据确认推进发送状态;
  3. 对端内核把字节放入接收队列;
  4. 对端应用调用 recv() 取走字节并完成业务处理。

RFC 9293 定义的是 TCP 状态机、序号和确认语义,不承诺一次用户态发送调用等价于对端应用处理成功。sendall() 解决的是 Python 调用是否把全部参数字节交给本机 socket;若业务需要知道对端已经处理,仍要设计应用层响应。

接收方向也有相同的分层。设备或虚拟接口向内核交付数据后,IP 与 TCP 处理把属于该连接的有效字节排进 socket 接收队列。socket 变为可读,只说明一次读取可能取得字节、EOF 或错误。应用仍要处理短读、消息边界与半关闭。

模式提炼:把“完成”绑定到观察点

完成 = 观察点 + 已发生事件 + 尚未证明的下游事件

这个模式也适用于磁盘写入、消息队列和数据库复制。API 返回、内核接受、远端确认与业务生效是不同里程碑;缺少应用层证据时,不能把较早的里程碑改写成较晚的完成。

一台主机里不只有一条队列

“网络队列”至少可能指 socket 缓冲、TCP 内部待发送数据、流量控制队列、设备发送环、设备接收环或 NAPI poll 中等待处理的工作。它们由不同组件维护,也用不同工具观察。把任意一个队列长度叫作“网卡拥塞”会跳过关键因果链。

/proc/net/tcp 能展示当前 TCP socket 的状态及发送、接收队列快照。Linux 文档同时说明该文本接口已经弃用,推荐使用 tcp_diag。它仍适合本篇的无依赖教学实验,但字段属于 Linux 观测接口,不是 TCP 协议的一部分,也不应成为跨平台程序接口。

发送队列非零可能表示字节尚未从本机 TCP 的发送状态中移除;接收队列非零表示应用还有可读数据。快照为零也不能证明“从来没有排队”,因为读取 /proc 时队列可能已经被消费。反过来,队列瞬时非零也不能直接证明持续拥塞,必须结合时间序列、确认、重传和路径指标。

接口统计又处在不同层次。Linux 提供 netlink、/proc/net/dev 和 sysfs 等入口,其中 rtnetlink 是读取标准接口统计的首选接口。/proc/net/dev 是历史文本接口,会合并部分字段。本篇为了不新增依赖,只读取 loopback 的字节和包计数;这些计数属于整台主机及该接口,不属于某个 PID。

观察对象 本篇入口 能说明什么 不能单独说明什么
应用调用 Python 返回值与应用响应 调用是否返回、对端应用是否回了本实验响应 物理帧形态、生产链路时延
TCP socket /proc/net/tcp 采样时刻的连接状态和队列 队列完整历史、唯一根因
接口统计 /proc/net/dev 共享接口计数的前后变化 变化全部由本实验造成
协议统计 /proc/net/snmp 主机级 TCP 计数快照 某条连接的独占计数
软网络统计 /proc/net/softnet_stat 每 CPU 原始行及有限字段变化 物理 NIC 中断与驱动队列行为

接收路径为何会出现软中断与 NAPI

物理网卡接收数据时,设备通常通过中断通知主机,再由驱动安排 NAPI 实例处理事件。Linux NAPI 文档说明,NAPI 通常运行在软件中断上下文,也可以配置为专用内核线程。驱动的 poll 方法接收 budget,限制一次轮询处理的接收包数量;耗尽预算时,后续轮询还能继续处理剩余工作。

这套机制用于限制单次接收工作并分摊开销,但不能被简化为“每个包触发一次中断”或“软中断就是一个固定线程”。多队列网卡还可能使用 RSS 把不同流分到不同硬件接收队列和 CPU,RPS/RFS 又能在软件层调整处理 CPU。实际路径取决于设备、驱动、内核配置和运行时设置。

loopback 是一个重要反例。发往 127.0.0.1 的数据不经过外部物理网卡,因此可以验证 socket 与协议栈的本机行为,却不能验证 PCIe 设备中断、真实 RX/TX ring、RSS 分布或物理链路上的帧。看到 /proc/net/softnet_stat 有变化,也不能把它解释成某块物理网卡完成了收包。

模式提炼:先标注路径,再解释指标

指标解释 = 对象范围 × 采样位置 × 时间窗口 × 共享程度

同名指标在连接、接口、CPU 和主机层可能含义不同。故障分析应先写清路径是否经过物理设备、采样点位于发送前还是接收后、计数是否由其他流量共享,再讨论数值变化。

卸载为什么会改变抓包所见

Linux 内核可以把较大的 sk_buff(简称 skb,内核保存报文及其元数据的对象)交给后续分段逻辑。TSO 允许设备把一个待发送对象分成多个 TCP 段;GSO 在软件中执行相近的分段;GRO 则能在接收路径合并可聚合的数据。校验和卸载还允许内核先保存部分校验状态,由设备在发送前完成计算。

因此,抓包文件里的“包”不必等于线上帧。捕获点若位于发送分段或校验和完成之前,可能看到大于接口 MTU 的对象,或看到尚未填入最终值的 TCP/UDP 校验和。Wireshark 官方说明把这些现象列为卸载造成的常见抓包表象。它们首先提示检查捕获位置和 offload 配置,不能仅凭本机发送侧抓包断言线上出现巨帧或校验和损坏。

接收侧聚合也会改变观察单位。若捕获发生在 GRO 合并之后,工具可能呈现一个由多个线上分段合并而来的较大对象。要判断线上实际帧,应在更接近介质的对端或独立观测点抓取,并记录两端 offload 状态。临时关闭 offload 会改变被测系统行为,只能在专用测试环境中操作。

本机对齐实验

同名素材目录中的 lab.py 只绑定 127.0.0.1。客户端连接临时服务端并发送 65536 字节;服务端先暂停读取,使数据有机会出现在接收队列中。脚本在应用读取前后对齐以下证据:

  • sendall() 是否已经返回;
  • 客户端和服务端四元组;
  • /proc/net/tcp 中两端的状态、tx_queue 与 rx_queue;
  • loopback 的 /proc/net/dev 计数;
  • 主机共享的 /proc/net/snmp 与 /proc/net/softnet_stat 快照;
  • 服务端实际收到的长度和 SHA-256,以及客户端收到的应用响应。

运行命令:

1
2
python3 source/_posts/2026-09-24-计算机网络30-数据如何经过Linux主机/lab.py --help
python3 source/_posts/2026-09-24-计算机网络30-数据如何经过Linux主机/lab.py

素材包括回环对齐脚本、本次运行输出、资料与实验记录和审阅记录。

实验要求服务端读取前能找到两端 ESTABLISHED 条目,并观察到服务端接收队列非零;随后服务端收到全部字节并返回 APP_OK。sendall() 先返回、应用响应后到达,直接显示了“本机接受发送”和“对端应用处理”不是同一事件。

接口与协议计数只报告前后差值,不把差值强行等同于这条连接。实验期间系统里的其他流量也可能更新同一计数器。字节差值还包含协议开销、确认以及 loopback 双向记账,不能拿应用负载字节直接反推物理帧数量。

条件、限制和反例

socket 发送队列增长不必然来自网络拥塞。对端暂不读取、接收窗口受限、本机调度延迟或发送程序突发写入都可能形成队列。只有把 socket 状态、确认进展、重传、接口队列和对端证据放在同一时间线上,才可能缩小原因范围。

软中断计数较高也不自动等于故障。流量规模、包大小、CPU 亲和性、合并策略和采样区间都会影响它。/proc/net/softnet_stat 的文本格式还是内核实现接口;本篇只保留原始行数和前三列汇总,不为所有内核版本赋予统一语义。

卸载不是“抓包错误”的唯一原因。截断长度、捕获丢包、隧道封装、镜像位置和解析器配置都可能产生异常表象。对比两端抓包、接口 MTU、offload 状态与实际线上观测,才能区分这些解释。

构建成功或页面返回 HTTP 200 只证明 Hexo 能生成并读取页面。它不能证明 TCP 传输、NAPI 调度、物理网卡卸载或端到端性能。

练习

练习一:某客户端的 sendall() 已返回,/proc/net/tcp 显示客户端 tx_queue 非零,服务端应用尚无日志。列出已经成立和仍未成立的事实。若要证明服务端业务已处理,应补哪一层证据?

练习二:发送端抓包显示一个 32 KiB 的 TCP 对象,而出口 MTU 为 1500。设计一个不接触生产系统的验证方案,至少包含捕获位置、TSO/GSO/GRO 与校验和 offload 状态,以及对端观测。说明为什么不能直接把 32 KiB 对象称为线上帧。

模式速查表

现象 先定位的观察点 需要补的证据
sendall() 返回但业务无结果 本机 socket 与应用协议 对端接收及应用响应
socket 队列持续增长 连接级发送/接收状态 确认、窗口、重传与对端读取
接口计数增长 接口和采样窗口 同期其他流量、方向与协议开销
软中断负载集中 CPU、队列与 NAPI 配置 RSS/RPS/RFS、驱动及时间序列
抓到超 MTU 对象或坏校验和 抓包点与卸载阶段 对端抓包和 offload 配置

官方参考资料

验证边界

已验证:Python 3 标准库在 Linux loopback 上建立一条自建 TCP 连接,对齐 sendall() 返回、两端 socket 四元组、/proc/net/tcp 队列快照、服务端完整读取、应用响应及主机共享计数器的前后变化。这是 LOCAL_INTEGRATION,不代表公网或生产结果。

NOT_RUN:没有使用物理 NIC、抓包权限、ethtool 管理权限或专用网络命名空间,因此没有验证真实中断、设备 RX/TX ring、RSS/RPS/RFS、TSO/GSO/GRO、校验和卸载、线上分段、丢包、吞吐和延迟。未来补证应在自建命名空间或专用测试主机中记录两端抓包、offload 配置、接口与 CPU 队列,再做受控对照;不得修改宿主默认路由或生产系统。